~ubuntu-branches/ubuntu/utopic/maas/utopic-security

« back to all changes in this revision

Viewing changes to src/provisioningserver/tftp.py

  • Committer: Package Import Robot
  • Author(s): Andres Rodriguez, Jeroen Vermeulen, Andres Rodriguez, Jason Hobbs, Raphaël Badin, Louis Bouchard, Gavin Panella
  • Date: 2014-08-21 19:36:30 UTC
  • mfrom: (1.3.1)
  • Revision ID: package-import@ubuntu.com-20140821193630-kertpu5hd8yyss8h
Tags: 1.7.0~beta7+bzr3266-0ubuntu1
* New Upstream Snapshot, Beta 7 bzr3266

[ Jeroen Vermeulen ]
* debian/extras/99-maas-sudoers
  debian/maas-dhcp.postinst
  debian/rules
  - Add second DHCP server instance for IPv6.
* debian/maas-region-controller-min.install
  debian/maas-region-controller-min.lintian-overrides
  - Install deployment user-data: maas_configure_interfaces.py script.
* debian/maas-cluster-controller.links
  debian/maas-cluster-controller.install
  debian/maas-cluster-controller.postinst
  - Reflect Celery removal changes made in trunk r3067.
  - Don't install celeryconfig_cluster.py any longer. 
  - Don't install maas_local_celeryconfig_cluster.py any longer.
  - Don't symlink maas_local_celeryconfig_cluster.py from /etc to /usr.
  - Don't insert UUID into maas_local_celeryconfig_cluster.py.

[ Andres Rodriguez ]
* debian/maas-region-controller-min.postrm: Cleanup lefover files.
* debian/maas-dhcp.postrm: Clean leftover configs.
* Provide new maas-proxy package that replaces the usage of
  squid-deb-proxy:
  - debian/control: New maas-proxy package that replaces the usage
    of squid-deb-proxy; Drop depends on squid-deb-proxy.
  - Add upstrart job.
  - Ensure squid3 is stopped as maas-proxy uses a caching proxy.
* Remove Celery references to cluster controller:
  - Rename upstart job from maas-pserv to maas-cluster; rename
    maas-cluster-celery to maas-cluster-register. Ensure services
    are stopped on upgrade.
  - debian/maintscript: Cleanup config files.
  - Remove all references to the MAAS celery daemon and config
    files as we don't use it like that anymore
* Move some entries in debian/maintscript to
  debian/maas-cluster-controller.maintscript
* Remove usage of txlongpoll and rabbitmq-server. Handle upgrades
  to ensure these are removed correctly.

[ Jason Hobbs ]
* debian/maas-region-controller-min.install: Install
  maas-generate-winrm-cert script.

