1
# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Twisted Application Plugin for the MAAS TFTP server."""
6
from __future__ import (
26
from urllib import urlencode
27
from urlparse import (
32
from netaddr import IPAddress
33
from provisioningserver.boot import BootMethodRegistry
34
from provisioningserver.cluster_config import get_cluster_uuid
35
from provisioningserver.drivers import ArchitectureRegistry
36
from provisioningserver.kernel_opts import KernelParameters
37
from provisioningserver.utils.network import get_all_interface_addresses
38
from provisioningserver.utils.twisted import deferred
39
from tftp.backend import FilesystemSynchronousBackend
40
from tftp.errors import FileNotFound
41
from tftp.protocol import TFTP
42
from twisted.application import internet
43
from twisted.application.service import MultiService
44
from twisted.internet import udp
45
from twisted.internet.abstract import isIPv6Address
46
from twisted.internet.address import (
50
from twisted.python.context import get
51
from twisted.web.client import getPage
52
import twisted.web.error
55
class TFTPBackend(FilesystemSynchronousBackend):
56
"""A partially dynamic read-only TFTP server.
58
Static files such as kernels and initrds, as well as any non-MAAS files
59
that the system may already be set up to serve, are served up normally.
60
But PXE configurations are generated on the fly.
62
When a PXE configuration file is requested, the server asynchronously
63
requests the appropriate parameters from the API (at a configurable
64
"generator URL") and generates a config file based on those.
66
The regular expressions `re_config_file` and `re_mac_address` specify
67
which files the server generates on the fly. Any other requests are
68
passed on to the filesystem.
70
Passing requests on to the API must be done very selectively, because
71
failures cause the boot process to halt. This is why the expression for
72
matching the MAC address is so narrowly defined: PXELINUX attempts to
73
fetch files at many similar paths which must not be passed on.
76
get_page = staticmethod(getPage)
78
def __init__(self, base_path, generator_url):
80
:param base_path: The root directory for this TFTP server.
81
:param generator_url: The URL which can be queried for the PXE
82
config. See `get_generator_url` for the types of queries it is
85
super(TFTPBackend, self).__init__(
86
base_path, can_read=True, can_write=False)
87
self.generator_url = urlparse(generator_url)
89
def get_generator_url(self, params):
90
"""Calculate the URL, including query, from which we can fetch
91
additional configuration parameters.
93
:param params: A dict, or iterable suitable for updating a dict, of
94
additional query parameters.
97
# Merge parameters from the generator URL.
98
query.update(parse_qsl(self.generator_url.query))
99
# Merge parameters obtained from the request.
101
# Merge updated query into the generator URL.
102
url = self.generator_url._replace(query=urlencode(query))
103
# TODO: do something more intelligent with unicode URLs here; see
104
# apiclient.utils.ascii_url() for inspiration.
105
return url.geturl().encode("ascii")
107
def get_boot_method(self, file_name):
108
"""Finds the correct boot method."""
109
for _, method in BootMethodRegistry:
110
params = method.match_path(self, file_name)
111
if params is not None:
112
return method, params
116
def get_kernel_params(self, params):
117
"""Return kernel parameters obtained from the API.
119
:param params: Parameters so far obtained, typically from the file
121
:return: A `KernelParameters` instance.
123
url = self.get_generator_url(params)
125
def reassemble(data):
126
return KernelParameters(**data)
128
d = self.get_page(url)
129
d.addCallback(json.loads)
130
d.addCallback(reassemble)
134
def get_boot_method_reader(self, boot_method, params):
135
"""Return an `IReader` for a boot method.
137
:param boot_method: Boot method that is generating the config
138
:param params: Parameters so far obtained, typically from the file
141
def generate(kernel_params):
142
return boot_method.get_reader(
143
self, kernel_params=kernel_params, **params)
145
d = self.get_kernel_params(params)
146
d.addCallback(generate)
150
def get_page_errback(failure, file_name):
151
failure.trap(twisted.web.error.Error)
152
# This twisted.web.error.Error.status object ends up being a
153
# string for some reason, but the constants we can compare against
154
# (both in httplib and twisted.web.http) are ints.
156
status_int = int(failure.value.status)
158
# Assume that it's some other error and propagate it
161
if status_int == httplib.NO_CONTENT:
162
# Convert HTTP No Content to a TFTP file not found
163
raise FileNotFound(file_name)
165
# Otherwise propogate the unknown error
169
def get_reader(self, file_name):
170
"""See `IBackend.get_reader()`.
172
If `file_name` matches a boot method then the response is obtained
173
from that boot method. Otherwise the filesystem is used to service
176
boot_method, params = self.get_boot_method(file_name)
177
if boot_method is None:
178
return super(TFTPBackend, self).get_reader(file_name)
180
# Map pxe namespace architecture names to MAAS's.
181
arch = params.get("arch")
183
maasarch = ArchitectureRegistry.get_by_pxealias(arch)
184
if maasarch is not None:
185
params["arch"] = maasarch.name.split("/")[0]
187
# Send the local and remote endpoint addresses.
188
local_host, local_port = get("local", (None, None))
189
params["local"] = local_host
190
remote_host, remote_port = get("remote", (None, None))
191
params["remote"] = remote_host
192
params["cluster_uuid"] = get_cluster_uuid()
193
d = self.get_boot_method_reader(boot_method, params)
194
d.addErrback(self.get_page_errback, file_name)
198
class Port(udp.Port):
199
"""A :py:class:`udp.Port` that groks IPv6."""
201
# This must be set by call sites.
205
"""See :py:meth:`twisted.internet.udp.Port.getHost`."""
206
host, port = self.socket.getsockname()[:2]
207
addr_type = IPv6Address if isIPv6Address(host) else IPv4Address
208
return addr_type('UDP', host, port)
211
class UDPServer(internet.UDPServer):
212
"""A :py:class:`~internet.UDPServer` that groks IPv6.
214
This creates the port directly instead of using the reactor's
215
``listenUDP`` method so that we can do a switcharoo to our own
216
IPv6-enabled port implementation.
220
"""See :py:meth:`twisted.application.internet.UDPServer._getPort`."""
221
return self._listenUDP(*self.args, **self.kwargs)
223
def _listenUDP(self, port, protocol, interface='', maxPacketSize=8192):
224
"""See :py:meth:`twisted.internet.reactor.listenUDP`."""
225
p = Port(port, protocol, interface, maxPacketSize)
226
p.addressFamily = AF_INET6 if isIPv6Address(interface) else AF_INET
231
class TFTPService(MultiService, object):
232
"""An umbrella service representing a set of running TFTP servers.
234
Creates a UDP server individually for each discovered network
235
interface, so that we can detect the interface via which we have
238
It then periodically updates the servers running in case there's a
239
change to the host machine's network configuration.
241
:ivar backend: The :class:`TFTPBackend` being used to service TFTP
244
:ivar port: The port on which each server is started.
246
:ivar refresher: A :class:`TimerService` that calls
247
``updateServers`` periodically.
251
def __init__(self, resource_root, port, generator):
253
:param resource_root: The root directory for this TFTP server.
254
:param port: The port on which each server should be started.
255
:param generator: The URL to be queried for PXE configuration.
256
This will normally point to the `pxeconfig` endpoint on the
257
region-controller API.
259
super(TFTPService, self).__init__()
260
self.backend = TFTPBackend(resource_root, generator)
262
# Establish a periodic call to self.updateServers() every 45
263
# seconds, so that this service eventually converges on truth.
264
# TimerService ensures that a call is made to it's target
265
# function immediately as it's started, so there's no need to
266
# call updateServers() from here.
267
self.refresher = internet.TimerService(45, self.updateServers)
268
self.refresher.setName("refresher")
269
self.refresher.setServiceParent(self)
271
def getServers(self):
272
"""Return a set of all configured servers.
274
:rtype: :class:`set` of :class:`internet.UDPServer`
277
service for service in self
278
if service is not self.refresher
281
def updateServers(self):
282
"""Run a server on every interface.
284
For each configured network interface this will start a TFTP
285
server. If called later it will bring up servers on newly
286
configured interfaces and bring down servers on deconfigured
289
addrs_established = set(service.name for service in self.getServers())
290
addrs_desired = set(get_all_interface_addresses())
292
for address in addrs_desired - addrs_established:
293
if not IPAddress(address).is_link_local():
294
tftp_service = UDPServer(
295
self.port, TFTP(self.backend), interface=address)
296
tftp_service.setName(address)
297
tftp_service.setServiceParent(self)
299
for address in addrs_established - addrs_desired:
300
tftp_service = self.getServiceNamed(address)
301
tftp_service.disownServiceParent()