Skip to content

Commit bbd6fdc

Browse files
author
alexquali
committed
Added ServerTech Handler. Added retries for each request
1 parent 6414df7 commit bbd6fdc

7 files changed

Lines changed: 177 additions & 101 deletions

File tree

dev_requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1+
pre-commit
2+
tox
3+
tox-factor
4+
-r test_requirements.txt
15
-r src/requirements.txt

src/driver.py

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,9 @@
1414
from cloudshell.shell.standards.pdu.driver_interface import PDUResourceDriverInterface
1515
from cloudshell.shell.standards.pdu.resource_config import RESTAPIPDUResourceConfig
1616

17-
from server_tech.flows.server_tech_autoload_flow import (
18-
ServerTechAutoloadFlow
19-
)
20-
from server_tech.flows.server_tech_state_flow import (
21-
ServerTechOutletsStateFlow
22-
)
17+
from server_tech.flows.server_tech_autoload_flow import ServerTechAutoloadFlow
18+
from server_tech.flows.server_tech_state_flow import ServerTechOutletsStateFlow
19+
from server_tech.handlers.server_tech_handler import ServerTechHandler
2320

2421

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

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

3732
@GlobalLock.lock
@@ -46,37 +41,38 @@ def get_inventory(self, context: AutoLoadCommandContext) -> AutoLoadDetails:
4641
resource.add_sub_resource('1', p1)
4742
return resource.create_autoload_details()
4843
"""
49-
5044
with LoggingSessionContext(context) as logger:
5145
api = CloudShellSessionContext(context).get_api()
5246
resource_config = RESTAPIPDUResourceConfig.from_context(context, api)
5347

5448
resource_model = PDUResourceModel.from_resource_config(resource_config)
55-
autoload_operations = ServerTechAutoloadFlow(config=resource_config)
56-
logger.info("Autoload started")
57-
response = autoload_operations.discover(self.SUPPORTED_OS, resource_model)
58-
logger.info("Autoload completed")
59-
return response
49+
50+
with ServerTechHandler.from_config(resource_config) as si:
51+
autoload_operations = ServerTechAutoloadFlow(si=si)
52+
logger.info("Autoload started")
53+
response = autoload_operations.discover(
54+
self.SUPPORTED_OS, resource_model
55+
)
56+
logger.info("Autoload completed")
57+
return response
6058

6159
def _change_power_state(
62-
self,
63-
context: ResourceCommandContext,
64-
ports: list[str],
65-
state: str
60+
self, context: ResourceCommandContext, ports: list[str], state: str
6661
) -> None: # noqa E501
6762
"""Set power outlets state based on provided data."""
6863
with LoggingSessionContext(context) as logger:
6964
api = CloudShellSessionContext(context).get_api()
7065

7166
resource_config = RESTAPIPDUResourceConfig.from_context(context, api)
7267

73-
outlets_operations = ServerTechOutletsStateFlow(config=resource_config)
74-
logger.info(f"Power {state.capitalize()} operation started")
75-
outlets_operations.set_outlets_state(
76-
ports=ports,
77-
state=state,
78-
)
79-
logger.info(f"Power {state.capitalize()} operation completed")
68+
with ServerTechHandler.from_config(resource_config) as si:
69+
outlets_operations = ServerTechOutletsStateFlow(si=si)
70+
logger.info(f"Power {state.capitalize()} operation started")
71+
outlets_operations.set_outlets_state(
72+
ports=ports,
73+
state=state,
74+
)
75+
logger.info(f"Power {state.capitalize()} operation completed")
8076

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

8985
def PowerCycle(
90-
self,
91-
context: ResourceCommandContext,
92-
ports: list[str],
93-
delay: str
86+
self, context: ResourceCommandContext, ports: list[str], delay: str
9487
) -> None: # noqa E501
9588
"""Set power state as CYCLE to provided outlets."""
9689
self._change_power_state(context=context, ports=ports, state="reboot")

src/server_tech/flows/server_tech_autoload_flow.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,33 @@
44
from typing import TYPE_CHECKING
55

66
from cloudshell.shell.flows.autoload.basic_flow import AbstractAutoloadFlow
7-
from server_tech.handlers.rest_api_handler import ServerTechAPI
87

98
if TYPE_CHECKING:
109
from cloudshell.shell.core.driver_context import AutoLoadDetails
11-
from cloudshell.shell.standards.pdu.resource_config import RESTAPIPDUResourceConfig
1210
from cloudshell.shell.standards.pdu.autoload_model import PDUResourceModel
1311

12+
from server_tech.handlers.server_tech_handler import ServerTechHandler
13+
1414

1515
logger = logging.getLogger(__name__)
1616

1717

1818
class ServerTechAutoloadFlow(AbstractAutoloadFlow):
1919
"""Autoload flow."""
2020

21-
def __init__(self, config: RESTAPIPDUResourceConfig):
21+
def __init__(self, si: ServerTechHandler):
2222
super().__init__()
23-
self.config = config
23+
self._si = si
2424

2525
def _autoload_flow(
26-
self,
27-
supported_os: list[str],
28-
resource_model: PDUResourceModel
26+
self, supported_os: list[str], resource_model: PDUResourceModel
2927
) -> AutoLoadDetails:
3028
"""Autoload Flow."""
3129
logger.info("*" * 70)
3230
logger.info("Start discovery process .....")
3331

34-
api = ServerTechAPI(
35-
address=self.config.address,
36-
username=self.config.api_user,
37-
password=self.config.api_password,
38-
port=self.config.api_port or None,
39-
scheme=self.config.api_scheme or None,
40-
)
41-
42-
outlets_info = api.get_outlets()
43-
pdu_info = api.get_pdu_info()
32+
outlets_info = self._si.get_outlets_info()
33+
pdu_info = self._si.get_pdu_info()
4434

4535
resource_model.vendor = "Server Technology"
4636
resource_model.model = pdu_info.get("model", "")
Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
from __future__ import annotations
22

3-
from attrs import define
43
from typing import TYPE_CHECKING
54

6-
from server_tech.handlers.rest_api_handler import ServerTechAPI
5+
from attrs import define
6+
77
from server_tech.helpers.errors import NotSupportedServerTechError
88

99
if TYPE_CHECKING:
10-
from cloudshell.shell.standards.pdu.resource_config import RESTAPIPDUResourceConfig
10+
from server_tech.handlers.server_tech_handler import ServerTechHandler
1111

1212

1313
@define
1414
class ServerTechOutletsStateFlow:
15-
config: RESTAPIPDUResourceConfig
15+
_si: ServerTechHandler
1616

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

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

3434
outlets = ServerTechOutletsStateFlow._ports_to_outlet_ids(ports=ports)
3535

36-
api = ServerTechAPI(
37-
address=self.config.address,
38-
username=self.config.api_user,
39-
password=self.config.api_password,
40-
port=self.config.api_port or None,
41-
scheme=self.config.api_scheme or None,
42-
)
43-
4436
for outlet_id in outlets:
45-
api.set_outlet_state(outlet_id=outlet_id, outlet_state=state)
37+
self._si.set_outlet_state(outlet_id=outlet_id, outlet_state=state)

src/server_tech/handlers/rest_api_handler.py

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22

33
import logging
44
import ssl
5+
import time
56
from abc import abstractmethod
67
from collections.abc import Callable
78

8-
from attrs import define, field
9-
from attrs.setters import frozen
10-
119
import requests
1210
import urllib3
13-
11+
from attrs import define, field
12+
from attrs.setters import frozen
1413

1514
from server_tech.helpers.errors import (
1615
BaseServerTechError,
1716
RESTAPIServerTechError,
17+
RESTAPIUnavailableServerTechError,
1818
)
1919

2020
logger = logging.getLogger(__name__)
@@ -127,76 +127,100 @@ def _do_delete(
127127
@define
128128
class ServerTechAPI(BaseAPIClient):
129129
BASE_ERRORS = {
130-
404: RESTAPIServerTechError,
130+
404: RESTAPIUnavailableServerTechError,
131131
405: RESTAPIServerTechError,
132132
503: RESTAPIServerTechError,
133133
}
134134
"""
135135
404 NOT FOUND Requested resource does not exist or is unavailable
136136
405 METHOD NOT ALLOWED Requested method was not permitted
137-
503 SERVICE UNAVAILABLE The server is too busy to send the resource or resource collection
137+
503 SERVICE UNAVAILABLE The server is too busy to send the resource or resource collection # noqa E501
138138
"""
139139

140+
class Decorators:
141+
@classmethod
142+
def get_data(
143+
cls, retries: int = 6, timeout: int = 5, raise_on_timeout: bool = True
144+
):
145+
def wrapper(decorated):
146+
def inner(*args, **kwargs):
147+
exception = None
148+
attempt = 0
149+
while attempt < retries:
150+
try:
151+
response = decorated(*args, **kwargs)
152+
if response:
153+
return response.json()
154+
else:
155+
return response
156+
except RESTAPIUnavailableServerTechError as e:
157+
exception = e
158+
time.sleep(timeout)
159+
attempt += 1
160+
161+
if raise_on_timeout:
162+
if exception:
163+
raise exception
164+
else:
165+
raise RESTAPIServerTechError(
166+
f"Cannot execute request for {retries * timeout} sec."
167+
)
168+
169+
return inner
170+
171+
return wrapper
172+
140173
def _base_url(self):
141-
# return f"{self.scheme}://{self.address}/jaws"
142174
return f"{self.scheme}://{self.address}:{self.port}/jaws"
143175

144-
def get_pdu_info(self) -> dict[str, str]:
145-
"""Get information about outlets."""
146-
pdu_info = {}
176+
@Decorators.get_data()
177+
def get_pdu_units_info(self) -> requests.Response:
178+
"""Get information about PDU units."""
147179
error_map = {}
148180

149181
units_data = self._do_get(
150-
path=f"config/info/units",
151-
http_error_map={**self.BASE_ERRORS, **error_map}
152-
).json()
153-
154-
for unit in units_data:
155-
pdu_info.update(
156-
{
157-
"model": unit.get("model_number", ""),
158-
"serial": unit.get("product_serial_number", ""),
159-
}
160-
)
182+
path="config/info/units", http_error_map={**self.BASE_ERRORS, **error_map}
183+
)
184+
185+
return units_data
186+
187+
@Decorators.get_data()
188+
def get_pdu_system_info(self) -> requests.Response:
189+
"""Get basic information about PDU."""
190+
error_map = {}
161191

162192
system_data = self._do_get(
163-
path=f"config/info/system",
164-
http_error_map={**self.BASE_ERRORS, **error_map}
165-
).json()
166-
pdu_info.update({"fw": system_data.get("firmware", "")})
167-
return pdu_info
193+
path="config/info/system", http_error_map={**self.BASE_ERRORS, **error_map}
194+
)
195+
196+
return system_data
168197

169-
def get_outlets(self) -> dict[str, str]:
198+
@Decorators.get_data()
199+
def get_outlets(self) -> requests.Response:
170200
"""Get information about outlets."""
171201
error_map = {}
172-
outlets_info = {}
173202

174-
response = self._do_get(
175-
path=f"control/outlets",
176-
http_error_map={**self.BASE_ERRORS, **error_map}
203+
outlets_info = self._do_get(
204+
path="control/outlets", http_error_map={**self.BASE_ERRORS, **error_map}
177205
)
178-
for data in response.json():
179-
outlets_info.update({data["id"]: data["control_state"]})
180206

181207
return outlets_info
182208

183-
def set_outlet_state(self, outlet_id: str, outlet_state: str) -> None:
209+
@Decorators.get_data()
210+
def set_outlet_state(self, outlet_id: str, outlet_state: str) -> requests.Response:
184211
"""Set outlet state.
185212
186213
Possible outlet states could be on/off/reboot.
187214
"""
188-
error_map = {
189-
400: RESTAPIServerTechError,
190-
409: RESTAPIServerTechError
191-
}
215+
error_map = {400: RESTAPIServerTechError, 409: RESTAPIServerTechError}
192216
"""
193-
400 BAD REQUEST Malformed patch document; a required patch object member is missing OR an unsupported operation was included.
217+
400 BAD REQUEST Malformed patch document; a required patch object member is missing OR an unsupported operation was included. # noqa E501
194218
409 CONFLICT Property specified for updating does not exist in resource
195219
"""
196-
self._do_patch(
220+
return self._do_patch(
197221
path=f"control/outlets/{outlet_id}",
198222
json={"control_action": outlet_state},
199-
http_error_map={**self.BASE_ERRORS, **error_map}
223+
http_error_map={**self.BASE_ERRORS, **error_map},
200224
)
201225

202226

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

0 commit comments

Comments
 (0)