~ubuntu-branches/ubuntu/vivid/ironic/vivid-updates

« back to all changes in this revision

Viewing changes to ironic/drivers/modules/agent.py

  • Committer: Package Import Robot
  • Author(s): James Page
  • Date: 2015-01-05 12:21:37 UTC
  • mfrom: (1.2.4)
  • Revision ID: package-import@ubuntu.com-20150105122137-171bqrdpcxqipunk
Tags: 2015.1~b1-0ubuntu1
* New upstream beta release:
  - d/control: Align version requirements with upstream release.
* d/watch: Update uversionmangle to deal with kilo beta versioning
  changes.
* d/control: Bumped Standards-Version to 3.9.6, no changes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
33
33
from ironic.conductor import utils as manager_utils
34
34
from ironic.drivers import base
35
35
from ironic.drivers.modules import agent_client
 
36
from ironic.drivers.modules import deploy_utils
36
37
from ironic.drivers.modules import image_cache
37
38
from ironic import objects
38
39
from ironic.openstack.common import fileutils
113
114
    return pxe_utils.get_deploy_kr_info(node.uuid, node.driver_info)
114
115
 
115
116
 
116
 
def _set_failed_state(task, msg):
117
 
    """Set a node's error state and provision state to signal Nova.
118
 
 
119
 
    When deploy steps aren't called by explicitly the conductor, but are
120
 
    the result of callbacks, we need to set the node's state explicitly.
121
 
    This tells Nova to change the instance's status so the user can see
122
 
    their deploy/tear down had an issue and makes debugging/deleting Nova
123
 
    instances easier.
124
 
    """
125
 
    node = task.node
126
 
    node.provision_state = states.DEPLOYFAIL
127
 
    node.target_provision_state = states.NOSTATE
128
 
    node.save()
129
 
    try:
130
 
        manager_utils.node_power_action(task, states.POWER_OFF)
131
 
    except Exception:
132
 
        msg = (_('Node %s failed to power off while handling deploy '
133
 
                 'failure. This may be a serious condition. Node '
134
 
                 'should be removed from Ironic or put in maintenance '
135
 
                 'mode until the problem is resolved.') % node.uuid)
136
 
        LOG.error(msg)
137
 
    finally:
138
 
        # NOTE(deva): node_power_action() erases node.last_error
139
 
        #             so we need to set it again here.
140
 
        node.last_error = msg
141
 
        node.save()
142
 
 
143
 
 
144
117
@image_cache.cleanup(priority=25)
145
118
class AgentTFTPImageCache(image_cache.ImageCache):
146
119
    def __init__(self, image_service=None):
153
126
            image_service=image_service)
154
127
 
155
128
 
156
 
# copied from pxe driver - should be refactored per LP1350594
157
 
def _fetch_images(ctx, cache, images_info):
158
 
    """Check for available disk space and fetch images using ImageCache.
159
 
 
160
 
    :param ctx: context
161
 
    :param cache: ImageCache instance to use for fetching
162
 
    :param images_info: list of tuples (image uuid, destination path)
163
 
    :raises: InstanceDeployFailure if unable to find enough disk space
164
 
    """
165
 
 
166
 
    try:
167
 
        image_cache.clean_up_caches(ctx, cache.master_dir, images_info)
168
 
    except exception.InsufficientDiskSpace as e:
169
 
        raise exception.InstanceDeployFailure(reason=e)
170
 
 
171
 
    # NOTE(dtantsur): This code can suffer from race condition,
172
 
    # if disk space is used between the check and actual download.
173
 
    # This is probably unavoidable, as we can't control other
174
 
    # (probably unrelated) processes
175
 
    for uuid, path in images_info:
176
 
        cache.fetch_image(uuid, path, ctx=ctx)
177
 
 
178
 
 
179
 
# copied from pxe driver - should be refactored per LP1350594
180
129
def _cache_tftp_images(ctx, node, pxe_info):
181
130
    """Fetch the necessary kernels and ramdisks for the instance."""
182
131
    fileutils.ensure_tree(
183
132
        os.path.join(CONF.pxe.tftp_root, node.uuid))
184
133
    LOG.debug("Fetching kernel and ramdisk for node %s",
185
134
              node.uuid)
186
 
    _fetch_images(ctx, AgentTFTPImageCache(), pxe_info.values())
 
135
    deploy_utils.fetch_images(ctx, AgentTFTPImageCache(), pxe_info.values())
187
136
 
188
137
 
