Skip to content

Commit

Permalink
Added ServerTech Handler. Added retries for each request
Browse files Browse the repository at this point in the history
  • Loading branch information
alexquali committed Jul 9, 2024
1 parent 6414df7 commit bbd6fdc
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 101 deletions.
4 changes: 4 additions & 0 deletions dev_requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
pre-commit
tox
tox-factor
-r test_requirements.txt
-r src/requirements.txt
51 changes: 22 additions & 29 deletions src/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,9 @@
from cloudshell.shell.standards.pdu.driver_interface import PDUResourceDriverInterface
from cloudshell.shell.standards.pdu.resource_config import RESTAPIPDUResourceConfig

from server_tech.flows.server_tech_autoload_flow import (
ServerTechAutoloadFlow
)
from server_tech.flows.server_tech_state_flow import (
ServerTechOutletsStateFlow
)
from server_tech.flows.server_tech_autoload_flow import ServerTechAutoloadFlow
from server_tech.flows.server_tech_state_flow import ServerTechOutletsStateFlow
from server_tech.handlers.server_tech_handler import ServerTechHandler


class ServerTechnologyShellDriver(ResourceDriverInterface, PDUResourceDriverInterface):
Expand All @@ -30,8 +27,6 @@ def __init__(self):
self._cli = None

def initialize(self, context: InitCommandContext) -> str:
# api = CloudShellSessionContext(context).get_api()
# resource_config = RESTAPIPDUResourceConfig.from_context(context, api)
return "Finished initializing"

