'--pid-file=%s' % self.get_conf_file_name(
'pid', ensure_conf_dir=True),
'--dhcp-hostsfile=%s' % self._output_hosts_file(),
+ '--addn-hosts=%s' % self._output_addn_hosts_file(),
'--dhcp-optsfile=%s' % self._output_opts_file(),
'--leasefile-ro',
]
self._release_unused_leases()
self._output_hosts_file()
+ self._output_addn_hosts_file()
self._output_opts_file()
if self.active:
cmd = ['kill', '-HUP', self.pid]
LOG.debug(_('Reloading allocations for network: %s'), self.network.id)
self.device_manager.update(self.network)
+ def _iter_hosts(self):
+ """Iterate over hosts.
+
+ For each host on the network we yield a tuple containing:
+ (
+ port, # a DictModel instance representing the port.
+ alloc, # a DictModel instance of the allocated ip and subnet.
+ host_name, # Host name.
+ name, # Host name and domain name in the format 'hostname.domain'.
+ )
+ """
+ for port in self.network.ports:
+ for alloc in port.fixed_ips:
+ hostname = 'host-%s' % alloc.ip_address.replace(
+ '.', '-').replace(':', '-')
+ fqdn = '%s.%s' % (hostname, self.conf.dhcp_domain)
+ yield (port, alloc, hostname, fqdn)
+
def _output_hosts_file(self):
- """Writes a dnsmasq compatible hosts file."""
- r = re.compile('[:.]')
+ """Writes a dnsmasq compatible dhcp hosts file.
+
+ The generated file is sent to the --dhcp-hostsfile option of dnsmasq,
+ and lists the hosts on the network which should receive a dhcp lease.
+ Each line in this file is in the form::
+
+ 'mac_address,FQDN,ip_address'
+
+ IMPORTANT NOTE: a dnsmasq instance does not resolve hosts defined in
+ this file if it did not give a lease to a host listed in it (e.g.:
+ multiple dnsmasq instances on the same network if this network is on
+ multiple network nodes). This file is only defining hosts which
+ should receive a dhcp lease, the hosts resolution in itself is
+ defined by the `_output_addn_hosts_file` method.
+ """
buf = six.StringIO()
filename = self.get_conf_file_name('host')
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.
+ ip_address = alloc.ip_address
+ if netaddr.valid_ipv6(ip_address):
+ ip_address = '[%s]' % ip_address
- for port in self.network.ports:
- for alloc in port.fixed_ips:
- name = 'host-%s.%s' % (r.sub('-', alloc.ip_address),
- self.conf.dhcp_domain)
- 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.
- ip_address = alloc.ip_address
- if netaddr.valid_ipv6(ip_address):
- ip_address = '[%s]' % ip_address
-
- LOG.debug(_('Adding %(mac)s : %(name)s : %(ip)s'),
- {"mac": port.mac_address, "name": name,
- "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))
- else:
- buf.write('%s,%s,%s\n' %
- (port.mac_address, name, ip_address))
+ LOG.debug(_('Adding %(mac)s : %(name)s : %(ip)s'),
+ {"mac": port.mac_address, "name": name,
+ "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))
+ else:
+ buf.write('%s,%s,%s\n' %
+ (port.mac_address, name, ip_address))
utils.replace_file(filename, buf.getvalue())
LOG.debug(_('Done building host file %s'), filename)
for ip, mac in old_leases - new_leases:
self._release_lease(mac, ip)
+ def _output_addn_hosts_file(self):
+ """Writes a dnsmasq compatible additional hosts file.
+
+ The generated file is sent to the --addn-hosts option of dnsmasq,
+ and lists the hosts on the network which should be resolved even if
+ the dnsmaq instance did not give a lease to the host (see the
+ `_output_hosts_file` method).
+ Each line in this file is in the same form as a standard /etc/hosts
+ file.
+ """
+ buf = six.StringIO()
+ for (port, alloc, hostname, fqdn) in self._iter_hosts():
+ # It is compulsory to write the `fqdn` before the `hostname` in
+ # order to obtain it in PTR responses.
+ buf.write('%s\t%s %s\n' % (alloc.ip_address, fqdn, hostname))
+ addn_hosts = self.get_conf_file_name('addn_hosts')
+ utils.replace_file(addn_hosts, buf.getvalue())
+ return addn_hosts
+
def _output_opts_file(self):
"""Write a dnsmasq compatible options file."""
'--except-interface=lo',
'--pid-file=/dhcp/%s/pid' % network.id,
'--dhcp-hostsfile=/dhcp/%s/host' % network.id,
+ '--addn-hosts=/dhcp/%s/addn_hosts' % network.id,
'--dhcp-optsfile=/dhcp/%s/opts' % network.id,
'--leasefile-ro']
self.safe.assert_called_once_with('/foo/opts', expected)
- def test_reload_allocations(self):
+ @property
+ def _test_reload_allocation_data(self):
exp_host_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/host'
exp_host_data = ('00:00:80:aa:bb:cc,host-192-168-0-2.openstacklocal,'
'192.168.0.2\n'
'openstacklocal,[fdca:3ba5:a17a:4ba3::3]\n'
'00:00:0f:rr:rr:rr,host-192-168-0-1.openstacklocal,'
'192.168.0.1\n').lstrip()
+ exp_addn_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/addn_hosts'
+ exp_addn_data = (
+ '192.168.0.2\t'
+ 'host-192-168-0-2.openstacklocal host-192-168-0-2\n'
+ 'fdca:3ba5:a17a:4ba3::2\t'
+ 'host-fdca-3ba5-a17a-4ba3--2.openstacklocal '
+ 'host-fdca-3ba5-a17a-4ba3--2\n'
+ '192.168.0.3\thost-192-168-0-3.openstacklocal '
+ 'host-192-168-0-3\n'
+ 'fdca:3ba5:a17a:4ba3::3\t'
+ 'host-fdca-3ba5-a17a-4ba3--3.openstacklocal '
+ 'host-fdca-3ba5-a17a-4ba3--3\n'
+ '192.168.0.1\t'
+ 'host-192-168-0-1.openstacklocal '
+ 'host-192-168-0-1\n'
+ ).lstrip()
exp_opt_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/opts'
- exp_opt_data = "tag:tag0,option:router,192.168.0.1"
fake_v6 = 'gdca:3ba5:a17a:4ba3::1'
fake_v6_cidr = 'gdca:3ba5:a17a:4ba3::/64'
exp_opt_data = """
tag:tag1,249,%s,%s""".lstrip() % (fake_v6,
fake_v6_cidr, fake_v6,
fake_v6_cidr, fake_v6)
+ return (exp_host_name, exp_host_data,
+ exp_addn_name, exp_addn_data,
+ exp_opt_name, exp_opt_data,)
+
+ def test_reload_allocations(self):
+ (exp_host_name, exp_host_data,
+ exp_addn_name, exp_addn_data,
+ exp_opt_name, exp_opt_data,) = self._test_reload_allocation_data
exp_args = ['kill', '-HUP', 5]
self.assertTrue(ip_map.called)
self.safe.assert_has_calls([mock.call(exp_host_name, exp_host_data),
+ mock.call(exp_addn_name, exp_addn_data),
mock.call(exp_opt_name, exp_opt_data)])
self.execute.assert_called_once_with(exp_args, 'sudo')
def test_reload_allocations_stale_pid(self):
- exp_host_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/host'
- exp_host_data = ('00:00:80:aa:bb:cc,host-192-168-0-2.openstacklocal,'
- '192.168.0.2\n'
- '00:00:f3:aa:bb:cc,host-fdca-3ba5-a17a-4ba3--2.'
- 'openstacklocal,[fdca:3ba5:a17a:4ba3::2]\n'
- '00:00:0f:aa:bb:cc,host-192-168-0-3.openstacklocal,'
- '192.168.0.3\n'
- '00:00:0f:aa:bb:cc,host-fdca-3ba5-a17a-4ba3--3.'
- 'openstacklocal,[fdca:3ba5:a17a:4ba3::3]\n'
- '00:00:0f:rr:rr:rr,host-192-168-0-1.openstacklocal,'
- '192.168.0.1\n').lstrip()
- exp_host_data.replace('\n', '')
- exp_opt_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/opts'
- exp_opt_data = "tag:tag0,option:router,192.168.0.1"
- fake_v6 = 'gdca:3ba5:a17a:4ba3::1'
- fake_v6_cidr = 'gdca:3ba5:a17a:4ba3::/64'
- exp_opt_data = """
-tag:tag0,option:dns-server,8.8.8.8
-tag:tag0,option:classless-static-route,20.0.0.1/24,20.0.0.1
-tag:tag0,249,20.0.0.1/24,20.0.0.1
-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)
+ (exp_host_name, exp_host_data,
+ exp_addn_name, exp_addn_data,
+ exp_opt_name, exp_opt_data,) = self._test_reload_allocation_data
with mock.patch('__builtin__.open') as mock_open:
mock_open.return_value.__enter__ = lambda s: s
dm.reload_allocations()
self.assertTrue(ipmap.called)
- self.safe.assert_has_calls([mock.call(exp_host_name,
- exp_host_data),
- mock.call(exp_opt_name, exp_opt_data)])
+ self.safe.assert_has_calls([
+ mock.call(exp_host_name, exp_host_data),
+ mock.call(exp_addn_name, exp_addn_data),
+ mock.call(exp_opt_name, exp_opt_data),
+ ])
mock_open.assert_called_once_with('/proc/5/cmdline', 'r')
def test_release_unused_leases(self):