~ubuntu-branches/ubuntu/trusty/ironic/trusty-proposed

« back to all changes in this revision

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

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2014-03-06 13:23:35 UTC
  • mfrom: (1.1.2)
  • Revision ID: package-import@ubuntu.com-20140306132335-5b49ji56jffxvtn4
Tags: 2014.1~b3-0ubuntu1
* New upstream release:
  - debian/patches/fix-requirements.patch: Dropped no longer needed.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
1
# -*- encoding: utf-8 -*-
3
2
#
4
3
# Copyright 2013 Hewlett-Packard Development Company, L.P.
29
28
from ironic.common import image_service as service
30
29
from ironic.common import images
31
30
from ironic.common import keystone
 
31
from ironic.common import neutron
32
32
from ironic.common import paths
33
33
from ironic.common import states
34
34
from ironic.common import utils
40
40
from ironic.openstack.common import lockutils
41
41
from ironic.openstack.common import log as logging
42
42
from ironic.openstack.common import loopingcall
 
43
from ironic.openstack.common import strutils
43
44
 
44
45
pxe_opts = [
45
 
    cfg.StrOpt('deploy_kernel',
46
 
               help='Default kernel image ID used in deployment phase'),
47
 
    cfg.StrOpt('deploy_ramdisk',
48
 
               help='Default ramdisk image ID used in deployment phase'),
49
 
    cfg.StrOpt('net_config_template',
50
 
               default=paths.basedir_def('ironic/net-dhcp.ubuntu.template'),
51
 
               help='Template file for injected network config'),
52
46
    cfg.StrOpt('pxe_append_params',
53
47
               default='nofb nomodeset vga=normal',
54
 
               help='additional append parameters for baremetal PXE boot'),
 
48
               help='Additional append parameters for baremetal PXE boot.'),
55
49
    cfg.StrOpt('pxe_config_template',
56
50
               default=paths.basedir_def(
57
51
                    'drivers/modules/pxe_config.template'),
58
 
               help='Template file for PXE configuration'),
59
 
    cfg.IntOpt('pxe_deploy_timeout',
60
 
                help='Timeout for PXE deployments. Default: 0 (unlimited)',
61
 
                default=0),
 
52
               help='Template file for PXE configuration.'),
62
53
    cfg.StrOpt('tftp_server',
63
54
               default='$my_ip',
64
 
               help='IP address of Ironic compute node\'s tftp server'),
 
55
               help='IP address of Ironic compute node\'s tftp server.'),
65
56
    cfg.StrOpt('tftp_root',
66
57
               default='/tftpboot',
67
 
               help='Ironic compute node\'s tftp root path'),
 
58
               help='Ironic compute node\'s tftp root path.'),
68
59
    cfg.StrOpt('images_path',
69
60
               default='/var/lib/ironic/images/',
70
 
               help='Directory where images are stored on disk'),
 
61
               help='Directory where images are stored on disk.'),
71
62
    cfg.StrOpt('tftp_master_path',
72
63
               default='/tftpboot/master_images',
73
 
               help='Directory where master tftp images are stored on disk'),
 
64
               help='Directory where master tftp images are stored on disk.'),
74
65
    cfg.StrOpt('instance_master_path',
75
66
               default='/var/lib/ironic/master_images',
76
 
               help='Directory where master tftp images are stored on disk'),
 
67
               help='Directory where master instance images are stored on '
 
68
                    'disk.'),
77
69
    # NOTE(dekehn): Additional boot files options may be created in the event
78
70
    #  other architectures require different boot files.
79
71
    cfg.StrOpt('pxe_bootfile_name',
101
93
 
102
94
    info = node.get('driver_info', {})
103
95
    d_info = {}
104
 
    d_info['instance_name'] = info.get('pxe_instance_name', None)
105
 
    d_info['image_source'] = info.get('pxe_image_source', None)
106
 
    d_info['deploy_kernel'] = info.get('pxe_deploy_kernel',
107
 
                                       CONF.pxe.deploy_kernel)
108
 
    d_info['deploy_ramdisk'] = info.get('pxe_deploy_ramdisk',
109
 
                                        CONF.pxe.deploy_ramdisk)
110
 
    d_info['root_gb'] = info.get('pxe_root_gb', None)
 
96
    d_info['image_source'] = info.get('pxe_image_source')
 
97
    d_info['deploy_kernel'] = info.get('pxe_deploy_kernel')
 
98
    d_info['deploy_ramdisk'] = info.get('pxe_deploy_ramdisk')
 
99
    d_info['root_gb'] = info.get('pxe_root_gb')
111
100
 
112
101
    missing_info = []
113
102
    for label in d_info:
119
108
                "were not passed to ironic: %s") % missing_info)
