Merge lp:~newell-jensen/maas/fix-1607980 into lp:~maas-committers/maas/trunk

Proposed by Newell Jensen
Status: Merged
Approved by: Newell Jensen
Approved revision: no longer in the source branch.
Merged at revision: 5221
Proposed branch: lp:~newell-jensen/maas/fix-1607980
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 253 lines (+224/-1)
2 files modified
src/maasserver/models/node.py (+3/-0)
src/maasserver/models/tests/test_node.py (+221/-1)
To merge this branch: bzr merge lp:~newell-jensen/maas/fix-1607980
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+301562@code.launchpad.net

Commit message

Add code to set owner to None in stop_rescue_mode on the Node model when the previous_status was the Broken state. Add missing unit tests for Rescue Mode on the Node model as well (this was accidentally left out of the rescue-mode branch).

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

Does this actually Fix the issue the branch is attached to? Meaning, now the machine deploys without accessing commissioning environment? Also inline:

Revision history for this message
Newell Jensen (newell-jensen) wrote :

> Does this actually Fix the issue the branch is attached to? Meaning, now the
> machine deploys without accessing commissioning environment? Also inline:

Yes this fixes the issue for the bug. See inline as well.

Revision history for this message
Newell Jensen (newell-jensen) :
Revision history for this message
Andres Rodriguez (andreserl) wrote :

