Merge ~blake-rouse/maas:fix-1843493 into maas:master
- Git
- lp:~blake-rouse/maas
- fix-1843493
- Merge into master
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) |
Related bugs: |
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.
Description of the change
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://
COMMIT: 80dae65319be4d2
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b fix-1843493 lp:~blake-rouse/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED BUILD
LOG: http://
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b fix-1843493 lp:~blake-rouse/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED BUILD
LOG: http://
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b fix-1843493 lp:~blake-rouse/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED BUILD
LOG: http://
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://
COMMIT: 850984c31a86fc9
Pedro GuimarĂ£es (pguimaraes) wrote : | # |
The compose method in src/maasserver/
https:/
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:/
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:/
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
1 | diff --git a/src/maasserver/forms/pods.py b/src/maasserver/forms/pods.py |
2 | index 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 |
125 | diff --git a/src/maasserver/forms/tests/test_pods.py b/src/maasserver/forms/tests/test_pods.py |
126 | index 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() |
LGTM