Merge ~mpontillo/maas:default-dns-ip--bug-1776604 into maas:master

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: 28700058f82627a613221c1187a2d5154c9b67ca
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~mpontillo/maas:default-dns-ip--bug-1776604
Merge into: maas:master
Diff against target: 212 lines (+132/-17)
3 files modified
src/maasserver/models/interface.py (+2/-0)
src/maasserver/models/staticipaddress.py (+30/-17)
src/maasserver/models/tests/test_staticipaddress.py (+100/-0)
Reviewer Review Type Date Requested Status
MAAS Lander Approve
Blake Rouse (community) Approve
Newell Jensen (community) Abstain
MAAS Maintainers risk-reduction Pending
Review via email: mp+349384@code.launchpad.net

Commit message

LP: #1776604 - Improve hostname mapping heuristic for bonded bridges

This commit changes the default hostname to IP address mapping to
account for situations where an interface parent's parent is a
machine's boot interface.

To post a comment you must log in.
Revision history for this message
Newell Jensen (newell-jensen) wrote :

After talking with Mike some about this branch and the complexity, it is hard to tell what exactly is happening here and whether or not this will actually fix the bug without regressions. Mike voiced these reservations as well. I think a good thing to do might be to add more unit tests if possible to squelch, if possible, any potential regression concerns. Going to abstain from approving this although the Python code does look good.

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

UNIT TESTS
-b default-dns-ip--bug-1776604 lp:~mpontillo/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 9835e6fb28821913e4ddcb86214a81d1a1a01547

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Let me explain a little more about what is going on here.

The get_hostname_ip_mapping() function basically takes in a subnet or domain, and returns a dictionary of {hostname: (ttl, [ip])} mappings.

Most of the function is a self-described "SQL horror". The reason for that is, we want to be able to do this in a single query for performance reasons. (Doing this in Django would be even worse.)

Previously, the SQL query itself tried to ensure that only one mapping would be returned per (fqdn, address_family) pair. It did this by using a SELECT DISTINCT to try to determine whether or not the interface was the boot interface. But this wasn't enough, because we have to account for a few different scenarios:

 - There might be many boot interfaces per machine, depending on the interface hierarchy. For example, if we have a physical interface 'eth0' and we boot from it, that's great, but what if the user also configured an 'eth1' and bonded {eth0, eth1} together? We won't see a proper IP address assigned to eth0 or eth1 in that case, so we need to dig deeper.

 - In addition to the above point, you might have a bridge interface whose parent is the bond (where your IP addresses are actually assigned). That means you have to check if the parent's parent is a boot interface before determining if it should get the default hostname for the machine. So the SQL had to be changed to account for the parent-of-a-parent-being-a-boot-interface scenario.

 - If we don't include the preference in the SELECT DISTINCT, we can't include it in the ORDER BY statement, otherwise the following error is raised:

    django.db.utils.ProgrammingError: SELECT DISTINCT ON expressions must match initial ORDER BY expressions

   ... therefore, the SELECT DISTINCT must include the preference, and we must filter out the rows we don't want in the Python code. (That's what the Python portion of this change does.)

bad432e... by Mike Pontillo

Sort is_boot before preference, just in case preference returns an incorrect result.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Also: we can /almost/ just remove the `preference` field, except we have to handle the case where there is no known boot interface.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

One last comment. I think it's difficult to add more tests to this file, because effectively this function is a heuristic. It tries to guess the best interface to assign the node's primary hostname to. In cases where it guesses wrong, it's possible that customers have come to rely on the previous behavior. For example, it looks like we may be missing cases where there the PXE interface is configured with a triple-parent, such as:

physical (eth0) -> bond ({eth0, eth1} via bond0) -> vlan (bond0.11) -> bridge (br-vlan11)

In this case it's possible that a customer might be relying on MAAS to return an IP address assigned to bond0, when in fact we /could/ have a good argument that we should change the logic to return an IP address assigned to br-vlan11.

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

UNIT TESTS
-b default-dns-ip--bug-1776604 lp:~mpontillo/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: bad432ebc000900f66d1cfe80661d63142a37089

review: Approve
Revision history for this message
Blake Rouse (blake-rouse) wrote :

This SQL makes me want to vomit. With all the original unit tests passing and the new unit test passing, I say it works as expected. At least how we believe it should be working. ;-)

review: Approve
19310d3... by Mike Pontillo

Remove Python portion of the code (it was unnecessary).

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

UNIT TESTS
-b default-dns-ip--bug-1776604 lp:~mpontillo/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 19310d33b988ecaba665461d438c637b19cca516

