Skip to content

Commit ecccebe

Browse files
authored
Add set_outlet and restart methods (#7)
Implement two specific ServerTech methods to change the outlets status (on, off, reboot) and to restart the PDU.
1 parent 0187827 commit ecccebe

File tree

10 files changed

+145
-8
lines changed

10 files changed

+145
-8
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ Please find below the list of supported getters:
1515
* get_interfaces_ip (will only return the `NET` interface management address)
1616
* get_users
1717

18+
## Additional features
19+
20+
The default NAPALM methods don't cover everything we can do on the PDUs (they actually do via CLI, but the driver does not control the PDUs via the CLIs yet). In order to perform actions such as changing the status of an outlet or resetting a PDU, new methods are implemented:
21+
22+
* set_outlet
23+
* restart
24+
1825
## Contributing
1926
Please read [CONTRIBUTING](CONTRIBUTING) for details on our process for submitting issues and requests.
2027

napalm_servertech_pro2/constants.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,14 @@
5454
"power user": 14,
5555
"admin": 15,
5656
}
57+
58+
SUPPORTED_OUTLET_ACTIONS = ["off", "on", "reboot"]
59+
60+
SUPPORTED_RESTART_ACTIONS = [
61+
"factory",
62+
"factory keep network",
63+
"new firmware",
64+
"new ssh keys",
65+
"new x509 certificate",
66+
"normal",
67+
]

napalm_servertech_pro2/pro2.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@
77
from netaddr import IPNetwork
88
from requests.auth import HTTPBasicAuth
99

10-
from napalm_servertech_pro2.constants import CONFIG_ITEMS, LOCAL_USER_LEVELS
11-
from napalm_servertech_pro2.utils import convert_uptime, parse_hardware
10+
from napalm_servertech_pro2.constants import (
11+
CONFIG_ITEMS,
12+
LOCAL_USER_LEVELS,
13+
SUPPORTED_OUTLET_ACTIONS,
14+
SUPPORTED_RESTART_ACTIONS,
15+
)
16+
from napalm_servertech_pro2.utils import (
17+
convert_uptime,
18+
parse_hardware,
19+
validate_actions,
20+
)
1221

1322

1423
class PRO2Driver(NetworkDriver):
@@ -33,7 +42,14 @@ def _req(self, path, method="GET", json=None, raise_err=True):
3342
raise err
3443
else:
3544
return {"err": str(err)}
36-
return req.json()
45+
if "application/json" in req.headers.get("Content-Type"):
46+
return req.json()
47+
else:
48+
return {
49+
"status": "success",
50+
"status_code": req.status_code,
51+
"content": req.text,
52+
}
3753

3854
def open(self):
3955
"""Open a connection to the device."""
@@ -199,3 +215,32 @@ def get_users(self):
199215
}
200216
for user in users
201217
}
218+
219+
def set_outlet(self, outlet_id, action):
220+
"""
221+
Change the status of an outlet
222+
223+
:param outlet_id: a string
224+
:param action: a string (values can be: on, off, reboot)
225+
:return: a dict
226+
"""
227+
validate_actions(action, SUPPORTED_OUTLET_ACTIONS)
228+
229+
outlet = self._req(
230+
f"/control/outlets/{outlet_id}", "PATCH", json={"control_action": action}
231+
)
232+
233+
return outlet
234+
235+
def restart(self, action):
236+
"""
237+
Restarts the PDU
238+
239+
:param action: a string (see SUPPORTED_RESTART_ACTIONS for valid values)
240+
:return: a dict
241+
"""
242+
validate_actions(action, SUPPORTED_RESTART_ACTIONS)
243+
244+
restart = self._req("/restart", "PATCH", json={"action": action})
245+
246+
return restart

