Merge ~ack/maas:1902425-deploy-ip-allocate-check into maas:master
- Git
- lp:~ack/maas
- 1902425-deploy-ip-allocate-check
- Merge into master
Status: | Merged |
---|---|
Approved by: | Alberto Donato |
Approved revision: | 5c8b3db135d6c74ee7c0222dbe54da382342b938 |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~ack/maas:1902425-deploy-ip-allocate-check |
Merge into: | maas:master |
Diff against target: |
663 lines (+197/-146) 7 files modified
src/maasserver/api/tests/test_machine.py (+19/-1) src/maasserver/exceptions.py (+6/-0) src/maasserver/models/interface.py (+8/-6) src/maasserver/models/neighbour.py (+1/-24) src/maasserver/models/node.py (+68/-83) src/maasserver/models/tests/test_node.py (+82/-31) src/maasserver/node_action.py (+13/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Adam Collard (community) | Approve | ||
MAAS Lander | Approve | ||
Review via email: mp+393807@code.launchpad.net |
Commit message
LP: #1902425 - keep trying new addresses when assigning on deploy.
Previously, the logic would retry only 2 times in case of any issue (call
error, IP in use).
If the call succeeds but the IP is in use, it should keep trying new addresses
until a free one is found.
Description of the change
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b 1902425-
STATUS: FAILED
LOG: http://
COMMIT: a19d946759690cd
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b 1902425-
STATUS: SUCCESS
COMMIT: 5c8b3db135d6c74
There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.
Preview Diff
1 | diff --git a/src/maasserver/api/tests/test_machine.py b/src/maasserver/api/tests/test_machine.py |
2 | index dc89858..dd6495c 100644 |
3 | --- a/src/maasserver/api/tests/test_machine.py |
4 | +++ b/src/maasserver/api/tests/test_machine.py |
5 | @@ -31,6 +31,7 @@ from maasserver.enum import ( |
6 | BRIDGE_TYPE, |
7 | FILESYSTEM_FORMAT_TYPE_CHOICES, |
8 | FILESYSTEM_TYPE, |
9 | + INTERFACE_LINK_TYPE, |
10 | INTERFACE_TYPE, |
11 | IPADDRESS_TYPE, |
12 | NODE_STATUS, |
13 | @@ -715,8 +716,9 @@ class TestMachineAPI(APITestCase.ForUser): |
14 | ).return_value = [rack_controller] |
15 | self.patch(node_module.Node, "_power_control_node") |
16 | self.patch(node_module.Node, "_start_deployment") |
17 | + self.patch(node_module.Node, "_claim_auto_ips") |
18 | self.patch(machines_module, "get_curtin_merged_config") |
19 | - machine = factory.make_Node( |
20 | + machine = factory.make_Node_with_Interface_on_Subnet( |
21 | owner=self.user, |
22 | interface=True, |
23 | power_type="virsh", |
24 | @@ -724,6 +726,22 @@ class TestMachineAPI(APITestCase.ForUser): |
25 | status=NODE_STATUS.ALLOCATED, |
26 | bmc_connected_to=rack_controller, |
27 | ) |
28 | + |
29 | + # assign an IP to both the machine and the rack on the same subnet |
30 | + machine_interface = machine.get_boot_interface() |
31 | + [auto_ip] = machine_interface.ip_addresses.filter( |
32 | + alloc_type=IPADDRESS_TYPE.AUTO |
33 | + ) |
34 | + ip = factory.pick_ip_in_Subnet(auto_ip.subnet) |
35 | + auto_ip.ip = ip |
36 | + auto_ip.save() |
37 | + rack_if = rack_controller.interface_set.first() |
38 | + rack_if.link_subnet( |
39 | + INTERFACE_LINK_TYPE.STATIC, |
40 | + auto_ip.subnet, |
41 | + factory.pick_ip_in_Subnet(auto_ip.subnet), |
42 | + ) |
43 | + |
44 | osystem = make_usable_osystem(self) |
45 | distro_series = osystem["default_release"] |
46 | user_data = ( |
47 | diff --git a/src/maasserver/exceptions.py b/src/maasserver/exceptions.py |
48 | index e449790..32bf622 100644 |
49 | --- a/src/maasserver/exceptions.py |
50 | +++ b/src/maasserver/exceptions.py |
51 | @@ -141,6 +141,12 @@ class StaticIPAddressExhaustion(MAASAPIException): |
52 | api_error = int(http.client.SERVICE_UNAVAILABLE) |
53 | |
54 | |
55 | +class IPAddressCheckFailed(MAASAPIException): |
56 | + """IP address allocation checks failed.""" |
57 | + |
58 | + api_error = int(http.client.SERVICE_UNAVAILABLE) |
59 | + |
60 | + |
61 | class StaticIPAddressUnavailable(MAASAPIException): |
62 | """Raised when a requested IP is not available.""" |
63 | |
64 | diff --git a/src/maasserver/models/interface.py b/src/maasserver/models/interface.py |
65 | index 815a2a7..b7738dc 100644 |
66 | --- a/src/maasserver/models/interface.py |
67 | +++ b/src/maasserver/models/interface.py |
68 | @@ -1539,13 +1539,15 @@ class Interface(CleanSave, TimestampedModel): |
69 | deleted = Neighbour.objects.delete_and_log_obsolete_neighbours( |
70 | ip, mac, interface=self, vid=vid |
71 | ) |
72 | - neighbour = Neighbour.objects.get_current_binding( |
73 | - ip, mac, interface=self, vid=vid |
74 | + |
75 | + neighbour, created = Neighbour.objects.get_or_create( |
76 | + defaults={"time": time}, |
77 | + ip=ip, |
78 | + mac_address=mac, |
79 | + vid=vid, |
80 | + interface=self, |
81 | ) |
82 | - if neighbour is None: |
83 | - neighbour = Neighbour.objects.create( |
84 | - interface=self, ip=ip, vid=vid, mac_address=mac, time=time |
85 | - ) |
86 | + if created: |
87 | # If we deleted a previous neighbour, then we have already |
88 | # generated a log statement about this neighbour. |
89 | if not deleted: |
90 | diff --git a/src/maasserver/models/neighbour.py b/src/maasserver/models/neighbour.py |
91 | index 49e4b13..90ad864 100644 |
92 | --- a/src/maasserver/models/neighbour.py |
93 | +++ b/src/maasserver/models/neighbour.py |
94 | @@ -18,7 +18,7 @@ from maasserver.fields import MACAddressField |
95 | from maasserver.models.cleansave import CleanSave |
96 | from maasserver.models.interface import Interface |
97 | from maasserver.models.timestampedmodel import TimestampedModel |
98 | -from maasserver.utils.orm import get_one, MAASQueriesMixin, UniqueViolation |
99 | +from maasserver.utils.orm import MAASQueriesMixin |
100 | from provisioningserver.logger import get_maas_logger |
101 | from provisioningserver.utils.network import get_mac_organization |
102 | |
103 | @@ -111,29 +111,6 @@ class NeighbourManager(Manager, NeighbourQueriesMixin): |
104 | deleted = True |
105 | return deleted |
106 | |
107 | - def get_current_binding(self, ip: str, mac: str, interface: str, vid: int): |
108 | - """Returns the current neighbour for the specified values. |
109 | - |
110 | - Returns None if an object representing the specified IP, MAC, |
111 | - Interface, and VID does not exist. (This is not an error condition; |
112 | - it happens normally when the binding is created for the first time.) |
113 | - |
114 | - The caller must ensure that any obsolete bindings are deleted before |
115 | - calling this method. |
116 | - |
117 | - :raises UniqueViolation: If more than one binding is found for the |
118 | - specified interface, IP address, VID, and MAC address. (Which |
119 | - should never happen due to the `unique_together`.) |
120 | - """ |
121 | - query = self.filter( |
122 | - interface=interface, ip=ip, vid=vid, mac_address=mac |
123 | - ) |
124 | - # If we get an exception here, it is most likely due to an unlikely |
125 | - # race condition. (either that, or the caller neglected to remove |
126 | - # obsolete bindings before calling this method.) Therefore, raise |
127 | - # a UniqueViolation so this operation can be retried. |
128 | - return get_one(query, exception_class=UniqueViolation) |
129 | - |
130 | def get_by_updated_with_related_nodes(self): |
131 | """Returns a `QuerySet` of neighbours, while also selecting related |
132 | interfaces and nodes. |
133 | diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py |
134 | index 54f3aab..bdaf5b3 100644 |
135 | --- a/src/maasserver/models/node.py |
136 | +++ b/src/maasserver/models/node.py |
137 | @@ -105,6 +105,7 @@ from maasserver.enum import ( |
138 | SERVICE_STATUS, |
139 | ) |
140 | from maasserver.exceptions import ( |
141 | + IPAddressCheckFailed, |
142 | NodeStateViolation, |
143 | NoScriptsFound, |
144 | PowerProblem, |
145 | @@ -4352,26 +4353,28 @@ class Node(CleanSave, TimestampedModel): |
146 | allocated_ips.add(ip) |
147 | return allocated_ips |
148 | |
149 | - def _claim_auto_ips(self, defer): |
150 | + @inlineCallbacks |
151 | + def _claim_auto_ips(self): |
152 | """Perform claim AUTO IP addresses from the post_commit `defer`.""" |
153 | |
154 | @transactional |
155 | def clean_expired(): |
156 | """Clean the expired AUTO IP addresses.""" |
157 | - ips = StaticIPAddress.objects.filter( |
158 | + StaticIPAddress.objects.filter( |
159 | temp_expires_on__lte=datetime.utcnow() |
160 | - ) |
161 | - ips.update(ip=None, temp_expires_on=None) |
162 | + ).update(ip=None, temp_expires_on=None) |
163 | + |
164 | + # track which IPs have been already attempted for allocation |
165 | + attempted_ips = set() |
166 | |
167 | @transactional |
168 | def get_racks_to_check(allocated_ips): |
169 | """Calculate the rack controllers to perform the IP check on.""" |
170 | - # No allocated ips. |
171 | if not allocated_ips: |
172 | return None, None |
173 | |
174 | # Map the allocated IP addresses to the subnets they belong. |
175 | - ip_ids = map(attrgetter("id"), allocated_ips) |
176 | + ip_ids = [ip.id for ip in allocated_ips] |
177 | ips_to_subnet = StaticIPAddress.objects.filter(id__in=ip_ids) |
178 | subnets_to_ips = defaultdict(set) |
179 | for ip in ips_to_subnet: |
180 | @@ -4398,10 +4401,9 @@ class Node(CleanSave, TimestampedModel): |
181 | |
182 | return subnets_to_ips, subnets_to_clients |
183 | |
184 | - def perform_ip_checks(result): |
185 | - subnets_to_ips, subnets_to_clients = result |
186 | + def perform_ip_checks(subnets_to_ips, subnets_to_clients): |
187 | # Early-out if no IP addresses where allocated. |
188 | - if subnets_to_ips is None: |
189 | + if not subnets_to_ips: |
190 | return None |
191 | defers = [] |
192 | for subnet_id, ips in subnets_to_ips.items(): |
193 | @@ -4426,16 +4428,12 @@ class Node(CleanSave, TimestampedModel): |
194 | return DeferredList(defers) |
195 | |
196 | @transactional |
197 | - def process_results(results, try_count=0, max_try_count=2): |
198 | + def process_results(results, do_cleanups): |
199 | maaslog.debug( |
200 | f"Processing IP allocation results for {self.system_id}" |
201 | ) |
202 | - # Early-out if no IP addresses where allocated. |
203 | - if results is None: |
204 | - return False, try_count, max_try_count |
205 | - needs_retry = False |
206 | - for _, result in results: |
207 | - check_result, rack_id, ips = result |
208 | + check_failed, ips_in_use = False, False |
209 | + for _, (check_result, rack_id, ips) in results: |
210 | ip_ids = [ip.id for ip in ips] |
211 | ip_to_obj = {ip.ip: ip for ip in ips} |
212 | if check_result is None: |
213 | @@ -4446,33 +4444,19 @@ class Node(CleanSave, TimestampedModel): |
214 | temp_expires_on=None |
215 | ) |
216 | elif isinstance(check_result, Failure): |
217 | + check_failed = True |
218 | # Failed to perform IP address checking on the rack |
219 | # controller. |
220 | - if try_count < max_try_count: |
221 | + static_ips = StaticIPAddress.objects.filter(id__in=ip_ids) |
222 | + if do_cleanups: |
223 | + # Clear the temp_expires_on marking the IP address as |
224 | + # assigned. Thsi was the last attempt to perform the |
225 | + # allocation. |
226 | + static_ips.update(temp_expires_on=None) |
227 | + else: |
228 | # Clear the assigned IP address so new IP can be |
229 | # assigned in the next pass. |
230 | - StaticIPAddress.objects.filter(id__in=ip_ids).update( |
231 | - ip=None, temp_expires_on=None |
232 | - ) |
233 | - needs_retry = True |
234 | - log.err( |
235 | - check_result, |
236 | - "Performing IP address checking failed, " |
237 | - "will be retried.", |
238 | - ) |
239 | - else: |
240 | - # Clear the temp_expires_on marking the IP address |
241 | - # as assigned. Enough tries where performed to verify |
242 | - # and it always failed. |
243 | - StaticIPAddress.objects.filter(id__in=ip_ids).update( |
244 | - temp_expires_on=None |
245 | - ) |
246 | - log.err( |
247 | - check_result, |
248 | - "Performing IP address checking failed, " |
249 | - "will *NOT* be retried. (marking IP addresses " |
250 | - "as available).", |
251 | - ) |
252 | + static_ips.update(ip=None, temp_expires_on=None) |
253 | else: |
254 | # IP address checking was successful, use the results |
255 | # to either mark the assigned IP address no longer |
256 | @@ -4480,6 +4464,7 @@ class Node(CleanSave, TimestampedModel): |
257 | for ip_result in check_result["ip_addresses"]: |
258 | ip_obj = ip_to_obj[ip_result["ip_address"]] |
259 | if ip_result["used"]: |
260 | + attempted_ips.add(ip_obj.ip) |
261 | # Create a Neighbour reference to the IP address |
262 | # so the next loop will not use that IP address. |
263 | rack_interface = Interface.objects.filter( |
264 | @@ -4498,57 +4483,53 @@ class Node(CleanSave, TimestampedModel): |
265 | ip_obj.ip = None |
266 | ip_obj.temp_expires_on = None |
267 | ip_obj.save() |
268 | - needs_retry = True |
269 | + ips_in_use = True |
270 | else: |
271 | # IP was free and offically assigned. |
272 | ip_obj.temp_expires_on = None |
273 | ip_obj.save() |
274 | - return needs_retry, try_count, max_try_count |
275 | - |
276 | - def retrier(result): |
277 | - needs_retry, try_count, max_try_count = result |
278 | - if needs_retry: |
279 | - if try_count >= max_try_count: |
280 | - # Over retry count, IP address allocation is exhausted. |
281 | - raise StaticIPAddressExhaustion( |
282 | - "Failed to allocate the required AUTO IP addresses " |
283 | - "after %d retries." % max_try_count |
284 | - ) |
285 | - else: |
286 | - maaslog.debug("Retrying claiming of IPs.") |
287 | - # Re-loop the whole process again until max_try_count. |
288 | - d = succeed(None) |
289 | - d.addCallback( |
290 | - lambda _: deferToDatabase( |
291 | - transactional(self.claim_auto_ips), |
292 | - temp_expires_after=timedelta(minutes=5), |
293 | - ) |
294 | - ) |
295 | - d.addCallback(partial(deferToDatabase, get_racks_to_check)) |
296 | - d.addCallback(perform_ip_checks) |
297 | - d.addCallback( |
298 | - partial( |
299 | - deferToDatabase, |
300 | - process_results, |
301 | - try_count=(try_count + 1), |
302 | - ) |
303 | - ) |
304 | - d.addCallback(retrier) |
305 | - return d |
306 | |
307 | - defer.addCallback(lambda _: deferToDatabase(clean_expired)) |
308 | - defer.addCallback( |
309 | - lambda _: deferToDatabase( |
310 | + return check_failed, ips_in_use |
311 | + |
312 | + retry, max_retries = 0, 2 |
313 | + while retry <= max_retries: |
314 | + yield deferToDatabase(clean_expired) |
315 | + allocated_ips = yield deferToDatabase( |
316 | transactional(self.claim_auto_ips), |
317 | temp_expires_after=timedelta(minutes=5), |
318 | ) |
319 | - ) |
320 | - defer.addCallback(partial(deferToDatabase, get_racks_to_check)) |
321 | - defer.addCallback(perform_ip_checks) |
322 | - defer.addCallback(partial(deferToDatabase, process_results)) |
323 | - defer.addCallback(retrier) |
324 | + # skip IPs that have been tested already |
325 | + allocated_ips = set( |
326 | + ip for ip in allocated_ips if ip.ip not in attempted_ips |
327 | + ) |
328 | + if not allocated_ips: |
329 | + raise StaticIPAddressExhaustion( |
330 | + "Failed to allocate the required AUTO IP addresses" |
331 | + ) |
332 | + subnets_to_ips, subnets_to_clients = yield deferToDatabase( |
333 | + get_racks_to_check, allocated_ips |
334 | + ) |
335 | + results = yield perform_ip_checks( |
336 | + subnets_to_ips, subnets_to_clients |
337 | + ) |
338 | + if not results: |
339 | + return |
340 | |
341 | - return defer |
342 | + last_run = retry == max_retries |
343 | + check_failed, ips_in_use = yield deferToDatabase( |
344 | + process_results, results, last_run |
345 | + ) |
346 | + if ips_in_use: |
347 | + retry = 0 |
348 | + elif check_failed: |
349 | + retry += 1 |
350 | + else: |
351 | + return |
352 | + |
353 | + # over the retry count, check failed |
354 | + raise IPAddressCheckFailed( |
355 | + f"IP address checks failed after {max_retries} retries." |
356 | + ) |
357 | |
358 | @transactional |
359 | def release_interface_config(self): |
360 | @@ -5560,12 +5541,16 @@ class Node(CleanSave, TimestampedModel): |
361 | d = post_commit() |
362 | claimed_ips = False |
363 | |
364 | + @inlineCallbacks |
365 | + def claim_auto_ips(_): |
366 | + yield self._claim_auto_ips() |
367 | + |
368 | if self.status == NODE_STATUS.ALLOCATED: |
369 | old_status = self.status |
370 | # Claim AUTO IP addresses for the node if it's ALLOCATED. |
371 | # The current state being ALLOCATED is our indication that the node |
372 | # is being deployed for the first time. |
373 | - d = self._claim_auto_ips(d) |
374 | + d.addCallback(claim_auto_ips) |
375 | set_deployment_timeout = True |
376 | self._start_deployment() |
377 | claimed_ips = True |
378 | @@ -5584,7 +5569,7 @@ class Node(CleanSave, TimestampedModel): |
379 | ], |
380 | script__apply_configured_networking=True, |
381 | ).exists(): |
382 | - d = self._claim_auto_ips(d) |
383 | + d.addCallback(claim_auto_ips) |
384 | claimed_ips = True |
385 | set_deployment_timeout = False |
386 | elif self.status == NODE_STATUS.DEPLOYED and self.ephemeral_deployment: |
387 | diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py |
388 | index 1a52a58..c8a1b58 100644 |
389 | --- a/src/maasserver/models/tests/test_node.py |
390 | +++ b/src/maasserver/models/tests/test_node.py |
391 | @@ -60,6 +60,7 @@ from maasserver.enum import ( |
392 | INTERFACE_LINK_TYPE, |
393 | INTERFACE_TYPE, |
394 | IPADDRESS_TYPE, |
395 | + IPRANGE_TYPE, |
396 | NODE_CREATION_TYPE, |
397 | NODE_STATUS, |
398 | NODE_STATUS_CHOICES, |
399 | @@ -71,6 +72,7 @@ from maasserver.enum import ( |
400 | SERVICE_STATUS, |
401 | ) |
402 | from maasserver.exceptions import ( |
403 | + IPAddressCheckFailed, |
404 | NodeStateViolation, |
405 | PodProblem, |
406 | PowerProblem, |
407 | @@ -215,6 +217,7 @@ from provisioningserver.utils import znums |
408 | from provisioningserver.utils.enum import map_enum, map_enum_reverse |
409 | from provisioningserver.utils.env import get_maas_id |
410 | from provisioningserver.utils.fs import NamedLock |
411 | +from provisioningserver.utils.network import inet_ntop |
412 | from provisioningserver.utils.testing import MAASIDFixture |
413 | |
414 | wait_for_reactor = wait_for(30) # 30 seconds. |
415 | @@ -8565,7 +8568,7 @@ class TestNode_Start(MAASTransactionServerTestCase): |
416 | claim_auto_ips = self.patch_autospec(node, "_claim_auto_ips") |
417 | node.start(user) |
418 | |
419 | - self.expectThat(claim_auto_ips, MockCalledOnceWith(ANY)) |
420 | + self.expectThat(claim_auto_ips, MockCalledOnce()) |
421 | |
422 | def test_only_claims_auto_addresses_when_allocated(self): |
423 | user = factory.make_User() |
424 | @@ -8684,8 +8687,6 @@ class TestNode_Start(MAASTransactionServerTestCase): |
425 | ), |
426 | ] |
427 | |
428 | - # No rack controllers are currently connected to the test region |
429 | - # so no IP address checking will be performed. |
430 | with post_commit_hooks: |
431 | node.start(user) |
432 | |
433 | @@ -8728,8 +8729,6 @@ class TestNode_Start(MAASTransactionServerTestCase): |
434 | first_ip.temp_expires_on = datetime.utcnow() - timedelta(minutes=5) |
435 | first_ip.save() |
436 | |
437 | - # 2 tries will be made on the rack controller, both IP will be |
438 | - # used. |
439 | client.side_effect = [ |
440 | defer.fail(Exception()), |
441 | defer.fail(Exception()), |
442 | @@ -8738,8 +8737,6 @@ class TestNode_Start(MAASTransactionServerTestCase): |
443 | ), |
444 | ] |
445 | |
446 | - # No rack controllers are currently connected to the test region |
447 | - # so no IP address checking will be performed. |
448 | with post_commit_hooks: |
449 | node.start(user) |
450 | |
451 | @@ -8747,7 +8744,7 @@ class TestNode_Start(MAASTransactionServerTestCase): |
452 | self.assertThat(auto_ip.ip, Equals(first_ip.ip)) |
453 | self.assertThat(auto_ip.temp_expires_on, Is(None)) |
454 | |
455 | - def test_claims_auto_ip_addresses_assigns_ip_on_three_failures(self): |
456 | + def test_claims_auto_ip_addresses_fails_on_three_failures(self): |
457 | user = factory.make_User() |
458 | node = self.make_acquired_node_with_interface( |
459 | user, power_type="manual" |
460 | @@ -8782,24 +8779,21 @@ class TestNode_Start(MAASTransactionServerTestCase): |
461 | first_ip.temp_expires_on = datetime.utcnow() - timedelta(minutes=5) |
462 | first_ip.save() |
463 | |
464 | - # 2 tries will be made on the rack controller, both IP will be |
465 | - # used. |
466 | client.side_effect = [ |
467 | defer.fail(Exception()), |
468 | defer.fail(Exception()), |
469 | defer.fail(Exception()), |
470 | ] |
471 | |
472 | - # No rack controllers are currently connected to the test region |
473 | - # so no IP address checking will be performed. |
474 | - with post_commit_hooks: |
475 | - node.start(user) |
476 | + with ExpectedException(IPAddressCheckFailed): |
477 | + with post_commit_hooks: |
478 | + node.start(user) |
479 | |
480 | auto_ip = reload_object(auto_ip) |
481 | self.assertThat(auto_ip.ip, Equals(first_ip.ip)) |
482 | self.assertThat(auto_ip.temp_expires_on, Is(None)) |
483 | |
484 | - def test_claims_auto_ip_addresses_fails_after_three_tries(self): |
485 | + def test_claims_auto_ip_addresses_eventually_succeds_with_many_used(self): |
486 | user = factory.make_User() |
487 | node = self.make_acquired_node_with_interface( |
488 | user, power_type="manual" |
489 | @@ -8841,16 +8835,21 @@ class TestNode_Start(MAASTransactionServerTestCase): |
490 | exclude_addresses=[first_ip.ip], |
491 | ) |
492 | second_ip.delete() |
493 | - |
494 | - # This is the next IP address that will actaully get picked for the |
495 | - # machine as it will be free from the database and will no be |
496 | - # reported as used from the rack controller. |
497 | third_ip = StaticIPAddress.objects.allocate_new( |
498 | subnet=auto_ip.subnet, |
499 | alloc_type=IPADDRESS_TYPE.AUTO, |
500 | exclude_addresses=[first_ip.ip, second_ip.ip], |
501 | ) |
502 | third_ip.delete() |
503 | + # This is the next IP address that will actaully get picked for the |
504 | + # machine as it will be free from the database and will no be |
505 | + # reported as used from the rack controller. |
506 | + fourth_ip = StaticIPAddress.objects.allocate_new( |
507 | + subnet=auto_ip.subnet, |
508 | + alloc_type=IPADDRESS_TYPE.AUTO, |
509 | + exclude_addresses=[first_ip.ip, second_ip.ip, third_ip.ip], |
510 | + ) |
511 | + fourth_ip.delete() |
512 | |
513 | # 2 tries will be made on the rack controller, both IP will be |
514 | # used. |
515 | @@ -8864,10 +8863,69 @@ class TestNode_Start(MAASTransactionServerTestCase): |
516 | defer.succeed( |
517 | {"ip_addresses": [{"ip_address": third_ip.ip, "used": True}]} |
518 | ), |
519 | + defer.succeed( |
520 | + {"ip_addresses": [{"ip_address": fourth_ip.ip, "used": False}]} |
521 | + ), |
522 | ] |
523 | |
524 | - # No rack controllers are currently connected to the test region |
525 | - # so no IP address checking will be performed. |
526 | + with post_commit_hooks: |
527 | + node.start(user) |
528 | + |
529 | + auto_ip = reload_object(auto_ip) |
530 | + self.assertEqual(auto_ip.ip, fourth_ip.ip) |
531 | + self.assertIsNone(auto_ip.temp_expires_on) |
532 | + |
533 | + def test_claims_auto_ip_doesnt_retry_in_use(self): |
534 | + user = factory.make_User() |
535 | + node = self.make_acquired_node_with_interface( |
536 | + user, power_type="manual" |
537 | + ) |
538 | + node_interface = node.get_boot_interface() |
539 | + [auto_ip] = node_interface.ip_addresses.filter( |
540 | + alloc_type=IPADDRESS_TYPE.AUTO |
541 | + ) |
542 | + |
543 | + # Create a rack controller that has an interface on the same subnet |
544 | + # as the node. |
545 | + rack = factory.make_RackController() |
546 | + rack.interface_set.all().delete() |
547 | + rackif = factory.make_Interface(vlan=node_interface.vlan, node=rack) |
548 | + rackif.neighbour_discovery_state = True |
549 | + rackif.save() |
550 | + rackif_ip = factory.pick_ip_in_Subnet(auto_ip.subnet) |
551 | + rackif.link_subnet( |
552 | + INTERFACE_LINK_TYPE.STATIC, auto_ip.subnet, rackif_ip |
553 | + ) |
554 | + |
555 | + # Mock the rack controller connected to the region controller. |
556 | + client = Mock() |
557 | + client.ident = rack.system_id |
558 | + self.patch(node_module, "getAllClients").return_value = [client] |
559 | + |
560 | + # Must be executed in a transaction as `allocate_new` uses savepoints. |
561 | + with transaction.atomic(): |
562 | + used_ip = StaticIPAddress.objects.allocate_new( |
563 | + subnet=auto_ip.subnet, alloc_type=IPADDRESS_TYPE.AUTO |
564 | + ) |
565 | + # save the address for now, so it doesn't show in free ranges |
566 | + used_ip.save() |
567 | + |
568 | + # reserve all available ranges |
569 | + subnet_ranges = auto_ip.subnet.get_ipranges_not_in_use() |
570 | + for rng in subnet_ranges.ranges: |
571 | + factory.make_IPRange( |
572 | + subnet=auto_ip.subnet, |
573 | + start_ip=inet_ntop(rng.first), |
574 | + end_ip=inet_ntop(rng.last), |
575 | + alloc_type=IPRANGE_TYPE.RESERVED, |
576 | + ) |
577 | + # remove the used IP, MAAS thinks it's available, but return it as used |
578 | + # in checks |
579 | + used_ip.delete() |
580 | + client.return_value = defer.succeed( |
581 | + {"ip_addresses": [{"ip_address": used_ip.ip, "used": True}]} |
582 | + ) |
583 | + |
584 | with ExpectedException(StaticIPAddressExhaustion): |
585 | with post_commit_hooks: |
586 | node.start(user) |
587 | @@ -8878,11 +8936,8 @@ class TestNode_Start(MAASTransactionServerTestCase): |
588 | user, power_state=POWER_STATE.ON |
589 | ) |
590 | |
591 | - post_commit_defer = self.patch(node_module, "post_commit") |
592 | mock_claim_auto_ips = self.patch(Node, "_claim_auto_ips") |
593 | - mock_claim_auto_ips.return_value = post_commit_defer |
594 | mock_power_control = self.patch(Node, "_power_control_node") |
595 | - mock_power_control.return_value = post_commit_defer |
596 | |
597 | node.start(user) |
598 | |
599 | @@ -8890,7 +8945,7 @@ class TestNode_Start(MAASTransactionServerTestCase): |
600 | self.assertThat(node.status, Equals(NODE_STATUS.DEPLOYING)) |
601 | |
602 | # Calls _claim_auto_ips. |
603 | - self.assertThat(mock_claim_auto_ips, MockCalledOnceWith(ANY)) |
604 | + self.assertThat(mock_claim_auto_ips, MockCalledOnce()) |
605 | |
606 | # Calls _power_control_node when power_cycle. |
607 | self.assertThat( |
608 | @@ -8929,15 +8984,11 @@ class TestNode_Start(MAASTransactionServerTestCase): |
609 | ) |
610 | node.save() |
611 | |
612 | - post_commit_defer = self.patch(node_module, "post_commit") |
613 | + self.patch(Node, "_power_control_node") |
614 | mock_claim_auto_ips = self.patch(Node, "_claim_auto_ips") |
615 | - mock_claim_auto_ips.return_value = post_commit_defer |
616 | - mock_power_control = self.patch(Node, "_power_control_node") |
617 | - mock_power_control.return_value = post_commit_defer |
618 | |
619 | node._start(user) |
620 | - |
621 | - self.assertThat(mock_claim_auto_ips, MockCalledOnceWith(ANY)) |
622 | + self.assertThat(mock_claim_auto_ips, MockCalledOnce()) |
623 | |
624 | def test_doesnt_claims_auto_ips_when_script_doenst_need_it(self): |
625 | user = factory.make_User() |
626 | diff --git a/src/maasserver/node_action.py b/src/maasserver/node_action.py |
627 | index c5a194d..3f89a6f 100644 |
628 | --- a/src/maasserver/node_action.py |
629 | +++ b/src/maasserver/node_action.py |
630 | @@ -33,7 +33,11 @@ from maasserver.enum import ( |
631 | NODE_TYPE_CHOICES_DICT, |
632 | POWER_STATE, |
633 | ) |
634 | -from maasserver.exceptions import NodeActionError, StaticIPAddressExhaustion |
635 | +from maasserver.exceptions import ( |
636 | + IPAddressCheckFailed, |
637 | + NodeActionError, |
638 | + StaticIPAddressExhaustion, |
639 | +) |
640 | from maasserver.models import Config, ResourcePool, Tag, Zone |
641 | from maasserver.node_status import is_failed_status, NON_MONITORED_STATUSES |
642 | from maasserver.permissions import NodePermission |
643 | @@ -544,6 +548,10 @@ class Deploy(NodeAction): |
644 | "%s: Failed to start, static IP addresses are exhausted." |
645 | % self.node.hostname |
646 | ) |
647 | + except IPAddressCheckFailed: |
648 | + raise NodeActionError( |
649 | + f"{self.node.hostname}: Failed to start, IP addresses check failed." |
650 | + ) |
651 | except RPC_EXCEPTIONS + (ExternalProcessError,) as exception: |
652 | raise NodeActionError(exception) |
653 | |
654 | @@ -577,6 +585,10 @@ class PowerOn(NodeAction): |
655 | "%s: Failed to start, static IP addresses are exhausted." |
656 | % self.node.hostname |
657 | ) |
658 | + except IPAddressCheckFailed: |
659 | + raise NodeActionError( |
660 | + f"{self.node.hostname}: Failed to start, IP addresses check failed." |
661 | + ) |
662 | except RPC_EXCEPTIONS + (ExternalProcessError,) as exception: |
663 | raise NodeActionError(exception) |
664 |
UNIT TESTS deploy- ip-allocate- check lp:~ack/maas/+git/maas into -b master lp:~maas-committers/maas
-b 1902425-
STATUS: FAILED maas-ci. internal: 8080/job/ maas/job/ branch- tester/ 8693/console cc6127d279f248c a19dc590a4
LOG: http://
COMMIT: 74b732eaf6d2e68