[ Raphaël Badin ]
* debian/extras/maas-region-admin: Bypass django-admin as it prints
  spurious messages to stdout (LP: #1365130).

[Louis Bouchard]
* debian/maas-cluster-controller.postinst:
  - Exclude /var/log/maas/rsyslog when changing ownership
    (LP: #1346703)

[Gavin Panella]
* debian/maas-cluster-controller.maas-clusterd.upstart:
  - Don't start-up the cluster controller unless a shared-secret has
    been installed.
* debian/maas-cluster-controller.maas-cluster-register.upstart: Drop.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2012-2014 Canonical Ltd.  This software is licensed under the
2
 
# GNU Affero General Public License version 3 (see the file LICENSE).
3
 
 
4
 
"""Twisted Application Plugin for the MAAS TFTP server."""
5
 
 
6
 
from __future__ import (
7
 
    absolute_import,
8
 
    print_function,
9
 
    unicode_literals,
10
 
    )
11
 
 
12
 
str = None
13
 
 
14
 
__metaclass__ = type
15
 
__all__ = [
16
 
    "TFTPBackend",
17
 
    "TFTPService",
18
 
    ]
19
 
 
20
 
import httplib
21
 
import json
22
 
from socket import (
23
 
    AF_INET,
24
 
    AF_INET6,
25
 
    )
26
 
from urllib import urlencode
27
 
from urlparse import (
28
 
    parse_qsl,
29
 
    urlparse,
30
 
    )
31
 
 
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 (
47
 
    IPv4Address,
48
 
    IPv6Address,
49
 
    )
50
 
from twisted.python.context import get
51
 
from twisted.web.client import getPage
52
 
import twisted.web.error
53
 
 
54
 
 
55
 
class TFTPBackend(FilesystemSynchronousBackend):
56
 
    """A partially dynamic read-only TFTP server.
57
 
 
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.
61
 
 
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.
65
 
 
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.
69
 
 
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.
74
 
    """
75
 
 
76
 
    get_page = staticmethod(getPage)
77
 
 
78
 
    def __init__(self, base_path, generator_url):
79
 
        """
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
83
 
            expected to accept.
84
 
        """
85
 
        super(TFTPBackend, self).__init__(
86
 
            base_path, can_read=True, can_write=False)
87
 
        self.generator_url = urlparse(generator_url)
88
 
 
89
 
    def get_generator_url(self, params):
90
 
        """Calculate the URL, including query, from which we can fetch
91
 
        additional configuration parameters.
92
 
 
93
 
        :param params: A dict, or iterable suitable for updating a dict, of
94
 
            additional query parameters.
95
 
        """
96
 
        query = {}
97
 
        # Merge parameters from the generator URL.
98
 
        query.update(parse_qsl(self.generator_url.query))
99
 
        # Merge parameters obtained from the request.
100
 
        query.update(params)
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")
106
 
 
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
113
 
        return None, None
114
 
 
115
 
    @deferred
116
 
    def get_kernel_params(self, params):
117
 
        """Return kernel parameters obtained from the API.
118
 
 
119
 
        :param params: Parameters so far obtained, typically from the file
120
 
            path requested.
121
 
        :return: A `KernelParameters` instance.
122
 
        """
123
 
        url = self.get_generator_url(params)
124
 
 
125
 
        def reassemble(data):
126
 
            return KernelParameters(**data)
127
 
 
128
 
        d = self.get_page(url)
129
 
        d.addCallback(json.loads)
130
 
        d.addCallback(reassemble)
131
 
        return d
132
 
 
133
 
    @deferred
134
 
    def get_boot_method_reader(self, boot_method, params):
135
 
        """Return an `IReader` for a boot method.
136
 
 
137
 
        :param boot_method: Boot method that is generating the config
138
 
        :param params: Parameters so far obtained, typically from the file
139
 
            path requested.
140
 
        """
141
 
        def generate(kernel_params):
142
 
            return boot_method.get_reader(
143
 
                self, kernel_params=kernel_params, **params)
144
 
 
145
 
        d = self.get_kernel_params(params)
146
 
        d.addCallback(generate)
147
 
        return d
148
 
 
149
 
    @staticmethod
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.
155
 
        try:
156
 
            status_int = int(failure.value.status)
157
 
        except ValueError:
158
 
            # Assume that it's some other error and propagate it
159
 
            return failure
160
 
 
161
 
        if status_int == httplib.NO_CONTENT:
162
 
            # Convert HTTP No Content to a TFTP file not found
163
 
            raise FileNotFound(file_name)
164
 
        else:
165
 
            # Otherwise propogate the unknown error
166
 
            return failure
167
 
 
168
 
    @deferred
169
 
    def get_reader(self, file_name):
170
 
        """See `IBackend.get_reader()`.
171
 
 
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
174
 
        the response.
175
 
        """
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)
179
 
 
180
 
        # Map pxe namespace architecture names to MAAS's.
181
 
        arch = params.get("arch")
182
 
        if arch is not None:
183
 
            maasarch = ArchitectureRegistry.get_by_pxealias(arch)
184
 
            if maasarch is not None:
185
 
                params["arch"] = maasarch.name.split("/")[0]
186
 
 
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)
195
 
        return d
196
 
 
197
 
 
198
 
class Port(udp.Port):
199
 
    """A :py:class:`udp.Port` that groks IPv6."""
200
 
 
201
 
    # This must be set by call sites.
202
 
    addressFamily = None
203
 
 
204
 
    def getHost(self):
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)
209
 
 
210
 
 
211
 
class UDPServer(internet.UDPServer):
212
 
    """A :py:class:`~internet.UDPServer` that groks IPv6.
213
 
 
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.
217
 
    """
218
 
 
219
 
    def _getPort(self):
220
 
        """See :py:meth:`twisted.application.internet.UDPServer._getPort`."""
221
 
        return self._listenUDP(*self.args, **self.kwargs)
222
 
 
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
227
 
        p.startListening()
228
 
        return p
229
 
 
230
 
 
231
 
class TFTPService(MultiService, object):
232
 
    """An umbrella service representing a set of running TFTP servers.
233
 
 
234
 
    Creates a UDP server individually for each discovered network
235
 
    interface, so that we can detect the interface via which we have
236
 
    received a datagram.
237
 
 
238
 
    It then periodically updates the servers running in case there's a
239
 
    change to the host machine's network configuration.
240
 
 
241
 
    :ivar backend: The :class:`TFTPBackend` being used to service TFTP
242
 
        requests.
243
 
 
244
 
    :ivar port: The port on which each server is started.
245
 
 
246
 
    :ivar refresher: A :class:`TimerService` that calls
247
 
        ``updateServers`` periodically.
248
 
 
249
 
    """
250
 
 
251
 
    def __init__(self, resource_root, port, generator):
252
 
        """
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.
258
 
        """
259
 
        super(TFTPService, self).__init__()
260
 
        self.backend = TFTPBackend(resource_root, generator)
261
 
        self.port = port
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)
270
 
 
271
 
    def getServers(self):
272
 
        """Return a set of all configured servers.
273
 
 
274
 
        :rtype: :class:`set` of :class:`internet.UDPServer`
275
 
        """
276
 
        return {
277
 
            service for service in self
278
 
            if service is not self.refresher
279
 
        }
280
 
 
281
 
    def updateServers(self):
282
 
        """Run a server on every interface.
283
 
 
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
287
 
        interfaces.
288
 
        """
289
 
        addrs_established = set(service.name for service in self.getServers())
290
 
        addrs_desired = set(get_all_interface_addresses())
291
 
 
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)
298
 
 
299
 
        for address in addrs_established - addrs_desired:
300
 
            tftp_service = self.getServiceNamed(address)
301
 
            tftp_service.disownServiceParent()