189
138
def build_instance_info_for_deploy(task):
204
153
 
205
154
    instance_info['image_url'] = swift_temp_url
206
155
    instance_info['image_checksum'] = image_info['checksum']
 
156
    instance_info['image_disk_format'] = image_info['disk_format']
 
157
    instance_info['image_container_format'] = image_info['container_format']
207
158
    return instance_info
208
159
 
209
160
 
220
171
    def validate(self, task):
221
172
        """Validate the driver-specific Node deployment info.
222
173
 
223
 
        This method validates whether the 'instance_info' property of the
224
 
        supplied node contains the required information for this driver to
225
 
        deploy images to the node.
 
174
        This method validates whether the properties of the supplied node
 
175
        contain the required information for this driver to deploy images to
 
176
        the node.
226
177
 
227
178
        :param task: a TaskManager instance
228
 
        :raises: InvalidParameterValue
 
179
        :raises: MissingParameterValue
229
180
        """
230
 
        try:
231
 
            _get_tftp_image_info(task.node)
232
 
        except KeyError:
233
 
            raise exception.InvalidParameterValue(_(
234
 
                    'Node %s failed to validate deploy image info') %
235
 
                    task.node.uuid)
 
181
        node = task.node
 
182
        params = {}
 
183
        params['driver_info.deploy_kernel'] = node.driver_info.get(
 
184
                                                              'deploy_kernel')
 
185
        params['driver_info.deploy_ramdisk'] = node.driver_info.get(
 
186
                                                              'deploy_ramdisk')
 
187
        params['instance_info.image_source'] = node.instance_info.get(
 
188
                                                               'image_source')
 
189
        error_msg = _('Node %s failed to validate deploy image info. Some '
 
190
                      'parameters were missing') % node.uuid
 
191
        deploy_utils.check_for_missing_params(params, error_msg)
236
192
 
237
193
    @task_manager.require_exclusive_lock
238
194
    def deploy(self, task):
326
282
 
327
283
 
328
284
class AgentVendorInterface(base.VendorInterface):
 
285
 
329
286
    def __init__(self):
330
 
        self.vendor_routes = {
331
 
            'heartbeat': self._heartbeat
332
 
        }
333
 
        self.driver_routes = {
334
 
            'lookup': self._lookup,
335
 
        }
336
287
        self.supported_payload_versions = ['2']
337
288
        self._client = _get_client()
338
289
 
354
305
        """
355
306
        pass
356
307
 
357
 
    def driver_vendor_passthru(self, task, method, **kwargs):
358
 
        """A node that does not know its UUID should POST to this method.
359
 
        Given method, route the command to the appropriate private function.
360
 
        """
361
 
        if method not in self.driver_routes:
362
 
            raise exception.InvalidParameterValue(_('No handler for method %s')
363
 
                                                  % method)
364
 
        func = self.driver_routes[method]
365
 
        return func(task, **kwargs)
366
 
 
367
 
    def vendor_passthru(self, task, **kwargs):
368
 
        """A node that knows its UUID should heartbeat to this passthru.
369
 
 
370
 
        It will get its node object back, with what Ironic thinks its provision
371
 
        state is and the target provision state is.
372
 
        """
373
 
        method = kwargs['method']  # Existence checked in mixin
374
 
        if method not in self.vendor_routes:
375
 
            raise exception.InvalidParameterValue(_('No handler for method '
376
 
                                                    '%s') % method)
377
 
        func = self.vendor_routes[method]
378
 
        try:
379
 
            return func(task, **kwargs)
380
 
        except exception.IronicException as e:
381
 
            with excutils.save_and_reraise_exception():
382
 
                # log this because even though the exception is being
383
 
                # reraised, it won't be handled if it is an async. call.
384
 
                LOG.exception(_LE('vendor_passthru failed with method %s'),
385
 
                              method)
386
 
        except Exception as e:
387
 
            # catch-all in case something bubbles up here
388
 
            # log this because even though the exception is being
389
 
            # reraised, it won't be handled if it is an async. call.
390
 
            LOG.exception(_LE('vendor_passthru failed with method %s'), method)
391
 
            raise exception.VendorPassthruException(message=e)
392
 
 
393
 
    def _heartbeat(self, task, **kwargs):
 
308
    def driver_validate(self, method, **kwargs):
 
309
        """Validate the driver deployment info.
 
310
 
 
311
        :param method: method to be validated.
 
