Merge ~blake-rouse/maas:fix-1707971-2.2 into maas:2.2

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: d31752199339adf737469d324f3ed8020e7aaf89
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~blake-rouse/maas:fix-1707971-2.2
Merge into: maas:2.2
Diff against target: 337 lines (+182/-16)
4 files modified
src/maasserver/rpc/regionservice.py (+10/-9)
src/maasserver/rpc/tests/test_regionservice.py (+28/-7)
src/provisioningserver/utils/network.py (+68/-0)
src/provisioningserver/utils/tests/test_network.py (+76/-0)
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+329063@code.launchpad.net

Commit message

Backport c2aed3017ef73b5af23c72d40c9c0c0fc1cf475f and 6ffe84b98701cff8ead64d082b807642da83f326 from master.

LP: #1707971 - Only expose the source address for each subnet on a region controller.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Self-approving backport.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/rpc/regionservice.py b/src/maasserver/rpc/regionservice.py
2index 4d962e8..3ddf50c 100644
3--- a/src/maasserver/rpc/regionservice.py
4+++ b/src/maasserver/rpc/regionservice.py
5@@ -81,6 +81,7 @@ from provisioningserver.security import calculate_digest
6 from provisioningserver.utils.events import EventGroup
7 from provisioningserver.utils.network import (
8 get_all_interface_addresses,
9+ get_all_interface_source_addresses,
10 resolves_to_loopback_address,
11 )
12 from provisioningserver.utils.ps import is_pid_running
13@@ -1171,7 +1172,14 @@ class RegionAdvertisingService(service.Service):
14 if port is None:
15 return set() # Not serving yet.
16 else:
17- addresses = set()
18+ addresses = get_all_interface_source_addresses()
19+ if len(addresses) > 0:
20+ return set(
21+ (addr, port)
22+ for addr in addresses
23+ )
24+ # There are no non-loopback addresses, so return loopback
25+ # address as a fallback.
26 loopback_addresses = set()
27 for addr in get_all_interface_addresses():
28 ipaddr = IPAddress(addr)
29@@ -1179,14 +1187,7 @@ class RegionAdvertisingService(service.Service):
30 continue # Don't advertise link-local addresses.
31 if ipaddr.is_loopback():
32 loopback_addresses.add((addr, port))
33- else:
34- addresses.add((addr, port))
35- if len(addresses) > 0:
36- return addresses
37- else:
38- # There are no non-loopback addresses, so return loopback
39- # address as a fallback.
40- return loopback_addresses
41+ return loopback_addresses
42
43
44 class RegionAdvertising:
45diff --git a/src/maasserver/rpc/tests/test_regionservice.py b/src/maasserver/rpc/tests/test_regionservice.py
46index 972a3d7..0a8016a 100644
47--- a/src/maasserver/rpc/tests/test_regionservice.py
48+++ b/src/maasserver/rpc/tests/test_regionservice.py
49@@ -65,6 +65,7 @@ from maastesting.matchers import (
50 MockAnyCall,
51 MockCalledOnceWith,
52 MockCallsMatch,
53+ MockNotCalled,
54 Provides,
55 )
56 from maastesting.runtest import MAASCrochetRunTest
57@@ -1269,10 +1270,13 @@ class TestRegionAdvertisingService(MAASTransactionServerTestCase):
58 getPort = getServiceNamed.return_value.getPort
59 getPort.return_value = port
60
61- def patch_addresses(self, addresses):
62+ def patch_addresses(self, source_addresses, old_addresses):
63+ get_all_interface_source_addresses = self.patch(
64+ regionservice, "get_all_interface_source_addresses")
65+ get_all_interface_source_addresses.return_value = source_addresses
66 get_all_interface_addresses = self.patch(
67 regionservice, "get_all_interface_addresses")
68- get_all_interface_addresses.return_value = addresses
69+ get_all_interface_addresses.return_value = old_addresses
70
71 @wait_for_reactor
72 @inlineCallbacks
73@@ -1286,7 +1290,7 @@ class TestRegionAdvertisingService(MAASTransactionServerTestCase):
74 dump = yield deferToDatabase(RegionAdvertising.dump)
75 self.assertItemsEqual([], dump)
76
77- def test__getAddresses_excluding_loopback(self):
78+ def test__getAddresses_uses_source_addresses(self):
79 service = RegionAdvertisingService()
80
81 example_port = factory.pick_port()
82@@ -1311,11 +1315,10 @@ class TestRegionAdvertisingService(MAASTransactionServerTestCase):
83 str(netaddr.ip.IPV6_LOOPBACK),
84 }
85 self.patch_addresses(
86- example_ipv4_addrs | example_ipv6_addrs |
87+ example_ipv4_addrs | example_ipv6_addrs,
88 example_link_local_addrs | example_loopback_addrs)
89
90- # IPv6 addresses, link-local addresses and loopback are excluded, and
91- # thus not advertised.
92+ # Only the source addresses are returned.
93 self.assertItemsEqual(
94 [(addr, example_port)
95 for addr in example_ipv4_addrs.union(example_ipv6_addrs)],
96@@ -1325,8 +1328,11 @@ class TestRegionAdvertisingService(MAASTransactionServerTestCase):
97 eventloop.services.getServiceNamed,
98 MockCalledOnceWith("rpc"))
99 self.assertThat(
100- regionservice.get_all_interface_addresses,
101+ regionservice.get_all_interface_source_addresses,
102 MockCalledOnceWith())
103+ self.assertThat(
104+ regionservice.get_all_interface_addresses,
105+ MockNotCalled())
106
107 def test__getAddresses_including_loopback(self):
108 service = RegionAdvertisingService()
109@@ -1334,6 +1340,16 @@ class TestRegionAdvertisingService(MAASTransactionServerTestCase):
110 example_port = factory.pick_port()
111 self.patch_port(example_port)
112
113+ example_ipv4_addrs = set()
114+ for _ in range(5):
115+ ip = factory.make_ipv4_address()
116+ if not netaddr.IPAddress(ip).is_loopback():
117+ example_ipv4_addrs.add(ip)
118+ example_ipv6_addrs = set()
119+ for _ in range(5):
120+ ip = factory.make_ipv6_address()
121+ if not netaddr.IPAddress(ip).is_loopback():
122+ example_ipv6_addrs.add(ip)
123 example_link_local_addrs = {
124 factory.pick_ip_in_network(netaddr.ip.IPV4_LINK_LOCAL),
125 factory.pick_ip_in_network(netaddr.ip.IPV6_LINK_LOCAL),
126@@ -1344,6 +1360,8 @@ class TestRegionAdvertisingService(MAASTransactionServerTestCase):
127 str(netaddr.ip.IPV6_LOOPBACK),
128 }
129 self.patch_addresses(
130+ set(),
131+ example_ipv4_addrs | example_ipv6_addrs |
132 example_link_local_addrs | example_loopback_addrs)
133
134 # Only IPv4 loopback is exposed.
135@@ -1355,6 +1373,9 @@ class TestRegionAdvertisingService(MAASTransactionServerTestCase):
136 eventloop.services.getServiceNamed,
137 MockCalledOnceWith("rpc"))
138 self.assertThat(
139+ regionservice.get_all_interface_source_addresses,
140+ MockCalledOnceWith())
141+ self.assertThat(
142 regionservice.get_all_interface_addresses,
143 MockCalledOnceWith())
144
145diff --git a/src/provisioningserver/utils/network.py b/src/provisioningserver/utils/network.py
146index 264921c..baf7148 100644
147--- a/src/provisioningserver/utils/network.py
148+++ b/src/provisioningserver/utils/network.py
149@@ -94,6 +94,9 @@ OuterRange = TypeVar('OuterRange', IPRange, IPNetwork, bytes, str)
150 # were passed into the `netaddr.IPAddress` constructor.
151 MaybeIPAddress = TypeVar('MaybeIPAddress', IPAddress, bytes, str, int)
152
153+IPAddressOrNetwork = TypeVar(
154+ 'IPAddressOrNetwork', IPNetwork, IPAddress, bytes, str, int)
155+
156
157 class IPRANGE_TYPE:
158 """Well-known purpose types for IP ranges."""
159@@ -1191,6 +1194,39 @@ def get_all_interfaces_definition(annotate_with_monitored: bool=True) -> dict:
160 return interfaces
161
162
163+def get_all_interface_subnets():
164+ """Returns all subnets that this machine has access to.
165+
166+ Uses the `get_all_interfaces_definition` to get the available interfaces,
167+ and returns a set of subnets for the machine.
168+
169+ :return: set of IP networks
170+ :rtype: set of `IPNetwork`
171+ """
172+ return set(
173+ IPNetwork(link["address"])
174+ for interface in get_all_interfaces_definition().values()
175+ for link in interface["links"]
176+ )
177+
178+
179+def get_all_interface_source_addresses():
180+ """Return one source address per subnets defined on this machine.
181+
182+ Uses the `get_all_interface_subnets` and `get_source_address` to determine
183+ the best source addresses for this machine.
184+
185+ :return: set of IP addresses
186+ :rtype: set of `str`
187+ """
188+ source_addresses = set()
189+ for network in get_all_interface_subnets():
190+ src = get_source_address(network)
191+ if src is not None:
192+ source_addresses.add(src)
193+ return source_addresses
194+
195+
196 def has_ipv4_address(interfaces: dict, interface: str) -> bool:
197 """Returns True if the specified interface has an IPv4 address assigned.
198
199@@ -1321,3 +1357,35 @@ def coerce_to_valid_hostname(hostname):
200 if hostname == '' or len(hostname) > 64:
201 return None
202 return hostname
203+
204+
205+def get_source_address(destination_ip: IPAddressOrNetwork):
206+ """Returns the local source address for the specified destination IP.
207+
208+ :param destination_ip: Can be an IP address in string format, an IPNetwork,
209+ or an IPAddress object.
210+ :return: the string representation of the local IP address that would be
211+ used for communication with the specified destination.
212+ """
213+ if isinstance(destination_ip, IPNetwork):
214+ destination_ip = IPAddress(destination_ip.first + 1)
215+ else:
216+ destination_ip = make_ipaddress(destination_ip)
217+ af = AF_INET if destination_ip.version == 4 else AF_INET6
218+ with socket.socket(af, socket.SOCK_DGRAM) as sock:
219+ peername = str(destination_ip)
220+ local_address = "0.0.0.0" if af == socket.AF_INET else "::"
221+ try:
222+ # Note: this sets up the socket *just enough* to get the source
223+ # address. No network traffic will be transmitted.
224+ sock.bind((local_address, 0))
225+ sock.connect((peername, 7))
226+ sockname = sock.getsockname()
227+ own_ip = sockname[0]
228+ return own_ip
229+ except OSError:
230+ # Probably "can't assign requested address", which probably means
231+ # we tried to connect to an IPv6 address, but IPv6 is not
232+ # configured. Could also happen if a network or broadcast address
233+ # is passed in, or we otherwise cannot route to the destination.
234+ return None
235diff --git a/src/provisioningserver/utils/tests/test_network.py b/src/provisioningserver/utils/tests/test_network.py
236index 57498af..fbfd678 100644
237--- a/src/provisioningserver/utils/tests/test_network.py
238+++ b/src/provisioningserver/utils/tests/test_network.py
239@@ -51,11 +51,14 @@ from provisioningserver.utils.network import (
240 format_eui,
241 get_all_addresses_for_interface,
242 get_all_interface_addresses,
243+ get_all_interface_source_addresses,
244+ get_all_interface_subnets,
245 get_all_interfaces_definition,
246 get_default_monitored_interfaces,
247 get_eui_organization,
248 get_interface_children,
249 get_mac_organization,
250+ get_source_address,
251 has_ipv4_address,
252 hex_str_to_bytes,
253 inet_ntop,
254@@ -1518,6 +1521,52 @@ class TestGetAllInterfacesDefinition(MAASTestCase):
255 self.assertInterfacesResult(ip_addr, iproute_info, {}, expected_result)
256
257
258+class TestGetAllInterfacesSubnets(MAASTestCase):
259+ """Tests for `get_all_interface_subnets()`."""
260+
261+ def test_includes_unique_subnets(self):
262+ interface_definition = {
263+ 'eth0': {
264+ 'links': [{
265+ 'address': '192.168.122.1/24',
266+ }, {
267+ 'address': '192.168.122.3/24',
268+ }],
269+ },
270+ 'eth1': {
271+ 'links': [{
272+ 'address': '192.168.123.1/24',
273+ }, {
274+ 'address': '192.168.123.2/24',
275+ }]
276+ }
277+ }
278+ self.patch(
279+ network_module,
280+ 'get_all_interfaces_definition').return_value = (
281+ interface_definition)
282+ self.assertEquals(
283+ set([
284+ IPNetwork('192.168.122.1/24'), IPNetwork('192.168.123.1/24')]),
285+ get_all_interface_subnets())
286+
287+
288+class TestGetAllInterfacesSourceAddresses(MAASTestCase):
289+ """Tests for `get_all_interface_source_addresses()`."""
290+
291+ def test_includes_unique_subnets(self):
292+ interface_subnets = set([
293+ IPNetwork('192.168.122.1/24'), IPNetwork('192.168.123.1/24')])
294+ self.patch(
295+ network_module,
296+ 'get_all_interface_subnets').return_value = interface_subnets
297+ self.patch(network_module, 'get_source_address').side_effect = [
298+ '192.168.122.1', None]
299+ self.assertEquals(
300+ set(['192.168.122.1']),
301+ get_all_interface_source_addresses())
302+
303+
304 class TestHasIPv4Address(MAASTestCase):
305 """Tests for `has_ipv4_address()`."""
306
307@@ -2119,3 +2168,30 @@ class TestCoerceHostname(MAASTestCase):
308
309 def test_returns_none_if_result_too_large(self):
310 self.assertIsNone(coerce_to_valid_hostname('a' * 65))
311+
312+
313+class TestGetSourceAddress(MAASTestCase):
314+
315+ def test__accepts_ipnetwork(self):
316+ self.assertThat(
317+ get_source_address(IPNetwork("127.0.0.1/8")), Equals("127.0.0.1"))
318+
319+ def test__accepts_ipaddress(self):
320+ self.assertThat(
321+ get_source_address(IPAddress("127.0.0.1")), Equals("127.0.0.1"))
322+
323+ def test__accepts_string(self):
324+ self.assertThat(
325+ get_source_address("127.0.0.1"), Equals("127.0.0.1"))
326+
327+ def test__supports_ipv6(self):
328+ self.assertThat(
329+ get_source_address("::1"), Equals("::1"))
330+
331+ def test__returns_none_if_no_route_found(self):
332+ self.assertThat(
333+ get_source_address("127.0.0.0"), Is(None))
334+
335+ def test__returns_appropriate_address_for_global_ip(self):
336+ self.assertThat(
337+ get_source_address("8.8.8.8"), Not(Is(None)))

Subscribers

People subscribed via source and target branches