Merge lp:~ltrager/maas/power_for_rackcontrollers into lp:~maas-committers/maas/trunk
- power_for_rackcontrollers
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Lee Trager | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 4983 | ||||
Proposed branch: | lp:~ltrager/maas/power_for_rackcontrollers | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Diff against target: |
2353 lines (+825/-715) 24 files modified
src/maasserver/api/devices.py (+0/-16) src/maasserver/api/machines.py (+4/-158) src/maasserver/api/nodes.py (+176/-22) src/maasserver/api/rackcontrollers.py (+4/-2) src/maasserver/api/support.py (+6/-5) src/maasserver/api/tests/test_machine.py (+1/-272) src/maasserver/api/tests/test_machines.py (+0/-53) src/maasserver/api/tests/test_node.py (+265/-20) src/maasserver/api/tests/test_nodes.py (+63/-0) src/maasserver/models/node.py (+6/-4) src/maasserver/models/tests/test_node.py (+9/-1) src/maasserver/node_action.py (+21/-18) src/maasserver/static/js/angular/controllers/node_details.js (+1/-1) src/maasserver/static/js/angular/controllers/nodes_list.js (+3/-2) src/maasserver/static/js/angular/controllers/tests/test_node_details.js (+1/-1) src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js (+5/-1) src/maasserver/static/js/angular/factories/general.js (+18/-4) src/maasserver/static/js/angular/factories/tests/test_general.js (+107/-68) src/maasserver/testing/sampledata.py (+34/-0) src/maasserver/tests/test_node_action.py (+9/-1) src/maasserver/websockets/handlers/general.py (+37/-33) src/maasserver/websockets/handlers/tests/test_device.py (+2/-2) src/maasserver/websockets/handlers/tests/test_general.py (+47/-28) src/maasserver/websockets/handlers/tests/test_machine.py (+6/-3) |
||||
To merge this branch: | bzr merge lp:~ltrager/maas/power_for_rackcontrollers | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
Gavin Panella (community) | Approve | ||
Review via email: mp+292615@code.launchpad.net |
Commit message
Add the ability to power rack controllers on and off through the API or UI
Description of the change
This adds the ability to power rack controllers on and off through the API or UI. The power options(power-on, power-off, query-power-state, mark-fixed, mark-broken) have all been moved onto the node API and are inherited by the machine API. I added a check on each of those calls to make sure they can only be used by machines and rack controllers. On the device and region API endpoints I've disabled these calls.
MAAS chooses whether a system should PXE or local boot based on its status. Because of this rack controllers now have their status set to deployed when they connect.
Lee Trager (ltrager) wrote : | # |
Since all the power commands were originally on the node API endpoint I figured I'd add them back there now that two different node types support power options. This will expand on the concept that the node API is used for general options and be one less thing for people migrating from the 1.0 API to worry about. I disabled them on devices and region controllers as you pointed out since we currently don't support power options on those node types. I could do the mixin thing you are suggestion but then the node API endpoint won't have these options.
If we want to have the power options on the node endpoint we'll have to disable the power commands as I currently do. If we want the power operations only on the machine and rack controller endpoints I can do the mixin as you suggest.
Gavin Panella (allenap) wrote : | # |
I don't think it's appropriate to have power control operations on the
generic node/nodes endpoint. That should probably only service the
common subset of operations.
Andres Rodriguez (andreserl) wrote : | # |
Agree with Gavin!
On Tuesday, April 26, 2016, Gavin Panella <email address hidden>
wrote:
> I don't think it's appropriate to have power control operations on the
> generic node/nodes endpoint. That should probably only service the
> common subset of operations.
>
> --
>
> https:/
> You are subscribed to branch lp:maas.
>
--
Andres Rodriguez (RoAkSoAx)
Ubuntu Server Developer
MSc. Telecom & Networking
Systems Engineer
Lee Trager (ltrager) wrote : | # |
I've moved all power commands to two new mixin classes, PowerMixin, and PowersMixin. Like the API endpoints the singular is used for individual object endpoints while the plural is for endpoints of multiple objects. Power commands now only show up for machines and rack-controllers but could be easily added to other node types.
I came across a bug in OperationsHandl
endpoints class, or its direct parent, would be automatically added to its exports list. Blake ran into the same bug when writing OwnerDataMixin. Since that mixin only has one method he redefined set_owner_data and called super(). As PowerMixin has a number of methods I modified support.py to add exportable methods from any base class, which are not defined as None in the child class, to the exports list. As this fixed the problem Blake was seeing I removed the set_owner_data methods which were just calling super().
Andres Rodriguez (andreserl) wrote : | # |
Hold on. Let's first discuss this before you move forward. Let's do it
tomorrow at the standup!
On Wednesday, April 27, 2016, Lee Trager <email address hidden> wrote:
> I've moved all power commands to two new mixin classes, PowerMixin, and
> PowersMixin. Like the API endpoints the singular is used for individual
> object endpoints while the plural is for endpoints of multiple objects.
> Power commands now only show up for machines and rack-controllers but could
> be easily added to other node types.
>
> I came across a bug in OperationsHandl
> an
> endpoints class, or its direct parent, would be automatically added to its
> exports list. Blake ran into the same bug when writing OwnerDataMixin.
> Since that mixin only has one method he redefined set_owner_data and called
> super(). As PowerMixin has a number of methods I modified support.py to add
> exportable methods from any base class, which are not defined as None in
> the child class, to the exports list. As this fixed the problem Blake was
> seeing I removed the set_owner_data methods which were just calling super().
> --
>
> https:/
> You are subscribed to branch lp:maas.
>
--
Andres Rodriguez (RoAkSoAx)
Ubuntu Server Developer
MSc. Telecom & Networking
Systems Engineer
Gavin Panella (allenap) wrote : | # |
Looks good. A few small comments, that's all. Thanks for making those changes.
Lee Trager (ltrager) wrote : | # |
As per the discussion on IRC the rack controller status is no longer changed. Instead I modified the power commands to always boot locally when not a machine.
While testing I realized that all power options were being displayed in the UI on all controller types. This was because MAAS sent one action list for all controllers. I've split this into region, rack, and region and rack action lists. Power options are now only listed when only rack controllers are selected.
Blake Rouse (blake-rouse) wrote : | # |
Lets fix the node actions like we discussed in the hangout.
Blake Rouse (blake-rouse) wrote : | # |
Okay this looks better, thanks for all the fixes. I know this branch was a lot of work.
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~ltrager/maas/power_for_rackcontrollers into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Get:2 http://
Get:3 http://
Hit:4 http://
Fetched 185 kB in 0s (417 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu3).
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
build-essential is already the newest version (12.1ubuntu2).
bzr is already the newest version (2.7.0-2ubuntu1).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu12).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
libpq-dev is already the newest version (9.5.2-1).
make is already the newest version (4.1-6).
postgresql is already the newest v...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~ltrager/maas/power_for_rackcontrollers into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Get:2 http://
Get:3 http://
Hit:4 http://
Fetched 185 kB in 0s (384 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu3).
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
build-essential is already the newest version (12.1ubuntu2).
bzr is already the newest version (2.7.0-2ubuntu1).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu12).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
libpq-dev is already the newest version (9.5.2-1).
make is already the newest version (4.1-6).
postgresql is already the newest v...
Preview Diff
1 | === modified file 'src/maasserver/api/devices.py' |
2 | --- src/maasserver/api/devices.py 2016-04-14 15:25:36 +0000 |
3 | +++ src/maasserver/api/devices.py 2016-04-29 23:54:15 +0000 |
4 | @@ -12,7 +12,6 @@ |
5 | NodesHandler, |
6 | OwnerDataMixin, |
7 | ) |
8 | -from maasserver.api.support import operation |
9 | from maasserver.enum import NODE_PERMISSION |
10 | from maasserver.exceptions import MAASAPIValidationError |
11 | from maasserver.forms import ( |
12 | @@ -117,21 +116,6 @@ |
13 | device.delete() |
14 | return rc.DELETED |
15 | |
16 | - @operation(idempotent=False) |
17 | - def set_owner_data(self, request, system_id): |
18 | - """Set key/value data for the current owner. |
19 | - |
20 | - Pass any key/value data to this method to add, modify, or remove. A key |
21 | - is removed when the value for that key is set to an empty string. |
22 | - |
23 | - This operation will not remove any previous keys unless explicitly |
24 | - passed with an empty string. |
25 | - |
26 | - Returns 404 if the device is not found. |
27 | - Returns 403 if the user does not have permission. |
28 | - """ |
29 | - return super().set_owner_data(request, system_id) |
30 | - |
31 | @classmethod |
32 | def resource_uri(cls, device=None): |
33 | # This method is called by piston in two different contexts: |
34 | |
35 | === modified file 'src/maasserver/api/machines.py' |
36 | --- src/maasserver/api/machines.py 2016-04-15 22:14:33 +0000 |
37 | +++ src/maasserver/api/machines.py 2016-04-29 23:54:15 +0000 |
38 | @@ -8,7 +8,6 @@ |
39 | "get_storage_layout_params", |
40 | ] |
41 | |
42 | -from base64 import b64decode |
43 | import re |
44 | |
45 | from django.conf import settings |
46 | @@ -29,6 +28,8 @@ |
47 | NodeHandler, |
48 | NodesHandler, |
49 | OwnerDataMixin, |
50 | + PowerMixin, |
51 | + PowersMixin, |
52 | store_node_power_parameters, |
53 | ) |
54 | from maasserver.api.support import ( |
55 | @@ -50,7 +51,6 @@ |
56 | MAASAPIValidationError, |
57 | NodesNotAvailable, |
58 | NodeStateViolation, |
59 | - StaticIPAddressExhaustion, |
60 | Unauthorized, |
61 | ) |
62 | from maasserver.forms import ( |
63 | @@ -178,7 +178,7 @@ |
64 | return storage_layout, params |
65 | |
66 | |
67 | -class MachineHandler(NodeHandler, OwnerDataMixin): |
68 | +class MachineHandler(NodeHandler, OwnerDataMixin, PowerMixin): |
69 | """Manage an individual Machine. |
70 | |
71 | The Machine is identified by its system_id. |
72 | @@ -278,36 +278,6 @@ |
73 | return ('machine_handler', (machine_system_id, )) |
74 | |
75 | @operation(idempotent=False) |
76 | - def power_off(self, request, system_id): |
77 | - """Power off a machine. |
78 | - |
79 | - :param stop_mode: An optional power off mode. If 'soft', |
80 | - perform a soft power down if the machine's power type supports |
81 | - it, otherwise perform a hard power off. For all values other |
82 | - than 'soft', and by default, perform a hard power off. A |
83 | - soft power off generally asks the OS to shutdown the system |
84 | - gracefully before powering off, while a hard power off |
85 | - occurs immediately without any warning to the OS. |
86 | - :type stop_mode: unicode |
87 | - :param comment: Optional comment for the event log. |
88 | - :type comment: unicode |
89 | - |
90 | - Returns 404 if the machine is not found. |
91 | - Returns 403 if the user does not have permission to stop the machine. |
92 | - """ |
93 | - stop_mode = request.POST.get('stop_mode', 'hard') |
94 | - comment = get_optional_param(request.POST, 'comment') |
95 | - machine = self.model.objects.get_node_or_404( |
96 | - system_id=system_id, user=request.user, |
97 | - perm=NODE_PERMISSION.EDIT) |
98 | - power_action_sent = machine.stop( |
99 | - request.user, stop_mode=stop_mode, comment=comment) |
100 | - if power_action_sent: |
101 | - return machine |
102 | - else: |
103 | - return None |
104 | - |
105 | - @operation(idempotent=False) |
106 | def deploy(self, request, system_id): |
107 | """Deploy an operating system to a machine. |
108 | |
109 | @@ -363,48 +333,6 @@ |
110 | return self.power_on(request, system_id) |
111 | |
112 | @operation(idempotent=False) |
113 | - def power_on(self, request, system_id): |
114 | - """Turn on a machine. |
115 | - |
116 | - :param user_data: If present, this blob of user-data to be made |
117 | - available to the machines through the metadata service. |
118 | - :type user_data: base64-encoded unicode |
119 | - :param comment: Optional comment for the event log. |
120 | - :type comment: unicode |
121 | - |
122 | - Ideally we'd have MIME multipart and content-transfer-encoding etc. |
123 | - deal with the encapsulation of binary data, but couldn't make it work |
124 | - with the framework in reasonable time so went for a dumb, manual |
125 | - encoding instead. |
126 | - |
127 | - Returns 404 if the machine is not found. |
128 | - Returns 403 if the user does not have permission to start the machine. |
129 | - Returns 503 if the start-up attempted to allocate an IP address, |
130 | - and there were no IP addresses available on the relevant cluster |
131 | - interface. |
132 | - """ |
133 | - user_data = request.POST.get('user_data', None) |
134 | - comment = get_optional_param(request.POST, 'comment') |
135 | - |
136 | - machine = self.model.objects.get_node_or_404( |
137 | - system_id=system_id, user=request.user, |
138 | - perm=NODE_PERMISSION.EDIT) |
139 | - if machine.owner is None: |
140 | - raise NodeStateViolation( |
141 | - "Can't start machine: it hasn't been allocated.") |
142 | - if user_data is not None: |
143 | - user_data = b64decode(user_data) |
144 | - try: |
145 | - machine.start(request.user, user_data=user_data, comment=comment) |
146 | - except StaticIPAddressExhaustion: |
147 | - # The API response should contain error text with the |
148 | - # system_id in it, as that is the primary API key to a machine. |
149 | - raise StaticIPAddressExhaustion( |
150 | - "%s: Unable to allocate static IP due to address" |
151 | - " exhaustion." % system_id) |
152 | - return machine |
153 | - |
154 | - @operation(idempotent=False) |
155 | def release(self, request, system_id): |
156 | """Release a machine. Opposite of `Machines.allocate`. |
157 | |
158 | @@ -654,65 +582,6 @@ |
159 | get_curtin_merged_config(machine), default_flow_style=False), |
160 | content_type='text/plain') |
161 | |
162 | - @operation(idempotent=False) |
163 | - def mark_broken(self, request, system_id): |
164 | - """Mark a machine as 'broken'. |
165 | - |
166 | - If the machine is allocated, release it first. |
167 | - |
168 | - :param comment: Optional comment for the event log. Will be |
169 | - displayed on the Node as an error description until marked fixed. |
170 | - :type comment: unicode |
171 | - |
172 | - Returns 404 if the machine is not found. |
173 | - Returns 403 if the user does not have permission to mark the machine |
174 | - broken. |
175 | - """ |
176 | - node = self.model.objects.get_node_or_404( |
177 | - user=request.user, system_id=system_id, perm=NODE_PERMISSION.EDIT) |
178 | - comment = get_optional_param(request.POST, 'comment') |
179 | - if not comment: |
180 | - # read old error_description to for backward compatibility |
181 | - comment = get_optional_param(request.POST, 'error_description') |
182 | - node.mark_broken(request.user, comment) |
183 | - return node |
184 | - |
185 | - @operation(idempotent=False) |
186 | - def mark_fixed(self, request, system_id): |
187 | - """Mark a broken machine as fixed and set its status as 'ready'. |
188 | - |
189 | - :param comment: Optional comment for the event log. |
190 | - :type comment: unicode |
191 | - |
192 | - Returns 404 if the machine is not found. |
193 | - Returns 403 if the user does not have permission to mark the machine |
194 | - fixed. |
195 | - """ |
196 | - comment = get_optional_param(request.POST, 'comment') |
197 | - node = self.model.objects.get_node_or_404( |
198 | - user=request.user, system_id=system_id, perm=NODE_PERMISSION.ADMIN) |
199 | - node.mark_fixed(request.user, comment) |
200 | - maaslog.info( |
201 | - "%s: User %s marked machine as fixed", node.hostname, |
202 | - request.user.username) |
203 | - return node |
204 | - |
205 | - @operation(idempotent=False) |
206 | - def set_owner_data(self, request, system_id): |
207 | - """Set key/value data for the current owner. |
208 | - |
209 | - Pass any key/value data to this method to add, modify, or remove. A key |
210 | - is removed when the value for that key is set to an empty string. |
211 | - |
212 | - This operation will not remove any previous keys unless explicitly |
213 | - passed with an empty string. All owner data is removed when the machine |
214 | - is no longer allocated to a user. |
215 | - |
216 | - Returns 404 if the machine is not found. |
217 | - Returns 403 if the user does not have permission. |
218 | - """ |
219 | - return super().set_owner_data(request, system_id) |
220 | - |
221 | |
222 | def create_machine(request): |
223 | """Service an http request to create a machine. |
224 | @@ -857,7 +726,7 @@ |
225 | return ('machines_handler', []) |
226 | |
227 | |
228 | -class MachinesHandler(NodesHandler): |
229 | +class MachinesHandler(NodesHandler, PowersMixin): |
230 | """Manage the collection of all the machines in the MAAS.""" |
231 | api_doc_section_name = "Machines" |
232 | anonymous = AnonMachinesHandler |
233 | @@ -1186,29 +1055,6 @@ |
234 | return machine |
235 | |
236 | @admin_method |
237 | - @operation(idempotent=True) |
238 | - def power_parameters(self, request): |
239 | - """Retrieve power parameters for multiple machines. |
240 | - |
241 | - :param id: An optional list of system ids. Only machines with |
242 | - matching system ids will be returned. |
243 | - :type id: iterable |
244 | - |
245 | - :return: A dictionary of power parameters, keyed by machine system_id. |
246 | - |
247 | - Raises 403 if the user is not an admin. |
248 | - """ |
249 | - match_ids = get_optional_list(request.GET, 'id') |
250 | - |
251 | - if match_ids is None: |
252 | - machines = self.base_model.objects.all() |
253 | - else: |
254 | - machines = self.base_model.objects.filter(system_id__in=match_ids) |
255 | - |
256 | - return {machine.system_id: machine.power_parameters |
257 | - for machine in machines} |
258 | - |
259 | - @admin_method |
260 | @operation(idempotent=False) |
261 | def add_chassis(self, request): |
262 | """Add special hardware types. |
263 | |
264 | === modified file 'src/maasserver/api/nodes.py' |
265 | --- src/maasserver/api/nodes.py 2016-04-15 22:14:33 +0000 |
266 | +++ src/maasserver/api/nodes.py 2016-04-29 23:54:15 +0000 |
267 | @@ -8,12 +8,14 @@ |
268 | "store_node_power_parameters", |
269 | ] |
270 | |
271 | +from base64 import b64decode |
272 | from itertools import chain |
273 | import json |
274 | |
275 | import bson |
276 | from django.http import HttpResponse |
277 | from django.shortcuts import get_object_or_404 |
278 | +from maasserver.api.logger import maaslog |
279 | from maasserver.api.support import ( |
280 | admin_method, |
281 | AnonymousOperationsHandler, |
282 | @@ -23,17 +25,21 @@ |
283 | from maasserver.api.utils import ( |
284 | get_mandatory_param, |
285 | get_optional_list, |
286 | + get_optional_param, |
287 | ) |
288 | from maasserver.clusterrpc.power_parameters import get_power_types |
289 | from maasserver.enum import ( |
290 | NODE_PERMISSION, |
291 | NODE_STATUS, |
292 | + NODE_TYPE, |
293 | NODE_TYPE_CHOICES, |
294 | ) |
295 | from maasserver.exceptions import ( |
296 | ClusterUnavailable, |
297 | MAASAPIBadRequest, |
298 | MAASAPIValidationError, |
299 | + NodeStateViolation, |
300 | + StaticIPAddressExhaustion, |
301 | ) |
302 | from maasserver.fields import MAC_RE |
303 | from maasserver.forms import BulkNodeActionForm |
304 | @@ -236,28 +242,6 @@ |
305 | node = get_object_or_404(self.model, system_id=system_id) |
306 | return node.power_parameters |
307 | |
308 | - @operation(idempotent=True) |
309 | - def query_power_state(self, request, system_id): |
310 | - """Query the power state of a node. |
311 | - |
312 | - Send a request to the node's power controller which asks it about |
313 | - the node's state. The reply to this could be delayed by up to |
314 | - 30 seconds while waiting for the power controller to respond. |
315 | - Use this method sparingly as it ties up an appserver thread |
316 | - while waiting. |
317 | - |
318 | - :param system_id: The node to query. |
319 | - :return: a dict whose key is "state" with a value of one of |
320 | - 'on' or 'off'. |
321 | - |
322 | - Returns 404 if the node is not found. |
323 | - Returns node's power state. |
324 | - """ |
325 | - node = get_object_or_404(self.model, system_id=system_id) |
326 | - return { |
327 | - "state": node.power_query().wait(45), |
328 | - } |
329 | - |
330 | |
331 | class AnonNodeHandler(AnonymousOperationsHandler): |
332 | """Anonymous access to Node.""" |
333 | @@ -413,3 +397,173 @@ |
334 | } |
335 | OwnerData.objects.set_owner_data(node, owner_data) |
336 | return node |
337 | + |
338 | + |
339 | +class PowerMixin: |
340 | + """Mixin which adds power commands to a node type.""" |
341 | + |
342 | + @operation(idempotent=True) |
343 | + def query_power_state(self, request, system_id): |
344 | + """Query the power state of a node. |
345 | + |
346 | + Send a request to the node's power controller which asks it about |
347 | + the node's state. The reply to this could be delayed by up to |
348 | + 30 seconds while waiting for the power controller to respond. |
349 | + Use this method sparingly as it ties up an appserver thread |
350 | + while waiting. |
351 | + |
352 | + :param system_id: The node to query. |
353 | + :return: a dict whose key is "state" with a value of one of |
354 | + 'on' or 'off'. |
355 | + |
356 | + Returns 404 if the node is not found. |
357 | + Returns node's power state. |
358 | + """ |
359 | + node = self.model.objects.get_node_or_404( |
360 | + system_id=system_id, user=request.user, |
361 | + perm=NODE_PERMISSION.VIEW) |
362 | + return { |
363 | + "state": node.power_query().wait(45), |
364 | + } |
365 | + |
366 | + @operation(idempotent=False) |
367 | + def power_on(self, request, system_id): |
368 | + """Turn on a node. |
369 | + |
370 | + :param user_data: If present, this blob of user-data to be made |
371 | + available to the nodes through the metadata service. |
372 | + :type user_data: base64-encoded unicode |
373 | + :param comment: Optional comment for the event log. |
374 | + :type comment: unicode |
375 | + |
376 | + Ideally we'd have MIME multipart and content-transfer-encoding etc. |
377 | + deal with the encapsulation of binary data, but couldn't make it work |
378 | + with the framework in reasonable time so went for a dumb, manual |
379 | + encoding instead. |
380 | + |
381 | + Returns 404 if the node is not found. |
382 | + Returns 403 if the user does not have permission to start the machine. |
383 | + Returns 503 if the start-up attempted to allocate an IP address, |
384 | + and there were no IP addresses available on the relevant cluster |
385 | + interface. |
386 | + """ |
387 | + user_data = request.POST.get('user_data', None) |
388 | + comment = get_optional_param(request.POST, 'comment') |
389 | + |
390 | + node = self.model.objects.get_node_or_404( |
391 | + system_id=system_id, user=request.user, |
392 | + perm=NODE_PERMISSION.EDIT) |
393 | + if node.owner is None and node.node_type != NODE_TYPE.RACK_CONTROLLER: |
394 | + raise NodeStateViolation( |
395 | + "Can't start node: it hasn't been allocated.") |
396 | + if user_data is not None: |
397 | + user_data = b64decode(user_data) |
398 | + try: |
399 | + node.start(request.user, user_data=user_data, comment=comment) |
400 | + except StaticIPAddressExhaustion: |
401 | + # The API response should contain error text with the |
402 | + # system_id in it, as that is the primary API key to a node. |
403 | + raise StaticIPAddressExhaustion( |
404 | + "%s: Unable to allocate static IP due to address" |
405 | + " exhaustion." % system_id) |
406 | + return node |
407 | + |
408 | + @operation(idempotent=False) |
409 | + def power_off(self, request, system_id): |
410 | + """Power off a node. |
411 | + |
412 | + :param stop_mode: An optional power off mode. If 'soft', |
413 | + perform a soft power down if the node's power type supports |
414 | + it, otherwise perform a hard power off. For all values other |
415 | + than 'soft', and by default, perform a hard power off. A |
416 | + soft power off generally asks the OS to shutdown the system |
417 | + gracefully before powering off, while a hard power off |
418 | + occurs immediately without any warning to the OS. |
419 | + :type stop_mode: unicode |
420 | + :param comment: Optional comment for the event log. |
421 | + :type comment: unicode |
422 | + |
423 | + Returns 404 if the node is not found. |
424 | + Returns 403 if the user does not have permission to stop the node. |
425 | + """ |
426 | + stop_mode = request.POST.get('stop_mode', 'hard') |
427 | + comment = get_optional_param(request.POST, 'comment') |
428 | + node = self.model.objects.get_node_or_404( |
429 | + system_id=system_id, user=request.user, |
430 | + perm=NODE_PERMISSION.EDIT) |
431 | + power_action_sent = node.stop( |
432 | + request.user, stop_mode=stop_mode, comment=comment) |
433 | + if power_action_sent: |
434 | + return node |
435 | + else: |
436 | + return None |
437 | + |
438 | + @operation(idempotent=False) |
439 | + def mark_broken(self, request, system_id): |
440 | + """Mark a node as 'broken'. |
441 | + |
442 | + If the node is allocated, release it first. |
443 | + |
444 | + :param comment: Optional comment for the event log. Will be |
445 | + displayed on the node as an error description until marked fixed. |
446 | + :type comment: unicode |
447 | + |
448 | + Returns 404 if the node is not found. |
449 | + Returns 403 if the user does not have permission to mark the node |
450 | + broken. |
451 | + """ |
452 | + node = self.model.objects.get_node_or_404( |
453 | + user=request.user, system_id=system_id, perm=NODE_PERMISSION.EDIT) |
454 | + comment = get_optional_param(request.POST, 'comment') |
455 | + if not comment: |
456 | + # read old error_description to for backward compatibility |
457 | + comment = get_optional_param(request.POST, 'error_description') |
458 | + node.mark_broken(request.user, comment) |
459 | + return node |
460 | + |
461 | + @operation(idempotent=False) |
462 | + def mark_fixed(self, request, system_id): |
463 | + """Mark a broken node as fixed and set its status as 'ready'. |
464 | + |
465 | + :param comment: Optional comment for the event log. |
466 | + :type comment: unicode |
467 | + |
468 | + Returns 404 if the machine is not found. |
469 | + Returns 403 if the user does not have permission to mark the machine |
470 | + fixed. |
471 | + """ |
472 | + comment = get_optional_param(request.POST, 'comment') |
473 | + node = self.model.objects.get_node_or_404( |
474 | + user=request.user, system_id=system_id, perm=NODE_PERMISSION.ADMIN) |
475 | + node.mark_fixed(request.user, comment) |
476 | + maaslog.info( |
477 | + "%s: User %s marked node as fixed", node.hostname, |
478 | + request.user.username) |
479 | + return node |
480 | + |
481 | + |
482 | +class PowersMixin: |
483 | + """Mixin which adds power commands to a nodes type.""" |
484 | + |
485 | + @admin_method |
486 | + @operation(idempotent=True) |
487 | + def power_parameters(self, request): |
488 | + """Retrieve power parameters for multiple machines. |
489 | + |
490 | + :param id: An optional list of system ids. Only machines with |
491 | + matching system ids will be returned. |
492 | + :type id: iterable |
493 | + |
494 | + :return: A dictionary of power parameters, keyed by machine system_id. |
495 | + |
496 | + Raises 403 if the user is not an admin. |
497 | + """ |
498 | + match_ids = get_optional_list(request.GET, 'id') |
499 | + |
500 | + if match_ids is None: |
501 | + machines = self.base_model.objects.all() |
502 | + else: |
503 | + machines = self.base_model.objects.filter(system_id__in=match_ids) |
504 | + |
505 | + return {machine.system_id: machine.power_parameters |
506 | + for machine in machines} |
507 | |
508 | === modified file 'src/maasserver/api/rackcontrollers.py' |
509 | --- src/maasserver/api/rackcontrollers.py 2016-04-15 22:14:33 +0000 |
510 | +++ src/maasserver/api/rackcontrollers.py 2016-04-29 23:54:15 +0000 |
511 | @@ -12,6 +12,8 @@ |
512 | from maasserver.api.nodes import ( |
513 | NodeHandler, |
514 | NodesHandler, |
515 | + PowerMixin, |
516 | + PowersMixin, |
517 | ) |
518 | from maasserver.api.support import ( |
519 | admin_method, |
520 | @@ -68,7 +70,7 @@ |
521 | ) |
522 | |
523 | |
524 | -class RackControllerHandler(NodeHandler): |
525 | +class RackControllerHandler(NodeHandler, PowerMixin): |
526 | """Manage an individual rack controller. |
527 | |
528 | The rack controller is identified by its system_id. |
529 | @@ -173,7 +175,7 @@ |
530 | return ('rackcontroller_handler', (rackcontroller_id, )) |
531 | |
532 | |
533 | -class RackControllersHandler(NodesHandler): |
534 | +class RackControllersHandler(NodesHandler, PowersMixin): |
535 | """Manage the collection of all rack controllers in MAAS.""" |
536 | api_doc_section_name = "RackControllers" |
537 | base_model = RackController |
538 | |
539 | === modified file 'src/maasserver/api/support.py' |
540 | --- src/maasserver/api/support.py 2016-04-21 13:19:30 +0000 |
541 | +++ src/maasserver/api/support.py 2016-04-29 23:54:15 +0000 |
542 | @@ -169,12 +169,13 @@ |
543 | # Add parent classes' exports if they still correspond to a valid |
544 | # method on the class we're considering. This allows subclasses to |
545 | # remove methods by defining an attribute of the same name as None. |
546 | - if cls.exports is not None: |
547 | - for key in cls.exports.keys(): |
548 | - if key[1] is not None: |
549 | - new_func = getattr(cls, key[1], None) |
550 | + for base in bases: |
551 | + for key, value in vars(base).items(): |
552 | + export = getattr(value, "export", None) |
553 | + if export is not None: |
554 | + new_func = getattr(cls, key, None) |
555 | if new_func is not None: |
556 | - exports[key] = new_func |
557 | + exports[export] = new_func |
558 | |
559 | # Export custom operations. |
560 | exports.update(operations) |
561 | |
562 | === modified file 'src/maasserver/api/tests/test_machine.py' |
563 | --- src/maasserver/api/tests/test_machine.py 2016-04-15 22:14:33 +0000 |
564 | +++ src/maasserver/api/tests/test_machine.py 2016-04-29 23:54:15 +0000 |
565 | @@ -58,7 +58,6 @@ |
566 | Equals, |
567 | HasLength, |
568 | MockCalledOnceWith, |
569 | - MockNotCalled, |
570 | ) |
571 | from metadataserver.models import ( |
572 | NodeKey, |
573 | @@ -67,7 +66,6 @@ |
574 | from metadataserver.nodeinituser import get_node_init_user |
575 | from mock import ANY |
576 | from netaddr import IPNetwork |
577 | -from provisioningserver.rpc.exceptions import PowerActionAlreadyInProgress |
578 | from provisioningserver.utils.enum import map_enum |
579 | from testtools.matchers import ( |
580 | ContainsDict, |
581 | @@ -316,142 +314,6 @@ |
582 | self.assertEqual(NODE_STATUS_CHOICES_DICT[status], |
583 | parsed_result['status_name']) |
584 | |
585 | - def test_POST_power_off_checks_permission(self): |
586 | - machine = factory.make_Node() |
587 | - machine_stop = self.patch(machine, 'stop') |
588 | - response = self.client.post( |
589 | - self.get_machine_uri(machine), {'op': 'power_off'}) |
590 | - self.assertEqual(http.client.FORBIDDEN, response.status_code) |
591 | - self.assertThat(machine_stop, MockNotCalled()) |
592 | - |
593 | - def test_POST_power_off_rejects_other_node_types(self): |
594 | - node = factory.make_Node_with_Interface_on_Subnet( |
595 | - owner=self.logged_in_user, |
596 | - node_type=factory.pick_choice( |
597 | - NODE_TYPE_CHOICES, but_not=[NODE_TYPE.MACHINE]), |
598 | - ) |
599 | - response = self.client.post( |
600 | - self.get_machine_uri(node), {'op': 'power_off'}) |
601 | - self.assertEqual( |
602 | - http.client.NOT_FOUND, response.status_code, response.content) |
603 | - |
604 | - def test_POST_power_off_returns_nothing_if_machine_was_not_stopped(self): |
605 | - # The machine may not be stopped because, for example, its power type |
606 | - # does not support it. In this case the machine is not returned to the |
607 | - # caller. |
608 | - machine = factory.make_Node(owner=self.logged_in_user) |
609 | - machine_stop = self.patch(node_module.Machine, 'stop') |
610 | - machine_stop.return_value = False |
611 | - response = self.client.post( |
612 | - self.get_machine_uri(machine), {'op': 'power_off'}) |
613 | - self.assertEqual(http.client.OK, response.status_code) |
614 | - self.assertIsNone(json_load_bytes(response.content)) |
615 | - self.assertThat(machine_stop, MockCalledOnceWith( |
616 | - ANY, stop_mode=ANY, comment=None)) |
617 | - |
618 | - def test_POST_power_off_returns_machine(self): |
619 | - machine = factory.make_Node(owner=self.logged_in_user) |
620 | - self.patch(node_module.Machine, 'stop').return_value = True |
621 | - response = self.client.post( |
622 | - self.get_machine_uri(machine), {'op': 'power_off'}) |
623 | - self.assertEqual(http.client.OK, response.status_code) |
624 | - self.assertEqual( |
625 | - machine.system_id, json_load_bytes(response.content)['system_id']) |
626 | - |
627 | - def test_POST_power_off_may_be_repeated(self): |
628 | - machine = factory.make_Node( |
629 | - owner=self.logged_in_user, interface=True, |
630 | - power_type='manual') |
631 | - self.patch(machine, 'stop') |
632 | - self.client.post(self.get_machine_uri(machine), {'op': 'power_off'}) |
633 | - response = self.client.post( |
634 | - self.get_machine_uri(machine), {'op': 'power_off'}) |
635 | - self.assertEqual(http.client.OK, response.status_code) |
636 | - |
637 | - def test_POST_power_off_power_offs_machines(self): |
638 | - machine = factory.make_Node(owner=self.logged_in_user) |
639 | - machine_stop = self.patch(node_module.Machine, 'stop') |
640 | - stop_mode = factory.make_name('stop_mode') |
641 | - comment = factory.make_name('comment') |
642 | - self.client.post( |
643 | - self.get_machine_uri(machine), |
644 | - {'op': 'power_off', 'stop_mode': stop_mode, 'comment': comment}) |
645 | - self.assertThat( |
646 | - machine_stop, |
647 | - MockCalledOnceWith( |
648 | - self.logged_in_user, stop_mode=stop_mode, comment=comment)) |
649 | - |
650 | - def test_POST_power_off_handles_missing_comment(self): |
651 | - machine = factory.make_Node(owner=self.logged_in_user) |
652 | - machine_stop = self.patch(node_module.Machine, 'stop') |
653 | - stop_mode = factory.make_name('stop_mode') |
654 | - self.client.post( |
655 | - self.get_machine_uri(machine), |
656 | - {'op': 'power_off', 'stop_mode': stop_mode}) |
657 | - self.assertThat( |
658 | - machine_stop, |
659 | - MockCalledOnceWith( |
660 | - self.logged_in_user, stop_mode=stop_mode, comment=None)) |
661 | - |
662 | - def test_POST_power_off_returns_503_when_power_already_in_progress(self): |
663 | - machine = factory.make_Node(owner=self.logged_in_user) |
664 | - exc_text = factory.make_name("exc_text") |
665 | - self.patch( |
666 | - node_module.Machine, |
667 | - 'stop').side_effect = PowerActionAlreadyInProgress(exc_text) |
668 | - response = self.client.post( |
669 | - self.get_machine_uri(machine), {'op': 'power_off'}) |
670 | - self.assertResponseCode(http.client.SERVICE_UNAVAILABLE, response) |
671 | - self.assertIn( |
672 | - exc_text, response.content.decode(settings.DEFAULT_CHARSET)) |
673 | - |
674 | - def test_POST_power_on_checks_permission(self): |
675 | - machine = factory.make_Node_with_Interface_on_Subnet( |
676 | - owner=factory.make_User()) |
677 | - response = self.client.post( |
678 | - self.get_machine_uri(machine), {'op': 'power_on'}) |
679 | - self.assertEqual(http.client.FORBIDDEN, response.status_code) |
680 | - |
681 | - def test_POST_power_on_checks_ownership(self): |
682 | - self.become_admin() |
683 | - machine = factory.make_Node_with_Interface_on_Subnet( |
684 | - status=NODE_STATUS.READY) |
685 | - response = self.client.post( |
686 | - self.get_machine_uri(machine), {'op': 'power_on'}) |
687 | - self.assertEqual(http.client.CONFLICT, response.status_code) |
688 | - self.assertEqual( |
689 | - "Can't start machine: it hasn't been allocated.", |
690 | - response.content.decode(settings.DEFAULT_CHARSET)) |
691 | - |
692 | - def test_POST_power_on_returns_machine(self): |
693 | - self.patch(node_module.Node, "_start") |
694 | - machine = factory.make_Node( |
695 | - owner=self.logged_in_user, interface=True, |
696 | - power_type='manual', |
697 | - architecture=make_usable_architecture(self)) |
698 | - osystem = make_usable_osystem(self) |
699 | - distro_series = osystem['default_release'] |
700 | - response = self.client.post( |
701 | - self.get_machine_uri(machine), |
702 | - { |
703 | - 'op': 'power_on', |
704 | - 'distro_series': distro_series, |
705 | - }) |
706 | - self.assertEqual(http.client.OK, response.status_code) |
707 | - self.assertEqual( |
708 | - machine.system_id, json_load_bytes(response.content)['system_id']) |
709 | - |
710 | - def test_POST_power_on_rejects_other_node_types(self): |
711 | - node = factory.make_Node_with_Interface_on_Subnet( |
712 | - owner=self.logged_in_user, |
713 | - node_type=factory.pick_choice( |
714 | - NODE_TYPE_CHOICES, but_not=[NODE_TYPE.MACHINE]), |
715 | - ) |
716 | - response = self.client.post( |
717 | - self.get_machine_uri(node), {'op': 'power_on'}) |
718 | - self.assertEqual( |
719 | - http.client.NOT_FOUND, response.status_code, response.content) |
720 | - |
721 | def test_POST_deploy_sets_osystem_and_distro_series(self): |
722 | self.patch(node_module.Node, "_start") |
723 | machine = factory.make_Node( |
724 | @@ -1515,7 +1377,7 @@ |
725 | def test_abort_changes_state(self): |
726 | machine = factory.make_Node( |
727 | status=NODE_STATUS.DISK_ERASING, owner=self.logged_in_user) |
728 | - machine_stop = self.patch(node_module.Node, "_stop") |
729 | + machine_stop = self.patch(node_module.Machine, "_stop") |
730 | machine_stop.side_effect = lambda user: post_commit() |
731 | |
732 | response = self.client.post( |
733 | @@ -2021,136 +1883,3 @@ |
734 | response.content.decode(settings.DEFAULT_CHARSET)) |
735 | self.assertThat( |
736 | mock_get_curtin_merged_config, MockCalledOnceWith(machine)) |
737 | - |
738 | - |
739 | -class TestMarkBroken(APITestCase): |
740 | - """Tests for /api/2.0/machines/<node>/?op=mark_broken""" |
741 | - |
742 | - def get_node_uri(self, node): |
743 | - """Get the API URI for `node`.""" |
744 | - return reverse('machine_handler', args=[node.system_id]) |
745 | - |
746 | - def test_mark_broken_changes_status(self): |
747 | - node = factory.make_Node( |
748 | - status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
749 | - response = self.client.post( |
750 | - self.get_node_uri(node), {'op': 'mark_broken'}) |
751 | - self.assertEqual(http.client.OK, response.status_code) |
752 | - self.assertEqual(NODE_STATUS.BROKEN, reload_object(node).status) |
753 | - |
754 | - def test_mark_broken_updates_error_description(self): |
755 | - # 'error_description' parameter was renamed 'comment' for consistency |
756 | - # make sure this comment updates the node's error_description |
757 | - node = factory.make_Node( |
758 | - status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
759 | - comment = factory.make_name('comment') |
760 | - response = self.client.post( |
761 | - self.get_node_uri(node), |
762 | - {'op': 'mark_broken', 'comment': comment}) |
763 | - self.assertEqual(http.client.OK, response.status_code) |
764 | - node = reload_object(node) |
765 | - self.assertEqual( |
766 | - (NODE_STATUS.BROKEN, comment), |
767 | - (node.status, node.error_description) |
768 | - ) |
769 | - |
770 | - def test_mark_broken_updates_error_description_compatibility(self): |
771 | - # test old 'error_description' parameter is honored for compatibility |
772 | - node = factory.make_Node( |
773 | - status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
774 | - error_description = factory.make_name('error_description') |
775 | - response = self.client.post( |
776 | - self.get_node_uri(node), |
777 | - {'op': 'mark_broken', 'error_description': error_description}) |
778 | - self.assertEqual(http.client.OK, response.status_code) |
779 | - node = reload_object(node) |
780 | - self.assertEqual( |
781 | - (NODE_STATUS.BROKEN, error_description), |
782 | - (node.status, node.error_description) |
783 | - ) |
784 | - |
785 | - def test_mark_broken_passes_comment(self): |
786 | - node = factory.make_Node( |
787 | - status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
788 | - node_mark_broken = self.patch(node_module.Node, 'mark_broken') |
789 | - comment = factory.make_name('comment') |
790 | - self.client.post( |
791 | - self.get_node_uri(node), |
792 | - {'op': 'mark_broken', 'comment': comment}) |
793 | - self.assertThat( |
794 | - node_mark_broken, |
795 | - MockCalledOnceWith(self.logged_in_user, comment)) |
796 | - |
797 | - def test_mark_broken_handles_missing_comment(self): |
798 | - node = factory.make_Node( |
799 | - status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
800 | - node_mark_broken = self.patch(node_module.Node, 'mark_broken') |
801 | - self.client.post( |
802 | - self.get_node_uri(node), {'op': 'mark_broken'}) |
803 | - self.assertThat( |
804 | - node_mark_broken, |
805 | - MockCalledOnceWith(self.logged_in_user, None)) |
806 | - |
807 | - def test_mark_broken_requires_ownership(self): |
808 | - node = factory.make_Node(status=NODE_STATUS.COMMISSIONING) |
809 | - response = self.client.post( |
810 | - self.get_node_uri(node), {'op': 'mark_broken'}) |
811 | - self.assertEqual(http.client.FORBIDDEN, response.status_code) |
812 | - |
813 | - def test_mark_broken_allowed_from_any_other_state(self): |
814 | - self.patch(node_module.Node, "_stop") |
815 | - for status, _ in NODE_STATUS_CHOICES: |
816 | - if status == NODE_STATUS.BROKEN: |
817 | - continue |
818 | - |
819 | - node = factory.make_Node(status=status, owner=self.logged_in_user) |
820 | - response = self.client.post( |
821 | - self.get_node_uri(node), {'op': 'mark_broken'}) |
822 | - self.expectThat( |
823 | - response.status_code, Equals(http.client.OK), response) |
824 | - node = reload_object(node) |
825 | - self.expectThat(node.status, Equals(NODE_STATUS.BROKEN)) |
826 | - |
827 | - |
828 | -class TestMarkFixed(APITestCase): |
829 | - """Tests for /api/2.0/machines/<node>/?op=mark_fixed""" |
830 | - |
831 | - def get_node_uri(self, node): |
832 | - """Get the API URI for `node`.""" |
833 | - return reverse('machine_handler', args=[node.system_id]) |
834 | - |
835 | - def test_mark_fixed_changes_status(self): |
836 | - self.become_admin() |
837 | - node = factory.make_Node(status=NODE_STATUS.BROKEN) |
838 | - response = self.client.post( |
839 | - self.get_node_uri(node), {'op': 'mark_fixed'}) |
840 | - self.assertEqual(http.client.OK, response.status_code) |
841 | - self.assertEqual(NODE_STATUS.READY, reload_object(node).status) |
842 | - |
843 | - def test_mark_fixed_requires_admin(self): |
844 | - node = factory.make_Node(status=NODE_STATUS.BROKEN) |
845 | - response = self.client.post( |
846 | - self.get_node_uri(node), {'op': 'mark_fixed'}) |
847 | - self.assertEqual(http.client.FORBIDDEN, response.status_code) |
848 | - |
849 | - def test_mark_fixed_passes_comment(self): |
850 | - self.become_admin() |
851 | - node = factory.make_Node(status=NODE_STATUS.BROKEN) |
852 | - node_mark_fixed = self.patch(node_module.Node, 'mark_fixed') |
853 | - comment = factory.make_name('comment') |
854 | - self.client.post( |
855 | - self.get_node_uri(node), |
856 | - {'op': 'mark_fixed', 'comment': comment}) |
857 | - self.assertThat( |
858 | - node_mark_fixed, |
859 | - MockCalledOnceWith(self.logged_in_user, comment)) |
860 | - |
861 | - def test_mark_fixed_handles_missing_comment(self): |
862 | - self.become_admin() |
863 | - node = factory.make_Node(status=NODE_STATUS.BROKEN) |
864 | - node_mark_fixed = self.patch(node_module.Node, 'mark_fixed') |
865 | - self.client.post( |
866 | - self.get_node_uri(node), {'op': 'mark_fixed'}) |
867 | - self.assertThat( |
868 | - node_mark_fixed, |
869 | - MockCalledOnceWith(self.logged_in_user, None)) |
870 | |
871 | === modified file 'src/maasserver/api/tests/test_machines.py' |
872 | --- src/maasserver/api/tests/test_machines.py 2016-04-14 15:25:36 +0000 |
873 | +++ src/maasserver/api/tests/test_machines.py 2016-04-29 23:54:15 +0000 |
874 | @@ -1464,59 +1464,6 @@ |
875 | machine = reload_object(machine) |
876 | self.assertEqual(original_zone, machine.zone) |
877 | |
878 | - def test_GET_power_parameters_requires_admin(self): |
879 | - response = self.client.get( |
880 | - reverse('machines_handler'), |
881 | - { |
882 | - 'op': 'power_parameters', |
883 | - }) |
884 | - self.assertEqual( |
885 | - http.client.FORBIDDEN, response.status_code, response.content) |
886 | - |
887 | - def test_GET_power_parameters_without_ids_does_not_filter(self): |
888 | - self.become_admin() |
889 | - machines = [ |
890 | - factory.make_Node(power_parameters={factory.make_string(): |
891 | - factory.make_string()}) |
892 | - for _ in range(0, 3) |
893 | - ] |
894 | - response = self.client.get( |
895 | - reverse('machines_handler'), |
896 | - { |
897 | - 'op': 'power_parameters', |
898 | - }) |
899 | - self.assertEqual( |
900 | - http.client.OK, response.status_code, response.content) |
901 | - parsed = json.loads(response.content.decode(settings.DEFAULT_CHARSET)) |
902 | - expected = { |
903 | - machine.system_id: machine.power_parameters |
904 | - for machine in machines |
905 | - } |
906 | - self.assertEqual(expected, parsed) |
907 | - |
908 | - def test_GET_power_parameters_with_ids_filters(self): |
909 | - self.become_admin() |
910 | - machines = [ |
911 | - factory.make_Node(power_parameters={factory.make_string(): |
912 | - factory.make_string()}) |
913 | - for _ in range(0, 6) |
914 | - ] |
915 | - expected_machines = random.sample(machines, 3) |
916 | - response = self.client.get( |
917 | - reverse('machines_handler'), |
918 | - { |
919 | - 'op': 'power_parameters', |
920 | - 'id': [machine.system_id for machine in expected_machines], |
921 | - }) |
922 | - self.assertEqual( |
923 | - http.client.OK, response.status_code, response.content) |
924 | - parsed = json.loads(response.content.decode(settings.DEFAULT_CHARSET)) |
925 | - expected = { |
926 | - machine.system_id: machine.power_parameters |
927 | - for machine in expected_machines |
928 | - } |
929 | - self.assertEqual(expected, parsed) |
930 | - |
931 | def test_POST_add_chassis_requires_admin(self): |
932 | response = self.client.post( |
933 | reverse('machines_handler'), |
934 | |
935 | === modified file 'src/maasserver/api/tests/test_node.py' |
936 | --- src/maasserver/api/tests/test_node.py 2016-04-15 22:14:33 +0000 |
937 | +++ src/maasserver/api/tests/test_node.py 2016-04-29 23:54:15 +0000 |
938 | @@ -12,6 +12,7 @@ |
939 | from django.core.urlresolvers import reverse |
940 | from maasserver.enum import ( |
941 | NODE_STATUS, |
942 | + NODE_STATUS_CHOICES, |
943 | POWER_STATE, |
944 | ) |
945 | from maasserver.models import ( |
946 | @@ -22,15 +23,26 @@ |
947 | from maasserver.testing.architecture import make_usable_architecture |
948 | from maasserver.testing.factory import factory |
949 | from maasserver.testing.oauthclient import OAuthAuthenticatedClient |
950 | +from maasserver.testing.osystems import make_usable_osystem |
951 | from maasserver.testing.testcase import MAASServerTestCase |
952 | from maasserver.utils.converters import json_load_bytes |
953 | +from maasserver.utils.orm import reload_object |
954 | +from maastesting.matchers import ( |
955 | + Equals, |
956 | + MockCalledOnceWith, |
957 | + MockNotCalled, |
958 | +) |
959 | from metadataserver.models import NodeKey |
960 | from metadataserver.nodeinituser import get_node_init_user |
961 | -from mock import Mock |
962 | +from mock import ( |
963 | + ANY, |
964 | + Mock, |
965 | +) |
966 | from provisioningserver.refresh.node_info_scripts import ( |
967 | LLDP_OUTPUT_NAME, |
968 | LSHW_OUTPUT_NAME, |
969 | ) |
970 | +from provisioningserver.rpc.exceptions import PowerActionAlreadyInProgress |
971 | |
972 | |
973 | class NodeAnonAPITest(MAASServerTestCase): |
974 | @@ -280,25 +292,6 @@ |
975 | http.client.FORBIDDEN, response.status_code, response.content) |
976 | |
977 | |
978 | -class TestQueryPowerState(APITestCase): |
979 | - """Tests for /api/2.0/nodes/<node>/?op=query_power_state""" |
980 | - |
981 | - def get_node_uri(self, node): |
982 | - """Get the API URI for `node`.""" |
983 | - return reverse('node_handler', args=[node.system_id]) |
984 | - |
985 | - def test_query_power_state(self): |
986 | - node = factory.make_Node() |
987 | - mock__power_control_node = self.patch( |
988 | - node_module.Node, "power_query").return_value |
989 | - mock__power_control_node.wait = Mock(return_value=POWER_STATE.ON) |
990 | - response = self.client.get( |
991 | - self.get_node_uri(node), {'op': 'query_power_state'}) |
992 | - self.assertEqual(http.client.OK, response.status_code) |
993 | - parsed_result = json_load_bytes(response.content) |
994 | - self.assertEqual(POWER_STATE.ON, parsed_result['state']) |
995 | - |
996 | - |
997 | class TestSetOwnerData(APITestCase): |
998 | """Tests for op=set_owner_data for both machines and devices.""" |
999 | |
1000 | @@ -375,3 +368,255 @@ |
1001 | self.assertEqual(http.client.OK, response.status_code) |
1002 | self.assertEqual( |
1003 | {}, json_load_bytes(response.content)['owner_data']) |
1004 | + |
1005 | + |
1006 | +class TestPowerMixin(APITestCase): |
1007 | + """Test the power mixin.""" |
1008 | + |
1009 | + def get_node_uri(self, node): |
1010 | + """Get the API URI for `node`.""" |
1011 | + # Use the machine handler to test as that will always support all |
1012 | + # power commands |
1013 | + return reverse('machine_handler', args=[node.system_id]) |
1014 | + |
1015 | + def test_POST_power_off_checks_permission(self): |
1016 | + machine = factory.make_Node() |
1017 | + machine_stop = self.patch(machine, 'stop') |
1018 | + response = self.client.post( |
1019 | + self.get_node_uri(machine), {'op': 'power_off'}) |
1020 | + self.assertEqual(http.client.FORBIDDEN, response.status_code) |
1021 | + self.assertThat(machine_stop, MockNotCalled()) |
1022 | + |
1023 | + def test_POST_power_off_returns_nothing_if_machine_was_not_stopped(self): |
1024 | + # The machine may not be stopped because, for example, its power type |
1025 | + # does not support it. In this case the machine is not returned to the |
1026 | + # caller. |
1027 | + machine = factory.make_Node(owner=self.logged_in_user) |
1028 | + machine_stop = self.patch(node_module.Machine, 'stop') |
1029 | + machine_stop.return_value = False |
1030 | + response = self.client.post( |
1031 | + self.get_node_uri(machine), {'op': 'power_off'}) |
1032 | + self.assertEqual(http.client.OK, response.status_code) |
1033 | + self.assertIsNone(json_load_bytes(response.content)) |
1034 | + self.assertThat(machine_stop, MockCalledOnceWith( |
1035 | + ANY, stop_mode=ANY, comment=None)) |
1036 | + |
1037 | + def test_POST_power_off_returns_machine(self): |
1038 | + machine = factory.make_Node(owner=self.logged_in_user) |
1039 | + self.patch(node_module.Machine, 'stop').return_value = True |
1040 | + response = self.client.post( |
1041 | + self.get_node_uri(machine), {'op': 'power_off'}) |
1042 | + self.assertEqual(http.client.OK, response.status_code) |
1043 | + self.assertEqual( |
1044 | + machine.system_id, json_load_bytes(response.content)['system_id']) |
1045 | + |
1046 | + def test_POST_power_off_may_be_repeated(self): |
1047 | + machine = factory.make_Node( |
1048 | + owner=self.logged_in_user, interface=True, |
1049 | + power_type='manual') |
1050 | + self.patch(machine, 'stop') |
1051 | + self.client.post(self.get_node_uri(machine), {'op': 'power_off'}) |
1052 | + response = self.client.post( |
1053 | + self.get_node_uri(machine), {'op': 'power_off'}) |
1054 | + self.assertEqual(http.client.OK, response.status_code) |
1055 | + |
1056 | + def test_POST_power_off_power_offs_machines(self): |
1057 | + machine = factory.make_Node(owner=self.logged_in_user) |
1058 | + machine_stop = self.patch(node_module.Machine, 'stop') |
1059 | + stop_mode = factory.make_name('stop_mode') |
1060 | + comment = factory.make_name('comment') |
1061 | + self.client.post( |
1062 | + self.get_node_uri(machine), |
1063 | + {'op': 'power_off', 'stop_mode': stop_mode, 'comment': comment}) |
1064 | + self.assertThat( |
1065 | + machine_stop, |
1066 | + MockCalledOnceWith( |
1067 | + self.logged_in_user, stop_mode=stop_mode, comment=comment)) |
1068 | + |
1069 | + def test_POST_power_off_handles_missing_comment(self): |
1070 | + machine = factory.make_Node(owner=self.logged_in_user) |
1071 | + machine_stop = self.patch(node_module.Machine, 'stop') |
1072 | + stop_mode = factory.make_name('stop_mode') |
1073 | + self.client.post( |
1074 | + self.get_node_uri(machine), |
1075 | + {'op': 'power_off', 'stop_mode': stop_mode}) |
1076 | + self.assertThat( |
1077 | + machine_stop, |
1078 | + MockCalledOnceWith( |
1079 | + self.logged_in_user, stop_mode=stop_mode, comment=None)) |
1080 | + |
1081 | + def test_POST_power_off_returns_503_when_power_already_in_progress(self): |
1082 | + machine = factory.make_Node(owner=self.logged_in_user) |
1083 | + exc_text = factory.make_name("exc_text") |
1084 | + self.patch( |
1085 | + node_module.Machine, |
1086 | + 'stop').side_effect = PowerActionAlreadyInProgress(exc_text) |
1087 | + response = self.client.post( |
1088 | + self.get_node_uri(machine), {'op': 'power_off'}) |
1089 | + self.assertResponseCode(http.client.SERVICE_UNAVAILABLE, response) |
1090 | + self.assertIn( |
1091 | + exc_text, response.content.decode(settings.DEFAULT_CHARSET)) |
1092 | + |
1093 | + def test_POST_power_on_checks_permission(self): |
1094 | + machine = factory.make_Node_with_Interface_on_Subnet( |
1095 | + owner=factory.make_User()) |
1096 | + response = self.client.post( |
1097 | + self.get_node_uri(machine), {'op': 'power_on'}) |
1098 | + self.assertEqual(http.client.FORBIDDEN, response.status_code) |
1099 | + |
1100 | + def test_POST_power_on_checks_ownership(self): |
1101 | + self.become_admin() |
1102 | + machine = factory.make_Node_with_Interface_on_Subnet( |
1103 | + status=NODE_STATUS.READY) |
1104 | + response = self.client.post( |
1105 | + self.get_node_uri(machine), {'op': 'power_on'}) |
1106 | + self.assertEqual(http.client.CONFLICT, response.status_code) |
1107 | + self.assertEqual( |
1108 | + "Can't start node: it hasn't been allocated.", |
1109 | + response.content.decode(settings.DEFAULT_CHARSET)) |
1110 | + |
1111 | + def test_POST_power_on_returns_machine(self): |
1112 | + self.patch(node_module.Machine, "_start") |
1113 | + machine = factory.make_Node( |
1114 | + owner=self.logged_in_user, interface=True, |
1115 | + power_type='manual', |
1116 | + architecture=make_usable_architecture(self)) |
1117 | + osystem = make_usable_osystem(self) |
1118 | + distro_series = osystem['default_release'] |
1119 | + response = self.client.post( |
1120 | + self.get_node_uri(machine), |
1121 | + { |
1122 | + 'op': 'power_on', |
1123 | + 'distro_series': distro_series, |
1124 | + }) |
1125 | + self.assertEqual(http.client.OK, response.status_code) |
1126 | + self.assertEqual( |
1127 | + machine.system_id, json_load_bytes(response.content)['system_id']) |
1128 | + |
1129 | + def test_mark_broken_changes_status(self): |
1130 | + node = factory.make_Node( |
1131 | + status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
1132 | + response = self.client.post( |
1133 | + self.get_node_uri(node), {'op': 'mark_broken'}) |
1134 | + self.assertEqual(http.client.OK, response.status_code) |
1135 | + self.assertEqual(NODE_STATUS.BROKEN, reload_object(node).status) |
1136 | + |
1137 | + def test_mark_broken_updates_error_description(self): |
1138 | + # 'error_description' parameter was renamed 'comment' for consistency |
1139 | + # make sure this comment updates the node's error_description |
1140 | + node = factory.make_Node( |
1141 | + status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
1142 | + comment = factory.make_name('comment') |
1143 | + response = self.client.post( |
1144 | + self.get_node_uri(node), |
1145 | + {'op': 'mark_broken', 'comment': comment}) |
1146 | + self.assertEqual(http.client.OK, response.status_code) |
1147 | + node = reload_object(node) |
1148 | + self.assertEqual( |
1149 | + (NODE_STATUS.BROKEN, comment), |
1150 | + (node.status, node.error_description) |
1151 | + ) |
1152 | + |
1153 | + def test_mark_broken_updates_error_description_compatibility(self): |
1154 | + # test old 'error_description' parameter is honored for compatibility |
1155 | + node = factory.make_Node( |
1156 | + status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
1157 | + error_description = factory.make_name('error_description') |
1158 | + response = self.client.post( |
1159 | + self.get_node_uri(node), |
1160 | + {'op': 'mark_broken', 'error_description': error_description}) |
1161 | + self.assertEqual(http.client.OK, response.status_code) |
1162 | + node = reload_object(node) |
1163 | + self.assertEqual( |
1164 | + (NODE_STATUS.BROKEN, error_description), |
1165 | + (node.status, node.error_description) |
1166 | + ) |
1167 | + |
1168 | + def test_mark_broken_passes_comment(self): |
1169 | + node = factory.make_Node( |
1170 | + status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
1171 | + node_mark_broken = self.patch(node_module.Machine, 'mark_broken') |
1172 | + comment = factory.make_name('comment') |
1173 | + self.client.post( |
1174 | + self.get_node_uri(node), |
1175 | + {'op': 'mark_broken', 'comment': comment}) |
1176 | + self.assertThat( |
1177 | + node_mark_broken, |
1178 | + MockCalledOnceWith(self.logged_in_user, comment)) |
1179 | + |
1180 | + def test_mark_broken_handles_missing_comment(self): |
1181 | + node = factory.make_Node( |
1182 | + status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
1183 | + node_mark_broken = self.patch(node_module.Machine, 'mark_broken') |
1184 | + self.client.post( |
1185 | + self.get_node_uri(node), {'op': 'mark_broken'}) |
1186 | + self.assertThat( |
1187 | + node_mark_broken, |
1188 | + MockCalledOnceWith(self.logged_in_user, None)) |
1189 | + |
1190 | + def test_mark_broken_requires_ownership(self): |
1191 | + node = factory.make_Node(status=NODE_STATUS.COMMISSIONING) |
1192 | + response = self.client.post( |
1193 | + self.get_node_uri(node), {'op': 'mark_broken'}) |
1194 | + self.assertEqual(http.client.FORBIDDEN, response.status_code) |
1195 | + |
1196 | + def test_mark_broken_allowed_from_any_other_state(self): |
1197 | + self.patch(node_module.Machine, "_stop") |
1198 | + for status, _ in NODE_STATUS_CHOICES: |
1199 | + if status == NODE_STATUS.BROKEN: |
1200 | + continue |
1201 | + |
1202 | + node = factory.make_Node(status=status, owner=self.logged_in_user) |
1203 | + response = self.client.post( |
1204 | + self.get_node_uri(node), {'op': 'mark_broken'}) |
1205 | + self.expectThat( |
1206 | + response.status_code, Equals(http.client.OK), response) |
1207 | + node = reload_object(node) |
1208 | + self.expectThat(node.status, Equals(NODE_STATUS.BROKEN)) |
1209 | + |
1210 | + def test_mark_fixed_changes_status(self): |
1211 | + self.become_admin() |
1212 | + node = factory.make_Node(status=NODE_STATUS.BROKEN) |
1213 | + response = self.client.post( |
1214 | + self.get_node_uri(node), {'op': 'mark_fixed'}) |
1215 | + self.assertEqual(http.client.OK, response.status_code) |
1216 | + self.assertEqual(NODE_STATUS.READY, reload_object(node).status) |
1217 | + |
1218 | + def test_mark_fixed_requires_admin(self): |
1219 | + node = factory.make_Node(status=NODE_STATUS.BROKEN) |
1220 | + response = self.client.post( |
1221 | + self.get_node_uri(node), {'op': 'mark_fixed'}) |
1222 | + self.assertEqual(http.client.FORBIDDEN, response.status_code) |
1223 | + |
1224 | + def test_mark_fixed_passes_comment(self): |
1225 | + self.become_admin() |
1226 | + node = factory.make_Node(status=NODE_STATUS.BROKEN) |
1227 | + node_mark_fixed = self.patch(node_module.Machine, 'mark_fixed') |
1228 | + comment = factory.make_name('comment') |
1229 | + self.client.post( |
1230 | + self.get_node_uri(node), |
1231 | + {'op': 'mark_fixed', 'comment': comment}) |
1232 | + self.assertThat( |
1233 | + node_mark_fixed, |
1234 | + MockCalledOnceWith(self.logged_in_user, comment)) |
1235 | + |
1236 | + def test_mark_fixed_handles_missing_comment(self): |
1237 | + self.become_admin() |
1238 | + node = factory.make_Node(status=NODE_STATUS.BROKEN) |
1239 | + node_mark_fixed = self.patch(node_module.Machine, 'mark_fixed') |
1240 | + self.client.post( |
1241 | + self.get_node_uri(node), {'op': 'mark_fixed'}) |
1242 | + self.assertThat( |
1243 | + node_mark_fixed, |
1244 | + MockCalledOnceWith(self.logged_in_user, None)) |
1245 | + |
1246 | + def test_query_power_state(self): |
1247 | + node = factory.make_Node() |
1248 | + mock__power_control_node = self.patch( |
1249 | + node_module.Node, "power_query").return_value |
1250 | + mock__power_control_node.wait = Mock(return_value=POWER_STATE.ON) |
1251 | + response = self.client.get( |
1252 | + self.get_node_uri(node), {'op': 'query_power_state'}) |
1253 | + self.assertEqual(http.client.OK, response.status_code) |
1254 | + parsed_result = json_load_bytes(response.content) |
1255 | + self.assertEqual(POWER_STATE.ON, parsed_result['state']) |
1256 | |
1257 | === modified file 'src/maasserver/api/tests/test_nodes.py' |
1258 | --- src/maasserver/api/tests/test_nodes.py 2016-04-11 16:23:26 +0000 |
1259 | +++ src/maasserver/api/tests/test_nodes.py 2016-04-29 23:54:15 +0000 |
1260 | @@ -576,3 +576,66 @@ |
1261 | response = self.client.put(reverse('nodes_handler'), {}) |
1262 | self.assertEqual( |
1263 | http.client.METHOD_NOT_ALLOWED, response.status_code) |
1264 | + |
1265 | + |
1266 | +class TestPowersMixin(APITestCase): |
1267 | + """Test the powers mixin.""" |
1268 | + |
1269 | + def get_node_uri(self, node): |
1270 | + """Get the API URI for `node`.""" |
1271 | + # Use the machine handler to test as that will always support all |
1272 | + # power commands |
1273 | + return reverse('machine_handler', args=[node.system_id]) |
1274 | + |
1275 | + def test_GET_power_parameters_requires_admin(self): |
1276 | + response = self.client.get( |
1277 | + reverse('machines_handler'), |
1278 | + { |
1279 | + 'op': 'power_parameters', |
1280 | + }) |
1281 | + self.assertEqual( |
1282 | + http.client.FORBIDDEN, response.status_code, response.content) |
1283 | + |
1284 | + def test_GET_power_parameters_without_ids_does_not_filter(self): |
1285 | + self.become_admin() |
1286 | + machines = [ |
1287 | + factory.make_Node(power_parameters={factory.make_string(): |
1288 | + factory.make_string()}) |
1289 | + for _ in range(0, 3) |
1290 | + ] |
1291 | + response = self.client.get( |
1292 | + reverse('machines_handler'), |
1293 | + { |
1294 | + 'op': 'power_parameters', |
1295 | + }) |
1296 | + self.assertEqual( |
1297 | + http.client.OK, response.status_code, response.content) |
1298 | + parsed = json.loads(response.content.decode(settings.DEFAULT_CHARSET)) |
1299 | + expected = { |
1300 | + machine.system_id: machine.power_parameters |
1301 | + for machine in machines |
1302 | + } |
1303 | + self.assertEqual(expected, parsed) |
1304 | + |
1305 | + def test_GET_power_parameters_with_ids_filters(self): |
1306 | + self.become_admin() |
1307 | + machines = [ |
1308 | + factory.make_Node(power_parameters={factory.make_string(): |
1309 | + factory.make_string()}) |
1310 | + for _ in range(0, 6) |
1311 | + ] |
1312 | + expected_machines = random.sample(machines, 3) |
1313 | + response = self.client.get( |
1314 | + reverse('machines_handler'), |
1315 | + { |
1316 | + 'op': 'power_parameters', |
1317 | + 'id': [machine.system_id for machine in expected_machines], |
1318 | + }) |
1319 | + self.assertEqual( |
1320 | + http.client.OK, response.status_code, response.content) |
1321 | + parsed = json.loads(response.content.decode(settings.DEFAULT_CHARSET)) |
1322 | + expected = { |
1323 | + machine.system_id: machine.power_parameters |
1324 | + for machine in expected_machines |
1325 | + } |
1326 | + self.assertEqual(expected, parsed) |
1327 | |
1328 | === modified file 'src/maasserver/models/node.py' |
1329 | --- src/maasserver/models/node.py 2016-04-29 18:26:59 +0000 |
1330 | +++ src/maasserver/models/node.py 2016-04-29 23:54:15 +0000 |
1331 | @@ -471,7 +471,7 @@ |
1332 | node = get_object_or_404( |
1333 | self.model, system_id=system_id, **kwargs) |
1334 | if user.has_perm(perm, node): |
1335 | - return node |
1336 | + return typecast_to_node_type(node) |
1337 | else: |
1338 | raise PermissionDenied() |
1339 | |
1340 | @@ -1823,7 +1823,8 @@ |
1341 | |
1342 | # boot_mode is something that tells the template whether this is |
1343 | # a PXE boot or a local HD boot. |
1344 | - if self.status == NODE_STATUS.DEPLOYED: |
1345 | + if (self.status == NODE_STATUS.DEPLOYED or |
1346 | + self.node_type != NODE_TYPE.MACHINE): |
1347 | power_params['boot_mode'] = 'local' |
1348 | else: |
1349 | power_params['boot_mode'] = 'pxe' |
1350 | @@ -1854,7 +1855,7 @@ |
1351 | return PowerInfo(False, False, False, None, None) |
1352 | else: |
1353 | if power_type == 'manual' or self.node_type in ( |
1354 | - NODE_TYPE.RACK_CONTROLLER, |
1355 | + NODE_TYPE.REGION_CONTROLLER, |
1356 | NODE_TYPE.REGION_AND_RACK_CONTROLLER): |
1357 | can_be_started = False |
1358 | can_be_stopped = False |
1359 | @@ -2598,7 +2599,8 @@ |
1360 | return "xinstall" |
1361 | else: |
1362 | return "local" |
1363 | - elif self.status == NODE_STATUS.DEPLOYED: |
1364 | + elif (self.status == NODE_STATUS.DEPLOYED or |
1365 | + self.node_type != NODE_TYPE.MACHINE): |
1366 | return "local" |
1367 | else: |
1368 | return "poweroff" |
1369 | |
1370 | === modified file 'src/maasserver/models/tests/test_node.py' |
1371 | --- src/maasserver/models/tests/test_node.py 2016-04-29 18:26:59 +0000 |
1372 | +++ src/maasserver/models/tests/test_node.py 2016-04-29 23:54:15 +0000 |
1373 | @@ -861,7 +861,7 @@ |
1374 | |
1375 | def test_get_effective_power_info_can_be_False_for_rack_controller(self): |
1376 | for node_type in (NODE_TYPE.REGION_AND_RACK_CONTROLLER, |
1377 | - NODE_TYPE.RACK_CONTROLLER): |
1378 | + NODE_TYPE.REGION_CONTROLLER): |
1379 | node = factory.make_Node(node_type=node_type) |
1380 | gepp = self.patch(node, "get_effective_power_parameters") |
1381 | # For manual the power can never be turned off or on. |
1382 | @@ -3426,6 +3426,14 @@ |
1383 | Node.objects.get_node_or_404( |
1384 | node.system_id, user, NODE_PERMISSION.VIEW)) |
1385 | |
1386 | + def test_get_node_or_404_returns_proper_node_object(self): |
1387 | + user = factory.make_User() |
1388 | + node = self.make_node(user, node_type=NODE_TYPE.RACK_CONTROLLER) |
1389 | + rack = Node.objects.get_node_or_404( |
1390 | + node.system_id, user, NODE_PERMISSION.VIEW) |
1391 | + self.assertEqual(node, rack) |
1392 | + self.assertIsInstance(rack, RackController) |
1393 | + |
1394 | def test_netboot_on(self): |
1395 | node = factory.make_Node(netboot=False) |
1396 | node.set_netboot(True) |
1397 | |
1398 | === modified file 'src/maasserver/node_action.py' |
1399 | --- src/maasserver/node_action.py 2015-12-05 01:43:40 +0000 |
1400 | +++ src/maasserver/node_action.py 2016-04-29 23:54:15 +0000 |
1401 | @@ -31,6 +31,7 @@ |
1402 | NODE_STATUS, |
1403 | NODE_STATUS_CHOICES_DICT, |
1404 | NODE_TYPE, |
1405 | + NODE_TYPE_CHOICES, |
1406 | POWER_STATE, |
1407 | ) |
1408 | from maasserver.exceptions import ( |
1409 | @@ -83,11 +84,10 @@ |
1410 | Will be used as the label for the action's button. |
1411 | """) |
1412 | |
1413 | - node_only = abstractproperty(""" |
1414 | - Can only be performed when the node type is node. |
1415 | + for_type = abstractproperty(""" |
1416 | + Can only be performed when the node type is in the for_type set. |
1417 | |
1418 | - A boolean value. True for only available for node_type node, false |
1419 | - otherwise. |
1420 | + A list of NODE_TYPEs which are applicable for this action. |
1421 | """) |
1422 | |
1423 | actionable_statuses = abstractproperty(""" |
1424 | @@ -124,9 +124,12 @@ |
1425 | If the node is not node_type node then actionable_statuses will not |
1426 | be used, as the status doesn't matter for a non-node type. |
1427 | """ |
1428 | - if self.node_only and not self.node.node_type == NODE_TYPE.MACHINE: |
1429 | - return False |
1430 | - if self.node.node_type == NODE_TYPE.MACHINE: |
1431 | + if self.node.node_type not in self.for_type: |
1432 | + return False |
1433 | + elif (self.node_permission == NODE_PERMISSION.ADMIN and |
1434 | + not self.user.is_superuser): |
1435 | + return False |
1436 | + elif self.node.node_type == NODE_TYPE.MACHINE: |
1437 | return self.node.status in self.actionable_statuses |
1438 | return True |
1439 | |
1440 | @@ -181,7 +184,7 @@ |
1441 | actionable_statuses = ALL_STATUSES |
1442 | permission = NODE_PERMISSION.EDIT |
1443 | node_permission = NODE_PERMISSION.ADMIN |
1444 | - node_only = False |
1445 | + for_type = {i for i, _ in enumerate(NODE_TYPE_CHOICES)} |
1446 | |
1447 | def execute(self): |
1448 | """Redirect to the delete view's confirmation page. |
1449 | @@ -201,7 +204,7 @@ |
1450 | actionable_statuses = ALL_STATUSES |
1451 | permission = NODE_PERMISSION.EDIT |
1452 | node_permission = NODE_PERMISSION.ADMIN |
1453 | - node_only = False |
1454 | + for_type = {i for i, _ in enumerate(NODE_TYPE_CHOICES)} |
1455 | |
1456 | def execute(self, zone_id=None): |
1457 | """See `NodeAction.execute`.""" |
1458 | @@ -225,7 +228,7 @@ |
1459 | NODE_STATUS.BROKEN, |
1460 | ) |
1461 | permission = NODE_PERMISSION.ADMIN |
1462 | - node_only = True |
1463 | + for_type = {NODE_TYPE.MACHINE} |
1464 | |
1465 | def execute( |
1466 | self, enable_ssh=False, skip_networking=False, |
1467 | @@ -256,7 +259,7 @@ |
1468 | NODE_STATUS.DEPLOYING |
1469 | ) |
1470 | permission = NODE_PERMISSION.ADMIN |
1471 | - node_only = True |
1472 | + for_type = {NODE_TYPE.MACHINE} |
1473 | |
1474 | def execute(self): |
1475 | """See `NodeAction.execute`.""" |
1476 | @@ -273,7 +276,7 @@ |
1477 | display_sentence = "acquired" |
1478 | actionable_statuses = (NODE_STATUS.READY, ) |
1479 | permission = NODE_PERMISSION.VIEW |
1480 | - node_only = True |
1481 | + for_type = {NODE_TYPE.MACHINE} |
1482 | |
1483 | def execute(self): |
1484 | """See `NodeAction.execute`.""" |
1485 | @@ -288,7 +291,7 @@ |
1486 | display_sentence = "deployed" |
1487 | actionable_statuses = (NODE_STATUS.READY, NODE_STATUS.ALLOCATED) |
1488 | permission = NODE_PERMISSION.VIEW |
1489 | - node_only = True |
1490 | + for_type = {NODE_TYPE.MACHINE} |
1491 | |
1492 | def execute(self, osystem=None, distro_series=None, hwe_kernel=None): |
1493 | """See `NodeAction.execute`.""" |
1494 | @@ -334,7 +337,7 @@ |
1495 | NODE_STATUS.BROKEN, |
1496 | ) |
1497 | permission = NODE_PERMISSION.EDIT |
1498 | - node_only = True |
1499 | + for_type = {NODE_TYPE.MACHINE, NODE_TYPE.RACK_CONTROLLER} |
1500 | |
1501 | def execute(self): |
1502 | """See `NodeAction.execute`.""" |
1503 | @@ -366,7 +369,7 @@ |
1504 | # Let a user power off a node in any non-active status. |
1505 | actionable_statuses = NON_MONITORED_STATUSES |
1506 | permission = NODE_PERMISSION.EDIT |
1507 | - node_only = True |
1508 | + for_type = {NODE_TYPE.MACHINE, NODE_TYPE.RACK_CONTROLLER} |
1509 | |
1510 | def execute(self): |
1511 | """See `NodeAction.execute`.""" |
1512 | @@ -395,7 +398,7 @@ |
1513 | NODE_STATUS.FAILED_DISK_ERASING, |
1514 | ) |
1515 | permission = NODE_PERMISSION.EDIT |
1516 | - node_only = True |
1517 | + for_type = {NODE_TYPE.MACHINE} |
1518 | |
1519 | def execute(self): |
1520 | """See `NodeAction.execute`.""" |
1521 | @@ -423,7 +426,7 @@ |
1522 | NODE_STATUS.DISK_ERASING, |
1523 | ] + FAILED_STATUSES |
1524 | permission = NODE_PERMISSION.EDIT |
1525 | - node_only = True |
1526 | + for_type = {NODE_TYPE.MACHINE, NODE_TYPE.RACK_CONTROLLER} |
1527 | |
1528 | def execute(self): |
1529 | """See `NodeAction.execute`.""" |
1530 | @@ -437,7 +440,7 @@ |
1531 | display_sentence = "marked fixed" |
1532 | actionable_statuses = (NODE_STATUS.BROKEN, ) |
1533 | permission = NODE_PERMISSION.ADMIN |
1534 | - node_only = True |
1535 | + for_type = {NODE_TYPE.MACHINE, NODE_TYPE.RACK_CONTROLLER} |
1536 | |
1537 | def execute(self): |
1538 | """See `NodeAction.execute`.""" |
1539 | |
1540 | === modified file 'src/maasserver/static/js/angular/controllers/node_details.js' |
1541 | --- src/maasserver/static/js/angular/controllers/node_details.js 2016-04-26 19:52:34 +0000 |
1542 | +++ src/maasserver/static/js/angular/controllers/node_details.js 2016-04-29 23:54:15 +0000 |
1543 | @@ -22,7 +22,7 @@ |
1544 | $scope.loaded = false; |
1545 | $scope.node = null; |
1546 | $scope.actionOption = null; |
1547 | - $scope.allActionOptions = GeneralManager.getData("node_actions"); |
1548 | + $scope.allActionOptions = GeneralManager.getData("machine_actions"); |
1549 | $scope.availableActionOptions = []; |
1550 | $scope.actionError = null; |
1551 | $scope.power_types = GeneralManager.getData("power_types"); |
1552 | |
1553 | === modified file 'src/maasserver/static/js/angular/controllers/nodes_list.js' |
1554 | --- src/maasserver/static/js/angular/controllers/nodes_list.js 2016-04-21 01:26:45 +0000 |
1555 | +++ src/maasserver/static/js/angular/controllers/nodes_list.js 2016-04-29 23:54:15 +0000 |
1556 | @@ -52,7 +52,7 @@ |
1557 | $scope.tabs.nodes.column = 'fqdn'; |
1558 | $scope.tabs.nodes.actionOption = null; |
1559 | $scope.tabs.nodes.takeActionOptions = GeneralManager.getData( |
1560 | - "node_actions"); |
1561 | + "machine_actions"); |
1562 | $scope.tabs.nodes.actionErrorCount = 0; |
1563 | $scope.tabs.nodes.actionProgress = { |
1564 | total: 0, |
1565 | @@ -114,8 +114,9 @@ |
1566 | $scope.tabs.controllers.filters = SearchService.getEmptyFilter(); |
1567 | $scope.tabs.controllers.column = 'fqdn'; |
1568 | $scope.tabs.controllers.actionOption = null; |
1569 | + // Rack controllers contain all options |
1570 | $scope.tabs.controllers.takeActionOptions = GeneralManager.getData( |
1571 | - "controller_actions"); |
1572 | + "rack_controller_actions"); |
1573 | $scope.tabs.controllers.actionErrorCount = 0; |
1574 | $scope.tabs.controllers.actionProgress = { |
1575 | total: 0, |
1576 | |
1577 | === modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details.js' |
1578 | --- src/maasserver/static/js/angular/controllers/tests/test_node_details.js 2016-04-26 19:52:34 +0000 |
1579 | +++ src/maasserver/static/js/angular/controllers/tests/test_node_details.js 2016-04-29 23:54:15 +0000 |
1580 | @@ -185,7 +185,7 @@ |
1581 | expect($scope.node).toBeNull(); |
1582 | expect($scope.actionOption).toBeNull(); |
1583 | expect($scope.allActionOptions).toBe( |
1584 | - GeneralManager.getData("node_actions")); |
1585 | + GeneralManager.getData("machine_actions")); |
1586 | expect($scope.availableActionOptions).toEqual([]); |
1587 | expect($scope.actionError).toBeNull(); |
1588 | expect($scope.osinfo).toBe(GeneralManager.getData("osinfo")); |
1589 | |
1590 | === modified file 'src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js' |
1591 | --- src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js 2016-03-28 13:54:47 +0000 |
1592 | +++ src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js 2016-04-29 23:54:15 +0000 |
1593 | @@ -306,7 +306,11 @@ |
1594 | SearchService.getEmptyFilter()); |
1595 | expect(tabScope.column).toBe("fqdn"); |
1596 | expect(tabScope.actionOption).toBeNull(); |
1597 | - expect(tabScope.takeActionOptions).toEqual([]); |
1598 | + // The controllers page uses a function so it can handle |
1599 | + // different controller types |
1600 | + if(tab !== "controllers") { |
1601 | + expect(tabScope.takeActionOptions).toEqual([]); |
1602 | + } |
1603 | expect(tabScope.actionErrorCount).toBe(0); |
1604 | expect(tabScope.zoneSelection).toBeNull(); |
1605 | |
1606 | |
1607 | === modified file 'src/maasserver/static/js/angular/factories/general.js' |
1608 | --- src/maasserver/static/js/angular/factories/general.js 2016-04-11 16:23:26 +0000 |
1609 | +++ src/maasserver/static/js/angular/factories/general.js 2016-04-29 23:54:15 +0000 |
1610 | @@ -21,8 +21,8 @@ |
1611 | function GeneralManager() { |
1612 | // Holds the available endpoints and its data. |
1613 | this._data = { |
1614 | - node_actions: { |
1615 | - method: "general.node_actions", |
1616 | + machine_actions: { |
1617 | + method: "general.machine_actions", |
1618 | data: [], |
1619 | loaded: false, |
1620 | polling: false, |
1621 | @@ -35,8 +35,22 @@ |
1622 | polling: false, |
1623 | nextPromise: null |
1624 | }, |
1625 | - controller_actions: { |
1626 | - method: "general.controller_actions", |
1627 | + region_controller_actions: { |
1628 | + method: "general.region_controller_actions", |
1629 | + data: [], |
1630 | + loaded: false, |
1631 | + polling: false, |
1632 | + nextPromise: null |
1633 | + }, |
1634 | + rack_controller_actions: { |
1635 | + method: "general.rack_controller_actions", |
1636 | + data: [], |
1637 | + loaded: false, |
1638 | + polling: false, |
1639 | + nextPromise: null |
1640 | + }, |
1641 | + region_and_rack_controller_actions: { |
1642 | + method: "general.region_and_rack_controller_actions", |
1643 | data: [], |
1644 | loaded: false, |
1645 | polling: false, |
1646 | |
1647 | === modified file 'src/maasserver/static/js/angular/factories/tests/test_general.js' |
1648 | --- src/maasserver/static/js/angular/factories/tests/test_general.js 2016-04-11 16:23:26 +0000 |
1649 | +++ src/maasserver/static/js/angular/factories/tests/test_general.js 2016-04-29 23:54:15 +0000 |
1650 | @@ -50,18 +50,19 @@ |
1651 | |
1652 | it("_data has expected keys", function() { |
1653 | expect(Object.keys(GeneralManager._data)).toEqual( |
1654 | - ["node_actions", "device_actions", "controller_actions", |
1655 | + ["machine_actions", "device_actions", "region_controller_actions", |
1656 | + "rack_controller_actions", "region_and_rack_controller_actions", |
1657 | "architectures", "hwe_kernels", "default_min_hwe_kernel", "osinfo", |
1658 | "bond_options", "version", "power_types"]); |
1659 | }); |
1660 | |
1661 | - it("_data.node_actions has correct data", function() { |
1662 | - var node_actions = GeneralManager._data.node_actions; |
1663 | - expect(node_actions.method).toBe("general.node_actions"); |
1664 | - expect(node_actions.data).toEqual([]); |
1665 | - expect(node_actions.loaded).toBe(false); |
1666 | - expect(node_actions.polling).toBe(false); |
1667 | - expect(node_actions.nextPromise).toBeNull(); |
1668 | + it("_data.machine_actions has correct data", function() { |
1669 | + var machine_actions = GeneralManager._data.machine_actions; |
1670 | + expect(machine_actions.method).toBe("general.machine_actions"); |
1671 | + expect(machine_actions.data).toEqual([]); |
1672 | + expect(machine_actions.loaded).toBe(false); |
1673 | + expect(machine_actions.polling).toBe(false); |
1674 | + expect(machine_actions.nextPromise).toBeNull(); |
1675 | }); |
1676 | |
1677 | it("_data.device_actions has correct data", function() { |
1678 | @@ -73,13 +74,37 @@ |
1679 | expect(device_actions.nextPromise).toBeNull(); |
1680 | }); |
1681 | |
1682 | - it("_data.controller_actions has correct data", function() { |
1683 | - var controller_actions = GeneralManager._data.controller_actions; |
1684 | - expect(controller_actions.method).toBe("general.controller_actions"); |
1685 | - expect(controller_actions.data).toEqual([]); |
1686 | - expect(controller_actions.loaded).toBe(false); |
1687 | - expect(controller_actions.polling).toBe(false); |
1688 | - expect(controller_actions.nextPromise).toBeNull(); |
1689 | + it("_data.region_controller_actions has correct data", function() { |
1690 | + var region_controller_actions = |
1691 | + GeneralManager._data.region_controller_actions; |
1692 | + expect(region_controller_actions.method).toBe( |
1693 | + "general.region_controller_actions"); |
1694 | + expect(region_controller_actions.data).toEqual([]); |
1695 | + expect(region_controller_actions.loaded).toBe(false); |
1696 | + expect(region_controller_actions.polling).toBe(false); |
1697 | + expect(region_controller_actions.nextPromise).toBeNull(); |
1698 | + }); |
1699 | + |
1700 | + it("_data.rack_controller_actions has correct data", function() { |
1701 | + var rack_controller_actions = |
1702 | + GeneralManager._data.rack_controller_actions; |
1703 | + expect(rack_controller_actions.method).toBe( |
1704 | + "general.rack_controller_actions"); |
1705 | + expect(rack_controller_actions.data).toEqual([]); |
1706 | + expect(rack_controller_actions.loaded).toBe(false); |
1707 | + expect(rack_controller_actions.polling).toBe(false); |
1708 | + expect(rack_controller_actions.nextPromise).toBeNull(); |
1709 | + }); |
1710 | + |
1711 | + it("_data.region_and_rack_controller_actions has correct data", function() { |
1712 | + var region_and_rack_controller_actions = |
1713 | + GeneralManager._data.region_and_rack_controller_actions; |
1714 | + expect(region_and_rack_controller_actions.method).toBe( |
1715 | + "general.region_and_rack_controller_actions"); |
1716 | + expect(region_and_rack_controller_actions.data).toEqual([]); |
1717 | + expect(region_and_rack_controller_actions.loaded).toBe(false); |
1718 | + expect(region_and_rack_controller_actions.polling).toBe(false); |
1719 | + expect(region_and_rack_controller_actions.nextPromise).toBeNull(); |
1720 | }); |
1721 | |
1722 | it("_data.architectures has correct data", function() { |
1723 | @@ -218,16 +243,16 @@ |
1724 | }); |
1725 | |
1726 | it("returns data object", function() { |
1727 | - expect(GeneralManager._getInternalData("node_actions")).toBe( |
1728 | - GeneralManager._data.node_actions); |
1729 | + expect(GeneralManager._getInternalData("machine_actions")).toBe( |
1730 | + GeneralManager._data.machine_actions); |
1731 | }); |
1732 | }); |
1733 | |
1734 | describe("getData", function() { |
1735 | |
1736 | it("returns data from internal data", function() { |
1737 | - expect(GeneralManager.getData("node_actions")).toBe( |
1738 | - GeneralManager._data.node_actions.data); |
1739 | + expect(GeneralManager.getData("machine_actions")).toBe( |
1740 | + GeneralManager._data.machine_actions.data); |
1741 | }); |
1742 | }); |
1743 | |
1744 | @@ -238,9 +263,12 @@ |
1745 | }); |
1746 | |
1747 | it("returns false if one false", function() { |
1748 | - GeneralManager._data.node_actions.loaded = true; |
1749 | + GeneralManager._data.machine_actions.loaded = true; |
1750 | GeneralManager._data.device_actions.loaded = true; |
1751 | - GeneralManager._data.controller_actions.loaded = true; |
1752 | + GeneralManager._data.rack_controller_actions.loaded = true; |
1753 | + GeneralManager._data.region_controller_actions.loaded = true; |
1754 | + GeneralManager._data.region_and_rack_controller_actions.loaded = |
1755 | + true; |
1756 | GeneralManager._data.architectures.loaded = true; |
1757 | GeneralManager._data.hwe_kernels.loaded = true; |
1758 | GeneralManager._data.osinfo.loaded = true; |
1759 | @@ -250,9 +278,12 @@ |
1760 | }); |
1761 | |
1762 | it("returns true if all true", function() { |
1763 | - GeneralManager._data.node_actions.loaded = true; |
1764 | + GeneralManager._data.machine_actions.loaded = true; |
1765 | GeneralManager._data.device_actions.loaded = true; |
1766 | - GeneralManager._data.controller_actions.loaded = true; |
1767 | + GeneralManager._data.rack_controller_actions.loaded = true; |
1768 | + GeneralManager._data.region_controller_actions.loaded = true; |
1769 | + GeneralManager._data.region_and_rack_controller_actions.loaded = |
1770 | + true; |
1771 | GeneralManager._data.architectures.loaded = true; |
1772 | GeneralManager._data.hwe_kernels.loaded = true; |
1773 | GeneralManager._data.default_min_hwe_kernel.loaded = true; |
1774 | @@ -268,8 +299,8 @@ |
1775 | |
1776 | it("returns loaded from internal data", function() { |
1777 | var loaded = {}; |
1778 | - GeneralManager._data.node_actions.loaded = loaded; |
1779 | - expect(GeneralManager.isDataLoaded("node_actions")).toBe(loaded); |
1780 | + GeneralManager._data.machine_actions.loaded = loaded; |
1781 | + expect(GeneralManager.isDataLoaded("machine_actions")).toBe(loaded); |
1782 | }); |
1783 | }); |
1784 | |
1785 | @@ -280,7 +311,7 @@ |
1786 | }); |
1787 | |
1788 | it("returns true if one true", function() { |
1789 | - GeneralManager._data.node_actions.polling = true; |
1790 | + GeneralManager._data.machine_actions.polling = true; |
1791 | GeneralManager._data.architectures.polling = false; |
1792 | GeneralManager._data.hwe_kernels.polling = false; |
1793 | GeneralManager._data.osinfo.polling = false; |
1794 | @@ -288,7 +319,7 @@ |
1795 | }); |
1796 | |
1797 | it("returns true if all true", function() { |
1798 | - GeneralManager._data.node_actions.polling = true; |
1799 | + GeneralManager._data.machine_actions.polling = true; |
1800 | GeneralManager._data.architectures.polling = true; |
1801 | GeneralManager._data.hwe_kernels.polling = true; |
1802 | GeneralManager._data.osinfo.polling = true; |
1803 | @@ -300,8 +331,9 @@ |
1804 | |
1805 | it("returns polling from internal data", function() { |
1806 | var polling = {}; |
1807 | - GeneralManager._data.node_actions.polling = polling; |
1808 | - expect(GeneralManager.isDataPolling("node_actions")).toBe(polling); |
1809 | + GeneralManager._data.machine_actions.polling = polling; |
1810 | + expect(GeneralManager.isDataPolling("machine_actions")).toBe( |
1811 | + polling); |
1812 | }); |
1813 | }); |
1814 | |
1815 | @@ -309,16 +341,16 @@ |
1816 | |
1817 | it("sets polling to true and calls _poll", function() { |
1818 | spyOn(GeneralManager, "_poll"); |
1819 | - GeneralManager.startPolling("node_actions"); |
1820 | - expect(GeneralManager._data.node_actions.polling).toBe(true); |
1821 | + GeneralManager.startPolling("machine_actions"); |
1822 | + expect(GeneralManager._data.machine_actions.polling).toBe(true); |
1823 | expect(GeneralManager._poll).toHaveBeenCalledWith( |
1824 | - GeneralManager._data.node_actions); |
1825 | + GeneralManager._data.machine_actions); |
1826 | }); |
1827 | |
1828 | it("does nothing if already polling", function() { |
1829 | spyOn(GeneralManager, "_poll"); |
1830 | - GeneralManager._data.node_actions.polling = true; |
1831 | - GeneralManager.startPolling("node_actions"); |
1832 | + GeneralManager._data.machine_actions.polling = true; |
1833 | + GeneralManager.startPolling("machine_actions"); |
1834 | expect(GeneralManager._poll).not.toHaveBeenCalled(); |
1835 | }); |
1836 | }); |
1837 | @@ -328,10 +360,10 @@ |
1838 | it("sets polling to false and cancels promise", function() { |
1839 | spyOn($timeout, "cancel"); |
1840 | var nextPromise = {}; |
1841 | - GeneralManager._data.node_actions.polling = true; |
1842 | - GeneralManager._data.node_actions.nextPromise = nextPromise; |
1843 | - GeneralManager.stopPolling("node_actions"); |
1844 | - expect(GeneralManager._data.node_actions.polling).toBe(false); |
1845 | + GeneralManager._data.machine_actions.polling = true; |
1846 | + GeneralManager._data.machine_actions.nextPromise = nextPromise; |
1847 | + GeneralManager.stopPolling("machine_actions"); |
1848 | + expect(GeneralManager._data.machine_actions.polling).toBe(false); |
1849 | expect($timeout.cancel).toHaveBeenCalledWith(nextPromise); |
1850 | }); |
1851 | }); |
1852 | @@ -341,32 +373,32 @@ |
1853 | it("calls callMethod with method", function() { |
1854 | spyOn(RegionConnection, "callMethod").and.returnValue( |
1855 | $q.defer().promise); |
1856 | - GeneralManager._loadData(GeneralManager._data.node_actions); |
1857 | + GeneralManager._loadData(GeneralManager._data.machine_actions); |
1858 | expect(RegionConnection.callMethod).toHaveBeenCalledWith( |
1859 | - GeneralManager._data.node_actions.method); |
1860 | + GeneralManager._data.machine_actions.method); |
1861 | }); |
1862 | |
1863 | it("sets loaded to true", function() { |
1864 | var defer = $q.defer(); |
1865 | spyOn(RegionConnection, "callMethod").and.returnValue( |
1866 | defer.promise); |
1867 | - GeneralManager._loadData(GeneralManager._data.node_actions); |
1868 | + GeneralManager._loadData(GeneralManager._data.machine_actions); |
1869 | defer.resolve([]); |
1870 | $rootScope.$digest(); |
1871 | - expect(GeneralManager._data.node_actions.loaded).toBe(true); |
1872 | + expect(GeneralManager._data.machine_actions.loaded).toBe(true); |
1873 | }); |
1874 | |
1875 | - it("sets node_actions data without changing reference", function() { |
1876 | + it("sets machine_actions data without changing reference", function() { |
1877 | var defer = $q.defer(); |
1878 | spyOn(RegionConnection, "callMethod").and.returnValue( |
1879 | defer.promise); |
1880 | - var actionsData = GeneralManager._data.node_actions.data; |
1881 | + var actionsData = GeneralManager._data.machine_actions.data; |
1882 | var newData = [makeName("action")]; |
1883 | - GeneralManager._loadData(GeneralManager._data.node_actions); |
1884 | + GeneralManager._loadData(GeneralManager._data.machine_actions); |
1885 | defer.resolve(newData); |
1886 | $rootScope.$digest(); |
1887 | - expect(GeneralManager._data.node_actions.data).toEqual(newData); |
1888 | - expect(GeneralManager._data.node_actions.data).toBe(actionsData); |
1889 | + expect(GeneralManager._data.machine_actions.data).toEqual(newData); |
1890 | + expect(GeneralManager._data.machine_actions.data).toBe(actionsData); |
1891 | }); |
1892 | |
1893 | it("sets osinfo data without changing reference", function() { |
1894 | @@ -388,7 +420,8 @@ |
1895 | defer.promise); |
1896 | spyOn(ErrorService, "raiseError"); |
1897 | var error = makeName("error"); |
1898 | - GeneralManager._loadData(GeneralManager._data.node_actions, true); |
1899 | + GeneralManager._loadData( |
1900 | + GeneralManager._data.machine_actions, true); |
1901 | defer.reject(error); |
1902 | $rootScope.$digest(); |
1903 | expect(ErrorService.raiseError).toHaveBeenCalledWith(error); |
1904 | @@ -400,7 +433,8 @@ |
1905 | defer.promise); |
1906 | spyOn(ErrorService, "raiseError"); |
1907 | var error = makeName("error"); |
1908 | - GeneralManager._loadData(GeneralManager._data.node_actions, false); |
1909 | + GeneralManager._loadData( |
1910 | + GeneralManager._data.machine_actions, false); |
1911 | defer.reject(error); |
1912 | $rootScope.$digest(); |
1913 | expect(ErrorService.raiseError).not.toHaveBeenCalled(); |
1914 | @@ -412,7 +446,7 @@ |
1915 | defer.promise); |
1916 | spyOn(ErrorService, "raiseError"); |
1917 | var error = makeName("error"); |
1918 | - GeneralManager._loadData(GeneralManager._data.node_actions); |
1919 | + GeneralManager._loadData(GeneralManager._data.machine_actions); |
1920 | defer.reject(error); |
1921 | $rootScope.$digest(); |
1922 | expect(ErrorService.raiseError).not.toHaveBeenCalled(); |
1923 | @@ -422,9 +456,10 @@ |
1924 | describe("_pollAgain", function() { |
1925 | |
1926 | it("sets nextPromise on data", function() { |
1927 | - GeneralManager._pollAgain(GeneralManager._data.node_actions); |
1928 | + GeneralManager._pollAgain(GeneralManager._data.machine_actions); |
1929 | expect( |
1930 | - GeneralManager._data.node_actions.nextPromise).not.toBeNull(); |
1931 | + GeneralManager._data.machine_actions.nextPromise |
1932 | + ).not.toBeNull(); |
1933 | }); |
1934 | }); |
1935 | |
1936 | @@ -433,29 +468,31 @@ |
1937 | it("calls _pollAgain with error timeout if not connected", function() { |
1938 | spyOn(RegionConnection, "isConnected").and.returnValue(false); |
1939 | spyOn(GeneralManager, "_pollAgain"); |
1940 | - GeneralManager._poll(GeneralManager._data.node_actions); |
1941 | + GeneralManager._poll(GeneralManager._data.machine_actions); |
1942 | expect(GeneralManager._pollAgain).toHaveBeenCalledWith( |
1943 | - GeneralManager._data.node_actions, |
1944 | + GeneralManager._data.machine_actions, |
1945 | GeneralManager._pollErrorTimeout); |
1946 | }); |
1947 | |
1948 | it("calls _loadData with raiseError false", function() { |
1949 | spyOn(GeneralManager, "_loadData").and.returnValue( |
1950 | $q.defer().promise); |
1951 | - GeneralManager._poll(GeneralManager._data.node_actions); |
1952 | + GeneralManager._poll(GeneralManager._data.machine_actions); |
1953 | expect(GeneralManager._loadData).toHaveBeenCalledWith( |
1954 | - GeneralManager._data.node_actions, false); |
1955 | + GeneralManager._data.machine_actions, false); |
1956 | }); |
1957 | |
1958 | - it("calls _pollAgain with empty timeout for node_actions", function() { |
1959 | + it( |
1960 | + "calls _pollAgain with empty timeout for machine_actions", |
1961 | + function() { |
1962 | var defer = $q.defer(); |
1963 | spyOn(GeneralManager, "_pollAgain"); |
1964 | spyOn(GeneralManager, "_loadData").and.returnValue(defer.promise); |
1965 | - GeneralManager._poll(GeneralManager._data.node_actions); |
1966 | + GeneralManager._poll(GeneralManager._data.machine_actions); |
1967 | defer.resolve([]); |
1968 | $rootScope.$digest(); |
1969 | expect(GeneralManager._pollAgain).toHaveBeenCalledWith( |
1970 | - GeneralManager._data.node_actions, |
1971 | + GeneralManager._data.machine_actions, |
1972 | GeneralManager._pollEmptyTimeout); |
1973 | }); |
1974 | |
1975 | @@ -471,17 +508,17 @@ |
1976 | GeneralManager._pollEmptyTimeout); |
1977 | }); |
1978 | |
1979 | - it("calls _pollAgain with timeout for node_actions", function() { |
1980 | + it("calls _pollAgain with timeout for machine_actions", function() { |
1981 | var defer = $q.defer(); |
1982 | spyOn(GeneralManager, "_pollAgain"); |
1983 | spyOn(GeneralManager, "_loadData").and.returnValue(defer.promise); |
1984 | - var node_actions = [makeName("action")]; |
1985 | - GeneralManager._data.node_actions.data = node_actions; |
1986 | - GeneralManager._poll(GeneralManager._data.node_actions); |
1987 | - defer.resolve(node_actions); |
1988 | + var machine_actions = [makeName("action")]; |
1989 | + GeneralManager._data.machine_actions.data = machine_actions; |
1990 | + GeneralManager._poll(GeneralManager._data.machine_actions); |
1991 | + defer.resolve(machine_actions); |
1992 | $rootScope.$digest(); |
1993 | expect(GeneralManager._pollAgain).toHaveBeenCalledWith( |
1994 | - GeneralManager._data.node_actions, |
1995 | + GeneralManager._data.machine_actions, |
1996 | GeneralManager._pollTimeout); |
1997 | }); |
1998 | |
1999 | @@ -491,12 +528,12 @@ |
2000 | spyOn(GeneralManager, "_loadData").and.returnValue(defer.promise); |
2001 | var error = makeName("error"); |
2002 | spyOn(console, "log"); |
2003 | - GeneralManager._poll(GeneralManager._data.node_actions); |
2004 | + GeneralManager._poll(GeneralManager._data.machine_actions); |
2005 | defer.reject(error); |
2006 | $rootScope.$digest(); |
2007 | expect(console.log).toHaveBeenCalledWith(error); |
2008 | expect(GeneralManager._pollAgain).toHaveBeenCalledWith( |
2009 | - GeneralManager._data.node_actions, |
2010 | + GeneralManager._data.machine_actions, |
2011 | GeneralManager._pollErrorTimeout); |
2012 | }); |
2013 | }); |
2014 | @@ -507,7 +544,7 @@ |
2015 | spyOn(GeneralManager, "_loadData").and.returnValue( |
2016 | $q.defer().promise); |
2017 | GeneralManager.loadItems(); |
2018 | - expect(GeneralManager._loadData.calls.count()).toBe(10); |
2019 | + expect(GeneralManager._loadData.calls.count()).toBe(12); |
2020 | }); |
2021 | |
2022 | it("resolve defer once all resolve", function(done) { |
2023 | @@ -521,6 +558,8 @@ |
2024 | $q.defer(), |
2025 | $q.defer(), |
2026 | $q.defer(), |
2027 | + $q.defer(), |
2028 | + $q.defer(), |
2029 | $q.defer() |
2030 | ]; |
2031 | var i = 0; |
2032 | |
2033 | === modified file 'src/maasserver/testing/sampledata.py' |
2034 | --- src/maasserver/testing/sampledata.py 2016-04-14 15:25:36 +0000 |
2035 | +++ src/maasserver/testing/sampledata.py 2016-04-29 23:54:15 +0000 |
2036 | @@ -299,6 +299,40 @@ |
2037 | alloc_type=IPADDRESS_TYPE.STICKY, ip="172.16.3.3", |
2038 | subnet=subnet_3, interface=bond0_10) |
2039 | |
2040 | + # Region controller (happy-region) |
2041 | + # eth0 - fabric 0 - untagged |
2042 | + # eth1 - fabric 0 - untagged |
2043 | + # eth2 - fabric 1 - untagged - 172.16.2.4/24 - static |
2044 | + # bond0 - fabric 0 - untagged - 172.16.1.4/24 - static |
2045 | + # bond0.10 - fabric 0 - 10 - 172.16.3.4/24 - static |
2046 | + region = factory.make_Node( |
2047 | + node_type=NODE_TYPE.REGION_CONTROLLER, |
2048 | + hostname="happy-region", interface=False) |
2049 | + eth0 = factory.make_Interface( |
2050 | + INTERFACE_TYPE.PHYSICAL, name="eth0", |
2051 | + node=region, vlan=fabric0_untagged) |
2052 | + eth1 = factory.make_Interface( |
2053 | + INTERFACE_TYPE.PHYSICAL, name="eth1", |
2054 | + node=region, vlan=fabric0_untagged) |
2055 | + eth2 = factory.make_Interface( |
2056 | + INTERFACE_TYPE.PHYSICAL, name="eth2", |
2057 | + node=region, vlan=fabric1_untagged) |
2058 | + bond0 = factory.make_Interface( |
2059 | + INTERFACE_TYPE.BOND, name="bond0", |
2060 | + node=region, vlan=fabric0_untagged, parents=[eth0, eth1]) |
2061 | + bond0_10 = factory.make_Interface( |
2062 | + INTERFACE_TYPE.VLAN, node=region, |
2063 | + vlan=fabric0_vlan10, parents=[bond0]) |
2064 | + factory.make_StaticIPAddress( |
2065 | + alloc_type=IPADDRESS_TYPE.STICKY, ip="172.16.1.4", |
2066 | + subnet=subnet_1, interface=bond0) |
2067 | + factory.make_StaticIPAddress( |
2068 | + alloc_type=IPADDRESS_TYPE.STICKY, ip="172.16.2.4", |
2069 | + subnet=subnet_2, interface=eth2) |
2070 | + factory.make_StaticIPAddress( |
2071 | + alloc_type=IPADDRESS_TYPE.STICKY, ip="172.16.3.4", |
2072 | + subnet=subnet_3, interface=bond0_10) |
2073 | + |
2074 | # Create one machine for every status. Each machine has a random interface |
2075 | # and storage configration. |
2076 | node_statuses = [ |
2077 | |
2078 | === modified file 'src/maasserver/tests/test_node_action.py' |
2079 | --- src/maasserver/tests/test_node_action.py 2016-03-28 13:54:47 +0000 |
2080 | +++ src/maasserver/tests/test_node_action.py 2016-04-29 23:54:15 +0000 |
2081 | @@ -74,7 +74,7 @@ |
2082 | display = "Action label" |
2083 | actionable_statuses = ALL_STATUSES |
2084 | permission = NODE_PERMISSION.VIEW |
2085 | - node_only = False |
2086 | + for_type = [NODE_TYPE.MACHINE] |
2087 | |
2088 | # For testing: an inhibition for inhibit() to return. |
2089 | fake_inhibition = None |
2090 | @@ -243,6 +243,14 @@ |
2091 | node = factory.make_Node(status=NODE_STATUS.BROKEN) |
2092 | self.assertFalse(MyAction(node, factory.make_User()).is_actionable()) |
2093 | |
2094 | + def test_is_actionable_checks_permission(self): |
2095 | + |
2096 | + class MyAction(FakeNodeAction): |
2097 | + node_permission = NODE_PERMISSION.ADMIN |
2098 | + |
2099 | + node = factory.make_Node() |
2100 | + self.assertFalse(MyAction(node, factory.make_User()).is_actionable()) |
2101 | + |
2102 | |
2103 | class TestDeleteAction(MAASServerTestCase): |
2104 | |
2105 | |
2106 | === modified file 'src/maasserver/websockets/handlers/general.py' |
2107 | --- src/maasserver/websockets/handlers/general.py 2016-03-28 13:54:47 +0000 |
2108 | +++ src/maasserver/websockets/handlers/general.py 2016-04-29 23:54:15 +0000 |
2109 | @@ -15,6 +15,7 @@ |
2110 | BOND_MODE_CHOICES, |
2111 | BOND_XMIT_HASH_POLICY_CHOICES, |
2112 | NODE_PERMISSION, |
2113 | + NODE_TYPE, |
2114 | ) |
2115 | from maasserver.models.bootresource import BootResource |
2116 | from maasserver.models.config import Config |
2117 | @@ -42,9 +43,11 @@ |
2118 | 'hwe_kernels', |
2119 | 'default_min_hwe_kernel', |
2120 | 'osinfo', |
2121 | - 'node_actions', |
2122 | + 'machine_actions', |
2123 | 'device_actions', |
2124 | - 'controller_actions', |
2125 | + 'rack_controller_actions', |
2126 | + 'region_controller_actions', |
2127 | + 'region_and_rack_controller_actions', |
2128 | 'random_hostname', |
2129 | 'bond_options', |
2130 | 'version', |
2131 | @@ -89,41 +92,42 @@ |
2132 | for name, action in actions.items() |
2133 | ] |
2134 | |
2135 | - def node_actions(self, params): |
2136 | - """Return all possible node actions.""" |
2137 | - if self.user.is_superuser: |
2138 | - actions = ACTIONS_DICT |
2139 | - else: |
2140 | - # Standard users will not be able to use any admin actions. Hide |
2141 | - # them as they will never be actionable on any node. |
2142 | - actions = dict() |
2143 | - for name, action in ACTIONS_DICT.items(): |
2144 | - permission = action.permission |
2145 | - if action.node_permission is not None: |
2146 | - permission = action.node_permission |
2147 | - if permission != NODE_PERMISSION.ADMIN: |
2148 | - actions[name] = action |
2149 | + def _node_actions(self, params, node_type): |
2150 | + # Only admins can perform controller actions |
2151 | + if (not self.user.is_superuser and node_type in [ |
2152 | + NODE_TYPE.RACK_CONTROLLER, NODE_TYPE.REGION_CONTROLLER, |
2153 | + NODE_TYPE.REGION_AND_RACK_CONTROLLER]): |
2154 | + return {} |
2155 | + |
2156 | + actions = dict() |
2157 | + for name, action in ACTIONS_DICT.items(): |
2158 | + if (action.node_permission == NODE_PERMISSION.ADMIN and |
2159 | + not self.user.is_superuser): |
2160 | + continue |
2161 | + elif node_type in action.for_type: |
2162 | + actions[name] = action |
2163 | return self.dehydrate_actions(actions) |
2164 | |
2165 | + def machine_actions(self, params): |
2166 | + """Return all possible machine actions.""" |
2167 | + return self._node_actions(params, NODE_TYPE.MACHINE) |
2168 | + |
2169 | def device_actions(self, params): |
2170 | """Return all possible device actions.""" |
2171 | - # Remove the actions that can only be performed on nodes. |
2172 | - actions = { |
2173 | - name: action |
2174 | - for name, action in ACTIONS_DICT.items() |
2175 | - if not action.node_only |
2176 | - } |
2177 | - return self.dehydrate_actions(actions) |
2178 | - |
2179 | - def controller_actions(self, params): |
2180 | - """Return all possible device actions.""" |
2181 | - # Remove the actions that can only be performed on nodes. |
2182 | - actions = { |
2183 | - name: action |
2184 | - for name, action in ACTIONS_DICT.items() |
2185 | - if not action.node_only |
2186 | - } |
2187 | - return self.dehydrate_actions(actions) |
2188 | + return self._node_actions(params, NODE_TYPE.DEVICE) |
2189 | + |
2190 | + def region_controller_actions(self, params): |
2191 | + """Return all possible region controller actions.""" |
2192 | + return self._node_actions(params, NODE_TYPE.REGION_CONTROLLER) |
2193 | + |
2194 | + def rack_controller_actions(self, params): |
2195 | + """Return all possible rack controller actions.""" |
2196 | + return self._node_actions(params, NODE_TYPE.RACK_CONTROLLER) |
2197 | + |
2198 | + def region_and_rack_controller_actions(self, params): |
2199 | + """Return all possible region and rack controller actions.""" |
2200 | + return self._node_actions( |
2201 | + params, NODE_TYPE.REGION_AND_RACK_CONTROLLER) |
2202 | |
2203 | def random_hostname(self, params): |
2204 | """Return a random hostname.""" |
2205 | |
2206 | === modified file 'src/maasserver/websockets/handlers/tests/test_device.py' |
2207 | --- src/maasserver/websockets/handlers/tests/test_device.py 2016-03-28 13:54:47 +0000 |
2208 | +++ src/maasserver/websockets/handlers/tests/test_device.py 2016-04-29 23:54:15 +0000 |
2209 | @@ -460,14 +460,14 @@ |
2210 | {"system_id": device.system_id, "action": "unknown"}) |
2211 | |
2212 | def test_action_performs_action(self): |
2213 | - user = factory.make_User() |
2214 | + user = factory.make_admin() |
2215 | device = factory.make_Node(owner=user, node_type=NODE_TYPE.DEVICE) |
2216 | handler = DeviceHandler(user, {}) |
2217 | handler.action({"system_id": device.system_id, "action": "delete"}) |
2218 | self.assertIsNone(reload_object(device)) |
2219 | |
2220 | def test_action_performs_action_passing_extra(self): |
2221 | - user = factory.make_User() |
2222 | + user = factory.make_admin() |
2223 | device = self.make_device_with_ip_address(owner=user) |
2224 | zone = factory.make_Zone() |
2225 | handler = DeviceHandler(user, {}) |
2226 | |
2227 | === modified file 'src/maasserver/websockets/handlers/tests/test_general.py' |
2228 | --- src/maasserver/websockets/handlers/tests/test_general.py 2016-03-28 13:54:47 +0000 |
2229 | +++ src/maasserver/websockets/handlers/tests/test_general.py 2016-04-29 23:54:15 +0000 |
2230 | @@ -11,7 +11,6 @@ |
2231 | BOND_MODE_CHOICES, |
2232 | BOND_XMIT_HASH_POLICY_CHOICES, |
2233 | BOOT_RESOURCE_TYPE, |
2234 | - NODE_PERMISSION, |
2235 | ) |
2236 | from maasserver.models import BootSourceCache |
2237 | from maasserver.models.config import Config |
2238 | @@ -105,40 +104,60 @@ |
2239 | } |
2240 | self.assertItemsEqual(expected_osinfo, handler.osinfo({})) |
2241 | |
2242 | - def test_node_actions_for_admin(self): |
2243 | + def test_machine_actions_for_admin(self): |
2244 | handler = GeneralHandler(factory.make_admin(), {}) |
2245 | actions_expected = self.dehydrate_actions(ACTIONS_DICT) |
2246 | - self.assertItemsEqual(actions_expected, handler.node_actions({})) |
2247 | + self.assertItemsEqual(actions_expected, handler.machine_actions({})) |
2248 | |
2249 | - def test_node_actions_for_non_admin(self): |
2250 | + def test_machine_actions_for_non_admin(self): |
2251 | handler = GeneralHandler(factory.make_User(), {}) |
2252 | - actions_expected = dict() |
2253 | - for name, action in ACTIONS_DICT.items(): |
2254 | - permission = action.permission |
2255 | - if action.node_permission is not None: |
2256 | - permission = action.node_permission |
2257 | - if permission != NODE_PERMISSION.ADMIN: |
2258 | - actions_expected[name] = action |
2259 | - actions_expected = self.dehydrate_actions(actions_expected) |
2260 | - self.assertItemsEqual(actions_expected, handler.node_actions({})) |
2261 | + self.assertItemsEqual( |
2262 | + ['release', 'mark-broken', 'on', 'deploy', 'mark-fixed', |
2263 | + 'commission', 'abort', 'acquire', 'off'], |
2264 | + [action['name'] for action in handler.machine_actions({})]) |
2265 | + |
2266 | + def test_device_actions_for_admin(self): |
2267 | + handler = GeneralHandler(factory.make_admin(), {}) |
2268 | + self.assertItemsEqual( |
2269 | + ['set-zone', 'delete'], |
2270 | + [action['name'] for action in handler.device_actions({})]) |
2271 | |
2272 | def test_device_actions_for_non_admin(self): |
2273 | handler = GeneralHandler(factory.make_User(), {}) |
2274 | - actions_expected = self.dehydrate_actions({ |
2275 | - name: action |
2276 | - for name, action in ACTIONS_DICT.items() |
2277 | - if not action.node_only |
2278 | - }) |
2279 | - self.assertItemsEqual(actions_expected, handler.device_actions({})) |
2280 | - |
2281 | - def test_controller_actions_for_non_admin(self): |
2282 | - handler = GeneralHandler(factory.make_User(), {}) |
2283 | - actions_expected = self.dehydrate_actions({ |
2284 | - name: action |
2285 | - for name, action in ACTIONS_DICT.items() |
2286 | - if not action.node_only |
2287 | - }) |
2288 | - self.assertItemsEqual(actions_expected, handler.controller_actions({})) |
2289 | + self.assertItemsEqual([], handler.device_actions({})) |
2290 | + |
2291 | + def test_region_controller_actions_for_admin(self): |
2292 | + handler = GeneralHandler(factory.make_admin(), {}) |
2293 | + self.assertItemsEqual( |
2294 | + ['set-zone', 'delete'], |
2295 | + [action['name'] |
2296 | + for action in handler.region_controller_actions({})]) |
2297 | + |
2298 | + def test_region_controller_actions_for_non_admin(self): |
2299 | + handler = GeneralHandler(factory.make_User(), {}) |
2300 | + self.assertItemsEqual([], handler.region_controller_actions({})) |
2301 | + |
2302 | + def test_rack_controller_actions_for_admin(self): |
2303 | + handler = GeneralHandler(factory.make_admin(), {}) |
2304 | + self.assertItemsEqual( |
2305 | + ['delete', 'mark-broken', 'mark-fixed', 'off', 'on', 'set-zone'], |
2306 | + [action['name'] for action in handler.rack_controller_actions({})]) |
2307 | + |
2308 | + def test_rack_controller_actions_for_non_admin(self): |
2309 | + handler = GeneralHandler(factory.make_User(), {}) |
2310 | + self.assertItemsEqual([], handler.rack_controller_actions({})) |
2311 | + |
2312 | + def test_region_and_rack_controller_actions_for_admin(self): |
2313 | + handler = GeneralHandler(factory.make_admin(), {}) |
2314 | + self.assertItemsEqual( |
2315 | + ['set-zone', 'delete'], |
2316 | + [action['name'] |
2317 | + for action in handler.region_and_rack_controller_actions({})]) |
2318 | + |
2319 | + def test_region_and_rack_controller_actions_for_non_admin(self): |
2320 | + handler = GeneralHandler(factory.make_User(), {}) |
2321 | + self.assertItemsEqual( |
2322 | + [], handler.region_and_rack_controller_actions({})) |
2323 | |
2324 | def test_random_hostname_checks_hostname_existence(self): |
2325 | existing_node = factory.make_Node(hostname="hostname") |
2326 | |
2327 | === modified file 'src/maasserver/websockets/handlers/tests/test_machine.py' |
2328 | --- src/maasserver/websockets/handlers/tests/test_machine.py 2016-03-31 23:34:55 +0000 |
2329 | +++ src/maasserver/websockets/handlers/tests/test_machine.py 2016-04-29 23:54:15 +0000 |
2330 | @@ -45,7 +45,10 @@ |
2331 | VolumeGroup, |
2332 | ) |
2333 | from maasserver.models.interface import Interface |
2334 | -from maasserver.models.node import Node |
2335 | +from maasserver.models.node import ( |
2336 | + Machine, |
2337 | + Node, |
2338 | +) |
2339 | from maasserver.models.nodeprobeddetails import get_single_probed_details |
2340 | from maasserver.models.partition import ( |
2341 | Partition, |
2342 | @@ -1909,9 +1912,9 @@ |
2343 | def test_action_performs_action_passing_extra(self): |
2344 | user = factory.make_User() |
2345 | factory.make_SSHKey(user) |
2346 | - self.patch(Node, 'on_network').return_value = True |
2347 | + self.patch(Machine, 'on_network').return_value = True |
2348 | node = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=user) |
2349 | - self.patch(Node, "_start").return_value = None |
2350 | + self.patch(Machine, "_start").return_value = None |
2351 | osystem = make_usable_osystem(self) |
2352 | handler = MachineHandler(user, {}) |
2353 | handler.action({ |
Not a full review, just one observation about:
+ # Disable power commands
+ query_power_state = power_on = power_off = mark_broken = mark_fixed = None
This is an abuse of inheritance. We do something like this for CRUD
methods when inheriting from OperationsHandler but that's because we
don't control the superclass, piston3.Resource.
Instead, break out query_power_state, power_on, power_off, mark_broken,
and mark_fixed to a mix-in and inherit only in the machine and rack
controller handler classes.