312
        """
 
313
        version = kwargs.get('version')
 
314
 
 
315
        if not version:
 
316
            raise exception.MissingParameterValue(_('Missing parameter '
 
317
                                                    'version'))
 
318
        if version not in self.supported_payload_versions:
 
319
            raise exception.InvalidParameterValue(_('Unknown lookup '
 
320
                                                    'payload version: %s')
 
321
                                                    % version)
 
322
 
 
323
    @base.passthru(['POST'])
 
324
    def heartbeat(self, task, **kwargs):
394
325
        """Method for agent to periodically check in.
395
326
 
396
327
        The agent should be sending its agent_url (so Ironic can talk back)
397
 
        as a kwarg.
398
 
 
399
 
        kwargs should have the following format:
400
 
        {
401
 
            'agent_url': 'http://AGENT_HOST:AGENT_PORT'
402
 
        }
403
 
                AGENT_PORT defaults to 9999.
 
328
        as a kwarg. kwargs should have the following format::
 
329
 
 
330
         {
 
331
             'agent_url': 'http://AGENT_HOST:AGENT_PORT'
 
332
         }
 
333
 
 
334
        AGENT_PORT defaults to 9999.
404
335
        """
405
336
        node = task.node
406
337
        driver_info = node.driver_info
409
340
            {'node': node.uuid,
410
341
             'heartbeat': driver_info.get('agent_last_heartbeat')})
411
342
        driver_info['agent_last_heartbeat'] = int(_time())
412
 
        # FIXME(rloo): This could raise KeyError exception if 'agent_url'
413
 
        #              wasn't specified. Instead, raise MissingParameterValue.
414
 
        driver_info['agent_url'] = kwargs['agent_url']
 
343
        try:
 
344
            driver_info['agent_url'] = kwargs['agent_url']
 
345
        except KeyError:
 
346
            raise exception.MissingParameterValue(_('For heartbeat operation, '
 
347
                                                    '"agent_url" must be '
 
348
                                                    'specified.'))
415
349
 
416
350
        node.driver_info = driver_info
417
351
        node.save()
418
352
 
419
353
        # Async call backs don't set error state on their own
420
354
        # TODO(jimrollenhagen) improve error messages here
 
355
        msg = _('Failed checking if deploy is done.')
421
356
        try:
422
357
            if node.provision_state == states.DEPLOYWAIT:
423
358
                msg = _('Node failed to get image for deploy.')
430
365
            LOG.exception(_LE('Async exception for %(node)s: %(msg)s'),
431
366
                          {'node': node,
432
367
                           'msg': msg})
433
 
            _set_failed_state(task, msg)
 
368
            deploy_utils.set_failed_state(task, msg)
434
369
 
435
370
    def _deploy_is_done(self, node):
436
371
        return self._client.deploy_is_done(node)
442
377
        LOG.debug('Continuing deploy for %s', node.uuid)
443
378
 
444
379
        image_info = {
445
 
            'id': image_source,
 
380
            'id': image_source.split('/')[-1],
446
381
            'urls': [node.instance_info['image_url']],
447
382
            'checksum': node.instance_info['image_checksum'],
 
383
            # NOTE(comstud): Older versions of ironic do not set
 
384
            # 'disk_format' nor 'container_format', so we use .get()
 
385
            # to maintain backwards compatibility in case code was
 
386
            # upgraded in the middle of a build request.
 
387
            'disk_format': node.instance_info.get('image_disk_format'),
 
388
            'container_format': node.instance_info.get(
 
389
                'image_container_format')
448
390
        }
449
391
 
450
392
        # Tell the client to download and write the image with the given args
473
415
            msg = _('node %(node)s command status errored: %(error)s') % (
474
416
                   {'node': node.uuid, 'error': error})
475
417
            LOG.error(msg)
476
 
            _set_failed_state(task, msg)
 
418
            deploy_utils.set_failed_state(task, msg)
477
419
            return
478
420
 
479
421
        LOG.debug('Rebooting node %s to disk', node.uuid)
485
427
        node.target_provision_state = states.NOSTATE
486
428
        node.save()
487
429
 
488
 
    def _lookup(self, context, **kwargs):
489
 
        """Method to be called the first time a ramdisk agent checks in. This
 
430
    @base.driver_passthru(['POST'], async=False)
 
431
    def lookup(self, context, **kwargs):
 
