Merge lp:~trapnine/maas/events-1 into lp:~maas-committers/maas/trunk

Proposed by Jeffrey C Jones
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
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)

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

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.

review: Needs Fixing
Revision history for this message
Jeffrey C Jones (trapnine) wrote :

fixed - please take another look

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good. Thanks for all the fixes.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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