Merge lp:~julian-edwards/maas/pxeconfig-updates-cluster-interface-bug-1387859 into lp:~maas-committers/maas/trunk

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 3407
Proposed branch: lp:~julian-edwards/maas/pxeconfig-updates-cluster-interface-bug-1387859
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 352 lines (+96/-109)
7 files modified
src/maasserver/api/pxeconfig.py (+5/-1)
src/maasserver/api/tests/test_pxeconfig.py (+14/-0)
src/maasserver/models/macaddress.py (+33/-32)
src/maasserver/models/node.py (+9/-5)
src/maasserver/models/tests/test_macaddress.py (+35/-44)
src/maasserver/rpc/leases.py (+0/-2)
src/maasserver/rpc/tests/test_regionservice.py (+0/-25)
To merge this branch: bzr merge lp:~julian-edwards/maas/pxeconfig-updates-cluster-interface-bug-1387859
Reviewer Review Type Date Requested Status
Graham Binns (community) Approve
Review via email: mp+243937@code.launchpad.net

Commit message

Set the mac.cluster_interface when receiving a pxeconfig API request, instead of doing it when leases are parsed. Decoupling this makes it more resilient in the face of lease parser failures, and removes the update latency inherent in the parser.

Description of the change

Thanks to whomever wrote the Node.add_mac tests which don't use patching, and instead test the desired behaviour — it meant that I could just change the underlying code and watch it pass.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

