Merge ~blake-rouse/maas:fix-1843493 into maas:master

Proposed by Blake Rouse
Status: Rejected
Rejected by: Blake Rouse
Proposed branch: ~blake-rouse/maas:fix-1843493
Merge into: maas:master
Diff against target: 306 lines (+121/-19)
2 files modified
src/maasserver/forms/pods.py (+46/-7)
src/maasserver/forms/tests/test_pods.py (+75/-12)
Reviewer Review Type Date Requested Status
MAAS Lander Needs Fixing
Newell Jensen (community) Approve
Review via email: mp+372623@code.launchpad.net

Commit message

Fixes LP: #1843493 - Handles serialization issues with saving composed machine information into the database.

To post a comment you must log in.
Revision history for this message
Newell Jensen (newell-jensen) wrote :

LGTM

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b fix-1843493 lp:~blake-rouse/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/6389/console
COMMIT: 80dae65319be4d28bd48fb2b326ea1259e8d57d2

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b fix-1843493 lp:~blake-rouse/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/6415/console
COMMIT: 850984c31a86fc99ce0595fe4862c3576dbcbbbe

review: Needs Fixing
Revision history for this message
Pedro GuimarĂ£es (pguimaraes) wrote :

The compose method in src/maasserver/forms/pods.py has a "if isInIOThread" contiditional. I've ran MP multiple times but I never seen MAAS going into that conditional:
https://github.com/maas/maas/blob/master/src/maasserver/forms/pods.py#L639

I've tried to force it, just to check what would happen: remove the if isInIOThread all together and left its conditional code to be ran (the else conditional + code was removed entirely)
So, I was not running within an IOThread, but used its code within if-conditional to give it a shot.

Eventually, MAAS fails with: https://pastebin.canonical.com/p/gBTPbMfBR8/

Indeed, end of "if isInIOThread" returns a Deferred object which we pass directly back to api/machines.py. I am not sure if the fact of running within an IOThread would change anything and Deferred would be processed on the spot. I guess not since Deferred processing demands reactor to be run to produce a result.

I imagine that, if that code ever runs, we're missing a decorator @asynchronous that would queue that Deferred for processing just like:
https://github.com/maas/maas/blob/master/src/provisioningserver/utils/twisted.py#L156

Unmerged commits

850984c... by Blake Rouse

Fix lint.

80dae65... by Blake Rouse

Update comment.

9022f83... by Blake Rouse

