Browse Source

Support Stateful and Stateless DHCPv6 by dnsmasq

* This patch adds support for subnets created with 'ipv6_address_mode'
  set to 'dhcpv6-stateful' or 'dhcpv6-stateless' by dnsmasq.
* If no dnsmasq process for subnet's network is launched, Neutron
  will launch new dnsmasq process on subnet's dhcp port in 'qdhcp-'
  namespace. If previous dnsmasq process is already launched,
  restart dnsmasq with new configuration.
* Neutron will update dnsmasq process and restart it when subnet
  gets updated.
* This patch enforces the version check of dnsmasq. dhcp-agent will
  fail to start if version of dnsmasq<2.63.

DocImpact
UpgradeImpact
Blueprint dnsmasq-ipv6-dhcpv6-stateful
Blueprint dnsmasq-ipv6-dhcpv6-stateless

Change-Id: I30e9950bbc5a89f01ccb9c561471f155a9fd1d11
changes/99/106299/12
Xu Han Peng 8 years ago
parent
commit
3686d035de
  1. 137
      neutron/agent/linux/dhcp.py
  2. 157
      neutron/tests/unit/test_linux_dhcp.py

137
neutron/agent/linux/dhcp.py

@ -313,10 +313,11 @@ class Dnsmasq(DhcpLocalProcess):
ver = re.findall("\d+.\d+", out)[0]
is_valid_version = float(ver) >= cls.MINIMUM_VERSION
if not is_valid_version:
LOG.warning(_('FAILED VERSION REQUIREMENT FOR DNSMASQ. '
'DHCP AGENT MAY NOT RUN CORRECTLY! '
'Please ensure that its version is %s '
'or above!'), cls.MINIMUM_VERSION)
LOG.error(_('FAILED VERSION REQUIREMENT FOR DNSMASQ. '
'DHCP AGENT MAY NOT RUN CORRECTLY! '
'Please ensure that its version is %s '
'or above!'), cls.MINIMUM_VERSION)
raise SystemExit(1)
except (OSError, RuntimeError, IndexError, ValueError):
LOG.error(_('Unable to determine dnsmasq version. '
'Please ensure that its version is %s '
@ -368,17 +369,12 @@ class Dnsmasq(DhcpLocalProcess):
else:
# Note(scollins) If the IPv6 attributes are not set, set it as
# static to preserve previous behavior
if (not getattr(subnet, 'ipv6_ra_mode', None) and
not getattr(subnet, 'ipv6_address_mode', None)):
addr_mode = getattr(subnet, 'ipv6_address_mode', None)
ra_mode = getattr(subnet, 'ipv6_ra_mode', None)
if (addr_mode in [constants.DHCPV6_STATEFUL,
constants.DHCPV6_STATELESS] or
not addr_mode and not ra_mode):
mode = 'static'
elif getattr(subnet, 'ipv6_ra_mode', None) is None:
# RA mode is not set - do not launch dnsmasq
continue
if self.version >= self.MINIMUM_VERSION:
set_tag = 'set:'
else:
set_tag = ''
cidr = netaddr.IPNetwork(subnet.cidr)
@ -390,14 +386,9 @@ class Dnsmasq(DhcpLocalProcess):
# mode is optional and is not set - skip it
if mode:
cmd.append('--dhcp-range=%s%s,%s,%s,%s' %
(set_tag, self._TAG_PREFIX % i,
('set:', self._TAG_PREFIX % i,
cidr.network, mode, lease))
else:
cmd.append('--dhcp-range=%s%s,%s,%s' %
(set_tag, self._TAG_PREFIX % i,
cidr.network, lease))
possible_leases += cidr.size
possible_leases += cidr.size
# Cap the limit because creating lots of subnets can inflate
# this possible lease cap.
@ -465,9 +456,8 @@ class Dnsmasq(DhcpLocalProcess):
# associated with the subnet being managed by this
# dhcp agent
if alloc.subnet_id in v6_nets:
ra_mode = v6_nets[alloc.subnet_id].ipv6_ra_mode
addr_mode = v6_nets[alloc.subnet_id].ipv6_address_mode
if (ra_mode is None and addr_mode == constants.IPV6_SLAAC):
if addr_mode != constants.DHCPV6_STATEFUL:
continue
hostname = 'host-%s' % alloc.ip_address.replace(
'.', '-').replace(':', '-')
@ -497,7 +487,6 @@ class Dnsmasq(DhcpLocalProcess):
LOG.debug(_('Building host file: %s'), filename)
for (port, alloc, hostname, name) in self._iter_hosts():
set_tag = ''
# (dzyu) Check if it is legal ipv6 address, if so, need wrap
# it with '[]' to let dnsmasq to distinguish MAC address from
# IPv6 address.
@ -510,12 +499,9 @@ class Dnsmasq(DhcpLocalProcess):
"ip": ip_address})
if getattr(port, 'extra_dhcp_opts', False):
if self.version >= self.MINIMUM_VERSION:
set_tag = 'set:'
buf.write('%s,%s,%s,%s%s\n' %
(port.mac_address, name, ip_address,
set_tag, port.id))
'set:', port.id))
else:
buf.write('%s,%s,%s\n' %
(port.mac_address, name, ip_address))
@ -575,17 +561,27 @@ class Dnsmasq(DhcpLocalProcess):
dhcp_ips = collections.defaultdict(list)
subnet_idx_map = {}
for i, subnet in enumerate(self.network.subnets):
if not subnet.enable_dhcp:
if (not subnet.enable_dhcp or
(subnet.ip_version == 6 and
getattr(subnet, 'ipv6_address_mode', None)
in [None, constants.IPV6_SLAAC])):
continue
if subnet.dns_nameservers:
options.append(
self._format_option(i, 'dns-server',
','.join(subnet.dns_nameservers)))
self._format_option(
subnet.ip_version, i, 'dns-server',
','.join(
Dnsmasq._convert_to_literal_addrs(
subnet.ip_version, subnet.dns_nameservers))))
else:
# use the dnsmasq ip as nameservers only if there is no
# dns-server submitted by the server
subnet_idx_map[subnet.id] = i
if self.conf.dhcp_domain and subnet.ip_version == 6:
options.append('tag:tag%s,option6:domain-search,%s' %
(i, ''.join(self.conf.dhcp_domain)))
gateway = subnet.gateway_ip
host_routes = []
for hr in subnet.host_routes:
@ -603,27 +599,42 @@ class Dnsmasq(DhcpLocalProcess):
'%s/32,%s' % (METADATA_DEFAULT_IP, subnet_dhcp_ip)
)
if host_routes:
if gateway and subnet.ip_version == 4:
host_routes.append("%s,%s" % ("0.0.0.0/0", gateway))
options.append(
self._format_option(i, 'classless-static-route',
','.join(host_routes)))
options.append(
self._format_option(i, WIN2k3_STATIC_DNS,
','.join(host_routes)))
if subnet.ip_version == 4:
if host_routes:
if gateway:
host_routes.append("%s,%s" % ("0.0.0.0/0", gateway))
options.append(
self._format_option(subnet.ip_version, i,
'classless-static-route',
','.join(host_routes)))
options.append(
self._format_option(subnet.ip_version, i,
WIN2k3_STATIC_DNS,
','.join(host_routes)))
if gateway:
options.append(self._format_option(i, 'router', gateway))
options.append(self._format_option(subnet.ip_version,
i, 'router',
gateway))
else:
options.append(self._format_option(i, 'router'))
options.append(self._format_option(subnet.ip_version,
i, 'router'))
for port in self.network.ports:
if getattr(port, 'extra_dhcp_opts', False):
options.extend(
self._format_option(port.id, opt.opt_name, opt.opt_value)
for opt in port.extra_dhcp_opts)
for ip_version in (4, 6):
if any(
netaddr.IPAddress(ip.ip_address).version == ip_version
for ip in port.fixed_ips):
options.extend(
# TODO(xuhanp):Instead of applying extra_dhcp_opts
# to both DHCPv4 and DHCPv6, we need to find a new
# way to specify options for v4 and v6
# respectively. We also need to validate the option
# before applying it.
self._format_option(ip_version, port.id,
opt.opt_name, opt.opt_value)
for opt in port.extra_dhcp_opts)
# provides all dnsmasq ip as dns-server if there is more than
# one dnsmasq for a subnet and there is no dns-server submitted
@ -636,10 +647,16 @@ class Dnsmasq(DhcpLocalProcess):
dhcp_ips[i].append(ip.ip_address)
for i, ips in dhcp_ips.items():
if len(ips) > 1:
options.append(self._format_option(i,
'dns-server',
','.join(ips)))
for ip_version in (4, 6):
vx_ips = [ip for ip in ips
if netaddr.IPAddress(ip).version == ip_version]
if vx_ips:
options.append(
self._format_option(
ip_version, i, 'dns-server',
','.join(
Dnsmasq._convert_to_literal_addrs(ip_version,
vx_ips))))
name = self.get_conf_file_name('opts')
utils.replace_file(name, '\n'.join(options))
@ -667,22 +684,26 @@ class Dnsmasq(DhcpLocalProcess):
return retval
def _format_option(self, tag, option, *args):
def _format_option(self, ip_version, tag, option, *args):
"""Format DHCP option by option name or code."""
if self.version >= self.MINIMUM_VERSION:
set_tag = 'tag:'
else:
set_tag = ''
option = str(option)
if isinstance(tag, int):
tag = self._TAG_PREFIX % tag
if not option.isdigit():
option = 'option:%s' % option
if ip_version == 4:
option = 'option:%s' % option
else:
option = 'option6:%s' % option
return ','.join(('tag:' + tag, '%s' % option) + args)
return ','.join((set_tag + tag, '%s' % option) + args)
@staticmethod
def _convert_to_literal_addrs(ip_version, ips):
if ip_version == 4:
return ips
return ['[' + ip + ']' for ip in ips]
def _enable_metadata(self, subnet):
'''Determine if the metadata route will be pushed to hosts on subnet.

157
neutron/tests/unit/test_linux_dhcp.py

@ -17,6 +17,7 @@ import contextlib
import os
import mock
import netaddr
from oslo.config import cfg
import testtools
@ -60,8 +61,8 @@ class FakePort2:
id = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
admin_state_up = False
device_owner = 'foo2'
fixed_ips = [FakeIPAllocation('fdca:3ba5:a17a:4ba3::2',
'ffffffff-ffff-ffff-ffff-ffffffffffff')]
fixed_ips = [FakeIPAllocation('192.168.0.3',
'dddddddd-dddd-dddd-dddd-dddddddddddd')]
mac_address = '00:00:f3:aa:bb:cc'
def __init__(self):
@ -72,10 +73,10 @@ class FakePort3:
id = '44444444-4444-4444-4444-444444444444'
admin_state_up = True
device_owner = 'foo3'
fixed_ips = [FakeIPAllocation('192.168.0.3',
fixed_ips = [FakeIPAllocation('192.168.0.4',
'dddddddd-dddd-dddd-dddd-dddddddddddd'),
FakeIPAllocation('fdca:3ba5:a17a:4ba3::3',
'ffffffff-ffff-ffff-ffff-ffffffffffff')]
FakeIPAllocation('192.168.1.2',
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee')]
mac_address = '00:00:0f:aa:bb:cc'
def __init__(self):
@ -95,6 +96,32 @@ class FakePort4:
self.extra_dhcp_opts = []
class FakeV6Port:
id = 'hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh'
admin_state_up = True
device_owner = 'foo3'
fixed_ips = [FakeIPAllocation('fdca:3ba5:a17a:4ba3::2',
'ffffffff-ffff-ffff-ffff-ffffffffffff')]
mac_address = '00:00:f3:aa:bb:cc'
def __init__(self):
self.extra_dhcp_opts = []
class FakeDualPort:
id = 'hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh'
admin_state_up = True
device_owner = 'foo3'
fixed_ips = [FakeIPAllocation('192.168.0.3',
'dddddddd-dddd-dddd-dddd-dddddddddddd'),
FakeIPAllocation('fdca:3ba5:a17a:4ba3::3',
'ffffffff-ffff-ffff-ffff-ffffffffffff')]
mac_address = '00:00:0f:aa:bb:cc'
def __init__(self):
self.extra_dhcp_opts = []
class FakeRouterPort:
id = 'rrrrrrrr-rrrr-rrrr-rrrr-rrrrrrrrrrrr'
admin_state_up = True
@ -224,6 +251,18 @@ class FakeV4SubnetNoDHCP:
dns_nameservers = []
class FakeV6SubnetDHCPStateful:
id = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
ip_version = 6
cidr = 'fdca:3ba5:a17a:4ba3::/64'
gateway_ip = 'fdca:3ba5:a17a:4ba3::1'
enable_dhcp = True
host_routes = [FakeV6HostRoute]
dns_nameservers = ['2001:0200:feed:7ac0::1']
ipv6_ra_mode = None
ipv6_address_mode = constants.DHCPV6_STATEFUL
class FakeV6SubnetSlaac:
id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
ip_version = 6
@ -271,14 +310,14 @@ class FakeV6Network:
class FakeDualNetwork:
id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
subnets = [FakeV4Subnet(), FakeV6Subnet()]
ports = [FakePort1(), FakePort2(), FakePort3(), FakeRouterPort()]
subnets = [FakeV4Subnet(), FakeV6SubnetDHCPStateful()]
ports = [FakePort1(), FakeV6Port(), FakeDualPort(), FakeRouterPort()]
namespace = 'qdhcp-ns'
class FakeDualNetworkGatewayRoute:
id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
subnets = [FakeV4SubnetGatewayRoute(), FakeV6Subnet()]
subnets = [FakeV4SubnetGatewayRoute(), FakeV6SubnetDHCPStateful()]
ports = [FakePort1(), FakePort2(), FakePort3(), FakeRouterPort()]
namespace = 'qdhcp-ns'
@ -724,11 +763,16 @@ class TestDnsmasq(TestBase):
prefix = '--dhcp-range=set:tag%d,%s,static,%s%s'
else:
prefix = '--dhcp-range=set:tag%d,%s,%s%s'
expected.extend(prefix %
(i, s.cidr.split('/')[0], lease_duration, seconds)
for i, s in enumerate(network.subnets))
expected.append('--dhcp-lease-max=%d' % max_leases)
possible_leases = 0
for i, s in enumerate(network.subnets):
if (s.ip_version != 6
or s.ipv6_address_mode == constants.DHCPV6_STATEFUL):
expected.extend([prefix % (
i, s.cidr.split('/')[0], lease_duration, seconds)])
possible_leases += netaddr.IPNetwork(s.cidr).size
expected.append('--dhcp-lease-max=%d' % min(
possible_leases, max_leases))
expected.extend(extra_options)
self.execute.return_value = ('', '')
@ -775,10 +819,9 @@ class TestDnsmasq(TestBase):
self.safe.assert_has_calls([mock.call(exp_host_name, exp_host_data),
mock.call(exp_addn_name, exp_addn_data)])
def test_spawn_no_dnsmasq_ipv6_mode(self):
def test_spawn_no_dhcp_range(self):
network = FakeV6Network()
subnet = FakeV6Subnet()
subnet.ipv6_ra_mode = True
subnet = FakeV6SubnetSlaac()
network.subnets = [subnet]
self._test_spawn(['--conf-file=', '--domain=openstacklocal'],
network, has_static=False)
@ -805,18 +848,15 @@ class TestDnsmasq(TestBase):
def test_output_opts_file(self):
fake_v6 = '2001:0200:feed:7ac0::1'
fake_v6_cidr = '2001:0200:feed:7ac0::/64'
expected = (
'tag:tag0,option:dns-server,8.8.8.8\n'
'tag:tag0,option:classless-static-route,20.0.0.1/24,20.0.0.1,'
'0.0.0.0/0,192.168.0.1\n'
'tag:tag0,249,20.0.0.1/24,20.0.0.1,0.0.0.0/0,192.168.0.1\n'
'tag:tag0,option:router,192.168.0.1\n'
'tag:tag1,option:dns-server,%s\n'
'tag:tag1,option:classless-static-route,%s,%s\n'
'tag:tag1,249,%s,%s').lstrip() % (fake_v6,
fake_v6_cidr, fake_v6,
fake_v6_cidr, fake_v6)
'tag:tag1,option6:dns-server,%s\n'
'tag:tag1,option6:domain-search,openstacklocal').lstrip() % (
'[' + fake_v6 + ']')
with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn:
conf_fn.return_value = '/foo/opts'
@ -828,15 +868,12 @@ class TestDnsmasq(TestBase):
def test_output_opts_file_gateway_route(self):
fake_v6 = '2001:0200:feed:7ac0::1'
fake_v6_cidr = '2001:0200:feed:7ac0::/64'
expected = """
tag:tag0,option:dns-server,8.8.8.8
tag:tag0,option:router,192.168.0.1
tag:tag1,option:dns-server,%s
tag:tag1,option:classless-static-route,%s,%s
tag:tag1,249,%s,%s""".lstrip() % (fake_v6,
fake_v6_cidr, fake_v6,
fake_v6_cidr, fake_v6)
tag:tag1,option6:dns-server,%s
tag:tag1,option6:domain-search,openstacklocal""".lstrip() % (
'[' + fake_v6 + ']')
with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn:
conf_fn.return_value = '/foo/opts'
@ -885,21 +922,6 @@ tag:tag0,option:router,192.168.0.1""".lstrip()
self.safe.assert_called_once_with('/foo/opts', expected)
def test_output_opts_file_single_dhcp_ver2_48(self):
expected = (
'tag0,option:dns-server,8.8.8.8\n'
'tag0,option:classless-static-route,20.0.0.1/24,20.0.0.1,'
'0.0.0.0/0,192.168.0.1\n'
'tag0,249,20.0.0.1/24,20.0.0.1,0.0.0.0/0,192.168.0.1\n'
'tag0,option:router,192.168.0.1').lstrip()
with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn:
conf_fn.return_value = '/foo/opts'
dm = dhcp.Dnsmasq(self.conf, FakeDualNetworkSingleDHCP(),
version=float(2.48))
dm._output_opts_file()
self.safe.assert_called_once_with('/foo/opts', expected)
def test_output_opts_file_no_gateway(self):
expected = """
tag:tag0,option:classless-static-route,169.254.169.254/32,192.168.1.1
@ -997,42 +1019,6 @@ tag:tag0,option:router""".lstrip()
self.safe.assert_called_once_with('/foo/opts', expected)
def test_output_opts_file_pxe_3port_1net_diff_details(self):
expected = (
'tag:tag0,option:dns-server,8.8.8.8\n'
'tag:tag0,option:classless-static-route,20.0.0.1/24,20.0.0.1,'
'0.0.0.0/0,192.168.0.1\n'
'tag:tag0,249,20.0.0.1/24,20.0.0.1,0.0.0.0/0,192.168.0.1\n'
'tag:tag0,option:router,192.168.0.1\n'
'tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,'
'option:tftp-server,192.168.0.3\n'
'tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,'
'option:server-ip-address,192.168.0.2\n'
'tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,'
'option:bootfile-name,pxelinux.0\n'
'tag:ffffffff-ffff-ffff-ffff-ffffffffffff,'
'option:tftp-server,192.168.0.5\n'
'tag:ffffffff-ffff-ffff-ffff-ffffffffffff,'
'option:server-ip-address,192.168.0.5\n'
'tag:ffffffff-ffff-ffff-ffff-ffffffffffff,'
'option:bootfile-name,pxelinux2.0\n'
'tag:44444444-4444-4444-4444-444444444444,'
'option:tftp-server,192.168.0.7\n'
'tag:44444444-4444-4444-4444-444444444444,'
'option:server-ip-address,192.168.0.7\n'
'tag:44444444-4444-4444-4444-444444444444,'
'option:bootfile-name,pxelinux3.0')
expected = expected.lstrip()
with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn:
conf_fn.return_value = '/foo/opts'
dm = dhcp.Dnsmasq(self.conf,
FakeV4NetworkPxe3Ports("portsDifferent"),
version=dhcp.Dnsmasq.MINIMUM_VERSION)
dm._output_opts_file()
self.safe.assert_called_once_with('/foo/opts', expected)
def test_output_opts_file_pxe_3port_2net(self):
expected = (
'tag:tag0,option:dns-server,8.8.8.8\n'
@ -1131,18 +1117,15 @@ tag:tag0,option:router""".lstrip()
).lstrip()
exp_opt_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/opts'
fake_v6 = '2001:0200:feed:7ac0::1'
fake_v6_cidr = '2001:0200:feed:7ac0::/64'
exp_opt_data = (
'tag:tag0,option:dns-server,8.8.8.8\n'
'tag:tag0,option:classless-static-route,20.0.0.1/24,20.0.0.1,'
'0.0.0.0/0,192.168.0.1\n'
'tag:tag0,249,20.0.0.1/24,20.0.0.1,0.0.0.0/0,192.168.0.1\n'
'tag:tag0,option:router,192.168.0.1\n'
'tag:tag1,option:dns-server,%s\n'
'tag:tag1,option:classless-static-route,%s,%s\n'
'tag:tag1,249,%s,%s').lstrip() % (fake_v6,
fake_v6_cidr, fake_v6,
fake_v6_cidr, fake_v6)
'tag:tag1,option6:dns-server,%s\n'
'tag:tag1,option6:domain-search,openstacklocal').lstrip() % (
'[' + fake_v6 + ']')
return (exp_host_name, exp_host_data,
exp_addn_name, exp_addn_data,
exp_opt_name, exp_opt_data,)
@ -1333,8 +1316,8 @@ tag:tag0,option:router""".lstrip()
float(2.65))
def test_check_fail_version(self):
self._check_version('Dnsmasq version 2.48 Copyright (c)...',
float(2.48))
with testtools.ExpectedException(SystemExit):
self._check_version('Dnsmasq version 2.62 Copyright (c)...', 0)
def test_check_version_failed_cmd_execution(self):
with testtools.ExpectedException(SystemExit):

Loading…
Cancel
Save