Merge lp:~trapnine/maas/events-1 into lp:~maas-committers/maas/trunk
- events-1
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Jeffrey C Jones | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 4282 | ||||
Proposed branch: | lp:~trapnine/maas/events-1 | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Diff against target: |
1438 lines (+438/-140) 9 files modified
src/maasserver/api/nodes.py (+4/-4) src/maasserver/fixtures/dev_fixture.yaml (+1/-1) src/maasserver/models/node.py (+115/-48) src/maasserver/models/tests/test_node.py (+226/-57) src/maasserver/node_action.py (+3/-4) src/maasserver/rpc/nodes.py (+1/-1) src/maasserver/tests/test_node_action.py (+14/-19) src/metadataserver/api.py (+8/-6) src/provisioningserver/events.py (+66/-0) |
||||
To merge this branch: | bzr merge lp:~trapnine/maas/events-1 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
MAAS Maintainers | Pending | ||
Andres Rodriguez | Pending | ||
Review via email: mp+271437@code.launchpad.net |
Commit message
Relevant node operations add a 'user request' entry into the event log.
An optional comment field is plumbed through the Node model methods and tests but aren't piped through the API in this branch (events-2 contains that change, MP pending).
User requests are displayed inline, hopefully before the events happening to the nodes. This allows one to see who (if anyone) triggered the events.
Description of the change
Relevant node operations add a 'user request' entry into the event log.
An optional comment field is plumbed through the Node model methods and tests but aren't piped through the API in this branch (events-2 contains that change, MP pending).
User requests are displayed inline, hopefully before the events happening to the nodes. This allows one to see who (if anyone) triggered the events.
Examples:
User aborting node commissioning - (admin)
User starting node commissioning - (admin)
User acquiring node - (admin)
User marking node fixed - (admin)
User marking node broken - (trapnine) - via web interface
User starting deployment - (trapnine)
Jeffrey C Jones (trapnine) wrote : | # |
fixed - please take another look
Blake Rouse (blake-rouse) wrote : | # |
Looks good. Thanks for all the fixes.
Preview Diff
1 | === modified file 'src/maasserver/api/nodes.py' |
2 | --- src/maasserver/api/nodes.py 2015-09-17 18:32:46 +0000 |
3 | +++ src/maasserver/api/nodes.py 2015-09-17 19:23:21 +0000 |
4 | @@ -498,7 +498,7 @@ |
5 | # postcondition is achieved, so call this success. |
6 | pass |
7 | elif node.status in RELEASABLE_STATUSES: |
8 | - node.release_or_erase() |
9 | + node.release_or_erase(request.user) |
10 | else: |
11 | raise NodeStateViolation( |
12 | "Node cannot be released in its current state ('%s')." |
13 | @@ -677,7 +677,7 @@ |
14 | user=request.user, system_id=system_id, perm=NODE_PERMISSION.EDIT) |
15 | error_description = get_optional_param( |
16 | request.POST, 'error_description', '') |
17 | - node.mark_broken(error_description) |
18 | + node.mark_broken(request.user, error_description) |
19 | return node |
20 | |
21 | @operation(idempotent=False) |
22 | @@ -690,7 +690,7 @@ |
23 | """ |
24 | node = Node.nodes.get_node_or_404( |
25 | user=request.user, system_id=system_id, perm=NODE_PERMISSION.ADMIN) |
26 | - node.mark_fixed() |
27 | + node.mark_fixed(request.user) |
28 | maaslog.info( |
29 | "%s: User %s marked node as fixed", node.hostname, |
30 | request.user.username) |
31 | @@ -1239,7 +1239,7 @@ |
32 | # Nothing to do. |
33 | pass |
34 | elif node.status in RELEASABLE_STATUSES: |
35 | - node.release_or_erase() |
36 | + node.release_or_erase(request.user) |
37 | released_ids.append(node.system_id) |
38 | else: |
39 | failed.append( |
40 | |
41 | === modified file 'src/maasserver/fixtures/dev_fixture.yaml' |
42 | --- src/maasserver/fixtures/dev_fixture.yaml 2015-08-14 13:48:59 +0000 |
43 | +++ src/maasserver/fixtures/dev_fixture.yaml 2015-09-17 19:23:21 +0000 |
44 | @@ -2933,7 +2933,7 @@ |
45 | disable_ipv4: false |
46 | distro_series: '' |
47 | error: finished [8/8] |
48 | - error_description: Manually marked as broken by user 'ubuntu' |
49 | + error_description: via web interface |
50 | hostname: eager-needle |
51 | installable: true |
52 | license_key: '' |
53 | |
54 | === modified file 'src/maasserver/models/node.py' |
55 | --- src/maasserver/models/node.py 2015-09-17 15:37:57 +0000 |
56 | +++ src/maasserver/models/node.py 2015-09-17 19:23:21 +0000 |
57 | @@ -120,6 +120,10 @@ |
58 | from metadataserver.enum import RESULT_TYPE |
59 | from netaddr import IPAddress |
60 | from piston.models import Token |
61 | +from provisioningserver.events import ( |
62 | + EVENT_DETAILS, |
63 | + EVENT_TYPES, |
64 | +) |
65 | from provisioningserver.logger import get_maas_logger |
66 | from provisioningserver.power import QUERY_POWER_TYPES |
67 | from provisioningserver.power.poweraction import UnknownPowerType |
68 | @@ -635,7 +639,22 @@ |
69 | """ |
70 | return timedelta(minutes=5).total_seconds() |
71 | |
72 | - def start_deployment(self): |
73 | + def _register_request_event(self, user, type_name, comment=None): |
74 | + """Register a node user request event.""" |
75 | + # don't register system generated non-user requests |
76 | + if user is not None: |
77 | + event_details = EVENT_DETAILS[type_name] |
78 | + description = "(%s)" % user.username |
79 | + if comment: |
80 | + description = "%s - %s" % (description, comment) |
81 | + # Avoid circular imports. |
82 | + from maasserver.models.event import Event |
83 | + Event.objects.register_event_and_event_type( |
84 | + self.system_id, type_name, type_level=event_details.level, |
85 | + type_description=event_details.description, |
86 | + event_description=description) |
87 | + |
88 | + def _start_deployment(self): |
89 | """Mark a node as being deployed.""" |
90 | self.status = NODE_STATUS.DEPLOYING |
91 | self.save() |
92 | @@ -669,8 +688,8 @@ |
93 | failed_status = get_failed_status(self.status) |
94 | if failed_status is not None: |
95 | timeout_timedelta = timedelta(seconds=context['timeout']) |
96 | - self.mark_failed( |
97 | - "Node operation '%s' timed out after %s." % ( |
98 | + self._mark_failed( |
99 | + None, "Node operation '%s' timed out after %s." % ( |
100 | ( |
101 | NODE_STATUS_CHOICES_DICT[self.status], |
102 | timeout_timedelta |
103 | @@ -931,6 +950,9 @@ |
104 | from metadataserver.user_data.commissioning import generate_user_data |
105 | from metadataserver.models import NodeResult |
106 | |
107 | + self._register_request_event( |
108 | + user, EVENT_TYPES.REQUEST_NODE_START_COMMISSIONING) |
109 | + |
110 | # Set the commissioning options on the node. |
111 | self.enable_ssh = enable_ssh |
112 | self.block_poweroff = block_poweroff |
113 | @@ -965,7 +987,7 @@ |
114 | # Node.start() has synchronous and asynchronous parts, so catch |
115 | # exceptions arising synchronously, and chain callbacks to the |
116 | # Deferred it returns for the asynchronous (post-commit) bits. |
117 | - starting = self.start(user, user_data=commissioning_user_data) |
118 | + starting = self._start(user, commissioning_user_data) |
119 | except Exception as error: |
120 | self.status = old_status |
121 | self.save() |
122 | @@ -1045,7 +1067,7 @@ |
123 | "must be started manually", hostname) |
124 | |
125 | @transactional |
126 | - def abort_commissioning(self, user): |
127 | + def abort_commissioning(self, user, comment=None): |
128 | """Power off a commissioning node and set its status to NEW. |
129 | |
130 | :return: a `Deferred` which contains the post-commit tasks that are |
131 | @@ -1058,6 +1080,9 @@ |
132 | "node %s is in state %s." |
133 | % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status])) |
134 | |
135 | + self._register_request_event( |
136 | + user, EVENT_TYPES.REQUEST_NODE_ABORT_COMMISSIONING, comment) |
137 | + |
138 | # Prepare a transition monitor for later. |
139 | monitor = TransitionMonitor.fromNode(self) |
140 | |
141 | @@ -1065,7 +1090,7 @@ |
142 | # Node.stop() has synchronous and asynchronous parts, so catch |
143 | # exceptions arising synchronously, and chain callbacks to the |
144 | # Deferred it returns for the asynchronous (post-commit) bits. |
145 | - stopping = self.stop(user) |
146 | + stopping = self._stop(user) |
147 | if self.owner is not None: |
148 | self.owner = None |
149 | self.save() |
150 | @@ -1104,7 +1129,7 @@ |
151 | return stopping.addErrback(eb_abort, self.hostname) |
152 | |
153 | @transactional |
154 | - def abort_deploying(self, user): |
155 | + def abort_deploying(self, user, comment=None): |
156 | """Power off a deploying node and set its status to ALLOCATED. |
157 | |
158 | :return: a `Deferred` which contains the post-commit tasks that are |
159 | @@ -1117,6 +1142,9 @@ |
160 | "node %s is in state %s." |
161 | % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status])) |
162 | |
163 | + self._register_request_event( |
164 | + user, EVENT_TYPES.REQUEST_NODE_ABORT_DEPLOYMENT, comment) |
165 | + |
166 | # Prepare a transition monitor for later. |
167 | monitor = TransitionMonitor.fromNode(self) |
168 | |
169 | @@ -1124,7 +1152,7 @@ |
170 | # Node.stop() has synchronous and asynchronous parts, so catch |
171 | # exceptions arising synchronously, and chain callbacks to the |
172 | # Deferred it returns for the asynchronous (post-commit) bits. |
173 | - stopping = self.stop(user) |
174 | + stopping = self._stop(user) |
175 | except Exception as error: |
176 | maaslog.error( |
177 | "%s: Error when aborting deployment: %s", |
178 | @@ -1410,7 +1438,7 @@ |
179 | |
180 | def acquire( |
181 | self, user, token=None, agent_name='', |
182 | - storage_layout=None, storage_layout_params={}): |
183 | + storage_layout=None, storage_layout_params={}, comment=None): |
184 | """Mark commissioned node as acquired by the given user and token.""" |
185 | assert self.owner is None |
186 | assert token is None or token.user == user |
187 | @@ -1421,6 +1449,9 @@ |
188 | "default_storage_layout") |
189 | self.set_storage_layout(storage_layout, params=storage_layout_params) |
190 | |
191 | + self._register_request_event( |
192 | + user, EVENT_TYPES.REQUEST_NODE_ACQUIRE, comment) |
193 | + |
194 | # Now allocate the node since the storage layout is setup correctly. |
195 | self.status = NODE_STATUS.ALLOCATED |
196 | self.owner = user |
197 | @@ -1464,7 +1495,7 @@ |
198 | maaslog.info("%s: moved from %s zone to %s zone." % ( |
199 | self.hostname, old_zone_name, self.zone.name)) |
200 | |
201 | - def start_disk_erasing(self, user): |
202 | + def start_disk_erasing(self, user, comment=None): |
203 | """Erase the disks on a node. |
204 | |
205 | :return: a `Deferred` which contains the post-commit tasks that are |
206 | @@ -1476,6 +1507,10 @@ |
207 | from metadataserver.user_data.disk_erasing import generate_user_data |
208 | |
209 | disk_erase_user_data = generate_user_data(node=self) |
210 | + |
211 | + self._register_request_event( |
212 | + user, EVENT_TYPES.REQUEST_NODE_ERASE_DISK, comment) |
213 | + |
214 | # Change the status of the node now to avoid races when starting |
215 | # nodes in bulk. |
216 | self.status = NODE_STATUS.DISK_ERASING |
217 | @@ -1485,7 +1520,7 @@ |
218 | # Node.start() has synchronous and asynchronous parts, so catch |
219 | # exceptions arising synchronously, and chain callbacks to the |
220 | # Deferred it returns for the asynchronous (post-commit) bits. |
221 | - starting = self.start(user, user_data=disk_erase_user_data) |
222 | + starting = self._start(user, disk_erase_user_data) |
223 | except Exception as error: |
224 | # We always mark the node as failed here, although we could |
225 | # potentially move it back to the state it was in previously. For |
226 | @@ -1545,7 +1580,7 @@ |
227 | "%s: Could not start node for disk erasure; it " |
228 | "must be started manually", hostname) |
229 | |
230 | - def abort_disk_erasing(self, user): |
231 | + def abort_disk_erasing(self, user, comment=None): |
232 | """Power off disk erasing node and set a failed status. |
233 | |
234 | :return: a `Deferred` which contains the post-commit tasks that are |
235 | @@ -1558,11 +1593,14 @@ |
236 | "node %s is in state %s." |
237 | % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status])) |
238 | |
239 | + self._register_request_event( |
240 | + user, EVENT_TYPES.REQUEST_NODE_ABORT_ERASE_DISK, comment) |
241 | + |
242 | try: |
243 | # Node.stop() has synchronous and asynchronous parts, so catch |
244 | # exceptions arising synchronously, and chain callbacks to the |
245 | # Deferred it returns for the asynchronous (post-commit) bits. |
246 | - stopping = self.stop(user) |
247 | + stopping = self._stop(user) |
248 | except Exception as error: |
249 | maaslog.error( |
250 | "%s: Error when aborting disk erasure: %s", |
251 | @@ -1613,26 +1651,29 @@ |
252 | callOut, maaslog.warning, "%s: Could not stop node to abort " |
253 | "disk erasure; it must be stopped manually", hostname) |
254 | |
255 | - def abort_operation(self, user): |
256 | + def abort_operation(self, user, comment=None): |
257 | """Abort the current operation. |
258 | - This currently only supports aborting Disk Erasing. |
259 | """ |
260 | if self.status == NODE_STATUS.DISK_ERASING: |
261 | - self.abort_disk_erasing(user) |
262 | + self.abort_disk_erasing(user, comment) |
263 | return |
264 | if self.status == NODE_STATUS.COMMISSIONING: |
265 | - self.abort_commissioning(user) |
266 | + self.abort_commissioning(user, comment) |
267 | return |
268 | if self.status == NODE_STATUS.DEPLOYING: |
269 | - self.abort_deploying(user) |
270 | + self.abort_deploying(user, comment) |
271 | return |
272 | - |
273 | raise NodeStateViolation( |
274 | "Cannot abort in current state: " |
275 | "node %s is in state %s." |
276 | % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status])) |
277 | |
278 | - def release(self): |
279 | + def release(self, user=None, comment=None): |
280 | + self._register_request_event( |
281 | + user, EVENT_TYPES.REQUEST_NODE_RELEASE, comment) |
282 | + self._release(user) |
283 | + |
284 | + def _release(self, user=None): |
285 | """Mark allocated or reserved node as available again and power off. |
286 | """ |
287 | maaslog.info("%s: Releasing node", self.hostname) |
288 | @@ -1643,7 +1684,7 @@ |
289 | # the issue this will cause. |
290 | if self.power_state != POWER_STATE.OFF: |
291 | try: |
292 | - self.stop(self.owner) |
293 | + self._stop(self.owner) |
294 | except Exception as ex: |
295 | maaslog.error( |
296 | "%s: Unable to shut node down: %s", self.hostname, |
297 | @@ -1694,16 +1735,15 @@ |
298 | # If this node has non-installable children, remove them. |
299 | self.children.all().delete() |
300 | |
301 | - def release_or_erase(self): |
302 | + def release_or_erase(self, user, comment=None): |
303 | """Either release the node or erase the node then release it, depending |
304 | on settings.""" |
305 | erase_on_release = Config.objects.get_config( |
306 | 'enable_disk_erasing_on_release') |
307 | if erase_on_release: |
308 | - self.start_disk_erasing(self.owner) |
309 | - return |
310 | - |
311 | - self.release() |
312 | + self.start_disk_erasing(user, comment) |
313 | + else: |
314 | + self.release(user, comment) |
315 | |
316 | def set_netboot(self, on=True): |
317 | """Set netboot on or off.""" |
318 | @@ -1725,7 +1765,12 @@ |
319 | arch, subarch = self.architecture.split('/') |
320 | return (arch, subarch) |
321 | |
322 | - def mark_failed(self, error_description): |
323 | + def mark_failed(self, user, comment=None): |
324 | + self._register_request_event( |
325 | + user, EVENT_TYPES.REQUEST_NODE_MARK_FAILED, comment) |
326 | + self._mark_failed(user, comment) |
327 | + |
328 | + def _mark_failed(self, user, comment=None): |
329 | """Mark this node as failed. |
330 | |
331 | The actual 'failed' state depends on the current status of the |
332 | @@ -1734,11 +1779,10 @@ |
333 | new_status = get_failed_status(self.status) |
334 | if new_status is not None: |
335 | self.status = new_status |
336 | - self.error_description = error_description |
337 | + self.error_description = comment if comment else '' |
338 | self.save() |
339 | maaslog.error( |
340 | - "%s: Marking node failed: %s", self.hostname, |
341 | - error_description) |
342 | + "%s: Marking node failed: %s", self.hostname, comment) |
343 | elif self.status == NODE_STATUS.NEW: |
344 | # Silently ignore, failing a new node makes no sense. |
345 | pass |
346 | @@ -1751,22 +1795,26 @@ |
347 | "be transitioned to a corresponding failed status." % |
348 | self.status) |
349 | |
350 | - def mark_broken(self, error_description): |
351 | + def mark_broken(self, user, comment=None): |
352 | """Mark this node as 'BROKEN'. |
353 | |
354 | If the node is allocated, release it first. |
355 | """ |
356 | + self._register_request_event( |
357 | + user, EVENT_TYPES.REQUEST_NODE_MARK_BROKEN, comment) |
358 | if self.status in RELEASABLE_STATUSES: |
359 | - self.release() |
360 | + self._release(user) |
361 | # release() normally sets the status to RELEASING and leaves the |
362 | # owner in place, override that here as we're broken. |
363 | self.status = NODE_STATUS.BROKEN |
364 | self.owner = None |
365 | - self.error_description = error_description |
366 | + self.error_description = comment if comment else '' |
367 | self.save() |
368 | |
369 | - def mark_fixed(self): |
370 | + def mark_fixed(self, user, comment=None): |
371 | """Mark a broken node as fixed and change its state to 'READY'.""" |
372 | + self._register_request_event( |
373 | + user, EVENT_TYPES.REQUEST_NODE_MARK_FIXED, comment) |
374 | if self.status != NODE_STATUS.BROKEN: |
375 | raise NodeStateViolation( |
376 | "Can't mark a non-broken node as 'Ready'.") |
377 | @@ -2116,11 +2164,23 @@ |
378 | return False |
379 | |
380 | @transactional |
381 | - def start(self, by_user, user_data=None): |
382 | + def start(self, user, user_data=None, comment=None): |
383 | + if not user.has_perm(NODE_PERMISSION.EDIT, self): |
384 | + # You can't start a node you don't own unless you're an admin. |
385 | + raise PermissionDenied() |
386 | + event = EVENT_TYPES.REQUEST_NODE_START |
387 | + # if status is ALLOCATED, this start is actually for a deplyment |
388 | + if self.status == NODE_STATUS.ALLOCATED: |
389 | + event = EVENT_TYPES.REQUEST_NODE_START_DEPLOYMENT |
390 | + self._register_request_event(user, event, comment) |
391 | + return self._start(user, user_data) |
392 | + |
393 | + @transactional |
394 | + def _start(self, user, user_data=None): |
395 | """Request on given user's behalf that the node be started up. |
396 | |
397 | - :param by_user: Requesting user. |
398 | - :type by_user: User_ |
399 | + :param user: Requesting user. |
400 | + :type user: User_ |
401 | :param user_data: Optional blob of user-data to be made available to |
402 | the node through the metadata service. If not given, any previous |
403 | user data is used. |
404 | @@ -2129,7 +2189,7 @@ |
405 | :raise StaticIPAddressExhaustion: if there are not enough IP addresses |
406 | left in the static range for this node to get all the addresses it |
407 | needs. |
408 | - :raise PermissionDenied: If `by_user` does not have permission to |
409 | + :raise PermissionDenied: If `user` does not have permission to |
410 | start this node. |
411 | |
412 | :return: a `Deferred` which contains the post-commit tasks that are |
413 | @@ -2142,7 +2202,7 @@ |
414 | # Avoid circular imports. |
415 | from metadataserver.models import NodeUserData |
416 | |
417 | - if not by_user.has_perm(NODE_PERMISSION.EDIT, self): |
418 | + if not user.has_perm(NODE_PERMISSION.EDIT, self): |
419 | # You can't start a node you don't own unless you're an admin. |
420 | raise PermissionDenied() |
421 | |
422 | @@ -2151,16 +2211,14 @@ |
423 | # node; the user may choose to start it manually. |
424 | NodeUserData.objects.set_user_data(self, user_data) |
425 | |
426 | - # Claim AUTO IP addresses for the node if it's ALLOCATED. |
427 | if self.status == NODE_STATUS.ALLOCATED: |
428 | + # Claim AUTO IP addresses for the node if it's ALLOCATED. |
429 | self.claim_auto_ips() |
430 | - |
431 | - if self.status == NODE_STATUS.ALLOCATED: |
432 | transition_monitor = ( |
433 | TransitionMonitor.fromNode(self) |
434 | .within(seconds=self.get_deployment_time()) |
435 | .status_should_be(self.status)) |
436 | - self.start_deployment() |
437 | + self._start_deployment() |
438 | else: |
439 | transition_monitor = None |
440 | |
441 | @@ -2185,15 +2243,24 @@ |
442 | self.nodegroup.uuid, power_info) |
443 | |
444 | @transactional |
445 | - def stop(self, by_user, stop_mode='hard'): |
446 | + def stop(self, user, stop_mode='hard', comment=None): |
447 | + if not user.has_perm(NODE_PERMISSION.EDIT, self): |
448 | + # You can't stop a node you don't own unless you're an admin. |
449 | + raise PermissionDenied() |
450 | + self._register_request_event( |
451 | + user, EVENT_TYPES.REQUEST_NODE_STOP, comment) |
452 | + return self._stop(user, stop_mode) |
453 | + |
454 | + @transactional |
455 | + def _stop(self, user, stop_mode='hard'): |
456 | """Request that the node be powered down. |
457 | |
458 | - :param by_user: Requesting user. |
459 | - :type by_user: User_ |
460 | + :param user: Requesting user. |
461 | + :type user: User_ |
462 | :param stop_mode: Power off mode - usually 'soft' or 'hard'. |
463 | :type stop_mode: unicode |
464 | |
465 | - :raise PermissionDenied: If `by_user` does not have permission to |
466 | + :raise PermissionDenied: If `user` does not have permission to |
467 | stop this node. |
468 | |
469 | :return: a `Deferred` which contains the post-commit tasks that are |
470 | @@ -2203,7 +2270,7 @@ |
471 | does not support it, `None` will be returned. The node must be |
472 | powered off manually. |
473 | """ |
474 | - if not by_user.has_perm(NODE_PERMISSION.EDIT, self): |
475 | + if not user.has_perm(NODE_PERMISSION.EDIT, self): |
476 | # You can't stop a node you don't own unless you're an admin. |
477 | raise PermissionDenied() |
478 | |
479 | |
480 | === modified file 'src/maasserver/models/tests/test_node.py' |
481 | --- src/maasserver/models/tests/test_node.py 2015-09-17 15:37:57 +0000 |
482 | +++ src/maasserver/models/tests/test_node.py 2015-09-17 19:23:21 +0000 |
483 | @@ -57,6 +57,7 @@ |
484 | PhysicalInterface, |
485 | UnknownInterface, |
486 | ) |
487 | +from maasserver.models.event import Event |
488 | from maasserver.models.node import PowerInfo |
489 | from maasserver.models.signals import power as node_query |
490 | from maasserver.models.timestampedmodel import now |
491 | @@ -107,6 +108,10 @@ |
492 | MagicMock, |
493 | sentinel, |
494 | ) |
495 | +from provisioningserver.events import ( |
496 | + EVENT_DETAILS, |
497 | + EVENT_TYPES, |
498 | +) |
499 | from provisioningserver.power import QUERY_POWER_TYPES |
500 | from provisioningserver.power.poweraction import UnknownPowerType |
501 | from provisioningserver.power.schema import JSON_POWER_TYPE_PARAMETERS |
502 | @@ -628,6 +633,16 @@ |
503 | (user, NODE_STATUS.ALLOCATED, agent_name), |
504 | (node.owner, node.status, node.agent_name)) |
505 | |
506 | + def test_acquire_logs_user_request(self): |
507 | + node = factory.make_Node(status=NODE_STATUS.READY, with_boot_disk=True) |
508 | + user = factory.make_User() |
509 | + token = create_auth_token(user) |
510 | + agent_name = factory.make_name('agent-name') |
511 | + register_event = self.patch(node, '_register_request_event') |
512 | + node.acquire(user, token, agent_name) |
513 | + self.assertThat(register_event, MockCalledOnceWith( |
514 | + user, EVENT_TYPES.REQUEST_NODE_ACQUIRE, None)) |
515 | + |
516 | def test_acquire_calls_set_storage_layout(self): |
517 | node = factory.make_Node(status=NODE_STATUS.READY, with_boot_disk=True) |
518 | user = factory.make_User() |
519 | @@ -731,7 +746,7 @@ |
520 | owner = factory.make_User() |
521 | node = factory.make_Node( |
522 | status=NODE_STATUS.ALLOCATED, owner=owner, agent_name=agent_name) |
523 | - node_start = self.patch(node, 'start') |
524 | + node_start = self.patch(node, '_start') |
525 | # Return a post-commit hook from Node.start(). |
526 | node_start.side_effect = lambda user, user_data: post_commit() |
527 | with post_commit_hooks: |
528 | @@ -740,7 +755,21 @@ |
529 | self.expectThat(node.status, Equals(NODE_STATUS.DISK_ERASING)) |
530 | self.expectThat(node.agent_name, Equals(agent_name)) |
531 | self.assertThat( |
532 | - node_start, MockCalledOnceWith(owner, user_data=ANY)) |
533 | + node_start, MockCalledOnceWith(owner, ANY)) |
534 | + |
535 | + def test_start_disk_erasing_logs_user_request(self): |
536 | + owner = factory.make_User() |
537 | + node = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=owner) |
538 | + node_start = self.patch(node, '_start') |
539 | + # Return a post-commit hook from Node.start(). |
540 | + node_start.side_effect = lambda user, user_data: post_commit() |
541 | + register_event = self.patch(node, '_register_request_event') |
542 | + with post_commit_hooks: |
543 | + node.start_disk_erasing(owner) |
544 | + self.assertThat( |
545 | + node_start, MockCalledOnceWith(owner, ANY)) |
546 | + self.assertThat(register_event, MockCalledOnceWith( |
547 | + owner, EVENT_TYPES.REQUEST_NODE_ERASE_DISK, None)) |
548 | |
549 | def test_abort_disk_erasing_changes_state_and_stops_node(self): |
550 | agent_name = factory.make_name('agent-name') |
551 | @@ -748,7 +777,7 @@ |
552 | node = factory.make_Node( |
553 | status=NODE_STATUS.DISK_ERASING, owner=owner, |
554 | agent_name=agent_name) |
555 | - node_stop = self.patch(node, 'stop') |
556 | + node_stop = self.patch(node, '_stop') |
557 | # Return a post-commit hook from Node.stop(). |
558 | node_stop.side_effect = lambda user: post_commit() |
559 | self.patch(Node, "_set_status") |
560 | @@ -765,6 +794,19 @@ |
561 | self.expectThat(node.owner, Equals(owner)) |
562 | self.expectThat(node.agent_name, Equals(agent_name)) |
563 | |
564 | + def test_abort_disk_erasing_logs_user_request(self): |
565 | + owner = factory.make_User() |
566 | + node = factory.make_Node(status=NODE_STATUS.DISK_ERASING, owner=owner) |
567 | + node_stop = self.patch(node, '_stop') |
568 | + # Return a post-commit hook from Node.stop(). |
569 | + node_stop.side_effect = lambda user: post_commit() |
570 | + self.patch(Node, "_set_status") |
571 | + register_event = self.patch(node, '_register_request_event') |
572 | + with post_commit_hooks: |
573 | + node.abort_disk_erasing(owner) |
574 | + self.assertThat(register_event, MockCalledOnceWith( |
575 | + owner, EVENT_TYPES.REQUEST_NODE_ABORT_ERASE_DISK, None)) |
576 | + |
577 | def test_start_disk_erasing_reverts_to_sane_state_on_error(self): |
578 | # If start_disk_erasing encounters an error when calling start(), it |
579 | # will transition the node to a sane state. Failures encountered in |
580 | @@ -773,7 +815,7 @@ |
581 | admin = factory.make_admin() |
582 | node = factory.make_Node(status=NODE_STATUS.ALLOCATED) |
583 | generate_user_data = self.patch(disk_erasing, 'generate_user_data') |
584 | - node_start = self.patch(node, 'start') |
585 | + node_start = self.patch(node, '_start') |
586 | node_start.side_effect = factory.make_exception() |
587 | |
588 | try: |
589 | @@ -786,7 +828,7 @@ |
590 | |
591 | self.assertThat( |
592 | node_start, MockCalledOnceWith( |
593 | - admin, user_data=generate_user_data.return_value)) |
594 | + admin, generate_user_data.return_value)) |
595 | self.assertEqual(NODE_STATUS.FAILED_DISK_ERASING, node.status) |
596 | |
597 | def test_start_disk_erasing_sets_status_on_post_commit_error(self): |
598 | @@ -795,7 +837,7 @@ |
599 | admin = factory.make_admin() |
600 | node = factory.make_Node(status=NODE_STATUS.ALLOCATED) |
601 | # Patch out some things that we don't want to do right now. |
602 | - self.patch(node, 'start').return_value = None |
603 | + self.patch(node, '_start').return_value = None |
604 | # Fake an error during the post-commit hook. |
605 | error_message = factory.make_name("error") |
606 | error_type = factory.make_exception_type() |
607 | @@ -824,7 +866,7 @@ |
608 | maaslog = self.patch(node_module, 'maaslog') |
609 | exception_type = factory.make_exception_type() |
610 | exception = exception_type(factory.make_name()) |
611 | - self.patch(node, 'start').side_effect = exception |
612 | + self.patch(node, '_start').side_effect = exception |
613 | self.assertRaises( |
614 | exception_type, node.start_disk_erasing, admin) |
615 | self.assertEqual(NODE_STATUS.FAILED_DISK_ERASING, node.status) |
616 | @@ -841,7 +883,7 @@ |
617 | agent_name=agent_name) |
618 | abort_commissioning = self.patch_autospec(node, 'abort_commissioning') |
619 | node.abort_operation(user) |
620 | - self.assertThat(abort_commissioning, MockCalledOnceWith(user)) |
621 | + self.assertThat(abort_commissioning, MockCalledOnceWith(user, None)) |
622 | |
623 | def test_abort_operation_aborts_disk_erasing(self): |
624 | agent_name = factory.make_name('agent-name') |
625 | @@ -851,7 +893,7 @@ |
626 | agent_name=agent_name) |
627 | abort_disk_erasing = self.patch_autospec(node, 'abort_disk_erasing') |
628 | node.abort_operation(owner) |
629 | - self.assertThat(abort_disk_erasing, MockCalledOnceWith(owner)) |
630 | + self.assertThat(abort_disk_erasing, MockCalledOnceWith(owner, None)) |
631 | |
632 | def test_abort_operation_aborts_deployment(self): |
633 | agent_name = factory.make_name('agent-name') |
634 | @@ -861,7 +903,21 @@ |
635 | agent_name=agent_name) |
636 | abort_deploying = self.patch_autospec(node, 'abort_deploying') |
637 | node.abort_operation(user) |
638 | - self.assertThat(abort_deploying, MockCalledOnceWith(user)) |
639 | + self.assertThat(abort_deploying, MockCalledOnceWith(user, None)) |
640 | + |
641 | + def test_abort_deployment_logs_user_request(self): |
642 | + agent_name = factory.make_name('agent-name') |
643 | + admin = factory.make_admin() |
644 | + node = factory.make_Node( |
645 | + status=NODE_STATUS.DEPLOYING, |
646 | + agent_name=agent_name) |
647 | + self.patch_autospec(node_module.TransitionMonitor, "stop") |
648 | + self.patch(Node, "_set_status") |
649 | + register_event = self.patch(node, '_register_request_event') |
650 | + with post_commit_hooks: |
651 | + node.abort_deploying(admin) |
652 | + self.assertThat(register_event, MockCalledOnceWith( |
653 | + admin, EVENT_TYPES.REQUEST_NODE_ABORT_DEPLOYMENT, None)) |
654 | |
655 | def test_abort_operation_raises_exception_for_unsupported_state(self): |
656 | agent_name = factory.make_name('agent-name') |
657 | @@ -878,7 +934,7 @@ |
658 | admin = factory.make_admin() |
659 | node = factory.make_Node( |
660 | status=NODE_STATUS.DISK_ERASING, power_type="virsh") |
661 | - node_stop = self.patch(node, 'stop') |
662 | + node_stop = self.patch(node, '_stop') |
663 | node_stop.side_effect = factory.make_exception() |
664 | |
665 | try: |
666 | @@ -898,7 +954,7 @@ |
667 | maaslog = self.patch(node_module, 'maaslog') |
668 | exception_class = factory.make_exception_type() |
669 | exception = exception_class(factory.make_name()) |
670 | - self.patch(node, 'stop').side_effect = exception |
671 | + self.patch(node, '_stop').side_effect = exception |
672 | self.assertRaises( |
673 | exception_class, node.abort_disk_erasing, admin) |
674 | self.assertEqual(NODE_STATUS.DISK_ERASING, node.status) |
675 | @@ -991,12 +1047,12 @@ |
676 | owner = factory.make_User() |
677 | node = factory.make_Node( |
678 | status=NODE_STATUS.ALLOCATED, owner=owner, agent_name=agent_name) |
679 | - self.patch(node, 'stop') |
680 | + self.patch(node, '_stop') |
681 | self.patch(node, 'start_transition_monitor') |
682 | node.power_state = POWER_STATE.OFF |
683 | with post_commit_hooks: |
684 | node.release() |
685 | - self.expectThat(node.stop, MockNotCalled()) |
686 | + self.expectThat(node._stop, MockNotCalled()) |
687 | self.expectThat(node.start_transition_monitor, MockNotCalled()) |
688 | self.expectThat(node.status, Equals(NODE_STATUS.READY)) |
689 | self.expectThat(node.owner, Is(None)) |
690 | @@ -1176,6 +1232,15 @@ |
691 | node.release() |
692 | self.assertTrue(node.netboot) |
693 | |
694 | + def test_release_logs_user_request(self): |
695 | + owner = factory.make_User() |
696 | + node = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=owner) |
697 | + register_event = self.patch(node, '_register_request_event') |
698 | + with post_commit_hooks: |
699 | + node.release(owner) |
700 | + self.assertThat(register_event, MockCalledOnceWith( |
701 | + owner, EVENT_TYPES.REQUEST_NODE_RELEASE, None)) |
702 | + |
703 | def test_release_clears_osystem_and_distro_series(self): |
704 | node = factory.make_Node( |
705 | status=NODE_STATUS.ALLOCATED, owner=factory.make_User()) |
706 | @@ -1192,7 +1257,7 @@ |
707 | status=NODE_STATUS.ALLOCATED, owner=user, power_type='virsh', |
708 | power_state=POWER_STATE.ON) |
709 | self.patch(node, 'start_transition_monitor') |
710 | - node_stop = self.patch(node, 'stop') |
711 | + node_stop = self.patch(node, '_stop') |
712 | node.release() |
713 | self.assertThat( |
714 | node_stop, MockCalledOnceWith(user)) |
715 | @@ -1203,7 +1268,7 @@ |
716 | status=NODE_STATUS.ALLOCATED, owner=user, power_type='virsh', |
717 | power_state=POWER_STATE.OFF) |
718 | self.patch(node, 'start_transition_monitor') |
719 | - node_stop = self.patch(node, 'stop') |
720 | + node_stop = self.patch(node, '_stop') |
721 | with post_commit_hooks: |
722 | node.release() |
723 | self.assertThat(node_stop, MockNotCalled()) |
724 | @@ -1239,7 +1304,7 @@ |
725 | power_state=POWER_STATE.ON, power_type='virsh') |
726 | release_auto_ips_later = self.patch_autospec( |
727 | node, "release_auto_ips_later") |
728 | - self.patch_autospec(node, 'stop') |
729 | + self.patch_autospec(node, '_stop') |
730 | self.patch(node, 'start_transition_monitor') |
731 | node.release() |
732 | self.assertThat( |
733 | @@ -1251,7 +1316,7 @@ |
734 | maaslog = self.patch(node_module, 'maaslog') |
735 | exception_class = factory.make_exception_type() |
736 | exception = exception_class(factory.make_name()) |
737 | - self.patch(node, 'stop').side_effect = exception |
738 | + self.patch(node, '_stop').side_effect = exception |
739 | self.assertRaises(exception_class, node.release) |
740 | self.assertEqual(NODE_STATUS.DEPLOYED, node.status) |
741 | self.assertThat( |
742 | @@ -1266,7 +1331,7 @@ |
743 | status=NODE_STATUS.DEPLOYED, power_type="virsh", |
744 | power_state=POWER_STATE.ON, |
745 | owner=factory.make_User()) |
746 | - node_stop = self.patch(node, 'stop') |
747 | + node_stop = self.patch(node, '_stop') |
748 | node_stop.side_effect = factory.make_exception() |
749 | |
750 | try: |
751 | @@ -1354,7 +1419,7 @@ |
752 | def test_start_commissioning_changes_status_and_starts_node(self): |
753 | node = factory.make_Node( |
754 | interface=True, status=NODE_STATUS.NEW, power_type='ether_wake') |
755 | - node_start = self.patch(node, 'start') |
756 | + node_start = self.patch(node, '_start') |
757 | # Return a post-commit hook from Node.start(). |
758 | node_start.side_effect = lambda user, user_data: post_commit() |
759 | admin = factory.make_admin() |
760 | @@ -1365,8 +1430,7 @@ |
761 | 'status': NODE_STATUS.COMMISSIONING, |
762 | } |
763 | self.assertAttributes(node, expected_attrs) |
764 | - self.assertThat(node_start, MockCalledOnceWith( |
765 | - admin, user_data=ANY)) |
766 | + self.assertThat(node_start, MockCalledOnceWith(admin, ANY)) |
767 | |
768 | def test_start_commissioning_sets_options(self): |
769 | node = factory.make_Node( |
770 | @@ -1392,7 +1456,7 @@ |
771 | |
772 | def test_start_commissioning_sets_user_data(self): |
773 | node = factory.make_Node(status=NODE_STATUS.NEW) |
774 | - node_start = self.patch(node, 'start') |
775 | + node_start = self.patch(node, '_start') |
776 | node_start.side_effect = lambda user, user_data: post_commit() |
777 | user_data = factory.make_string().encode('ascii') |
778 | generate_user_data = self.patch( |
779 | @@ -1401,8 +1465,7 @@ |
780 | admin = factory.make_admin() |
781 | node.start_commissioning(admin) |
782 | post_commit_hooks.reset() # Ignore these for now. |
783 | - self.assertThat(node_start, MockCalledOnceWith( |
784 | - admin, user_data=user_data)) |
785 | + self.assertThat(node_start, MockCalledOnceWith(admin, user_data)) |
786 | |
787 | def test_start_commissioning_clears_node_commissioning_results(self): |
788 | node = factory.make_Node(status=NODE_STATUS.NEW) |
789 | @@ -1458,7 +1521,7 @@ |
790 | admin = factory.make_admin() |
791 | node = factory.make_Node(status=NODE_STATUS.NEW) |
792 | generate_user_data = self.patch(commissioning, 'generate_user_data') |
793 | - node_start = self.patch(node, 'start') |
794 | + node_start = self.patch(node, '_start') |
795 | node_start.side_effect = factory.make_exception() |
796 | |
797 | try: |
798 | @@ -1471,8 +1534,7 @@ |
799 | |
800 | self.assertThat( |
801 | node_start, |
802 | - MockCalledOnceWith( |
803 | - admin, user_data=generate_user_data.return_value)) |
804 | + MockCalledOnceWith(admin, generate_user_data.return_value)) |
805 | self.assertEqual(NODE_STATUS.NEW, node.status) |
806 | |
807 | def test_start_commissioning_reverts_status_on_post_commit_error(self): |
808 | @@ -1485,7 +1547,7 @@ |
809 | node = factory.make_Node(status=status) |
810 | # Patch out some things that we don't want to do right now. |
811 | self.patch(node, '_start_transition_monitor_async') |
812 | - self.patch(node, 'start').return_value = None |
813 | + self.patch(node, '_start').return_value = None |
814 | # Fake an error during the post-commit hook. |
815 | error_message = factory.make_name("error") |
816 | error_type = factory.make_exception_type() |
817 | @@ -1512,7 +1574,7 @@ |
818 | node = factory.make_Node(status=NODE_STATUS.NEW) |
819 | maaslog = self.patch(node_module, 'maaslog') |
820 | exception = NoConnectionsAvailable(factory.make_name()) |
821 | - self.patch(node, 'start').side_effect = exception |
822 | + self.patch(node, '_start').side_effect = exception |
823 | self.assertRaises( |
824 | NoConnectionsAvailable, node.start_commissioning, admin) |
825 | self.assertEqual(NODE_STATUS.NEW, node.status) |
826 | @@ -1521,6 +1583,20 @@ |
827 | "%s: Could not start node for commissioning: %s", |
828 | node.hostname, exception)) |
829 | |
830 | + def test_start_commissioning_logs_user_request(self): |
831 | + node = factory.make_Node( |
832 | + interface=True, status=NODE_STATUS.NEW, power_type='ether_wake') |
833 | + register_event = self.patch(node, '_register_request_event') |
834 | + node_start = self.patch(node, '_start') |
835 | + # Return a post-commit hook from Node.start(). |
836 | + node_start.side_effect = lambda user, user_data: post_commit() |
837 | + admin = factory.make_admin() |
838 | + node.start_commissioning(admin) |
839 | + post_commit_hooks.reset() # Ignore these for now. |
840 | + node = reload_object(node) |
841 | + self.assertThat(register_event, MockCalledOnceWith( |
842 | + admin, EVENT_TYPES.REQUEST_NODE_START_COMMISSIONING)) |
843 | + |
844 | def test_abort_commissioning_reverts_to_sane_state_on_error(self): |
845 | # If abort commissioning hits an error when trying to stop the |
846 | # node, it will revert the node to the state it was in before |
847 | @@ -1528,7 +1604,7 @@ |
848 | admin = factory.make_admin() |
849 | node = factory.make_Node( |
850 | status=NODE_STATUS.COMMISSIONING, power_type="virsh") |
851 | - node_stop = self.patch(node, 'stop') |
852 | + node_stop = self.patch(node, '_stop') |
853 | node_stop.side_effect = factory.make_exception() |
854 | |
855 | try: |
856 | @@ -1576,13 +1652,24 @@ |
857 | [monitor], _ = monitor_stop.call_args # Extract `self`. |
858 | self.assertAttributes(monitor, {"system_id": node.system_id}) |
859 | |
860 | + def test_abort_commissioning_logs_user_request(self): |
861 | + node = factory.make_Node(status=NODE_STATUS.COMMISSIONING) |
862 | + admin = factory.make_admin() |
863 | + self.patch_autospec(node_module.TransitionMonitor, "stop") |
864 | + self.patch(Node, "_set_status") |
865 | + register_event = self.patch(node, '_register_request_event') |
866 | + with post_commit_hooks: |
867 | + node.abort_commissioning(admin) |
868 | + self.assertThat(register_event, MockCalledOnceWith( |
869 | + admin, EVENT_TYPES.REQUEST_NODE_ABORT_COMMISSIONING, None)) |
870 | + |
871 | def test_abort_commissioning_logs_and_raises_errors_in_stopping(self): |
872 | admin = factory.make_admin() |
873 | node = factory.make_Node(status=NODE_STATUS.COMMISSIONING) |
874 | maaslog = self.patch(node_module, 'maaslog') |
875 | exception_class = factory.make_exception_type() |
876 | exception = exception_class(factory.make_name()) |
877 | - self.patch(node, 'stop').side_effect = exception |
878 | + self.patch(node, '_stop').side_effect = exception |
879 | self.assertRaises( |
880 | exception_class, node.abort_commissioning, admin) |
881 | self.assertEqual(NODE_STATUS.COMMISSIONING, node.status) |
882 | @@ -1596,7 +1683,7 @@ |
883 | status=NODE_STATUS.COMMISSIONING, power_type='virsh') |
884 | admin = factory.make_admin() |
885 | |
886 | - node_stop = self.patch(node, 'stop') |
887 | + node_stop = self.patch(node, '_stop') |
888 | # Return a post-commit hook from Node.stop(). |
889 | node_stop.side_effect = lambda user: post_commit() |
890 | self.patch(Node, "_set_status") |
891 | @@ -1642,7 +1729,7 @@ |
892 | enable_ssh=True) |
893 | admin = factory.make_admin() |
894 | |
895 | - node_stop = self.patch(node, 'stop') |
896 | + node_stop = self.patch(node, '_stop') |
897 | # Return a post-commit hook from Node.stop(). |
898 | node_stop.side_effect = lambda user: post_commit() |
899 | self.patch(Node, "_set_status") |
900 | @@ -1780,16 +1867,26 @@ |
901 | for status in NODE_FAILURE_STATUS_TRANSITIONS |
902 | } |
903 | for node in nodes_mapping.values(): |
904 | - node.mark_failed(factory.make_name('error-description')) |
905 | + node.mark_failed(None, factory.make_name('error-description')) |
906 | self.assertEqual( |
907 | NODE_FAILURE_STATUS_TRANSITIONS, |
908 | {status: node.status for status, node in nodes_mapping.items()}) |
909 | |
910 | + def test_mark_failed_logs_user_request(self): |
911 | + owner = factory.make_User() |
912 | + self.disable_node_query() |
913 | + node = factory.make_Node(status=NODE_STATUS.COMMISSIONING, owner=owner) |
914 | + description = factory.make_name('error-description') |
915 | + register_event = self.patch(node, '_register_request_event') |
916 | + node.mark_failed(owner, description) |
917 | + self.assertThat(register_event, MockCalledOnceWith( |
918 | + owner, EVENT_TYPES.REQUEST_NODE_MARK_FAILED, description)) |
919 | + |
920 | def test_mark_failed_updates_error_description(self): |
921 | self.disable_node_query() |
922 | node = factory.make_Node(status=NODE_STATUS.COMMISSIONING) |
923 | description = factory.make_name('error-description') |
924 | - node.mark_failed(description) |
925 | + node.mark_failed(None, description) |
926 | self.assertEqual(description, reload_object(node).error_description) |
927 | |
928 | def test_mark_failed_raises_for_unauthorized_node_status(self): |
929 | @@ -1799,63 +1896,82 @@ |
930 | status = factory.pick_choice(NODE_STATUS_CHOICES, but_not=but_not) |
931 | node = factory.make_Node(status=status) |
932 | description = factory.make_name('error-description') |
933 | - self.assertRaises(NodeStateViolation, node.mark_failed, description) |
934 | + self.assertRaises( |
935 | + NodeStateViolation, node.mark_failed, None, description) |
936 | |
937 | def test_mark_failed_ignores_if_already_failed(self): |
938 | status = random.choice([ |
939 | NODE_STATUS.FAILED_DEPLOYMENT, NODE_STATUS.FAILED_COMMISSIONING]) |
940 | node = factory.make_Node(status=status) |
941 | description = factory.make_name('error-description') |
942 | - node.mark_failed(description) |
943 | + node.mark_failed(None, description) |
944 | self.assertEqual(status, node.status) |
945 | |
946 | def test_mark_failed_ignores_if_status_is_NEW(self): |
947 | node = factory.make_Node(status=NODE_STATUS.NEW) |
948 | description = factory.make_name('error-description') |
949 | - node.mark_failed(description) |
950 | + node.mark_failed(None, description) |
951 | self.assertEqual(NODE_STATUS.NEW, node.status) |
952 | |
953 | def test_mark_broken_changes_status_to_broken(self): |
954 | - node = factory.make_Node( |
955 | - status=NODE_STATUS.NEW, owner=factory.make_User()) |
956 | - node.mark_broken(factory.make_name('error-description')) |
957 | + user = factory.make_User() |
958 | + node = factory.make_Node(status=NODE_STATUS.NEW, owner=user) |
959 | + node.mark_broken(user, factory.make_name('error-description')) |
960 | self.assertEqual(NODE_STATUS.BROKEN, reload_object(node).status) |
961 | |
962 | + def test_mark_broken_logs_user_request(self): |
963 | + owner = factory.make_User() |
964 | + node = factory.make_Node(status=NODE_STATUS.NEW, owner=owner) |
965 | + description = factory.make_name('error-description') |
966 | + register_event = self.patch(node, '_register_request_event') |
967 | + node.mark_broken(owner, description) |
968 | + self.assertThat(register_event, MockCalledOnceWith( |
969 | + owner, EVENT_TYPES.REQUEST_NODE_MARK_BROKEN, description)) |
970 | + |
971 | def test_mark_broken_releases_allocated_node(self): |
972 | - node = factory.make_Node( |
973 | - status=NODE_STATUS.ALLOCATED, owner=factory.make_User()) |
974 | + user = factory.make_User() |
975 | + node = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=user) |
976 | err_desc = factory.make_name('error-description') |
977 | - release = self.patch(node, 'release') |
978 | - node.mark_broken(err_desc) |
979 | + release = self.patch(node, '_release') |
980 | + node.mark_broken(user, err_desc) |
981 | self.expectThat(node.owner, Is(None)) |
982 | - self.assertThat(release, MockCalledOnceWith()) |
983 | + self.assertThat(release, MockCalledOnceWith(user)) |
984 | |
985 | def test_mark_fixed_sets_default_osystem_and_distro_series(self): |
986 | node = factory.make_Node(status=NODE_STATUS.BROKEN) |
987 | node.osystem = factory.make_name('osystem') |
988 | node.distro_series = factory.make_name('distro_series') |
989 | - node.mark_fixed() |
990 | + node.mark_fixed(factory.make_User()) |
991 | expected_osystem = expected_distro_series = '' |
992 | self.expectThat(expected_osystem, Equals(node.osystem)) |
993 | self.expectThat(expected_distro_series, Equals(node.distro_series)) |
994 | |
995 | def test_mark_fixed_changes_status(self): |
996 | node = factory.make_Node(status=NODE_STATUS.BROKEN) |
997 | - node.mark_fixed() |
998 | + node.mark_fixed(factory.make_User()) |
999 | self.assertEqual(NODE_STATUS.READY, reload_object(node).status) |
1000 | |
1001 | + def test_mark_fixed_logs_user_request(self): |
1002 | + owner = factory.make_User() |
1003 | + node = factory.make_Node(status=NODE_STATUS.BROKEN, owner=owner) |
1004 | + register_event = self.patch(node, '_register_request_event') |
1005 | + node.mark_fixed(owner) |
1006 | + self.assertThat(register_event, MockCalledOnceWith( |
1007 | + owner, EVENT_TYPES.REQUEST_NODE_MARK_FIXED, None)) |
1008 | + |
1009 | def test_mark_fixed_updates_error_description(self): |
1010 | description = factory.make_name('error-description') |
1011 | node = factory.make_Node( |
1012 | status=NODE_STATUS.BROKEN, error_description=description) |
1013 | - node.mark_fixed() |
1014 | + node.mark_fixed(factory.make_User()) |
1015 | self.assertEqual('', reload_object(node).error_description) |
1016 | |
1017 | def test_mark_fixed_fails_if_node_isnt_broken(self): |
1018 | status = factory.pick_choice( |
1019 | NODE_STATUS_CHOICES, but_not=[NODE_STATUS.BROKEN]) |
1020 | node = factory.make_Node(status=status) |
1021 | - self.assertRaises(NodeStateViolation, node.mark_fixed) |
1022 | + self.assertRaises( |
1023 | + NodeStateViolation, node.mark_fixed, factory.make_User()) |
1024 | |
1025 | def test_mark_fixed_clears_installation_results(self): |
1026 | node = factory.make_Node(status=NODE_STATUS.BROKEN) |
1027 | @@ -1863,7 +1979,7 @@ |
1028 | self.assertEquals( |
1029 | [node_result], list(NodeResult.objects.filter( |
1030 | node=node, result_type=RESULT_TYPE.INSTALLATION))) |
1031 | - node.mark_fixed() |
1032 | + node.mark_fixed(factory.make_User()) |
1033 | self.assertEquals( |
1034 | [], list(NodeResult.objects.filter( |
1035 | node=node, result_type=RESULT_TYPE.INSTALLATION))) |
1036 | @@ -1946,9 +2062,17 @@ |
1037 | |
1038 | def test_start_deployment_changes_state(self): |
1039 | node = factory.make_Node(status=NODE_STATUS.ALLOCATED) |
1040 | - node.start_deployment() |
1041 | + node._start_deployment() |
1042 | self.assertEqual(NODE_STATUS.DEPLOYING, reload_object(node).status) |
1043 | |
1044 | + def test_start_deployment_logs_user_request(self): |
1045 | + admin = factory.make_admin() |
1046 | + node = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=admin) |
1047 | + register_event = self.patch(node, '_register_request_event') |
1048 | + node.start(admin) |
1049 | + self.assertThat(register_event, MockCalledOnceWith( |
1050 | + admin, EVENT_TYPES.REQUEST_NODE_START_DEPLOYMENT, None)) |
1051 | + |
1052 | def test_handle_monitor_expired_marks_node_as_failed(self): |
1053 | self.disable_node_query() |
1054 | status = random.choice(MONITORED_STATUSES) |
1055 | @@ -2160,6 +2284,32 @@ |
1056 | node.boot_interface = interface |
1057 | node.save() |
1058 | |
1059 | + def test__register_request_event_saves_event(self): |
1060 | + node = factory.make_Node() |
1061 | + user = factory.make_User() |
1062 | + log_mock = self.patch_autospec( |
1063 | + Event.objects, 'register_event_and_event_type') |
1064 | + event_name = EVENT_TYPES.REQUEST_NODE_START |
1065 | + event_details = EVENT_DETAILS[event_name] |
1066 | + comment = factory.make_name("comment") |
1067 | + event_description = "(%s) - %s" % (user.username, comment) |
1068 | + node._register_request_event(user, event_name, comment) |
1069 | + self.assertThat(log_mock, MockCalledOnceWith( |
1070 | + node.system_id, |
1071 | + EVENT_TYPES.REQUEST_NODE_START, |
1072 | + type_level=event_details.level, |
1073 | + type_description=event_details.description, |
1074 | + event_description=event_description)) |
1075 | + |
1076 | + def test__register_request_event_with_none_user_saves_no_event(self): |
1077 | + node = factory.make_Node() |
1078 | + log_mock = self.patch_autospec( |
1079 | + Event.objects, 'register_event_and_event_type') |
1080 | + event_name = EVENT_TYPES.REQUEST_NODE_START |
1081 | + comment = factory.make_name("comment") |
1082 | + node._register_request_event(None, event_name, comment) |
1083 | + self.assertThat(log_mock, MockNotCalled()) |
1084 | + |
1085 | |
1086 | class TestNodeIsBootInterfaceOnManagedInterface(MAASServerTestCase): |
1087 | |
1088 | @@ -2433,8 +2583,8 @@ |
1089 | 'enable_disk_erasing_on_release', True) |
1090 | erase_mock = self.patch_autospec(node, 'start_disk_erasing') |
1091 | release_mock = self.patch_autospec(node, 'release') |
1092 | - node.release_or_erase() |
1093 | - self.assertThat(erase_mock, MockCalledOnceWith(owner)) |
1094 | + node.release_or_erase(owner) |
1095 | + self.assertThat(erase_mock, MockCalledOnceWith(owner, None)) |
1096 | self.assertThat(release_mock, MockNotCalled()) |
1097 | |
1098 | def test_release_or_erase_releases_when_disabled(self): |
1099 | @@ -2444,8 +2594,8 @@ |
1100 | 'enable_disk_erasing_on_release', False) |
1101 | erase_mock = self.patch_autospec(node, 'start_disk_erasing') |
1102 | release_mock = self.patch_autospec(node, 'release') |
1103 | - node.release_or_erase() |
1104 | - self.assertThat(release_mock, MockCalledOnceWith()) |
1105 | + node.release_or_erase(owner) |
1106 | + self.assertThat(release_mock, MockCalledOnceWith(owner, None)) |
1107 | self.assertThat(erase_mock, MockNotCalled()) |
1108 | |
1109 | |
1110 | @@ -3234,6 +3384,15 @@ |
1111 | node._start_transition_monitor_async, |
1112 | MockCalledOnceWith(ANY, node.hostname)) |
1113 | |
1114 | + def test_start_logs_user_request(self): |
1115 | + admin = factory.make_admin() |
1116 | + node = factory.make_Node(status=NODE_STATUS.NEW, owner=admin) |
1117 | + self.patch(node, '_start') |
1118 | + register_event = self.patch(node, '_register_request_event') |
1119 | + node.start(admin) |
1120 | + self.assertThat(register_event, MockCalledOnceWith( |
1121 | + admin, EVENT_TYPES.REQUEST_NODE_START, None)) |
1122 | + |
1123 | def test__raises_failures_when_power_action_fails(self): |
1124 | class PraiseBeToJTVException(Exception): |
1125 | """A nonsense exception for this test. |
1126 | @@ -3373,6 +3532,16 @@ |
1127 | self.assertRaises(PermissionDenied, node.stop, user) |
1128 | self.assertThat(power_off_node, MockNotCalled()) |
1129 | |
1130 | + def test_stop_logs_user_request(self): |
1131 | + admin = factory.make_admin() |
1132 | + node = self.make_node_with_interface(admin) |
1133 | + self.patch_autospec(node_module, "power_off_node") |
1134 | + register_event = self.patch(node, '_register_request_event') |
1135 | + with post_commit_hooks: |
1136 | + node.stop(admin) |
1137 | + self.assertThat(register_event, MockCalledOnceWith( |
1138 | + admin, EVENT_TYPES.REQUEST_NODE_STOP, None)) |
1139 | + |
1140 | def test__allows_admin_to_stop_any_node(self): |
1141 | power_off_node = self.patch_autospec(node_module, "power_off_node") |
1142 | owner = factory.make_User() |
1143 | |
1144 | === modified file 'src/maasserver/node_action.py' |
1145 | --- src/maasserver/node_action.py 2015-09-17 15:37:57 +0000 |
1146 | +++ src/maasserver/node_action.py 2015-09-17 19:23:21 +0000 |
1147 | @@ -410,7 +410,7 @@ |
1148 | def execute(self): |
1149 | """See `NodeAction.execute`.""" |
1150 | try: |
1151 | - self.node.release_or_erase() |
1152 | + self.node.release_or_erase(self.user) |
1153 | self.node.hwe_kernel = "" |
1154 | self.node.save() |
1155 | except RPC_EXCEPTIONS + (ExternalProcessError,) as exception: |
1156 | @@ -437,8 +437,7 @@ |
1157 | |
1158 | def execute(self): |
1159 | """See `NodeAction.execute`.""" |
1160 | - self.node.mark_broken( |
1161 | - "Manually marked as broken by user '%s'" % self.user.username) |
1162 | + self.node.mark_broken(self.user, "via web interface") |
1163 | |
1164 | |
1165 | class MarkFixed(NodeAction): |
1166 | @@ -459,7 +458,7 @@ |
1167 | raise NodeActionError( |
1168 | "Unable to be mark fixed because it has not been commissioned " |
1169 | "successfully.") |
1170 | - self.node.mark_fixed() |
1171 | + self.node.mark_fixed(self.user) |
1172 | |
1173 | def has_commissioning_data(self): |
1174 | """Return True when the node is missing the required commissioning |
1175 | |
1176 | === modified file 'src/maasserver/rpc/nodes.py' |
1177 | --- src/maasserver/rpc/nodes.py 2015-08-28 14:01:35 +0000 |
1178 | +++ src/maasserver/rpc/nodes.py 2015-09-17 19:23:21 +0000 |
1179 | @@ -58,7 +58,7 @@ |
1180 | except Node.DoesNotExist: |
1181 | raise NoSuchNode.from_system_id(system_id) |
1182 | try: |
1183 | - node.mark_failed(error_description) |
1184 | + node.mark_failed(None, error_description) |
1185 | except exceptions.NodeStateViolation as e: |
1186 | raise NodeStateViolation(e) |
1187 | |
1188 | |
1189 | === modified file 'src/maasserver/tests/test_node_action.py' |
1190 | --- src/maasserver/tests/test_node_action.py 2015-08-28 01:56:18 +0000 |
1191 | +++ src/maasserver/tests/test_node_action.py 2015-09-17 19:23:21 +0000 |
1192 | @@ -282,7 +282,7 @@ |
1193 | interface=True, status=self.status, |
1194 | power_type='ether_wake', power_state=POWER_STATE.OFF) |
1195 | self.patch_autospec(node, 'start_transition_monitor') |
1196 | - node_start = self.patch(node, 'start') |
1197 | + node_start = self.patch(node, '_start') |
1198 | node_start.side_effect = lambda user, user_data: post_commit() |
1199 | admin = factory.make_admin() |
1200 | action = Commission(node, admin) |
1201 | @@ -290,7 +290,7 @@ |
1202 | action.execute() |
1203 | self.assertEqual(NODE_STATUS.COMMISSIONING, node.status) |
1204 | self.assertThat( |
1205 | - node_start, MockCalledOnceWith(admin, user_data=ANY)) |
1206 | + node_start, MockCalledOnceWith(admin, ANY)) |
1207 | |
1208 | |
1209 | class TestAbortAction(MAASTransactionServerTestCase): |
1210 | @@ -301,7 +301,7 @@ |
1211 | node = factory.make_Node( |
1212 | status=NODE_STATUS.DISK_ERASING, owner=owner) |
1213 | |
1214 | - node_stop = self.patch_autospec(node, 'stop') |
1215 | + node_stop = self.patch_autospec(node, '_stop') |
1216 | # Return a post-commit hook from Node.stop(). |
1217 | node_stop.side_effect = lambda user: post_commit() |
1218 | |
1219 | @@ -326,7 +326,7 @@ |
1220 | admin = factory.make_admin() |
1221 | |
1222 | self.patch_autospec(node, 'stop_transition_monitor') |
1223 | - node_stop = self.patch_autospec(node, 'stop') |
1224 | + node_stop = self.patch_autospec(node, '_stop') |
1225 | # Return a post-commit hook from Node.stop(). |
1226 | node_stop.side_effect = lambda user: post_commit() |
1227 | |
1228 | @@ -351,7 +351,7 @@ |
1229 | admin = factory.make_admin() |
1230 | |
1231 | self.patch_autospec(node, 'stop_transition_monitor') |
1232 | - node_stop = self.patch_autospec(node, 'stop') |
1233 | + node_stop = self.patch_autospec(node, '_stop') |
1234 | # Return a post-commit hook from Node.stop(). |
1235 | node_stop.side_effect = lambda user: post_commit() |
1236 | |
1237 | @@ -701,7 +701,7 @@ |
1238 | power_type='ipmi', power_state=POWER_STATE.ON, |
1239 | owner=user, power_parameters=params) |
1240 | self.patch(node, 'start_transition_monitor') |
1241 | - node_stop = self.patch_autospec(node, 'stop') |
1242 | + node_stop = self.patch_autospec(node, '_stop') |
1243 | |
1244 | Release(node, user).execute() |
1245 | |
1246 | @@ -727,7 +727,7 @@ |
1247 | self.assertTrue(action.is_permitted()) |
1248 | action.execute() |
1249 | self.assertEqual( |
1250 | - "Manually marked as broken by user '%s'" % user.username, |
1251 | + "via web interface", |
1252 | reload_object(node).error_description |
1253 | ) |
1254 | |
1255 | @@ -831,8 +831,8 @@ |
1256 | |
1257 | def patch_rpc_methods(self, node): |
1258 | exception = self.make_exception() |
1259 | - self.patch(node, 'start').side_effect = exception |
1260 | - self.patch(node, 'stop').side_effect = exception |
1261 | + self.patch(node, '_start').side_effect = exception |
1262 | + self.patch(node, '_stop').side_effect = exception |
1263 | self.patch_autospec(node, 'start_transition_monitor') |
1264 | self.patch_autospec(node, 'stop_transition_monitor') |
1265 | |
1266 | @@ -852,8 +852,7 @@ |
1267 | self.patch_rpc_methods(action.node) |
1268 | exception = self.assertRaises(NodeActionError, action.execute) |
1269 | self.assertEqual( |
1270 | - get_error_message_for_exception( |
1271 | - action.node.start.side_effect), |
1272 | + get_error_message_for_exception(action.node._start.side_effect), |
1273 | unicode(exception)) |
1274 | |
1275 | def test_Abort_handles_rpc_errors(self): |
1276 | @@ -862,8 +861,7 @@ |
1277 | self.patch_rpc_methods(action.node) |
1278 | exception = self.assertRaises(NodeActionError, action.execute) |
1279 | self.assertEqual( |
1280 | - get_error_message_for_exception( |
1281 | - action.node.stop.side_effect), |
1282 | + get_error_message_for_exception(action.node._stop.side_effect), |
1283 | unicode(exception)) |
1284 | |
1285 | def test_PowerOn_handles_rpc_errors(self): |
1286 | @@ -871,8 +869,7 @@ |
1287 | self.patch_rpc_methods(action.node) |
1288 | exception = self.assertRaises(NodeActionError, action.execute) |
1289 | self.assertEqual( |
1290 | - get_error_message_for_exception( |
1291 | - action.node.start.side_effect), |
1292 | + get_error_message_for_exception(action.node._start.side_effect), |
1293 | unicode(exception)) |
1294 | |
1295 | def test_PowerOff_handles_rpc_errors(self): |
1296 | @@ -880,8 +877,7 @@ |
1297 | self.patch_rpc_methods(action.node) |
1298 | exception = self.assertRaises(NodeActionError, action.execute) |
1299 | self.assertEqual( |
1300 | - get_error_message_for_exception( |
1301 | - action.node.stop.side_effect), |
1302 | + get_error_message_for_exception(action.node._stop.side_effect), |
1303 | unicode(exception)) |
1304 | |
1305 | def test_Release_handles_rpc_errors(self): |
1306 | @@ -890,6 +886,5 @@ |
1307 | self.patch_rpc_methods(action.node) |
1308 | exception = self.assertRaises(NodeActionError, action.execute) |
1309 | self.assertEqual( |
1310 | - get_error_message_for_exception( |
1311 | - action.node.stop.side_effect), |
1312 | + get_error_message_for_exception(action.node._stop.side_effect), |
1313 | unicode(exception)) |
1314 | |
1315 | === modified file 'src/metadataserver/api.py' |
1316 | --- src/metadataserver/api.py 2015-09-17 14:39:49 +0000 |
1317 | +++ src/metadataserver/api.py 2015-09-17 19:23:21 +0000 |
1318 | @@ -355,14 +355,15 @@ |
1319 | elif node.status == NODE_STATUS.DEPLOYING: |
1320 | if result in ['FAIL', 'FAILURE']: |
1321 | node.mark_failed( |
1322 | - "Installation failed (refer to the installation log " |
1323 | - "for more information).") |
1324 | + None, |
1325 | + "Installation failed (refer to the " |
1326 | + "installation log for more information).") |
1327 | elif node.status == NODE_STATUS.DISK_ERASING: |
1328 | if result == 'SUCCESS': |
1329 | # disk erasing complete, release node. |
1330 | node.release() |
1331 | elif result in ['FAIL', 'FAILURE']: |
1332 | - node.mark_failed("Failed to erase disks.") |
1333 | + node.mark_failed(None, "Failed to erase disks.") |
1334 | |
1335 | # Deallocate the node if we enter any terminal state. |
1336 | if node.status in [ |
1337 | @@ -518,15 +519,16 @@ |
1338 | self._store_installation_results(node, request) |
1339 | if status == SIGNAL_STATUS.FAILED: |
1340 | node.mark_failed( |
1341 | - "Installation failed (refer to the installation log " |
1342 | - "for more information).") |
1343 | + None, |
1344 | + "Installation failed (refer to the " |
1345 | + "installation log for more information).") |
1346 | target_status = None |
1347 | elif node.status == NODE_STATUS.DISK_ERASING: |
1348 | if status == SIGNAL_STATUS.OK: |
1349 | # disk erasing complete, release node |
1350 | node.release() |
1351 | elif status == SIGNAL_STATUS.FAILED: |
1352 | - node.mark_failed("Failed to erase disks.") |
1353 | + node.mark_failed(None, "Failed to erase disks.") |
1354 | target_status = None |
1355 | |
1356 | if target_status in (None, node.status): |
1357 | |
1358 | === modified file 'src/provisioningserver/events.py' |
1359 | --- src/provisioningserver/events.py 2015-09-02 16:20:10 +0000 |
1360 | +++ src/provisioningserver/events.py 2015-09-17 19:23:21 +0000 |
1361 | @@ -64,6 +64,20 @@ |
1362 | NODE_STATUS_EVENT = "NODE_STATUS_EVENT" |
1363 | NODE_COMMISSIONING_EVENT = "NODE_COMMISSIONING_EVENT" |
1364 | NODE_INSTALL_EVENT = "NODE_INSTALL_EVENT" |
1365 | + # Node user request events |
1366 | + REQUEST_NODE_START_COMMISSIONING = "REQUEST_NODE_START_COMMISSIONING" |
1367 | + REQUEST_NODE_ABORT_COMMISSIONING = "REQUEST_NODE_ABORT_COMMISSIONING" |
1368 | + REQUEST_NODE_ABORT_DEPLOYMENT = "REQUEST_NODE_ABORT_DEPLOYMENT" |
1369 | + REQUEST_NODE_ACQUIRE = "REQUEST_NODE_ACQUIRE" |
1370 | + REQUEST_NODE_ERASE_DISK = "REQUEST_NODE_ERASE_DISK" |
1371 | + REQUEST_NODE_ABORT_ERASE_DISK = "REQUEST_NODE_ABORT_ERASE_DISK" |
1372 | + REQUEST_NODE_RELEASE = "REQUEST_NODE_RELEASE" |
1373 | + REQUEST_NODE_MARK_FAILED = "REQUEST_NODE_MARK_FAILED" |
1374 | + REQUEST_NODE_MARK_BROKEN = "REQUEST_NODE_MARK_BROKEN" |
1375 | + REQUEST_NODE_MARK_FIXED = "REQUEST_NODE_MARK_FIXED" |
1376 | + REQUEST_NODE_START_DEPLOYMENT = "REQUEST_NODE_START_DEPLOYMENT" |
1377 | + REQUEST_NODE_START = "REQUEST_NODE_START" |
1378 | + REQUEST_NODE_STOP = "REQUEST_NODE_STOP" |
1379 | |
1380 | |
1381 | EventDetail = namedtuple("EventDetail", ("description", "level")) |
1382 | @@ -127,6 +141,58 @@ |
1383 | description="Node installation", |
1384 | level=DEBUG, |
1385 | ), |
1386 | + EVENT_TYPES.REQUEST_NODE_START_COMMISSIONING: EventDetail( |
1387 | + description="User starting node commissioning", |
1388 | + level=INFO, |
1389 | + ), |
1390 | + EVENT_TYPES.REQUEST_NODE_ABORT_COMMISSIONING: EventDetail( |
1391 | + description="User aborting node commissioning", |
1392 | + level=INFO, |
1393 | + ), |
1394 | + EVENT_TYPES.REQUEST_NODE_ABORT_DEPLOYMENT: EventDetail( |
1395 | + description="User aborting deployment", |
1396 | + level=INFO, |
1397 | + ), |
1398 | + EVENT_TYPES.REQUEST_NODE_ACQUIRE: EventDetail( |
1399 | + description="User acquiring node", |
1400 | + level=INFO, |
1401 | + ), |
1402 | + EVENT_TYPES.REQUEST_NODE_ERASE_DISK: EventDetail( |
1403 | + description="User erasing disk", |
1404 | + level=INFO, |
1405 | + ), |
1406 | + EVENT_TYPES.REQUEST_NODE_ABORT_ERASE_DISK: EventDetail( |
1407 | + description="User aborting disk erase", |
1408 | + level=INFO, |
1409 | + ), |
1410 | + EVENT_TYPES.REQUEST_NODE_RELEASE: EventDetail( |
1411 | + description="User releasing node", |
1412 | + level=INFO, |
1413 | + ), |
1414 | + EVENT_TYPES.REQUEST_NODE_MARK_FAILED: EventDetail( |
1415 | + description="User marking node failed", |
1416 | + level=INFO, |
1417 | + ), |
1418 | + EVENT_TYPES.REQUEST_NODE_MARK_BROKEN: EventDetail( |
1419 | + description="User marking node broken", |
1420 | + level=INFO, |
1421 | + ), |
1422 | + EVENT_TYPES.REQUEST_NODE_MARK_FIXED: EventDetail( |
1423 | + description="User marking node fixed", |
1424 | + level=INFO, |
1425 | + ), |
1426 | + EVENT_TYPES.REQUEST_NODE_START_DEPLOYMENT: EventDetail( |
1427 | + description="User starting deployment", |
1428 | + level=INFO, |
1429 | + ), |
1430 | + EVENT_TYPES.REQUEST_NODE_START: EventDetail( |
1431 | + description="User powering up node", |
1432 | + level=INFO, |
1433 | + ), |
1434 | + EVENT_TYPES.REQUEST_NODE_STOP: EventDetail( |
1435 | + description="User powering down node", |
1436 | + level=INFO, |
1437 | + ), |
1438 | } |
1439 | |
1440 |
The implementation looks good. You have some coding style issues that need to be fixed and you have an issue with logging the event before permissions are checked in the start and stop methods on the Node model. But overall its really good, just those fixes and it will be ready to land.
Also you are missing a commit message. Setting a commit message is actually more important than setting a description. The commit message is what will be placed in the bzr log when your branch is merged by the lander. I would move the first two paragraphs of your description into the commit message. You can leave the examples in the description. You can duplicate the information if you want. Only the commit message will go into the bzr log the description will just stay on launchpad if this merge ever needs to be looked at again.