120
109
 
121
110
    # Internal use only
122
 
    d_info['deploy_key'] = info.get('pxe_deploy_key', None)
 
111
    d_info['deploy_key'] = info.get('pxe_deploy_key')
123
112
 
124
113
    #TODO(ghe): Should we get rid of swap partition?
125
114
    d_info['swap_mb'] = info.get('pxe_swap_mb', 1)
126
 
    d_info['key_data'] = info.get('pxe_key_data', None)
 
115
    d_info['ephemeral_gb'] = info.get('pxe_ephemeral_gb', 0)
 
116
    d_info['ephemeral_format'] = info.get('pxe_ephemeral_format')
127
117
 
128
 
    for param in ('root_gb', 'swap_mb'):
 
118
    err_msg_invalid = _("Can not validate PXE bootloader. Invalid parameter "
 
119
                        "pxe_%(param)s. Reason: %(reason)s")
 
120
    for param in ('root_gb', 'swap_mb', 'ephemeral_gb'):
129
121
        try:
130
122
            int(d_info[param])
131
123
        except ValueError:
132
 
            raise exception.InvalidParameterValue(_(
133
 
                "Can not validate PXE bootloader. Invalid "
134
 
                "parameter %s") % param)
135
 
 
 
124
            reason = _("'%s' is not an integer value.") % d_info[param]
 
125
            raise exception.InvalidParameterValue(err_msg_invalid %
 
126
                                            {'param': param, 'reason': reason})
 
127
 
 
128
    if d_info['ephemeral_gb'] and not d_info['ephemeral_format']:
 
129
        msg = _("The deploy contains an ephemeral partition, but no "
 
130
                "filesystem type was specified by the pxe_ephemeral_format "
 
131
                "parameter")
 
132
        raise exception.InvalidParameterValue(msg)
 
133
 
 
134
    preserve_ephemeral = info.get('pxe_preserve_ephemeral', False)
 
135
    try:
 
136
        d_info['preserve_ephemeral'] = strutils.bool_from_string(
 
137
                                            preserve_ephemeral, strict=True)
 
138
    except ValueError as e:
 
139
        raise exception.InvalidParameterValue(err_msg_invalid %
 
140
                                  {'param': 'preserve_ephemeral', 'reason': e})
136
141
    return d_info
137
142
 
138
143
 
192
197
            return [p.address for p in r.ports]
193
198
 
194
199
 
 
200
def _get_node_vif_ids(task):
 
201
    """Get all Neutron VIF ids for a node.
 
202
       This function does not handle multi node operations.
 
203
 
 
204
    :param task: a TaskManager instance.
 
205
    :returns: A dict of the Node's port UUIDs and their associated VIFs
 
206
 
 
207
    """
 
208
    port_vifs = {}
 
209
    for port in task.resources[0].ports:
 
210
        vif = port.extra.get('vif_port_id')
 
211
        if vif:
 
212
            port_vifs[port.uuid] = vif
 
213
    return port_vifs
 
214
 
 
215
 
