blob: fb381114c060595ad0d4ba544d6e2f3d55c21cd9 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2022 The Fuchsia Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import enum
import logging
import time
from enum import IntEnum, unique
from typing import Protocol
from antlion import tracelogger
from antlion.validation import MapValidator
MOBLY_CONTROLLER_CONFIG_NAME = "PduDevice"
ACTS_CONTROLLER_REFERENCE_NAME = "pdu_devices"
# Allow time for capacitors to discharge.
DEFAULT_REBOOT_DELAY_SEC = 5.0
class PduType(enum.StrEnum):
NP02B = "synaccess.np02b"
WEBPOWERSWITCH = "digital_loggers.webpowerswitch"
class PduError(Exception):
"""An exception for use within PduDevice implementations"""
def create(configs: list[dict[str, object]]) -> list[PduDevice]:
"""Creates a PduDevice for each config in configs.
Args:
configs: List of configs from PduDevice field.
Fields:
device: a string "<brand>.<model>" that corresponds to module
in pdu_lib/
host: a string of the device ip address
username (optional): a string of the username for device sign-in
password (optional): a string of the password for device sign-in
Return:
A list of PduDevice objects.
"""
pdus: list[PduDevice] = []
for config in configs:
c = MapValidator(config)
device = c.get(str, "device")
pduType = PduType(device)
host = c.get(str, "host")
username = c.get(str, "username", None)
password = c.get(str, "password", None)
match pduType:
case PduType.NP02B:
from antlion.controllers.pdu_lib.synaccess.np02b import (
PduDevice as NP02B,
)
pdus.append(NP02B(host, username, password))
case PduType.WEBPOWERSWITCH:
from antlion.controllers.pdu_lib.digital_loggers.webpowerswitch import (
PduDevice as WebPowerSwitch,
)
pdus.append(WebPowerSwitch(host, username, password))
return pdus
def destroy(pdu_list: list[PduDevice]) -> None:
"""Ensure any connections to devices are closed.
Args:
pdu_list: A list of PduDevice objects.
"""
for pdu in pdu_list:
pdu.close()
def get_info(pdu_list: list[PduDevice]) -> list[dict[str, str | None]]:
"""Retrieves info from a list of PduDevice objects.
Args:
pdu_list: A list of PduDevice objects.
Return:
A list containing a dictionary for each PduDevice, with keys:
'host': a string of the device ip address
'username': a string of the username
'password': a string of the password
"""
info = []
for pdu in pdu_list:
info.append(
{"host": pdu.host, "username": pdu.username, "password": pdu.password}
)
return info
def get_pdu_port_for_device(
device_pdu_config: dict[str, object], pdus: list[PduDevice]
) -> tuple[PduDevice, int]:
"""Retrieves the pdu object and port of that PDU powering a given device.
This is especially necessary when there are multilpe devices on a single PDU
or multiple PDUs registered.
Args:
device_pdu_config: a dict, representing the config of the device.
pdus: a list of registered PduDevice objects.
Returns:
A tuple: (PduObject for the device, string port number on that PDU).
Raises:
ValueError, if there is no PDU matching the given host in the config.
Example ACTS config:
...
"testbed": [
...
"FuchsiaDevice": [
{
"ip": "<device_ip>",
"ssh_config": "/path/to/sshconfig",
"PduDevice": {
"host": "192.168.42.185",
"port": 2
}
}
],
"AccessPoint": [
{
"ssh_config": {
...
},
"PduDevice": {
"host": "192.168.42.185",
"port" 1
}
}
],
"PduDevice": [
{
"device": "synaccess.np02b",
"host": "192.168.42.185"
}
]
],
...
"""
config = MapValidator(device_pdu_config)
pdu_ip = config.get(str, "host")
port = config.get(int, "port")
for pdu in pdus:
if pdu.host == pdu_ip:
return pdu, port
raise ValueError(f"No PduDevice with host: {pdu_ip}")
class PDU(Protocol):
"""Control power delivery to a device with a PDU."""
def port(self, index: int) -> Port:
"""Access a single port.
Args:
index: Index of the port, likely the number identifier above the outlet.
Returns:
Controller for the specified port.
"""
...
def __len__(self) -> int:
"""Count the number of ports.
Returns:
Number of ports on this PDU.
"""
...
class Port(Protocol):
"""Controlling the power delivery to a single port of a PDU."""
def status(self) -> PowerState:
"""Return the power state for this port.
Returns:
Power state
"""
...
def set(self, state: PowerState) -> None:
"""Set the power state for this port.
Args:
state: Desired power state
"""
...
def reboot(self, delay_sec: float = DEFAULT_REBOOT_DELAY_SEC) -> None:
"""Set the power state OFF then ON after a delay.
Args:
delay_sec: Length to wait before turning back ON. This is important to allow
the device's capacitors to discharge.
"""
self.set(PowerState.OFF)
time.sleep(delay_sec)
self.set(PowerState.ON)
@unique
class PowerState(IntEnum):
OFF = 0
ON = 1
class PduDevice(object):
"""An object that defines the basic Pdu functionality and abstracts
the actual hardware.
This is a pure abstract class. Implementations should be of the same
class name (eg. class PduDevice(pdu.PduDevice)) and exist in
pdu_lib/<brand>/<device_name>.py. PduDevice objects should not be
instantiated by users directly.
TODO(http://b/318877544): Replace PduDevice with PDU
"""
def __init__(self, host: str, username: str | None, password: str | None) -> None:
if type(self) is PduDevice:
raise NotImplementedError("Base class: cannot be instantiated directly")
self.host = host
self.username = username
self.password = password
self.log = tracelogger.TraceLogger(logging.getLogger())
def on_all(self) -> None:
"""Turns on all outlets on the device."""
raise NotImplementedError("Base class: cannot be called directly")
def off_all(self) -> None:
"""Turns off all outlets on the device."""
raise NotImplementedError("Base class: cannot be called directly")
def on(self, outlet: int) -> None:
"""Turns on specific outlet on the device.
Args:
outlet: index of the outlet to turn on.
"""
raise NotImplementedError("Base class: cannot be called directly")
def off(self, outlet: int) -> None:
"""Turns off specific outlet on the device.
Args:
outlet: index of the outlet to turn off.
"""
raise NotImplementedError("Base class: cannot be called directly")
def reboot(self, outlet: int) -> None:
"""Toggles a specific outlet on the device to off, then to on.
Args:
outlet: index of the outlet to reboot.
"""
raise NotImplementedError("Base class: cannot be called directly")
def status(self) -> dict[str, bool]:
"""Retrieves the status of the outlets on the device.
Return:
A dictionary matching outlet string to:
True: if outlet is On
False: if outlet is Off
"""
raise NotImplementedError("Base class: cannot be called directly")
def close(self) -> None:
"""Closes connection to the device."""
raise NotImplementedError("Base class: cannot be called directly")