31
30
from neutron.agent.linux import utils
32
31
from neutron.common import constants
33
32
from neutron.common import exceptions
33
from neutron.common import ipv6_utils
34
34
from neutron.common import utils as commonutils
35
from neutron.i18n import _LE
35
from neutron.i18n import _LE, _LI
36
36
from neutron.openstack.common import log as logging
37
37
from neutron.openstack.common import uuidutils
39
39
LOG = logging.getLogger(__name__)
42
cfg.StrOpt('dhcp_confs',
43
default='$state_path/dhcp',
44
help=_('Location to store DHCP server config files')),
45
cfg.StrOpt('dhcp_domain',
46
default='openstacklocal',
47
help=_('Domain to use for building the hostnames')),
48
cfg.StrOpt('dnsmasq_config_file',
50
help=_('Override the default dnsmasq settings with this file')),
51
cfg.ListOpt('dnsmasq_dns_servers',
52
help=_('Comma-separated list of the DNS servers which will be '
53
'used as forwarders.'),
54
deprecated_name='dnsmasq_dns_server'),
55
cfg.BoolOpt('dhcp_delete_namespaces', default=False,
56
help=_("Delete namespace after removing a dhcp server.")),
60
help=_('Limit number of leases to prevent a denial-of-service.')),
61
cfg.BoolOpt('dhcp_broadcast_reply', default=False,
62
help=_("Use broadcast in DHCP replies")),
136
113
@six.add_metaclass(abc.ABCMeta)
137
114
class DhcpBase(object):
139
def __init__(self, conf, network, root_helper='sudo',
116
def __init__(self, conf, network, process_monitor, root_helper='sudo',
140
117
version=None, plugin=None):
142
119
self.network = network
143
120
self.root_helper = root_helper
121
self.process_monitor = process_monitor
144
122
self.device_manager = DeviceManager(self.conf,
145
123
self.root_helper, plugin)
146
124
self.version = version
192
170
class DhcpLocalProcess(DhcpBase):
173
def __init__(self, conf, network, process_monitor, root_helper='sudo',
174
version=None, plugin=None):
175
super(DhcpLocalProcess, self).__init__(conf, network, process_monitor,
176
root_helper, version, plugin)
177
self.confs_dir = self.get_confs_dir(conf)
178
self.network_conf_dir = os.path.join(self.confs_dir, network.id)
179
self._ensure_network_conf_dir()
182
def get_confs_dir(conf):
183
return os.path.abspath(os.path.normpath(conf.dhcp_confs))
185
def get_conf_file_name(self, kind):
186
"""Returns the file name for a given kind of config file."""
187
return os.path.join(self.network_conf_dir, kind)
189
def _ensure_network_conf_dir(self):
190
"""Ensures the directory for configuration files is created."""
191
if not os.path.isdir(self.network_conf_dir):
192
os.makedirs(self.network_conf_dir, 0o755)
194
def _remove_config_files(self):
195
shutil.rmtree(self.network_conf_dir, ignore_errors=True)
195
197
def _enable_dhcp(self):
196
198
"""check if there is a subnet within the network with dhcp enabled."""
197
199
for subnet in self.network.subnets:
206
208
elif self._enable_dhcp():
209
self._ensure_network_conf_dir()
207
210
interface_name = self.device_manager.setup(self.network)
208
211
self.interface_name = interface_name
209
212
self.spawn_process()
211
214
def disable(self, retain_port=False):
212
215
"""Disable DHCP for this network by killing the local process."""
217
cmd = ['kill', '-9', pid]
218
utils.execute(cmd, self.root_helper)
220
LOG.debug('DHCP for %(net_id)s is stale, pid %(pid)d '
221
'does not exist, performing cleanup',
222
{'net_id': self.network.id, 'pid': pid})
224
self.device_manager.destroy(self.network,
227
LOG.debug('No DHCP started for %s', self.network.id)
216
pid_filename = self.get_conf_file_name('pid')
218
pid = self.process_monitor.get_pid(uuid=self.network.id,
219
service=DNSMASQ_SERVICE_NAME,
220
pid_file=pid_filename)
222
self.process_monitor.disable(uuid=self.network.id,
223
namespace=self.network.namespace,
224
service=DNSMASQ_SERVICE_NAME,
225
pid_file=pid_filename)
226
if pid and not retain_port:
227
self.device_manager.destroy(self.network, self.interface_name)
229
229
self._remove_config_files()
238
238
LOG.exception(_LE('Failed trying to delete namespace: %s'),
239
239
self.network.namespace)
241
def _remove_config_files(self):
242
confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs))
243
conf_dir = os.path.join(confs_dir, self.network.id)
244
shutil.rmtree(conf_dir, ignore_errors=True)
246
def get_conf_file_name(self, kind, ensure_conf_dir=False):
247
"""Returns the file name for a given kind of config file."""
248
confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs))
249
conf_dir = os.path.join(confs_dir, self.network.id)
251
if not os.path.isdir(conf_dir):
252
os.makedirs(conf_dir, 0o755)
254
return os.path.join(conf_dir, kind)
256
241
def _get_value_from_conf_file(self, kind, converter=None):
257
242
"""A helper function to read a value from one of the state files."""
258
243
file_name = self.get_conf_file_name(kind)
268
253
msg = _('Unable to access %s')
270
LOG.debug(msg % file_name)
255
LOG.debug(msg, file_name)
275
"""Last known pid for the DHCP process spawned for this network."""
276
return self._get_value_from_conf_file('pid', int)
284
cmdline = '/proc/%s/cmdline' % pid
286
with open(cmdline, "r") as f:
287
return self.network.id in f.readline()
292
259
def interface_name(self):
293
260
return self._get_value_from_conf_file('interface')
295
262
@interface_name.setter
296
263
def interface_name(self, value):
297
interface_file_path = self.get_conf_file_name('interface',
298
ensure_conf_dir=True)
264
interface_file_path = self.get_conf_file_name('interface')
299
265
utils.replace_file(interface_file_path, value)
269
pid_filename = self.get_conf_file_name('pid')
270
return self.process_monitor.is_active(self.network.id,
271
DNSMASQ_SERVICE_NAME,
272
pid_file=pid_filename)
301
274
@abc.abstractmethod
302
275
def spawn_process(self):
316
289
NEUTRON_NETWORK_ID_KEY = 'NEUTRON_NETWORK_ID'
317
290
NEUTRON_RELAY_SOCKET_PATH_KEY = 'NEUTRON_RELAY_SOCKET_PATH'
318
MINIMUM_VERSION = 2.63
321
293
def check_version(cls):
324
cmd = ['dnsmasq', '--version']
325
out = utils.execute(cmd)
326
ver = re.findall("\d+.\d+", out)[0]
327
is_valid_version = float(ver) >= cls.MINIMUM_VERSION
328
if not is_valid_version:
329
LOG.error(_LE('FAILED VERSION REQUIREMENT FOR DNSMASQ. '
330
'DHCP AGENT MAY NOT RUN CORRECTLY! '
331
'Please ensure that its version is %s '
332
'or above!'), cls.MINIMUM_VERSION)
334
except (OSError, RuntimeError, IndexError, ValueError):
335
LOG.error(_LE('Unable to determine dnsmasq version. '
336
'Please ensure that its version is %s '
337
'or above!'), cls.MINIMUM_VERSION)
342
297
def existing_dhcp_networks(cls, conf, root_helper):
343
298
"""Return a list of existing networks ids that we have configs for."""
345
confs_dir = os.path.abspath(os.path.normpath(conf.dhcp_confs))
348
c for c in os.listdir(confs_dir)
349
if uuidutils.is_uuid_like(c)
352
def spawn_process(self):
353
"""Spawns a Dnsmasq process for the network."""
355
self.NEUTRON_NETWORK_ID_KEY: self.network.id,
299
confs_dir = cls.get_confs_dir(conf)
302
c for c in os.listdir(confs_dir)
303
if uuidutils.is_uuid_like(c)
308
def _build_cmdline_callback(self, pid_file):
363
314
'--bind-interfaces',
364
315
'--interface=%s' % self.interface_name,
365
316
'--except-interface=lo',
366
'--pid-file=%s' % self.get_conf_file_name(
367
'pid', ensure_conf_dir=True),
368
'--dhcp-hostsfile=%s' % self._output_hosts_file(),
369
'--addn-hosts=%s' % self._output_addn_hosts_file(),
370
'--dhcp-optsfile=%s' % self._output_opts_file(),
317
'--pid-file=%s' % pid_file,
318
'--dhcp-hostsfile=%s' % self.get_conf_file_name('host'),
319
'--addn-hosts=%s' % self.get_conf_file_name('addn_hosts'),
320
'--dhcp-optsfile=%s' % self.get_conf_file_name('opts'),
371
321
'--leasefile-ro',
322
'--dhcp-authoritative',
374
325
possible_leases = 0
399
350
# mode is optional and is not set - skip it
401
cmd.append('--dhcp-range=%s%s,%s,%s,%s' %
402
('set:', self._TAG_PREFIX % i,
403
cidr.network, mode, lease))
352
if subnet.ip_version == 4:
353
cmd.append('--dhcp-range=%s%s,%s,%s,%s' %
354
('set:', self._TAG_PREFIX % i,
355
cidr.network, mode, lease))
357
cmd.append('--dhcp-range=%s%s,%s,%s,%d,%s' %
358
('set:', self._TAG_PREFIX % i,
360
cidr.prefixlen, lease))
404
361
possible_leases += cidr.size
406
363
# Cap the limit because creating lots of subnets can inflate
420
377
if self.conf.dhcp_broadcast_reply:
421
378
cmd.append('--dhcp-broadcast')
423
ip_wrapper = ip_lib.IPWrapper(self.root_helper,
424
self.network.namespace)
425
ip_wrapper.netns.execute(cmd, addl_env=env)
382
def spawn_process(self):
383
"""Spawn the process, if it's not spawned already."""
384
self._spawn_or_reload_process(reload_with_HUP=False)
386
def _spawn_or_reload_process(self, reload_with_HUP):
387
"""Spawns or reloads a Dnsmasq process for the network.
389
When reload_with_HUP is True, dnsmasq receives a HUP signal,
390
or it's reloaded if the process is not running.
393
self._output_config_files()
395
pid_filename = self.get_conf_file_name('pid')
397
self.process_monitor.enable(
398
uuid=self.network.id,
399
cmd_callback=self._build_cmdline_callback,
400
namespace=self.network.namespace,
401
service=DNSMASQ_SERVICE_NAME,
402
cmd_addl_env={self.NEUTRON_NETWORK_ID_KEY: self.network.id},
403
reload_cfg=reload_with_HUP,
404
pid_file=pid_filename)
427
406
def _release_lease(self, mac_address, ip):
428
407
"""Release a DHCP lease."""
431
410
self.network.namespace)
432
411
ip_wrapper.netns.execute(cmd)
413
def _output_config_files(self):
414
self._output_hosts_file()
415
self._output_addn_hosts_file()
416
self._output_opts_file()
434
418
def reload_allocations(self):
435
419
"""Rebuild the dnsmasq config and signal the dnsmasq to reload."""
444
428
self._release_unused_leases()
445
self._output_hosts_file()
446
self._output_addn_hosts_file()
447
self._output_opts_file()
449
cmd = ['kill', '-HUP', self.pid]
450
utils.execute(cmd, self.root_helper)
452
LOG.debug('Pid %d is stale, relaunching dnsmasq', self.pid)
429
self._spawn_or_reload_process(reload_with_HUP=True)
453
430
LOG.debug('Reloading allocations for network: %s', self.network.id)
454
431
self.device_manager.update(self.network, self.interface_name)
461
438
port, # a DictModel instance representing the port.
462
439
alloc, # a DictModel instance of the allocated ip and subnet.
440
# if alloc is None, it means there is no need to allocate
441
# an IPv6 address because of stateless DHCPv6 network.
463
442
host_name, # Host name.
464
443
name, # Canonical hostname in the format 'hostname[.domain]'.
474
453
if alloc.subnet_id in v6_nets:
475
454
addr_mode = v6_nets[alloc.subnet_id].ipv6_address_mode
476
if addr_mode != constants.DHCPV6_STATEFUL:
455
if addr_mode == constants.IPV6_SLAAC:
457
elif addr_mode == constants.DHCPV6_STATELESS:
458
alloc = hostname = fqdn = None
459
yield (port, alloc, hostname, fqdn)
478
462
hostname = 'host-%s' % alloc.ip_address.replace(
479
463
'.', '-').replace(':', '-')
502
486
filename = self.get_conf_file_name('host')
504
488
LOG.debug('Building host file: %s', filename)
489
dhcp_enabled_subnet_ids = [s.id for s in self.network.subnets
505
491
for (port, alloc, hostname, name) in self._iter_hosts():
493
if getattr(port, 'extra_dhcp_opts', False):
494
buf.write('%s,%s%s\n' %
495
(port.mac_address, 'set:', port.id))
496
LOG.debug('Adding %(mac)s : set:%(tag)s',
497
{"mac": port.mac_address, "tag": port.id})
500
# don't write ip address which belongs to a dhcp disabled subnet.
501
if alloc.subnet_id not in dhcp_enabled_subnet_ids:
506
504
# (dzyu) Check if it is legal ipv6 address, if so, need wrap
507
505
# it with '[]' to let dnsmasq to distinguish MAC address from
510
508
if netaddr.valid_ipv6(ip_address):
511
509
ip_address = '[%s]' % ip_address
513
LOG.debug('Adding %(mac)s : %(name)s : %(ip)s',
514
{"mac": port.mac_address, "name": name,
517
511
if getattr(port, 'extra_dhcp_opts', False):
518
512
buf.write('%s,%s,%s,%s%s\n' %
519
513
(port.mac_address, name, ip_address,
520
514
'set:', port.id))
515
LOG.debug('Adding %(mac)s : %(name)s : %(ip)s : '
517
{"mac": port.mac_address, "name": name,
518
"ip": ip_address, "tag": port.id})
522
520
buf.write('%s,%s,%s\n' %
523
521
(port.mac_address, name, ip_address))
522
LOG.debug('Adding %(mac)s : %(name)s : %(ip)s',
523
{"mac": port.mac_address, "name": name,
525
526
utils.replace_file(filename, buf.getvalue())
526
527
LOG.debug('Done building host file %s', filename)
561
562
for (port, alloc, hostname, fqdn) in self._iter_hosts():
562
563
# It is compulsory to write the `fqdn` before the `hostname` in
563
564
# order to obtain it in PTR responses.
564
buf.write('%s\t%s %s\n' % (alloc.ip_address, fqdn, hostname))
566
buf.write('%s\t%s %s\n' % (alloc.ip_address, fqdn, hostname))
565
567
addn_hosts = self.get_conf_file_name('addn_hosts')
566
568
utils.replace_file(addn_hosts, buf.getvalue())
567
569
return addn_hosts
569
571
def _output_opts_file(self):
570
572
"""Write a dnsmasq compatible options file."""
573
options, subnet_index_map = self._generate_opts_per_subnet()
574
options += self._generate_opts_per_port(subnet_index_map)
576
name = self.get_conf_file_name('opts')
577
utils.replace_file(name, '\n'.join(options))
580
def _generate_opts_per_subnet(self):
582
subnet_index_map = {}
572
583
if self.conf.enable_isolated_metadata:
573
584
subnet_to_interface_ip = self._make_subnet_interface_ip_map()
577
585
isolated_subnets = self.get_isolated_subnets(self.network)
578
dhcp_ips = collections.defaultdict(list)
580
586
for i, subnet in enumerate(self.network.subnets):
581
587
if (not subnet.enable_dhcp or
582
588
(subnet.ip_version == 6 and
594
600
# use the dnsmasq ip as nameservers only if there is no
595
601
# dns-server submitted by the server
596
subnet_idx_map[subnet.id] = i
602
subnet_index_map[subnet.id] = i
598
604
if self.conf.dhcp_domain and subnet.ip_version == 6:
599
605
options.append('tag:tag%s,option6:domain-search,%s' %
639
645
options.append(self._format_option(subnet.ip_version,
647
return options, subnet_index_map
649
def _generate_opts_per_port(self, subnet_index_map):
651
dhcp_ips = collections.defaultdict(list)
642
652
for port in self.network.ports:
643
653
if getattr(port, 'extra_dhcp_opts', False):
644
for ip_version in (4, 6):
646
netaddr.IPAddress(ip.ip_address).version == ip_version
647
for ip in port.fixed_ips):
649
# TODO(xuhanp):Instead of applying extra_dhcp_opts
650
# to both DHCPv4 and DHCPv6, we need to find a new
651
# way to specify options for v4 and v6
652
# respectively. We also need to validate the option
653
# before applying it.
654
self._format_option(ip_version, port.id,
655
opt.opt_name, opt.opt_value)
656
for opt in port.extra_dhcp_opts)
654
port_ip_versions = set(
655
[netaddr.IPAddress(ip.ip_address).version
656
for ip in port.fixed_ips])
657
for opt in port.extra_dhcp_opts:
658
opt_ip_version = opt.ip_version
659
if opt_ip_version in port_ip_versions:
661
self._format_option(opt_ip_version, port.id,
662
opt.opt_name, opt.opt_value))
664
LOG.info(_LI("Cannot apply dhcp option %(opt)s "
665
"because it's ip_version %(version)d "
666
"is not in port's address IP versions"),
667
{'opt': opt.opt_name,
668
'version': opt_ip_version})
658
670
# provides all dnsmasq ip as dns-server if there is more than
659
671
# one dnsmasq for a subnet and there is no dns-server submitted
661
673
if port.device_owner == constants.DEVICE_OWNER_DHCP:
662
674
for ip in port.fixed_ips:
663
i = subnet_idx_map.get(ip.subnet_id)
675
i = subnet_index_map.get(ip.subnet_id)
666
678
dhcp_ips[i].append(ip.ip_address)
677
689
Dnsmasq._convert_to_literal_addrs(ip_version,
680
name = self.get_conf_file_name('opts')
681
utils.replace_file(name, '\n'.join(options))
684
693
def _make_subnet_interface_ip_map(self):
685
694
ip_dev = ip_lib.IPDevice(
706
715
def _format_option(self, ip_version, tag, option, *args):
707
716
"""Format DHCP option by option name or code."""
708
717
option = str(option)
718
pattern = "(tag:(.*),)?(.*)$"
719
matches = re.match(pattern, option)
720
extra_tag = matches.groups()[0]
721
option = matches.groups()[2]
710
723
if isinstance(tag, int):
711
724
tag = self._TAG_PREFIX % tag
715
728
option = 'option:%s' % option
717
730
option = 'option6:%s' % option
719
return ','.join(('tag:' + tag, '%s' % option) + args)
732
tags = ('tag:' + tag, extra_tag[:-1], '%s' % option)
734
tags = ('tag:' + tag, '%s' % option)
735
return ','.join(tags + args)
722
738
def _convert_to_literal_addrs(ip_version, ips):
736
752
subnets = dict((subnet.id, subnet) for subnet in network.subnets)
738
754
for port in network.ports:
739
if port.device_owner not in (constants.DEVICE_OWNER_ROUTER_INTF,
740
constants.DEVICE_OWNER_DVR_INTERFACE):
755
if port.device_owner not in constants.ROUTER_INTERFACE_OWNERS:
742
757
for alloc in port.fixed_ips:
743
758
if subnets[alloc.subnet_id].gateway_ip == alloc.ip_address:
966
981
for fixed_ip in port.fixed_ips:
967
982
subnet = fixed_ip.subnet
968
net = netaddr.IPNetwork(subnet.cidr)
969
ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
970
ip_cidrs.append(ip_cidr)
983
if not ipv6_utils.is_auto_address_subnet(subnet):
984
net = netaddr.IPNetwork(subnet.cidr)
985
ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
986
ip_cidrs.append(ip_cidr)
972
988
if (self.conf.enable_isolated_metadata and
973
989
self.conf.use_namespaces):