1#!/usr/bin/env python3
2#
3#   Copyright 2019 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import importlib
18import logging
19
20from acts import tracelogger
21
22MOBLY_CONTROLLER_CONFIG_NAME = 'PduDevice'
23ACTS_CONTROLLER_REFERENCE_NAME = 'pdu_devices'
24
25
26def create(configs):
27    """Creates a PduDevice for each config in configs.
28
29    Args:
30        configs: List of configs from PduDevice field.
31            Fields:
32                device: a string "<brand>.<model>" that corresponds to module
33                    in pdu_lib/
34                host: a string of the device ip address
35                username (optional): a string of the username for device sign-in
36                password (optional): a string of the password for device sign-in
37    Return:
38        A list of PduDevice objects.
39    """
40    if configs:
41        pdus = []
42        for config in configs:
43            device = config.get('device')
44            if not device:
45                raise PduError("Config must provide a device")
46
47            host = config.get('host')
48            if not device:
49                raise PduError("Config must provide a host ip address")
50            username = config.get('username')
51            password = config.get('password')
52            pdu = _create_device(device, host, username, password)
53            pdus.append(pdu)
54        return pdus
55
56
57def destroy(pdu_list):
58    """Ensure any connections to devices are closed.
59
60    Args:
61        pdu_list: A list of PduDevice objects.
62    """
63    for pdu in pdu_list:
64        pdu.close()
65
66
67def get_info(pdu_list):
68    """Retrieves info from a list of PduDevice objects.
69
70    Args:
71        pdu_list: A list of PduDevice objects.
72    Return:
73        A list containing a dictionary for each PduDevice, with keys:
74            'host': a string of the device ip address
75            'username': a string of the username
76            'password': a string of the password
77    """
78    info = []
79    for pdu in pdu_list:
80        info.append({
81            'host': pdu.host,
82            'username': pdu.username,
83            'password': pdu.password
84        })
85    return info
86
87
88def _create_device(device, host, username, password):
89    """Factory method that returns an instance of PduDevice implementation
90    based on the device string.
91    """
92    module_name = 'acts.controllers.pdu_lib.' + device
93    module = importlib.import_module(module_name)
94    return module.PduDevice(host, username, password)
95
96
97def get_pdu_port_for_device(device_pdu_config, pdus):
98    """Retrieves the pdu object and port of that PDU powering a given device.
99    This is especially necessary when there are multilpe devices on a single PDU
100    or multiple PDUs registered.
101
102    Args:
103        device_pdu_config: a dict, representing the config of the device.
104        pdus: a list of registered PduDevice objects.
105
106    Returns:
107        A tuple: (PduObject for the device, string port number on that PDU).
108
109    Raises:
110        ValueError, if there is no PDU matching the given host in the config.
111
112    Example ACTS config:
113        ...
114        "testbed": [
115            ...
116            "FuchsiaDevice": [
117                {
118                    "ip": "<device_ip>",
119                    "ssh_config": "/path/to/sshconfig",
120                    "PduDevice": {
121                        "host": "192.168.42.185",
122                        "port": 2
123                    }
124                }
125            ],
126            "AccessPoint": [
127                {
128                    "ssh_config": {
129                        ...
130                    },
131                    "PduDevice": {
132                        "host": "192.168.42.185",
133                        "port" 1
134                    }
135                }
136            ],
137            "PduDevice": [
138                {
139                    "device": "synaccess.np02b",
140                    "host": "192.168.42.185"
141                }
142            ]
143        ],
144        ...
145    """
146    pdu_ip = device_pdu_config['host']
147    port = device_pdu_config['port']
148    for pdu in pdus:
149        if pdu.host == pdu_ip:
150            return pdu, port
151    raise ValueError('No PduDevice with host: %s' % pdu_ip)
152
153
154class PduDevice(object):
155    """An object that defines the basic Pdu functionality and abstracts
156    the actual hardware.
157
158    This is a pure abstract class. Implementations should be of the same
159    class name (eg. class PduDevice(pdu.PduDevice)) and exist in
160    pdu_lib/<brand>/<device_name>.py. PduDevice objects should not be
161    instantiated by users directly.
162    """
163    def __init__(self, host, username, password):
164        if type(self) is PduDevice:
165            raise NotImplementedError(
166                "Base class: cannot be instantiated directly")
167        self.host = host
168        self.username = username
169        self.password = password
170        self.log = tracelogger.TraceLogger(logging.getLogger())
171
172    def on_all(self):
173        """Turns on all outlets on the device."""
174        raise NotImplementedError("Base class: cannot be called directly")
175
176    def off_all(self):
177        """Turns off all outlets on the device."""
178        raise NotImplementedError("Base class: cannot be called directly")
179
180    def on(self, outlet):
181        """Turns on specific outlet on the device.
182        Args:
183            outlet: a string of the outlet to turn on.
184        """
185        raise NotImplementedError("Base class: cannot be called directly")
186
187    def off(self, outlet):
188        """Turns off specific outlet on the device.
189        Args:
190            outlet: a string of the outlet to turn off.
191        """
192        raise NotImplementedError("Base class: cannot be called directly")
193
194    def reboot(self, outlet):
195        """Toggles a specific outlet on the device to off, then to on.
196        Args:
197            outlet: a string of the outlet to reboot.
198        """
199        raise NotImplementedError("Base class: cannot be called directly")
200
201    def status(self):
202        """Retrieves the status of the outlets on the device.
203
204        Return:
205            A dictionary matching outlet string to:
206                True: if outlet is On
207                False: if outlet is Off
208        """
209        raise NotImplementedError("Base class: cannot be called directly")
210
211    def close(self):
212        """Closes connection to the device."""
213        raise NotImplementedError("Base class: cannot be called directly")
214
215
216class PduError(Exception):
217    """An exception for use within PduDevice implementations"""
218    pass