432
        """Find a matching node for the agent.
 
433
 
 
434
        Method to be called the first time a ramdisk agent checks in. This
490
435
        can be because this is a node just entering decom or a node that
491
436
        rebooted for some reason. We will use the mac addresses listed in the
492
437
        kwargs to find the matching node, then return the node object to the
493
 
        agent. The agent can that use that UUID to use the normal vendor
 
438
        agent. The agent can that use that UUID to use the node vendor
494
439
        passthru method.
495
440
 
496
441
        Currently, we don't handle the instance where the agent doesn't have
497
442
        a matching node (i.e. a brand new, never been in Ironic node).
498
443
 
499
 
        kwargs should have the following format:
500
 
        {
501
 
            "version": "2"
502
 
            "inventory": {
503
 
                "interfaces": [
504
 
                    {
505
 
                        "name": "eth0",
506
 
                        "mac_address": "00:11:22:33:44:55",
507
 
                        "switch_port_descr": "port24"
508
 
                        "switch_chassis_descr": "tor1"
509
 
                    },
510
 
                    ...
511
 
                ], ...
512
 
            }
513
 
        }
 
444
        kwargs should have the following format::
 
445
 
 
446
         {
 
447
             "version": "2"
 
448
             "inventory": {
 
449
                 "interfaces": [
 
450
                     {
 
451
                         "name": "eth0",
 
452
                         "mac_address": "00:11:22:33:44:55",
 
453
                         "switch_port_descr": "port24"
 
454
                         "switch_chassis_descr": "tor1"
 
455
                     }, ...
 
456
                 ], ...
 
457
             }
 
458
         }
514
459
 
515
460
        The interfaces list should include a list of the non-IPMI MAC addresses
516
461
        in the form aa:bb:cc:dd:ee:ff.
523
468
        :raises: NotFound if no matching node is found.
524
469
        :raises: InvalidParameterValue with unknown payload version
525
470
        """
526
 
        version = kwargs.get('version')
527
 
 
528
 
        if version not in self.supported_payload_versions:
529
 
            raise exception.InvalidParameterValue(_('Unknown lookup payload'
530
 
                                                    'version: %s') % version)
531
 
        interfaces = self._get_interfaces(version, kwargs)
 
471
        inventory = kwargs.get('inventory')
 
472
        interfaces = self._get_interfaces(inventory)
532
473
        mac_addresses = self._get_mac_addresses(interfaces)
533
474
 
534
475
        node = self._find_node_by_macs(context, mac_addresses)
553
494
            'node': node
554
495
        }
555
496
 
556
 
    def _get_interfaces(self, version, inventory):
 
497
    def _get_interfaces(self, inventory):
557
498
        interfaces = []
558
499
        try:
559
 
            interfaces = inventory['inventory']['interfaces']
 
500
            interfaces = inventory['interfaces']
560
501
        except (KeyError, TypeError):
561
502
            raise exception.InvalidParameterValue(_(
562
503
                'Malformed network interfaces lookup: %s') % inventory)
564
505
        return interfaces
565
506
 
566
507
    def _get_mac_addresses(self, interfaces):
567
 
        """Returns MACs for the network devices
568
 
        """
 
508
        """Returns MACs for the network devices."""
569
509
        mac_addresses = []
570
510
 
571
511
        for interface in interfaces:
578
518
        return mac_addresses
579
519
 
580
520
    def _find_node_by_macs(self, context, mac_addresses):
581
 
        """Given a list of MAC addresses, find the ports that match the MACs
 
521
        """Get nodes for a given list of MAC addresses.
 
522
 
 
523
        Given a list of MAC addresses, find the ports that match the MACs
582
524
        and return the node they are all connected to.
583
525
 
584
526
        :raises: NodeNotFound if the ports point to multiple nodes or no
600
542
        return node
601
543
 
602
544
    def _find_ports_by_macs(self, context, mac_addresses):
603
 
        """Given a list of MAC addresses, find the ports that match the MACs
 
545
        """Get ports for a given list of MAC addresses.
 
546
 
 
547
        Given a list of MAC addresses, find the ports that match the MACs
604
548
        and return them as a list of Port objects, or an empty list if there
605
549
        are no matches
606
550
        """
617
561
        return ports
618
562
 
619
563
    def _get_node_id(self, ports):
620
 
        """Given a list of ports, either return the node_id they all share or
 
564
        """Get a node ID for a list of ports.
 
565
 
 
566
        Given a list of ports, either return the node_id they all share or
621
567
        raise a NotFound if there are multiple node_ids, which indicates some
622
568
        ports are connected to one node and the remaining port(s) are connected
623
569
        to one or more other nodes.