~ubuntu-branches/ubuntu/precise/maas/precise-security

« back to all changes in this revision

Viewing changes to src/maasserver/provisioning.py

  • Committer: Package Import Robot
  • Author(s): Andres Rodriguez, Scott Moser, Andres Rodriguez, Dave Walker (Daviey), Gavin Panella
  • Date: 2012-04-12 16:46:22 UTC
  • mfrom: (1.1.11)
  • Revision ID: package-import@ubuntu.com-20120412164622-u1qnsq0s9tsc2f14
Tags: 0.1+bzr462+dfsg-0ubuntu1
* New upstream release (LP: #980240)

[ Scott Moser ]
* add dependency on distro-info (LP: #949442)
* debian/control: add dependency on tgt for ephemeral iscsi environment

[ Andres Rodriguez ]
* Make package lintian clean:
  - maas{-dhcp}.lintian-overrides: Add to make lintian clean.
  - debian/control: Add missing dependencies; correct section and desc.
  - debian/maas.postinst: Do not use absolute path for rabbitmqctl.
  - debian/patches: Add headers to all patches.
* debian/maas-dhcp.postrm: Added to disable dnsmasq in cobbler on removal.
* debian/maas.config: Do not set a password with pwgen as it is not an
  essential package; allow dbconfig-common to create a password instead by
  creating an empty question. (LP: #977475)
* Run MAAS, pserv, txlongpoll as non-root user. (LP: #975436)
  - debian/maas.postinst: Create user/group; set correct permissions for
    directories.
  - debian/maas.postrm: Remove user/group; restart apache2.
  - debian/maas.maas-{pserv,txlongpoll}.upstart: Update to run as non-root
    'maas' user.
* debian/patches/01-fix-database-settings.patch: Remove adding of PSERV_URL.
* debian/maas.postinst:
  - Handle config file upgrade from versions lower than 0.1+bzr445+dfsg-0ubuntu1,
    by creating new passwords and updating accordingly
  - use local variables in functions.
  - Handle maas tgt configuration for upgrades from 0.1+bzr459+dfsg-0ubuntu1.
* debian/extras/99-maas: Add squid-deb-proxy file to enable PPAs. (LP: #979383)
* debian/maas.install: Install missing commissioning-user-data script.

[ Dave Walker (Daviey) ]
* debian/patches/02-pserv-config.patch: Refreshed to apply to updated config.

[ Gavin Panella ]
* debian/maas.postinst: Update pserv.yaml and maas_local_settings.py to use
  password.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
__metaclass__ = type
12
12
__all__ = [
13
13
    'get_provisioning_api_proxy',
 
14
    'present_detailed_user_friendly_fault',
14
15
    'ProvisioningProxy',
15
16
    ]
16
17
 
 
18
from functools import partial
17
19
from logging import getLogger
18
20
from textwrap import dedent
19
21
from urllib import urlencode
26
28
    post_save,
27
29
    )
28
30
from django.dispatch import receiver
 
31
from maasserver.components import (
 
32
    COMPONENT,
 
33
    discard_persistent_error,
 
34
    register_persistent_error,
 
35
    )
29
36
from maasserver.exceptions import MAASAPIException
30
37
from maasserver.models import (
31
38
    Config,
34
41
    NODE_STATUS,
35
42
    )
36
43
from provisioningserver.enum import PSERV_FAULT
 
44
import yaml
37
45
 
38
 
# Presentation templates for various provisioning faults.
39
 
PRESENTATIONS = {
 
46
# Presentation templates for various provisioning faults (will be used
 
47
# for long-lasting warnings about failing components).
 
48
DETAILED_PRESENTATIONS = {
40
49
    PSERV_FAULT.NO_COBBLER: """
41
50
        The provisioning server was unable to reach the Cobbler service:
42
51
        %(fault_string)s
69
78
        If the error message is not clear, you may need to check the
70
79
        Cobbler logs in /var/log/cobbler/ or pserv.log.
71
80
        """,
 
81
    PSERV_FAULT.COBBLER_DNS_LOOKUP_ERROR: """
 
82
        The provisioning server was unable to resolve the Cobbler server's
 
83
        DNS address: %(fault_string)s.
 
84
 
 
85
        Has Cobbler been properly installed and is it accessible by the
 
86
        provisioning server?  Check /var/log/cobbler/ and pserv.log.
 
87
        """,
72
88
    8002: """
73
89
        Unable to reach provisioning server (%(fault_string)s).
74
90
 
77
93
        """,
78
94
}
79
95
 
80
 
 
81
 
def present_user_friendly_fault(fault):
 
96
# Shorter presentation templates for various provisioning faults (will
 
97
# be used for one-off messages).
 
98
SHORT_PRESENTATIONS = {
 
99
    PSERV_FAULT.NO_COBBLER: """
 
100
        Unable to reach the Cobbler server.
 
101
        """,
 
102
    PSERV_FAULT.COBBLER_AUTH_FAILED: """
 
103
        Failed to authenticate with the Cobbler server.
 
104
        """,
 
105
    PSERV_FAULT.COBBLER_AUTH_ERROR: """
 
106
        Failed to authenticate with the Cobbler server.
 
107
        """,
 
108
    PSERV_FAULT.NO_SUCH_PROFILE: """
 
109
        Missing system profile: %(fault_string)s.
 
110
        """,
 
111
    PSERV_FAULT.GENERIC_COBBLER_ERROR: """
 
112
        Unknown problem encountered with the Cobbler server.
 
113
        """,
 
114
    PSERV_FAULT.COBBLER_DNS_LOOKUP_ERROR: """
 
115
        Unable to resolve the Cobbler server's DNS address:
 
116
        %(fault_string)s.
 
117
        """,
 
118
    8002: """
 
119
        Unable to reach provisioning server.
 
120
        """,
 
121
}
 
122
 
 
123
 
 
124
def _present_user_friendly_fault(fault, presentations):
82
125
    """Return a more user-friendly exception to represent `fault`.
83
126
 
84
127
    :param fault: An exception raised by, or received across, xmlrpc.
85
128
    :type fault: :class:`xmlrpclib.Fault`
 
129
    :param presentations: A mapping error -> message.
 
130
    :type fault: dict
86
131
    :return: A more user-friendly exception, if one can be produced.
87
132
        Otherwise, this returns None and the original exception should be
88
133
        re-raised.  (This is left to the caller in order to minimize
93
138
        'fault_code': fault.faultCode,
94
139
        'fault_string': fault.faultString,
95
140
    }
96
 
    user_friendly_text = PRESENTATIONS.get(fault.faultCode)
 
141
    user_friendly_text = presentations.get(fault.faultCode)
97
142
    if user_friendly_text is None:
98
143
        return None
99
144
    else:
101
146
            user_friendly_text.lstrip('\n') % params))
102
147
 
103
148
 
 
149
present_user_friendly_fault = partial(
 
150
    _present_user_friendly_fault, presentations=SHORT_PRESENTATIONS)
 
151
"""Return a concise but user-friendly exception to represent `fault`.
 
152
 
 
153
:param fault: An exception raised by, or received across, xmlrpc.
 
154
:type fault: :class:`xmlrpclib.Fault`
 
155
:return: A more user-friendly exception, if one can be produced.
 
156
    Otherwise, this returns None and the original exception should be
 
157
    re-raised.  (This is left to the caller in order to minimize
 
158
    erosion of the backtrace).
 
159
:rtype: :class:`MAASAPIException`, or None.
 
160
"""
 
161
 
 
162
 
 
163
present_detailed_user_friendly_fault = partial(
 
164
    _present_user_friendly_fault, presentations=DETAILED_PRESENTATIONS)
 
165
"""Return a detailed and user-friendly exception to represent `fault`.
 
166
 
 
167
:param fault: An exception raised by, or received across, xmlrpc.
 
168
:type fault: :class:`xmlrpclib.Fault`
 
169
:return: A more user-friendly exception, if one can be produced.
 
170
    Otherwise, this returns None and the original exception should be
 
171
    re-raised.  (This is left to the caller in order to minimize
 
172
    erosion of the backtrace).
 
173
:rtype: :class:`MAASAPIException`, or None.
 
174
"""
 
175
 
 
176
# A mapping method_name -> list of components.
 
177
# For each method name, indicate the list of components that the method
 
178
# uses.  This way, when calling the method is a success, if means that
 
179
# the related components are working properly.
 
180
METHOD_COMPONENTS = {
 
181
    'add_node': [COMPONENT.PSERV, COMPONENT.COBBLER, COMPONENT.IMPORT_ISOS],
 
182
    'modify_nodes': [COMPONENT.PSERV, COMPONENT.COBBLER],
 
183
    'delete_nodes_by_name': [COMPONENT.PSERV, COMPONENT.COBBLER],
 
184
}
 
185
 
 
186
# A mapping exception -> component.
 
187
# For each exception in this dict, the related component is there to
 
188
# tell us which component will be marked as 'failing' when this
 
189
# exception is raised.
 
190
EXCEPTIONS_COMPONENTS = {
 
191
    PSERV_FAULT.NO_COBBLER: COMPONENT.COBBLER,
 
192
    PSERV_FAULT.COBBLER_AUTH_FAILED: COMPONENT.COBBLER,
 
193
    PSERV_FAULT.COBBLER_AUTH_ERROR: COMPONENT.COBBLER,
 
194
    PSERV_FAULT.NO_SUCH_PROFILE: COMPONENT.IMPORT_ISOS,
 
195
    PSERV_FAULT.GENERIC_COBBLER_ERROR: COMPONENT.COBBLER,
 
196
    PSERV_FAULT.COBBLER_DNS_LOOKUP_ERROR: COMPONENT.COBBLER,
 
197
    8002: COMPONENT.PSERV,
 
198
}
 
199
 
 
200
 
 
201
def register_working_components(method_name):
 
202
    """Register that the components related to the provided method
 
203
    (if any) are working.
 
204
    """
 
205
    components = METHOD_COMPONENTS.get(method_name, [])
 
206
    for component in components:
 
207
        discard_persistent_error(component)
 
208
 
 
209
 
 
210
def register_failing_component(exception):
 
211
    """Register that the component corresponding to exception (if any)
 
212
    is failing.
 
213
    """
 
214
    component = EXCEPTIONS_COMPONENTS.get(exception.faultCode, None)
 
215
    if component is not None:
 
216
        detailed_friendly_fault = unicode(
 
217
            present_detailed_user_friendly_fault(exception))
 
218
        register_persistent_error(component, detailed_friendly_fault)
 
219
 
 
220
 
104
221
class ProvisioningCaller:
105
222
    """Wrapper for an XMLRPC call.
106
223
 
107
 
    Runs xmlrpc exceptions through `present_user_friendly_fault` for better
 
224
    - Runs xmlrpc exceptions through `present_user_friendly_fault` for better
108
225
    presentation to the user.
 
226
    - Registers failing/working components.
109
227
    """
110
228
 
111
 
    def __init__(self, method):
 
229
    def __init__(self, method_name, method):
 
230
        # Keep track of the method name; xmlrpclib does not take lightly
 
231
        # to us attempting to look it up as an attribute of the method
 
232
        # object.
 
233
        self.method_name = method_name
112
234
        self.method = method
113
235
 
114
 
    def __call__(self, *args, **kwargs):
 
236
    def __call__(self, *args):
115
237
        try:
116
 
            return self.method(*args, **kwargs)
 
238
            result = self.method(*args)
117
239
        except xmlrpclib.Fault as e:
 
240
            # Register failing component.
 
241
            register_failing_component(e)
 
242
            # Raise a more user-friendly error.
118
243
            friendly_fault = present_user_friendly_fault(e)
119
244
            if friendly_fault is None:
120
245
                raise
121
246
            else:
122
247
                raise friendly_fault
 
248
        else:
 
249
            # The call was a success, discard persistent errors for
 
250
            # components referenced by this method.
 
251
            register_working_components(self.method_name)
 
252
            return result
123
253
 
124
254
 
125
255
class ProvisioningProxy:
133
263
    def __init__(self, xmlrpc_proxy):
134
264
        self.proxy = xmlrpc_proxy
135
265
 
136
 
    def patch(self, method, replacement):
137
 
        setattr(self.proxy, method, replacement)
138
 
 
139
266
    def __getattr__(self, attribute_name):
140
267
        """Return a wrapped version of the requested method."""
141
268
        attribute = getattr(self.proxy, attribute_name)
142
 
        if getattr(attribute, '__call__', None) is None:
 
269
        if callable(attribute):
 
270
            # This attribute is callable.  Wrap it in a caller.
 
271
            return ProvisioningCaller(attribute_name, attribute)
 
272
        else:
143
273
            # This is a regular attribute.  Return it as-is.
144
274
            return attribute
145
 
        else:
146
 
            # This attribute is callable.  Wrap it in a caller.
147
 
            return ProvisioningCaller(attribute)
 
275
 
 
276
 
 
277
class ProvisioningTransport(xmlrpclib.Transport):
 
278
    """An XML-RPC transport that sets a low socket timeout."""
 
279
 
 
280
    @property
 
281
    def timeout(self):
 
282
        return settings.PSERV_TIMEOUT
 
283
 
 
284
    def make_connection(self, host):
 
285
        """See `xmlrpclib.Transport.make_connection`.
 
286
 
 
287
        This also sets the desired socket timeout.
 
288
        """
 
289
        connection = xmlrpclib.Transport.make_connection(self, host)
 
290
        connection.timeout = self.timeout
 
291
        return connection
148
292
 
149
293
 
150
294
def get_provisioning_api_proxy():
157
301
    if settings.USE_REAL_PSERV:
158
302
        # Use a real provisioning server.  This requires PSERV_URL to be
159
303
        # set.
 
304
        xmlrpc_transport = ProvisioningTransport(use_datetime=True)
160
305
        xmlrpc_proxy = xmlrpclib.ServerProxy(
161
 
            settings.PSERV_URL, allow_none=True, use_datetime=True)
 
306
            settings.PSERV_URL, transport=xmlrpc_transport, allow_none=True)
162
307
    else:
163
308
        # Create a fake.  The code that provides the testing fake is not
164
309
        # available in an installed production system, so import it only
187
332
    return urljoin(maas_url, path)
188
333
 
189
334
 
190
 
def compose_metadata(node):
191
 
    """Put together metadata information for `node`.
192
 
 
193
 
    :param node: The node to provide with metadata.
194
 
    :type node: Node
195
 
    :return: A dict containing metadata information that will be seeded to
196
 
        the node, so that it can access the metadata service.
197
 
    """
198
 
    # Circular import.
199
 
    from metadataserver.models import NodeKey
200
 
    token = NodeKey.objects.get_token_for_node(node)
 
335
def compose_cloud_init_preseed(token):
 
336
    """Compose the preseed value for a node in any state but Commissioning."""
201
337
    credentials = urlencode({
202
338
        'oauth_consumer_key': token.consumer.key,
203
339
        'oauth_token_key': token.key,
204
340
        'oauth_token_secret': token.secret,
205
341
        })
206
 
    return {
207
 
        'maas-metadata-url': get_metadata_server_url(),
208
 
        'maas-metadata-credentials': credentials,
209
 
    }
 
342
 
 
343
    # Preseed data to send to cloud-init.  We set this as MAAS_PRESEED in
 
344
    # ks_meta, and it gets fed straight into debconf.
 
345
    metadata_preseed_items = [
 
346
        ('datasources', 'multiselect', 'MAAS'),
 
347
        ('maas-metadata-url', 'string', get_metadata_server_url()),
 
348
        ('maas-metadata-credentials', 'string', credentials),
 
349
        ]
 
350
 
 
351
    return '\n'.join(
 
352
        "cloud-init   cloud-init/%s  %s %s" % (
 
353
            item_name,
 
354
            item_type,
 
355
            item_value,
 
356
            )
 
357
        for item_name, item_type, item_value in metadata_preseed_items)
 
358
 
 
359
 
 
360
def compose_commissioning_preseed(token):
 
361
    """Compose the preseed value for a Commissioning node."""
 
362
    return "#cloud-config\n%s" % yaml.dump({
 
363
        'datasource': {
 
364
            'MAAS': {
 
365
                'metadata_url': get_metadata_server_url(),
 
366
                'consumer_key': token.consumer.key,
 
367
                'token_key': token.key,
 
368
                'token_secret': token.secret,
 
369
            }
 
370
        }
 
371
    })
 
372
 
 
373
 
 
374
def compose_preseed(node):
 
375
    """Put together preseed data for `node`.
 
376
 
 
377
    This produces preseed data in different formats depending on the node's
 
378
    state: if it's Commissioning, it boots into commissioning mode with its
 
379
    own profile, its own user_data, and also its own preseed format.  It's
 
380
    basically a network boot.
 
381
    Otherwise, it will get a different format that feeds directly into the
 
382
    installer.
 
383
 
 
384
    :param node: The node to compose preseed data for.
 
385
    :type node: Node
 
386
    :return: Preseed data containing the information the node needs in order
 
387
        to access the metadata service: its URL and auth token.
 
388
    """
 
389
    # Circular import.
 
390
    from metadataserver.models import NodeKey
 
391
    token = NodeKey.objects.get_token_for_node(node)
 
392
    if node.status == NODE_STATUS.COMMISSIONING:
 
393
        return compose_commissioning_preseed(token)
 
394
    else:
 
395
        return compose_cloud_init_preseed(token)
210
396
 
211
397
 
212
398
def name_arch_in_cobbler_style(architecture):
245
431
    papi = get_provisioning_api_proxy()
246
432
    profile = select_profile_for_node(instance)
247
433
    power_type = instance.get_effective_power_type()
248
 
    metadata = compose_metadata(instance)
 
434
    preseed_data = compose_preseed(instance)
249
435
    papi.add_node(
250
436
        instance.system_id, instance.hostname,
251
 
        profile, power_type, metadata)
 
437
        profile, power_type, preseed_data)
 
438
 
 
439
    # When the node is allocated this must not modify the netboot_enabled
 
440
    # parameter. The node, once it has booted and installed itself, asks the
 
441
    # provisioning server to disable netbooting. If this were to enable
 
442
    # netbooting again, the node would reinstall itself the next time it
 
443
    # booted. However, netbooting must be enabled at the point the node is
 
444
    # allocated so that the first install goes ahead, hence why it is set for
 
445
    # all other statuses... with one exception; retired nodes are never
 
446
    # netbooted.
 
447
    if instance.status != NODE_STATUS.ALLOCATED:
 
448
        netboot_enabled = instance.status not in (
 
449
            NODE_STATUS.DECLARED, NODE_STATUS.RETIRED)
 
450
        delta = {"netboot_enabled": netboot_enabled}
 
451
        papi.modify_nodes({instance.system_id: delta})
252
452
 
253
453
 
254
454
def set_node_mac_addresses(node):