Merge ~blake-rouse/maas:fix-1836276 into maas:master

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: 21213e9f59ad439d7548d1289d44e8b438631fe3
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~blake-rouse/maas:fix-1836276
Merge into: maas:master
Diff against target: 475 lines (+135/-54)
8 files modified
src/maasserver/dhcp.py (+22/-11)
src/maasserver/tests/test_dhcp.py (+37/-1)
src/provisioningserver/dhcp/config.py (+38/-30)
src/provisioningserver/dhcp/testing/config.py (+6/-2)
src/provisioningserver/dhcp/tests/test_config.py (+24/-9)
src/provisioningserver/rpc/cluster.py (+1/-0)
src/provisioningserver/rpc/dhcp.py (+1/-0)
src/provisioningserver/rpc/tests/test_clusterservice.py (+6/-1)
Reviewer Review Type Date Requested Status
Newell Jensen (community) Approve
MAAS Lander Approve
Review via email: mp+371302@code.launchpad.net

Commit message

Fixes LP: #1836276 - Select IP address from interface for shared network defined from the region controller, allowing relayed VLAN's to point to the correct IP address on the rack controller.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b fix-1836276 lp:~blake-rouse/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 6bd4762051b3ec6b8cbb526178a09a5ae41b94a8

review: Approve
Revision history for this message
Adam Collard (adam-collard) :
~blake-rouse/maas:fix-1836276 updated
21213e9... by Blake Rouse

Extract if statement.

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

Thanks for the review. I extracted the if statement into a helper function, much cleaner, thanks for the suggestion.

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

UNIT TESTS
-b fix-1836276 lp:~blake-rouse/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 21213e9f59ad439d7548d1289d44e8b438631fe3

