Merge ~ack/maas:1902425-deploy-ip-allocate-check into maas:master

Proposed by Alberto Donato
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)
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.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b 1902425-deploy-ip-allocate-check lp:~ack/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/8693/console
COMMIT: 74b732eaf6d2e68cc6127d279f248ca19dc590a4

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

UNIT TESTS
-b 1902425-deploy-ip-allocate-check lp:~ack/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/8694/console
COMMIT: a19d946759690cd77edc3377005d888839474d8f

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

UNIT TESTS
-b 1902425-deploy-ip-allocate-check lp:~ack/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 5c8b3db135d6c74ee7c0222dbe54da382342b938

review: Approve
Revision history for this message
Adam Collard (adam-collard) wrote :

LGTM

review: Approve

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/api/tests/test_machine.py b/src/maasserver/api/tests/test_machine.py
2index 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 = (
47diff --git a/src/maasserver/exceptions.py b/src/maasserver/exceptions.py
48index 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
64diff --git a/src/maasserver/models/interface.py b/src/maasserver/models/interface.py
65index 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:
90diff --git a/src/maasserver/models/neighbour.py b/src/maasserver/models/neighbour.py
91index 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.
133diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
134index 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:
387diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py
388index 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()
626diff --git a/src/maasserver/node_action.py b/src/maasserver/node_action.py
627index 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

Subscribers

People subscribed via source and target branches