Fixes LP: #1843493 - Handles serialization issues with saving composed machine information into the database.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/forms/pods.py b/src/maasserver/forms/pods.py
2index 1a68329..85c1da2 100644
3--- a/src/maasserver/forms/pods.py
4+++ b/src/maasserver/forms/pods.py
5@@ -25,6 +25,7 @@ from maasserver.clusterrpc.driver_parameters import (
6 )
7 from maasserver.clusterrpc.pods import (
8 compose_machine,
9+ decompose_machine,
10 discover_pod,
11 get_best_discovered_result,
12 )
13@@ -59,7 +60,10 @@ from maasserver.node_constraint_filter_forms import (
14 from maasserver.rpc import getClientFromIdentifiers
15 from maasserver.utils.dns import validate_hostname
16 from maasserver.utils.forms import set_form_error
17-from maasserver.utils.orm import transactional
18+from maasserver.utils.orm import (
19+ post_commit_do,
20+ transactional,
21+)
22 from maasserver.utils.threads import deferToDatabase
23 import petname
24 from provisioningserver.drivers import SETTING_SCOPE
25@@ -76,6 +80,7 @@ from provisioningserver.enum import (
26 MACVLAN_MODE_CHOICES,
27 )
28 from provisioningserver.logger import LegacyLogger
29+from provisioningserver.rpc.exceptions import PodInvalidResources
30 from provisioningserver.utils.network import get_ifname_for_label
31 from provisioningserver.utils.twisted import asynchronous
32 from twisted.internet.defer import inlineCallbacks
33@@ -607,7 +612,7 @@ class ComposeMachineForm(forms.Form):
34
35 return client, interfaces
36
37- def create_and_sync(result):
38+ def create_and_sync(result, post_pod_sync=False):
39 requested_machine, result = result
40 discovered_machine, pod_hints = result
41 created_machine = self.pod.create_machine(
42@@ -620,7 +625,18 @@ class ComposeMachineForm(forms.Form):
43 pool=self.get_value_for('pool'),
44 zone=self.get_value_for('zone'),
45 )
46- self.pod.sync_hints(pod_hints)
47+ if post_pod_sync:
48+ # Do not perform the hints syncing in this transaction as it
49+ # can cause a transactional issue and we want to ensure that
50+ # the machine is created and committed. Post commit update the
51+ # pod hints as the current running transaction doesn't
52+ # need that data for the resulting output (only for future
53+ # requests).
54+ post_commit_do(
55+ deferToDatabase,
56+ transactional(self.pod.sync_hints), pod_hints)
57+ else:
58+ self.pod.sync_hints(pod_hints)
59 return created_machine
60
61 @inlineCallbacks
62@@ -663,11 +679,22 @@ class ComposeMachineForm(forms.Form):
63 pod_id=pod_id, name=name)
64 return d
65
66+ @asynchronous
67+ def wrap_decompose_machine(
68+ client_idents, pod_type, parameters, pod_id, name):
69+ """Wrapper to get the client."""
70+ d = getClientFromIdentifiers(client_idents)
71+ d.addCallback(
72+ decompose_machine, pod_type, parameters,
73+ pod_id=pod_id, name=name)
74+ return d
75+
76 _, result = db_work(None)
77+ client_ids = self.pod.get_client_identifiers()
78 try:
79 requested_machine = self.get_requested_machine(result)
80 result = wrap_compose_machine(
81- self.pod.get_client_identifiers(),
82+ client_ids,
83 self.pod.power_type,
84 power_parameters,
85 requested_machine,
86@@ -678,7 +705,19 @@ class ComposeMachineForm(forms.Form):
87 "Unable to compose a machine because '%s' driver "
88 "timed out after %d seconds." % (
89 self.pod.power_type, timeout))
90- return create_and_sync((requested_machine, result))
91+ try:
92+ return create_and_sync(
93+ (requested_machine, result), post_pod_sync=True)
94+ except Exception:
95+ # Issue saving the machine into the database. The composed
96+ # machine must be decomposed or the MAAS information will be
97+ # out of sync of the actual Pod.
98+ discovered_machine, _ = result
99+ power_parameters.update(discovered_machine.power_parameters)
100+ wrap_decompose_machine(
101+ client_ids, self.pod.power_type, power_parameters,
102+ self.pod.id, self.pod.name).wait(timeout)
103+ raise
104
105
106 class ComposeMachineForPodsForm(forms.Form):
107@@ -720,7 +759,7 @@ class ComposeMachineForPodsForm(forms.Form):
108 return form.compose(
109 skip_commissioning=True,
110 creation_type=NODE_CREATION_TYPE.DYNAMIC)
111- except Exception:
112+ except (PodInvalidResources, PodProblem, ValidationError):
113 continue
114 # Second, try to compose a machine from commitable pods
115 for form in commit_forms:
116@@ -728,7 +767,7 @@ class ComposeMachineForPodsForm(forms.Form):
117 return form.compose(
118 skip_commissioning=True,
119 creation_type=NODE_CREATION_TYPE.DYNAMIC)
120- except Exception:
121+ except (PodInvalidResources, PodProblem, ValidationError):
122 continue
123 # No machine found.
124 return None
125diff --git a/src/maasserver/forms/tests/test_pods.py b/src/maasserver/forms/tests/test_pods.py
126index a49cc5d..5c3af2c 100644
127--- a/src/maasserver/forms/tests/test_pods.py
128+++ b/src/maasserver/forms/tests/test_pods.py
129@@ -45,7 +45,10 @@ from maasserver.testing.testcase import (
130 MAASServerTestCase,
131 MAASTransactionServerTestCase,
132 )
133-from maasserver.utils.orm import reload_object
134+from maasserver.utils.orm import (
135+ post_commit_hooks,
136+ reload_object,
137+)
138 from maasserver.utils.threads import deferToDatabase
139 from maastesting.matchers import (
140 MockCalledOnce,
141@@ -70,6 +73,7 @@ from provisioningserver.enum import (
142 MACVLAN_MODE,
143 MACVLAN_MODE_CHOICES,
144 )
145+from provisioningserver.rpc.exceptions import PodInvalidResources
146 from testtools import ExpectedException
147 from testtools.matchers import (
148 Equals,
149@@ -1507,7 +1511,8 @@ class TestComposeMachineForm(MAASTransactionServerTestCase):
150 'interfaces': interfaces,
151 }, request=request, pod=pod)
152 self.assertTrue(form.is_valid(), form.errors)
153- machine = form.compose()
154+ with post_commit_hooks:
155+ machine = form.compose()
156 ip = StaticIPAddress.objects.filter(ip=expected_ip).first()
157 self.assertThat(ip.get_interface().node, Equals(machine))
158
159@@ -1531,7 +1536,8 @@ class TestComposeMachineForm(MAASTransactionServerTestCase):
160
161 form = ComposeMachineForm(data={}, request=request, pod=pod)
162 self.assertTrue(form.is_valid())
163- created_machine = form.compose()
164+ with post_commit_hooks:
165+ created_machine = form.compose()
166 self.assertThat(created_machine, MatchesAll(
167 IsInstance(Machine),
168 MatchesStructure(
169@@ -1560,7 +1566,8 @@ class TestComposeMachineForm(MAASTransactionServerTestCase):
170
171 form = ComposeMachineForm(data={}, request=request, pod=pod)
172 self.assertTrue(form.is_valid())
173- form.compose()
174+ with post_commit_hooks:
175+ form.compose()
176 self.assertThat(mock_compose_machine, MockCalledOnceWith(
177 ANY, pod.power_type, {
178 'power_address': ANY,
179@@ -1570,6 +1577,45 @@ class TestComposeMachineForm(MAASTransactionServerTestCase):
180 make_known_host_interfaces(pod)),
181 pod_id=pod.id, name=pod.name))
182
183+ def test__compose_decomposes_on_failure(self):
184+ request = MagicMock()
185+ pod = make_pod_with_hints()
186+
187+ # Mock the RPC client.
188+ client = MagicMock()
189+ mock_getClient = self.patch(pods_module, "getClientFromIdentifiers")
190+ mock_getClient.return_value = succeed(client)
191+
192+ # Mock the result of the composed machine.
193+ composed_machine, pod_hints = self.make_compose_machine_result(pod)
194+ mock_compose_machine = self.patch(pods_module, "compose_machine")
195+ mock_compose_machine.return_value = succeed(
196+ (composed_machine, pod_hints))
197+
198+ # Mock start_commissioning so it doesn't use post commit hooks.
199+ self.patch(Machine, "start_commissioning")
200+
201+ # Mock Pod class causing the create_machine to raise an exception.
202+ self.patch(Pod, 'create_machine').side_effect = (
203+ factory.make_exception())
204+
205+ # Mock decompose to verify its called.
206+ mock_decompose_machine = self.patch(pods_module, "decompose_machine")
207+ mock_decompose_machine.return_value = succeed(None)
208+
209+ form = ComposeMachineForm(data={}, request=request, pod=pod)
210+ self.assertTrue(form.is_valid())
211+ self.assertRaises(Exception, form.compose)
212+
213+ power_params = pod.power_parameters
214+ power_params['default_storage_pool_id'] = (
215+ pod.default_storage_pool.pool_id)
216+ self.assertThat(
217+ mock_decompose_machine,
218+ MockCalledOnceWith(
219+ None, pod.power_type, power_params,
220+ pod_id=pod.id, name=pod.name))
221+
222 def test__compose_duplicated_hostname(self):
223 factory.make_Node(hostname='test')
224
225@@ -1619,7 +1665,8 @@ class TestComposeMachineForm(MAASTransactionServerTestCase):
226 form = ComposeMachineForm(
227 data={"skip_commissioning": 'true'}, request=request, pod=pod)
228 self.assertTrue(form.is_valid())
229- created_machine = form.compose()
230+ with post_commit_hooks:
231+ created_machine = form.compose()
232 self.assertThat(created_machine, MatchesAll(
233 IsInstance(Machine),
234 MatchesStructure(
235@@ -1648,7 +1695,8 @@ class TestComposeMachineForm(MAASTransactionServerTestCase):
236
237 form = ComposeMachineForm(data={}, request=request, pod=pod)
238 self.assertTrue(form.is_valid())
239- created_machine = form.compose(skip_commissioning=True)
240+ with post_commit_hooks:
241+ created_machine = form.compose(skip_commissioning=True)
242 self.assertThat(created_machine, MatchesAll(
243 IsInstance(Machine),
244 MatchesStructure(
245@@ -1680,7 +1728,8 @@ class TestComposeMachineForm(MAASTransactionServerTestCase):
246 "skip_commissioning": 'true',
247 }, request=request, pod=pod)
248 self.assertTrue(form.is_valid())
249- created_machine = form.compose()
250+ with post_commit_hooks:
251+ created_machine = form.compose()
252 self.assertThat(created_machine, MatchesAll(
253 IsInstance(Machine),
254 MatchesStructure(
255@@ -1708,7 +1757,8 @@ class TestComposeMachineForm(MAASTransactionServerTestCase):
256 "skip_commissioning": 'true',
257 }, request=request, pod=pod)
258 self.assertTrue(form.is_valid())
259- created_machine = form.compose()
260+ with post_commit_hooks:
261+ created_machine = form.compose()
262 self.assertEqual(pool, created_machine.pool)
263
264 def test__compose_uses_pod_pool(self):
265@@ -1732,7 +1782,8 @@ class TestComposeMachineForm(MAASTransactionServerTestCase):
266 "skip_commissioning": 'true',
267 }, request=request, pod=pod)
268 self.assertTrue(form.is_valid())
269- created_machine = form.compose()
270+ with post_commit_hooks:
271+ created_machine = form.compose()
272 self.assertEqual(pod.pool, created_machine.pool)
273
274 def test__compose_check_over_commit_ratios_raises_error_for_cores(self):
275@@ -1909,7 +1960,11 @@ class TestComposeMachineForPodsForm(MAASServerTestCase):
276 data = self.make_data(pods)
277 form = ComposeMachineForPodsForm(request=request, data=data, pods=pods)
278 mock_form_compose = self.patch(ComposeMachineForm, 'compose')
279- mock_form_compose.side_effect = [factory.make_exception(), None]
280+ mock_form_compose.side_effect = [
281+ factory.make_exception(
282+ message=factory.make_name('error'),
283+ bases=(PodInvalidResources, PodProblem, ValidationError)),
284+ None]
285 self.assertTrue(form.is_valid())
286
287 form.compose()
288@@ -1932,8 +1987,16 @@ class TestComposeMachineForPodsForm(MAASServerTestCase):
289 form = ComposeMachineForPodsForm(request=request, data=data, pods=pods)
290 mock_form_compose = self.patch(ComposeMachineForm, 'compose')
291 mock_form_compose.side_effect = [
292- factory.make_exception(), factory.make_exception(),
293- factory.make_exception(), None]
294+ factory.make_exception(
295+ message=factory.make_name('error'),
296+ bases=(PodInvalidResources, PodProblem, ValidationError)),
297+ factory.make_exception(
298+ message=factory.make_name('error'),
299+ bases=(PodInvalidResources, PodProblem, ValidationError)),
300+ factory.make_exception(
301+ message=factory.make_name('error'),
302+ bases=(PodInvalidResources, PodProblem, ValidationError)),
303+ None]
304 self.assertTrue(form.is_valid())
305
306 form.compose()

Subscribers

People subscribed via source and target branches