Merge ~ack/maas:1902425-again-2.9 into maas:2.9

Proposed by Alberto Donato
Status: Merged
Approved by: Alberto Donato
Approved revision: 7e2efe7305ffa8ad446f87b8d2410d246d8e2644
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ack/maas:1902425-again-2.9
Merge into: maas:2.9
Diff against target: 435 lines (+159/-64)
4 files modified
src/maasserver/models/interface.py (+7/-20)
src/maasserver/models/node.py (+15/-8)
src/maasserver/models/tests/test_interface.py (+12/-22)
src/maasserver/models/tests/test_node.py (+125/-14)
Reviewer Review Type Date Requested Status
MAAS Lander Approve
Alberto Donato (community) Approve
Review via email: mp+398150@code.launchpad.net

Commit message

LP: #1902425

 - always create neighbour entries for IPs in use.
 - exclude already tested IPs when claiming IPs during deploy

Rework Interface.update_neighbour move check for neighbour discovery to call sites.
This also changes IP claim checks to always create a neighbour entry when the IP is found to be in use (even if the rack interface doesn't have neighbour discovery enabled).

This fixes a corner case for the linked bug which happens when the used IP is
on an interface that has neighbour discovery disabled, in which case the IP is
not recorded in MAAS, and can get picked up again.

To post a comment you must log in.
Revision history for this message
Alberto Donato (ack) :
review: Approve
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 1902425-again-2.9 lp:~ack/maas/+git/maas into -b 2.9 lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 7e2efe7305ffa8ad446f87b8d2410d246d8e2644

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/models/interface.py b/src/maasserver/models/interface.py
2index b7738dc..b5d9625 100644
3--- a/src/maasserver/models/interface.py
4+++ b/src/maasserver/models/interface.py
5@@ -111,7 +111,7 @@ class InterfaceQueriesMixin(MAASQueriesMixin):
6 specifiers,
7 specifier_types=specifier_types,
8 separator=separator,
9- **kwargs
10+ **kwargs,
11 )
12
13 def _add_interface_id_query(self, current_q, op, item):
14@@ -1522,20 +1522,10 @@ class Interface(CleanSave, TimestampedModel):
15 % (self.get_log_string(), vid, vlan.fabric.get_name())
16 )
17
18- def update_neighbour(self, neighbour_json: dict):
19- """Updates the neighbour table for this interface.
20-
21- Input is expected to be the neighbour JSON from the controller.
22- """
23- # Circular imports
24+ def update_neighbour(self, ip, mac, time, vid=None):
25+ """Updates the neighbour table for this interface."""
26 from maasserver.models.neighbour import Neighbour
27
28- if self.neighbour_discovery_state is False:
29- return None
30- ip = neighbour_json["ip"]
31- mac = neighbour_json["mac"]
32- time = neighbour_json["time"]
33- vid = neighbour_json.get("vid", None)
34 deleted = Neighbour.objects.delete_and_log_obsolete_neighbours(
35 ip, mac, interface=self, vid=vid
36 )
37@@ -1552,13 +1542,10 @@ class Interface(CleanSave, TimestampedModel):
38 # generated a log statement about this neighbour.
39 if not deleted:
40 maaslog.info(
41- "%s: New MAC, IP binding observed%s: %s, %s"
42- % (
43- self.get_log_string(),
44- Neighbour.objects.get_vid_log_snippet(vid),
45- mac,
46- ip,
47- )
48+ f"{self.get_log_string()}: "
49+ "New MAC, IP binding "
50+ f"observed{Neighbour.objects.get_vid_log_snippet(vid)}: "
51+ f"{mac}, {ip}"
52 )
53 else:
54 neighbour.time = time
55diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
56index 5d506b4..48852de 100644
57--- a/src/maasserver/models/node.py
58+++ b/src/maasserver/models/node.py
59@@ -4337,9 +4337,11 @@ class Node(CleanSave, TimestampedModel):
60 bridge_fd=bridge_fd,
61 )
62
63- def claim_auto_ips(self, temp_expires_after=None):
64+ def claim_auto_ips(self, exclude_addresses=None, temp_expires_after=None):
65 """Assign IP addresses to all interface links set to AUTO."""
66- exclude_addresses = set()
67+ exclude_addresses = (
68+ exclude_addresses.copy() if exclude_addresses else set()
69+ )
70 allocated_ips = set()
71 # Query for the interfaces again here; if we use the cached
72 # interface_set, we could skip a newly-created bridge if it was created
73@@ -4480,11 +4482,9 @@ class Node(CleanSave, TimestampedModel):
74 rack_interface = rack_interface.order_by("id")
75 rack_interface = rack_interface.first()
76 rack_interface.update_neighbour(
77- {
78- "ip": ip_obj.ip,
79- "mac": ip_result.get("mac_address"),
80- "time": time.time(),
81- }
82+ ip_obj.ip,
83+ ip_result.get("mac_address"),
84+ time.time(),
85 )
86 ip_obj.ip = None
87 ip_obj.temp_expires_on = None
88@@ -4502,6 +4502,7 @@ class Node(CleanSave, TimestampedModel):
89 yield deferToDatabase(clean_expired)
90 allocated_ips = yield deferToDatabase(
91 transactional(self.claim_auto_ips),
92+ exclude_addresses=attempted_ips,
93 temp_expires_after=timedelta(minutes=5),
94 )
95 if not allocated_ips:
96@@ -6723,8 +6724,14 @@ class Controller(Node):
97 for neighbour in neighbours:
98 interface = interfaces.get(neighbour["interface"], None)
99 if interface is not None:
100- interface.update_neighbour(neighbour)
101 vid = neighbour.get("vid", None)
102+ if interface.neighbour_discovery_state:
103+ interface.update_neighbour(
104+ neighbour["ip"],
105+ neighbour["mac"],
106+ neighbour["time"],
107+ vid=vid,
108+ )
109 if vid is not None:
110 interface.report_vid(vid)
111
112diff --git a/src/maasserver/models/tests/test_interface.py b/src/maasserver/models/tests/test_interface.py
113index b86dbfb..ee16f56 100644
114--- a/src/maasserver/models/tests/test_interface.py
115+++ b/src/maasserver/models/tests/test_interface.py
116@@ -1363,7 +1363,7 @@ class InterfaceTest(MAASServerTestCase):
117 self.assertEquals(0, interface.link_speed)
118
119
120-class InterfaceUpdateNeighbourTest(MAASServerTestCase):
121+class TestInterfaceUpdateNeighbour(MAASServerTestCase):
122 """Tests for `Interface.update_neighbour`."""
123
124 def make_neighbour_json(self, ip=None, mac=None, time=None, **kwargs):
125@@ -1384,22 +1384,15 @@ class InterfaceUpdateNeighbourTest(MAASServerTestCase):
126 vid = None
127 return {"ip": ip, "mac": mac, "time": time, "vid": vid}
128
129- def test_ignores_updates_if_neighbour_discovery_state_is_false(self):
130+ def test_adds_new_neighbour(self):
131 iface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
132- iface.update_neighbour(self.make_neighbour_json())
133- self.assertThat(Neighbour.objects.count(), Equals(0))
134-
135- def test_adds_new_neighbour_if_neighbour_discovery_state_is_true(self):
136- iface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
137- iface.neighbour_discovery_state = True
138- iface.update_neighbour(self.make_neighbour_json())
139+ iface.update_neighbour(**self.make_neighbour_json())
140 self.assertThat(Neighbour.objects.count(), Equals(1))
141
142 def test_updates_existing_neighbour(self):
143 iface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
144- iface.neighbour_discovery_state = True
145 json = self.make_neighbour_json()
146- iface.update_neighbour(json)
147+ iface.update_neighbour(**json)
148 neighbour = get_one(Neighbour.objects.all())
149 # Pretend this was updated one day ago.
150 yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
151@@ -1410,7 +1403,7 @@ class InterfaceUpdateNeighbourTest(MAASServerTestCase):
152 Equals(int(yesterday.timestamp())),
153 )
154 json["time"] += 1
155- iface.update_neighbour(json)
156+ iface.update_neighbour(**json)
157 neighbour = reload_object(neighbour)
158 self.assertThat(Neighbour.objects.count(), Equals(1))
159 self.assertThat(neighbour.time, Equals(json["time"]))
160@@ -1422,13 +1415,12 @@ class InterfaceUpdateNeighbourTest(MAASServerTestCase):
161
162 def test_replaces_obsolete_neighbour(self):
163 iface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
164- iface.neighbour_discovery_state = True
165 json = self.make_neighbour_json()
166- iface.update_neighbour(json)
167+ iface.update_neighbour(**json)
168 # Have a different MAC address claim ownership of the IP.
169 json["time"] += 1
170 json["mac"] = factory.make_mac_address()
171- iface.update_neighbour(json)
172+ iface.update_neighbour(**json)
173 self.assertThat(Neighbour.objects.count(), Equals(1))
174 self.assertThat(
175 list(Neighbour.objects.all())[0].mac_address, Equals(json["mac"])
176@@ -1439,10 +1431,8 @@ class InterfaceUpdateNeighbourTest(MAASServerTestCase):
177
178 def test_logs_new_binding(self):
179 iface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
180- iface.neighbour_discovery_state = True
181- json = self.make_neighbour_json()
182 with FakeLogger("maas.interface") as maaslog:
183- iface.update_neighbour(json)
184+ iface.update_neighbour(**self.make_neighbour_json())
185 self.assertDocTestMatches(
186 "...: New MAC, IP binding observed...", maaslog.output
187 )
188@@ -1451,18 +1441,18 @@ class InterfaceUpdateNeighbourTest(MAASServerTestCase):
189 iface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
190 iface.neighbour_discovery_state = True
191 json = self.make_neighbour_json()
192- iface.update_neighbour(json)
193+ iface.update_neighbour(**json)
194 # Have a different MAC address claim ownership of the IP.
195 json["time"] += 1
196 json["mac"] = factory.make_mac_address()
197 with FakeLogger("maas.neighbour") as maaslog:
198- iface.update_neighbour(json)
199+ iface.update_neighbour(**json)
200 self.assertDocTestMatches(
201 "...: IP address...moved from...to...", maaslog.output
202 )
203
204
205-class InterfaceUpdateMDNSEntryTest(MAASServerTestCase):
206+class TestInterfaceUpdateMDNSEntry(MAASServerTestCase):
207 """Tests for `Interface.update_mdns_entry`."""
208
209 def make_mdns_entry_json(self, ip=None, hostname=None):
210@@ -1477,7 +1467,7 @@ class InterfaceUpdateMDNSEntryTest(MAASServerTestCase):
211
212 def test_ignores_updates_if_mdns_discovery_state_is_false(self):
213 iface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
214- iface.update_neighbour(self.make_mdns_entry_json())
215+ iface.update_mdns_entry(self.make_mdns_entry_json())
216 self.assertThat(MDNS.objects.count(), Equals(0))
217
218 def test_adds_new_entry_if_mdns_discovery_state_is_true(self):
219diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py
220index a5a5aa7..61f1fe6 100644
221--- a/src/maasserver/models/tests/test_node.py
222+++ b/src/maasserver/models/tests/test_node.py
223@@ -79,6 +79,7 @@ from maasserver.exceptions import (
224 StaticIPAddressExhaustion,
225 )
226 from maasserver.models import (
227+ BMCRoutableRackControllerRelationship,
228 BondInterface,
229 BootResource,
230 BridgeInterface,
231@@ -86,11 +87,13 @@ from maasserver.models import (
232 Controller,
233 Device,
234 Domain,
235+ Event,
236 EventType,
237 Fabric,
238 Interface,
239 LicenseKey,
240 Machine,
241+ Neighbour,
242 Node,
243 )
244 from maasserver.models import (
245@@ -100,6 +103,7 @@ from maasserver.models import (
246 RAID,
247 RegionController,
248 RegionRackRPCConnection,
249+ ResourcePool,
250 Service,
251 StaticIPAddress,
252 Subnet,
253@@ -108,12 +112,10 @@ from maasserver.models import (
254 VLANInterface,
255 VolumeGroup,
256 )
257-from maasserver.models import Bcache
258+from maasserver.models import Bcache, BMC
259 from maasserver.models import bmc as bmc_module
260 from maasserver.models import node as node_module
261-from maasserver.models.bmc import BMC, BMCRoutableRackControllerRelationship
262 from maasserver.models.config import NetworkDiscoveryConfig
263-from maasserver.models.event import Event
264 import maasserver.models.interface as interface_module
265 from maasserver.models.node import (
266 DEFAULT_BIOS_BOOT_METHOD,
267@@ -123,7 +125,6 @@ from maasserver.models.node import (
268 PowerInfo,
269 )
270 from maasserver.models.partitiontable import PARTITION_TABLE_EXTRA_SPACE
271-from maasserver.models.resourcepool import ResourcePool
272 from maasserver.models.signals import power as node_query
273 from maasserver.models.timestampedmodel import now
274 from maasserver.node_status import (
275@@ -8694,6 +8695,74 @@ class TestNode_Start(MAASTransactionServerTestCase):
276 self.assertThat(auto_ip.ip, Equals(third_ip.ip))
277 self.assertThat(auto_ip.temp_expires_on, Is(None))
278
279+ def test_claims_auto_ip_addresses_skips_used_ip_discovery_disabled(self):
280+ user = factory.make_User()
281+ node = self.make_acquired_node_with_interface(
282+ user, power_type="manual"
283+ )
284+ node_interface = node.get_boot_interface()
285+ [auto_ip] = node_interface.ip_addresses.filter(
286+ alloc_type=IPADDRESS_TYPE.AUTO
287+ )
288+
289+ # Create a rack controller that has an interface on the same subnet
290+ # as the node. Don't enable neighbour discovery
291+ rack = factory.make_RackController()
292+ rack.interface_set.all().delete()
293+ rackif = factory.make_Interface(vlan=node_interface.vlan, node=rack)
294+ rackif_ip = factory.pick_ip_in_Subnet(auto_ip.subnet)
295+ rackif.link_subnet(
296+ INTERFACE_LINK_TYPE.STATIC, auto_ip.subnet, rackif_ip
297+ )
298+
299+ # Mock the rack controller connected to the region controller.
300+ client = Mock()
301+ client.ident = rack.system_id
302+ self.patch(node_module, "getAllClients").return_value = [client]
303+
304+ # Must be executed in a transaction as `allocate_new` uses savepoints.
305+ with transaction.atomic():
306+ # Get two IPs and remove them so they're unknown
307+ ip = StaticIPAddress.objects.allocate_new(
308+ subnet=auto_ip.subnet, alloc_type=IPADDRESS_TYPE.AUTO
309+ )
310+ ip1 = ip.ip
311+ ip.delete()
312+ ip = StaticIPAddress.objects.allocate_new(
313+ subnet=auto_ip.subnet,
314+ alloc_type=IPADDRESS_TYPE.AUTO,
315+ exclude_addresses=[ip1],
316+ )
317+ ip2 = ip.ip
318+ ip.delete()
319+
320+ client.side_effect = [
321+ defer.succeed(
322+ {
323+ "ip_addresses": [
324+ {
325+ "ip_address": ip1,
326+ "used": True,
327+ "mac_address": factory.make_mac_address(),
328+ }
329+ ]
330+ }
331+ ),
332+ defer.succeed(
333+ {"ip_addresses": [{"ip_address": ip2, "used": False}]}
334+ ),
335+ ]
336+
337+ with post_commit_hooks:
338+ node.start(user)
339+
340+ auto_ip = reload_object(auto_ip)
341+ self.assertThat(auto_ip.ip, Equals(ip2))
342+ self.assertThat(auto_ip.temp_expires_on, Is(None))
343+ self.assertCountEqual(
344+ [ip1], Neighbour.objects.values_list("ip", flat=True)
345+ )
346+
347 def test_claims_auto_ip_addresses_retries_on_failure_from_rack(self):
348 user = factory.make_User()
349 node = self.make_acquired_node_with_interface(
350@@ -9914,21 +9983,51 @@ class TestControllerUpdateDiscoveryState(MAASServerTestCase):
351 class TestReportNeighbours(MAASServerTestCase):
352 """Tests for `Controller.report_neighbours()."""
353
354- def test_calls_update_neighbour_for_each_neighbour(self):
355+ def test_no_update_neighbours_calls_if_discovery_disabled(self):
356 rack = factory.make_RackController()
357 factory.make_Interface(name="eth0", node=rack)
358- factory.make_Interface(name="eth1", node=rack)
359 update_neighbour = self.patch(
360 interface_module.Interface, "update_neighbour"
361 )
362 neighbours = [
363- {"interface": "eth0", "mac": factory.make_mac_address()},
364- {"interface": "eth1", "mac": factory.make_mac_address()},
365+ {
366+ "interface": "eth0",
367+ "mac": factory.make_mac_address(),
368+ "ip": factory.make_ipv4_address(),
369+ "time": datetime.now(),
370+ },
371+ ]
372+ rack.report_neighbours(neighbours)
373+ update_neighbour.assert_not_called()
374+
375+ def test_calls_update_neighbour_for_each_neighbour(self):
376+ rack = factory.make_RackController()
377+ if1 = factory.make_Interface(name="eth0", node=rack)
378+ if1.neighbour_discovery_state = True
379+ if1.save()
380+ if2 = factory.make_Interface(name="eth1", node=rack)
381+ if2.neighbour_discovery_state = True
382+ if2.save()
383+ update_neighbour = self.patch(
384+ interface_module.Interface, "update_neighbour"
385+ )
386+ neighbours = [
387+ {
388+ "interface": "eth0",
389+ "mac": factory.make_mac_address(),
390+ "ip": factory.make_ipv4_address(),
391+ "time": datetime.now(),
392+ },
393+ {
394+ "interface": "eth1",
395+ "mac": factory.make_mac_address(),
396+ "ip": factory.make_ipv4_address(),
397+ "time": datetime.now(),
398+ },
399 ]
400 rack.report_neighbours(neighbours)
401- self.assertThat(
402- update_neighbour,
403- MockCallsMatch(*[call(neighbour) for neighbour in neighbours]),
404+ update_neighbour.assert_has_calls(
405+ [call(n["ip"], n["mac"], n["time"], vid=None) for n in neighbours]
406 )
407
408 def test_calls_report_vid_for_each_vid(self):
409@@ -9939,11 +10038,23 @@ class TestReportNeighbours(MAASServerTestCase):
410 self.patch(interface_module.Interface, "update_neighbour")
411 report_vid = self.patch(interface_module.Interface, "report_vid")
412 neighbours = [
413- {"interface": "eth0", "mac": factory.make_mac_address(), "vid": 3},
414- {"interface": "eth1", "mac": factory.make_mac_address(), "vid": 7},
415+ {
416+ "interface": "eth0",
417+ "ip": factory.make_ipv4_address(),
418+ "time": datetime.now(),
419+ "mac": factory.make_mac_address(),
420+ "vid": 3,
421+ },
422+ {
423+ "interface": "eth1",
424+ "ip": factory.make_ipv4_address(),
425+ "time": datetime.now(),
426+ "mac": factory.make_mac_address(),
427+ "vid": 7,
428+ },
429 ]
430 rack.report_neighbours(neighbours)
431- self.assertThat(report_vid, MockCallsMatch(call(3), call(7)))
432+ report_vid.assert_has_calls([call(3), call(7)])
433
434
435 class TestReportMDNSEntries(MAASServerTestCase):

Subscribers

People subscribed via source and target branches