195
216
def _get_pxe_mac_path(mac):
196
217
    """Convert a MAC address into a PXE config file name.
197
218
 
300
321
        if not os.path.exists(lock_file):
301
322
            raise loopingcall.LoopingCallDone()
302
323
    # If the download of the image needed is in progress (lock file present)
303
 
    # we wait until the locks dissapears and create the link.
 
324
    # we wait until the locks disappears and create the link.
304
325
 
305
326
    if master_path is None:
306
327
        #NOTE(ghe): We don't share images between instances/hosts
331
352
 
332
353
def _cache_tftp_images(ctx, node, pxe_info):
333
354
    """Fetch the necessary kernels and ramdisks for the instance."""
334
 
    d_info = _parse_driver_info(node)
335
355
    fileutils.ensure_tree(
336
356
        os.path.join(CONF.pxe.tftp_root, node.uuid))
337
 
    LOG.debug(_("Fetching kernel and ramdisk for instance %s") %
338
 
              d_info['instance_name'])
 
357
    LOG.debug(_("Fetching kernel and ramdisk for node %s") %
 
358
              node.uuid)
339
359
    for label in pxe_info:
340
360
        (uuid, path) = pxe_info[label]
341
361
        if not os.path.exists(path):
364
384
    image_path = _get_image_file_path(node.uuid)
365
385
    uuid = d_info['image_source']
366
386
 
367
 
    LOG.debug(_("Fetching image %(ami)s for instance %(name)s") %
368
 
              {'ami': uuid, 'name': d_info['instance_name']})
 
387
    LOG.debug(_("Fetching image %(ami)s for node %(uuid)s") %
 
388
              {'ami': uuid, 'uuid': node.uuid})
369
389
 
370
390
    if not os.path.exists(image_path):
371
391
        _get_image(ctx, image_path, uuid, CONF.pxe.instance_master_path)
457
477
            ]
458
478
 
459
479
 
 
480
def _update_neutron(task, node):
 
481
    """Send or update the DHCP BOOT options to Neutron for this node."""
 
482
    options = _dhcp_options_for_instance()
 
483
    vifs = _get_node_vif_ids(task)
 
484
    if not vifs:
 
485
        LOG.warning(_("No VIFs found for node %(node)s when attempting to "
 
486
                      "update Neutron DHCP BOOT options."),
 
487
                      {'node': node.uuid})
 
488
        return
 
489
 
 
490
    # TODO(deva): decouple instantiation of NeutronAPI from task.context.
 
491
    #             Try to use the user's task.context.auth_token, but if it
 
492
    #             is not present, fall back to a server-generated context.
 
493
    #             We don't need to recreate this in every method call.
 
494
    api = neutron.NeutronAPI(task.context)
 
495
    failures = []
 
496
    for port_id, port_vif in vifs.iteritems():
 
497
        try:
 
498
            api.update_port_dhcp_opts(port_vif, options)
 
499
        except exception.FailedToUpdateDHCPOptOnPort:
 
500
            failures.append(port_id)
 
501
 
 
502
    if failures:
 
503
        if len(failures) == len(vifs):
 
504
            raise exception.FailedToUpdateDHCPOptOnPort(_(
 
505
                "Failed to set DHCP BOOT options for any port on node %s.") %
 
506
                node.uuid)
 
507
        else:
 
508
            LOG.warning(_("Some errors were encountered when updating the "
 
509
                          "DHCP BOOT options for node %(node)s on the "
 
510
                          "following ports: %(ports)s."),
 
511
                          {'node': node.uuid, 'ports': failures})
 
512
 
 
513
 
460
514
def _create_pxe_config(task, node, pxe_info):
461
515
    """Generate pxe configuration file and link mac ports to it for
462
516
    tftp booting.
475
529
        utils.create_link_without_raise(pxe_config_file_path, mac_path)
476
530
 
477
531
 
478
 
def _update_neutron(task, node):
479
 
    """Send the DHCP BOOT options to Neutron for this node."""
480
 
    # FIXME: just a stub for the moment.
481
 
    pass
482
 
 
483
 
 
484
532
class PXEDeploy(base.DeployInterface):
485
533
    """PXE Deploy Interface: just a stub until the real driver is ported."""
486
534
 
487
 
    def validate(self, node):
 
535
    def validate(self, task, node):
488
536
        """Validate the driver-specific Node deployment info.
489
537
 
490
 
        This method validates whether the 'driver_info' property of the
491
 
        supplied node contains the required information for this driver to
492
 
        deploy images to the node.
493
 
 
 
538
        :param task: a task from TaskManager.
494
539
        :param node: a single Node to validate.
495
540
        :returns: InvalidParameterValue.
496
541
        """
 
542
        if not _get_node_mac_addresses(task, node):
 
543
            raise exception.InvalidParameterValue(_("Node %s does not have "
 
544
                                "any port associated with it.") % node.uuid)
497
545
        _parse_driver_info(node)
498
546
 
499
547
    @task_manager.require_exclusive_lock
500
548
    def deploy(self, task, node):
501
549
        """Perform start deployment a node.
502
550
 
503
 
        Given a node with complete metadata, deploy the indicated image
504
 
        to the node.
 
551
        Creates a temporary keystone token file, updates the Neutron DHCP port
 
552
        options for next boot, and issues a reboot request to the power driver.
 
553
        This causes the node to boot into the deployment ramdisk and triggers
 
554
        the next phase of PXE-based deployment via
 
555
        VendorPassthru._continue_deploy().
505
556
 
506
557
        :param task: a TaskManager instance.
507
558
        :param node: the Node to act upon.
513
564
        _update_neutron(task, node)
514
565
        manager_utils.node_power_action(task, node, states.REBOOT)
515
566
 
516
 
        return states.DEPLOYING
 
567
        return states.DEPLOYWAIT
517
568
 
518
569
    @task_manager.require_exclusive_lock
519
570
    def tear_down(self, task, node):
520
571
        """Tear down a previous deployment.