napalm_servertech_pro2/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,13 @@ def parse_hardware(hardware_string):
3535
"ram": int(m.group("ram")),
3636
"flash": int(m.group("flash")),
3737
}
38+
39+
40+
def validate_actions(action, supported_actions):
41+
"""Ensures the inputed action is supported, raises an exception otherwise."""
42+
if action not in supported_actions:
43+
raise ValueError(
44+
f'Action "{action}" is not supported.'
45+
" the list of valid actions is: {}".format(", ".join(supported_actions))
46+
)
47+
return True

tests/unit/conftest.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77
from napalm.base.test import conftest as parent_conftest
88
from napalm.base.test.double import BaseTestDouble
9+
from requests.models import HTTPError
910
from napalm_servertech_pro2 import pro2
1011

1112

@@ -50,21 +51,47 @@ def open(self):
5051
class FakePRO2Api(BaseTestDouble):
5152
"""ServerTech fake API."""
5253

54+
REQUESTS = {
55+
"control/outlets/XX99": {
56+
"headers": {"Content-Type": "text/html"},
57+
"status_code": 404,
58+
}
59+
}
60+
5361
def request(self, method, **kwargs):
54-
filename = f'{self.sanitize_text(kwargs["url"].split("/jaws/")[1])}.json'
55-
path = self.find_file(filename)
56-
return FakeRequest(method, path)
62+
address = kwargs["url"].split("/jaws/")[1]
63+
if self.REQUESTS.get(address):
64+
return FakeRequest(
65+
method,
66+
address,
67+
self.REQUESTS[address]["headers"],
68+
self.REQUESTS[address]["status_code"],
69+
)
70+
else:
71+
filename = f"{self.sanitize_text(address)}.json"
72+
path = self.find_file(filename)
73+
headers = {
74+
"Content-Type": "application/json" if method == "GET" else "text/html"
75+
}
76+
status_code = 200 if method == "GET" else 204
77+
return FakeRequest(method, path, headers, status_code)
5778

5879

5980
class FakeRequest:
6081
"""A fake API request."""
6182

62-
def __init__(self, method, path):
83+
def __init__(self, method, path, headers, status_code):
6384
self.method = method
6485
self.path = path
86+
self.headers = headers
87+
self.status_code = status_code
88+
self.text = ""
6589

6690
def raise_for_status(self):
67-
return True
91+
if self.status_code >= 400:
92+
raise HTTPError
93+
else:
94+
return True
6895

6996
def json(self):
7097
with open(self.path, "r") as file:

tests/unit/mocked_data/control_outlets_AA01.json

Whitespace-only changes.

tests/unit/mocked_data/restart.json

Whitespace-only changes.

tests/unit/test_add.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Tests for getters."""
2+
3+
import pytest
4+
from requests import HTTPError
5+
6+
7+
@pytest.mark.usefixtures("set_device_parameters")
8+
class TestAdd(object):
9+
"""Test additional methods."""
10+
11+
def test_set_outlet(self):
12+
res = self.device.set_outlet("AA01", "on")
13+
assert res["status"] == "success"
14+
15+
with pytest.raises(ValueError):
16+
self.device.set_outlet("AA01", "no")
17+
18+
with pytest.raises(HTTPError):
19+
self.device.set_outlet("XX99", "on")
20+
21+
def test_reboot(self):
22+
res = self.device.restart("normal")
23+
assert res["status"] == "success"
24+
25+
with pytest.raises(ValueError):
26+
self.device.restart("reboot")

tests/unit/test_getters.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@
88
@pytest.mark.usefixtures("set_device_parameters")
99
class TestGetter(BaseTestGetters):
1010
"""Test get_* methods."""
11+
12+
def test_method_signatures(self):
13+
return True

tests/utils/test_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,11 @@ def test_parse_hardware():
2626
"ram": 2048,
2727
"flash": 2048,
2828
}
29+
30+
31+
def test_validate_actions():
32+
supported_actions = ["foo", "bar"]
33+
assert utils.validate_actions("foo", supported_actions) is True
34+
35+
with pytest.raises(ValueError):
36+
utils.validate_actions("oof", supported_actions)

0 commit comments

Comments
 (0)