|
1 # Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved. |
|
2 # |
|
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may |
|
4 # not use this file except in compliance with the License. You may obtain |
|
5 # a copy of the License at |
|
6 # |
|
7 # http://www.apache.org/licenses/LICENSE-2.0 |
|
8 # |
|
9 # Unless required by applicable law or agreed to in writing, software |
|
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
|
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
|
12 # License for the specific language governing permissions and limitations |
|
13 # under the License. |
|
14 # |
|
15 # @author: Girish Moodalbail, Oracle, Inc. |
|
16 |
|
17 import abc |
|
18 import os |
|
19 import re |
|
20 import shutil |
|
21 import StringIO |
|
22 |
|
23 import netaddr |
|
24 |
|
25 from oslo.config import cfg |
|
26 from quantum.agent.linux import utils |
|
27 from quantum.openstack.common import log as logging |
|
28 from quantum.openstack.common import uuidutils |
|
29 |
|
30 LOG = logging.getLogger(__name__) |
|
31 |
|
32 OPTS = [ |
|
33 cfg.StrOpt('dhcp_confs', |
|
34 default='$state_path/dhcp', |
|
35 help=_('Location to store DHCP server config files')), |
|
36 cfg.IntOpt('dhcp_lease_time', |
|
37 default=120, |
|
38 help=_('Lifetime of a DHCP lease in seconds')), |
|
39 cfg.StrOpt('dhcp_domain', |
|
40 default='openstacklocal', |
|
41 help=_('Domain to use for building the hostnames')), |
|
42 cfg.StrOpt('dnsmasq_config_file', |
|
43 default='', |
|
44 help=_('Override the default dnsmasq settings with this file')), |
|
45 cfg.StrOpt('dnsmasq_dns_server', |
|
46 help=_('Use another DNS server before any in ' |
|
47 '/etc/resolv.conf.')), |
|
48 ] |
|
49 |
|
50 IPV4 = 4 |
|
51 IPV6 = 6 |
|
52 UDP = 'udp' |
|
53 TCP = 'tcp' |
|
54 DNS_PORT = 53 |
|
55 DHCPV4_PORT = 67 |
|
56 DHCPV6_PORT = 467 |
|
57 |
|
58 |
|
59 class DhcpBase(object): |
|
60 __metaclass__ = abc.ABCMeta |
|
61 |
|
62 def __init__(self, conf, network, root_helper='sudo', |
|
63 device_delegate=None, namespace=None, version=None): |
|
64 self.conf = conf |
|
65 self.network = network |
|
66 self.root_helper = root_helper |
|
67 self.device_delegate = device_delegate |
|
68 self.namespace = namespace |
|
69 self.version = version |
|
70 |
|
71 @abc.abstractmethod |
|
72 def enable(self): |
|
73 """Enables DHCP for this network.""" |
|
74 |
|
75 @abc.abstractmethod |
|
76 def disable(self, retain_port=False): |
|
77 """Disable dhcp for this network.""" |
|
78 |
|
79 def restart(self): |
|
80 """Restart the dhcp service for the network.""" |
|
81 self.disable(retain_port=True) |
|
82 self.enable() |
|
83 |
|
84 @abc.abstractproperty |
|
85 def active(self): |
|
86 """Boolean representing the running state of the DHCP server.""" |
|
87 |
|
88 @abc.abstractmethod |
|
89 def reload_allocations(self): |
|
90 """Force the DHCP server to reload the assignment database.""" |
|
91 |
|
92 @classmethod |
|
93 def existing_dhcp_networks(cls, conf, root_helper): |
|
94 """Return a list of existing networks ids (ones we have configs for)""" |
|
95 |
|
96 raise NotImplementedError |
|
97 |
|
98 @classmethod |
|
99 def check_version(cls): |
|
100 """Execute version checks on DHCP server.""" |
|
101 |
|
102 raise NotImplementedError |
|
103 |
|
104 |
|
105 class DhcpLocalProcess(DhcpBase): |
|
106 PORTS = [] |
|
107 |
|
108 def _enable_dhcp(self): |
|
109 """check if there is a subnet within the network with dhcp enabled.""" |
|
110 return any(s for s in self.network.subnets if s.enable_dhcp) |
|
111 |
|
112 def enable(self): |
|
113 """Enables DHCP for this network by spawning a local process.""" |
|
114 interface_name = self.device_delegate.setup(self.network, |
|
115 reuse_existing=True) |
|
116 if self.active: |
|
117 self.restart() |
|
118 elif self._enable_dhcp(): |
|
119 self.interface_name = interface_name |
|
120 self.spawn_process() |
|
121 |
|
122 def disable(self, retain_port=False): |
|
123 """Disable DHCP for this network by killing the local process.""" |
|
124 pid = self.pid |
|
125 |
|
126 if self.active: |
|
127 cmd = ['kill', '-9', pid] |
|
128 utils.execute(cmd) |
|
129 |
|
130 if not retain_port: |
|
131 self.device_delegate.destroy(self.network, self.interface_name) |
|
132 |
|
133 elif pid: |
|
134 LOG.debug(_('DHCP for %(net_id)s pid %(pid)d is stale, ignoring ' |
|
135 'command'), {'net_id': self.network.id, 'pid': pid}) |
|
136 else: |
|
137 LOG.debug(_('No DHCP started for %s'), self.network.id) |
|
138 |
|
139 self._remove_config_files() |
|
140 |
|
141 def _remove_config_files(self): |
|
142 confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs)) |
|
143 conf_dir = os.path.join(confs_dir, self.network.id) |
|
144 shutil.rmtree(conf_dir, ignore_errors=True) |
|
145 |
|
146 def get_conf_file_name(self, kind, ensure_conf_dir=False): |
|
147 """Returns the file name for a given kind of config file.""" |
|
148 confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs)) |
|
149 conf_dir = os.path.join(confs_dir, self.network.id) |
|
150 if ensure_conf_dir: |
|
151 if not os.path.isdir(conf_dir): |
|
152 os.makedirs(conf_dir, 0755) |
|
153 |
|
154 return os.path.join(conf_dir, kind) |
|
155 |
|
156 def _get_value_from_conf_file(self, kind, converter=None): |
|
157 """A helper function to read a value from one of the state files.""" |
|
158 file_name = self.get_conf_file_name(kind) |
|
159 msg = _('Error while reading %s') |
|
160 |
|
161 try: |
|
162 with open(file_name, 'r') as f: |
|
163 try: |
|
164 return converter and converter(f.read()) or f.read() |
|
165 except ValueError, e: |
|
166 msg = _('Unable to convert value in %s') |
|
167 except IOError, e: |
|
168 msg = _('Unable to access %s') |
|
169 |
|
170 LOG.debug(msg % file_name) |
|
171 return None |
|
172 |
|
173 @property |
|
174 def pid(self): |
|
175 """Last known pid for the DHCP process spawned for this network.""" |
|
176 return self._get_value_from_conf_file('pid', int) |
|
177 |
|
178 @property |
|
179 def active(self): |
|
180 pid = self.pid |
|
181 if pid is None: |
|
182 return False |
|
183 |
|
184 cmd = ['pargs', pid] |
|
185 try: |
|
186 return self.network.id in utils.execute(cmd) |
|
187 except RuntimeError: |
|
188 return False |
|
189 |
|
190 @property |
|
191 def interface_name(self): |
|
192 return self._get_value_from_conf_file('interface') |
|
193 |
|
194 @interface_name.setter |
|
195 def interface_name(self, value): |
|
196 interface_file_path = self.get_conf_file_name('interface', |
|
197 ensure_conf_dir=True) |
|
198 utils.replace_file(interface_file_path, value) |
|
199 |
|
200 @abc.abstractmethod |
|
201 def spawn_process(self): |
|
202 pass |
|
203 |
|
204 |
|
205 class Dnsmasq(DhcpLocalProcess): |
|
206 # The ports that need to be opened when security policies are active |
|
207 # on the Quantum port used for DHCP. These are provided as a convenience |
|
208 # for users of this class. |
|
209 PORTS = {IPV4: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV4_PORT)], |
|
210 IPV6: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV6_PORT)]} |
|
211 |
|
212 _TAG_PREFIX = 'tag%d' |
|
213 |
|
214 QUANTUM_NETWORK_ID_KEY = 'QUANTUM_NETWORK_ID' |
|
215 QUANTUM_RELAY_SOCKET_PATH_KEY = 'QUANTUM_RELAY_SOCKET_PATH' |
|
216 |
|
217 @classmethod |
|
218 def check_version(cls): |
|
219 # For Solaris, we rely on the packaging system to ensure a |
|
220 # matching/supported version of dnsmasq |
|
221 pass |
|
222 |
|
223 @classmethod |
|
224 def existing_dhcp_networks(cls, conf, root_helper): |
|
225 """Return a list of existing networks ids (ones we have configs for)""" |
|
226 |
|
227 confs_dir = os.path.abspath(os.path.normpath(conf.dhcp_confs)) |
|
228 |
|
229 class FakeNetwork: |
|
230 def __init__(self, net_id): |
|
231 self.id = net_id |
|
232 |
|
233 return [ |
|
234 c for c in os.listdir(confs_dir) |
|
235 if (uuidutils.is_uuid_like(c) and |
|
236 cls(conf, FakeNetwork(c), root_helper).active) |
|
237 ] |
|
238 |
|
239 def spawn_process(self): |
|
240 """Spawns a Dnsmasq process for the network.""" |
|
241 env = { |
|
242 self.QUANTUM_NETWORK_ID_KEY: self.network.id |
|
243 } |
|
244 |
|
245 cmd = [ |
|
246 '/usr/lib/inet/dnsmasq', |
|
247 '--no-hosts', |
|
248 '--no-resolv', |
|
249 '--strict-order', |
|
250 '--bind-interfaces', |
|
251 '--interface=%s' % self.interface_name, |
|
252 '--except-interface=lo0', |
|
253 '--pid-file=%s' % self.get_conf_file_name( |
|
254 'pid', ensure_conf_dir=True), |
|
255 #TODO(gmoodalb): calculate value from cidr (defaults to 150) |
|
256 #'--dhcp-lease-max=%s' % ?, |
|
257 '--dhcp-hostsfile=%s' % self._output_hosts_file(), |
|
258 '--dhcp-optsfile=%s' % self._output_opts_file(), |
|
259 #'--dhcp-script=%s' % self._lease_relay_script_path(), |
|
260 '--leasefile-ro', |
|
261 ] |
|
262 |
|
263 for i, subnet in enumerate(self.network.subnets): |
|
264 # if a subnet is specified to have dhcp disabled |
|
265 if not subnet.enable_dhcp: |
|
266 continue |
|
267 if subnet.ip_version == 4: |
|
268 mode = 'static' |
|
269 else: |
|
270 #TODO(gmoodalb): how do we indicate other options |
|
271 #ra-only, slaac, ra-nameservers, and ra-stateless. |
|
272 mode = 'static' |
|
273 cmd.append('--dhcp-range=set:%s,%s,%s,%ss' % |
|
274 (self._TAG_PREFIX % i, |
|
275 netaddr.IPNetwork(subnet.cidr).network, |
|
276 mode, self.conf.dhcp_lease_time)) |
|
277 |
|
278 cmd.append('--conf-file=%s' % self.conf.dnsmasq_config_file) |
|
279 if self.conf.dnsmasq_dns_server: |
|
280 cmd.append('--server=%s' % self.conf.dnsmasq_dns_server) |
|
281 |
|
282 if self.conf.dhcp_domain: |
|
283 cmd.append('--domain=%s' % self.conf.dhcp_domain) |
|
284 utils.execute(cmd) |
|
285 |
|
286 def reload_allocations(self): |
|
287 """Rebuild the dnsmasq config and signal the dnsmasq to reload.""" |
|
288 |
|
289 # If all subnets turn off dhcp, kill the process. |
|
290 if not self._enable_dhcp(): |
|
291 self.disable() |
|
292 LOG.debug(_('Killing dhcpmasq for network since all subnets have ' |
|
293 'turned off DHCP: %s'), self.network.id) |
|
294 return |
|
295 |
|
296 self._output_hosts_file() |
|
297 self._output_opts_file() |
|
298 |
|
299 if self.active: |
|
300 cmd = ['kill', '-HUP', self.pid] |
|
301 utils.execute(cmd) |
|
302 else: |
|
303 LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), self.pid) |
|
304 LOG.debug(_('Reloading allocations for network: %s'), self.network.id) |
|
305 |
|
306 def _output_hosts_file(self): |
|
307 """Writes a dnsmasq compatible hosts file.""" |
|
308 r = re.compile('[:.]') |
|
309 buf = StringIO.StringIO() |
|
310 |
|
311 for port in self.network.ports: |
|
312 for alloc in port.fixed_ips: |
|
313 name = 'host-%s.%s' % (r.sub('-', alloc.ip_address), |
|
314 self.conf.dhcp_domain) |
|
315 buf.write('%s,%s,%s\n' % |
|
316 (port.mac_address, name, alloc.ip_address)) |
|
317 |
|
318 name = self.get_conf_file_name('host') |
|
319 utils.replace_file(name, buf.getvalue()) |
|
320 return name |
|
321 |
|
322 def _output_opts_file(self): |
|
323 """Write a dnsmasq compatible options file.""" |
|
324 |
|
325 options = [] |
|
326 for i, subnet in enumerate(self.network.subnets): |
|
327 if not subnet.enable_dhcp: |
|
328 continue |
|
329 if subnet.dns_nameservers: |
|
330 options.append( |
|
331 self._format_option(i, 'dns-server', |
|
332 ','.join(subnet.dns_nameservers))) |
|
333 |
|
334 host_routes = ["%s,%s" % (hr.destination, hr.nexthop) |
|
335 for hr in subnet.host_routes] |
|
336 |
|
337 if host_routes: |
|
338 options.append( |
|
339 self._format_option(i, 'classless-static-route', |
|
340 ','.join(host_routes))) |
|
341 |
|
342 if subnet.ip_version == 4: |
|
343 if subnet.gateway_ip: |
|
344 options.append(self._format_option(i, 'router', |
|
345 subnet.gateway_ip)) |
|
346 else: |
|
347 options.append(self._format_option(i, 'router')) |
|
348 |
|
349 name = self.get_conf_file_name('opts') |
|
350 utils.replace_file(name, '\n'.join(options)) |
|
351 return name |
|
352 |
|
353 def _format_option(self, index, option_name, *args): |
|
354 return ','.join(('tag:' + self._TAG_PREFIX % index, |
|
355 'option:%s' % option_name) + args) |
|
356 |
|
357 @classmethod |
|
358 def lease_update(cls): |
|
359 pass |