branch | s11u2-sru |
changeset 4156 | 4b1def16fe9b |
parent 3619 | 639868f63ef4 |
4146:097063f324c0 | 4156:4b1def16fe9b |
---|---|
18 # under the License. |
18 # under the License. |
19 # |
19 # |
20 # @author: Girish Moodalbail, Oracle, Inc. |
20 # @author: Girish Moodalbail, Oracle, Inc. |
21 |
21 |
22 import abc |
22 import abc |
23 import collections |
|
23 import os |
24 import os |
24 import re |
25 import re |
25 import shutil |
26 import shutil |
26 import socket |
27 import socket |
27 import StringIO |
|
28 import sys |
28 import sys |
29 import uuid |
29 import uuid |
30 |
30 |
31 import netaddr |
31 import netaddr |
32 from oslo.config import cfg |
32 from oslo.config import cfg |
33 import six |
|
33 |
34 |
34 from neutron.agent.linux import utils |
35 from neutron.agent.linux import utils |
35 from neutron.agent.solaris import net_lib |
36 from neutron.agent.solaris import net_lib |
36 from neutron.common import constants |
37 from neutron.common import constants |
37 from neutron.common import exceptions |
38 from neutron.common import exceptions |
50 default='openstacklocal', |
51 default='openstacklocal', |
51 help=_('Domain to use for building the hostnames')), |
52 help=_('Domain to use for building the hostnames')), |
52 cfg.StrOpt('dnsmasq_config_file', |
53 cfg.StrOpt('dnsmasq_config_file', |
53 default='', |
54 default='', |
54 help=_('Override the default dnsmasq settings with this file')), |
55 help=_('Override the default dnsmasq settings with this file')), |
55 cfg.StrOpt('dnsmasq_dns_server', |
56 cfg.ListOpt('dnsmasq_dns_servers', |
56 help=_('Use another DNS server before any in ' |
57 help=_('Comma-separated list of the DNS servers which will be ' |
57 '/etc/resolv.conf.')), |
58 'used as forwarders.'), |
59 deprecated_name='dnsmasq_dns_server'), |
|
60 cfg.BoolOpt('dhcp_delete_namespaces', default=False, |
|
61 help=_("Delete namespace after removing a dhcp server.")), |
|
58 cfg.IntOpt( |
62 cfg.IntOpt( |
59 'dnsmasq_lease_max', |
63 'dnsmasq_lease_max', |
60 default=(2 ** 24), |
64 default=(2 ** 24), |
61 help=_('Limit number of leases to prevent a denial-of-service.')), |
65 help=_('Limit number of leases to prevent a denial-of-service.')), |
62 cfg.StrOpt('interface_driver', |
|
63 help=_("The driver used to manage the virtual interface.")), |
|
64 ] |
66 ] |
65 |
67 |
66 IPV4 = 4 |
68 IPV4 = 4 |
67 IPV6 = 6 |
69 IPV6 = 6 |
68 UDP = 'udp' |
70 UDP = 'udp' |
76 METADATA_DEFAULT_PREFIX) |
78 METADATA_DEFAULT_PREFIX) |
77 METADATA_PORT = 80 |
79 METADATA_PORT = 80 |
78 WIN2k3_STATIC_DNS = 249 |
80 WIN2k3_STATIC_DNS = 249 |
79 |
81 |
80 |
82 |
81 class DictModel(object): |
83 class DictModel(dict): |
82 """Convert dict into an object that provides attribute access to values.""" |
84 """Convert dict into an object that provides attribute access to values.""" |
83 def __init__(self, d): |
85 |
84 for key, value in d.iteritems(): |
86 def __init__(self, *args, **kwargs): |
85 if isinstance(value, list): |
87 """Convert dict values to DictModel values.""" |
86 value = [DictModel(item) if isinstance(item, dict) else item |
88 super(DictModel, self).__init__(*args, **kwargs) |
87 for item in value] |
89 |
88 elif isinstance(value, dict): |
90 def needs_upgrade(item): |
89 value = DictModel(value) |
91 """Check if `item` is a dict and needs to be changed to DictModel. |
90 |
92 """ |
91 setattr(self, key, value) |
93 return isinstance(item, dict) and not isinstance(item, DictModel) |
94 |
|
95 def upgrade(item): |
|
96 """Upgrade item if it needs to be upgraded.""" |
|
97 if needs_upgrade(item): |
|
98 return DictModel(item) |
|
99 else: |
|
100 return item |
|
101 |
|
102 for key, value in self.iteritems(): |
|
103 if isinstance(value, (list, tuple)): |
|
104 # Keep the same type but convert dicts to DictModels |
|
105 self[key] = type(value)( |
|
106 (upgrade(item) for item in value) |
|
107 ) |
|
108 elif needs_upgrade(value): |
|
109 # Change dict instance values to DictModel instance values |
|
110 self[key] = DictModel(value) |
|
111 |
|
112 def __getattr__(self, name): |
|
113 try: |
|
114 return self[name] |
|
115 except KeyError as e: |
|
116 raise AttributeError(e) |
|
117 |
|
118 def __setattr__(self, name, value): |
|
119 self[name] = value |
|
120 |
|
121 def __delattr__(self, name): |
|
122 del self[name] |
|
92 |
123 |
93 |
124 |
94 class NetModel(DictModel): |
125 class NetModel(DictModel): |
95 |
126 |
96 def __init__(self, use_namespaces, d): |
127 def __init__(self, use_namespaces, d): |
101 @property |
132 @property |
102 def namespace(self): |
133 def namespace(self): |
103 return self._ns_name |
134 return self._ns_name |
104 |
135 |
105 |
136 |
137 @six.add_metaclass(abc.ABCMeta) |
|
106 class DhcpBase(object): |
138 class DhcpBase(object): |
107 __metaclass__ = abc.ABCMeta |
|
108 |
139 |
109 def __init__(self, conf, network, root_helper='sudo', |
140 def __init__(self, conf, network, root_helper='sudo', |
110 version=None, plugin=None): |
141 version=None, plugin=None): |
111 self.conf = conf |
142 self.conf = conf |
112 self.network = network |
143 self.network = network |
125 |
156 |
126 def restart(self): |
157 def restart(self): |
127 """Restart the dhcp service for the network.""" |
158 """Restart the dhcp service for the network.""" |
128 self.disable(retain_port=True) |
159 self.disable(retain_port=True) |
129 self.enable() |
160 self.enable() |
130 self.device_manager.update(self.network) |
|
131 |
161 |
132 @abc.abstractproperty |
162 @abc.abstractproperty |
133 def active(self): |
163 def active(self): |
134 """Boolean representing the running state of the DHCP server.""" |
164 """Boolean representing the running state of the DHCP server.""" |
135 |
165 |
136 @abc.abstractmethod |
166 @abc.abstractmethod |
137 def release_lease(self, mac_address, removed_ips): |
|
138 """Release a DHCP lease.""" |
|
139 |
|
140 @abc.abstractmethod |
|
141 def reload_allocations(self): |
167 def reload_allocations(self): |
142 """Force the DHCP server to reload the assignment database.""" |
168 """Force the DHCP server to reload the assignment database.""" |
143 |
169 |
144 @classmethod |
170 @classmethod |
145 def existing_dhcp_networks(cls, conf, root_helper): |
171 def existing_dhcp_networks(cls, conf, root_helper): |
146 """Return a list of existing networks ids that we have configs for.""" |
172 """Return a list of existing networks ids that we have configs for.""" |
147 |
173 |
148 raise NotImplementedError |
174 raise NotImplementedError() |
149 |
175 |
150 @classmethod |
176 @classmethod |
151 def check_version(cls): |
177 def check_version(cls): |
152 """Execute version checks on DHCP server.""" |
178 """Execute version checks on DHCP server.""" |
153 |
179 |
154 raise NotImplementedError |
180 raise NotImplementedError() |
181 |
|
182 @classmethod |
|
183 def get_isolated_subnets(cls, network): |
|
184 """Returns a dict indicating whether or not a subnet is isolated""" |
|
185 raise NotImplementedError() |
|
186 |
|
187 @classmethod |
|
188 def should_enable_metadata(cls, conf, network): |
|
189 """True if the metadata-proxy should be enabled for the network.""" |
|
190 raise NotImplementedError() |
|
155 |
191 |
156 |
192 |
157 class DhcpLocalProcess(DhcpBase): |
193 class DhcpLocalProcess(DhcpBase): |
158 PORTS = [] |
194 PORTS = [] |
159 |
195 |
164 return True |
200 return True |
165 return False |
201 return False |
166 |
202 |
167 def enable(self): |
203 def enable(self): |
168 """Enables DHCP for this network by spawning a local process.""" |
204 """Enables DHCP for this network by spawning a local process.""" |
169 interface_name = self.device_manager.setup(self.network, |
|
170 reuse_existing=True) |
|
171 if self.active: |
205 if self.active: |
172 self.restart() |
206 self.restart() |
173 elif self._enable_dhcp(): |
207 elif self._enable_dhcp(): |
208 interface_name = self.device_manager.setup(self.network) |
|
174 self.interface_name = interface_name |
209 self.interface_name = interface_name |
175 self.spawn_process() |
210 self.spawn_process() |
176 |
211 |
177 def disable(self, retain_port=False): |
212 def disable(self, retain_port=False): |
178 """Disable DHCP for this network by killing the local process.""" |
213 """Disable DHCP for this network by killing the local process.""" |
179 pid = self.pid |
214 pid = self.pid |
180 |
215 |
181 if self.active: |
216 if pid: |
182 cmd = ['kill', '-9', pid] |
217 if self.active: |
183 utils.execute(cmd, self.root_helper) |
218 cmd = ['kill', '-9', pid] |
219 utils.execute(cmd, self.root_helper) |
|
220 else: |
|
221 LOG.debug(_('DHCP for %(net_id)s is stale, pid %(pid)d ' |
|
222 'does not exist, performing cleanup'), |
|
223 {'net_id': self.network.id, 'pid': pid}) |
|
184 if not retain_port: |
224 if not retain_port: |
185 self.device_manager.destroy(self.network, self.interface_name) |
225 self.device_manager.destroy(self.network, |
186 |
226 self.interface_name) |
187 elif pid: |
|
188 LOG.debug(_('DHCP for %(net_id)s pid %(pid)d is stale, ignoring ' |
|
189 'command'), {'net_id': self.network.id, 'pid': pid}) |
|
190 else: |
227 else: |
191 LOG.debug(_('No DHCP started for %s'), self.network.id) |
228 LOG.debug(_('No DHCP started for %s'), self.network.id) |
192 |
229 |
193 self._remove_config_files() |
230 self._remove_config_files() |
194 |
231 |
266 |
303 |
267 _TAG_PREFIX = 'tag%d' |
304 _TAG_PREFIX = 'tag%d' |
268 |
305 |
269 NEUTRON_NETWORK_ID_KEY = 'NEUTRON_NETWORK_ID' |
306 NEUTRON_NETWORK_ID_KEY = 'NEUTRON_NETWORK_ID' |
270 NEUTRON_RELAY_SOCKET_PATH_KEY = 'NEUTRON_RELAY_SOCKET_PATH' |
307 NEUTRON_RELAY_SOCKET_PATH_KEY = 'NEUTRON_RELAY_SOCKET_PATH' |
271 MINIMUM_VERSION = 2.59 |
308 MINIMUM_VERSION = 2.63 |
309 MINIMUM_IPV6_VERSION = 2.67 |
|
272 |
310 |
273 @classmethod |
311 @classmethod |
274 def check_version(cls): |
312 def check_version(cls): |
275 ver = 0 |
313 ver = 0 |
276 try: |
314 try: |
283 if not is_valid_version: |
321 if not is_valid_version: |
284 LOG.warning(_('FAILED VERSION REQUIREMENT FOR DNSMASQ. ' |
322 LOG.warning(_('FAILED VERSION REQUIREMENT FOR DNSMASQ. ' |
285 'DHCP AGENT MAY NOT RUN CORRECTLY! ' |
323 'DHCP AGENT MAY NOT RUN CORRECTLY! ' |
286 'Please ensure that its version is %s ' |
324 'Please ensure that its version is %s ' |
287 'or above!'), cls.MINIMUM_VERSION) |
325 'or above!'), cls.MINIMUM_VERSION) |
326 raise SystemExit(1) |
|
327 is_valid_version = float(ver) >= cls.MINIMUM_IPV6_VERSION |
|
328 if not is_valid_version: |
|
329 LOG.warning(_('FAILED VERSION REQUIREMENT FOR DNSMASQ. ' |
|
330 'DHCP AGENT MAY NOT RUN CORRECTLY WHEN ' |
|
331 'SERVING IPV6 STATEFUL SUBNETS! ' |
|
332 'Please ensure that its version is %s ' |
|
333 'or above!'), cls.MINIMUM_IPV6_VERSION) |
|
288 except (OSError, RuntimeError, IndexError, ValueError): |
334 except (OSError, RuntimeError, IndexError, ValueError): |
289 LOG.warning(_('Unable to determine dnsmasq version. ' |
335 LOG.warning(_('Unable to determine dnsmasq version. ' |
290 'Please ensure that its version is %s ' |
336 'Please ensure that its version is %s ' |
291 'or above!'), cls.MINIMUM_VERSION) |
337 'or above!'), cls.MINIMUM_VERSION) |
338 raise SystemExit(1) |
|
292 return float(ver) |
339 return float(ver) |
293 |
340 |
294 @classmethod |
341 @classmethod |
295 def existing_dhcp_networks(cls, conf, root_helper): |
342 def existing_dhcp_networks(cls, conf, root_helper): |
296 """Return a list of existing networks ids that we have configs for.""" |
343 """Return a list of existing networks ids that we have configs for.""" |
317 '--interface=%s' % self.interface_name, |
364 '--interface=%s' % self.interface_name, |
318 '--except-interface=lo0', |
365 '--except-interface=lo0', |
319 '--pid-file=%s' % self.get_conf_file_name( |
366 '--pid-file=%s' % self.get_conf_file_name( |
320 'pid', ensure_conf_dir=True), |
367 'pid', ensure_conf_dir=True), |
321 '--dhcp-hostsfile=%s' % self._output_hosts_file(), |
368 '--dhcp-hostsfile=%s' % self._output_hosts_file(), |
369 '--addn-hosts=%s' % self._output_addn_hosts_file(), |
|
322 '--dhcp-optsfile=%s' % self._output_opts_file(), |
370 '--dhcp-optsfile=%s' % self._output_opts_file(), |
323 '--leasefile-ro', |
371 '--leasefile-ro', |
324 ] |
372 ] |
325 |
373 |
326 possible_leases = 0 |
374 possible_leases = 0 |
327 for i, subnet in enumerate(self.network.subnets): |
375 for i, subnet in enumerate(self.network.subnets): |
376 mode = None |
|
328 # if a subnet is specified to have dhcp disabled |
377 # if a subnet is specified to have dhcp disabled |
329 if not subnet.enable_dhcp: |
378 if not subnet.enable_dhcp: |
330 continue |
379 continue |
331 if subnet.ip_version == 4: |
380 if subnet.ip_version == 4: |
332 mode = 'static' |
381 mode = 'static' |
333 else: |
382 else: |
334 # TODO(gmoodalb): how do we indicate other options |
|
335 # ra-only, slaac, ra-nameservers, and ra-stateless. |
|
336 # We need to also set the DUID for the DHCPv6 server to use |
383 # We need to also set the DUID for the DHCPv6 server to use |
337 macaddr_cmd = ['/usr/sbin/dladm', 'show-linkprop', |
384 macaddr_cmd = ['/usr/sbin/dladm', 'show-linkprop', |
338 '-co', 'value', '-p', 'mac-address', |
385 '-co', 'value', '-p', 'mac-address', |
339 self.interface_name] |
386 self.interface_name] |
340 stdout = utils.execute(macaddr_cmd) |
387 stdout = utils.execute(macaddr_cmd) |
341 uid = stdout.splitlines()[0].strip() |
388 uid = stdout.splitlines()[0].strip() |
389 # format the MAC address |
|
390 uid = ':'.join(['%.2x' % w for w in netaddr.EUI(uid).words]) |
|
342 # IANA assigned ID for Oracle |
391 # IANA assigned ID for Oracle |
343 enterprise_id = '111' |
392 enterprise_id = '111' |
344 cmd.append('--dhcp-duid=%s,%s' % (enterprise_id, uid)) |
393 cmd.append('--dhcp-duid=%s,%s' % (enterprise_id, uid)) |
345 mode = 'static' |
394 |
346 if self.version >= self.MINIMUM_VERSION: |
395 # Note(scollins) If the IPv6 attributes are not set, set it as |
347 set_tag = 'set:' |
396 # static to preserve previous behavior |
397 addr_mode = getattr(subnet, 'ipv6_address_mode', None) |
|
398 ra_mode = getattr(subnet, 'ipv6_ra_mode', None) |
|
399 if (addr_mode in [constants.DHCPV6_STATEFUL, |
|
400 constants.DHCPV6_STATELESS] or |
|
401 not addr_mode and not ra_mode): |
|
402 mode = 'static' |
|
403 |
|
404 cidr = netaddr.IPNetwork(subnet.cidr) |
|
405 |
|
406 if self.conf.dhcp_lease_duration == -1: |
|
407 lease = 'infinite' |
|
348 else: |
408 else: |
349 set_tag = '' |
409 lease = '%ss' % self.conf.dhcp_lease_duration |
350 |
410 |
351 cidr = netaddr.IPNetwork(subnet.cidr) |
411 # mode is optional and is not set - skip it |
352 |
412 if mode: |
353 cmd.append('--dhcp-range=%s%s,%s,%s,%ss' % |
413 if subnet.ip_version == 4: |
354 (set_tag, self._TAG_PREFIX % i, |
414 cmd.append('--dhcp-range=%s%s,%s,%s,%s' % |
355 cidr.network, |
415 ('set:', self._TAG_PREFIX % i, |
356 mode, |
416 cidr.network, mode, lease)) |
357 self.conf.dhcp_lease_duration)) |
417 else: |
358 possible_leases += cidr.size |
418 cmd.append('--dhcp-range=%s%s,%s,%s,%d,%s' % |
419 ('set:', self._TAG_PREFIX % i, |
|
420 cidr.network, mode, |
|
421 cidr.prefixlen, lease)) |
|
422 possible_leases += cidr.size |
|
359 |
423 |
360 # Cap the limit because creating lots of subnets can inflate |
424 # Cap the limit because creating lots of subnets can inflate |
361 # this possible lease cap. |
425 # this possible lease cap. |
362 cmd.append('--dhcp-lease-max=%d' % |
426 cmd.append('--dhcp-lease-max=%d' % |
363 min(possible_leases, self.conf.dnsmasq_lease_max)) |
427 min(possible_leases, self.conf.dnsmasq_lease_max)) |
364 |
428 |
365 cmd.append('--conf-file=%s' % self.conf.dnsmasq_config_file) |
429 cmd.append('--conf-file=%s' % self.conf.dnsmasq_config_file) |
366 if self.conf.dnsmasq_dns_server: |
430 if self.conf.dnsmasq_dns_servers: |
367 cmd.append('--server=%s' % self.conf.dnsmasq_dns_server) |
431 cmd.extend( |
432 '--server=%s' % server |
|
433 for server in self.conf.dnsmasq_dns_servers) |
|
368 |
434 |
369 if self.conf.dhcp_domain: |
435 if self.conf.dhcp_domain: |
370 cmd.append('--domain=%s' % self.conf.dhcp_domain) |
436 cmd.append('--domain=%s' % self.conf.dhcp_domain) |
371 |
437 |
372 # TODO(gmoodalb): prepend the env vars before command |
438 # TODO(gmoodalb): prepend the env vars before command |
373 utils.execute(cmd, self.root_helper) |
439 utils.execute(cmd, self.root_helper) |
374 |
440 |
375 def release_lease(self, mac_address, removed_ips): |
441 def _release_lease(self, mac_address, ip): |
376 """Release a DHCP lease.""" |
442 """Release a DHCP lease.""" |
377 for ip in removed_ips or []: |
443 cmd = ['/usr/lib/inet/dhcp_release', self.interface_name, |
378 cmd = ['/usr/lib/inet/dhcp_release', self.interface_name, |
444 ip, mac_address] |
379 ip, mac_address] |
445 utils.execute(cmd, self.root_helper) |
380 utils.execute(cmd, self.root_helper) |
|
381 |
446 |
382 def reload_allocations(self): |
447 def reload_allocations(self): |
383 """Rebuild the dnsmasq config and signal the dnsmasq to reload.""" |
448 """Rebuild the dnsmasq config and signal the dnsmasq to reload.""" |
384 |
449 |
385 # If all subnets turn off dhcp, kill the process. |
450 # If all subnets turn off dhcp, kill the process. |
387 self.disable() |
452 self.disable() |
388 LOG.debug(_('Killing dhcpmasq for network since all subnets have ' |
453 LOG.debug(_('Killing dhcpmasq for network since all subnets have ' |
389 'turned off DHCP: %s'), self.network.id) |
454 'turned off DHCP: %s'), self.network.id) |
390 return |
455 return |
391 |
456 |
457 self._release_unused_leases() |
|
392 self._output_hosts_file() |
458 self._output_hosts_file() |
459 self._output_addn_hosts_file() |
|
393 self._output_opts_file() |
460 self._output_opts_file() |
394 if self.active: |
461 if self.active: |
395 cmd = ['kill', '-HUP', self.pid] |
462 cmd = ['kill', '-HUP', self.pid] |
396 utils.execute(cmd, self.root_helper) |
463 utils.execute(cmd, self.root_helper) |
397 else: |
464 else: |
398 LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), self.pid) |
465 LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), self.pid) |
399 LOG.debug(_('Reloading allocations for network: %s'), self.network.id) |
466 LOG.debug(_('Reloading allocations for network: %s'), self.network.id) |
400 self.device_manager.update(self.network) |
467 self.device_manager.update(self.network, self.interface_name) |
401 |
468 |
402 def _output_hosts_file(self): |
469 def _iter_hosts(self): |
403 """Writes a dnsmasq compatible hosts file.""" |
470 """Iterate over hosts. |
404 r = re.compile('[:.]') |
471 |
405 buf = StringIO.StringIO() |
472 For each host on the network we yield a tuple containing: |
406 |
473 ( |
474 port, # a DictModel instance representing the port. |
|
475 alloc, # a DictModel instance of the allocated ip and subnet. |
|
476 host_name, # Host name. |
|
477 name, # Canonical hostname in the format 'hostname[.domain]'. |
|
478 ) |
|
479 """ |
|
480 v6_nets = dict((subnet.id, subnet) for subnet in |
|
481 self.network.subnets if subnet.ip_version == 6) |
|
407 for port in self.network.ports: |
482 for port in self.network.ports: |
408 for alloc in port.fixed_ips: |
483 for alloc in port.fixed_ips: |
409 name = 'host-%s.%s' % (r.sub('-', alloc.ip_address), |
484 # Note(scollins) Only create entries that are |
410 self.conf.dhcp_domain) |
485 # associated with the subnet being managed by this |
411 set_tag = '' |
486 # dhcp agent |
412 if getattr(port, 'extra_dhcp_opts', False): |
487 if alloc.subnet_id in v6_nets: |
413 if self.version >= self.MINIMUM_VERSION: |
488 addr_mode = v6_nets[alloc.subnet_id].ipv6_address_mode |
414 set_tag = 'set:' |
489 if addr_mode != constants.DHCPV6_STATEFUL: |
415 |
490 continue |
416 buf.write('%s,%s,%s,%s%s\n' % |
491 hostname = 'host-%s' % alloc.ip_address.replace( |
417 (port.mac_address, name, alloc.ip_address, |
492 '.', '-').replace(':', '-') |
418 set_tag, port.id)) |
493 fqdn = hostname |
419 else: |
494 if self.conf.dhcp_domain: |
420 buf.write('%s,%s,%s\n' % |
495 fqdn = '%s.%s' % (fqdn, self.conf.dhcp_domain) |
421 (port.mac_address, name, alloc.ip_address)) |
496 yield (port, alloc, hostname, fqdn) |
422 |
497 |
423 name = self.get_conf_file_name('host') |
498 def _output_hosts_file(self): |
424 utils.replace_file(name, buf.getvalue()) |
499 """Writes a dnsmasq compatible dhcp hosts file. |
425 return name |
500 |
501 The generated file is sent to the --dhcp-hostsfile option of dnsmasq, |
|
502 and lists the hosts on the network which should receive a dhcp lease. |
|
503 Each line in this file is in the form:: |
|
504 |
|
505 'mac_address,FQDN,ip_address' |
|
506 |
|
507 IMPORTANT NOTE: a dnsmasq instance does not resolve hosts defined in |
|
508 this file if it did not give a lease to a host listed in it (e.g.: |
|
509 multiple dnsmasq instances on the same network if this network is on |
|
510 multiple network nodes). This file is only defining hosts which |
|
511 should receive a dhcp lease, the hosts resolution in itself is |
|
512 defined by the `_output_addn_hosts_file` method. |
|
513 """ |
|
514 buf = six.StringIO() |
|
515 filename = self.get_conf_file_name('host') |
|
516 |
|
517 LOG.debug(_('Building host file: %s'), filename) |
|
518 for (port, alloc, hostname, name) in self._iter_hosts(): |
|
519 # (dzyu) Check if it is legal ipv6 address, if so, need wrap |
|
520 # it with '[]' to let dnsmasq to distinguish MAC address from |
|
521 # IPv6 address. |
|
522 ip_address = alloc.ip_address |
|
523 if netaddr.valid_ipv6(ip_address): |
|
524 ip_address = '[%s]' % ip_address |
|
525 |
|
526 LOG.debug(_('Adding %(mac)s : %(name)s : %(ip)s'), |
|
527 {"mac": port.mac_address, "name": name, |
|
528 "ip": ip_address}) |
|
529 |
|
530 if getattr(port, 'extra_dhcp_opts', False): |
|
531 buf.write('%s,%s,%s,%s%s\n' % |
|
532 (port.mac_address, name, ip_address, |
|
533 'set:', port.id)) |
|
534 else: |
|
535 buf.write('%s,%s,%s\n' % |
|
536 (port.mac_address, name, ip_address)) |
|
537 |
|
538 utils.replace_file(filename, buf.getvalue()) |
|
539 LOG.debug(_('Done building host file %s'), filename) |
|
540 return filename |
|
541 |
|
542 def _read_hosts_file_leases(self, filename): |
|
543 leases = set() |
|
544 if os.path.exists(filename): |
|
545 with open(filename) as f: |
|
546 for l in f.readlines(): |
|
547 host = l.strip().split(',') |
|
548 leases.add((host[2], host[0])) |
|
549 return leases |
|
550 |
|
551 def _release_unused_leases(self): |
|
552 filename = self.get_conf_file_name('host') |
|
553 old_leases = self._read_hosts_file_leases(filename) |
|
554 |
|
555 new_leases = set() |
|
556 for port in self.network.ports: |
|
557 for alloc in port.fixed_ips: |
|
558 new_leases.add((alloc.ip_address, port.mac_address)) |
|
559 |
|
560 for ip, mac in old_leases - new_leases: |
|
561 self._release_lease(mac, ip) |
|
562 |
|
563 def _output_addn_hosts_file(self): |
|
564 """Writes a dnsmasq compatible additional hosts file. |
|
565 |
|
566 The generated file is sent to the --addn-hosts option of dnsmasq, |
|
567 and lists the hosts on the network which should be resolved even if |
|
568 the dnsmaq instance did not give a lease to the host (see the |
|
569 `_output_hosts_file` method). |
|
570 Each line in this file is in the same form as a standard /etc/hosts |
|
571 file. |
|
572 """ |
|
573 buf = six.StringIO() |
|
574 for (port, alloc, hostname, fqdn) in self._iter_hosts(): |
|
575 # It is compulsory to write the `fqdn` before the `hostname` in |
|
576 # order to obtain it in PTR responses. |
|
577 buf.write('%s\t%s %s\n' % (alloc.ip_address, fqdn, hostname)) |
|
578 addn_hosts = self.get_conf_file_name('addn_hosts') |
|
579 utils.replace_file(addn_hosts, buf.getvalue()) |
|
580 return addn_hosts |
|
426 |
581 |
427 def _output_opts_file(self): |
582 def _output_opts_file(self): |
428 """Write a dnsmasq compatible options file.""" |
583 """Write a dnsmasq compatible options file.""" |
429 |
584 |
430 if self.conf.enable_isolated_metadata: |
585 if self.conf.enable_isolated_metadata: |
431 subnet_to_interface_ip = self._make_subnet_interface_ip_map() |
586 subnet_to_interface_ip = self._make_subnet_interface_ip_map() |
432 |
587 |
433 options = [] |
588 options = [] |
589 |
|
590 isolated_subnets = self.get_isolated_subnets(self.network) |
|
591 dhcp_ips = collections.defaultdict(list) |
|
592 subnet_idx_map = {} |
|
434 for i, subnet in enumerate(self.network.subnets): |
593 for i, subnet in enumerate(self.network.subnets): |
435 if not subnet.enable_dhcp: |
594 if (not subnet.enable_dhcp or |
595 (subnet.ip_version == 6 and |
|
596 getattr(subnet, 'ipv6_address_mode', None) |
|
597 in [None, constants.IPV6_SLAAC])): |
|
436 continue |
598 continue |
437 if subnet.dns_nameservers: |
599 if subnet.dns_nameservers: |
438 options.append( |
600 options.append( |
439 self._format_option(i, 'dns-server', |
601 self._format_option( |
440 ','.join(subnet.dns_nameservers))) |
602 subnet.ip_version, i, 'dns-server', |
603 ','.join( |
|
604 Dnsmasq._convert_to_literal_addrs( |
|
605 subnet.ip_version, subnet.dns_nameservers)))) |
|
606 else: |
|
607 # use the dnsmasq ip as nameservers only if there is no |
|
608 # dns-server submitted by the server |
|
609 subnet_idx_map[subnet.id] = i |
|
610 |
|
611 if self.conf.dhcp_domain and subnet.ip_version == 6: |
|
612 options.append('tag:tag%s,option6:domain-search,%s' % |
|
613 (i, ''.join(self.conf.dhcp_domain))) |
|
441 |
614 |
442 gateway = subnet.gateway_ip |
615 gateway = subnet.gateway_ip |
443 host_routes = [] |
616 host_routes = [] |
444 for hr in subnet.host_routes: |
617 for hr in subnet.host_routes: |
445 if hr.destination == "0.0.0.0/0": |
618 if hr.destination == "0.0.0.0/0": |
446 gateway = hr.nexthop |
619 if not gateway: |
620 gateway = hr.nexthop |
|
447 else: |
621 else: |
448 host_routes.append("%s,%s" % (hr.destination, hr.nexthop)) |
622 host_routes.append("%s,%s" % (hr.destination, hr.nexthop)) |
449 |
623 |
450 # Add host routes for isolated network segments |
624 # Add host routes for isolated network segments |
451 enable_metadata = ( |
625 |
452 self.conf.enable_isolated_metadata |
626 if (isolated_subnets[subnet.id] and |
453 and not subnet.gateway_ip |
627 self.conf.enable_isolated_metadata and |
454 and subnet.ip_version == 4) |
628 subnet.ip_version == 4): |
455 |
|
456 if enable_metadata: |
|
457 subnet_dhcp_ip = subnet_to_interface_ip[subnet.id] |
629 subnet_dhcp_ip = subnet_to_interface_ip[subnet.id] |
458 host_routes.append( |
630 host_routes.append( |
459 '%s/32,%s' % (METADATA_DEFAULT_IP, subnet_dhcp_ip) |
631 '%s/32,%s' % (METADATA_DEFAULT_IP, subnet_dhcp_ip) |
460 ) |
632 ) |
461 |
633 |
462 if host_routes: |
|
463 options.append( |
|
464 self._format_option(i, 'classless-static-route', |
|
465 ','.join(host_routes))) |
|
466 options.append( |
|
467 self._format_option(i, WIN2k3_STATIC_DNS, |
|
468 ','.join(host_routes))) |
|
469 |
|
470 if subnet.ip_version == 4: |
634 if subnet.ip_version == 4: |
635 if host_routes: |
|
636 if gateway: |
|
637 host_routes.append("%s,%s" % ("0.0.0.0/0", gateway)) |
|
638 options.append( |
|
639 self._format_option(subnet.ip_version, i, |
|
640 'classless-static-route', |
|
641 ','.join(host_routes))) |
|
642 options.append( |
|
643 self._format_option(subnet.ip_version, i, |
|
644 WIN2k3_STATIC_DNS, |
|
645 ','.join(host_routes))) |
|
646 |
|
471 if gateway: |
647 if gateway: |
472 options.append(self._format_option(i, 'router', gateway)) |
648 options.append(self._format_option(subnet.ip_version, |
649 i, 'router', |
|
650 gateway)) |
|
473 else: |
651 else: |
474 options.append(self._format_option(i, 'router')) |
652 options.append(self._format_option(subnet.ip_version, |
653 i, 'router')) |
|
475 |
654 |
476 for port in self.network.ports: |
655 for port in self.network.ports: |
477 if getattr(port, 'extra_dhcp_opts', False): |
656 if getattr(port, 'extra_dhcp_opts', False): |
478 options.extend( |
657 for ip_version in (4, 6): |
479 self._format_option(port.id, opt.opt_name, opt.opt_value) |
658 if any( |
480 for opt in port.extra_dhcp_opts) |
659 netaddr.IPAddress(ip.ip_address).version == ip_version |
660 for ip in port.fixed_ips): |
|
661 options.extend( |
|
662 # TODO(xuhanp):Instead of applying extra_dhcp_opts |
|
663 # to both DHCPv4 and DHCPv6, we need to find a new |
|
664 # way to specify options for v4 and v6 |
|
665 # respectively. We also need to validate the option |
|
666 # before applying it. |
|
667 self._format_option(ip_version, port.id, |
|
668 opt.opt_name, opt.opt_value) |
|
669 for opt in port.extra_dhcp_opts) |
|
670 |
|
671 # provides all dnsmasq ip as dns-server if there is more than |
|
672 # one dnsmasq for a subnet and there is no dns-server submitted |
|
673 # by the server |
|
674 if port.device_owner == constants.DEVICE_OWNER_DHCP: |
|
675 for ip in port.fixed_ips: |
|
676 i = subnet_idx_map.get(ip.subnet_id) |
|
677 if i is None: |
|
678 continue |
|
679 dhcp_ips[i].append(ip.ip_address) |
|
680 |
|
681 for i, ips in dhcp_ips.items(): |
|
682 for ip_version in (4, 6): |
|
683 vx_ips = [ip for ip in ips |
|
684 if netaddr.IPAddress(ip).version == ip_version] |
|
685 if vx_ips: |
|
686 options.append( |
|
687 self._format_option( |
|
688 ip_version, i, 'dns-server', |
|
689 ','.join( |
|
690 Dnsmasq._convert_to_literal_addrs(ip_version, |
|
691 vx_ips)))) |
|
481 |
692 |
482 name = self.get_conf_file_name('opts') |
693 name = self.get_conf_file_name('opts') |
483 utils.replace_file(name, '\n'.join(options)) |
694 utils.replace_file(name, '\n'.join(options)) |
484 return name |
695 return name |
485 |
696 |
486 def _make_subnet_interface_ip_map(self): |
697 def _make_subnet_interface_ip_map(self): |
487 # TODO(gmoodalb): need to complete this when we support metadata |
698 # TODO(gmoodalb): need to complete this when we support metadata |
488 pass |
699 pass |
489 |
700 |
490 def _format_option(self, tag, option, *args): |
701 def _format_option(self, ip_version, tag, option, *args): |
491 """Format DHCP option by option name or code.""" |
702 """Format DHCP option by option name or code.""" |
492 if self.version >= self.MINIMUM_VERSION: |
|
493 set_tag = 'tag:' |
|
494 else: |
|
495 set_tag = '' |
|
496 |
|
497 option = str(option) |
703 option = str(option) |
498 |
704 |
499 if isinstance(tag, int): |
705 if isinstance(tag, int): |
500 tag = self._TAG_PREFIX % tag |
706 tag = self._TAG_PREFIX % tag |
501 |
707 |
502 if not option.isdigit(): |
708 if not option.isdigit(): |
503 option = 'option:%s' % option |
709 if ip_version == 4: |
504 |
710 option = 'option:%s' % option |
505 return ','.join((set_tag + tag, '%s' % option) + args) |
711 else: |
712 option = 'option6:%s' % option |
|
713 |
|
714 return ','.join(('tag:' + tag, '%s' % option) + args) |
|
715 |
|
716 @staticmethod |
|
717 def _convert_to_literal_addrs(ip_version, ips): |
|
718 if ip_version == 4: |
|
719 return ips |
|
720 return ['[' + ip + ']' for ip in ips] |
|
721 |
|
722 @classmethod |
|
723 def get_isolated_subnets(cls, network): |
|
724 """Returns a dict indicating whether or not a subnet is isolated |
|
725 |
|
726 A subnet is considered non-isolated if there is a port connected to |
|
727 the subnet, and the port's ip address matches that of the subnet's |
|
728 gateway. The port must be owned by a nuetron router. |
|
729 """ |
|
730 isolated_subnets = collections.defaultdict(lambda: True) |
|
731 subnets = dict((subnet.id, subnet) for subnet in network.subnets) |
|
732 |
|
733 for port in network.ports: |
|
734 if port.device_owner != constants.DEVICE_OWNER_ROUTER_INTF: |
|
735 continue |
|
736 for alloc in port.fixed_ips: |
|
737 if subnets[alloc.subnet_id].gateway_ip == alloc.ip_address: |
|
738 isolated_subnets[alloc.subnet_id] = False |
|
739 |
|
740 return isolated_subnets |
|
741 |
|
742 @classmethod |
|
743 def should_enable_metadata(cls, conf, network): |
|
744 """True if there exists a subnet for which a metadata proxy is needed |
|
745 """ |
|
746 return False |
|
506 |
747 |
507 @classmethod |
748 @classmethod |
508 def lease_update(cls): |
749 def lease_update(cls): |
509 network_id = os.environ.get(cls.NEUTRON_NETWORK_ID_KEY) |
750 network_id = os.environ.get(cls.NEUTRON_NETWORK_ID_KEY) |
510 dhcp_relay_socket = os.environ.get(cls.NEUTRON_RELAY_SOCKET_PATH_KEY) |
751 dhcp_relay_socket = os.environ.get(cls.NEUTRON_RELAY_SOCKET_PATH_KEY) |
536 def __init__(self, conf, root_helper, plugin): |
777 def __init__(self, conf, root_helper, plugin): |
537 self.conf = conf |
778 self.conf = conf |
538 self.root_helper = root_helper |
779 self.root_helper = root_helper |
539 self.plugin = plugin |
780 self.plugin = plugin |
540 if not conf.interface_driver: |
781 if not conf.interface_driver: |
541 raise SystemExit(_('You must specify an interface driver')) |
782 msg = _('An interface driver must be specified') |
783 LOG.error(msg) |
|
784 raise SystemExit(1) |
|
542 try: |
785 try: |
543 self.driver = importutils.import_object( |
786 self.driver = importutils.import_object( |
544 conf.interface_driver, conf) |
787 conf.interface_driver, conf) |
545 except Exception as e: |
788 except Exception as e: |
546 msg = (_("Error importing interface driver '%(driver)s': " |
789 msg = (_("Error importing interface driver '%(driver)s': " |
547 "%(inner)s") % {'driver': conf.interface_driver, |
790 "%(inner)s") % {'driver': conf.interface_driver, |
548 'inner': e}) |
791 'inner': e}) |
549 raise SystemExit(msg) |
792 LOG.error(msg) |
793 raise SystemExit(1) |
|
550 |
794 |
551 def get_interface_name(self, network, port): |
795 def get_interface_name(self, network, port): |
552 """Return interface(device) name for use by the DHCP process.""" |
796 """Return interface(device) name for use by the DHCP process.""" |
553 return self.driver.get_device_name(port) |
797 return self.driver.get_device_name(port) |
554 |
798 |
557 # There could be more than one dhcp server per network, so create |
801 # There could be more than one dhcp server per network, so create |
558 # a device id that combines host and network ids |
802 # a device id that combines host and network ids |
559 |
803 |
560 host_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname()) |
804 host_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname()) |
561 return 'dhcp%s-%s' % (host_uuid, network.id) |
805 return 'dhcp%s-%s' % (host_uuid, network.id) |
806 |
|
807 def _set_default_route(self, network, device_name): |
|
808 """Sets the default gateway for this dhcp namespace. |
|
809 |
|
810 This method is idempotent and will only adjust the route if adjusting |
|
811 it would change it from what it already is. This makes it safe to call |
|
812 and avoids unnecessary perturbation of the system. |
|
813 """ |
|
814 # we do not support namespaces |
|
815 pass |
|
562 |
816 |
563 def setup_dhcp_port(self, network): |
817 def setup_dhcp_port(self, network): |
564 """Create/update DHCP port for the host if needed and return port.""" |
818 """Create/update DHCP port for the host if needed and return port.""" |
565 |
819 |
566 device_id = self.get_device_id(network) |
820 device_id = self.get_device_id(network) |
593 # we need to add those to the port and call update. |
847 # we need to add those to the port and call update. |
594 if dhcp_enabled_subnet_ids: |
848 if dhcp_enabled_subnet_ids: |
595 port_fixed_ips.extend( |
849 port_fixed_ips.extend( |
596 [dict(subnet_id=s) for s in dhcp_enabled_subnet_ids]) |
850 [dict(subnet_id=s) for s in dhcp_enabled_subnet_ids]) |
597 dhcp_port = self.plugin.update_dhcp_port( |
851 dhcp_port = self.plugin.update_dhcp_port( |
598 port.id, {'port': {'fixed_ips': port_fixed_ips}}) |
852 port.id, {'port': {'network_id': network.id, |
853 'fixed_ips': port_fixed_ips}}) |
|
854 if not dhcp_port: |
|
855 raise exceptions.Conflict() |
|
599 else: |
856 else: |
600 dhcp_port = port |
857 dhcp_port = port |
601 # break since we found port that matches device_id |
858 # break since we found port that matches device_id |
602 break |
859 break |
860 |
|
861 # check for a reserved DHCP port |
|
862 if dhcp_port is None: |
|
863 LOG.debug(_('DHCP port %(device_id)s on network %(network_id)s' |
|
864 ' does not yet exist. Checking for a reserved port.'), |
|
865 {'device_id': device_id, 'network_id': network.id}) |
|
866 for port in network.ports: |
|
867 port_device_id = getattr(port, 'device_id', None) |
|
868 if port_device_id == constants.DEVICE_ID_RESERVED_DHCP_PORT: |
|
869 dhcp_port = self.plugin.update_dhcp_port( |
|
870 port.id, {'port': {'network_id': network.id, |
|
871 'device_id': device_id}}) |
|
872 if dhcp_port: |
|
873 break |
|
603 |
874 |
604 # DHCP port has not yet been created. |
875 # DHCP port has not yet been created. |
605 if dhcp_port is None: |
876 if dhcp_port is None: |
606 LOG.debug(_('DHCP port %(device_id)s on network %(network_id)s' |
877 LOG.debug(_('DHCP port %(device_id)s on network %(network_id)s' |
607 ' does not yet exist.'), {'device_id': device_id, |
878 ' does not yet exist.'), {'device_id': device_id, |
613 network_id=network.id, |
884 network_id=network.id, |
614 tenant_id=network.tenant_id, |
885 tenant_id=network.tenant_id, |
615 fixed_ips=[dict(subnet_id=s) for s in dhcp_enabled_subnet_ids]) |
886 fixed_ips=[dict(subnet_id=s) for s in dhcp_enabled_subnet_ids]) |
616 dhcp_port = self.plugin.create_dhcp_port({'port': port_dict}) |
887 dhcp_port = self.plugin.create_dhcp_port({'port': port_dict}) |
617 |
888 |
889 if not dhcp_port: |
|
890 raise exceptions.Conflict() |
|
891 |
|
618 # Convert subnet_id to subnet dict |
892 # Convert subnet_id to subnet dict |
619 fixed_ips = [dict(subnet_id=fixed_ip.subnet_id, |
893 fixed_ips = [dict(subnet_id=fixed_ip.subnet_id, |
620 ip_address=fixed_ip.ip_address, |
894 ip_address=fixed_ip.ip_address, |
621 subnet=subnets[fixed_ip.subnet_id]) |
895 subnet=subnets[fixed_ip.subnet_id]) |
622 for fixed_ip in dhcp_port.fixed_ips] |
896 for fixed_ip in dhcp_port.fixed_ips] |
625 for item in fixed_ips] |
899 for item in fixed_ips] |
626 dhcp_port.fixed_ips = ips |
900 dhcp_port.fixed_ips = ips |
627 |
901 |
628 return dhcp_port |
902 return dhcp_port |
629 |
903 |
630 def setup(self, network, reuse_existing=False): |
904 def setup(self, network): |
631 """Create and initialize a device for network's DHCP on this host.""" |
905 """Create and initialize a device for network's DHCP on this host.""" |
632 port = self.setup_dhcp_port(network) |
906 port = self.setup_dhcp_port(network) |
633 interface_name = self.get_interface_name(network, port) |
907 interface_name = self.get_interface_name(network, port) |
634 |
908 |
635 if net_lib.Datalink.datalink_exists(interface_name): |
909 if net_lib.Datalink.datalink_exists(interface_name): |
636 if not reuse_existing: |
910 LOG.debug(_('Reusing existing device: %s.'), interface_name) |
637 raise exceptions.PreexistingDeviceFailure( |
|
638 dev_name=interface_name) |
|
639 |
|
640 LOG.debug(_('Reusing existing device: %s.'), interface_name) |
|
641 else: |
911 else: |
642 self.driver.plug(network.tenant_id, network.id, |
912 self.driver.plug(network.tenant_id, network.id, |
643 port.id, |
913 port.id, interface_name) |
644 interface_name) |
|
645 ip_cidrs = [] |
914 ip_cidrs = [] |
646 for fixed_ip in port.fixed_ips: |
915 for fixed_ip in port.fixed_ips: |
647 subnet = fixed_ip.subnet |
916 subnet = fixed_ip.subnet |
648 net = netaddr.IPNetwork(subnet.cidr) |
917 net = netaddr.IPNetwork(subnet.cidr) |
649 ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen) |
918 ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen) |
650 ip_cidrs.append(ip_cidr) |
919 ip_cidrs.append(ip_cidr) |
651 |
920 |
652 if (self.conf.enable_isolated_metadata and self.conf.use_namespaces): |
921 if (self.conf.enable_isolated_metadata and |
922 self.conf.use_namespaces): |
|
653 ip_cidrs.append(METADATA_DEFAULT_CIDR) |
923 ip_cidrs.append(METADATA_DEFAULT_CIDR) |
654 |
924 |
655 self.driver.init_l3(interface_name, ip_cidrs) |
925 self.driver.init_l3(interface_name, ip_cidrs) |
656 |
926 |
927 if self.conf.use_namespaces: |
|
928 self._set_default_route(network, interface_name) |
|
929 |
|
657 return interface_name |
930 return interface_name |
658 |
931 |
659 def update(self, network): |
932 def update(self, network, device_name): |
660 """Update device settings for the network's DHCP on this host.""" |
933 """Update device settings for the network's DHCP on this host.""" |
661 pass |
934 pass |
662 |
935 |
663 def destroy(self, network, device_name): |
936 def destroy(self, network, device_name): |
664 """Destroy the device used for the network's DHCP on this host.""" |
937 """Destroy the device used for the network's DHCP on this host.""" |