review: Approve
2870005... by Mike Pontillo

Further simplify SQL query. Add additional logging and tests.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

I was able to simplify the code a little, removing the extra Python bits.

I also added a log statement for when IP addresses are automatically removed, added another test case, and added some extra IPs within the tests to make sure the correct addresses are always returned for a a node's default hostname.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good.

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

UNIT TESTS
-b default-dns-ip--bug-1776604 lp:~mpontillo/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 28700058f82627a613221c1187a2d5154c9b67ca

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/maasserver/models/interface.py b/src/maasserver/models/interface.py
index 4624e8a..5839e73 100644
--- a/src/maasserver/models/interface.py
+++ b/src/maasserver/models/interface.py
@@ -1128,6 +1128,8 @@ class Interface(CleanSave, TimestampedModel):
1128 """Remove all the `IPAddress` link on the interface."""1128 """Remove all the `IPAddress` link on the interface."""
1129 for ip_address in self.ip_addresses.exclude(1129 for ip_address in self.ip_addresses.exclude(
1130 alloc_type=IPADDRESS_TYPE.DISCOVERED):1130 alloc_type=IPADDRESS_TYPE.DISCOVERED):
1131 maaslog.info("%s: IP address automatically unlinked: %s" % (
1132 self.get_log_string(), ip_address))
1131 self.unlink_ip_address(ip_address, clearing_config=clearing_config)1133 self.unlink_ip_address(ip_address, clearing_config=clearing_config)
11321134
1133 def claim_auto_ips(self, exclude_addresses=[]):1135 def claim_auto_ips(self, exclude_addresses=[]):
diff --git a/src/maasserver/models/staticipaddress.py b/src/maasserver/models/staticipaddress.py
index bfc9d61..68be5c5 100644
--- a/src/maasserver/models/staticipaddress.py
+++ b/src/maasserver/models/staticipaddress.py
@@ -75,6 +75,8 @@ _special_mapping_result = _mapping_base_fields + (
7575
76_mapping_query_result = _mapping_base_fields + (76_mapping_query_result = _mapping_base_fields + (
77 'is_boot',77 'is_boot',
78 'preference',
79 'family',
78)80)
7981
80_interface_mapping_result = _mapping_base_fields + (82_interface_mapping_result = _mapping_base_fields + (
@@ -504,7 +506,7 @@ class StaticIPAddressManager(Manager):
504 domain.ttl,506 domain.ttl,
505 %s)""" % default_ttl507 %s)""" % default_ttl
506 sql_query = """508 sql_query = """
507 SELECT DISTINCT ON (node.hostname, is_boot, family(staticip.ip))509 SELECT DISTINCT ON (fqdn, is_boot, family)
508 CONCAT(node.hostname, '.', domain.name) AS fqdn,510 CONCAT(node.hostname, '.', domain.name) AS fqdn,
509 node.system_id,511 node.system_id,
510 node.node_type,512 node.node_type,
@@ -515,16 +517,38 @@ class StaticIPAddressManager(Manager):
515 node.boot_interface_id IS NOT NULL AND517 node.boot_interface_id IS NOT NULL AND
516 (518 (
517 node.boot_interface_id = interface.id OR519 node.boot_interface_id = interface.id OR
518 node.boot_interface_id = parent.id520 node.boot_interface_id = parent.id OR
521 node.boot_interface_id = parent_parent.id
519 ),522 ),
520 False523 False
521 ) AS is_boot524 ) AS is_boot,
525 CASE
526 WHEN interface.type = 'bridge' AND
527 parent_parent.id = node.boot_interface_id THEN 1
528 WHEN interface.type = 'bridge' AND
529 parent.id = node.boot_interface_id THEN 2
530 WHEN interface.type = 'bond' AND
531 parent.id = node.boot_interface_id THEN 3
532 WHEN interface.type = 'physical' AND
533 interface.id = node.boot_interface_id THEN 4
534 WHEN interface.type = 'bond' THEN 5
535 WHEN interface.type = 'physical' THEN 6
536 WHEN interface.type = 'vlan' THEN 7
537 WHEN interface.type = 'alias' THEN 8
538 WHEN interface.type = 'unknown' THEN 9
539 ELSE 10
540 END AS preference,
541 family(staticip.ip) AS family
522 FROM542 FROM
523 maasserver_interface AS interface543 maasserver_interface AS interface
524 LEFT OUTER JOIN maasserver_interfacerelationship AS rel ON544 LEFT OUTER JOIN maasserver_interfacerelationship AS rel ON
525 interface.id = rel.child_id545 interface.id = rel.child_id
526 LEFT OUTER JOIN maasserver_interface AS parent ON546 LEFT OUTER JOIN maasserver_interface AS parent ON
527 rel.parent_id = parent.id547 rel.parent_id = parent.id
548 LEFT OUTER JOIN maasserver_interfacerelationship AS parent_rel ON
549 parent.id = parent_rel.child_id
550 LEFT OUTER JOIN maasserver_interface AS parent_parent ON
551 parent_rel.parent_id = parent_parent.id
528 JOIN maasserver_node AS node ON552 JOIN maasserver_node AS node ON
529 node.id = interface.node_id553 node.id = interface.node_id
530 JOIN maasserver_domain AS domain ON554 JOIN maasserver_domain AS domain ON
@@ -562,21 +586,10 @@ class StaticIPAddressManager(Manager):
562 staticip.ip IS NOT NULL AND586 staticip.ip IS NOT NULL AND
563 host(staticip.ip) != ''587 host(staticip.ip) != ''
564 ORDER BY588 ORDER BY
565 node.hostname,589 fqdn,
566 is_boot DESC,590 is_boot DESC,
567 family(staticip.ip),591 family,
568 CASE592 preference,
569 WHEN interface.type = 'bond' AND
570 parent.id = node.boot_interface_id THEN 1
571 WHEN interface.type = 'physical' AND
572 interface.id = node.boot_interface_id THEN 2
573 WHEN interface.type = 'bond' THEN 3
574 WHEN interface.type = 'physical' THEN 4
575 WHEN interface.type = 'vlan' THEN 5
576 WHEN interface.type = 'alias' THEN 6
577 WHEN interface.type = 'unknown' THEN 7
578 ELSE 8
579 END,
580 /*593 /*
581 * We want STICKY and USER_RESERVED addresses to be preferred,594 * We want STICKY and USER_RESERVED addresses to be preferred,
582 * followed by AUTO, DHCP, and finally DISCOVERED.595 * followed by AUTO, DHCP, and finally DISCOVERED.
diff --git a/src/maasserver/models/tests/test_staticipaddress.py b/src/maasserver/models/tests/test_staticipaddress.py
index 7b72861..7e1f945 100644
--- a/src/maasserver/models/tests/test_staticipaddress.py
+++ b/src/maasserver/models/tests/test_staticipaddress.py
@@ -837,6 +837,106 @@ class TestStaticIPAddressManagerMapping(MAASServerTestCase):
837 node.system_id, 30, {vlanip.ip}, node.node_type)}837 node.system_id, 30, {vlanip.ip}, node.node_type)}
838 self.assertEqual(expected_mapping, mapping)838 self.assertEqual(expected_mapping, mapping)
839839
840 def test_get_hostname_ip_mapping_prefers_bridged_bond_pxe_interface(self):
841 subnet = factory.make_Subnet(
842 cidr='10.0.0.0/24', dns_servers=[], gateway_ip='10.0.0.1')
843 node = factory.make_Node_with_Interface_on_Subnet(
844 hostname='host', subnet=subnet)
845 eth0 = node.get_boot_interface()
846 eth0.name = 'eth0'
847 eth0.save()
848 eth1 = factory.make_Interface(
849 INTERFACE_TYPE.PHYSICAL, node=node, name='eth1', vlan=subnet.vlan)
850 eth2 = factory.make_Interface(
851 INTERFACE_TYPE.PHYSICAL, node=node, name='eth2', vlan=subnet.vlan)
852 node.boot_interface = eth1
853 node.save()
854 bond0 = factory.make_Interface(
855 INTERFACE_TYPE.BOND, node=node, parents=[eth1, eth2], name='bond0')
856 br_bond0 = factory.make_Interface(
857 INTERFACE_TYPE.BRIDGE, parents=[bond0], name='br-bond0')
858 phy_staticip = factory.make_StaticIPAddress(
859 alloc_type=IPADDRESS_TYPE.STICKY, interface=eth0,
860 subnet=subnet, ip='10.0.0.2')
861 bridge_ip = factory.make_StaticIPAddress(
862 alloc_type=IPADDRESS_TYPE.STICKY, interface=br_bond0,
863 subnet=subnet, ip='10.0.0.3')
864 # Make an IP addresses that shouldn't be preferred, but is also on
865 # an interface whose parent is a boot interface. (Some caution is
866 # advised when doing this, since we have interface signals that will
867 # automatically remove these addresses in some cases.)
868 bond0_extra_ip = factory.make_StaticIPAddress(
869 alloc_type=IPADDRESS_TYPE.STICKY, interface=bond0,
870 subnet=subnet, ip='10.0.0.4')
871 mapping = StaticIPAddress.objects.get_hostname_ip_mapping(
872 node.domain)
873 expected_mapping = {
874 node.fqdn: HostnameIPMapping(
875 node.system_id, 30, {bridge_ip.ip}, node.node_type),
876 "%s.%s" % (eth0.name, node.fqdn): HostnameIPMapping(
877 node.system_id, 30, {phy_staticip.ip}, node.node_type),
878 "%s.%s" % (bond0.name, node.fqdn): HostnameIPMapping(
879 node.system_id, 30, {bond0_extra_ip.ip}, node.node_type)
880 }
881 self.assertThat(mapping, Equals(expected_mapping))
882
883 def test_get_hostname_ip_mapping_with_v4_and_v6_and_bridged_bonds(self):
884 subnet_v4 = factory.make_Subnet(
885 cidr=str(factory.make_ipv4_network().cidr))
886 subnet_v6 = factory.make_Subnet(
887 cidr='2001:db8::/64')
888 node = factory.make_Node_with_Interface_on_Subnet(
889 hostname='host', subnet=subnet_v4)
890 eth0 = node.get_boot_interface()
891 eth0.name = 'eth0'
892 eth0.save()
893 eth1 = factory.make_Interface(
894 INTERFACE_TYPE.PHYSICAL, node=node, name='eth1')
895 eth2 = factory.make_Interface(
896 INTERFACE_TYPE.PHYSICAL, node=node, name='eth2')
897 node.boot_interface = eth1
898 node.save()
899 bond0 = factory.make_Interface(
900 INTERFACE_TYPE.BOND, node=node, parents=[eth1, eth2], name='bond0')
901 br_bond0 = factory.make_Interface(
902 INTERFACE_TYPE.BRIDGE, parents=[bond0], name='br-bond0')
903 phy_staticip_v4 = factory.make_StaticIPAddress(
904 alloc_type=IPADDRESS_TYPE.STICKY, interface=eth0,
905 subnet=subnet_v4)
906 bridge_ip_v4 = factory.make_StaticIPAddress(
907 alloc_type=IPADDRESS_TYPE.STICKY, interface=br_bond0,
908 subnet=subnet_v4)
909 phy_staticip_v6 = factory.make_StaticIPAddress(
910 alloc_type=IPADDRESS_TYPE.STICKY, interface=eth0,
911 subnet=subnet_v6)
912 bridge_ip_v6 = factory.make_StaticIPAddress(
913 alloc_type=IPADDRESS_TYPE.STICKY, interface=br_bond0,
914 subnet=subnet_v6)
915 # Make a few IP addresses that shouldn't be associated with the
916 # default hosname. (They'll show up as associated with bond0, however.)
917 # Technically MAAS should remove these since they belong to a parent
918 # interface, but in this case the interface won't be re-saved.
919 extra_ipv4 = factory.make_StaticIPAddress(
920 alloc_type=IPADDRESS_TYPE.STICKY, interface=bond0,
921 subnet=subnet_v4)
922 extra_ipv6 = factory.make_StaticIPAddress(
923 alloc_type=IPADDRESS_TYPE.STICKY, interface=bond0,
924 subnet=subnet_v6)
925 mapping = StaticIPAddress.objects.get_hostname_ip_mapping(
926 node.domain)
927 expected_mapping = {
928 node.fqdn: HostnameIPMapping(
929 node.system_id, 30, {bridge_ip_v4.ip, bridge_ip_v6.ip},
930 node.node_type),
931 "%s.%s" % (eth0.name, node.fqdn): HostnameIPMapping(
932 node.system_id, 30, {phy_staticip_v4.ip, phy_staticip_v6.ip},
933 node.node_type),
934 "%s.%s" % (bond0.name, node.fqdn): HostnameIPMapping(
935 node.system_id, 30, {extra_ipv4.ip, extra_ipv6.ip},
936 node.node_type)
937 }
938 self.assertThat(mapping, Equals(expected_mapping))
939
840 def test_get_hostname_ip_mapping_returns_domain_head_ips(self):940 def test_get_hostname_ip_mapping_returns_domain_head_ips(self):
841 parent = factory.make_Domain()941 parent = factory.make_Domain()
842 name = factory.make_name()942 name = factory.make_name()

Subscribers

People subscribed via source and target branches