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
=== modified file 'src/maasserver/api/pxeconfig.py'
--- src/maasserver/api/pxeconfig.py 2014-12-05 16:13:38 +0000
+++ src/maasserver/api/pxeconfig.py 2014-12-08 02:56:05 +0000
@@ -34,6 +34,7 @@
34 MACAddress,34 MACAddress,
35 NodeGroup,35 NodeGroup,
36 )36 )
37from maasserver.models.macaddress import update_mac_cluster_interfaces
37from maasserver.preseed import (38from maasserver.preseed import (
38 compose_enlistment_preseed_url,39 compose_enlistment_preseed_url,
39 compose_preseed_url,40 compose_preseed_url,
@@ -163,12 +164,15 @@
163 is preferred.164 is preferred.
164 """165 """
165 request_mac = request.GET.get('mac', None)166 request_mac = request.GET.get('mac', None)
167 request_ip = request.GET.get('remote', None)
166 node = get_node_from_mac_string(request_mac)168 node = get_node_from_mac_string(request_mac)
167169
168 if node is not None:170 if node is not None:
169 # Update the record of which MAC address this node uses to PXE boot.171 # Update the record of which MAC address and cluster interface
172 # this node uses to PXE boot.
170 node.pxe_mac = MACAddress.objects.get(mac_address=request_mac)173 node.pxe_mac = MACAddress.objects.get(mac_address=request_mac)
171 node.save()174 node.save()
175 update_mac_cluster_interfaces(request_ip, request_mac, node.nodegroup)
172176
173 if node is None or node.get_boot_purpose() == "commissioning":177 if node is None or node.get_boot_purpose() == "commissioning":
174 osystem = Config.objects.get_config('commissioning_osystem')178 osystem = Config.objects.get_config('commissioning_osystem')
175179
=== modified file 'src/maasserver/api/tests/test_pxeconfig.py'
--- src/maasserver/api/tests/test_pxeconfig.py 2014-12-05 16:13:38 +0000
+++ src/maasserver/api/tests/test_pxeconfig.py 2014-12-08 02:56:05 +0000
@@ -576,3 +576,17 @@
576 params_out = self.get_pxeconfig(params)576 params_out = self.get_pxeconfig(params)
577 self.assertEqual(commissioning_osystem, params_out['osystem'])577 self.assertEqual(commissioning_osystem, params_out['osystem'])
578 self.assertEqual(commissioning_series, params_out['release'])578 self.assertEqual(commissioning_series, params_out['release'])
579
580 def test_pxeconfig_updates_cluster_interface_for_request_mac(self):
581 params = self.get_mac_params()
582 node = factory.make_Node(mac=True)
583 params['cluster_uuid'] = node.nodegroup.uuid
584 params['mac'] = node.get_primary_mac()
585 update_mac_cluster_interfaces = self.patch(
586 pxeconfig_module, 'update_mac_cluster_interfaces')
587 response = self.client.get(reverse('pxeconfig'), params)
588 self.assertEqual(httplib.OK, response.status_code, response.content)
589 self.assertThat(
590 update_mac_cluster_interfaces,
591 MockCalledOnceWith(
592 params['remote'], unicode(params['mac']), node.nodegroup))
579593
=== modified file 'src/maasserver/models/macaddress.py'
--- src/maasserver/models/macaddress.py 2014-11-27 11:06:23 +0000
+++ src/maasserver/models/macaddress.py 2014-12-08 02:56:05 +0000
@@ -65,8 +65,14 @@
65 return None65 return None
6666
6767
68def update_mac_cluster_interfaces(leases, cluster):68def update_mac_cluster_interfaces(ip, mac, cluster):
69 """Calculate and store which interface a MAC is attached to."""69 """Calculate and store which interface a MAC is attached to."""
70 try:
71 mac_address = MACAddress.objects.get(mac_address=mac)
72 except MACAddress.DoesNotExist:
73 # Silently ignore MAC addresses that we don't know about.
74 return
75
70 interface_ranges = {}76 interface_ranges = {}
71 # Only consider configured interfaces.77 # Only consider configured interfaces.
72 interfaces = (78 interfaces = (
@@ -74,6 +80,8 @@
74 .exclude(ip_range_low__isnull=True)80 .exclude(ip_range_low__isnull=True)
75 .exclude(ip_range_high__isnull=True)81 .exclude(ip_range_high__isnull=True)
76 )82 )
83 # Pre-calculate a dict of interface ranges, keyed by cluster
84 # interface.
77 for interface in interfaces:85 for interface in interfaces:
78 ip_range = IPRange(86 ip_range = IPRange(
79 interface.ip_range_low, interface.ip_range_high)87 interface.ip_range_low, interface.ip_range_high)
@@ -83,37 +91,30 @@
83 else:91 else:
84 static_range = []92 static_range = []
85 interface_ranges[interface] = (ip_range, static_range)93 interface_ranges[interface] = (ip_range, static_range)
86 for ip, mac in leases.items():94
87 try:95 # Look through the interface ranges to see if any match the passed
88 mac_address = MACAddress.objects.get(mac_address=mac)96 # IP address.
89 except MACAddress.DoesNotExist:97 for interface, (ip_range, static_range) in interface_ranges.items():
90 # Silently ignore MAC addresses that we don't know about.98 ipaddress = IPAddress(ip)
91 continue99 # Set the cluster interface only if it's new/changed.
92 for interface, (ip_range, static_range) in interface_ranges.items():100 # This is only an optimisation to prevent repeated logging.
93 ipaddress = IPAddress(ip)101 changed = mac_address.cluster_interface != interface
94 # Set the cluster interface only if it's new/changed.102 in_range = ipaddress in ip_range or ipaddress in static_range
95 # This is only an optimisation to prevent repeated logging.103 if in_range and changed:
96 changed = mac_address.cluster_interface != interface104 mac_address.cluster_interface = interface
97 in_range = ipaddress in ip_range or ipaddress in static_range105 mac_address.save()
98 if in_range and changed:106 maaslog.info(
99 mac_address.cluster_interface = interface107 "%s %s linked to cluster interface %s",
100 mac_address.save()108 mac_address.node.hostname, mac_address, interface.name)
101 maaslog.info(109
102 "%s %s linked to cluster interface %s",110 # Locate the Network to which this MAC belongs and link it.
103 mac_address.node.hostname, mac_address, interface.name)111 ipnetwork = interface.network
104112 if ipnetwork is not None:
105 # Locate the Network to which this MAC belongs.113 try:
106 ipnetwork = interface.network114 network = Network.objects.get(ip=ipnetwork.ip.format())
107 if ipnetwork is not None:115 network.macaddress_set.add(mac_address)
108 try:116 except Network.DoesNotExist:
109 network = Network.objects.get(ip=ipnetwork.ip.format())117 pass
110 network.macaddress_set.add(mac_address)
111 except Network.DoesNotExist:
112 pass
113
114 # Cheap optimisation. No other interfaces will match, so
115 # break out of the loop.
116 break
117118
118119
119class MACAddress(CleanSave, TimestampedModel):120class MACAddress(CleanSave, TimestampedModel):
120121
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2014-12-05 21:32:33 +0000
+++ src/maasserver/models/node.py 2014-12-08 02:56:05 +0000
@@ -105,7 +105,10 @@
105 get_db_state,105 get_db_state,
106 strip_domain,106 strip_domain,
107 )107 )
108from maasserver.utils.orm import commit_within_atomic_block108from maasserver.utils.orm import (
109 commit_within_atomic_block,
110 get_one,
111 )
109from metadataserver.enum import RESULT_TYPE112from metadataserver.enum import RESULT_TYPE
110from metadataserver.models import NodeResult113from metadataserver.models import NodeResult
111from netaddr import IPAddress114from netaddr import IPAddress
@@ -783,10 +786,11 @@
783786
784 # See if there's a lease for this MAC and set its787 # See if there's a lease for this MAC and set its
785 # cluster_interface if so.788 # cluster_interface if so.
786 nodegroup_leases = {789 leases = DHCPLease.objects.filter(
787 lease.ip: lease.mac790 nodegroup=self.nodegroup, mac=mac.mac_address)
788 for lease in DHCPLease.objects.filter(nodegroup=self.nodegroup)}791 lease = get_one(leases)
789 update_mac_cluster_interfaces(nodegroup_leases, self.nodegroup)792 if lease is not None:
793 update_mac_cluster_interfaces(lease.ip, lease.mac, self.nodegroup)
790794
791 return mac795 return mac
792796
793797
=== modified file 'src/maasserver/models/tests/test_macaddress.py'
--- src/maasserver/models/tests/test_macaddress.py 2014-11-27 11:06:23 +0000
+++ src/maasserver/models/tests/test_macaddress.py 2014-12-08 02:56:05 +0000
@@ -736,49 +736,45 @@
736 }736 }
737 return cluster, mac_addresses, leases737 return cluster, mac_addresses, leases
738738
739 def make_cluster_with_mac_and_node_and_ip(self, use_static_range=False):
740 cluster = factory.make_NodeGroup()
741 mac_address = factory.make_MACAddress_with_Node()
742 interface = factory.make_NodeGroupInterface(nodegroup=cluster)
743 ip = get_random_ip_from_interface_range(interface, use_static_range)
744 return cluster, interface, mac_address, ip
745
739 def test_updates_mac_cluster_interfaces(self):746 def test_updates_mac_cluster_interfaces(self):
740 cluster, mac_addresses, leases = (747 cluster, interface, mac_address, ip = (
741 self.make_cluster_with_macs_and_leases())748 self.make_cluster_with_mac_and_node_and_ip())
742 update_mac_cluster_interfaces(leases, cluster)749 update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
743 results = {750 mac_address = reload_object(mac_address)
744 mac_address: mac_address.cluster_interface751 self.assertEqual(interface, mac_address.cluster_interface)
745 for mac_address in MACAddress.objects.filter(
746 mac_address__in=leases.values())
747 }
748 self.assertEqual(mac_addresses, results)
749752
750 def test_considers_static_range_when_updating_interfaces(self):753 def test_considers_static_range_when_updating_interfaces(self):
751 cluster, mac_addresses, leases = (754 cluster, mac_addresses, leases = (
752 self.make_cluster_with_macs_and_leases(use_static_range=True))755 self.make_cluster_with_macs_and_leases(use_static_range=True))
753 update_mac_cluster_interfaces(leases, cluster)756 cluster, interface, mac_address, ip = (
754 results = {757 self.make_cluster_with_mac_and_node_and_ip(use_static_range=True))
755 mac_address: mac_address.cluster_interface758 update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
756 for mac_address in MACAddress.objects.filter(759 mac_address = reload_object(mac_address)
757 mac_address__in=leases.values())760 self.assertEqual(interface, mac_address.cluster_interface)
758 }
759 self.assertEqual(mac_addresses, results)
760761
761 def test_updates_network_relations(self):762 def test_updates_network_relations(self):
762 # update_mac_cluster_interfaces should also associate the mac763 # update_mac_cluster_interfaces should also associate the mac
763 # with the network on which it resides.764 # with the network on which it resides.
764 cluster, mac_addresses, leases = (765 cluster, mac_addresses, leases = (
765 self.make_cluster_with_macs_and_leases())766 self.make_cluster_with_macs_and_leases())
766 expected_relations = []767 cluster, interface, mac_address, ip = (
767 for mac, interface in mac_addresses.viewitems():768 self.make_cluster_with_mac_and_node_and_ip())
768 net = create_Network_from_NodeGroupInterface(interface)769 net = create_Network_from_NodeGroupInterface(interface)
769 expected_relations.append((net, mac))770 update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
770 update_mac_cluster_interfaces(leases, cluster)771 [observed_macddress] = net.macaddress_set.all()
771 # Doing a single giant comparison here would be unintuitive and772 self.expectThat(mac_address, Equals(observed_macddress))
772 # fugly, so I'm iterating.773 self.expectThat(
773 for net, mac in expected_relations:774 net, MatchesStructure.byEquality(
774 [observed_macddress] = net.macaddress_set.all()775 default_gateway=interface.router_ip,
775 self.expectThat(mac, Equals(observed_macddress))776 netmask=interface.subnet_mask,
776 interface = mac_addresses[mac]777 ))
777 self.expectThat(
778 net, MatchesStructure.byEquality(
779 default_gateway=interface.router_ip,
780 netmask=interface.subnet_mask,
781 ))
782778
783 def test_does_not_overwrite_network_with_same_name(self):779 def test_does_not_overwrite_network_with_same_name(self):
784 cluster = factory.make_NodeGroup()780 cluster = factory.make_NodeGroup()
@@ -794,10 +790,8 @@
794 def test_ignores_mac_not_attached_to_cluster(self):790 def test_ignores_mac_not_attached_to_cluster(self):
795 cluster = factory.make_NodeGroup()791 cluster = factory.make_NodeGroup()
796 mac_address = factory.make_MACAddress_with_Node()792 mac_address = factory.make_MACAddress_with_Node()
797 leases = {793 ip = factory.make_ipv4_address()
798 factory.make_ipv4_address(): mac_address.mac_address794 update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
799 }
800 update_mac_cluster_interfaces(leases, cluster)
801 mac_address = MACAddress.objects.get(795 mac_address = MACAddress.objects.get(
802 id=mac_address.id)796 id=mac_address.id)
803 self.assertIsNone(mac_address.cluster_interface)797 self.assertIsNone(mac_address.cluster_interface)
@@ -805,13 +799,11 @@
805 def test_ignores_unknown_macs(self):799 def test_ignores_unknown_macs(self):
806 cluster = factory.make_NodeGroup()800 cluster = factory.make_NodeGroup()
807 mac_address = factory.make_mac_address()801 mac_address = factory.make_mac_address()
808 leases = {802 ip = factory.make_ipv4_address()
809 factory.make_ipv4_address(): mac_address
810 }
811 # This is a test to show that update_mac_cluster_interfaces()803 # This is a test to show that update_mac_cluster_interfaces()
812 # doesn't raise an Http404 when it comes across something it804 # doesn't raise an Http404 when it comes across something it
813 # doesn't know, hence the lack of meaningful assertions.805 # doesn't know, hence the lack of meaningful assertions.
814 update_mac_cluster_interfaces(leases, cluster)806 update_mac_cluster_interfaces(ip, mac_address, cluster)
815 self.assertFalse(807 self.assertFalse(
816 MACAddress.objects.filter(mac_address=mac_address).exists())808 MACAddress.objects.filter(mac_address=mac_address).exists())
817809
@@ -824,10 +816,9 @@
824 ip=factory.make_ipv4_address(),816 ip=factory.make_ipv4_address(),
825 management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)817 management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
826 mac_address = factory.make_mac_address()818 mac_address = factory.make_mac_address()
827 leases = {819 ip = factory.make_ipv4_address()
828 factory.make_ipv4_address(): mac_address820 self.assertIsNone(
829 }821 update_mac_cluster_interfaces(ip, mac_address, cluster))
830 self.assertIsNone(update_mac_cluster_interfaces(leases, cluster))
831 # The real test is that update_mac_cluster_interfaces() doesn't822 # The real test is that update_mac_cluster_interfaces() doesn't
832 # stacktrace because of the unconfigured interface (see bug823 # stacktrace because of the unconfigured interface (see bug
833 # 1332596).824 # 1332596).
834825
=== modified file 'src/maasserver/rpc/leases.py'
--- src/maasserver/rpc/leases.py 2014-10-01 23:07:50 +0000
+++ src/maasserver/rpc/leases.py 2014-12-08 02:56:05 +0000
@@ -17,7 +17,6 @@
17]17]
1818
19from maasserver.models.dhcplease import DHCPLease19from maasserver.models.dhcplease import DHCPLease
20from maasserver.models.macaddress import update_mac_cluster_interfaces
21from maasserver.models.nodegroup import NodeGroup20from maasserver.models.nodegroup import NodeGroup
22from maasserver.utils.async import transactional21from maasserver.utils.async import transactional
23from provisioningserver.pserv_services.lease_upload_service import (22from provisioningserver.pserv_services.lease_upload_service import (
@@ -50,5 +49,4 @@
50 else:49 else:
51 leases = convert_mappings_to_leases(mappings)50 leases = convert_mappings_to_leases(mappings)
52 DHCPLease.objects.update_leases(nodegroup, leases)51 DHCPLease.objects.update_leases(nodegroup, leases)
53 update_mac_cluster_interfaces(leases, nodegroup)
54 return {}52 return {}
5553
=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
--- src/maasserver/rpc/tests/test_regionservice.py 2014-11-25 13:06:34 +0000
+++ src/maasserver/rpc/tests/test_regionservice.py 2014-12-08 02:56:05 +0000
@@ -420,31 +420,6 @@
420 self.expectThat(mac, Equals(mapping["mac"]))420 self.expectThat(mac, Equals(mapping["mac"]))
421421
422 @wait_for_reactor422 @wait_for_reactor
423 @inlineCallbacks
424 def test__updates_mac_to_cluster_links(self):
425 uuid = factory.make_name("uuid")
426 nodegroup = yield deferToThread(self.make_node_group, uuid)
427 cluster_interface = yield deferToThread(
428 self.make_node_group_interface, nodegroup)
429 mac_address = yield deferToThread(self.make_mac_address)
430
431 mapping = {
432 "ip": cluster_interface.ip_range_low,
433 "mac": mac_address.mac_address.get_raw(),
434 }
435
436 response = yield call_responder(Region(), UpdateLeases, {
437 b"uuid": uuid, b"mappings": [mapping]})
438 self.assertThat(response, Equals({}))
439
440 @transactional
441 def get_cluster_interface():
442 return reload_object(mac_address).cluster_interface
443
444 observed = yield deferToThread(get_cluster_interface)
445 self.assertThat(observed, Equals(cluster_interface))
446
447 @wait_for_reactor
448 def test__raises_NoSuchCluster_if_cluster_not_found(self):423 def test__raises_NoSuchCluster_if_cluster_not_found(self):
449 uuid = factory.make_name("uuid")424 uuid = factory.make_name("uuid")
450 d = call_responder(425 d = call_responder(