521
572
 
522
 
        Given a node that has been previously deployed to,
523
 
        do all cleanup and tear down necessary to "un-deploy" that node.
 
573
        Power off the node. All actual clean-up is done in the clean_up()
 
574
        method which should be called separately.
524
575
 
525
576
        :param task: a TaskManager instance.
526
577
        :param node: the Node to act upon.
531
582
        return states.DELETED
532
583
 
533
584
    def prepare(self, task, node):
 
585
        """Prepare the deployment environment for this node.
 
586
 
 
587
        Generates the TFTP configuration for PXE-booting both the deployment
 
588
        and user images, fetches the images from Glance and adds them to the
 
589
        local cache.
 
590
 
 
591
        :param task: a TaskManager instance.
 
592
        :param node: the Node to act upon.
 
593
        """
534
594
        # TODO(deva): optimize this if rerun on existing files
535
595
        pxe_info = _get_tftp_image_info(node, task.context)
536
596
        _create_pxe_config(task, node, pxe_info)
537
597
        _cache_images(node, pxe_info, task.context)
538
598
 
539
599
    def clean_up(self, task, node):
 
600
        """Clean up the deployment environment for this node.
 
601
 
 
602
        Delete the deploy and user images from the local cache, if no remaining
 
603
        active nodes require them. Removes the TFTP configuration files for
 
604
        this node. As a precaution, this method also ensures the keystone auth
 
605
        token file was removed.
 
606
 
 
607
        :param task: a TaskManager instance.
 
608
        :param node: the Node to act upon.
 
609
        """
540
610
        # FIXME(ghe): Possible error to get image info if eliminated from
541
611
        #             glance. Retrieve image info and store in db.
542
612
        #             If we keep master images, no need to get the info,
567
637
 
568
638
class PXERescue(base.RescueInterface):
569
639
 
570
 
    def validate(self, node):
 
640
    def validate(self, task, node):
571
641
        pass
572
642
 
573
643
    def rescue(self, task, node):
595
665
                  'pxe_config_path': _get_pxe_config_file_path(
596
666
                                                    node.uuid),
597
667
                  'root_mb': 1024 * int(d_info['root_gb']),
598
 
                  'swap_mb': int(d_info['swap_mb'])
599
 
 
 
668
                  'swap_mb': int(d_info['swap_mb']),
 
669
                  'ephemeral_mb': 1024 * int(d_info['ephemeral_gb']),
 
670
                  'preserve_ephemeral': d_info['preserve_ephemeral'],
600
671
            }
601
672
 
602
673
        missing = [key for key in params.keys() if params[key] is None]
605
676
                    "Parameters %s were not passed to ironic"
606
677
                    " for deploy.") % missing)
607
678
 
 
679
        # ephemeral_format is nullable
 
680
        params['ephemeral_format'] = d_info.get('ephemeral_format')
 
681
 
608
682
        return params
609
683
 
610
 
    def validate(self, node, **kwargs):
 
684
    def validate(self, task, node, **kwargs):
611
685
        method = kwargs['method']
612
686
        if method == 'pass_deploy_info':
613
687
            self._get_deploy_info(node, **kwargs)
646
720
                node.last_error = msg
647
721
                node.save(task.context)
648
722
 
 
723
        if node.provision_state != states.DEPLOYWAIT:
 
724
            LOG.error(_('Node %s is not waiting to be deployed.') %
 
725
                      node.uuid)
 
726
            return
 
727
        node.provision_state = states.DEPLOYING
 
728
        node.save(task.context)
649
729
        # remove cached keystone token immediately
650
730
        _destroy_token_file(node)
651
731
 
663
743
 
664
744
        try:
665
745
            deploy_utils.deploy(**params)
666
 
        except Exception:
667
 
            # NOTE(deva): deploy() already logs any failure
668
 
            #             so we don't need to log it again here.
 
746
        except Exception as e:
 
747
            LOG.error(_('PXE deploy failed for instance %(instance)s. '
 
748
                        'Error: %(error)s') % {'instance': node.instance_uuid,
 
749
                                               'error': e})
669
750
            _set_failed_state(_('PXE driver failed to continue deployment.'))
670
751
        else:
671
752
            LOG.info(_('Deployment to node %s done') % node.uuid)