Merge ~mpontillo/maas:workaround-netaddr-slash128-bug--2.4 into maas:2.4

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: 9738651ab460b6e6eb2d53216dc4074554fa3397
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~mpontillo/maas:workaround-netaddr-slash128-bug--2.4
Merge into: maas:2.4
Diff against target: 92 lines (+56/-8)
2 files modified
src/provisioningserver/utils/network.py (+29/-8)
src/provisioningserver/utils/tests/test_network.py (+27/-0)
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+354010@code.launchpad.net

Commit message

Backport 80dcada - LP: #1789721 - Workaround for StopIteration seen via get_source_address().

This works around a bug in netaddr that causes StopIteration to be raised when a /128 IPv6 network is passed in.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Self-approve backport.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/provisioningserver/utils/network.py b/src/provisioningserver/utils/network.py
2index 6093871..06ed74e 100644
3--- a/src/provisioningserver/utils/network.py
4+++ b/src/provisioningserver/utils/network.py
5@@ -1421,20 +1421,41 @@ def coerce_to_valid_hostname(hostname):
6 return hostname
7
8
9-def get_source_address(destination_ip: IPAddressOrNetwork):
10- """Returns the local source address for the specified destination IP.
11+def get_source_address(destination: IPAddressOrNetwork):
12+ """Returns the local source address for the specified destination.
13
14- :param destination_ip: Can be an IP address in string format, an IPNetwork,
15+ :param destination: Can be an IP address in string format, an IPNetwork,
16 or an IPAddress object.
17 :return: the string representation of the local IP address that would be
18 used for communication with the specified destination.
19 """
20- if isinstance(destination_ip, IPNetwork):
21- destination_ip = IPAddress(next(destination_ip.iter_hosts()))
22+ if isinstance(destination, IPNetwork):
23+ if destination.prefixlen == 128:
24+ # LP: #1789721 - netaddr raises an exception when using
25+ # iter_hosts() with a /128 address.
26+ destination = destination.ip
27+ else:
28+ # Make sure to return a host (not a network) if possible.
29+ # Using iter_hosts() here is a general way to accomplish that.
30+ destination = IPAddress(next(destination.iter_hosts()))
31 else:
32- destination_ip = make_ipaddress(destination_ip)
33- if destination_ip.is_ipv4_mapped():
34- destination_ip = destination_ip.ipv4()
35+ destination = make_ipaddress(destination)
36+ if destination.is_ipv4_mapped():
37+ destination = destination.ipv4()
38+ return get_source_address_for_ipaddress(destination)
39+
40+
41+def get_source_address_for_ipaddress(destination_ip: IPAddress):
42+ """Returns the local source address for the specified IPAddress.
43+
44+ Callers should generally use the `get_source_address()` utility instead;
45+ it is more flexible about the type of the input parameter.
46+
47+ :param destination_ip: Can be an IP address in string format, an IPNetwork,
48+ or an IPAddress object.
49+ :return: the string representation of the local IP address that would be
50+ used for communication with the specified destination.
51+ """
52 af = AF_INET if destination_ip.version == 4 else AF_INET6
53 with socket.socket(af, socket.SOCK_DGRAM) as sock:
54 peername = str(destination_ip)
55diff --git a/src/provisioningserver/utils/tests/test_network.py b/src/provisioningserver/utils/tests/test_network.py
56index f23f31b..b7472ef 100644
57--- a/src/provisioningserver/utils/tests/test_network.py
58+++ b/src/provisioningserver/utils/tests/test_network.py
59@@ -2294,6 +2294,33 @@ class TestGetSourceAddress(MAASTestCase):
60 self.assertThat(
61 get_source_address(IPNetwork("127.0.0.1/8")), Equals("127.0.0.1"))
62
63+ def test__ipnetwork_works_for_ipv4_slash32(self):
64+ mock = self.patch(network_module, 'get_source_address_for_ipaddress')
65+ mock.return_value = "10.0.0.1"
66+ self.assertThat(
67+ get_source_address(IPNetwork("10.0.0.1/32")), Equals("10.0.0.1"))
68+
69+ def test__ipnetwork_works_for_ipv6_slash128(self):
70+ mock = self.patch(network_module, 'get_source_address_for_ipaddress')
71+ mock.return_value = "2001:67c:1560::1"
72+ self.assertThat(
73+ get_source_address(
74+ IPNetwork("2001:67c:1560::/128")), Equals("2001:67c:1560::1"))
75+
76+ def test__ipnetwork_works_for_ipv6_slash48(self):
77+ mock = self.patch(network_module, 'get_source_address_for_ipaddress')
78+ mock.return_value = "2001:67c:1560::1"
79+ self.assertThat(
80+ get_source_address(
81+ IPNetwork("2001:67c:1560::/48")), Equals("2001:67c:1560::1"))
82+
83+ def test__ipnetwork_works_for_ipv6_slash64(self):
84+ mock = self.patch(network_module, 'get_source_address_for_ipaddress')
85+ mock.return_value = "2001:67c:1560::1"
86+ self.assertThat(
87+ get_source_address(
88+ IPNetwork("2001:67c:1560::1/64")), Equals("2001:67c:1560::1"))
89+
90 def test__accepts_ipaddress(self):
91 self.assertThat(
92 get_source_address(IPAddress("127.0.0.1")), Equals("127.0.0.1"))

Subscribers

People subscribed via source and target branches