Merge lp:~trapnine/maas/bug-1381000 into lp:~maas-committers/maas/trunk
- bug-1381000
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Andres Rodriguez | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 4344 | ||||
Proposed branch: | lp:~trapnine/maas/bug-1381000 | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Diff against target: |
1296 lines (+668/-32) 43 files modified
src/maasserver/clusterrpc/power_parameters.py (+7/-3) src/maasserver/clusterrpc/tests/test_power_parameters.py (+7/-2) src/maasserver/rpc/tests/test_nodes.py (+2/-2) src/maasserver/static/js/angular/controllers/node_details.js (+18/-0) src/maasserver/static/partials/node-details.html (+9/-0) src/maasserver/tests/test_middleware.py (+1/-1) src/maastesting/testcase.py (+1/-1) src/provisioningserver/drivers/__init__.py (+9/-9) src/provisioningserver/drivers/hardware/apc.py (+4/-0) src/provisioningserver/drivers/hardware/hmc.py (+4/-0) src/provisioningserver/drivers/power/__init__.py (+23/-2) src/provisioningserver/drivers/power/amt.py (+40/-0) src/provisioningserver/drivers/power/apc.py (+8/-0) src/provisioningserver/drivers/power/dli.py (+32/-0) src/provisioningserver/drivers/power/ether_wake.py (+34/-0) src/provisioningserver/drivers/power/fence_cdu.py (+32/-0) src/provisioningserver/drivers/power/hmc.py (+8/-0) src/provisioningserver/drivers/power/ipmi.py (+6/-0) src/provisioningserver/drivers/power/moonshot.py (+6/-0) src/provisioningserver/drivers/power/mscm.py (+4/-0) src/provisioningserver/drivers/power/msftocs.py (+4/-0) src/provisioningserver/drivers/power/seamicro.py (+6/-0) src/provisioningserver/drivers/power/tests/test_amt.py (+51/-0) src/provisioningserver/drivers/power/tests/test_apc.py (+15/-0) src/provisioningserver/drivers/power/tests/test_base.py (+3/-0) src/provisioningserver/drivers/power/tests/test_dli.py (+51/-0) src/provisioningserver/drivers/power/tests/test_ether_wake.py (+51/-0) src/provisioningserver/drivers/power/tests/test_fence_cdu.py (+51/-0) src/provisioningserver/drivers/power/tests/test_hmc.py (+15/-0) src/provisioningserver/drivers/power/tests/test_ipmi.py (+18/-1) src/provisioningserver/drivers/power/tests/test_moonshot.py (+18/-1) src/provisioningserver/drivers/power/tests/test_mscm.py (+6/-0) src/provisioningserver/drivers/power/tests/test_msftocs.py (+6/-0) src/provisioningserver/drivers/power/tests/test_seamicro.py (+18/-1) src/provisioningserver/drivers/power/tests/test_ucsm.py (+6/-0) src/provisioningserver/drivers/power/tests/test_virsh.py (+15/-0) src/provisioningserver/drivers/power/tests/test_vmware.py (+15/-0) src/provisioningserver/drivers/power/ucsm.py (+4/-0) src/provisioningserver/drivers/power/virsh.py (+12/-0) src/provisioningserver/drivers/power/vmware.py (+6/-0) src/provisioningserver/drivers/tests/test_base.py (+34/-7) src/provisioningserver/power/schema.py (+6/-0) src/provisioningserver/rpc/clusterservice.py (+2/-2) |
||||
To merge this branch: | bzr merge lp:~trapnine/maas/bug-1381000 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andres Rodriguez (community) | Approve | ||
Blake Rouse (community) | Approve | ||
Review via email: mp+272875@code.launchpad.net |
Commit message
Checks selected node power types for missing system packages and warns user about them.
Checks are fast, and run on cluster every time power type info is loaded from cluster server (generally via 'command -v').
Messages are displayed dynamically when power type is changed/selected in the power dropdown.
Description of the change
See commit message.
Next step is to prevent deployment of nodes with uninstalled power drivers, but this commit satisfies the bug as reported.
I used the power driver architecture to implement the checks. The template based power types now have drivers, but they're not in the PowerDriverRegistry and are only used for the package check. The templates can be merged into these drivers later and then they can be in the Registry.
I also blew away the PowerTypeRegistry, as it was rendered superfluous.
REVIEWERS:
Please confirm these are the correct apps/packages to check for.
I built this by inspecting the power driver code and templates.
DRIVER: BINARY CHECKED / PACKAGE DISPLAYED
amt: amttool/amtterm and wsman/wsmancli (we will demand both, not just amttool! do most people have V8+ amt's?)
apc: snmpset/snmp
dli: wget/wget (hey, the template invokes it!)
etherwake: wakeonlan/wakeonlan or etherwake/etherwake
fence_cdu (sentry switch CDU): fence_cdu/
hmc: chsysstate/'HMC Management Software' (no package AFAIK)
ipmi: ipmipower/
moonshot (iLO IPMI): ipmipower/
mscm (iLO4 chassis manager): uses paramiko, no check
microsoft ocs: uses urllib2, no check
seamicro: ipmitool/ipmitool (wasn't 100% sure on this one - can't use ipmipower?)
cisco ucsm: uses urllib2, no check
virsh: virsh/libvirt-bin, virt-login-
vmware: tests import of pyVmomi, pyVim.connect / python-pyvmomi package
Jeffrey C Jones (trapnine) wrote : | # |
> The implementation looks sound. Really like how you did it. Got some comments,
> the biggest issue is the missing unit tests. You are missing unit tests for
> all of the power drivers. Please add unit tests on each to test the newly
> added methods functionality.
thanks for the feedback. added & fixed, please test!
Blake Rouse (blake-rouse) wrote : | # |
Thanks for all the fixes.
MAAS Lander (maas-lander) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
Andres Rodriguez (andreserl) : | # |
Preview Diff
1 | === modified file 'src/maasserver/clusterrpc/power_parameters.py' |
2 | --- src/maasserver/clusterrpc/power_parameters.py 2015-08-21 13:59:15 +0000 |
3 | +++ src/maasserver/clusterrpc/power_parameters.py 2015-10-01 23:31:26 +0000 |
4 | @@ -86,7 +86,8 @@ |
5 | return form_field |
6 | |
7 | |
8 | -def add_power_type_parameters(name, description, fields, parameters_set): |
9 | +def add_power_type_parameters( |
10 | + name, description, fields, missing_packages, parameters_set): |
11 | """Add new power type parameters to the given parameters_set if it |
12 | does not already exist. |
13 | |
14 | @@ -113,7 +114,8 @@ |
15 | } |
16 | validate(fields, field_set_schema) |
17 | parameters_set.append( |
18 | - {'name': name, 'description': description, 'fields': fields}) |
19 | + {'name': name, 'description': description, 'fields': fields, |
20 | + 'missing_packages': missing_packages}) |
21 | |
22 | |
23 | def get_power_type_parameters_from_json(json_power_type_parameters): |
24 | @@ -195,5 +197,7 @@ |
25 | name = power_type['name'] |
26 | fields = power_type['fields'] |
27 | description = power_type['description'] |
28 | - add_power_type_parameters(name, description, fields, merged_types) |
29 | + missing_packages = power_type['missing_packages'] |
30 | + add_power_type_parameters( |
31 | + name, description, fields, missing_packages, merged_types) |
32 | return sorted(merged_types, key=itemgetter("description")) |
33 | |
34 | === modified file 'src/maasserver/clusterrpc/tests/test_power_parameters.py' |
35 | --- src/maasserver/clusterrpc/tests/test_power_parameters.py 2015-08-21 16:28:40 +0000 |
36 | +++ src/maasserver/clusterrpc/tests/test_power_parameters.py 2015-10-01 23:31:26 +0000 |
37 | @@ -253,6 +253,7 @@ |
38 | }] |
39 | add_power_type_parameters( |
40 | name='blah', description='baz', fields=[self.make_field()], |
41 | + missing_packages=[], |
42 | parameters_set=existing_parameters) |
43 | self.assertEqual( |
44 | [{'name': 'blah', 'description': 'baz', 'fields': {}}], |
45 | @@ -261,24 +262,28 @@ |
46 | def test_adds_new_power_type_parameters(self): |
47 | existing_parameters = [] |
48 | fields = [self.make_field()] |
49 | + missing_packages = ['package1', 'package2'] |
50 | add_power_type_parameters( |
51 | name='blah', description='baz', fields=fields, |
52 | + missing_packages=missing_packages, |
53 | parameters_set=existing_parameters) |
54 | self.assertEqual( |
55 | - [{'name': 'blah', 'description': 'baz', 'fields': fields}], |
56 | + [{'name': 'blah', 'description': 'baz', 'fields': fields, |
57 | + 'missing_packages': missing_packages}], |
58 | existing_parameters) |
59 | |
60 | def test_validates_new_parameters(self): |
61 | self.assertRaises( |
62 | jsonschema.ValidationError, add_power_type_parameters, |
63 | name='blah', description='baz', fields=[{}], |
64 | - parameters_set=[]) |
65 | + missing_packages=[], parameters_set=[]) |
66 | |
67 | def test_subsequent_parameters_set_is_valid(self): |
68 | parameters_set = [] |
69 | fields = [self.make_field()] |
70 | add_power_type_parameters( |
71 | name='blah', description='baz', fields=fields, |
72 | + missing_packages=[], |
73 | parameters_set=parameters_set) |
74 | jsonschema.validate( |
75 | parameters_set, JSON_POWER_TYPE_SCHEMA) |
76 | |
77 | === modified file 'src/maasserver/rpc/tests/test_nodes.py' |
78 | --- src/maasserver/rpc/tests/test_nodes.py 2015-08-29 01:42:44 +0000 |
79 | +++ src/maasserver/rpc/tests/test_nodes.py 2015-10-01 23:31:26 +0000 |
80 | @@ -45,7 +45,7 @@ |
81 | from maasserver.testing.testcase import MAASServerTestCase |
82 | from maasserver.utils.orm import post_commit_hooks |
83 | from maastesting.twisted import always_succeed_with |
84 | -from provisioningserver.drivers import PowerTypeRegistry |
85 | +from provisioningserver.drivers import gen_power_types |
86 | from provisioningserver.power import QUERY_POWER_TYPES |
87 | from provisioningserver.rpc.cluster import ( |
88 | DescribePowerTypes, |
89 | @@ -76,7 +76,7 @@ |
90 | |
91 | fixture = self.useFixture(MockLiveRegionToClusterRPCFixture()) |
92 | protocol = fixture.makeCluster(cluster, DescribePowerTypes) |
93 | - self.power_types = [item for name, item in PowerTypeRegistry] |
94 | + self.power_types = list(gen_power_types()) |
95 | protocol.DescribePowerTypes.side_effect = always_succeed_with( |
96 | {'power_types': self.power_types}) |
97 | return protocol |
98 | |
99 | === modified file 'src/maasserver/static/js/angular/controllers/node_details.js' |
100 | --- src/maasserver/static/js/angular/controllers/node_details.js 2015-09-27 20:01:05 +0000 |
101 | +++ src/maasserver/static/js/angular/controllers/node_details.js 2015-10-01 23:31:26 +0000 |
102 | @@ -903,6 +903,24 @@ |
103 | } |
104 | }; |
105 | |
106 | + // Check to see if the power type has any missing system packages. |
107 | + $scope.hasPowerError = function() { |
108 | + if(angular.isObject($scope.power.type)) { |
109 | + return $scope.power.type.missing_packages.length > 0; |
110 | + } else { |
111 | + return false; |
112 | + } |
113 | + }; |
114 | + |
115 | + // Returns an array of missing system packages. |
116 | + $scope.getPowerError = function() { |
117 | + if(angular.isObject($scope.power.type)) { |
118 | + return $scope.power.type.missing_packages; |
119 | + } else { |
120 | + return false; |
121 | + } |
122 | + }; |
123 | + |
124 | // Load all the required managers. |
125 | ManagerHelperService.loadManagers([ |
126 | NodesManager, |
127 | |
128 | === modified file 'src/maasserver/static/partials/node-details.html' |
129 | --- src/maasserver/static/partials/node-details.html 2015-10-01 14:07:46 +0000 |
130 | +++ src/maasserver/static/partials/node-details.html 2015-10-01 23:31:26 +0000 |
131 | @@ -267,6 +267,15 @@ |
132 | <div class="twelve-col"> |
133 | <h3 class="title">Power</h3> |
134 | </div> |
135 | + <div class="twelve-col ng-hide error" data-ng-show="hasPowerError()"> |
136 | + <ul class="flash-messages"> |
137 | + <li class="flash-messages__item error"> |
138 | + To control power with this power type, install the |
139 | + '<span ng-repeat="error in getPowerError()">{$ error $}{$$last ? '' : ', '$}</span>' |
140 | + package(s) on cluster '{$ node.nodegroup.name $}' |
141 | + </li> |
142 | + </ul> |
143 | + </div> |
144 | <fieldset class="six-col" |
145 | data-ng-disabled="!power.editing" |
146 | data-maas-power-parameters="power.types" |
147 | |
148 | === modified file 'src/maasserver/tests/test_middleware.py' |
149 | --- src/maasserver/tests/test_middleware.py 2015-05-29 16:47:37 +0000 |
150 | +++ src/maasserver/tests/test_middleware.py 2015-10-01 23:31:26 +0000 |
151 | @@ -355,7 +355,7 @@ |
152 | middleware = RPCErrorsMiddleware() |
153 | request = factory.make_fake_request(factory.make_string(), 'POST') |
154 | error_message = ( |
155 | - "No connections availble for cluster %s" % |
156 | + "No connections available for cluster %s" % |
157 | factory.make_name('cluster')) |
158 | error = NoConnectionsAvailable(error_message) |
159 | response = middleware.process_exception(request, error) |
160 | |
161 | === modified file 'src/maastesting/testcase.py' |
162 | --- src/maastesting/testcase.py 2015-09-24 16:22:12 +0000 |
163 | +++ src/maastesting/testcase.py 2015-10-01 23:31:26 +0000 |
164 | @@ -258,7 +258,7 @@ |
165 | """ |
166 | # If 'attribute' is None, assume 'obj' is a 'fully-qualified' object, |
167 | # and assume that its __module__ is what we want to patch. For more |
168 | - # complex use cases, the two-paramerter 'patch' will still need to |
169 | + # complex use cases, the two-parameter 'patch' will still need to |
170 | # be used. |
171 | if attribute is None: |
172 | attribute = obj.__name__ |
173 | |
174 | === modified file 'src/provisioningserver/drivers/__init__.py' |
175 | --- src/provisioningserver/drivers/__init__.py 2015-08-21 13:59:15 +0000 |
176 | +++ src/provisioningserver/drivers/__init__.py 2015-10-01 23:31:26 +0000 |
177 | @@ -133,6 +133,15 @@ |
178 | validate(setting_fields, JSON_SETTING_SCHEMA) |
179 | |
180 | |
181 | +def gen_power_types(): |
182 | + from provisioningserver.drivers.power import power_drivers_by_name |
183 | + for power_type in JSON_POWER_TYPE_PARAMETERS: |
184 | + driver = power_drivers_by_name.get(power_type['name']) |
185 | + if driver is not None: |
186 | + power_type['missing_packages'] = driver.detect_missing_packages() |
187 | + yield power_type |
188 | + |
189 | + |
190 | class Architecture: |
191 | |
192 | def __init__(self, name, description, pxealiases=None, |
193 | @@ -232,10 +241,6 @@ |
194 | """Registry for boot resource classes.""" |
195 | |
196 | |
197 | -class PowerTypeRegistry(Registry): |
198 | - """Registry for power type classes.""" |
199 | - |
200 | - |
201 | builtin_architectures = [ |
202 | Architecture(name="i386/generic", description="i386"), |
203 | Architecture(name="amd64/generic", description="amd64"), |
204 | @@ -268,8 +273,3 @@ |
205 | ] |
206 | for arch in builtin_architectures: |
207 | ArchitectureRegistry.register_item(arch.name, arch) |
208 | - |
209 | - |
210 | -builtin_power_types = JSON_POWER_TYPE_PARAMETERS |
211 | -for power_type in builtin_power_types: |
212 | - PowerTypeRegistry.register_item(power_type['name'], power_type) |
213 | |
214 | === modified file 'src/provisioningserver/drivers/hardware/apc.py' |
215 | --- src/provisioningserver/drivers/hardware/apc.py 2015-05-18 20:01:38 +0000 |
216 | +++ src/provisioningserver/drivers/hardware/apc.py 2015-10-01 23:31:26 +0000 |
217 | @@ -101,3 +101,7 @@ |
218 | elif power_state == APCState.ON: |
219 | return 'on' |
220 | raise APCException('Unknown power state: %r' % power_state) |
221 | + |
222 | + |
223 | +def required_package(): |
224 | + return ['snmpset', 'snmp'] |
225 | |
226 | === modified file 'src/provisioningserver/drivers/hardware/hmc.py' |
227 | --- src/provisioningserver/drivers/hardware/hmc.py 2015-07-07 21:23:57 +0000 |
228 | +++ src/provisioningserver/drivers/hardware/hmc.py 2015-10-01 23:31:26 +0000 |
229 | @@ -110,3 +110,7 @@ |
230 | elif power_state in HMCState.ON: |
231 | return 'on' |
232 | raise HMCException('Unknown power state: %s' % power_state) |
233 | + |
234 | + |
235 | +def required_package(): |
236 | + return ['chsysstate', 'HMC Management Software'] |
237 | |
238 | === modified file 'src/provisioningserver/drivers/power/__init__.py' |
239 | --- src/provisioningserver/drivers/power/__init__.py 2015-09-24 16:22:12 +0000 |
240 | +++ src/provisioningserver/drivers/power/__init__.py 2015-10-01 23:31:26 +0000 |
241 | @@ -195,6 +195,12 @@ |
242 | self.clock = reactor |
243 | |
244 | @abstractmethod |
245 | + def detect_missing_packages(self): |
246 | + """Implement this method for the actual implementation |
247 | + of the check for the driver's missing support packages. |
248 | + """ |
249 | + |
250 | + @abstractmethod |
251 | def power_on(self, system_id, **kwargs): |
252 | """Implement this method for the actual implementation |
253 | of the power on command. |
254 | @@ -313,7 +319,11 @@ |
255 | return schemas |
256 | |
257 | |
258 | +from provisioningserver.drivers.power.amt import AMTPowerDriver |
259 | from provisioningserver.drivers.power.apc import APCPowerDriver |
260 | +from provisioningserver.drivers.power.dli import DLIPowerDriver |
261 | +from provisioningserver.drivers.power.ether_wake import EtherWakePowerDriver |
262 | +from provisioningserver.drivers.power.fence_cdu import FenceCDUPowerDriver |
263 | from provisioningserver.drivers.power.hmc import HMCPowerDriver |
264 | from provisioningserver.drivers.power.ipmi import IPMIPowerDriver |
265 | from provisioningserver.drivers.power.msftocs import MicrosoftOCSPowerDriver |
266 | @@ -324,7 +334,7 @@ |
267 | from provisioningserver.drivers.power.virsh import VirshPowerDriver |
268 | from provisioningserver.drivers.power.vmware import VMwarePowerDriver |
269 | |
270 | -builtin_power_drivers = [ |
271 | +registered_power_drivers = [ |
272 | APCPowerDriver(), |
273 | HMCPowerDriver(), |
274 | IPMIPowerDriver(), |
275 | @@ -336,5 +346,16 @@ |
276 | VirshPowerDriver(), |
277 | VMwarePowerDriver(), |
278 | ] |
279 | -for driver in builtin_power_drivers: |
280 | +for driver in registered_power_drivers: |
281 | PowerDriverRegistry.register_item(driver.name, driver) |
282 | + |
283 | +unregistered_power_drivers = [ |
284 | + AMTPowerDriver(), |
285 | + DLIPowerDriver(), |
286 | + EtherWakePowerDriver(), |
287 | + FenceCDUPowerDriver(), |
288 | +] |
289 | +power_drivers_by_name = { |
290 | + d.name: d for d in |
291 | + registered_power_drivers + unregistered_power_drivers |
292 | +} |
293 | |
294 | === added file 'src/provisioningserver/drivers/power/amt.py' |
295 | --- src/provisioningserver/drivers/power/amt.py 1970-01-01 00:00:00 +0000 |
296 | +++ src/provisioningserver/drivers/power/amt.py 2015-10-01 23:31:26 +0000 |
297 | @@ -0,0 +1,40 @@ |
298 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
299 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
300 | + |
301 | +"""Template-based AMT Power Driver.""" |
302 | + |
303 | +str = None |
304 | + |
305 | +__metaclass__ = type |
306 | +__all__ = [] |
307 | + |
308 | +from provisioningserver.drivers.power import PowerDriver |
309 | +from provisioningserver.utils import shell |
310 | + |
311 | + |
312 | +REQUIRED_PACKAGES = [["amttool", "amtterm"], ["wsman", "wsmancli"]] |
313 | + |
314 | + |
315 | +class AMTPowerDriver(PowerDriver): |
316 | + name = 'amt' |
317 | + description = "AMT Power Driver." |
318 | + settings = [] |
319 | + |
320 | + def detect_missing_packages(self): |
321 | + missing_packages = [] |
322 | + # when this becomes a non-templated, registered power driver, we can |
323 | + # detect what version of AMT is on the Node to find out if wsman is |
324 | + # required (see amt.template). For now, we assume wsman is required |
325 | + for binary, package in REQUIRED_PACKAGES: |
326 | + if not shell.has_command_available(binary): |
327 | + missing_packages.append(package) |
328 | + return missing_packages |
329 | + |
330 | + def power_on(self, system_id, **kwargs): |
331 | + raise NotImplementedError |
332 | + |
333 | + def power_off(self, system_id, **kwargs): |
334 | + raise NotImplementedError |
335 | + |
336 | + def power_query(self, system_id, **kwargs): |
337 | + raise NotImplementedError |
338 | |
339 | === modified file 'src/provisioningserver/drivers/power/apc.py' |
340 | --- src/provisioningserver/drivers/power/apc.py 2015-07-02 21:24:06 +0000 |
341 | +++ src/provisioningserver/drivers/power/apc.py 2015-10-01 23:31:26 +0000 |
342 | @@ -17,8 +17,10 @@ |
343 | from provisioningserver.drivers.hardware.apc import ( |
344 | power_control_apc, |
345 | power_state_apc, |
346 | + required_package, |
347 | ) |
348 | from provisioningserver.drivers.power import PowerDriver |
349 | +from provisioningserver.utils import shell |
350 | |
351 | |
352 | def extract_apc_parameters(params): |
353 | @@ -34,6 +36,12 @@ |
354 | description = "APC Power Driver." |
355 | settings = [] |
356 | |
357 | + def detect_missing_packages(self): |
358 | + binary, package = required_package() |
359 | + if not shell.has_command_available(binary): |
360 | + return [package] |
361 | + return [] |
362 | + |
363 | def power_on(self, system_id, **kwargs): |
364 | """Power on Apc outlet.""" |
365 | power_change = 'on' |
366 | |
367 | === added file 'src/provisioningserver/drivers/power/dli.py' |
368 | --- src/provisioningserver/drivers/power/dli.py 1970-01-01 00:00:00 +0000 |
369 | +++ src/provisioningserver/drivers/power/dli.py 2015-10-01 23:31:26 +0000 |
370 | @@ -0,0 +1,32 @@ |
371 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
372 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
373 | + |
374 | +"""Template-based DLI Power Driver.""" |
375 | + |
376 | +str = None |
377 | + |
378 | +__metaclass__ = type |
379 | +__all__ = [] |
380 | + |
381 | +from provisioningserver.drivers.power import PowerDriver |
382 | +from provisioningserver.utils import shell |
383 | + |
384 | + |
385 | +class DLIPowerDriver(PowerDriver): |
386 | + name = 'dli' |
387 | + description = "DLI Power Driver." |
388 | + settings = [] |
389 | + |
390 | + def detect_missing_packages(self): |
391 | + if not shell.has_command_available('wget'): |
392 | + return ['wget'] |
393 | + return [] |
394 | + |
395 | + def power_on(self, system_id, **kwargs): |
396 | + raise NotImplementedError |
397 | + |
398 | + def power_off(self, system_id, **kwargs): |
399 | + raise NotImplementedError |
400 | + |
401 | + def power_query(self, system_id, **kwargs): |
402 | + raise NotImplementedError |
403 | |
404 | === added file 'src/provisioningserver/drivers/power/ether_wake.py' |
405 | --- src/provisioningserver/drivers/power/ether_wake.py 1970-01-01 00:00:00 +0000 |
406 | +++ src/provisioningserver/drivers/power/ether_wake.py 2015-10-01 23:31:26 +0000 |
407 | @@ -0,0 +1,34 @@ |
408 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
409 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
410 | + |
411 | +"""Template-based ether-wake Power Driver.""" |
412 | + |
413 | +str = None |
414 | + |
415 | +__metaclass__ = type |
416 | +__all__ = [] |
417 | + |
418 | +from provisioningserver.drivers.power import PowerDriver |
419 | +from provisioningserver.utils import shell |
420 | + |
421 | + |
422 | +class EtherWakePowerDriver(PowerDriver): |
423 | + name = 'ether_wake' |
424 | + description = "Ether-wake Power Driver." |
425 | + settings = [] |
426 | + |
427 | + def detect_missing_packages(self): |
428 | + if shell.has_command_available('wakeonlan') or \ |
429 | + shell.has_command_available('etherwake'): |
430 | + return [] |
431 | + # you need one or the other, not both |
432 | + return ['wakeonlan or etherwake'] |
433 | + |
434 | + def power_on(self, system_id, **kwargs): |
435 | + raise NotImplementedError |
436 | + |
437 | + def power_off(self, system_id, **kwargs): |
438 | + raise NotImplementedError |
439 | + |
440 | + def power_query(self, system_id, **kwargs): |
441 | + raise NotImplementedError |
442 | |
443 | === added file 'src/provisioningserver/drivers/power/fence_cdu.py' |
444 | --- src/provisioningserver/drivers/power/fence_cdu.py 1970-01-01 00:00:00 +0000 |
445 | +++ src/provisioningserver/drivers/power/fence_cdu.py 2015-10-01 23:31:26 +0000 |
446 | @@ -0,0 +1,32 @@ |
447 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
448 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
449 | + |
450 | +"""Template-based Fence CDU Power Driver.""" |
451 | + |
452 | +str = None |
453 | + |
454 | +__metaclass__ = type |
455 | +__all__ = [] |
456 | + |
457 | +from provisioningserver.drivers.power import PowerDriver |
458 | +from provisioningserver.utils import shell |
459 | + |
460 | + |
461 | +class FenceCDUPowerDriver(PowerDriver): |
462 | + name = 'fence_cdu' |
463 | + description = "Fence CDU Power Driver." |
464 | + settings = [] |
465 | + |
466 | + def detect_missing_packages(self): |
467 | + if not shell.has_command_available('fence_cdu'): |
468 | + return ['fence-agents'] |
469 | + return [] |
470 | + |
471 | + def power_on(self, system_id, **kwargs): |
472 | + raise NotImplementedError |
473 | + |
474 | + def power_off(self, system_id, **kwargs): |
475 | + raise NotImplementedError |
476 | + |
477 | + def power_query(self, system_id, **kwargs): |
478 | + raise NotImplementedError |
479 | |
480 | === modified file 'src/provisioningserver/drivers/power/hmc.py' |
481 | --- src/provisioningserver/drivers/power/hmc.py 2015-06-25 20:39:09 +0000 |
482 | +++ src/provisioningserver/drivers/power/hmc.py 2015-10-01 23:31:26 +0000 |
483 | @@ -17,8 +17,10 @@ |
484 | from provisioningserver.drivers.hardware.hmc import ( |
485 | power_control_hmc, |
486 | power_state_hmc, |
487 | + required_package, |
488 | ) |
489 | from provisioningserver.drivers.power import PowerDriver |
490 | +from provisioningserver.utils import shell |
491 | |
492 | |
493 | def extract_hmc_parameters(params): |
494 | @@ -36,6 +38,12 @@ |
495 | description = "IBM Hardware Management Console Power Driver." |
496 | settings = [] |
497 | |
498 | + def detect_missing_packages(self): |
499 | + binary, package = required_package() |
500 | + if not shell.has_command_available(binary): |
501 | + return [package] |
502 | + return [] |
503 | + |
504 | def power_on(self, system_id, **kwargs): |
505 | ip, username, password, server_name, lpar = ( |
506 | extract_hmc_parameters(kwargs)) |
507 | |
508 | === modified file 'src/provisioningserver/drivers/power/ipmi.py' |
509 | --- src/provisioningserver/drivers/power/ipmi.py 2015-08-27 05:52:33 +0000 |
510 | +++ src/provisioningserver/drivers/power/ipmi.py 2015-10-01 23:31:26 +0000 |
511 | @@ -26,6 +26,7 @@ |
512 | PowerDriver, |
513 | PowerFatalError, |
514 | ) |
515 | +from provisioningserver.utils import shell |
516 | from provisioningserver.utils.network import find_ip_via_arp |
517 | from provisioningserver.utils.shell import ( |
518 | call_and_check, |
519 | @@ -51,6 +52,11 @@ |
520 | description = "IPMI Power Driver." |
521 | settings = [] |
522 | |
523 | + def detect_missing_packages(self): |
524 | + if not shell.has_command_available('ipmipower'): |
525 | + return ['freeipmi-tools'] |
526 | + return [] |
527 | + |
528 | def get_c_environment(self): |
529 | env = os.environ.copy() |
530 | env['LC_ALL'] = 'C' |
531 | |
532 | === modified file 'src/provisioningserver/drivers/power/moonshot.py' |
533 | --- src/provisioningserver/drivers/power/moonshot.py 2015-08-26 17:26:37 +0000 |
534 | +++ src/provisioningserver/drivers/power/moonshot.py 2015-10-01 23:31:26 +0000 |
535 | @@ -20,6 +20,7 @@ |
536 | PowerDriver, |
537 | PowerFatalError, |
538 | ) |
539 | +from provisioningserver.utils import shell |
540 | from provisioningserver.utils.shell import ( |
541 | call_and_check, |
542 | ExternalProcessError, |
543 | @@ -32,6 +33,11 @@ |
544 | description = "Moonshot IPMI Power Driver." |
545 | settings = [] |
546 | |
547 | + def detect_missing_packages(self): |
548 | + if not shell.has_command_available('ipmipower'): |
549 | + return ['freeipmi-tools'] |
550 | + return [] |
551 | + |
552 | def _issue_ipmitool_command( |
553 | self, power_change, power_hwaddress=None, power_address=None, |
554 | power_user=None, power_pass=None, ipmitool=None, **extra): |
555 | |
556 | === modified file 'src/provisioningserver/drivers/power/mscm.py' |
557 | --- src/provisioningserver/drivers/power/mscm.py 2015-06-05 21:19:20 +0000 |
558 | +++ src/provisioningserver/drivers/power/mscm.py 2015-10-01 23:31:26 +0000 |
559 | @@ -35,6 +35,10 @@ |
560 | description = "Moonshot HP iLO Chassis Manager Power Driver." |
561 | settings = [] |
562 | |
563 | + def detect_missing_packages(self): |
564 | + # uses pure-python paramiko ssh client - nothing to look for! |
565 | + return [] |
566 | + |
567 | def power_on(self, system_id, **kwargs): |
568 | """Power on MSCM node.""" |
569 | host, username, password, node_id = extract_mscm_parameters(kwargs) |
570 | |
571 | === modified file 'src/provisioningserver/drivers/power/msftocs.py' |
572 | --- src/provisioningserver/drivers/power/msftocs.py 2015-07-02 22:29:11 +0000 |
573 | +++ src/provisioningserver/drivers/power/msftocs.py 2015-10-01 23:31:26 +0000 |
574 | @@ -36,6 +36,10 @@ |
575 | description = "MicrosoftOCS Power Driver." |
576 | settings = [] |
577 | |
578 | + def detect_missing_packages(self): |
579 | + # uses urllib2 http client - nothing to look for! |
580 | + return [] |
581 | + |
582 | def power_on(self, system_id, **kwargs): |
583 | """Power on MicrosoftOCS node.""" |
584 | power_change = 'on' |
585 | |
586 | === modified file 'src/provisioningserver/drivers/power/seamicro.py' |
587 | --- src/provisioningserver/drivers/power/seamicro.py 2015-07-18 20:16:36 +0000 |
588 | +++ src/provisioningserver/drivers/power/seamicro.py 2015-10-01 23:31:26 +0000 |
589 | @@ -23,6 +23,7 @@ |
590 | PowerDriver, |
591 | PowerFatalError, |
592 | ) |
593 | +from provisioningserver.utils import shell |
594 | from provisioningserver.utils.shell import ( |
595 | call_and_check, |
596 | ExternalProcessError, |
597 | @@ -44,6 +45,11 @@ |
598 | description = "SeaMicro Power Driver." |
599 | settings = [] |
600 | |
601 | + def detect_missing_packages(self): |
602 | + if not shell.has_command_available('ipmitool'): |
603 | + return ['ipmitool'] |
604 | + return [] |
605 | + |
606 | def _power_control_seamicro15k_ipmi( |
607 | self, ip, username, password, server_id, power_change): |
608 | """Power on/off SeaMicro node via ipmitool.""" |
609 | |
610 | === added file 'src/provisioningserver/drivers/power/tests/test_amt.py' |
611 | --- src/provisioningserver/drivers/power/tests/test_amt.py 1970-01-01 00:00:00 +0000 |
612 | +++ src/provisioningserver/drivers/power/tests/test_amt.py 2015-10-01 23:31:26 +0000 |
613 | @@ -0,0 +1,51 @@ |
614 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
615 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
616 | + |
617 | +"""Tests for `provisioningserver.drivers.power.amt`.""" |
618 | + |
619 | +from __future__ import ( |
620 | + absolute_import, |
621 | + print_function, |
622 | + unicode_literals, |
623 | + ) |
624 | + |
625 | +str = None |
626 | + |
627 | +__metaclass__ = type |
628 | +__all__ = [] |
629 | + |
630 | +from maastesting.testcase import MAASTestCase |
631 | +from provisioningserver.drivers.power import amt as amt_module |
632 | +from provisioningserver.utils.shell import has_command_available |
633 | + |
634 | + |
635 | +class TestAMTPowerDriver(MAASTestCase): |
636 | + |
637 | + def test_missing_packages(self): |
638 | + mock = self.patch(has_command_available) |
639 | + mock.return_value = False |
640 | + driver = amt_module.AMTPowerDriver() |
641 | + missing = driver.detect_missing_packages() |
642 | + self.assertItemsEqual(["amtterm", "wsmancli"], missing) |
643 | + |
644 | + def test_no_missing_packages(self): |
645 | + mock = self.patch(has_command_available) |
646 | + mock.return_value = True |
647 | + driver = amt_module.AMTPowerDriver() |
648 | + missing = driver.detect_missing_packages() |
649 | + self.assertItemsEqual([], missing) |
650 | + |
651 | + def test_power_on(self): |
652 | + driver = amt_module.AMTPowerDriver() |
653 | + self.assertRaises( |
654 | + NotImplementedError, driver.power_on, "fake_id") |
655 | + |
656 | + def test_power_off(self): |
657 | + driver = amt_module.AMTPowerDriver() |
658 | + self.assertRaises( |
659 | + NotImplementedError, driver.power_off, "fake_id") |
660 | + |
661 | + def test_power_query(self): |
662 | + driver = amt_module.AMTPowerDriver() |
663 | + self.assertRaises( |
664 | + NotImplementedError, driver.power_query, "fake_id") |
665 | |
666 | === modified file 'src/provisioningserver/drivers/power/tests/test_apc.py' |
667 | --- src/provisioningserver/drivers/power/tests/test_apc.py 2015-07-07 02:54:21 +0000 |
668 | +++ src/provisioningserver/drivers/power/tests/test_apc.py 2015-10-01 23:31:26 +0000 |
669 | @@ -24,11 +24,26 @@ |
670 | APCPowerDriver, |
671 | extract_apc_parameters, |
672 | ) |
673 | +from provisioningserver.utils.shell import has_command_available |
674 | from testtools.matchers import Equals |
675 | |
676 | |
677 | class TestAPCPowerDriver(MAASTestCase): |
678 | |
679 | + def test_missing_packages(self): |
680 | + mock = self.patch(has_command_available) |
681 | + mock.return_value = False |
682 | + driver = apc_module.APCPowerDriver() |
683 | + missing = driver.detect_missing_packages() |
684 | + self.assertItemsEqual(["snmp"], missing) |
685 | + |
686 | + def test_no_missing_packages(self): |
687 | + mock = self.patch(has_command_available) |
688 | + mock.return_value = True |
689 | + driver = apc_module.APCPowerDriver() |
690 | + missing = driver.detect_missing_packages() |
691 | + self.assertItemsEqual([], missing) |
692 | + |
693 | def make_parameters(self): |
694 | system_id = factory.make_name('system_id') |
695 | ip = factory.make_ipv4_address() |
696 | |
697 | === modified file 'src/provisioningserver/drivers/power/tests/test_base.py' |
698 | --- src/provisioningserver/drivers/power/tests/test_base.py 2015-08-21 17:50:25 +0000 |
699 | +++ src/provisioningserver/drivers/power/tests/test_base.py 2015-10-01 23:31:26 +0000 |
700 | @@ -253,6 +253,9 @@ |
701 | self.wait_time = wait_time |
702 | super(FakePowerDriver, self).__init__(clock) |
703 | |
704 | + def detect_missing_packages(self): |
705 | + raise NotImplementedError |
706 | + |
707 | def power_on(self, system_id, **kwargs): |
708 | raise NotImplementedError |
709 | |
710 | |
711 | === added file 'src/provisioningserver/drivers/power/tests/test_dli.py' |
712 | --- src/provisioningserver/drivers/power/tests/test_dli.py 1970-01-01 00:00:00 +0000 |
713 | +++ src/provisioningserver/drivers/power/tests/test_dli.py 2015-10-01 23:31:26 +0000 |
714 | @@ -0,0 +1,51 @@ |
715 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
716 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
717 | + |
718 | +"""Tests for `provisioningserver.drivers.power.dli`.""" |
719 | + |
720 | +from __future__ import ( |
721 | + absolute_import, |
722 | + print_function, |
723 | + unicode_literals, |
724 | + ) |
725 | + |
726 | +str = None |
727 | + |
728 | +__metaclass__ = type |
729 | +__all__ = [] |
730 | + |
731 | +from maastesting.testcase import MAASTestCase |
732 | +from provisioningserver.drivers.power import dli as dli_module |
733 | +from provisioningserver.utils.shell import has_command_available |
734 | + |
735 | + |
736 | +class TestDLIPowerDriver(MAASTestCase): |
737 | + |
738 | + def test_missing_packages(self): |
739 | + mock = self.patch(has_command_available) |
740 | + mock.return_value = False |
741 | + driver = dli_module.DLIPowerDriver() |
742 | + missing = driver.detect_missing_packages() |
743 | + self.assertItemsEqual(["wget"], missing) |
744 | + |
745 | + def test_no_missing_packages(self): |
746 | + mock = self.patch(has_command_available) |
747 | + mock.return_value = True |
748 | + driver = dli_module.DLIPowerDriver() |
749 | + missing = driver.detect_missing_packages() |
750 | + self.assertItemsEqual([], missing) |
751 | + |
752 | + def test_power_on(self): |
753 | + driver = dli_module.DLIPowerDriver() |
754 | + self.assertRaises( |
755 | + NotImplementedError, driver.power_on, "fake_id") |
756 | + |
757 | + def test_power_off(self): |
758 | + driver = dli_module.DLIPowerDriver() |
759 | + self.assertRaises( |
760 | + NotImplementedError, driver.power_off, "fake_id") |
761 | + |
762 | + def test_power_query(self): |
763 | + driver = dli_module.DLIPowerDriver() |
764 | + self.assertRaises( |
765 | + NotImplementedError, driver.power_query, "fake_id") |
766 | |
767 | === added file 'src/provisioningserver/drivers/power/tests/test_ether_wake.py' |
768 | --- src/provisioningserver/drivers/power/tests/test_ether_wake.py 1970-01-01 00:00:00 +0000 |
769 | +++ src/provisioningserver/drivers/power/tests/test_ether_wake.py 2015-10-01 23:31:26 +0000 |
770 | @@ -0,0 +1,51 @@ |
771 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
772 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
773 | + |
774 | +"""Tests for `provisioningserver.drivers.power.ether_wake`.""" |
775 | + |
776 | +from __future__ import ( |
777 | + absolute_import, |
778 | + print_function, |
779 | + unicode_literals, |
780 | + ) |
781 | + |
782 | +str = None |
783 | + |
784 | +__metaclass__ = type |
785 | +__all__ = [] |
786 | + |
787 | +from maastesting.testcase import MAASTestCase |
788 | +from provisioningserver.drivers.power import ether_wake as ether_wake_module |
789 | +from provisioningserver.utils.shell import has_command_available |
790 | + |
791 | + |
792 | +class TestEtherWakePowerDriver(MAASTestCase): |
793 | + |
794 | + def test_missing_packages(self): |
795 | + mock = self.patch(has_command_available) |
796 | + mock.return_value = False |
797 | + driver = ether_wake_module.EtherWakePowerDriver() |
798 | + missing = driver.detect_missing_packages() |
799 | + self.assertItemsEqual(["wakeonlan or etherwake"], missing) |
800 | + |
801 | + def test_no_missing_packages(self): |
802 | + mock = self.patch(has_command_available) |
803 | + mock.return_value = True |
804 | + driver = ether_wake_module.EtherWakePowerDriver() |
805 | + missing = driver.detect_missing_packages() |
806 | + self.assertItemsEqual([], missing) |
807 | + |
808 | + def test_power_on(self): |
809 | + driver = ether_wake_module.EtherWakePowerDriver() |
810 | + self.assertRaises( |
811 | + NotImplementedError, driver.power_on, "fake_id") |
812 | + |
813 | + def test_power_off(self): |
814 | + driver = ether_wake_module.EtherWakePowerDriver() |
815 | + self.assertRaises( |
816 | + NotImplementedError, driver.power_off, "fake_id") |
817 | + |
818 | + def test_power_query(self): |
819 | + driver = ether_wake_module.EtherWakePowerDriver() |
820 | + self.assertRaises( |
821 | + NotImplementedError, driver.power_query, "fake_id") |
822 | |
823 | === added file 'src/provisioningserver/drivers/power/tests/test_fence_cdu.py' |
824 | --- src/provisioningserver/drivers/power/tests/test_fence_cdu.py 1970-01-01 00:00:00 +0000 |
825 | +++ src/provisioningserver/drivers/power/tests/test_fence_cdu.py 2015-10-01 23:31:26 +0000 |
826 | @@ -0,0 +1,51 @@ |
827 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
828 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
829 | + |
830 | +"""Tests for `provisioningserver.drivers.power.fence_cdu`.""" |
831 | + |
832 | +from __future__ import ( |
833 | + absolute_import, |
834 | + print_function, |
835 | + unicode_literals, |
836 | + ) |
837 | + |
838 | +str = None |
839 | + |
840 | +__metaclass__ = type |
841 | +__all__ = [] |
842 | + |
843 | +from maastesting.testcase import MAASTestCase |
844 | +from provisioningserver.drivers.power import fence_cdu as fence_cdu_module |
845 | +from provisioningserver.utils.shell import has_command_available |
846 | + |
847 | + |
848 | +class TestFenceCDUPowerDriver(MAASTestCase): |
849 | + |
850 | + def test_missing_packages(self): |
851 | + mock = self.patch(has_command_available) |
852 | + mock.return_value = False |
853 | + driver = fence_cdu_module.FenceCDUPowerDriver() |
854 | + missing = driver.detect_missing_packages() |
855 | + self.assertItemsEqual(["fence-agents"], missing) |
856 | + |
857 | + def test_no_missing_packages(self): |
858 | + mock = self.patch(has_command_available) |
859 | + mock.return_value = True |
860 | + driver = fence_cdu_module.FenceCDUPowerDriver() |
861 | + missing = driver.detect_missing_packages() |
862 | + self.assertItemsEqual([], missing) |
863 | + |
864 | + def test_power_on(self): |
865 | + driver = fence_cdu_module.FenceCDUPowerDriver() |
866 | + self.assertRaises( |
867 | + NotImplementedError, driver.power_on, "fake_id") |
868 | + |
869 | + def test_power_off(self): |
870 | + driver = fence_cdu_module.FenceCDUPowerDriver() |
871 | + self.assertRaises( |
872 | + NotImplementedError, driver.power_off, "fake_id") |
873 | + |
874 | + def test_power_query(self): |
875 | + driver = fence_cdu_module.FenceCDUPowerDriver() |
876 | + self.assertRaises( |
877 | + NotImplementedError, driver.power_query, "fake_id") |
878 | |
879 | === modified file 'src/provisioningserver/drivers/power/tests/test_hmc.py' |
880 | --- src/provisioningserver/drivers/power/tests/test_hmc.py 2015-07-07 01:39:59 +0000 |
881 | +++ src/provisioningserver/drivers/power/tests/test_hmc.py 2015-10-01 23:31:26 +0000 |
882 | @@ -22,11 +22,26 @@ |
883 | extract_hmc_parameters, |
884 | HMCPowerDriver, |
885 | ) |
886 | +from provisioningserver.utils.shell import has_command_available |
887 | from testtools.matchers import Equals |
888 | |
889 | |
890 | class TestHMCPowerDriver(MAASTestCase): |
891 | |
892 | + def test_missing_packages(self): |
893 | + mock = self.patch(has_command_available) |
894 | + mock.return_value = False |
895 | + driver = hmc_module.HMCPowerDriver() |
896 | + missing = driver.detect_missing_packages() |
897 | + self.assertItemsEqual(['HMC Management Software'], missing) |
898 | + |
899 | + def test_no_missing_packages(self): |
900 | + mock = self.patch(has_command_available) |
901 | + mock.return_value = True |
902 | + driver = hmc_module.HMCPowerDriver() |
903 | + missing = driver.detect_missing_packages() |
904 | + self.assertItemsEqual([], missing) |
905 | + |
906 | def make_parameters(self): |
907 | system_id = factory.make_name('system_id') |
908 | ip = factory.make_name('power_address') |
909 | |
910 | === modified file 'src/provisioningserver/drivers/power/tests/test_ipmi.py' |
911 | --- src/provisioningserver/drivers/power/tests/test_ipmi.py 2015-08-27 05:52:33 +0000 |
912 | +++ src/provisioningserver/drivers/power/tests/test_ipmi.py 2015-10-01 23:31:26 +0000 |
913 | @@ -36,7 +36,10 @@ |
914 | IPMI_CONFIG, |
915 | IPMIPowerDriver, |
916 | ) |
917 | -from provisioningserver.utils.shell import ExternalProcessError |
918 | +from provisioningserver.utils.shell import ( |
919 | + ExternalProcessError, |
920 | + has_command_available, |
921 | +) |
922 | from testtools.matchers import ( |
923 | Contains, |
924 | Equals, |
925 | @@ -89,6 +92,20 @@ |
926 | |
927 | class TestIPMIPowerDriver(MAASTestCase): |
928 | |
929 | + def test_missing_packages(self): |
930 | + mock = self.patch(has_command_available) |
931 | + mock.return_value = False |
932 | + driver = ipmi_module.IPMIPowerDriver() |
933 | + missing = driver.detect_missing_packages() |
934 | + self.assertItemsEqual(['freeipmi-tools'], missing) |
935 | + |
936 | + def test_no_missing_packages(self): |
937 | + mock = self.patch(has_command_available) |
938 | + mock.return_value = True |
939 | + driver = ipmi_module.IPMIPowerDriver() |
940 | + missing = driver.detect_missing_packages() |
941 | + self.assertItemsEqual([], missing) |
942 | + |
943 | def test__finds_power_address_from_mac_address(self): |
944 | (power_address, power_user, power_pass, power_driver, power_off_mode, |
945 | ipmipower, ipmi_chassis_config, params) = make_parameters() |
946 | |
947 | === modified file 'src/provisioningserver/drivers/power/tests/test_moonshot.py' |
948 | --- src/provisioningserver/drivers/power/tests/test_moonshot.py 2015-08-26 17:26:37 +0000 |
949 | +++ src/provisioningserver/drivers/power/tests/test_moonshot.py 2015-10-01 23:31:26 +0000 |
950 | @@ -23,7 +23,10 @@ |
951 | PowerFatalError, |
952 | ) |
953 | from provisioningserver.drivers.power.moonshot import MoonshotIPMIPowerDriver |
954 | -from provisioningserver.utils.shell import ExternalProcessError |
955 | +from provisioningserver.utils.shell import ( |
956 | + ExternalProcessError, |
957 | + has_command_available, |
958 | +) |
959 | from testtools.matchers import Equals |
960 | |
961 | |
962 | @@ -48,6 +51,20 @@ |
963 | |
964 | class TestMoonshotIPMIPowerDriver(MAASTestCase): |
965 | |
966 | + def test_missing_packages(self): |
967 | + mock = self.patch(has_command_available) |
968 | + mock.return_value = False |
969 | + driver = moonshot_module.MoonshotIPMIPowerDriver() |
970 | + missing = driver.detect_missing_packages() |
971 | + self.assertItemsEqual(['freeipmi-tools'], missing) |
972 | + |
973 | + def test_no_missing_packages(self): |
974 | + mock = self.patch(has_command_available) |
975 | + mock.return_value = True |
976 | + driver = moonshot_module.MoonshotIPMIPowerDriver() |
977 | + missing = driver.detect_missing_packages() |
978 | + self.assertItemsEqual([], missing) |
979 | + |
980 | def test__issue_ipmitool_command_issues_power_on(self): |
981 | params = make_parameters() |
982 | power_change = 'on' |
983 | |
984 | === modified file 'src/provisioningserver/drivers/power/tests/test_mscm.py' |
985 | --- src/provisioningserver/drivers/power/tests/test_mscm.py 2015-07-13 18:15:20 +0000 |
986 | +++ src/provisioningserver/drivers/power/tests/test_mscm.py 2015-10-01 23:31:26 +0000 |
987 | @@ -28,6 +28,12 @@ |
988 | |
989 | class TestMSCMPowerDriver(MAASTestCase): |
990 | |
991 | + def test_missing_packages(self): |
992 | + # there's nothing to check for, just confirm it returns [] |
993 | + driver = mscm_module.MSCMPowerDriver() |
994 | + missing = driver.detect_missing_packages() |
995 | + self.assertItemsEqual([], missing) |
996 | + |
997 | def make_parameters(self): |
998 | system_id = factory.make_name('system_id') |
999 | host = factory.make_name('power_address') |
1000 | |
1001 | === modified file 'src/provisioningserver/drivers/power/tests/test_msftocs.py' |
1002 | --- src/provisioningserver/drivers/power/tests/test_msftocs.py 2015-07-07 02:34:03 +0000 |
1003 | +++ src/provisioningserver/drivers/power/tests/test_msftocs.py 2015-10-01 23:31:26 +0000 |
1004 | @@ -27,6 +27,12 @@ |
1005 | |
1006 | class TestMicrosoftOCSPowerDriver(MAASTestCase): |
1007 | |
1008 | + def test_missing_packages(self): |
1009 | + # there's nothing to check for, just confirm it returns [] |
1010 | + driver = msftocs_module.MicrosoftOCSPowerDriver() |
1011 | + missing = driver.detect_missing_packages() |
1012 | + self.assertItemsEqual([], missing) |
1013 | + |
1014 | def make_parameters(self): |
1015 | system_id = factory.make_name('system_id') |
1016 | ip = factory.make_name('power_address') |
1017 | |
1018 | === modified file 'src/provisioningserver/drivers/power/tests/test_seamicro.py' |
1019 | --- src/provisioningserver/drivers/power/tests/test_seamicro.py 2015-07-18 20:05:39 +0000 |
1020 | +++ src/provisioningserver/drivers/power/tests/test_seamicro.py 2015-10-01 23:31:26 +0000 |
1021 | @@ -27,12 +27,29 @@ |
1022 | extract_seamicro_parameters, |
1023 | SeaMicroPowerDriver, |
1024 | ) |
1025 | -from provisioningserver.utils.shell import ExternalProcessError |
1026 | +from provisioningserver.utils.shell import ( |
1027 | + ExternalProcessError, |
1028 | + has_command_available, |
1029 | +) |
1030 | from testtools.matchers import Equals |
1031 | |
1032 | |
1033 | class TestSeaMicroPowerDriver(MAASTestCase): |
1034 | |
1035 | + def test_missing_packages(self): |
1036 | + mock = self.patch(has_command_available) |
1037 | + mock.return_value = False |
1038 | + driver = seamicro_module.SeaMicroPowerDriver() |
1039 | + missing = driver.detect_missing_packages() |
1040 | + self.assertItemsEqual(['ipmitool'], missing) |
1041 | + |
1042 | + def test_no_missing_packages(self): |
1043 | + mock = self.patch(has_command_available) |
1044 | + mock.return_value = True |
1045 | + driver = seamicro_module.SeaMicroPowerDriver() |
1046 | + missing = driver.detect_missing_packages() |
1047 | + self.assertItemsEqual([], missing) |
1048 | + |
1049 | def make_parameters(self): |
1050 | ip = factory.make_name('power_address') |
1051 | username = factory.make_name('power_user') |
1052 | |
1053 | === modified file 'src/provisioningserver/drivers/power/tests/test_ucsm.py' |
1054 | --- src/provisioningserver/drivers/power/tests/test_ucsm.py 2015-07-13 18:22:16 +0000 |
1055 | +++ src/provisioningserver/drivers/power/tests/test_ucsm.py 2015-10-01 23:31:26 +0000 |
1056 | @@ -27,6 +27,12 @@ |
1057 | |
1058 | class TestUCSMPowerDriver(MAASTestCase): |
1059 | |
1060 | + def test_missing_packages(self): |
1061 | + # there's nothing to check for, just confirm it returns [] |
1062 | + driver = ucsm_module.UCSMPowerDriver() |
1063 | + missing = driver.detect_missing_packages() |
1064 | + self.assertItemsEqual([], missing) |
1065 | + |
1066 | def make_parameters(self): |
1067 | system_id = factory.make_name('system_id') |
1068 | url = factory.make_name('power_address') |
1069 | |
1070 | === modified file 'src/provisioningserver/drivers/power/tests/test_virsh.py' |
1071 | --- src/provisioningserver/drivers/power/tests/test_virsh.py 2015-07-09 01:11:44 +0000 |
1072 | +++ src/provisioningserver/drivers/power/tests/test_virsh.py 2015-10-01 23:31:26 +0000 |
1073 | @@ -22,11 +22,26 @@ |
1074 | extract_virsh_parameters, |
1075 | VirshPowerDriver, |
1076 | ) |
1077 | +from provisioningserver.utils.shell import has_command_available |
1078 | from testtools.matchers import Equals |
1079 | |
1080 | |
1081 | class TestVirshPowerDriver(MAASTestCase): |
1082 | |
1083 | + def test_missing_packages(self): |
1084 | + mock = self.patch(has_command_available) |
1085 | + mock.return_value = False |
1086 | + driver = virsh_module.VirshPowerDriver() |
1087 | + missing = driver.detect_missing_packages() |
1088 | + self.assertItemsEqual(['libvirt-bin'], missing) |
1089 | + |
1090 | + def test_no_missing_packages(self): |
1091 | + mock = self.patch(has_command_available) |
1092 | + mock.return_value = True |
1093 | + driver = virsh_module.VirshPowerDriver() |
1094 | + missing = driver.detect_missing_packages() |
1095 | + self.assertItemsEqual([], missing) |
1096 | + |
1097 | def make_parameters(self): |
1098 | system_id = factory.make_name('system_id') |
1099 | poweraddr = factory.make_name('power_address') |
1100 | |
1101 | === modified file 'src/provisioningserver/drivers/power/tests/test_vmware.py' |
1102 | --- src/provisioningserver/drivers/power/tests/test_vmware.py 2015-07-07 02:07:39 +0000 |
1103 | +++ src/provisioningserver/drivers/power/tests/test_vmware.py 2015-10-01 23:31:26 +0000 |
1104 | @@ -17,6 +17,7 @@ |
1105 | from maastesting.factory import factory |
1106 | from maastesting.matchers import MockCalledOnceWith |
1107 | from maastesting.testcase import MAASTestCase |
1108 | +from provisioningserver.drivers.hardware.vmware import try_pyvmomi_import |
1109 | from provisioningserver.drivers.power import vmware as vmware_module |
1110 | from provisioningserver.drivers.power.vmware import ( |
1111 | extract_vmware_parameters, |
1112 | @@ -27,6 +28,20 @@ |
1113 | |
1114 | class TestVMwarePowerDriver(MAASTestCase): |
1115 | |
1116 | + def test_missing_packages(self): |
1117 | + mock = self.patch(try_pyvmomi_import) |
1118 | + mock.return_value = False |
1119 | + driver = vmware_module.VMwarePowerDriver() |
1120 | + missing = driver.detect_missing_packages() |
1121 | + self.assertItemsEqual(["python-pyvmomi"], missing) |
1122 | + |
1123 | + def test_no_missing_packages(self): |
1124 | + mock = self.patch(try_pyvmomi_import) |
1125 | + mock.return_value = True |
1126 | + driver = vmware_module.VMwarePowerDriver() |
1127 | + missing = driver.detect_missing_packages() |
1128 | + self.assertItemsEqual([], missing) |
1129 | + |
1130 | def make_parameters(self): |
1131 | system_id = factory.make_name('system_id') |
1132 | host = factory.make_name('power_address') |
1133 | |
1134 | === modified file 'src/provisioningserver/drivers/power/ucsm.py' |
1135 | --- src/provisioningserver/drivers/power/ucsm.py 2015-07-13 18:22:16 +0000 |
1136 | +++ src/provisioningserver/drivers/power/ucsm.py 2015-10-01 23:31:26 +0000 |
1137 | @@ -35,6 +35,10 @@ |
1138 | description = "Cisco UCS Power Driver." |
1139 | settings = [] |
1140 | |
1141 | + def detect_missing_packages(self): |
1142 | + # uses urllib2 http client - nothing to look for! |
1143 | + return [] |
1144 | + |
1145 | def power_on(self, system_id, **kwargs): |
1146 | """Power on UCSM node.""" |
1147 | url, username, password, uuid = extract_ucsm_parameters(kwargs) |
1148 | |
1149 | === modified file 'src/provisioningserver/drivers/power/virsh.py' |
1150 | --- src/provisioningserver/drivers/power/virsh.py 2015-06-23 16:45:25 +0000 |
1151 | +++ src/provisioningserver/drivers/power/virsh.py 2015-10-01 23:31:26 +0000 |
1152 | @@ -19,6 +19,11 @@ |
1153 | power_state_virsh, |
1154 | ) |
1155 | from provisioningserver.drivers.power import PowerDriver |
1156 | +from provisioningserver.utils import shell |
1157 | + |
1158 | + |
1159 | +REQUIRED_PACKAGES = [["virsh", "libvirt-bin"], |
1160 | + ["virt-login-shell", "libvirt-bin"]] |
1161 | |
1162 | |
1163 | def extract_virsh_parameters(params): |
1164 | @@ -34,6 +39,13 @@ |
1165 | description = "Virsh Power Driver." |
1166 | settings = [] |
1167 | |
1168 | + def detect_missing_packages(self): |
1169 | + missing_packages = set() |
1170 | + for binary, package in REQUIRED_PACKAGES: |
1171 | + if not shell.has_command_available(binary): |
1172 | + missing_packages.add(package) |
1173 | + return list(missing_packages) |
1174 | + |
1175 | def power_on(self, system_id, **kwargs): |
1176 | """Power on Virsh node.""" |
1177 | power_change = 'on' |
1178 | |
1179 | === modified file 'src/provisioningserver/drivers/power/vmware.py' |
1180 | --- src/provisioningserver/drivers/power/vmware.py 2015-06-30 22:28:33 +0000 |
1181 | +++ src/provisioningserver/drivers/power/vmware.py 2015-10-01 23:31:26 +0000 |
1182 | @@ -14,6 +14,7 @@ |
1183 | __metaclass__ = type |
1184 | __all__ = [] |
1185 | |
1186 | +from provisioningserver.drivers.hardware import vmware |
1187 | from provisioningserver.drivers.hardware.vmware import ( |
1188 | power_control_vmware, |
1189 | power_query_vmware, |
1190 | @@ -38,6 +39,11 @@ |
1191 | description = "VMware Power Driver." |
1192 | settings = [] |
1193 | |
1194 | + def detect_missing_packages(self): |
1195 | + if not vmware.try_pyvmomi_import(): |
1196 | + return ["python-pyvmomi"] |
1197 | + return [] |
1198 | + |
1199 | def power_on(self, system_id, **kwargs): |
1200 | """Power on VMware node.""" |
1201 | power_change = 'on' |
1202 | |
1203 | === modified file 'src/provisioningserver/drivers/tests/test_base.py' |
1204 | --- src/provisioningserver/drivers/tests/test_base.py 2015-05-07 18:14:38 +0000 |
1205 | +++ src/provisioningserver/drivers/tests/test_base.py 2015-10-01 23:31:26 +0000 |
1206 | @@ -31,7 +31,6 @@ |
1207 | BootResourceRegistry, |
1208 | JSON_SETTING_SCHEMA, |
1209 | make_setting_field, |
1210 | - PowerTypeRegistry, |
1211 | SETTING_PARAMETER_FIELD_SCHEMA, |
1212 | validate_settings, |
1213 | ) |
1214 | @@ -153,9 +152,37 @@ |
1215 | self.assertEqual( |
1216 | None, ArchitectureRegistry.get_by_pxealias("stinkywinky")) |
1217 | |
1218 | - def test_power_type_registry(self): |
1219 | - self.assertItemsEqual([], PowerTypeRegistry) |
1220 | - PowerTypeRegistry.register_item("resource", sentinel.resource) |
1221 | - self.assertIn( |
1222 | - sentinel.resource, |
1223 | - (item for name, item in PowerTypeRegistry)) |
1224 | + def test_gen_power_types(self): |
1225 | + |
1226 | + from provisioningserver.drivers import power |
1227 | + from provisioningserver.power import schema |
1228 | + |
1229 | + class TestGenPowerTypesPowerDriver(power.PowerDriver): |
1230 | + name = 'test_gen_power_types' |
1231 | + description = "test_gen_power_types Power Driver." |
1232 | + settings = [] |
1233 | + |
1234 | + def detect_missing_packages(self): |
1235 | + # these packages are forever missing |
1236 | + return ['fake-package-one', 'fake-package-two'] |
1237 | + |
1238 | + def power_on(self, system_id, **kwargs): |
1239 | + raise NotImplementedError |
1240 | + |
1241 | + def power_off(self, system_id, **kwargs): |
1242 | + raise NotImplementedError |
1243 | + |
1244 | + def power_query(self, system_id, **kwargs): |
1245 | + raise NotImplementedError |
1246 | + |
1247 | + # add my fake driver |
1248 | + driver = TestGenPowerTypesPowerDriver() |
1249 | + power.power_drivers_by_name[driver.name] = driver |
1250 | + schema.JSON_POWER_TYPE_PARAMETERS += [{'name': "test_gen_power_types"}] |
1251 | + |
1252 | + # make sure fake packages are reported missing |
1253 | + power_types = list(drivers.gen_power_types()) |
1254 | + self.assertEqual(15, len(power_types)) |
1255 | + self.assertItemsEqual( |
1256 | + ['fake-package-one', 'fake-package-two'], |
1257 | + power_types[-1].get('missing_packages')) |
1258 | |
1259 | === modified file 'src/provisioningserver/power/schema.py' |
1260 | --- src/provisioningserver/power/schema.py 2015-08-21 13:59:15 +0000 |
1261 | +++ src/provisioningserver/power/schema.py 2015-10-01 23:31:26 +0000 |
1262 | @@ -96,6 +96,12 @@ |
1263 | 'description': { |
1264 | 'type': 'string', |
1265 | }, |
1266 | + 'missing_packages': { |
1267 | + 'type': 'array', |
1268 | + 'items': { |
1269 | + 'type': 'string', |
1270 | + }, |
1271 | + }, |
1272 | 'fields': { |
1273 | 'type': 'array', |
1274 | 'items': POWER_TYPE_PARAMETER_FIELD_SCHEMA, |
1275 | |
1276 | === modified file 'src/provisioningserver/rpc/clusterservice.py' |
1277 | --- src/provisioningserver/rpc/clusterservice.py 2015-09-24 16:22:12 +0000 |
1278 | +++ src/provisioningserver/rpc/clusterservice.py 2015-10-01 23:31:26 +0000 |
1279 | @@ -29,7 +29,7 @@ |
1280 | from provisioningserver.config import ClusterConfiguration |
1281 | from provisioningserver.drivers import ( |
1282 | ArchitectureRegistry, |
1283 | - PowerTypeRegistry, |
1284 | + gen_power_types, |
1285 | ) |
1286 | from provisioningserver.drivers.hardware.mscm import probe_and_enlist_mscm |
1287 | from provisioningserver.drivers.hardware.msftocs import ( |
1288 | @@ -193,7 +193,7 @@ |
1289 | :py:class:`~provisioningserver.rpc.cluster.DescribePowerTypes`. |
1290 | """ |
1291 | return { |
1292 | - 'power_types': [item for name, item in PowerTypeRegistry], |
1293 | + 'power_types': list(gen_power_types()), |
1294 | } |
1295 | |
1296 | @cluster.ListSupportedArchitectures.responder |
The implementation looks sound. Really like how you did it. Got some comments, the biggest issue is the missing unit tests. You are missing unit tests for all of the power drivers. Please add unit tests on each to test the newly added methods functionality.