Merge ~adacre/maas:eaton_driver into maas:master
- Git
- lp:~adacre/maas
- eaton_driver
- Merge into master
Status: | Superseded |
---|---|
Proposed branch: | ~adacre/maas:eaton_driver |
Merge into: | maas:master |
Diff against target: |
324 lines (+294/-0) 3 files modified
src/provisioningserver/drivers/power/eaton.py (+131/-0) src/provisioningserver/drivers/power/registry.py (+2/-0) src/provisioningserver/drivers/power/tests/test_eaton.py (+161/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Lander | Needs Fixing | ||
MAAS Maintainers | Pending | ||
Review via email:
|
Commit message
Description of the change
Adds support for Eaton branded switched PDUs. Based heavily off of the APC driver.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Alex Dacre (adacre) wrote : | # |
Not sure which tests are failing - the tests added as part of this power driver are passing:
adacre@
WARNING: bin/test.
selectors at the command-line. Run bin/test.
Tests running...
SUCCESS: provisioningser
SUCCESS: provisioningser
SUCCESS: provisioningser
SUCCESS: provisioningser
SUCCESS: provisioningser
SUCCESS: provisioningser
SUCCESS: provisioningser
SUCCESS: provisioningser
SUCCESS: provisioningser
SUCCESS: provisioningser
Ran 10 tests in 2.121s
OK
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Adam Collard (adam-collard) wrote : | # |
> UNIT TESTS
> -b eaton_driver lp:~adacre/maas/+git/maas into -b master lp:~maas-
> committers/maas
>
> STATUS: FAILED
> LOG: http://
> tester/7045/console
> COMMIT: 2d27119ed35b950
The test failure here is a lint issue, can you please use `make format-py` to reformat your modules, and ensure that `make lint` passes?
Unmerged commits
- de4a88f... by Alex Dacre
-
Add eaton power driver
address linter issue
Preview Diff
1 | diff --git a/src/provisioningserver/drivers/power/eaton.py b/src/provisioningserver/drivers/power/eaton.py |
2 | new file mode 100644 |
3 | index 0000000..21c5843 |
4 | --- /dev/null |
5 | +++ b/src/provisioningserver/drivers/power/eaton.py |
6 | @@ -0,0 +1,131 @@ |
7 | +"""Eaton Power Driver. |
8 | + |
9 | +Support for managing Eaton PDU outlets via SNMP. |
10 | +""" |
11 | + |
12 | +__all__ = [] |
13 | + |
14 | +import re |
15 | +from time import sleep |
16 | + |
17 | +from provisioningserver.drivers import ( |
18 | + make_ip_extractor, |
19 | + make_setting_field, |
20 | + SETTING_SCOPE, |
21 | +) |
22 | +from provisioningserver.drivers.power import PowerActionError, PowerDriver |
23 | +from provisioningserver.utils import shell |
24 | + |
25 | + |
26 | +class EatonState: |
27 | + OFF = "0" |
28 | + ON = "1" |
29 | + |
30 | + |
31 | +class EatonFunction: |
32 | + QUERY = "2" |
33 | + OFF = "3" |
34 | + ON = "4" |
35 | + |
36 | + |
37 | +class EatonPowerDriver(PowerDriver): |
38 | + |
39 | + name = "eaton" |
40 | + chassis = True |
41 | + description = "Eaton PDU" |
42 | + settings = [ |
43 | + make_setting_field("power_address", "IP for Eaton PDU", required=True), |
44 | + make_setting_field( |
45 | + "node_outlet", |
46 | + "Eaton PDU node outlet number (1-24)", |
47 | + scope=SETTING_SCOPE.NODE, |
48 | + required=True, |
49 | + ), |
50 | + make_setting_field( |
51 | + "power_on_delay", "Power ON outlet delay (seconds)", default="5" |
52 | + ), |
53 | + ] |
54 | + ip_extractor = make_ip_extractor("power_address") |
55 | + queryable = False |
56 | + |
57 | + def detect_missing_packages(self): |
58 | + binary, package = ["snmpset", "snmp"] |
59 | + if not shell.has_command_available(binary): |
60 | + return [package] |
61 | + return [] |
62 | + |
63 | + def run_process(self, *command): |
64 | + """Run SNMP command in subprocess.""" |
65 | + result = shell.run_command(*command) |
66 | + if result.returncode != 0: |
67 | + raise PowerActionError( |
68 | + "Eaton Power Driver external process error for command %s: %s" |
69 | + % ("".join(command), result.stderr) |
70 | + ) |
71 | + match = re.search(r"INTEGER:\s*([0-1])", result.stdout) |
72 | + if match is None: |
73 | + raise PowerActionError( |
74 | + "Eaton Power Driver unable to extract outlet power state" |
75 | + " from: %s" % result.stdout |
76 | + ) |
77 | + else: |
78 | + return match.group(1) |
79 | + |
80 | + def power_on(self, system_id, context): |
81 | + """Power on Eaton outlet.""" |
82 | + if self.power_query(system_id, context) == "on": |
83 | + self.power_off(system_id, context) |
84 | + sleep(float(context["power_on_delay"])) |
85 | + self.run_process( |
86 | + "snmpset", |
87 | + *_get_common_args( |
88 | + context["power_address"], |
89 | + EatonFunction.ON, |
90 | + context["node_outlet"], |
91 | + ), |
92 | + "i", |
93 | + "0", |
94 | + ) |
95 | + |
96 | + def power_off(self, system_id, context): |
97 | + """Power off Eaton outlet.""" |
98 | + self.run_process( |
99 | + "snmpset", |
100 | + *_get_common_args( |
101 | + context["power_address"], |
102 | + EatonFunction.OFF, |
103 | + context["node_outlet"], |
104 | + ), |
105 | + "i", |
106 | + "0", |
107 | + ) |
108 | + |
109 | + def power_query(self, system_id, context): |
110 | + """Power query for Eaton outlet.""" |
111 | + power_state = self.run_process( |
112 | + "snmpget", |
113 | + *_get_common_args( |
114 | + context["power_address"], |
115 | + EatonFunction.QUERY, |
116 | + context["node_outlet"], |
117 | + ), |
118 | + ) |
119 | + if power_state == EatonState.OFF: |
120 | + return "off" |
121 | + elif power_state == EatonState.ON: |
122 | + return "on" |
123 | + else: |
124 | + raise PowerActionError( |
125 | + "Eaton Power Driver retrieved unknown power state: %r" |
126 | + % power_state |
127 | + ) |
128 | + |
129 | + |
130 | +def _get_common_args(address, function, outlet): |
131 | + return [ |
132 | + "-c", |
133 | + "private", |
134 | + "-v1", |
135 | + address, |
136 | + f"1.3.6.1.4.1.534.6.6.7.6.6.1.{function}.0.{outlet}", |
137 | + ] |
138 | diff --git a/src/provisioningserver/drivers/power/registry.py b/src/provisioningserver/drivers/power/registry.py |
139 | index 8ea5b27..a5e9a7d 100644 |
140 | --- a/src/provisioningserver/drivers/power/registry.py |
141 | +++ b/src/provisioningserver/drivers/power/registry.py |
142 | @@ -12,6 +12,7 @@ from provisioningserver.drivers.power import JSON_POWER_DRIVERS_SCHEMA |
143 | from provisioningserver.drivers.power.amt import AMTPowerDriver |
144 | from provisioningserver.drivers.power.apc import APCPowerDriver |
145 | from provisioningserver.drivers.power.dli import DLIPowerDriver |
146 | +from provisioningserver.drivers.power.eaton import EatonPowerDriver |
147 | from provisioningserver.drivers.power.hmc import HMCPowerDriver |
148 | from provisioningserver.drivers.power.ipmi import IPMIPowerDriver |
149 | from provisioningserver.drivers.power.manual import ManualPowerDriver |
150 | @@ -52,6 +53,7 @@ power_drivers = [ |
151 | AMTPowerDriver(), |
152 | APCPowerDriver(), |
153 | DLIPowerDriver(), |
154 | + EatonPowerDriver(), |
155 | HMCPowerDriver(), |
156 | IPMIPowerDriver(), |
157 | ManualPowerDriver(), |
158 | diff --git a/src/provisioningserver/drivers/power/tests/test_eaton.py b/src/provisioningserver/drivers/power/tests/test_eaton.py |
159 | new file mode 100644 |
160 | index 0000000..9550827 |
161 | --- /dev/null |
162 | +++ b/src/provisioningserver/drivers/power/tests/test_eaton.py |
163 | @@ -0,0 +1,161 @@ |
164 | +"""Tests for `provisioningserver.drivers.power.eaton`.""" |
165 | + |
166 | +__all__ = [] |
167 | + |
168 | +from testtools.matchers import Equals |
169 | + |
170 | +from maastesting.factory import factory |
171 | +from maastesting.matchers import MockCalledOnceWith |
172 | +from maastesting.testcase import MAASTestCase |
173 | +from provisioningserver.drivers.power import eaton as eaton_module |
174 | +from provisioningserver.drivers.power import PowerActionError |
175 | +from provisioningserver.utils.shell import has_command_available, ProcessResult |
176 | + |
177 | +COMMON_ARGS = "-c private -v1 {} 1.3.6.1.4.1.534.6.6.7.6.6.1.{}.0.{}" |
178 | +COMMON_OUTPUT = "iso.3.6.1.4.1.534.6.6.7.6.6.1.%s.0.%s = INTEGER: 0\n" |
179 | + |
180 | + |
181 | +class TestEatonPowerDriver(MAASTestCase): |
182 | + def make_context(self): |
183 | + return { |
184 | + "power_address": factory.make_name("power_address"), |
185 | + "node_outlet": factory.make_name("node_outlet"), |
186 | + "power_on_delay": "5", |
187 | + } |
188 | + |
189 | + def test_missing_packages(self): |
190 | + mock = self.patch(has_command_available) |
191 | + mock.return_value = False |
192 | + driver = eaton_module.EatonPowerDriver() |
193 | + missing = driver.detect_missing_packages() |
194 | + self.assertItemsEqual(["snmp"], missing) |
195 | + |
196 | + def test_no_missing_packages(self): |
197 | + mock = self.patch(has_command_available) |
198 | + mock.return_value = True |
199 | + driver = eaton_module.EatonPowerDriver() |
200 | + missing = driver.detect_missing_packages() |
201 | + self.assertItemsEqual([], missing) |
202 | + |
203 | + def patch_run_command(self, stdout="", stderr="", returncode=0): |
204 | + mock_run_command = self.patch(eaton_module.shell, "run_command") |
205 | + mock_run_command.return_value = ProcessResult( |
206 | + stdout=stdout, stderr=stderr, returncode=returncode |
207 | + ) |
208 | + return mock_run_command |
209 | + |
210 | + def test_run_process_calls_command_and_returns_output(self): |
211 | + driver = eaton_module.EatonPowerDriver() |
212 | + context = self.make_context() |
213 | + command = ["snmpget"] + COMMON_ARGS.format( |
214 | + context["power_address"], |
215 | + eaton_module.EatonFunction.QUERY, |
216 | + context["node_outlet"], |
217 | + ).split() |
218 | + mock_run_command = self.patch_run_command( |
219 | + stdout=COMMON_OUTPUT |
220 | + % (eaton_module.EatonFunction.QUERY, context["node_outlet"]), |
221 | + stderr="error_output", |
222 | + ) |
223 | + output = driver.run_process(*command) |
224 | + mock_run_command.assert_called_once_with(*command) |
225 | + self.expectThat(output, Equals(eaton_module.EatonState.OFF)) |
226 | + |
227 | + def test_run_process_crashes_on_external_process_error(self): |
228 | + driver = eaton_module.EatonPowerDriver() |
229 | + self.patch_run_command(returncode=1) |
230 | + self.assertRaises( |
231 | + PowerActionError, driver.run_process, factory.make_name("command") |
232 | + ) |
233 | + |
234 | + def test_run_process_crashes_on_no_power_state_match_found(self): |
235 | + driver = eaton_module.EatonPowerDriver() |
236 | + self.patch_run_command(stdout="Error") |
237 | + self.assertRaises( |
238 | + PowerActionError, driver.run_process, factory.make_name("command") |
239 | + ) |
240 | + |
241 | + def test_power_on_calls_run_process(self): |
242 | + driver = eaton_module.EatonPowerDriver() |
243 | + system_id = factory.make_name("system_id") |
244 | + context = self.make_context() |
245 | + mock_power_query = self.patch(driver, "power_query") |
246 | + mock_power_query.return_value = "on" |
247 | + self.patch(driver, "power_off") |
248 | + mock_sleep = self.patch(eaton_module, "sleep") |
249 | + mock_run_process = self.patch(driver, "run_process") |
250 | + driver.power_on(system_id, context) |
251 | + |
252 | + self.expectThat( |
253 | + mock_power_query, MockCalledOnceWith(system_id, context) |
254 | + ) |
255 | + self.expectThat( |
256 | + mock_sleep, MockCalledOnceWith(float(context["power_on_delay"])) |
257 | + ) |
258 | + command = ( |
259 | + ["snmpset"] |
260 | + + COMMON_ARGS.format( |
261 | + context["power_address"], |
262 | + eaton_module.EatonFunction.ON, |
263 | + context["node_outlet"], |
264 | + ).split() |
265 | + + ["i", "0"] |
266 | + ) |
267 | + mock_run_process.assert_called_once_with(*command) |
268 | + |
269 | + def test_power_off_calls_run_process(self): |
270 | + driver = eaton_module.EatonPowerDriver() |
271 | + system_id = factory.make_name("system_id") |
272 | + context = self.make_context() |
273 | + mock_run_process = self.patch(driver, "run_process") |
274 | + driver.power_off(system_id, context) |
275 | + command = ( |
276 | + ["snmpset"] |
277 | + + COMMON_ARGS.format( |
278 | + context["power_address"], |
279 | + eaton_module.EatonFunction.OFF, |
280 | + context["node_outlet"], |
281 | + ).split() |
282 | + + ["i", "0"] |
283 | + ) |
284 | + mock_run_process.assert_called_once_with(*command) |
285 | + |
286 | + def test_power_query_returns_power_state_on(self): |
287 | + driver = eaton_module.EatonPowerDriver() |
288 | + system_id = factory.make_name("system_id") |
289 | + context = self.make_context() |
290 | + mock_run_process = self.patch(driver, "run_process") |
291 | + mock_run_process.return_value = eaton_module.EatonState.ON |
292 | + result = driver.power_query(system_id, context) |
293 | + command = ["snmpget"] + COMMON_ARGS.format( |
294 | + context["power_address"], |
295 | + eaton_module.EatonFunction.QUERY, |
296 | + context["node_outlet"], |
297 | + ).split() |
298 | + mock_run_process.assert_called_once_with(*command) |
299 | + self.expectThat(result, Equals("on")) |
300 | + |
301 | + def test_power_query_returns_power_state_off(self): |
302 | + driver = eaton_module.EatonPowerDriver() |
303 | + system_id = factory.make_name("system_id") |
304 | + context = self.make_context() |
305 | + mock_run_process = self.patch(driver, "run_process") |
306 | + mock_run_process.return_value = eaton_module.EatonState.OFF |
307 | + result = driver.power_query(system_id, context) |
308 | + command = ["snmpget"] + COMMON_ARGS.format( |
309 | + context["power_address"], |
310 | + eaton_module.EatonFunction.QUERY, |
311 | + context["node_outlet"], |
312 | + ).split() |
313 | + mock_run_process.assert_called_once_with(*command) |
314 | + self.expectThat(result, Equals("off")) |
315 | + |
316 | + def test_power_query_crashes_for_uknown_power_state(self): |
317 | + driver = eaton_module.EatonPowerDriver() |
318 | + system_id = factory.make_name("system_id") |
319 | + context = self.make_context() |
320 | + mock_run_process = self.patch(driver, "run_process") |
321 | + mock_run_process.return_value = "Error" |
322 | + self.assertRaises( |
323 | + PowerActionError, driver.power_query, system_id, context |
324 | + ) |
UNIT TESTS
-b eaton_driver lp:~adacre/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED maas-ci- jenkins. internal: 8080/job/ maas/job/ branch- tester/ 7045/console 72331ffdd3aa802 e0406e651f
LOG: http://
COMMIT: 2d27119ed35b950