Merge ~ack/maas:1902425-again-2.9 into maas:2.9
- Git
- lp:~ack/maas
- 1902425-again-2.9
- Merge into 2.9
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) |
Related bugs: |
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.
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.
Description of the change
Alberto Donato (ack) : | # |
MAAS Lander (maas-lander) wrote : | # |
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b 1902425-again-2.9 lp:~ack/maas/+git/maas into -b 2.9 lp:~maas-committers/maas
STATUS: FAILED BUILD
LOG: http://
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: 7e2efe7305ffa8a
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/models/interface.py b/src/maasserver/models/interface.py |
2 | index 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 |
55 | diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py |
56 | index 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 | |
112 | diff --git a/src/maasserver/models/tests/test_interface.py b/src/maasserver/models/tests/test_interface.py |
113 | index 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): |
219 | diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py |
220 | index 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): |
LANDING
-b 1902425-again-2.9 lp:~ack/maas/+git/maas into -b 2.9 lp:~maas-committers/maas
STATUS: FAILED BUILD maas-ci. internal: 8080/job/ maas/job/ branch- tester/ 9239/consoleTex t
LOG: http://