@GlobalLock.lock
Expand All @@ -46,37 +41,38 @@ def get_inventory(self, context: AutoLoadCommandContext) -> AutoLoadDetails:
resource.add_sub_resource('1', p1)
return resource.create_autoload_details()
"""

with LoggingSessionContext(context) as logger:
api = CloudShellSessionContext(context).get_api()
resource_config = RESTAPIPDUResourceConfig.from_context(context, api)

resource_model = PDUResourceModel.from_resource_config(resource_config)
autoload_operations = ServerTechAutoloadFlow(config=resource_config)
logger.info("Autoload started")
response = autoload_operations.discover(self.SUPPORTED_OS, resource_model)
logger.info("Autoload completed")
return response

with ServerTechHandler.from_config(resource_config) as si:
autoload_operations = ServerTechAutoloadFlow(si=si)
logger.info("Autoload started")
response = autoload_operations.discover(
self.SUPPORTED_OS, resource_model
)
logger.info("Autoload completed")
return response

def _change_power_state(
self,
context: ResourceCommandContext,
ports: list[str],
state: str
self, context: ResourceCommandContext, ports: list[str], state: str
) -> None: # noqa E501
"""Set power outlets state based on provided data."""
with LoggingSessionContext(context) as logger:
api = CloudShellSessionContext(context).get_api()

resource_config = RESTAPIPDUResourceConfig.from_context(context, api)

outlets_operations = ServerTechOutletsStateFlow(config=resource_config)
logger.info(f"Power {state.capitalize()} operation started")
outlets_operations.set_outlets_state(
ports=ports,
state=state,
)
logger.info(f"Power {state.capitalize()} operation completed")
with ServerTechHandler.from_config(resource_config) as si:
outlets_operations = ServerTechOutletsStateFlow(si=si)
logger.info(f"Power {state.capitalize()} operation started")
outlets_operations.set_outlets_state(
ports=ports,
state=state,
)
logger.info(f"Power {state.capitalize()} operation completed")

def PowerOn(self, context: ResourceCommandContext, ports: list[str]) -> None:
"""Set power state as ON to provided outlets."""
Expand All @@ -87,10 +83,7 @@ def PowerOff(self, context: ResourceCommandContext, ports: list[str]) -> None:
self._change_power_state(context=context, ports=ports, state="off")

def PowerCycle(
self,
context: ResourceCommandContext,
ports: list[str],
delay: str
self, context: ResourceCommandContext, ports: list[str], delay: str
) -> None: # noqa E501
"""Set power state as CYCLE to provided outlets."""
self._change_power_state(context=context, ports=ports, state="reboot")
Expand Down
24 changes: 7 additions & 17 deletions src/server_tech/flows/server_tech_autoload_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,33 @@
from typing import TYPE_CHECKING

from cloudshell.shell.flows.autoload.basic_flow import AbstractAutoloadFlow
from server_tech.handlers.rest_api_handler import ServerTechAPI

if TYPE_CHECKING:
from cloudshell.shell.core.driver_context import AutoLoadDetails
from cloudshell.shell.standards.pdu.resource_config import RESTAPIPDUResourceConfig
from cloudshell.shell.standards.pdu.autoload_model import PDUResourceModel

from server_tech.handlers.server_tech_handler import ServerTechHandler


logger = logging.getLogger(__name__)


class ServerTechAutoloadFlow(AbstractAutoloadFlow):
"""Autoload flow."""

def __init__(self, config: RESTAPIPDUResourceConfig):
def __init__(self, si: ServerTechHandler):
super().__init__()
self.config = config
self._si = si

def _autoload_flow(
self,
supported_os: list[str],
resource_model: PDUResourceModel
self, supported_os: list[str], resource_model: PDUResourceModel
) -> AutoLoadDetails:
"""Autoload Flow."""
logger.info("*" * 70)
logger.info("Start discovery process .....")

api = ServerTechAPI(
address=self.config.address,
username=self.config.api_user,
password=self.config.api_password,
port=self.config.api_port or None,
scheme=self.config.api_scheme or None,
)

outlets_info = api.get_outlets()
pdu_info = api.get_pdu_info()
outlets_info = self._si.get_outlets_info()
pdu_info = self._si.get_pdu_info()

resource_model.vendor = "Server Technology"
resource_model.model = pdu_info.get("model", "")
Expand Down
18 changes: 5 additions & 13 deletions src/server_tech/flows/server_tech_state_flow.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
from __future__ import annotations

from attrs import define
from typing import TYPE_CHECKING

from server_tech.handlers.rest_api_handler import ServerTechAPI
from attrs import define

from server_tech.helpers.errors import NotSupportedServerTechError

if TYPE_CHECKING:
from cloudshell.shell.standards.pdu.resource_config import RESTAPIPDUResourceConfig
from server_tech.handlers.server_tech_handler import ServerTechHandler


@define
class ServerTechOutletsStateFlow:
config: RESTAPIPDUResourceConfig
_si: ServerTechHandler

AVAILABLE_STATES = ["on", "off", "reboot"]

Expand All @@ -33,13 +33,5 @@ def set_outlets_state(self, ports: list[str], state: str) -> None:

outlets = ServerTechOutletsStateFlow._ports_to_outlet_ids(ports=ports)

api = ServerTechAPI(
address=self.config.address,
username=self.config.api_user,
password=self.config.api_password,
port=self.config.api_port or None,
scheme=self.config.api_scheme or None,
)

for outlet_id in outlets:
api.set_outlet_state(outlet_id=outlet_id, outlet_state=state)
self._si.set_outlet_state(outlet_id=outlet_id, outlet_state=state)
108 changes: 66 additions & 42 deletions src/server_tech/handlers/rest_api_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

import logging
import ssl
import time
from abc import abstractmethod
from collections.abc import Callable

from attrs import define, field
from attrs.setters import frozen

import requests
import urllib3

from attrs import define, field
from attrs.setters import frozen

from server_tech.helpers.errors import (
BaseServerTechError,
RESTAPIServerTechError,
RESTAPIUnavailableServerTechError,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -127,76 +127,100 @@ def _do_delete(
@define
class ServerTechAPI(BaseAPIClient):
BASE_ERRORS = {
404: RESTAPIServerTechError,
404: RESTAPIUnavailableServerTechError,
405: RESTAPIServerTechError,
503: RESTAPIServerTechError,
}
"""
404 NOT FOUND Requested resource does not exist or is unavailable
405 METHOD NOT ALLOWED Requested method was not permitted
503 SERVICE UNAVAILABLE The server is too busy to send the resource or resource collection
503 SERVICE UNAVAILABLE The server is too busy to send the resource or resource collection # noqa E501
"""

class Decorators:
@classmethod
def get_data(
cls, retries: int = 6, timeout: int = 5, raise_on_timeout: bool = True
):
def wrapper(decorated):
def inner(*args, **kwargs):
exception = None
attempt = 0
while attempt < retries:
try:
response = decorated(*args, **kwargs)
if response:
return response.json()
else:
return response
except RESTAPIUnavailableServerTechError as e:
exception = e
time.sleep(timeout)
attempt += 1

if raise_on_timeout:
if exception:
raise exception
else:
raise RESTAPIServerTechError(
f"Cannot execute request for {retries * timeout} sec."
)

return inner

return wrapper

def _base_url(self):
# return f"{self.scheme}://{self.address}/jaws"
return f"{self.scheme}://{self.address}:{self.port}/jaws"

def get_pdu_info(self) -> dict[str, str]:
"""Get information about outlets."""
pdu_info = {}
@Decorators.get_data()
def get_pdu_units_info(self) -> requests.Response:
"""Get information about PDU units."""
error_map = {}

units_data = self._do_get(
path=f"config/info/units",
http_error_map={**self.BASE_ERRORS, **error_map}
).json()

for unit in units_data:
pdu_info.update(
{
"model": unit.get("model_number", ""),
"serial": unit.get("product_serial_number", ""),
}
)
path="config/info/units", http_error_map={**self.BASE_ERRORS, **error_map}
)

return units_data

@Decorators.get_data()
def get_pdu_system_info(self) -> requests.Response:
"""Get basic information about PDU."""
error_map = {}

system_data = self._do_get(
path=f"config/info/system",
http_error_map={**self.BASE_ERRORS, **error_map}
).json()
pdu_info.update({"fw": system_data.get("firmware", "")})
return pdu_info
path="config/info/system", http_error_map={**self.BASE_ERRORS, **error_map}
)

return system_data

def get_outlets(self) -> dict[str, str]:
@Decorators.get_data()
def get_outlets(self) -> requests.Response:
"""Get information about outlets."""
error_map = {}
outlets_info = {}

response = self._do_get(
path=f"control/outlets",
http_error_map={**self.BASE_ERRORS, **error_map}
outlets_info = self._do_get(
path="control/outlets", http_error_map={**self.BASE_ERRORS, **error_map}
)
for data in response.json():
outlets_info.update({data["id"]: data["control_state"]})

return outlets_info

def set_outlet_state(self, outlet_id: str, outlet_state: str) -> None:
@Decorators.get_data()
def set_outlet_state(self, outlet_id: str, outlet_state: str) -> requests.Response:
"""Set outlet state.
Possible outlet states could be on/off/reboot.
"""
error_map = {
400: RESTAPIServerTechError,
409: RESTAPIServerTechError
}
error_map = {400: RESTAPIServerTechError, 409: RESTAPIServerTechError}
"""
400 BAD REQUEST Malformed patch document; a required patch object member is missing OR an unsupported operation was included.
400 BAD REQUEST Malformed patch document; a required patch object member is missing OR an unsupported operation was included. # noqa E501
409 CONFLICT Property specified for updating does not exist in resource
"""
self._do_patch(
return self._do_patch(
path=f"control/outlets/{outlet_id}",
json={"control_action": outlet_state},
http_error_map={**self.BASE_ERRORS, **error_map}
http_error_map={**self.BASE_ERRORS, **error_map},
)


Expand All @@ -217,7 +241,7 @@ def set_outlet_state(self, outlet_id: str, outlet_state: str) -> None:
POST
201 CREATED Resource created successfully.
400 BAD REQUEST Message contained either bad values (e.g. out of range) for properties, or non-existent properties
400 BAD REQUEST Message contained either bad values (e.g. out of range) for properties, or non-existent properties # noqa E501
404 NOT FOUND Requested resource collection does not exist or is unavailable
405 METHOD NOT ALLOWED Requested method was not permitted
409 CONFLICT Requested resource already exists
Expand Down
Loading

0 comments on commit bbd6fdc

Please sign in to comment.