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
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)',
52
help='Template file for PXE configuration.'),
62
53
cfg.StrOpt('tftp_server',
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 '
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',
102
94
info = node.get('driver_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')
112
101
missing_info = []
113
102
for label in d_info:
119
108
"were not passed to ironic: %s") % missing_info)
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')
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')
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'):
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)
124
reason = _("'%s' is not an integer value.") % d_info[param]
125
raise exception.InvalidParameterValue(err_msg_invalid %
126
{'param': param, 'reason': reason})
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 "
132
raise exception.InvalidParameterValue(msg)
134
preserve_ephemeral = info.get('pxe_preserve_ephemeral', False)
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})
192
197
return [p.address for p in r.ports]
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.
204
:param task: a TaskManager instance.
205
:returns: A dict of the Node's port UUIDs and their associated VIFs
209
for port in task.resources[0].ports:
210
vif = port.extra.get('vif_port_id')
212
port_vifs[port.uuid] = vif
195
216
def _get_pxe_mac_path(mac):
196
217
"""Convert a MAC address into a PXE config file name.
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") %
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']
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})
370
390
if not os.path.exists(image_path):
371
391
_get_image(ctx, image_path, uuid, CONF.pxe.instance_master_path)
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)
485
LOG.warning(_("No VIFs found for node %(node)s when attempting to "
486
"update Neutron DHCP BOOT options."),
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)
496
for port_id, port_vif in vifs.iteritems():
498
api.update_port_dhcp_opts(port_vif, options)
499
except exception.FailedToUpdateDHCPOptOnPort:
500
failures.append(port_id)
503
if len(failures) == len(vifs):
504
raise exception.FailedToUpdateDHCPOptOnPort(_(
505
"Failed to set DHCP BOOT options for any port on node %s.") %
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})
460
514
def _create_pxe_config(task, node, pxe_info):
461
515
"""Generate pxe configuration file and link mac ports to it for
475
529
utils.create_link_without_raise(pxe_config_file_path, mac_path)
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.
484
532
class PXEDeploy(base.DeployInterface):
485
533
"""PXE Deploy Interface: just a stub until the real driver is ported."""
487
def validate(self, node):
535
def validate(self, task, node):
488
536
"""Validate the driver-specific Node deployment info.
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.
538
:param task: a task from TaskManager.
494
539
:param node: a single Node to validate.
495
540
:returns: InvalidParameterValue.
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)
499
547
@task_manager.require_exclusive_lock
500
548
def deploy(self, task, node):
501
549
"""Perform start deployment a node.
503
Given a node with complete metadata, deploy the indicated image
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().
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)
516
return states.DEPLOYING
567
return states.DEPLOYWAIT
518
569
@task_manager.require_exclusive_lock
519
570
def tear_down(self, task, node):
520
571
"""Tear down a previous deployment.
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.
525
576
:param task: a TaskManager instance.
526
577
:param node: the Node to act upon.
531
582
return states.DELETED
533
584
def prepare(self, task, node):
585
"""Prepare the deployment environment for this node.
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
591
:param task: a TaskManager instance.
592
:param node: the Node to act upon.
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)
539
599
def clean_up(self, task, node):
600
"""Clean up the deployment environment for this node.
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.
607
:param task: a TaskManager instance.
608
:param node: the Node to act upon.
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,
595
665
'pxe_config_path': _get_pxe_config_file_path(
597
667
'root_mb': 1024 * int(d_info['root_gb']),
598
'swap_mb': int(d_info['swap_mb'])
668
'swap_mb': int(d_info['swap_mb']),
669
'ephemeral_mb': 1024 * int(d_info['ephemeral_gb']),
670
'preserve_ephemeral': d_info['preserve_ephemeral'],
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)
679
# ephemeral_format is nullable
680
params['ephemeral_format'] = d_info.get('ephemeral_format')
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)