LGTM. Nice work!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/pxeconfig.py'
2--- src/maasserver/api/pxeconfig.py 2014-12-05 16:13:38 +0000
3+++ src/maasserver/api/pxeconfig.py 2014-12-08 02:56:05 +0000
4@@ -34,6 +34,7 @@
5 MACAddress,
6 NodeGroup,
7 )
8+from maasserver.models.macaddress import update_mac_cluster_interfaces
9 from maasserver.preseed import (
10 compose_enlistment_preseed_url,
11 compose_preseed_url,
12@@ -163,12 +164,15 @@
13 is preferred.
14 """
15 request_mac = request.GET.get('mac', None)
16+ request_ip = request.GET.get('remote', None)
17 node = get_node_from_mac_string(request_mac)
18
19 if node is not None:
20- # Update the record of which MAC address this node uses to PXE boot.
21+ # Update the record of which MAC address and cluster interface
22+ # this node uses to PXE boot.
23 node.pxe_mac = MACAddress.objects.get(mac_address=request_mac)
24 node.save()
25+ update_mac_cluster_interfaces(request_ip, request_mac, node.nodegroup)
26
27 if node is None or node.get_boot_purpose() == "commissioning":
28 osystem = Config.objects.get_config('commissioning_osystem')
29
30=== modified file 'src/maasserver/api/tests/test_pxeconfig.py'
31--- src/maasserver/api/tests/test_pxeconfig.py 2014-12-05 16:13:38 +0000
32+++ src/maasserver/api/tests/test_pxeconfig.py 2014-12-08 02:56:05 +0000
33@@ -576,3 +576,17 @@
34 params_out = self.get_pxeconfig(params)
35 self.assertEqual(commissioning_osystem, params_out['osystem'])
36 self.assertEqual(commissioning_series, params_out['release'])
37+
38+ def test_pxeconfig_updates_cluster_interface_for_request_mac(self):
39+ params = self.get_mac_params()
40+ node = factory.make_Node(mac=True)
41+ params['cluster_uuid'] = node.nodegroup.uuid
42+ params['mac'] = node.get_primary_mac()
43+ update_mac_cluster_interfaces = self.patch(
44+ pxeconfig_module, 'update_mac_cluster_interfaces')
45+ response = self.client.get(reverse('pxeconfig'), params)
46+ self.assertEqual(httplib.OK, response.status_code, response.content)
47+ self.assertThat(
48+ update_mac_cluster_interfaces,
49+ MockCalledOnceWith(
50+ params['remote'], unicode(params['mac']), node.nodegroup))
51
52=== modified file 'src/maasserver/models/macaddress.py'
53--- src/maasserver/models/macaddress.py 2014-11-27 11:06:23 +0000
54+++ src/maasserver/models/macaddress.py 2014-12-08 02:56:05 +0000
55@@ -65,8 +65,14 @@
56 return None
57
58
59-def update_mac_cluster_interfaces(leases, cluster):
60+def update_mac_cluster_interfaces(ip, mac, cluster):
61 """Calculate and store which interface a MAC is attached to."""
62+ try:
63+ mac_address = MACAddress.objects.get(mac_address=mac)
64+ except MACAddress.DoesNotExist:
65+ # Silently ignore MAC addresses that we don't know about.
66+ return
67+
68 interface_ranges = {}
69 # Only consider configured interfaces.
70 interfaces = (
71@@ -74,6 +80,8 @@
72 .exclude(ip_range_low__isnull=True)
73 .exclude(ip_range_high__isnull=True)
74 )
75+ # Pre-calculate a dict of interface ranges, keyed by cluster
76+ # interface.
77 for interface in interfaces:
78 ip_range = IPRange(
79 interface.ip_range_low, interface.ip_range_high)
80@@ -83,37 +91,30 @@
81 else:
82 static_range = []
83 interface_ranges[interface] = (ip_range, static_range)
84- for ip, mac in leases.items():
85- try:
86- mac_address = MACAddress.objects.get(mac_address=mac)
87- except MACAddress.DoesNotExist:
88- # Silently ignore MAC addresses that we don't know about.
89- continue
90- for interface, (ip_range, static_range) in interface_ranges.items():
91- ipaddress = IPAddress(ip)
92- # Set the cluster interface only if it's new/changed.
93- # This is only an optimisation to prevent repeated logging.
94- changed = mac_address.cluster_interface != interface
95- in_range = ipaddress in ip_range or ipaddress in static_range
96- if in_range and changed:
97- mac_address.cluster_interface = interface
98- mac_address.save()
99- maaslog.info(
100- "%s %s linked to cluster interface %s",
101- mac_address.node.hostname, mac_address, interface.name)
102-
103- # Locate the Network to which this MAC belongs.
104- ipnetwork = interface.network
105- if ipnetwork is not None:
106- try:
107- network = Network.objects.get(ip=ipnetwork.ip.format())
108- network.macaddress_set.add(mac_address)
109- except Network.DoesNotExist:
110- pass
111-
112- # Cheap optimisation. No other interfaces will match, so
113- # break out of the loop.
114- break
115+
116+ # Look through the interface ranges to see if any match the passed
117+ # IP address.
118+ for interface, (ip_range, static_range) in interface_ranges.items():
119+ ipaddress = IPAddress(ip)
120+ # Set the cluster interface only if it's new/changed.
121+ # This is only an optimisation to prevent repeated logging.
122+ changed = mac_address.cluster_interface != interface
123+ in_range = ipaddress in ip_range or ipaddress in static_range
124+ if in_range and changed:
125+ mac_address.cluster_interface = interface
126+ mac_address.save()
127+ maaslog.info(
128+ "%s %s linked to cluster interface %s",
129+ mac_address.node.hostname, mac_address, interface.name)
130+
131+ # Locate the Network to which this MAC belongs and link it.
132+ ipnetwork = interface.network
133+ if ipnetwork is not None:
134+ try:
135+ network = Network.objects.get(ip=ipnetwork.ip.format())
136+ network.macaddress_set.add(mac_address)
137+ except Network.DoesNotExist:
138+ pass
139
140
141 class MACAddress(CleanSave, TimestampedModel):
142
143=== modified file 'src/maasserver/models/node.py'
144--- src/maasserver/models/node.py 2014-12-05 21:32:33 +0000
145+++ src/maasserver/models/node.py 2014-12-08 02:56:05 +0000
146@@ -105,7 +105,10 @@
147 get_db_state,
148 strip_domain,
149 )
150-from maasserver.utils.orm import commit_within_atomic_block
151+from maasserver.utils.orm import (
152+ commit_within_atomic_block,
153+ get_one,
154+ )
155 from metadataserver.enum import RESULT_TYPE
156 from metadataserver.models import NodeResult
157 from netaddr import IPAddress
158@@ -783,10 +786,11 @@
159
160 # See if there's a lease for this MAC and set its
161 # cluster_interface if so.
162- nodegroup_leases = {
163- lease.ip: lease.mac
164- for lease in DHCPLease.objects.filter(nodegroup=self.nodegroup)}
165- update_mac_cluster_interfaces(nodegroup_leases, self.nodegroup)
166+ leases = DHCPLease.objects.filter(
167+ nodegroup=self.nodegroup, mac=mac.mac_address)
168+ lease = get_one(leases)
169+ if lease is not None:
170+ update_mac_cluster_interfaces(lease.ip, lease.mac, self.nodegroup)
171
172 return mac
173
174
175=== modified file 'src/maasserver/models/tests/test_macaddress.py'
176--- src/maasserver/models/tests/test_macaddress.py 2014-11-27 11:06:23 +0000
177+++ src/maasserver/models/tests/test_macaddress.py 2014-12-08 02:56:05 +0000
178@@ -736,49 +736,45 @@
179 }
180 return cluster, mac_addresses, leases
181
182+ def make_cluster_with_mac_and_node_and_ip(self, use_static_range=False):
183+ cluster = factory.make_NodeGroup()
184+ mac_address = factory.make_MACAddress_with_Node()
185+ interface = factory.make_NodeGroupInterface(nodegroup=cluster)
186+ ip = get_random_ip_from_interface_range(interface, use_static_range)
187+ return cluster, interface, mac_address, ip
188+
189 def test_updates_mac_cluster_interfaces(self):
190- cluster, mac_addresses, leases = (
191- self.make_cluster_with_macs_and_leases())
192- update_mac_cluster_interfaces(leases, cluster)
193- results = {
194- mac_address: mac_address.cluster_interface
195- for mac_address in MACAddress.objects.filter(
196- mac_address__in=leases.values())
197- }
198- self.assertEqual(mac_addresses, results)
199+ cluster, interface, mac_address, ip = (
200+ self.make_cluster_with_mac_and_node_and_ip())
201+ update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
202+ mac_address = reload_object(mac_address)
203+ self.assertEqual(interface, mac_address.cluster_interface)
204
205 def test_considers_static_range_when_updating_interfaces(self):
206 cluster, mac_addresses, leases = (
207 self.make_cluster_with_macs_and_leases(use_static_range=True))
208- update_mac_cluster_interfaces(leases, cluster)
209- results = {
210- mac_address: mac_address.cluster_interface
211- for mac_address in MACAddress.objects.filter(
212- mac_address__in=leases.values())
213- }
214- self.assertEqual(mac_addresses, results)
215+ cluster, interface, mac_address, ip = (
216+ self.make_cluster_with_mac_and_node_and_ip(use_static_range=True))
217+ update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
218+ mac_address = reload_object(mac_address)
219+ self.assertEqual(interface, mac_address.cluster_interface)
220
221 def test_updates_network_relations(self):
222 # update_mac_cluster_interfaces should also associate the mac
223 # with the network on which it resides.
224 cluster, mac_addresses, leases = (
225 self.make_cluster_with_macs_and_leases())
226- expected_relations = []
227- for mac, interface in mac_addresses.viewitems():
228- net = create_Network_from_NodeGroupInterface(interface)
229- expected_relations.append((net, mac))
230- update_mac_cluster_interfaces(leases, cluster)
231- # Doing a single giant comparison here would be unintuitive and
232- # fugly, so I'm iterating.
233- for net, mac in expected_relations:
234- [observed_macddress] = net.macaddress_set.all()
235- self.expectThat(mac, Equals(observed_macddress))
236- interface = mac_addresses[mac]
237- self.expectThat(
238- net, MatchesStructure.byEquality(
239- default_gateway=interface.router_ip,
240- netmask=interface.subnet_mask,
241- ))
242+ cluster, interface, mac_address, ip = (
243+ self.make_cluster_with_mac_and_node_and_ip())
244+ net = create_Network_from_NodeGroupInterface(interface)
245+ update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
246+ [observed_macddress] = net.macaddress_set.all()
247+ self.expectThat(mac_address, Equals(observed_macddress))
248+ self.expectThat(
249+ net, MatchesStructure.byEquality(
250+ default_gateway=interface.router_ip,
251+ netmask=interface.subnet_mask,
252+ ))
253
254 def test_does_not_overwrite_network_with_same_name(self):
255 cluster = factory.make_NodeGroup()
256@@ -794,10 +790,8 @@
257 def test_ignores_mac_not_attached_to_cluster(self):
258 cluster = factory.make_NodeGroup()
259 mac_address = factory.make_MACAddress_with_Node()
260- leases = {
261- factory.make_ipv4_address(): mac_address.mac_address
262- }
263- update_mac_cluster_interfaces(leases, cluster)
264+ ip = factory.make_ipv4_address()
265+ update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
266 mac_address = MACAddress.objects.get(
267 id=mac_address.id)
268 self.assertIsNone(mac_address.cluster_interface)
269@@ -805,13 +799,11 @@
270 def test_ignores_unknown_macs(self):
271 cluster = factory.make_NodeGroup()
272 mac_address = factory.make_mac_address()
273- leases = {
274- factory.make_ipv4_address(): mac_address
275- }
276+ ip = factory.make_ipv4_address()
277 # This is a test to show that update_mac_cluster_interfaces()
278 # doesn't raise an Http404 when it comes across something it
279 # doesn't know, hence the lack of meaningful assertions.
280- update_mac_cluster_interfaces(leases, cluster)
281+ update_mac_cluster_interfaces(ip, mac_address, cluster)
282 self.assertFalse(
283 MACAddress.objects.filter(mac_address=mac_address).exists())
284
285@@ -824,10 +816,9 @@
286 ip=factory.make_ipv4_address(),
287 management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
288 mac_address = factory.make_mac_address()
289- leases = {
290- factory.make_ipv4_address(): mac_address
291- }
292- self.assertIsNone(update_mac_cluster_interfaces(leases, cluster))
293+ ip = factory.make_ipv4_address()
294+ self.assertIsNone(
295+ update_mac_cluster_interfaces(ip, mac_address, cluster))
296 # The real test is that update_mac_cluster_interfaces() doesn't
297 # stacktrace because of the unconfigured interface (see bug
298 # 1332596).
299
300=== modified file 'src/maasserver/rpc/leases.py'
301--- src/maasserver/rpc/leases.py 2014-10-01 23:07:50 +0000
302+++ src/maasserver/rpc/leases.py 2014-12-08 02:56:05 +0000
303@@ -17,7 +17,6 @@
304 ]
305
306 from maasserver.models.dhcplease import DHCPLease
307-from maasserver.models.macaddress import update_mac_cluster_interfaces
308 from maasserver.models.nodegroup import NodeGroup
309 from maasserver.utils.async import transactional
310 from provisioningserver.pserv_services.lease_upload_service import (
311@@ -50,5 +49,4 @@
312 else:
313 leases = convert_mappings_to_leases(mappings)
314 DHCPLease.objects.update_leases(nodegroup, leases)
315- update_mac_cluster_interfaces(leases, nodegroup)
316 return {}
317
318=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
319--- src/maasserver/rpc/tests/test_regionservice.py 2014-11-25 13:06:34 +0000
320+++ src/maasserver/rpc/tests/test_regionservice.py 2014-12-08 02:56:05 +0000
321@@ -420,31 +420,6 @@
322 self.expectThat(mac, Equals(mapping["mac"]))
323
324 @wait_for_reactor
325- @inlineCallbacks
326- def test__updates_mac_to_cluster_links(self):
327- uuid = factory.make_name("uuid")
328- nodegroup = yield deferToThread(self.make_node_group, uuid)
329- cluster_interface = yield deferToThread(
330- self.make_node_group_interface, nodegroup)
331- mac_address = yield deferToThread(self.make_mac_address)
332-
333- mapping = {
334- "ip": cluster_interface.ip_range_low,
335- "mac": mac_address.mac_address.get_raw(),
336- }
337-
338- response = yield call_responder(Region(), UpdateLeases, {
339- b"uuid": uuid, b"mappings": [mapping]})
340- self.assertThat(response, Equals({}))
341-
342- @transactional
343- def get_cluster_interface():
344- return reload_object(mac_address).cluster_interface
345-
346- observed = yield deferToThread(get_cluster_interface)
347- self.assertThat(observed, Equals(cluster_interface))
348-
349- @wait_for_reactor
350 def test__raises_NoSuchCluster_if_cluster_not_found(self):
351 uuid = factory.make_name("uuid")
352 d = call_responder(