14 # Unless required by applicable law or agreed to in writing, software |
14 # Unless required by applicable law or agreed to in writing, software |
15 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
15 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
16 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
16 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
17 # License for the specific language governing permissions and limitations |
17 # License for the specific language governing permissions and limitations |
18 # under the License. |
18 # under the License. |
19 # |
|
20 # @author: Girish Moodalbail, Oracle, Inc. |
|
21 |
19 |
22 import abc |
20 import abc |
23 import collections |
|
24 import os |
|
25 import re |
|
26 import shutil |
|
27 import socket |
|
28 import sys |
|
29 import uuid |
|
30 |
|
31 import netaddr |
21 import netaddr |
|
22 |
32 from oslo.config import cfg |
23 from oslo.config import cfg |
33 import six |
24 from oslo_log import log as logging |
34 |
25 |
35 from neutron.agent.linux import utils |
26 from neutron.agent.linux import utils |
|
27 from neutron.agent.linux import dhcp |
36 from neutron.agent.solaris import net_lib |
28 from neutron.agent.solaris import net_lib |
37 from neutron.common import constants |
29 from neutron.common import constants |
38 from neutron.common import exceptions |
30 from neutron.common import exceptions |
39 from neutron.openstack.common import importutils |
31 from neutron.common import ipv6_utils |
40 from neutron.openstack.common import jsonutils |
32 |
41 from neutron.openstack.common import log as logging |
|
42 from neutron.openstack.common import uuidutils |
|
43 |
33 |
44 LOG = logging.getLogger(__name__) |
34 LOG = logging.getLogger(__name__) |
45 |
35 |
46 OPTS = [ |
36 |
47 cfg.StrOpt('dhcp_confs', |
37 class Dnsmasq(dhcp.Dnsmasq): |
48 default='$state_path/dhcp', |
38 """ Wrapper around Linux implementation of Dnsmasq.""" |
49 help=_('Location to store DHCP server config files')), |
39 |
50 cfg.StrOpt('dhcp_domain', |
40 def __init__(self, conf, network, process_monitor, version=None, |
51 default='openstacklocal', |
41 plugin=None): |
52 help=_('Domain to use for building the hostnames')), |
42 super(Dnsmasq, self).__init__(conf, network, process_monitor, |
53 cfg.StrOpt('dnsmasq_config_file', |
43 version, plugin) |
54 default='', |
44 self.device_manager = DeviceManager(self.conf, plugin) |
55 help=_('Override the default dnsmasq settings with this file')), |
45 |
56 cfg.ListOpt('dnsmasq_dns_servers', |
46 def _build_cmdline_callback(self, pid_file): |
57 help=_('Comma-separated list of the DNS servers which will be ' |
|
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.")), |
|
62 cfg.IntOpt( |
|
63 'dnsmasq_lease_max', |
|
64 default=(2 ** 24), |
|
65 help=_('Limit number of leases to prevent a denial-of-service.')), |
|
66 ] |
|
67 |
|
68 IPV4 = 4 |
|
69 IPV6 = 6 |
|
70 UDP = 'udp' |
|
71 TCP = 'tcp' |
|
72 DNS_PORT = 53 |
|
73 DHCPV4_PORT = 67 |
|
74 DHCPV6_PORT = 547 |
|
75 METADATA_DEFAULT_PREFIX = 16 |
|
76 METADATA_DEFAULT_IP = '169.254.169.254' |
|
77 METADATA_DEFAULT_CIDR = '%s/%d' % (METADATA_DEFAULT_IP, |
|
78 METADATA_DEFAULT_PREFIX) |
|
79 METADATA_PORT = 80 |
|
80 WIN2k3_STATIC_DNS = 249 |
|
81 |
|
82 |
|
83 class DictModel(dict): |
|
84 """Convert dict into an object that provides attribute access to values.""" |
|
85 |
|
86 def __init__(self, *args, **kwargs): |
|
87 """Convert dict values to DictModel values.""" |
|
88 super(DictModel, self).__init__(*args, **kwargs) |
|
89 |
|
90 def needs_upgrade(item): |
|
91 """Check if `item` is a dict and needs to be changed to DictModel. |
|
92 """ |
|
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] |
|
123 |
|
124 |
|
125 class NetModel(DictModel): |
|
126 |
|
127 def __init__(self, use_namespaces, d): |
|
128 super(NetModel, self).__init__(d) |
|
129 |
|
130 self._ns_name = None |
|
131 |
|
132 @property |
|
133 def namespace(self): |
|
134 return self._ns_name |
|
135 |
|
136 |
|
137 @six.add_metaclass(abc.ABCMeta) |
|
138 class DhcpBase(object): |
|
139 |
|
140 def __init__(self, conf, network, root_helper='sudo', |
|
141 version=None, plugin=None): |
|
142 self.conf = conf |
|
143 self.network = network |
|
144 self.root_helper = None |
|
145 self.device_manager = DeviceManager(self.conf, |
|
146 self.root_helper, plugin) |
|
147 self.version = version |
|
148 |
|
149 @abc.abstractmethod |
|
150 def enable(self): |
|
151 """Enables DHCP for this network.""" |
|
152 |
|
153 @abc.abstractmethod |
|
154 def disable(self, retain_port=False): |
|
155 """Disable dhcp for this network.""" |
|
156 |
|
157 def restart(self): |
|
158 """Restart the dhcp service for the network.""" |
|
159 self.disable(retain_port=True) |
|
160 self.enable() |
|
161 |
|
162 @abc.abstractproperty |
|
163 def active(self): |
|
164 """Boolean representing the running state of the DHCP server.""" |
|
165 |
|
166 @abc.abstractmethod |
|
167 def reload_allocations(self): |
|
168 """Force the DHCP server to reload the assignment database.""" |
|
169 |
|
170 @classmethod |
|
171 def existing_dhcp_networks(cls, conf, root_helper): |
|
172 """Return a list of existing networks ids that we have configs for.""" |
|
173 |
|
174 raise NotImplementedError() |
|
175 |
|
176 @classmethod |
|
177 def check_version(cls): |
|
178 """Execute version checks on DHCP server.""" |
|
179 |
|
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() |
|
191 |
|
192 |
|
193 class DhcpLocalProcess(DhcpBase): |
|
194 PORTS = [] |
|
195 |
|
196 def _enable_dhcp(self): |
|
197 """check if there is a subnet within the network with dhcp enabled.""" |
|
198 for subnet in self.network.subnets: |
|
199 if subnet.enable_dhcp: |
|
200 return True |
|
201 return False |
|
202 |
|
203 def enable(self): |
|
204 """Enables DHCP for this network by spawning a local process.""" |
|
205 if self.active: |
|
206 self.restart() |
|
207 elif self._enable_dhcp(): |
|
208 interface_name = self.device_manager.setup(self.network) |
|
209 self.interface_name = interface_name |
|
210 self.spawn_process() |
|
211 |
|
212 def disable(self, retain_port=False): |
|
213 """Disable DHCP for this network by killing the local process.""" |
|
214 pid = self.pid |
|
215 |
|
216 if pid: |
|
217 if self.active: |
|
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}) |
|
224 if not retain_port: |
|
225 self.device_manager.destroy(self.network, |
|
226 self.interface_name) |
|
227 else: |
|
228 LOG.debug(_('No DHCP started for %s'), self.network.id) |
|
229 |
|
230 self._remove_config_files() |
|
231 |
|
232 def _remove_config_files(self): |
|
233 confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs)) |
|
234 conf_dir = os.path.join(confs_dir, self.network.id) |
|
235 shutil.rmtree(conf_dir, ignore_errors=True) |
|
236 |
|
237 def get_conf_file_name(self, kind, ensure_conf_dir=False): |
|
238 """Returns the file name for a given kind of config file.""" |
|
239 confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs)) |
|
240 conf_dir = os.path.join(confs_dir, self.network.id) |
|
241 if ensure_conf_dir: |
|
242 if not os.path.isdir(conf_dir): |
|
243 os.makedirs(conf_dir, 0o755) |
|
244 |
|
245 return os.path.join(conf_dir, kind) |
|
246 |
|
247 def _get_value_from_conf_file(self, kind, converter=None): |
|
248 """A helper function to read a value from one of the state files.""" |
|
249 file_name = self.get_conf_file_name(kind) |
|
250 msg = _('Error while reading %s') |
|
251 |
|
252 try: |
|
253 with open(file_name, 'r') as f: |
|
254 try: |
|
255 return converter and converter(f.read()) or f.read() |
|
256 except ValueError: |
|
257 msg = _('Unable to convert value in %s') |
|
258 except IOError: |
|
259 msg = _('Unable to access %s') |
|
260 |
|
261 LOG.debug(msg % file_name) |
|
262 return None |
|
263 |
|
264 @property |
|
265 def pid(self): |
|
266 """Last known pid for the DHCP process spawned for this network.""" |
|
267 return self._get_value_from_conf_file('pid', int) |
|
268 |
|
269 @property |
|
270 def active(self): |
|
271 pid = self.pid |
|
272 if pid is None: |
|
273 return False |
|
274 |
|
275 cmd = ['/usr/bin/pargs', pid] |
|
276 try: |
|
277 return self.network.id in utils.execute(cmd) |
|
278 except RuntimeError: |
|
279 return False |
|
280 |
|
281 @property |
|
282 def interface_name(self): |
|
283 return self._get_value_from_conf_file('interface') |
|
284 |
|
285 @interface_name.setter |
|
286 def interface_name(self, value): |
|
287 interface_file_path = self.get_conf_file_name('interface', |
|
288 ensure_conf_dir=True) |
|
289 utils.replace_file(interface_file_path, value) |
|
290 |
|
291 @abc.abstractmethod |
|
292 def spawn_process(self): |
|
293 pass |
|
294 |
|
295 |
|
296 class Dnsmasq(DhcpLocalProcess): |
|
297 # The ports that need to be opened when security policies are active |
|
298 # on the Neutron port used for DHCP. These are provided as a convenience |
|
299 # for users of this class. |
|
300 PORTS = {IPV4: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV4_PORT)], |
|
301 IPV6: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV6_PORT)], |
|
302 } |
|
303 |
|
304 _TAG_PREFIX = 'tag%d' |
|
305 |
|
306 NEUTRON_NETWORK_ID_KEY = 'NEUTRON_NETWORK_ID' |
|
307 NEUTRON_RELAY_SOCKET_PATH_KEY = 'NEUTRON_RELAY_SOCKET_PATH' |
|
308 MINIMUM_VERSION = 2.63 |
|
309 MINIMUM_IPV6_VERSION = 2.67 |
|
310 |
|
311 @classmethod |
|
312 def check_version(cls): |
|
313 ver = 0 |
|
314 try: |
|
315 cmd = ['/usr/lib/inet/dnsmasq', '--version'] |
|
316 out = utils.execute(cmd) |
|
317 ver = re.findall("\d+.\d+", out)[0] |
|
318 is_valid_version = float(ver) >= cls.MINIMUM_VERSION |
|
319 # For Solaris, we rely on the packaging system to ensure a |
|
320 # matching/supported version of dnsmasq. |
|
321 if not is_valid_version: |
|
322 LOG.warning(_('FAILED VERSION REQUIREMENT FOR DNSMASQ. ' |
|
323 'DHCP AGENT MAY NOT RUN CORRECTLY! ' |
|
324 'Please ensure that its version is %s ' |
|
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) |
|
334 except (OSError, RuntimeError, IndexError, ValueError): |
|
335 LOG.warning(_('Unable to determine dnsmasq version. ' |
|
336 'Please ensure that its version is %s ' |
|
337 'or above!'), cls.MINIMUM_VERSION) |
|
338 raise SystemExit(1) |
|
339 return float(ver) |
|
340 |
|
341 @classmethod |
|
342 def existing_dhcp_networks(cls, conf, root_helper): |
|
343 """Return a list of existing networks ids that we have configs for.""" |
|
344 |
|
345 confs_dir = os.path.abspath(os.path.normpath(conf.dhcp_confs)) |
|
346 |
|
347 return [ |
|
348 c for c in os.listdir(confs_dir) |
|
349 if uuidutils.is_uuid_like(c) |
|
350 ] |
|
351 |
|
352 def spawn_process(self): |
|
353 """Spawns a Dnsmasq process for the network.""" |
|
354 env = { |
|
355 self.NEUTRON_NETWORK_ID_KEY: self.network.id, |
|
356 } |
|
357 |
|
358 cmd = [ |
47 cmd = [ |
359 '/usr/lib/inet/dnsmasq', |
48 '/usr/lib/inet/dnsmasq', |
360 '--no-hosts', |
49 '--no-hosts', |
361 '--no-resolv', |
50 '--no-resolv', |
362 '--strict-order', |
51 '--strict-order', |
363 '--bind-interfaces', |
52 '--bind-interfaces', |
364 '--interface=%s' % self.interface_name, |
53 '--interface=%s' % self.interface_name, |
365 '--except-interface=lo0', |
54 '--except-interface=lo0', |
366 '--pid-file=%s' % self.get_conf_file_name( |
55 '--pid-file=%s' % pid_file, |
367 'pid', ensure_conf_dir=True), |
56 '--dhcp-hostsfile=%s' % self.get_conf_file_name('host'), |
368 '--dhcp-hostsfile=%s' % self._output_hosts_file(), |
57 '--addn-hosts=%s' % self.get_conf_file_name('addn_hosts'), |
369 '--addn-hosts=%s' % self._output_addn_hosts_file(), |
58 '--dhcp-optsfile=%s' % self.get_conf_file_name('opts'), |
370 '--dhcp-optsfile=%s' % self._output_opts_file(), |
|
371 '--leasefile-ro', |
59 '--leasefile-ro', |
|
60 '--dhcp-authoritative' |
372 ] |
61 ] |
373 |
62 |
374 possible_leases = 0 |
63 possible_leases = 0 |
375 for i, subnet in enumerate(self.network.subnets): |
64 for i, subnet in enumerate(self.network.subnets): |
376 mode = None |
65 mode = None |
433 for server in self.conf.dnsmasq_dns_servers) |
128 for server in self.conf.dnsmasq_dns_servers) |
434 |
129 |
435 if self.conf.dhcp_domain: |
130 if self.conf.dhcp_domain: |
436 cmd.append('--domain=%s' % self.conf.dhcp_domain) |
131 cmd.append('--domain=%s' % self.conf.dhcp_domain) |
437 |
132 |
438 # TODO(gmoodalb): prepend the env vars before command |
133 if self.conf.dhcp_broadcast_reply: |
439 utils.execute(cmd, self.root_helper) |
134 cmd.append('--dhcp-broadcast') |
|
135 |
|
136 return cmd |
440 |
137 |
441 def _release_lease(self, mac_address, ip): |
138 def _release_lease(self, mac_address, ip): |
442 """Release a DHCP lease.""" |
139 """Release a DHCP lease.""" |
443 cmd = ['/usr/lib/inet/dhcp_release', self.interface_name, |
140 cmd = ['/usr/lib/inet/dhcp_release', self.interface_name, |
444 ip, mac_address] |
141 ip, mac_address] |
445 utils.execute(cmd, self.root_helper) |
142 utils.execute(cmd) |
446 |
|
447 def reload_allocations(self): |
|
448 """Rebuild the dnsmasq config and signal the dnsmasq to reload.""" |
|
449 |
|
450 # If all subnets turn off dhcp, kill the process. |
|
451 if not self._enable_dhcp(): |
|
452 self.disable() |
|
453 LOG.debug(_('Killing dhcpmasq for network since all subnets have ' |
|
454 'turned off DHCP: %s'), self.network.id) |
|
455 return |
|
456 |
|
457 self._release_unused_leases() |
|
458 self._output_hosts_file() |
|
459 self._output_addn_hosts_file() |
|
460 self._output_opts_file() |
|
461 if self.active: |
|
462 cmd = ['kill', '-HUP', self.pid] |
|
463 utils.execute(cmd, self.root_helper) |
|
464 else: |
|
465 LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), self.pid) |
|
466 LOG.debug(_('Reloading allocations for network: %s'), self.network.id) |
|
467 self.device_manager.update(self.network, self.interface_name) |
|
468 |
|
469 def _iter_hosts(self): |
|
470 """Iterate over hosts. |
|
471 |
|
472 For each host on the network we yield a tuple containing: |
|
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) |
|
482 for port in self.network.ports: |
|
483 for alloc in port.fixed_ips: |
|
484 # Note(scollins) Only create entries that are |
|
485 # associated with the subnet being managed by this |
|
486 # dhcp agent |
|
487 if alloc.subnet_id in v6_nets: |
|
488 addr_mode = v6_nets[alloc.subnet_id].ipv6_address_mode |
|
489 if addr_mode != constants.DHCPV6_STATEFUL: |
|
490 continue |
|
491 hostname = 'host-%s' % alloc.ip_address.replace( |
|
492 '.', '-').replace(':', '-') |
|
493 fqdn = hostname |
|
494 if self.conf.dhcp_domain: |
|
495 fqdn = '%s.%s' % (fqdn, self.conf.dhcp_domain) |
|
496 yield (port, alloc, hostname, fqdn) |
|
497 |
|
498 def _output_hosts_file(self): |
|
499 """Writes a dnsmasq compatible dhcp hosts file. |
|
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 |
|
581 |
|
582 def _output_opts_file(self): |
|
583 """Write a dnsmasq compatible options file.""" |
|
584 |
|
585 if self.conf.enable_isolated_metadata: |
|
586 subnet_to_interface_ip = self._make_subnet_interface_ip_map() |
|
587 |
|
588 options = [] |
|
589 |
|
590 isolated_subnets = self.get_isolated_subnets(self.network) |
|
591 dhcp_ips = collections.defaultdict(list) |
|
592 subnet_idx_map = {} |
|
593 for i, subnet in enumerate(self.network.subnets): |
|
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])): |
|
598 continue |
|
599 if subnet.dns_nameservers: |
|
600 options.append( |
|
601 self._format_option( |
|
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))) |
|
614 |
|
615 gateway = subnet.gateway_ip |
|
616 host_routes = [] |
|
617 for hr in subnet.host_routes: |
|
618 if hr.destination == "0.0.0.0/0": |
|
619 if not gateway: |
|
620 gateway = hr.nexthop |
|
621 else: |
|
622 host_routes.append("%s,%s" % (hr.destination, hr.nexthop)) |
|
623 |
|
624 # Add host routes for isolated network segments |
|
625 |
|
626 if (isolated_subnets[subnet.id] and |
|
627 self.conf.enable_isolated_metadata and |
|
628 subnet.ip_version == 4): |
|
629 subnet_dhcp_ip = subnet_to_interface_ip[subnet.id] |
|
630 host_routes.append( |
|
631 '%s/32,%s' % (METADATA_DEFAULT_IP, subnet_dhcp_ip) |
|
632 ) |
|
633 |
|
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 |
|
647 if gateway: |
|
648 options.append(self._format_option(subnet.ip_version, |
|
649 i, 'router', |
|
650 gateway)) |
|
651 else: |
|
652 options.append(self._format_option(subnet.ip_version, |
|
653 i, 'router')) |
|
654 |
|
655 for port in self.network.ports: |
|
656 if getattr(port, 'extra_dhcp_opts', False): |
|
657 for ip_version in (4, 6): |
|
658 if any( |
|
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)))) |
|
692 |
|
693 name = self.get_conf_file_name('opts') |
|
694 utils.replace_file(name, '\n'.join(options)) |
|
695 return name |
|
696 |
143 |
697 def _make_subnet_interface_ip_map(self): |
144 def _make_subnet_interface_ip_map(self): |
698 # TODO(gmoodalb): need to complete this when we support metadata |
145 # TODO(gmoodalb): need to complete this when we support metadata |
|
146 # in neutron-dhcp-agent as-well for isolated subnets |
699 pass |
147 pass |
700 |
|
701 def _format_option(self, ip_version, tag, option, *args): |
|
702 """Format DHCP option by option name or code.""" |
|
703 option = str(option) |
|
704 |
|
705 if isinstance(tag, int): |
|
706 tag = self._TAG_PREFIX % tag |
|
707 |
|
708 if not option.isdigit(): |
|
709 if ip_version == 4: |
|
710 option = 'option:%s' % option |
|
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 |
148 |
742 @classmethod |
149 @classmethod |
743 def should_enable_metadata(cls, conf, network): |
150 def should_enable_metadata(cls, conf, network): |
744 """True if there exists a subnet for which a metadata proxy is needed |
151 # TODO(gmoodalb): need to complete this when we support metadata |
745 """ |
152 # in neutron-dhcp-agent as-well for isolated subnets |
746 return False |
153 return False |
747 |
154 |
748 @classmethod |
155 |
749 def lease_update(cls): |
156 class DeviceManager(dhcp.DeviceManager): |
750 network_id = os.environ.get(cls.NEUTRON_NETWORK_ID_KEY) |
157 |
751 dhcp_relay_socket = os.environ.get(cls.NEUTRON_RELAY_SOCKET_PATH_KEY) |
158 def __init__(self, conf, plugin): |
752 |
159 super(DeviceManager, self).__init__(conf, plugin) |
753 action = sys.argv[1] |
|
754 if action not in ('add', 'del', 'old'): |
|
755 sys.exit() |
|
756 |
|
757 mac_address = sys.argv[2] |
|
758 ip_address = sys.argv[3] |
|
759 |
|
760 if action == 'del': |
|
761 lease_remaining = 0 |
|
762 else: |
|
763 lease_remaining = int(os.environ.get('DNSMASQ_TIME_REMAINING', 0)) |
|
764 |
|
765 data = dict(network_id=network_id, mac_address=mac_address, |
|
766 ip_address=ip_address, lease_remaining=lease_remaining) |
|
767 |
|
768 if os.path.exists(dhcp_relay_socket): |
|
769 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
|
770 sock.connect(dhcp_relay_socket) |
|
771 sock.send(jsonutils.dumps(data)) |
|
772 sock.close() |
|
773 |
|
774 |
|
775 class DeviceManager(object): |
|
776 |
|
777 def __init__(self, conf, root_helper, plugin): |
|
778 self.conf = conf |
|
779 self.root_helper = root_helper |
|
780 self.plugin = plugin |
|
781 if not conf.interface_driver: |
|
782 msg = _('An interface driver must be specified') |
|
783 LOG.error(msg) |
|
784 raise SystemExit(1) |
|
785 try: |
|
786 self.driver = importutils.import_object( |
|
787 conf.interface_driver, conf) |
|
788 except Exception as e: |
|
789 msg = (_("Error importing interface driver '%(driver)s': " |
|
790 "%(inner)s") % {'driver': conf.interface_driver, |
|
791 'inner': e}) |
|
792 LOG.error(msg) |
|
793 raise SystemExit(1) |
|
794 |
|
795 def get_interface_name(self, network, port): |
|
796 """Return interface(device) name for use by the DHCP process.""" |
|
797 return self.driver.get_device_name(port) |
|
798 |
|
799 def get_device_id(self, network): |
|
800 """Return a unique DHCP device ID for this host on the network.""" |
|
801 # There could be more than one dhcp server per network, so create |
|
802 # a device id that combines host and network ids |
|
803 |
|
804 host_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname()) |
|
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 |
|
816 |
160 |
817 def setup_dhcp_port(self, network): |
161 def setup_dhcp_port(self, network): |
818 """Create/update DHCP port for the host if needed and return port.""" |
162 """Create/update DHCP port for the host if needed and return port.""" |
819 |
163 |
820 device_id = self.get_device_id(network) |
164 device_id = self.get_device_id(network) |