review: Approve
Revision history for this message
Newell Jensen (newell-jensen) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/dhcp.py b/src/maasserver/dhcp.py
2index 21faa35..ab47384 100644
3--- a/src/maasserver/dhcp.py
4+++ b/src/maasserver/dhcp.py
5@@ -151,6 +151,17 @@ def _key_interface_subnet_dynamic_range_count(interface):
6 return count
7
8
9+def _ip_version_on_vlan(ip_address, ip_version, vlan):
10+ """Return True when the `ip_address` is the same `ip_version` and is on
11+ the same `vlan` or relay VLAN's for the `vlan."""
12+ return (
13+ ip_is_version(ip_address, ip_version) and
14+ ip_address.subnet is not None and
15+ ip_address.subnet.vlan is not None and (
16+ ip_address.subnet.vlan == vlan or
17+ vlan in list(ip_address.subnet.vlan.relay_vlans.all())))
18+
19+
20 def get_interfaces_with_ip_on_vlan(rack_controller, vlan, ip_version):
21 """Return a list of interfaces that have an assigned IP address on `vlan`.
22
23@@ -164,20 +175,16 @@ def get_interfaces_with_ip_on_vlan(rack_controller, vlan, ip_version):
24 interfaces_with_static = []
25 interfaces_with_discovered = []
26 for interface in rack_controller.interface_set.all().prefetch_related(
27- "ip_addresses__subnet__vlan",
28+ "ip_addresses__subnet__vlan__relay_vlans",
29 "ip_addresses__subnet__iprange_set"):
30 for ip_address in interface.ip_addresses.all():
31 if ip_address.alloc_type in [
32 IPADDRESS_TYPE.AUTO, IPADDRESS_TYPE.STICKY]:
33- if (ip_is_version(ip_address, ip_version) and
34- ip_address.subnet is not None and
35- ip_address.subnet.vlan == vlan):
36+ if _ip_version_on_vlan(ip_address, ip_version, vlan):
37 interfaces_with_static.append(interface)
38 break
39 elif ip_address.alloc_type == IPADDRESS_TYPE.DISCOVERED:
40- if (ip_is_version(ip_address, ip_version) and
41- ip_address.subnet is not None and
42- ip_address.subnet.vlan == vlan):
43+ if _ip_version_on_vlan(ip_address, ip_version, vlan):
44 interfaces_with_discovered.append(interface)
45 break
46 if len(interfaces_with_static) == 1:
47@@ -689,14 +696,16 @@ def get_dhcp_configuration(rack_controller, test_dhcp_snippet=None):
48 failover_peer, subnets, hosts, interface = config
49 if failover_peer is not None:
50 failover_peers_v4.append(failover_peer)
51- shared_networks_v4.append({
52+ shared_network = {
53 "name": "vlan-%d" % vlan.id,
54 "mtu": vlan.mtu,
55 "subnets": subnets,
56- })
57+ }
58+ shared_networks_v4.append(shared_network)
59 hosts_v4.extend(hosts)
60 if interface is not None:
61 interfaces_v4.add(interface)
62+ shared_network["interface"] = interface
63 # IPv6
64 if len(subnets_v6) > 0:
65 config = get_dhcp_configure_for(
66@@ -706,14 +715,16 @@ def get_dhcp_configuration(rack_controller, test_dhcp_snippet=None):
67 failover_peer, subnets, hosts, interface = config
68 if failover_peer is not None:
69 failover_peers_v6.append(failover_peer)
70- shared_networks_v6.append({
71+ shared_network = {
72 "name": "vlan-%d" % vlan.id,
73 "mtu": vlan.mtu,
74 "subnets": subnets,
75- })
76+ }
77+ shared_networks_v6.append(shared_network)
78 hosts_v6.extend(hosts)
79 if interface is not None:
80 interfaces_v6.add(interface)
81+ shared_network["interface"] = interface
82 # When no interfaces exist for each IP version clear the shared networks
83 # as DHCP server cannot be started and needs to be stopped.
84 if len(interfaces_v4) == 0:
85diff --git a/src/maasserver/tests/test_dhcp.py b/src/maasserver/tests/test_dhcp.py
86index 8601d84..2c25bcc 100644
87--- a/src/maasserver/tests/test_dhcp.py
88+++ b/src/maasserver/tests/test_dhcp.py
89@@ -266,7 +266,7 @@ class TestGetInterfacesWithIPOnVLAN(MAASServerTestCase):
90 # affects the number of queries performed when performing this
91 # operation. It is important to keep this number as low as possible.
92 self.assertEqual(
93- query_10_count, 5,
94+ query_10_count, 6,
95 "Number of queries has changed; make sure this is expected.")
96 self.assertEqual(
97 query_10_count, query_20_count,
98@@ -426,6 +426,42 @@ class TestGetInterfacesWithIPOnVLAN(MAASServerTestCase):
99 dhcp.get_interfaces_with_ip_on_vlan(
100 rack_controller, vlan, subnet.get_ipnetwork().version))
101
102+ def test__returns_interface_with_static_ip_on_vlan_from_relay(self):
103+ rack_controller = factory.make_RackController()
104+ vlan = factory.make_VLAN()
105+ relayed_to_another = factory.make_VLAN(relay_vlan=vlan)
106+ subnet = factory.make_Subnet(vlan=vlan)
107+ interface = factory.make_Interface(
108+ INTERFACE_TYPE.PHYSICAL, node=rack_controller, vlan=vlan)
109+ factory.make_StaticIPAddress(
110+ alloc_type=IPADDRESS_TYPE.AUTO, subnet=subnet, interface=interface)
111+ self.assertEquals(
112+ [interface],
113+ dhcp.get_interfaces_with_ip_on_vlan(
114+ rack_controller, relayed_to_another,
115+ subnet.get_ipnetwork().version))
116+
117+ def test__returns_interfaces_with_discovered_ips_on_vlan_from_relay(self):
118+ rack_controller = factory.make_RackController()
119+ vlan = factory.make_VLAN()
120+ relayed_to_another = factory.make_VLAN(relay_vlan=vlan)
121+ subnet = factory.make_Subnet(vlan=vlan)
122+ interface_one = factory.make_Interface(
123+ INTERFACE_TYPE.PHYSICAL, node=rack_controller, vlan=vlan)
124+ factory.make_StaticIPAddress(
125+ alloc_type=IPADDRESS_TYPE.DISCOVERED,
126+ subnet=subnet, interface=interface_one)
127+ interface_two = factory.make_Interface(
128+ INTERFACE_TYPE.PHYSICAL, node=rack_controller, vlan=vlan)
129+ factory.make_StaticIPAddress(
130+ alloc_type=IPADDRESS_TYPE.DISCOVERED,
131+ subnet=subnet, interface=interface_two)
132+ self.assertItemsEqual(
133+ [interface_one, interface_two],
134+ dhcp.get_interfaces_with_ip_on_vlan(
135+ rack_controller, relayed_to_another,
136+ subnet.get_ipnetwork().version))
137+
138
139 class TestGenManagedVLANsFor(MAASServerTestCase):
140 """Tests for `gen_managed_vlans_for`."""
141diff --git a/src/provisioningserver/dhcp/config.py b/src/provisioningserver/dhcp/config.py
142index 7199e83..63bd67f 100644
143--- a/src/provisioningserver/dhcp/config.py
144+++ b/src/provisioningserver/dhcp/config.py
145@@ -350,6 +350,32 @@ def normalise_any_iterable_to_quoted_comma_list(iterable):
146 return ", ".join(map(quote, iterable))
147
148
149+def get_rack_ip_for_subnet(version, cidr, interface):
150+ """
151+ Return the IP address on this rack controller.
152+
153+ First the code will try to find an IP address that is in the cidr, but if
154+ not it will use the first IP address on the interface (to support
155+ VLAN relay).
156+ """
157+ cidr = IPNetwork(cidr)
158+ if interface:
159+ ip_addresses = [
160+ IPAddress(addr)
161+ for addr in net_utils.get_all_addresses_for_interface(interface)]
162+ else:
163+ ip_addresses = [
164+ IPAddress(addr)
165+ for addr in net_utils.get_all_interface_addresses()]
166+ for ip_addr in ip_addresses:
167+ if ip_addr in cidr:
168+ return ip_addr
169+ for ip_addr in ip_addresses:
170+ if ip_addr.version == version:
171+ return ip_addr
172+ return None
173+
174+
175 @typed
176 def get_config_v4(
177 template_name: str, global_dhcp_snippets: Sequence[dict],
178@@ -373,22 +399,15 @@ def get_config_v4(
179 "running_in_snap": snappy.running_in_snap(),
180 }
181
182- rack_addrs = [
183- IPAddress(addr)
184- for addr in net_utils.get_all_interface_addresses()]
185-
186 for shared_network in shared_networks:
187+ interface = shared_network.get("interface", None)
188 for subnet in shared_network["subnets"]:
189- cidr = IPNetwork(subnet['subnet_cidr'])
190- rack_ips = [
191- str(rack_addr)
192- for rack_addr in rack_addrs
193- if rack_addr in cidr
194- ]
195- if len(rack_ips) > 0:
196- subnet["next_server"] = rack_ips[0]
197+ rack_ip = get_rack_ip_for_subnet(
198+ 4, subnet['subnet_cidr'], interface)
199+ if rack_ip is not None:
200+ subnet["next_server"] = rack_ip
201 subnet["bootloader"] = compose_conditional_bootloader(
202- False, rack_ips[0])
203+ False, rack_ip)
204 ntp_servers = subnet["ntp_servers"] # Is a list.
205 ntp_servers_ipv4, ntp_servers_ipv6 = _get_addresses(*ntp_servers)
206 subnet["ntp_servers_ipv4"] = ", ".join(ntp_servers_ipv4)
207@@ -428,12 +447,8 @@ def get_config_v6(
208 "running_in_snap": snappy.running_in_snap(),
209 }
210
211- rack_addrs = [
212- IPAddress(addr)
213- for addr in net_utils.get_all_interface_addresses()]
214-
215 shared_networks = _process_network_parameters_v6(
216- rack_addrs, failover_peers, shared_networks)
217+ failover_peers, shared_networks)
218
219 try:
220 return template.substitute(
221@@ -446,15 +461,12 @@ def get_config_v6(
222 "Failed to render DHCP configuration.") from error
223
224
225-def _process_network_parameters_v6(
226- rack_addrs, failover_peers, shared_networks):
227+def _process_network_parameters_v6(failover_peers, shared_networks):
228 """Preprocess shared_networks prior to rendering the template.
229
230 This is a separate function, partly for readability, and partly for ease
231 of testing.
232
233- :param rack_addrs: a list of IPAddress values for the interfaces on this
234- rack controller.
235 :param failover_peers: failover_peers from get_config_v6.
236 :param shared_networks: shared_networks from get_config_v6.
237 :return: an updated shared_networks, suitable for rendering the template.
238@@ -462,15 +474,11 @@ def _process_network_parameters_v6(
239 peers = {x["name"]: x for x in failover_peers}
240
241 for shared_network in shared_networks:
242+ interface = shared_network.get("interface", None)
243 for subnet in shared_network["subnets"]:
244- cidr = IPNetwork(subnet['subnet_cidr'])
245- rack_ip_found = False
246- for rack_addr in rack_addrs:
247- if rack_addr in cidr:
248- rack_ip = str(rack_addr)
249- rack_ip_found = True
250- break
251- if rack_ip_found:
252+ rack_ip = get_rack_ip_for_subnet(
253+ 6, subnet['subnet_cidr'], interface)
254+ if rack_ip is not None:
255 subnet["bootloader"] = compose_conditional_bootloader(
256 True, rack_ip)
257 ntp_servers = subnet["ntp_servers"] # Is a list.
258diff --git a/src/provisioningserver/dhcp/testing/config.py b/src/provisioningserver/dhcp/testing/config.py
259index c2640d4..068f82f 100644
260--- a/src/provisioningserver/dhcp/testing/config.py
261+++ b/src/provisioningserver/dhcp/testing/config.py
262@@ -133,7 +133,8 @@ def make_subnet_config(network=None, pools=None, ipv6=False,
263 }
264
265
266-def make_shared_network(name=None, subnets=None, ipv6=False):
267+def make_shared_network(
268+ name=None, subnets=None, ipv6=False, with_interface=False):
269 """Return complete DHCP configuration dict for a shared network."""
270 if name is None:
271 name = factory.make_name("vlan")
272@@ -142,11 +143,14 @@ def make_shared_network(name=None, subnets=None, ipv6=False):
273 make_subnet_config(ipv6=ipv6)
274 for _ in range(3)
275 ]
276- return {
277+ data = {
278 "name": name,
279 "mtu": 1500,
280 "subnets": subnets,
281 }
282+ if with_interface:
283+ data["interface"] = factory.make_name("eth")
284+ return data
285
286
287 def make_shared_network_v1(name=None, subnets=None, ipv6=False):
288diff --git a/src/provisioningserver/dhcp/tests/test_config.py b/src/provisioningserver/dhcp/tests/test_config.py
289index 13d6a18..69deb0e 100644
290--- a/src/provisioningserver/dhcp/tests/test_config.py
291+++ b/src/provisioningserver/dhcp/tests/test_config.py
292@@ -61,7 +61,7 @@ def is_ip_address(string):
293 return True
294
295
296-def make_sample_params_only(ipv6=False):
297+def make_sample_params_only(ipv6=False, with_interface=False):
298 """Return a dict of arbitrary DHCP configuration parameters.
299
300 :param ipv6: When true, prepare configuration for a DHCPv6 server,
301@@ -73,7 +73,7 @@ def make_sample_params_only(ipv6=False):
302 for _ in range(3)
303 ]
304 shared_networks = [
305- make_shared_network(ipv6=ipv6)
306+ make_shared_network(ipv6=ipv6, with_interface=with_interface)
307 for _ in range(3)
308 ]
309
310@@ -114,7 +114,7 @@ def read_aliases_from_etc_hosts():
311 aliases_from_etc_hosts = tuple(read_aliases_from_etc_hosts())
312
313
314-def make_sample_params(test, ipv6=False):
315+def make_sample_params(test, ipv6=False, with_interface=False):
316 """Return a dict of arbitrary DHCP configuration parameters.
317
318 This differs from `make_sample_params_only` in that it arranges it such
319@@ -126,7 +126,8 @@ def make_sample_params(test, ipv6=False):
320 otherwise prepare configuration for a DHCPv4 server.
321 :return: A dictionary of sample configuration.
322 """
323- sample_params = make_sample_params_only(ipv6=ipv6)
324+ sample_params = make_sample_params_only(
325+ ipv6=ipv6, with_interface=with_interface)
326
327 # So that get_config can resolve the configuration, collect hostnames from
328 # the sample params that need to resolve then replace them with names that
329@@ -433,7 +434,7 @@ class TestGetConfig(MAASTestCase):
330 class TestGetConfigIPv4(MAASTestCase):
331 """Tests for `get_config`."""
332
333- def test__includes_next_server_in_config(self):
334+ def test__includes_next_server_in_config_from_all_addresses(self):
335 params = make_sample_params(self, ipv6=False)
336 subnet = params['shared_networks'][0]['subnets'][0]
337 next_server_ip = factory.pick_ip_in_network(
338@@ -446,6 +447,20 @@ class TestGetConfigIPv4(MAASTestCase):
339 self.assertThat(
340 config_output, Contains('next-server %s;' % next_server_ip))
341
342+ def test__includes_next_server_in_config_from_interface_addresses(self):
343+ params = make_sample_params(self, ipv6=False, with_interface=True)
344+ subnet = params['shared_networks'][0]['subnets'][0]
345+ next_server_ip = factory.pick_ip_in_network(
346+ netaddr.IPNetwork(subnet['subnet_cidr']))
347+ self.patch(
348+ net_utils, 'get_all_addresses_for_interface').return_value = [
349+ next_server_ip
350+ ]
351+ config_output = config.get_config('dhcpd.conf.template', **params)
352+ validate_dhcpd_configuration(self, config_output, False)
353+ self.assertThat(
354+ config_output, Contains('next-server %s;' % next_server_ip))
355+
356
357 class Test_process_shared_network_v6(MAASTestCase):
358 """Tests for `_process_network_parameters_v6`."""
359@@ -456,14 +471,12 @@ class Test_process_shared_network_v6(MAASTestCase):
360 'ip_range_high': '2001:db8:3:1::ffff',
361 'ip_range_low': '2001:db8:3:1::',
362 'failover_peer': None},
363- rack_addrs=[netaddr.IPAddress('2001:db8:3:280::2')],
364 failover_peers=[])),
365 ('primary', dict(
366 expected={
367 'ip_range_high': '2001:db8:3:1::7fff',
368 'ip_range_low': '2001:db8:3:1::',
369 'failover_peer': 'failover-vlan-5020'},
370- rack_addrs=[netaddr.IPAddress('2001:db8:3:280::2')],
371 failover_peers=[{
372 'mode': 'primary',
373 'peer_address': '2001:db8:3:0:1::',
374@@ -474,7 +487,6 @@ class Test_process_shared_network_v6(MAASTestCase):
375 'ip_range_high': '2001:db8:3:1::ffff',
376 'ip_range_low': '2001:db8:3:1::8000',
377 'failover_peer': 'failover-vlan-5020'},
378- rack_addrs=[netaddr.IPAddress('2001:db8:3:0:1::')],
379 failover_peers=[{
380 'mode': 'secondary',
381 'address': '2001:db8:3:0:1::',
382@@ -485,6 +497,7 @@ class Test_process_shared_network_v6(MAASTestCase):
383 shared_networks = [
384 {
385 'name': 'vlan-5020',
386+ 'interface': 'eth0',
387 'subnets': [
388 {
389 'domain_name': 'maas.example.com',
390@@ -503,12 +516,14 @@ class Test_process_shared_network_v6(MAASTestCase):
391 }],
392 }]
393 self.patch(
394+ config, 'get_rack_ip_for_subnet').return_value = None
395+ self.patch(
396 config, 'compose_conditional_bootloader').return_value = ""
397 self.patch(
398 config, '_get_addresses').return_value = (
399 [''], ['2001:db8:280::2'])
400 actual = config._process_network_parameters_v6(
401- self.rack_addrs, self.failover_peers, shared_networks)
402+ self.failover_peers, shared_networks)
403 self.assertDictEqual(
404 actual[0]['subnets'][0]['pools'][0], self.expected)
405
406diff --git a/src/provisioningserver/rpc/cluster.py b/src/provisioningserver/rpc/cluster.py
407index 5469cf1..40988e2 100644
408--- a/src/provisioningserver/rpc/cluster.py
409+++ b/src/provisioningserver/rpc/cluster.py
410@@ -416,6 +416,7 @@ class _ConfigureDHCP_V2(amp.Command):
411 ], optional=True)),
412 ])),
413 (b"mtu", amp.Integer(optional=True)),
414+ (b"interface", amp.Unicode(optional=True)),
415 ])),
416 (b"hosts", CompressedAmpList([
417 (b"host", amp.Unicode()),
418diff --git a/src/provisioningserver/rpc/dhcp.py b/src/provisioningserver/rpc/dhcp.py
419index dc42518..babf9ed 100644
420--- a/src/provisioningserver/rpc/dhcp.py
421+++ b/src/provisioningserver/rpc/dhcp.py
422@@ -529,6 +529,7 @@ def downgrade_shared_networks(shared_networks):
423 Mutates `shared_networks` in place.
424 """
425 for shared_network in shared_networks:
426+ shared_network.pop("interface", None)
427 for subnet in shared_network["subnets"]:
428 dns_servers = subnet["dns_servers"]
429 if not isinstance(dns_servers, str):
430diff --git a/src/provisioningserver/rpc/tests/test_clusterservice.py b/src/provisioningserver/rpc/tests/test_clusterservice.py
431index 07e1645..0b19738 100644
432--- a/src/provisioningserver/rpc/tests/test_clusterservice.py
433+++ b/src/provisioningserver/rpc/tests/test_clusterservice.py
434@@ -2323,6 +2323,7 @@ class TestClusterProtocol_ConfigureDHCP(MAASTestCase):
435 "command": cluster.ConfigureDHCPv4,
436 "make_network": factory.make_ipv4_network,
437 "make_shared_network": make_shared_network_v1,
438+ "make_shared_network_kwargs": {},
439 "concurrency_lock": concurrency.dhcpv4,
440 }),
441 ("DHCPv4,V2", {
442@@ -2330,6 +2331,7 @@ class TestClusterProtocol_ConfigureDHCP(MAASTestCase):
443 "command": cluster.ConfigureDHCPv4_V2,
444 "make_network": factory.make_ipv4_network,
445 "make_shared_network": make_shared_network,
446+ "make_shared_network_kwargs": {"with_interface": True},
447 "concurrency_lock": concurrency.dhcpv4,
448 }),
449 ("DHCPv6", {
450@@ -2337,6 +2339,7 @@ class TestClusterProtocol_ConfigureDHCP(MAASTestCase):
451 "command": cluster.ConfigureDHCPv6,
452 "make_network": factory.make_ipv6_network,
453 "make_shared_network": make_shared_network_v1,
454+ "make_shared_network_kwargs": {},
455 "concurrency_lock": concurrency.dhcpv6,
456 }),
457 ("DHCPv6,V2", {
458@@ -2344,6 +2347,7 @@ class TestClusterProtocol_ConfigureDHCP(MAASTestCase):
459 "command": cluster.ConfigureDHCPv6_V2,
460 "make_network": factory.make_ipv6_network,
461 "make_shared_network": make_shared_network,
462+ "make_shared_network_kwargs": {"with_interface": True},
463 "concurrency_lock": concurrency.dhcpv6,
464 }),
465 )
466@@ -2361,7 +2365,8 @@ class TestClusterProtocol_ConfigureDHCP(MAASTestCase):
467
468 omapi_key = factory.make_name('key')
469 failover_peers = [make_failover_peer_config()]
470- shared_networks = [self.make_shared_network()]
471+ shared_networks = [self.make_shared_network(
472+ **self.make_shared_network_kwargs)]
473 shared_networks = fix_shared_networks_failover(
474 shared_networks, failover_peers)
475 hosts = [make_host()]

Subscribers

People subscribed via source and target branches