lgtm!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/node.py'
2--- src/maasserver/models/node.py 2016-07-28 18:28:23 +0000
3+++ src/maasserver/models/node.py 2016-07-30 00:43:40 +0000
4@@ -3424,6 +3424,9 @@
5 # exceptions arising synchronously, and chain callbacks to the
6 # Deferred it returns for the asynchronous (post-commit) bits.
7 stopping = self._stop(user)
8+ if self.owner is not None:
9+ self.owner = None
10+ self.save()
11 elif self.previous_status == NODE_STATUS.DEPLOYED:
12 stopping = self._power_cycle()
13 else:
14
15=== modified file 'src/maasserver/models/tests/test_node.py'
16--- src/maasserver/models/tests/test_node.py 2016-07-28 16:19:04 +0000
17+++ src/maasserver/models/tests/test_node.py 2016-07-30 00:43:40 +0000
18@@ -3222,6 +3222,226 @@
19 "bcache volume. Mount /boot on a non-bcache device to be able to "
20 "deploy this node."], node.storage_layout_issues())
21
22+ def test_start_rescue_mode_raises_PermissionDenied_if_no_edit(self):
23+ user = factory.make_User()
24+ node = factory.make_Node(
25+ owner=user,
26+ status=random.choice((NODE_STATUS.BROKEN, NODE_STATUS.DEPLOYED)))
27+ self.assertRaises(
28+ PermissionDenied, node.start_rescue_mode, factory.make_User())
29+
30+ def test_start_rescue_mode_errors_for_unconfigured_power_type(self):
31+ node = factory.make_Node(
32+ status=random.choice((NODE_STATUS.BROKEN, NODE_STATUS.DEPLOYED)),
33+ power_type='')
34+ admin = factory.make_admin()
35+ self.assertRaises(
36+ UnknownPowerType, node.start_rescue_mode, admin)
37+
38+ def test_start_rescue_mode_sets_status_owner_and_power_cycles_node(self):
39+ node = factory.make_Node(
40+ status=random.choice((NODE_STATUS.BROKEN, NODE_STATUS.DEPLOYED)))
41+ mock_node_power_cycle = self.patch(node, '_power_cycle')
42+ # Return a post-commit hook from Node.power_cycle().
43+ mock_node_power_cycle.side_effect = lambda: post_commit()
44+ admin = factory.make_admin()
45+ node.start_rescue_mode(admin)
46+ post_commit_hooks.reset() # Ignore these for now.
47+ node = reload_object(node)
48+ expected_attrs = {
49+ 'status': NODE_STATUS.RESCUE_MODE,
50+ 'owner': admin,
51+ }
52+ self.assertAttributes(node, expected_attrs)
53+ self.expectThat(node.owner, Equals(admin))
54+ self.expectThat(mock_node_power_cycle, MockCalledOnceWith())
55+
56+ def test_start_rescue_mode_reverts_status_on_error(self):
57+ # When start_rescue_mode encounters an error when trying to
58+ # power cycle the node, it will revert the node to its previous
59+ # status.
60+ admin = factory.make_admin()
61+ status = random.choice((NODE_STATUS.BROKEN, NODE_STATUS.DEPLOYED))
62+ node = factory.make_Node(status=status)
63+ mock_node_power_cycle = self.patch(node, '_power_cycle')
64+ mock_node_power_cycle.side_effect = factory.make_exception()
65+
66+ try:
67+ with transaction.atomic():
68+ node.start_rescue_mode(admin)
69+ except mock_node_power_cycle.side_effect.__class__:
70+ # We don't care about the error here, so suppress it. It
71+ # exists only to cause the transaction to abort.
72+ pass
73+
74+ self.expectThat(mock_node_power_cycle, MockCalledOnceWith())
75+ self.expectThat(status, Equals(node.status))
76+
77+ def test_start_rescue_mode_reverts_status_on_post_commit_error(self):
78+ self.disable_node_query()
79+ # When start_rescue_mode encounters an error in its post-commit
80+ # hook, it will revert the node to its previous status.
81+ admin = factory.make_admin()
82+ status = random.choice((NODE_STATUS.BROKEN, NODE_STATUS.DEPLOYED))
83+ node = factory.make_Node(status=status)
84+ # Patch out some things that we don't want to do right now.
85+ self.patch(node, '_power_cycle').return_value = None
86+ # Fake an error during the post-commit hook.
87+ error_message = factory.make_name("error")
88+ error_type = factory.make_exception_type()
89+ _start_async = self.patch_autospec(node, "_start_rescue_mode_async")
90+ _start_async.side_effect = error_type(error_message)
91+ # Capture calls to _set_status.
92+ self.patch_autospec(Node, "_set_status")
93+
94+ with LoggerFixture("maas") as logger:
95+ with ExpectedException(error_type):
96+ with post_commit_hooks:
97+ node.start_rescue_mode(admin)
98+
99+ # The status is set to be reverted to its initial status.
100+ self.expectThat(node._set_status, MockCalledOnceWith(
101+ node.system_id, status=status))
102+ # It's logged too.
103+ self.expectThat(logger.output, Contains(
104+ "%s: Could not start rescue mode for node: %s\n"
105+ % (node.hostname, error_message)))
106+
107+ def test_start_rescue_mode_logs_and_raises_errors_in_power_cycling(self):
108+ admin = factory.make_admin()
109+ status = random.choice((NODE_STATUS.BROKEN, NODE_STATUS.DEPLOYED))
110+ node = factory.make_Node(status=status)
111+ mock_maaslog = self.patch(node_module, 'maaslog')
112+ exception = NoConnectionsAvailable(factory.make_name())
113+ self.patch(node, '_power_cycle').side_effect = exception
114+ self.assertRaises(
115+ NoConnectionsAvailable, node.start_rescue_mode, admin)
116+ self.expectThat(status, Equals(node.status))
117+ self.expectThat(
118+ mock_maaslog.error, MockCalledOnceWith(
119+ "%s: Could not start rescue mode for node: %s",
120+ node.hostname, exception))
121+
122+ def test_start_rescue_mode_logs_user_request(self):
123+ node = factory.make_Node(
124+ status=random.choice((NODE_STATUS.BROKEN, NODE_STATUS.DEPLOYED)))
125+ mock_register_event = self.patch(node, '_register_request_event')
126+ mock_node_power_cycle = self.patch(node, '_power_cycle')
127+ # Return a post-commit hook from Node.power_cycle().
128+ mock_node_power_cycle.side_effect = lambda: post_commit()
129+ admin = factory.make_admin()
130+ node.start_rescue_mode(admin)
131+ post_commit_hooks.reset() # Ignore these for now.
132+ node = reload_object(node)
133+ self.assertThat(
134+ mock_register_event, MockCalledOnceWith(
135+ admin, EVENT_TYPES.REQUEST_NODE_START_RESCUE_MODE,
136+ action='start rescue mode'))
137+
138+ def test_stop_rescue_mode_raises_PermissionDenied_if_no_edit(self):
139+ user = factory.make_User()
140+ node = factory.make_Node(owner=user, status=NODE_STATUS.RESCUE_MODE)
141+ self.assertRaises(
142+ PermissionDenied, node.stop_rescue_mode, factory.make_User())
143+
144+ def test_stop_rescue_mode_stops_node_sets_owner_and_status_to_broken(self):
145+ node = factory.make_Node(
146+ status=NODE_STATUS.RESCUE_MODE, previous_status=NODE_STATUS.BROKEN)
147+ admin = factory.make_admin()
148+ mock_node_stop = self.patch(node, '_stop')
149+ # Return a post-commit hook from Node.stop().
150+ mock_node_stop.side_effect = lambda user: post_commit()
151+ self.patch(Node, "_set_status")
152+
153+ with post_commit_hooks:
154+ node.stop_rescue_mode(admin)
155+
156+ self.expectThat(mock_node_stop, MockCalledOnceWith(admin))
157+ self.expectThat(
158+ node._set_status, MockCalledOnceWith(
159+ node.system_id, status=NODE_STATUS.BROKEN))
160+ self.assertIsNone(node.owner)
161+
162+ def test_stop_rescue_mode_cycles_node_and_sets_status_to_deployed(self):
163+ node = factory.make_Node(
164+ status=NODE_STATUS.RESCUE_MODE,
165+ previous_status=NODE_STATUS.DEPLOYED)
166+ admin = factory.make_admin()
167+ mock_node_power_cycle = self.patch(node, '_power_cycle')
168+ # Return a post-commit hook from Node._power_cycle().
169+ mock_node_power_cycle.side_effect = lambda: post_commit()
170+ self.patch(Node, "_set_status")
171+
172+ with post_commit_hooks:
173+ node.stop_rescue_mode(admin)
174+
175+ self.expectThat(mock_node_power_cycle, MockCalledOnceWith())
176+ self.expectThat(
177+ node._set_status, MockCalledOnceWith(
178+ node.system_id, status=NODE_STATUS.DEPLOYED))
179+
180+ def test_stop_rescue_mode_logs_user_request(self):
181+ node = factory.make_Node(status=NODE_STATUS.RESCUE_MODE)
182+ admin = factory.make_admin()
183+ self.patch(Node, "_set_status")
184+ self.patch(Node, "_stop").return_value = None
185+ self.patch(Node, "_power_cycle").return_value = None
186+ mock_register_event = self.patch(node, '_register_request_event')
187+ with post_commit_hooks:
188+ node.stop_rescue_mode(admin)
189+ self.assertThat(
190+ mock_register_event, MockCalledOnceWith(
191+ admin, EVENT_TYPES.REQUEST_NODE_STOP_RESCUE_MODE,
192+ action='stop rescue mode'))
193+
194+ def test_stop_rescue_mode_logs_and_raises_errors_in_stopping(self):
195+ admin = factory.make_admin()
196+ node = factory.make_Node(
197+ status=NODE_STATUS.RESCUE_MODE,
198+ previous_status=NODE_STATUS.BROKEN)
199+ mock_maaslog = self.patch(node_module, 'maaslog')
200+ exception_class = factory.make_exception_type()
201+ exception = exception_class(factory.make_name())
202+ self.patch(node, '_stop').side_effect = exception
203+ self.assertRaises(
204+ exception_class, node.stop_rescue_mode, admin)
205+ self.expectThat(NODE_STATUS.RESCUE_MODE, Equals(node.status))
206+ self.expectThat(
207+ mock_maaslog.error, MockCalledOnceWith(
208+ "%s: Could not stop rescue mode for node: %s",
209+ node.hostname, exception))
210+
211+ def test_stop_rescue_mode_reverts_status_on_post_commit_error(self):
212+ self.disable_node_query()
213+ # When stop_rescue_mode encounters an error in its post-commit
214+ # hook, it will revert the node to its previous status.
215+ admin = factory.make_admin()
216+ previous_status = NODE_STATUS.DEPLOYED
217+ node = factory.make_Node(
218+ status=NODE_STATUS.RESCUE_MODE, previous_status=previous_status)
219+ # Patch out some things that we don't want to do right now.
220+ self.patch(node, '_power_cycle').return_value = None
221+ # Fake an error during the post-commit hook.
222+ error_message = factory.make_name("error")
223+ error_type = factory.make_exception_type()
224+ _stop_async = self.patch_autospec(node, "_stop_rescue_mode_async")
225+ _stop_async.side_effect = error_type(error_message)
226+ # Capture calls to _set_status.
227+ self.patch_autospec(Node, "_set_status")
228+
229+ with LoggerFixture("maas") as logger:
230+ with ExpectedException(error_type):
231+ with post_commit_hooks:
232+ node.stop_rescue_mode(admin)
233+
234+ # The status is set to be reverted to its initial status.
235+ self.expectThat(_stop_async, MockCalledOnceWith(
236+ False, node.hostname, node.system_id, previous_status))
237+ # It's logged too.
238+ self.expectThat(logger.output, Contains(
239+ "%s: Could not stop rescue mode for node: %s\n"
240+ % (node.hostname, error_message)))
241+
242
243 class TestNodePowerParameters(MAASServerTestCase):
244
245@@ -4821,7 +5041,7 @@
246
247
248 class TestNode_PowerCycle(MAASServerTestCase):
249- """Tests for Node.power_cycle()."""
250+ """Tests for Node._power_cycle()."""
251
252 def make_acquired_node_with_interface(
253 self, user, bmc_connected_to=None, power_type="virsh"):