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.
102
PSERV_FAULT.COBBLER_AUTH_FAILED: """
103
Failed to authenticate with the Cobbler server.
105
PSERV_FAULT.COBBLER_AUTH_ERROR: """
106
Failed to authenticate with the Cobbler server.
108
PSERV_FAULT.NO_SUCH_PROFILE: """
109
Missing system profile: %(fault_string)s.
111
PSERV_FAULT.GENERIC_COBBLER_ERROR: """
112
Unknown problem encountered with the Cobbler server.
114
PSERV_FAULT.COBBLER_DNS_LOOKUP_ERROR: """
115
Unable to resolve the Cobbler server's DNS address:
119
Unable to reach provisioning server.
124
def _present_user_friendly_fault(fault, presentations):
82
125
"""Return a more user-friendly exception to represent `fault`.
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.
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
101
146
user_friendly_text.lstrip('\n') % params))
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`.
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.
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`.
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.
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],
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,
201
def register_working_components(method_name):
202
"""Register that the components related to the provided method
203
(if any) are working.
205
components = METHOD_COMPONENTS.get(method_name, [])
206
for component in components:
207
discard_persistent_error(component)
210
def register_failing_component(exception):
211
"""Register that the component corresponding to exception (if any)
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)
104
221
class ProvisioningCaller:
105
222
"""Wrapper for an XMLRPC call.
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.
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
233
self.method_name = method_name
112
234
self.method = method
114
def __call__(self, *args, **kwargs):
236
def __call__(self, *args):
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:
122
247
raise friendly_fault
249
# The call was a success, discard persistent errors for
250
# components referenced by this method.
251
register_working_components(self.method_name)
125
255
class ProvisioningProxy:
133
263
def __init__(self, xmlrpc_proxy):
134
264
self.proxy = xmlrpc_proxy
136
def patch(self, method, replacement):
137
setattr(self.proxy, method, replacement)
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)
143
273
# This is a regular attribute. Return it as-is.
146
# This attribute is callable. Wrap it in a caller.
147
return ProvisioningCaller(attribute)
277
class ProvisioningTransport(xmlrpclib.Transport):
278
"""An XML-RPC transport that sets a low socket timeout."""
282
return settings.PSERV_TIMEOUT
284
def make_connection(self, host):
285
"""See `xmlrpclib.Transport.make_connection`.
287
This also sets the desired socket timeout.
289
connection = xmlrpclib.Transport.make_connection(self, host)
290
connection.timeout = self.timeout
150
294
def get_provisioning_api_proxy():
187
332
return urljoin(maas_url, path)
190
def compose_metadata(node):
191
"""Put together metadata information for `node`.
193
:param node: The node to provide with metadata.
195
:return: A dict containing metadata information that will be seeded to
196
the node, so that it can access the metadata service.
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,
207
'maas-metadata-url': get_metadata_server_url(),
208
'maas-metadata-credentials': credentials,
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),
352
"cloud-init cloud-init/%s %s %s" % (
357
for item_name, item_type, item_value in metadata_preseed_items)
360
def compose_commissioning_preseed(token):
361
"""Compose the preseed value for a Commissioning node."""
362
return "#cloud-config\n%s" % yaml.dump({
365
'metadata_url': get_metadata_server_url(),
366
'consumer_key': token.consumer.key,
367
'token_key': token.key,
368
'token_secret': token.secret,
374
def compose_preseed(node):
375
"""Put together preseed data for `node`.
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
384
:param node: The node to compose preseed data for.
386
:return: Preseed data containing the information the node needs in order
387
to access the metadata service: its URL and auth token.
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)
395
return compose_cloud_init_preseed(token)
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)
250
436
instance.system_id, instance.hostname,
251
profile, power_type, metadata)
437
profile, power_type, preseed_data)
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
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})
254
454
def set_node_mac_addresses(node):