Take iSCSI deploy out of pxe driver
This commit aims at separating the iSCSI deploy portions from pxe driver so that it can reused in the newly proposed iLO virtualmedia driver. Implements: blueprint ironic-ilo-virtualmedia-driver Change-Id: Id0a4b0fab797c884448a27b1cd5e3a1da05786a7
This commit is contained in:
parent
e0e0291efd
commit
6d16dd8951
|
@ -1057,16 +1057,13 @@
|
|||
[pxe]
|
||||
|
||||
#
|
||||
# Options defined in ironic.drivers.modules.pxe
|
||||
# Options defined in ironic.drivers.modules.iscsi_deploy
|
||||
#
|
||||
|
||||
# Additional append parameters for baremetal PXE boot. (string
|
||||
# value)
|
||||
#pxe_append_params=nofb nomodeset vga=normal
|
||||
|
||||
# Template file for PXE configuration. (string value)
|
||||
#pxe_config_template=$pybasedir/drivers/modules/pxe_config.template
|
||||
|
||||
# Default file system format for ephemeral partition, if one
|
||||
# is created. (string value)
|
||||
#default_ephemeral_format=ext4
|
||||
|
@ -1074,6 +1071,30 @@
|
|||
# Directory where images are stored on disk. (string value)
|
||||
#images_path=/var/lib/ironic/images/
|
||||
|
||||
# Directory where master instance images are stored on disk.
|
||||
# (string value)
|
||||
#instance_master_path=/var/lib/ironic/master_images
|
||||
|
||||
# Maximum size (in MiB) of cache for master images, including
|
||||
# those in use. (integer value)
|
||||
#image_cache_size=20480
|
||||
|
||||
# Maximum TTL (in minutes) for old master images in cache.
|
||||
# (integer value)
|
||||
#image_cache_ttl=10080
|
||||
|
||||
# The disk devices to scan while doing the deploy. (string
|
||||
# value)
|
||||
#disk_devices=cciss/c0d0,sda,hda,vda
|
||||
|
||||
|
||||
#
|
||||
# Options defined in ironic.drivers.modules.pxe
|
||||
#
|
||||
|
||||
# Template file for PXE configuration. (string value)
|
||||
#pxe_config_template=$pybasedir/drivers/modules/pxe_config.template
|
||||
|
||||
# IP address of Ironic compute node's tftp server. (string
|
||||
# value)
|
||||
#tftp_server=$my_ip
|
||||
|
@ -1085,21 +1106,9 @@
|
|||
# (string value)
|
||||
#tftp_master_path=/tftpboot/master_images
|
||||
|
||||
# Directory where master instance images are stored on disk.
|
||||
# (string value)
|
||||
#instance_master_path=/var/lib/ironic/master_images
|
||||
|
||||
# Neutron bootfile DHCP parameter. (string value)
|
||||
#pxe_bootfile_name=pxelinux.0
|
||||
|
||||
# Maximum size (in MiB) of cache for master images, including
|
||||
# those in use. (integer value)
|
||||
#image_cache_size=20480
|
||||
|
||||
# Maximum TTL (in minutes) for old master images in cache.
|
||||
# (integer value)
|
||||
#image_cache_ttl=10080
|
||||
|
||||
# Ironic compute node's HTTP server URL. Example:
|
||||
# http://192.1.2.3:8080 (string value)
|
||||
#http_url=<None>
|
||||
|
|
|
@ -26,6 +26,7 @@ from oslo.utils import excutils
|
|||
from ironic.common import disk_partitioner
|
||||
from ironic.common import exception
|
||||
from ironic.common import utils
|
||||
from ironic.drivers.modules import image_cache
|
||||
from ironic.openstack.common import log as logging
|
||||
from ironic.openstack.common import processutils
|
||||
|
||||
|
@ -279,7 +280,7 @@ def work_on_disk(dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format,
|
|||
:param preserve_ephemeral: If True, no filesystem is written to the
|
||||
ephemeral block device, preserving whatever content it had (if the
|
||||
partition table has not changed).
|
||||
|
||||
:returns: the UUID of the root partition.
|
||||
"""
|
||||
if not is_block_device(dev):
|
||||
raise exception.InstanceDeployFailure(_("Parent device '%s' not found")
|
||||
|
@ -324,7 +325,7 @@ def work_on_disk(dev, root_mb, swap_mb, ephemeral_mb, ephemeral_format,
|
|||
return root_uuid
|
||||
|
||||
|
||||
def deploy(address, port, iqn, lun, image_path, pxe_config_path,
|
||||
def deploy(address, port, iqn, lun, image_path,
|
||||
root_mb, swap_mb, ephemeral_mb, ephemeral_format, node_uuid,
|
||||
preserve_ephemeral=False):
|
||||
"""All-in-one function to deploy a node.
|
||||
|
@ -334,7 +335,6 @@ def deploy(address, port, iqn, lun, image_path, pxe_config_path,
|
|||
:param iqn: The iSCSI qualified name.
|
||||
:param lun: The iSCSI logical unit number.
|
||||
:param image_path: Path for the instance's disk image.
|
||||
:param pxe_config_path: Path for the instance PXE config file.
|
||||
:param root_mb: Size of the root partition in megabytes.
|
||||
:param swap_mb: Size of the swap partition in megabytes.
|
||||
:param ephemeral_mb: Size of the ephemeral partition in megabytes. If 0,
|
||||
|
@ -345,7 +345,7 @@ def deploy(address, port, iqn, lun, image_path, pxe_config_path,
|
|||
:param preserve_ephemeral: If True, no filesystem is written to the
|
||||
ephemeral block device, preserving whatever content it had (if the
|
||||
partition table has not changed).
|
||||
|
||||
:returns: the UUID of the root partition.
|
||||
"""
|
||||
dev = get_dev(address, port, iqn, lun)
|
||||
image_mb = get_image_mb(image_path)
|
||||
|
@ -370,7 +370,59 @@ def deploy(address, port, iqn, lun, image_path, pxe_config_path,
|
|||
finally:
|
||||
logout_iscsi(address, port, iqn)
|
||||
delete_iscsi(address, port, iqn)
|
||||
switch_pxe_config(pxe_config_path, root_uuid)
|
||||
|
||||
return root_uuid
|
||||
|
||||
|
||||
def notify_deploy_complete(address):
|
||||
"""Notifies the completion of deployment to the baremetal node.
|
||||
|
||||
:param address: The IP address of the node.
|
||||
"""
|
||||
# Ensure the node started netcat on the port after POST the request.
|
||||
time.sleep(3)
|
||||
notify(address, 10000)
|
||||
|
||||
|
||||
def check_for_missing_params(info_dict, error_msg, param_prefix=''):
|
||||
"""Check for empty params in the provided dictionary.
|
||||
|
||||
:param info_dict: The dictionary to inspect.
|
||||
:param error_msg: The error message to prefix before printing the
|
||||
information about missing parameters.
|
||||
:param param_prefix: Add this prefix to each parameter for error messages
|
||||
:raises: MissingParameterValue, if one or more parameters are
|
||||
empty in the provided dictionary.
|
||||
"""
|
||||
missing_info = []
|
||||
for label, value in info_dict.items():
|
||||
if not value:
|
||||
missing_info.append(param_prefix + label)
|
||||
|
||||
if missing_info:
|
||||
exc_msg = _("%(error_msg)s. The following parameters were "
|
||||
"not passed to ironic: %(missing_info)s")
|
||||
raise exception.MissingParameterValue(exc_msg %
|
||||
{'error_msg': error_msg, 'missing_info': missing_info})
|
||||
|
||||
|
||||
def fetch_images(ctx, cache, images_info):
|
||||
"""Check for available disk space and fetch images using ImageCache.
|
||||
|
||||
:param ctx: context
|
||||
:param cache: ImageCache instance to use for fetching
|
||||
:param images_info: list of tuples (image uuid, destination path)
|
||||
:raises: InstanceDeployFailure if unable to find enough disk space
|
||||
"""
|
||||
|
||||
try:
|
||||
image_cache.clean_up_caches(ctx, cache.master_dir, images_info)
|
||||
except exception.InsufficientDiskSpace as e:
|
||||
raise exception.InstanceDeployFailure(reason=e)
|
||||
|
||||
# NOTE(dtantsur): This code can suffer from race condition,
|
||||
# if disk space is used between the check and actual download.
|
||||
# This is probably unavoidable, as we can't control other
|
||||
# (probably unrelated) processes
|
||||
for uuid, path in images_info:
|
||||
cache.fetch_image(uuid, path, ctx=ctx)
|
||||
|
|
|
@ -5,7 +5,7 @@ dhcp
|
|||
goto deploy
|
||||
|
||||
:deploy
|
||||
kernel {{ pxe_options.deployment_aki_path }} selinux=0 disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn={{ pxe_options.deployment_iscsi_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac}
|
||||
kernel {{ pxe_options.deployment_aki_path }} selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac}
|
||||
initrd {{ pxe_options.deployment_ari_path }}
|
||||
boot
|
||||
|
||||
|
|
|
@ -0,0 +1,410 @@
|
|||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import i18n
|
||||
from ironic.common import image_service as service
|
||||
from ironic.common import keystone
|
||||
from ironic.common import states
|
||||
from ironic.common import utils
|
||||
from ironic.conductor import utils as manager_utils
|
||||
from ironic.drivers.modules import deploy_utils
|
||||
from ironic.drivers.modules import image_cache
|
||||
from ironic.drivers import utils as driver_utils
|
||||
from ironic.openstack.common import fileutils
|
||||
from ironic.openstack.common import log as logging
|
||||
from ironic.openstack.common import strutils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
_LE = i18n._LE
|
||||
_LI = i18n._LI
|
||||
|
||||
# NOTE(rameshg87): This file now registers some of opts in pxe group.
|
||||
# This is acceptable for now as a future refactoring into
|
||||
# separate boot and deploy interfaces is planned, and moving config
|
||||
# options twice is not recommended. Hence we would move the parameters
|
||||
# to the appropriate place in the final refactoring.
|
||||
pxe_opts = [
|
||||
cfg.StrOpt('pxe_append_params',
|
||||
default='nofb nomodeset vga=normal',
|
||||
help='Additional append parameters for baremetal PXE boot.'),
|
||||
cfg.StrOpt('default_ephemeral_format',
|
||||
default='ext4',
|
||||
help='Default file system format for ephemeral partition, '
|
||||
'if one is created.'),
|
||||
cfg.StrOpt('images_path',
|
||||
default='/var/lib/ironic/images/',
|
||||
help='Directory where images are stored on disk.'),
|
||||
cfg.StrOpt('instance_master_path',
|
||||
default='/var/lib/ironic/master_images',
|
||||
help='Directory where master instance images are stored on '
|
||||
'disk.'),
|
||||
cfg.IntOpt('image_cache_size',
|
||||
default=20480,
|
||||
help='Maximum size (in MiB) of cache for master images, '
|
||||
'including those in use.'),
|
||||
# 10080 here is 1 week - 60*24*7. It is entirely arbitrary in the absence
|
||||
# of a facility to disable the ttl entirely.
|
||||
cfg.IntOpt('image_cache_ttl',
|
||||
default=10080,
|
||||
help='Maximum TTL (in minutes) for old master images in '
|
||||
'cache.'),
|
||||
cfg.StrOpt('disk_devices',
|
||||
default='cciss/c0d0,sda,hda,vda',
|
||||
help='The disk devices to scan while doing the deploy.'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(pxe_opts, group='pxe')
|
||||
|
||||
|
||||
@image_cache.cleanup(priority=50)
|
||||
class InstanceImageCache(image_cache.ImageCache):
|
||||
|
||||
def __init__(self, image_service=None):
|
||||
super(self.__class__, self).__init__(
|
||||
CONF.pxe.instance_master_path,
|
||||
# MiB -> B
|
||||
cache_size=CONF.pxe.image_cache_size * 1024 * 1024,
|
||||
# min -> sec
|
||||
cache_ttl=CONF.pxe.image_cache_ttl * 60,
|
||||
image_service=image_service)
|
||||
|
||||
|
||||
def _get_image_dir_path(node_uuid):
|
||||
"""Generate the dir for an instances disk."""
|
||||
return os.path.join(CONF.pxe.images_path, node_uuid)
|
||||
|
||||
|
||||
def _get_image_file_path(node_uuid):
|
||||
"""Generate the full path for an instances disk."""
|
||||
return os.path.join(_get_image_dir_path(node_uuid), 'disk')
|
||||
|
||||
|
||||
def parse_instance_info(node):
|
||||
"""Gets the instance specific Node deployment info.
|
||||
|
||||
This method validates whether the 'instance_info' property of the
|
||||
supplied node contains the required information for this driver to
|
||||
deploy images to the node.
|
||||
|
||||
:param node: a single Node.
|
||||
:returns: A dict with the instance_info values.
|
||||
:raises: MissingParameterValue, if any of the required parameters are
|
||||
missing.
|
||||
:raises: InvalidParameterValue, if any of the parameters have invalid
|
||||
value.
|
||||
"""
|
||||
info = node.instance_info
|
||||
i_info = {}
|
||||
i_info['image_source'] = info.get('image_source')
|
||||
i_info['root_gb'] = info.get('root_gb')
|
||||
|
||||
error_msg = _("Cannot validate iSCSI deploy")
|
||||
deploy_utils.check_for_missing_params(i_info, error_msg)
|
||||
|
||||
# Internal use only
|
||||
i_info['deploy_key'] = info.get('deploy_key')
|
||||
|
||||
i_info['swap_mb'] = info.get('swap_mb', 0)
|
||||
i_info['ephemeral_gb'] = info.get('ephemeral_gb', 0)
|
||||
i_info['ephemeral_format'] = info.get('ephemeral_format')
|
||||
|
||||
err_msg_invalid = _("Cannot validate parameter for iSCSI deploy. "
|
||||
"Invalid parameter %(param)s. Reason: %(reason)s")
|
||||
for param in ('root_gb', 'swap_mb', 'ephemeral_gb'):
|
||||
try:
|
||||
int(i_info[param])
|
||||
except ValueError:
|
||||
reason = _("'%s' is not an integer value.") % i_info[param]
|
||||
raise exception.InvalidParameterValue(err_msg_invalid %
|
||||
{'param': param, 'reason': reason})
|
||||
|
||||
if i_info['ephemeral_gb'] and not i_info['ephemeral_format']:
|
||||
i_info['ephemeral_format'] = CONF.pxe.default_ephemeral_format
|
||||
|
||||
preserve_ephemeral = info.get('preserve_ephemeral', False)
|
||||
try:
|
||||
i_info['preserve_ephemeral'] = strutils.bool_from_string(
|
||||
preserve_ephemeral, strict=True)
|
||||
except ValueError as e:
|
||||
raise exception.InvalidParameterValue(err_msg_invalid %
|
||||
{'param': 'preserve_ephemeral', 'reason': e})
|
||||
return i_info
|
||||
|
||||
|
||||
def check_image_size(task):
|
||||
"""Check if the requested image is larger than the root partition size.
|
||||
|
||||
:param task: a TaskManager instance containing the node to act on.
|
||||
:raises: InstanceDeployFailure if size of the image is greater than root
|
||||
partition.
|
||||
"""
|
||||
i_info = parse_instance_info(task.node)
|
||||
image_path = _get_image_file_path(task.node.uuid)
|
||||
image_mb = deploy_utils.get_image_mb(image_path)
|
||||
root_mb = 1024 * int(i_info['root_gb'])
|
||||
if image_mb > root_mb:
|
||||
msg = (_('Root partition is too small for requested image. '
|
||||
'Image size: %(image_mb)d MB, Root size: %(root_mb)d MB')
|
||||
% {'image_mb': image_mb, 'root_mb': root_mb})
|
||||
raise exception.InstanceDeployFailure(msg)
|
||||
|
||||
|
||||
def cache_instance_image(ctx, node):
|
||||
"""Fetch the instance's image from Glance
|
||||
|
||||
This method pulls the AMI and writes them to the appropriate place
|
||||
on local disk.
|
||||
|
||||
:param ctx: context
|
||||
:param node: an ironic node object
|
||||
:returns: a tuple containing the uuid of the image and the path in
|
||||
the filesystem where image is cached.
|
||||
"""
|
||||
i_info = parse_instance_info(node)
|
||||
fileutils.ensure_tree(_get_image_dir_path(node.uuid))
|
||||
image_path = _get_image_file_path(node.uuid)
|
||||
uuid = i_info['image_source']
|
||||
|
||||
LOG.debug("Fetching image %(ami)s for node %(uuid)s",
|
||||
{'ami': uuid, 'uuid': node.uuid})
|
||||
|
||||
deploy_utils.fetch_images(ctx, InstanceImageCache(), [(uuid, image_path)])
|
||||
|
||||
return (uuid, image_path)
|
||||
|
||||
|
||||
def destroy_images(node_uuid):
|
||||
"""Delete instance's image file.
|
||||
|
||||
:param node_uuid: the uuid of the ironic node.
|
||||
"""
|
||||
utils.unlink_without_raise(_get_image_file_path(node_uuid))
|
||||
utils.rmtree_without_raise(_get_image_dir_path(node_uuid))
|
||||
InstanceImageCache().clean_up()
|
||||
|
||||
|
||||
def get_deploy_info(node, **kwargs):
|
||||
"""Returns the information required for doing iSCSI deploy in a
|
||||
dictionary.
|
||||
|
||||
:param node: ironic node object
|
||||
:param kwargs: the keyword args passed from the conductor node.
|
||||
:raises: MissingParameterValue, if some required parameters were not
|
||||
passed.
|
||||
:raises: InvalidParameterValue, if any of the parameters have invalid
|
||||
value.
|
||||
"""
|
||||
|
||||
deploy_key = kwargs.get('key')
|
||||
i_info = parse_instance_info(node)
|
||||
if i_info['deploy_key'] != deploy_key:
|
||||
raise exception.InvalidParameterValue(_("Deploy key does not match"))
|
||||
|
||||
params = {'address': kwargs.get('address'),
|
||||
'port': kwargs.get('port', '3260'),
|
||||
'iqn': kwargs.get('iqn'),
|
||||
'lun': kwargs.get('lun', '1'),
|
||||
'image_path': _get_image_file_path(node.uuid),
|
||||
'root_mb': 1024 * int(i_info['root_gb']),
|
||||
'swap_mb': int(i_info['swap_mb']),
|
||||
'ephemeral_mb': 1024 * int(i_info['ephemeral_gb']),
|
||||
'preserve_ephemeral': i_info['preserve_ephemeral'],
|
||||
'node_uuid': node.uuid,
|
||||
}
|
||||
|
||||
missing = [key for key in params if params[key] is None]
|
||||
if missing:
|
||||
raise exception.MissingParameterValue(_(
|
||||
"Parameters %s were not passed to ironic"
|
||||
" for deploy.") % missing)
|
||||
|
||||
# ephemeral_format is nullable
|
||||
params['ephemeral_format'] = i_info.get('ephemeral_format')
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def set_failed_state(task, msg):
|
||||
"""Sets the deploy status as failed with relevant messages.
|
||||
|
||||
This method sets the deployment as fail with the given message.
|
||||
It sets node's provision_state to DEPLOYFAIL and updates last_error
|
||||
with the given error message. It also powers off the baremetal node.
|
||||
|
||||
:param task: a TaskManager instance containing the node to act on.
|
||||
:param msg: the message to set in last_error of the node.
|
||||
"""
|
||||
node = task.node
|
||||
node.provision_state = states.DEPLOYFAIL
|
||||
node.target_provision_state = states.NOSTATE
|
||||
node.save(task.context)
|
||||
try:
|
||||
manager_utils.node_power_action(task, states.POWER_OFF)
|
||||
except Exception:
|
||||
msg2 = (_('Node %s failed to power off while handling deploy '
|
||||
'failure. This may be a serious condition. Node '
|
||||
'should be removed from Ironic or put in maintenance '
|
||||
'mode until the problem is resolved.') % node.uuid)
|
||||
LOG.exception(msg2)
|
||||
finally:
|
||||
# NOTE(deva): node_power_action() erases node.last_error
|
||||
# so we need to set it again here.
|
||||
node.last_error = msg
|
||||
node.save(task.context)
|
||||
|
||||
|
||||
def continue_deploy(task, **kwargs):
|
||||
"""Resume a deployment upon getting POST data from deploy ramdisk.
|
||||
|
||||
This method raises no exceptions because it is intended to be
|
||||
invoked asynchronously as a callback from the deploy ramdisk.
|
||||
|
||||
:param task: a TaskManager instance containing the node to act on.
|
||||
:param kwargs: the kwargs to be passed to deploy.
|
||||
:returns: UUID of the root partition or None on error.
|
||||
"""
|
||||
node = task.node
|
||||
|
||||
node.provision_state = states.DEPLOYING
|
||||
node.save(task.context)
|
||||
|
||||
params = get_deploy_info(node, **kwargs)
|
||||
ramdisk_error = kwargs.get('error')
|
||||
|
||||
if ramdisk_error:
|
||||
LOG.error(_LE('Error returned from deploy ramdisk: %s'),
|
||||
ramdisk_error)
|
||||
set_failed_state(task, _('Failure in deploy ramdisk.'))
|
||||
destroy_images(node.uuid)
|
||||
return
|
||||
|
||||
LOG.info(_LI('Continuing deployment for node %(node)s, params %(params)s'),
|
||||
{'node': node.uuid, 'params': params})
|
||||
|
||||
root_uuid = None
|
||||
try:
|
||||
root_uuid = deploy_utils.deploy(**params)
|
||||
except Exception as e:
|
||||
LOG.error(_LE('Deploy failed for instance %(instance)s. '
|
||||
'Error: %(error)s'),
|
||||
{'instance': node.instance_uuid, 'error': e})
|
||||
set_failed_state(task, _('Failed to continue iSCSI deployment.'))
|
||||
|
||||
destroy_images(node.uuid)
|
||||
return root_uuid
|
||||
|
||||
|
||||
def build_deploy_ramdisk_options(node, ctx):
|
||||
"""Build the ramdisk config options for a node
|
||||
|
||||
This method builds the ramdisk options for a node,
|
||||
given all the required parameters for doing iscsi deploy.
|
||||
|
||||
:param node: a single Node.
|
||||
:param ctx: security context
|
||||
:returns: A dictionary of options to be passed to ramdisk for performing
|
||||
the deploy.
|
||||
"""
|
||||
# NOTE: we should strip '/' from the end because this is intended for
|
||||
# hardcoded ramdisk script
|
||||
ironic_api = (CONF.conductor.api_url or
|
||||
keystone.get_service_url()).rstrip('/')
|
||||
|
||||
deploy_key = utils.random_alnum(32)
|
||||
i_info = node.instance_info
|
||||
i_info['deploy_key'] = deploy_key
|
||||
node.instance_info = i_info
|
||||
node.save(ctx)
|
||||
|
||||
deploy_options = {
|
||||
'deployment_id': node['uuid'],
|
||||
'deployment_key': deploy_key,
|
||||
'iscsi_target_iqn': "iqn-%s" % node.uuid,
|
||||
'ironic_api_url': ironic_api,
|
||||
'disk': CONF.pxe.disk_devices,
|
||||
}
|
||||
return deploy_options
|
||||
|
||||
|
||||
def validate_glance_image_properties(ctx, deploy_info, properties):
|
||||
"""Validate the image in Glance.
|
||||
|
||||
Check if the image exist in Glance and if it contains the
|
||||
properties passed.
|
||||
|
||||
:param ctx: security context
|
||||
:param deploy_info: the deploy_info to be validated
|
||||
:param properties: the list of image meta-properties to be validated.
|
||||
:raises: InvalidParameterValue if the glance image doesn't exist.
|
||||
:raises: MissingParameterValue if the glance image doesn't contain
|
||||
the mentioned properties.
|
||||
"""
|
||||
image_id = deploy_info['image_source']
|
||||
try:
|
||||
glance_service = service.Service(version=1, context=ctx)
|
||||
image_props = glance_service.show(image_id)['properties']
|
||||
except (exception.GlanceConnectionFailed,
|
||||
exception.ImageNotAuthorized,
|
||||
exception.Invalid):
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"Failed to connect to Glance to get the properties "
|
||||
"of the image %s") % image_id)
|
||||
except exception.ImageNotFound:
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"Image %s not found in Glance") % image_id)
|
||||
|
||||
missing_props = []
|
||||
for prop in properties:
|
||||
if not image_props.get(prop):
|
||||
missing_props.append(prop)
|
||||
|
||||
if missing_props:
|
||||
props = ', '.join(missing_props)
|
||||
raise exception.MissingParameterValue(_(
|
||||
"Image %(image)s is missing the following properties: "
|
||||
"%(properties)s") % {'image': image_id, 'properties': props})
|
||||
|
||||
|
||||
def validate(task):
|
||||
"""Validates the pre-requisites for iSCSI deploy.
|
||||
|
||||
Validates whether node in the task provided has some ports enrolled.
|
||||
This method validates whether conductor url is available either from CONF
|
||||
file or from keystone.
|
||||
|
||||
:param task: a TaskManager instance containing the node to act on.
|
||||
:raises: InvalidParameterValue if no ports are enrolled for the given node.
|
||||
"""
|
||||
node = task.node
|
||||
if not driver_utils.get_node_mac_addresses(task):
|
||||
raise exception.InvalidParameterValue(_("Node %s does not have "
|
||||
"any port associated with it.") % node.uuid)
|
||||
|
||||
try:
|
||||
# TODO(lucasagomes): Validate the format of the URL
|
||||
CONF.conductor.api_url or keystone.get_service_url()
|
||||
except (exception.CatalogFailure,
|
||||
exception.CatalogNotFound,
|
||||
exception.CatalogUnauthorized):
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"Couldn't get the URL of the Ironic API service from the "
|
||||
"configuration file or keystone catalog."))
|
|
@ -21,11 +21,10 @@ import os
|
|||
import shutil
|
||||
|
||||
from oslo.config import cfg
|
||||
from oslo.utils import strutils
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import i18n
|
||||
from ironic.common import image_service as service
|
||||
from ironic.common import keystone
|
||||
from ironic.common import neutron
|
||||
from ironic.common import paths
|
||||
from ironic.common import pxe_utils
|
||||
|
@ -36,26 +35,16 @@ from ironic.conductor import utils as manager_utils
|
|||
from ironic.drivers import base
|
||||
from ironic.drivers.modules import deploy_utils
|
||||
from ironic.drivers.modules import image_cache
|
||||
from ironic.drivers import utils as driver_utils
|
||||
from ironic.drivers.modules import iscsi_deploy
|
||||
from ironic.openstack.common import fileutils
|
||||
from ironic.openstack.common import log as logging
|
||||
|
||||
|
||||
pxe_opts = [
|
||||
cfg.StrOpt('pxe_append_params',
|
||||
default='nofb nomodeset vga=normal',
|
||||
help='Additional append parameters for baremetal PXE boot.'),
|
||||
cfg.StrOpt('pxe_config_template',
|
||||
default=paths.basedir_def(
|
||||
'drivers/modules/pxe_config.template'),
|
||||
help='Template file for PXE configuration.'),
|
||||
cfg.StrOpt('default_ephemeral_format',
|
||||
default='ext4',
|
||||
help='Default file system format for ephemeral partition, '
|
||||
'if one is created.'),
|
||||
cfg.StrOpt('images_path',
|
||||
default='/var/lib/ironic/images/',
|
||||
help='Directory where images are stored on disk.'),
|
||||
cfg.StrOpt('tftp_server',
|
||||
default='$my_ip',
|
||||
help='IP address of Ironic compute node\'s tftp server.'),
|
||||
|
@ -65,25 +54,11 @@ pxe_opts = [
|
|||
cfg.StrOpt('tftp_master_path',
|
||||
default='/tftpboot/master_images',
|
||||
help='Directory where master tftp images are stored on disk.'),
|
||||
cfg.StrOpt('instance_master_path',
|
||||
default='/var/lib/ironic/master_images',
|
||||
help='Directory where master instance images are stored on '
|
||||
'disk.'),
|
||||
# NOTE(dekehn): Additional boot files options may be created in the event
|
||||
# other architectures require different boot files.
|
||||
cfg.StrOpt('pxe_bootfile_name',
|
||||
default='pxelinux.0',
|
||||
help='Neutron bootfile DHCP parameter.'),
|
||||
cfg.IntOpt('image_cache_size',
|
||||
default=20480,
|
||||
help='Maximum size (in MiB) of cache for master images, '
|
||||
'including those in use.'),
|
||||
# 10080 here is 1 week - 60*24*7. It is entirely arbitrary in the absence
|
||||
# of a facility to disable the ttl entirely.
|
||||
cfg.IntOpt('image_cache_ttl',
|
||||
default=10080,
|
||||
help='Maximum TTL (in minutes) for old master images in '
|
||||
'cache.'),
|
||||
cfg.StrOpt('http_url',
|
||||
help='Ironic compute node\'s HTTP server URL. '
|
||||
'Example: http://192.1.2.3:8080'),
|
||||
|
@ -101,10 +76,14 @@ pxe_opts = [
|
|||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
_LE = i18n._LE
|
||||
_LI = i18n._LI
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(pxe_opts, group='pxe')
|
||||
CONF.import_opt('use_ipv6', 'ironic.netconf')
|
||||
|
||||
|
||||
REQUIRED_PROPERTIES = {
|
||||
'pxe_deploy_kernel': _("UUID (from Glance) of the deployment kernel. "
|
||||
"Required."),
|
||||
|
@ -114,18 +93,6 @@ REQUIRED_PROPERTIES = {
|
|||
COMMON_PROPERTIES = REQUIRED_PROPERTIES
|
||||
|
||||
|
||||
def _check_for_missing_params(info_dict, param_prefix=''):
|
||||
missing_info = []
|
||||
for label, value in info_dict.items():
|
||||
if not value:
|
||||
missing_info.append(param_prefix + label)
|
||||
|
||||
if missing_info:
|
||||
raise exception.MissingParameterValue(_(
|
||||
"Can not validate PXE bootloader. The following parameters "
|
||||
"were not passed to ironic: %s") % missing_info)
|
||||
|
||||
|
||||
def _parse_driver_info(node):
|
||||
"""Gets the driver specific Node deployment info.
|
||||
|
||||
|
@ -142,61 +109,12 @@ def _parse_driver_info(node):
|
|||
d_info['deploy_kernel'] = info.get('pxe_deploy_kernel')
|
||||
d_info['deploy_ramdisk'] = info.get('pxe_deploy_ramdisk')
|
||||
|
||||
_check_for_missing_params(d_info, 'pxe_')
|
||||
error_msg = _("Cannot validate PXE bootloader")
|
||||
deploy_utils.check_for_missing_params(d_info, error_msg, 'pxe_')
|
||||
|
||||
return d_info
|
||||
|
||||
|
||||
def _parse_instance_info(node):
|
||||
"""Gets the instance specific Node deployment info.
|
||||
|
||||
This method validates whether the 'instance_info' property of the
|
||||
supplied node contains the required information for this driver to
|
||||
deploy images to the node.
|
||||
|
||||
:param node: a single Node.
|
||||
:returns: A dict with the instance_info values.
|
||||
:raises: MissingParameterValue
|
||||
:raises: InvalidParameterValue
|
||||
"""
|
||||
|
||||
info = node.instance_info
|
||||
i_info = {}
|
||||
i_info['image_source'] = info.get('image_source')
|
||||
i_info['root_gb'] = info.get('root_gb')
|
||||
|
||||
_check_for_missing_params(i_info)
|
||||
|
||||
# Internal use only
|
||||
i_info['deploy_key'] = info.get('deploy_key')
|
||||
|
||||
i_info['swap_mb'] = info.get('swap_mb', 0)
|
||||
i_info['ephemeral_gb'] = info.get('ephemeral_gb', 0)
|
||||
i_info['ephemeral_format'] = info.get('ephemeral_format')
|
||||
|
||||
err_msg_invalid = _("Can not validate PXE bootloader. Invalid parameter "
|
||||
"%(param)s. Reason: %(reason)s")
|
||||
for param in ('root_gb', 'swap_mb', 'ephemeral_gb'):
|
||||
try:
|
||||
int(i_info[param])
|
||||
except ValueError:
|
||||
reason = _("'%s' is not an integer value.") % i_info[param]
|
||||
raise exception.InvalidParameterValue(err_msg_invalid %
|
||||
{'param': param, 'reason': reason})
|
||||
|
||||
if i_info['ephemeral_gb'] and not i_info['ephemeral_format']:
|
||||
i_info['ephemeral_format'] = CONF.pxe.default_ephemeral_format
|
||||
|
||||
preserve_ephemeral = info.get('preserve_ephemeral', False)
|
||||
try:
|
||||
i_info['preserve_ephemeral'] = strutils.bool_from_string(
|
||||
preserve_ephemeral, strict=True)
|
||||
except ValueError as e:
|
||||
raise exception.InvalidParameterValue(err_msg_invalid %
|
||||
{'param': 'preserve_ephemeral', 'reason': e})
|
||||
return i_info
|
||||
|
||||
|
||||
def _parse_deploy_info(node):
|
||||
"""Gets the instance and driver specific Node deployment info.
|
||||
|
||||
|
@ -210,7 +128,7 @@ def _parse_deploy_info(node):
|
|||
:raises: InvalidParameterValue
|
||||
"""
|
||||
info = {}
|
||||
info.update(_parse_instance_info(node))
|
||||
info.update(iscsi_deploy.parse_instance_info(node))
|
||||
info.update(_parse_driver_info(node))
|
||||
return info
|
||||
|
||||
|
@ -230,17 +148,6 @@ def _build_pxe_config_options(node, pxe_info, ctx):
|
|||
:returns: A dictionary of pxe options to be used in the pxe bootfile
|
||||
template.
|
||||
"""
|
||||
# NOTE: we should strip '/' from the end because this is intended for
|
||||
# hardcoded ramdisk script
|
||||
ironic_api = (CONF.conductor.api_url or
|
||||
keystone.get_service_url()).rstrip('/')
|
||||
|
||||
deploy_key = utils.random_alnum(32)
|
||||
i_info = node.instance_info
|
||||
i_info['deploy_key'] = deploy_key
|
||||
node.instance_info = i_info
|
||||
node.save(ctx)
|
||||
|
||||
if CONF.pxe.ipxe_enabled:
|
||||
deploy_kernel = '/'.join([CONF.pxe.http_url, node.uuid,
|
||||
'deploy_kernel'])
|
||||
|
@ -255,38 +162,29 @@ def _build_pxe_config_options(node, pxe_info, ctx):
|
|||
ramdisk = pxe_info['ramdisk'][1]
|
||||
|
||||
pxe_options = {
|
||||
'deployment_id': node['uuid'],
|
||||
'deployment_key': deploy_key,
|
||||
'deployment_iscsi_iqn': "iqn-%s" % node.uuid,
|
||||
'deployment_aki_path': deploy_kernel,
|
||||
'deployment_ari_path': deploy_ramdisk,
|
||||
'aki_path': kernel,
|
||||
'ari_path': ramdisk,
|
||||
'ironic_api_url': ironic_api,
|
||||
'pxe_append_params': CONF.pxe.pxe_append_params,
|
||||
}
|
||||
|
||||
deploy_ramdisk_options = iscsi_deploy.build_deploy_ramdisk_options(node,
|
||||
ctx)
|
||||
pxe_options.update(deploy_ramdisk_options)
|
||||
return pxe_options
|
||||
|
||||
|
||||
def _get_image_dir_path(node_uuid):
|
||||
"""Generate the dir for an instances disk."""
|
||||
return os.path.join(CONF.pxe.images_path, node_uuid)
|
||||
|
||||
|
||||
def _get_image_file_path(node_uuid):
|
||||
"""Generate the full path for an instances disk."""
|
||||
return os.path.join(_get_image_dir_path(node_uuid), 'disk')
|
||||
|
||||
|
||||
def _get_token_file_path(node_uuid):
|
||||
"""Generate the path for PKI token file."""
|
||||
return os.path.join(CONF.pxe.tftp_root, 'token-' + node_uuid)
|
||||
|
||||
|
||||
class PXEImageCache(image_cache.ImageCache):
|
||||
def __init__(self, master_dir, image_service=None):
|
||||
super(PXEImageCache, self).__init__(
|
||||
master_dir,
|
||||
@image_cache.cleanup(priority=25)
|
||||
class TFTPImageCache(image_cache.ImageCache):
|
||||
def __init__(self, image_service=None):
|
||||
super(TFTPImageCache, self).__init__(
|
||||
CONF.pxe.tftp_master_path,
|
||||
# MiB -> B
|
||||
cache_size=CONF.pxe.image_cache_size * 1024 * 1024,
|
||||
# min -> sec
|
||||
|
@ -294,77 +192,13 @@ class PXEImageCache(image_cache.ImageCache):
|
|||
image_service=image_service)
|
||||
|
||||
|
||||
@image_cache.cleanup(priority=25)
|
||||
class TFTPImageCache(PXEImageCache):
|
||||
def __init__(self, image_service=None):
|
||||
super(TFTPImageCache, self).__init__(CONF.pxe.tftp_master_path)
|
||||
|
||||
|
||||
@image_cache.cleanup(priority=50)
|
||||
class InstanceImageCache(PXEImageCache):
|
||||
def __init__(self, image_service=None):
|
||||
super(InstanceImageCache, self).__init__(CONF.pxe.instance_master_path)
|
||||
|
||||
|
||||
def _fetch_images(ctx, cache, images_info):
|
||||
"""Check for available disk space and fetch images using ImageCache.
|
||||
|
||||
:param ctx: context
|
||||
:param cache: ImageCache instance to use for fetching
|
||||
:param images_info: list of tuples (image uuid, destination path)
|
||||
:raises: InstanceDeployFailure if unable to find enough disk space
|
||||
"""
|
||||
|
||||
try:
|
||||
image_cache.clean_up_caches(ctx, cache.master_dir, images_info)
|
||||
except exception.InsufficientDiskSpace as e:
|
||||
raise exception.InstanceDeployFailure(reason=e)
|
||||
|
||||
# NOTE(dtantsur): This code can suffer from race condition,
|
||||
# if disk space is used between the check and actual download.
|
||||
# This is probably unavoidable, as we can't control other
|
||||
# (probably unrelated) processes
|
||||
for uuid, path in images_info:
|
||||
cache.fetch_image(uuid, path, ctx=ctx)
|
||||
|
||||
|
||||
def _cache_ramdisk_kernel(ctx, node, pxe_info):
|
||||
"""Fetch the necessary kernels and ramdisks for the instance."""
|
||||
fileutils.ensure_tree(
|
||||
os.path.join(pxe_utils.get_root_dir(), node.uuid))
|
||||
LOG.debug("Fetching kernel and ramdisk for node %s",
|
||||
node.uuid)
|
||||
_fetch_images(ctx, TFTPImageCache(), pxe_info.values())
|
||||
|
||||
|
||||
def _cache_instance_image(ctx, node):
|
||||
"""Fetch the instance's image from Glance
|
||||
|
||||
This method pulls the relevant AMI and associated kernel and ramdisk,
|
||||
and the deploy kernel and ramdisk from Glance, and writes them
|
||||
to the appropriate places on local disk.
|
||||
|
||||
Both sets of kernel and ramdisk are needed for PXE booting, so these
|
||||
are stored under CONF.pxe.tftp_root.
|
||||
|
||||
At present, the AMI is cached and certain files are injected.
|
||||
Debian/ubuntu-specific assumptions are made regarding the injected
|
||||
files. In a future revision, this functionality will be replaced by a
|
||||
more scalable and os-agnostic approach: the deployment ramdisk will
|
||||
fetch from Glance directly, and write its own last-mile configuration.
|
||||
|
||||
"""
|
||||
i_info = _parse_instance_info(node)
|
||||
fileutils.ensure_tree(_get_image_dir_path(node.uuid))
|
||||
image_path = _get_image_file_path(node.uuid)
|
||||
uuid = i_info['image_source']
|
||||
|
||||
LOG.debug("Fetching image %(ami)s for node %(uuid)s" %
|
||||
{'ami': uuid, 'uuid': node.uuid})
|
||||
|
||||
_fetch_images(ctx, InstanceImageCache(), [(uuid, image_path)])
|
||||
|
||||
return (uuid, image_path)
|
||||
deploy_utils.fetch_images(ctx, TFTPImageCache(), pxe_info.values())
|
||||
|
||||
|
||||
def _get_image_info(node, ctx):
|
||||
|
@ -401,13 +235,6 @@ def _get_image_info(node, ctx):
|
|||
return image_info
|
||||
|
||||
|
||||
def _destroy_images(node_uuid):
|
||||
"""Delete instance's image file."""
|
||||
utils.unlink_without_raise(_get_image_file_path(node_uuid))
|
||||
utils.rmtree_without_raise(_get_image_dir_path(node_uuid))
|
||||
InstanceImageCache().clean_up()
|
||||
|
||||
|
||||
def _create_token_file(task):
|
||||
"""Save PKI token to file."""
|
||||
token_file_path = _get_token_file_path(task.node.uuid)
|
||||
|
@ -424,54 +251,6 @@ def _destroy_token_file(node):
|
|||
utils.unlink_without_raise(token_file_path)
|
||||
|
||||
|
||||
def _check_image_size(task):
|
||||
"""Check if the requested image is larger than the root partition size."""
|
||||
i_info = _parse_instance_info(task.node)
|
||||
image_path = _get_image_file_path(task.node.uuid)
|
||||
image_mb = deploy_utils.get_image_mb(image_path)
|
||||
root_mb = 1024 * int(i_info['root_gb'])
|
||||
if image_mb > root_mb:
|
||||
msg = (_('Root partition is too small for requested image. '
|
||||
'Image size: %(image_mb)d MB, Root size: %(root_mb)d MB')
|
||||
% {'image_mb': image_mb, 'root_mb': root_mb})
|
||||
raise exception.InstanceDeployFailure(msg)
|
||||
|
||||
|
||||
def _validate_glance_image(ctx, deploy_info):
|
||||
"""Validate the image in Glance.
|
||||
|
||||
Check if the image exist in Glance and if it contains the
|
||||
'kernel_id' and 'ramdisk_id' properties.
|
||||
|
||||
:raises: InvalidParameterValue.
|
||||
:raises: MissingParameterValue
|
||||
"""
|
||||
image_id = deploy_info['image_source']
|
||||
try:
|
||||
glance_service = service.Service(version=1, context=ctx)
|
||||
image_props = glance_service.show(image_id)['properties']
|
||||
except (exception.GlanceConnectionFailed,
|
||||
exception.ImageNotAuthorized,
|
||||
exception.Invalid):
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"Failed to connect to Glance to get the properties "
|
||||
"of the image %s") % image_id)
|
||||
except exception.ImageNotFound:
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"Image %s not found in Glance") % image_id)
|
||||
|
||||
missing_props = []
|
||||
for prop in ('kernel_id', 'ramdisk_id'):
|
||||
if not image_props.get(prop):
|
||||
missing_props.append(prop)
|
||||
|
||||
if missing_props:
|
||||
props = ', '.join(missing_props)
|
||||
raise exception.MissingParameterValue(_(
|
||||
"Image %(image)s is missing the following properties: "
|
||||
"%(properties)s") % {'image': image_id, 'properties': props})
|
||||
|
||||
|
||||
class PXEDeploy(base.DeployInterface):
|
||||
"""PXE Deploy Interface: just a stub until the real driver is ported."""
|
||||
|
||||
|
@ -485,31 +264,19 @@ class PXEDeploy(base.DeployInterface):
|
|||
:raises: InvalidParameterValue.
|
||||
:raises: MissingParameterValue
|
||||
"""
|
||||
node = task.node
|
||||
if not driver_utils.get_node_mac_addresses(task):
|
||||
raise exception.MissingParameterValue(_("Node %s does not have "
|
||||
"any port associated with it.") % node.uuid)
|
||||
|
||||
d_info = _parse_deploy_info(node)
|
||||
|
||||
if CONF.pxe.ipxe_enabled:
|
||||
if not CONF.pxe.http_url or not CONF.pxe.http_root:
|
||||
raise exception.MissingParameterValue(_(
|
||||
"iPXE boot is enabled but no HTTP URL or HTTP "
|
||||
"root was specified."))
|
||||
|
||||
# Try to get the URL of the Ironic API
|
||||
try:
|
||||
# TODO(lucasagomes): Validate the format of the URL
|
||||
CONF.conductor.api_url or keystone.get_service_url()
|
||||
except (exception.CatalogFailure,
|
||||
exception.CatalogNotFound,
|
||||
exception.CatalogUnauthorized):
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"Couldn't get the URL of the Ironic API service from the "
|
||||
"configuration file or keystone catalog."))
|
||||
d_info = _parse_deploy_info(task.node)
|
||||
|
||||
_validate_glance_image(task.context, d_info)
|
||||
iscsi_deploy.validate(task)
|
||||
|
||||
props = ['kernel_id', 'ramdisk_id']
|
||||
iscsi_deploy.validate_glance_image_properties(task.context, d_info,
|
||||
props)
|
||||
|
||||
@task_manager.require_exclusive_lock
|
||||
def deploy(self, task):
|
||||
|
@ -525,8 +292,8 @@ class PXEDeploy(base.DeployInterface):
|
|||
:param task: a TaskManager instance containing the node to act on.
|
||||
:returns: deploy state DEPLOYING.
|
||||
"""
|
||||
_cache_instance_image(task.context, task.node)
|
||||
_check_image_size(task)
|
||||
iscsi_deploy.cache_instance_image(task.context, task.node)
|
||||
iscsi_deploy.check_image_size(task)
|
||||
|
||||
# TODO(yuriyz): more secure way needed for pass auth token
|
||||
# to deploy ramdisk
|
||||
|
@ -591,7 +358,7 @@ class PXEDeploy(base.DeployInterface):
|
|||
|
||||
pxe_utils.clean_up_pxe_config(task)
|
||||
|
||||
_destroy_images(node.uuid)
|
||||
iscsi_deploy.destroy_images(node.uuid)
|
||||
_destroy_token_file(node)
|
||||
|
||||
def take_over(self, task):
|
||||
|
@ -602,46 +369,24 @@ class PXEDeploy(base.DeployInterface):
|
|||
class VendorPassthru(base.VendorInterface):
|
||||
"""Interface to mix IPMI and PXE vendor-specific interfaces."""
|
||||
|
||||
def _get_deploy_info(self, node, **kwargs):
|
||||
d_info = _parse_deploy_info(node)
|
||||
|
||||
deploy_key = kwargs.get('key')
|
||||
if d_info['deploy_key'] != deploy_key:
|
||||
raise exception.InvalidParameterValue(_("Deploy key does not"
|
||||
" match"))
|
||||
|
||||
params = {'address': kwargs.get('address'),
|
||||
'port': kwargs.get('port', '3260'),
|
||||
'iqn': kwargs.get('iqn'),
|
||||
'lun': kwargs.get('lun', '1'),
|
||||
'image_path': _get_image_file_path(node.uuid),
|
||||
'pxe_config_path':
|
||||
pxe_utils.get_pxe_config_file_path(node.uuid),
|
||||
'root_mb': 1024 * int(d_info['root_gb']),
|
||||
'swap_mb': int(d_info['swap_mb']),
|
||||
'ephemeral_mb': 1024 * int(d_info['ephemeral_gb']),
|
||||
'preserve_ephemeral': d_info['preserve_ephemeral'],
|
||||
'node_uuid': node.uuid,
|
||||
}
|
||||
|
||||
missing = [key for key in params.keys() if params[key] is None]
|
||||
if missing:
|
||||
raise exception.MissingParameterValue(_(
|
||||
"Parameters %s were not passed to ironic"
|
||||
" for deploy.") % missing)
|
||||
|
||||
# ephemeral_format is nullable
|
||||
params['ephemeral_format'] = d_info.get('ephemeral_format')
|
||||
|
||||
return params
|
||||
|
||||
def get_properties(self):
|
||||
return COMMON_PROPERTIES
|
||||
|
||||
def validate(self, task, **kwargs):
|
||||
"""Validates the inputs for a vendor passthru.
|
||||
|
||||
This method checks whether the vendor passthru method is a valid one,
|
||||
and then validates whether the required information for executing the
|
||||
vendor passthru has been provided or not.
|
||||
|
||||
:param task: a TaskManager instance containing the node to act on.
|
||||
:param kwargs: kwargs containins the method name and its parameters.
|
||||
:raises: InvalidParameterValue if method is invalid or any parameters
|
||||
to the method is invalid.
|
||||
"""
|
||||
method = kwargs['method']
|
||||
if method == 'pass_deploy_info':
|
||||
self._get_deploy_info(task.node, **kwargs)
|
||||
iscsi_deploy.get_deploy_info(task.node, **kwargs)
|
||||
else:
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"Unsupported method (%s) passed to PXE driver.")
|
||||
|
@ -649,69 +394,51 @@ class VendorPassthru(base.VendorInterface):
|
|||
|
||||
@task_manager.require_exclusive_lock
|
||||
def _continue_deploy(self, task, **kwargs):
|
||||
"""Resume a deployment upon getting POST data from deploy ramdisk.
|
||||
"""Continues the deployment of baremetal node over iSCSI.
|
||||
|
||||
This method raises no exceptions because it is intended to be
|
||||
invoked asynchronously as a callback from the deploy ramdisk.
|
||||
This method continues the deployment of the baremetal node over iSCSI
|
||||
from where the deployment ramdisk has left off.
|
||||
|
||||
:param task: a TaskManager instance containing the node to act on.
|
||||
:param kwargs: kwargs for performing iscsi deployment.
|
||||
"""
|
||||
node = task.node
|
||||
|
||||
def _set_failed_state(msg):
|
||||
node.provision_state = states.DEPLOYFAIL
|
||||
node.target_provision_state = states.NOSTATE
|
||||
node.save(task.context)
|
||||
try:
|
||||
manager_utils.node_power_action(task, states.POWER_OFF)
|
||||
except Exception:
|
||||
msg = (_('Node %s failed to power off while handling deploy '
|
||||
'failure. This may be a serious condition. Node '
|
||||
'should be removed from Ironic or put in maintenance '
|
||||
'mode until the problem is resolved.') % node.uuid)
|
||||
LOG.error(msg)
|
||||
finally:
|
||||
# NOTE(deva): node_power_action() erases node.last_error
|
||||
# so we need to set it again here.
|
||||
node.last_error = msg
|
||||
node.save(task.context)
|
||||
|
||||
if node.provision_state != states.DEPLOYWAIT:
|
||||
LOG.error(_('Node %s is not waiting to be deployed.') %
|
||||
node.uuid)
|
||||
LOG.error(_LE('Node %s is not waiting to be deployed.'), node.uuid)
|
||||
return
|
||||
node.provision_state = states.DEPLOYING
|
||||
node.save(task.context)
|
||||
# remove cached keystone token immediately
|
||||
|
||||
_destroy_token_file(node)
|
||||
|
||||
params = self._get_deploy_info(node, **kwargs)
|
||||
ramdisk_error = kwargs.get('error')
|
||||
root_uuid = iscsi_deploy.continue_deploy(task, **kwargs)
|
||||
|
||||
if ramdisk_error:
|
||||
LOG.error(_('Error returned from PXE deploy ramdisk: %s')
|
||||
% ramdisk_error)
|
||||
_set_failed_state(_('Failure in PXE deploy ramdisk.'))
|
||||
_destroy_images(node.uuid)
|
||||
if not root_uuid:
|
||||
return
|
||||
|
||||
LOG.info(_('Continuing deployment for node %(node)s, params '
|
||||
'%(params)s') % {'node': node.uuid, 'params': params})
|
||||
|
||||
try:
|
||||
deploy_utils.deploy(**params)
|
||||
except Exception as e:
|
||||
LOG.error(_('PXE deploy failed for instance %(instance)s. '
|
||||
'Error: %(error)s') % {'instance': node.instance_uuid,
|
||||
'error': e})
|
||||
_set_failed_state(_('PXE driver failed to continue deployment.'))
|
||||
else:
|
||||
LOG.info(_('Deployment to node %s done') % node.uuid)
|
||||
pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid)
|
||||
deploy_utils.switch_pxe_config(pxe_config_path, root_uuid)
|
||||
|
||||
deploy_utils.notify_deploy_complete(kwargs['address'])
|
||||
|
||||
LOG.info(_LI('Deployment to node %s done'), node.uuid)
|
||||
node.provision_state = states.ACTIVE
|
||||
node.target_provision_state = states.NOSTATE
|
||||
node.save(task.context)
|
||||
except Exception as e:
|
||||
|
||||
_destroy_images(node.uuid)
|
||||
LOG.error(_LE('Deploy failed for instance %(instance)s. '
|
||||
'Error: %(error)s'),
|
||||
{'instance': node.instance_uuid, 'error': e})
|
||||
msg = _('Failed to continue iSCSI deployment.')
|
||||
iscsi_deploy.set_failed_state(task, msg)
|
||||
|
||||
def vendor_passthru(self, task, **kwargs):
|
||||
"""Invokes a vendor passthru method.
|
||||
|
||||
:param task: a TaskManager instance containing the node to act on.
|
||||
:param kwargs: kwargs containins the method name and its parameters.
|
||||
"""
|
||||
method = kwargs['method']
|
||||
if method == 'pass_deploy_info':
|
||||
self._continue_deploy(task, **kwargs)
|
||||
|
|
|
@ -2,7 +2,7 @@ default deploy
|
|||
|
||||
label deploy
|
||||
kernel {{ pxe_options.deployment_aki_path }}
|
||||
append initrd={{ pxe_options.deployment_ari_path }} rootfstype=ramfs selinux=0 disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn={{ pxe_options.deployment_iscsi_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }}
|
||||
append initrd={{ pxe_options.deployment_ari_path }} rootfstype=ramfs selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }}
|
||||
ipappend 3
|
||||
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ from ironic.common import disk_partitioner
|
|||
from ironic.common import exception
|
||||
from ironic.common import utils as common_utils
|
||||
from ironic.drivers.modules import deploy_utils as utils
|
||||
from ironic.drivers.modules import image_cache
|
||||
from ironic.openstack.common import processutils
|
||||
from ironic.tests import base as tests_base
|
||||
|
||||
|
@ -118,7 +119,6 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
iqn = 'iqn.xyz'
|
||||
lun = 1
|
||||
image_path = '/tmp/xyz/image'
|
||||
pxe_config_path = '/tmp/abc/pxeconfig'
|
||||
root_mb = 128
|
||||
swap_mb = 64
|
||||
ephemeral_mb = 0
|
||||
|
@ -133,7 +133,7 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
name_list = ['get_dev', 'get_image_mb', 'discovery', 'login_iscsi',
|
||||
'logout_iscsi', 'delete_iscsi', 'make_partitions',
|
||||
'is_block_device', 'dd', 'mkswap', 'block_uuid',
|
||||
'switch_pxe_config', 'notify', 'destroy_disk_metadata']
|
||||
'notify', 'destroy_disk_metadata']
|
||||
parent_mock = self._mock_calls(name_list)
|
||||
parent_mock.get_dev.return_value = dev
|
||||
parent_mock.get_image_mb.return_value = 1
|
||||
|
@ -156,16 +156,15 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
mock.call.mkswap(swap_part),
|
||||
mock.call.block_uuid(root_part),
|
||||
mock.call.logout_iscsi(address, port, iqn),
|
||||
mock.call.delete_iscsi(address, port, iqn),
|
||||
mock.call.switch_pxe_config(pxe_config_path,
|
||||
root_uuid),
|
||||
mock.call.notify(address, 10000)]
|
||||
mock.call.delete_iscsi(address, port, iqn)]
|
||||
|
||||
utils.deploy(address, port, iqn, lun, image_path, pxe_config_path,
|
||||
root_mb, swap_mb, ephemeral_mb, ephemeral_format,
|
||||
node_uuid)
|
||||
returned_root_uuid = utils.deploy(address, port, iqn, lun,
|
||||
image_path, root_mb, swap_mb,
|
||||
ephemeral_mb, ephemeral_format,
|
||||
node_uuid)
|
||||
|
||||
self.assertEqual(calls_expected, parent_mock.mock_calls)
|
||||
self.assertEqual(root_uuid, returned_root_uuid)
|
||||
|
||||
def test_deploy_without_swap(self):
|
||||
"""Check loosely all functions are called with right args."""
|
||||
|
@ -174,7 +173,6 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
iqn = 'iqn.xyz'
|
||||
lun = 1
|
||||
image_path = '/tmp/xyz/image'
|
||||
pxe_config_path = '/tmp/abc/pxeconfig'
|
||||
root_mb = 128
|
||||
swap_mb = 0
|
||||
ephemeral_mb = 0
|
||||
|
@ -188,7 +186,7 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
name_list = ['get_dev', 'get_image_mb', 'discovery', 'login_iscsi',
|
||||
'logout_iscsi', 'delete_iscsi', 'make_partitions',
|
||||
'is_block_device', 'dd', 'block_uuid',
|
||||
'switch_pxe_config', 'notify', 'destroy_disk_metadata']
|
||||
'notify', 'destroy_disk_metadata']
|
||||
parent_mock = self._mock_calls(name_list)
|
||||
parent_mock.get_dev.return_value = dev
|
||||
parent_mock.get_image_mb.return_value = 1
|
||||
|
@ -208,16 +206,15 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
mock.call.dd(image_path, root_part),
|
||||
mock.call.block_uuid(root_part),
|
||||
mock.call.logout_iscsi(address, port, iqn),
|
||||
mock.call.delete_iscsi(address, port, iqn),
|
||||
mock.call.switch_pxe_config(pxe_config_path,
|
||||
root_uuid),
|
||||
mock.call.notify(address, 10000)]
|
||||
mock.call.delete_iscsi(address, port, iqn)]
|
||||
|
||||
utils.deploy(address, port, iqn, lun, image_path, pxe_config_path,
|
||||
root_mb, swap_mb, ephemeral_mb, ephemeral_format,
|
||||
node_uuid)
|
||||
returned_root_uuid = utils.deploy(address, port, iqn, lun,
|
||||
image_path, root_mb, swap_mb,
|
||||
ephemeral_mb, ephemeral_format,
|
||||
node_uuid)
|
||||
|
||||
self.assertEqual(calls_expected, parent_mock.mock_calls)
|
||||
self.assertEqual(root_uuid, returned_root_uuid)
|
||||
|
||||
def test_deploy_with_ephemeral(self):
|
||||
"""Check loosely all functions are called with right args."""
|
||||
|
@ -226,7 +223,6 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
iqn = 'iqn.xyz'
|
||||
lun = 1
|
||||
image_path = '/tmp/xyz/image'
|
||||
pxe_config_path = '/tmp/abc/pxeconfig'
|
||||
root_mb = 128
|
||||
swap_mb = 64
|
||||
ephemeral_mb = 256
|
||||
|
@ -242,7 +238,7 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
name_list = ['get_dev', 'get_image_mb', 'discovery', 'login_iscsi',
|
||||
'logout_iscsi', 'delete_iscsi', 'make_partitions',
|
||||
'is_block_device', 'dd', 'mkswap', 'block_uuid',
|
||||
'switch_pxe_config', 'notify', 'mkfs_ephemeral',
|
||||
'notify', 'mkfs_ephemeral',
|
||||
'destroy_disk_metadata']
|
||||
parent_mock = self._mock_calls(name_list)
|
||||
parent_mock.get_dev.return_value = dev
|
||||
|
@ -270,16 +266,15 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
ephemeral_format),
|
||||
mock.call.block_uuid(root_part),
|
||||
mock.call.logout_iscsi(address, port, iqn),
|
||||
mock.call.delete_iscsi(address, port, iqn),
|
||||
mock.call.switch_pxe_config(pxe_config_path,
|
||||
root_uuid),
|
||||
mock.call.notify(address, 10000)]
|
||||
mock.call.delete_iscsi(address, port, iqn)]
|
||||
|
||||
utils.deploy(address, port, iqn, lun, image_path, pxe_config_path,
|
||||
root_mb, swap_mb, ephemeral_mb, ephemeral_format,
|
||||
node_uuid)
|
||||
returned_root_uuid = utils.deploy(address, port, iqn, lun,
|
||||
image_path, root_mb, swap_mb,
|
||||
ephemeral_mb, ephemeral_format,
|
||||
node_uuid)
|
||||
|
||||
self.assertEqual(calls_expected, parent_mock.mock_calls)
|
||||
self.assertEqual(root_uuid, returned_root_uuid)
|
||||
|
||||
def test_deploy_preserve_ephemeral(self):
|
||||
"""Check if all functions are called with right args."""
|
||||
|
@ -288,7 +283,6 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
iqn = 'iqn.xyz'
|
||||
lun = 1
|
||||
image_path = '/tmp/xyz/image'
|
||||
pxe_config_path = '/tmp/abc/pxeconfig'
|
||||
root_mb = 128
|
||||
swap_mb = 64
|
||||
ephemeral_mb = 256
|
||||
|
@ -304,8 +298,7 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
name_list = ['get_dev', 'get_image_mb', 'discovery', 'login_iscsi',
|
||||
'logout_iscsi', 'delete_iscsi', 'make_partitions',
|
||||
'is_block_device', 'dd', 'mkswap', 'block_uuid',
|
||||
'switch_pxe_config', 'notify', 'mkfs_ephemeral',
|
||||
'get_dev_block_size']
|
||||
'notify', 'mkfs_ephemeral', 'get_dev_block_size']
|
||||
parent_mock = self._mock_calls(name_list)
|
||||
parent_mock.get_dev.return_value = dev
|
||||
parent_mock.get_image_mb.return_value = 1
|
||||
|
@ -330,17 +323,16 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
mock.call.mkswap(swap_part),
|
||||
mock.call.block_uuid(root_part),
|
||||
mock.call.logout_iscsi(address, port, iqn),
|
||||
mock.call.delete_iscsi(address, port, iqn),
|
||||
mock.call.switch_pxe_config(pxe_config_path,
|
||||
root_uuid),
|
||||
mock.call.notify(address, 10000)]
|
||||
mock.call.delete_iscsi(address, port, iqn)]
|
||||
|
||||
utils.deploy(address, port, iqn, lun, image_path, pxe_config_path,
|
||||
root_mb, swap_mb, ephemeral_mb, ephemeral_format,
|
||||
node_uuid, preserve_ephemeral=True)
|
||||
returned_root_uuid = utils.deploy(address, port, iqn, lun,
|
||||
image_path, root_mb, swap_mb,
|
||||
ephemeral_mb, ephemeral_format,
|
||||
node_uuid, preserve_ephemeral=True)
|
||||
self.assertEqual(calls_expected, parent_mock.mock_calls)
|
||||
self.assertFalse(parent_mock.mkfs_ephemeral.called)
|
||||
self.assertFalse(parent_mock.get_dev_block_size.called)
|
||||
self.assertEqual(root_uuid, returned_root_uuid)
|
||||
|
||||
def test_always_logout_and_delete_iscsi(self):
|
||||
"""Check if logout_iscsi() and delete_iscsi() are called.
|
||||
|
@ -354,7 +346,6 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
iqn = 'iqn.xyz'
|
||||
lun = 1
|
||||
image_path = '/tmp/xyz/image'
|
||||
pxe_config_path = '/tmp/abc/pxeconfig'
|
||||
root_mb = 128
|
||||
swap_mb = 64
|
||||
ephemeral_mb = 256
|
||||
|
@ -393,8 +384,8 @@ class PhysicalWorkTestCase(tests_base.TestCase):
|
|||
|
||||
self.assertRaises(TestException, utils.deploy,
|
||||
address, port, iqn, lun, image_path,
|
||||
pxe_config_path, root_mb, swap_mb, ephemeral_mb,
|
||||
ephemeral_format, node_uuid)
|
||||
root_mb, swap_mb, ephemeral_mb, ephemeral_format,
|
||||
node_uuid)
|
||||
|
||||
self.assertEqual(calls_expected, parent_mock.mock_calls)
|
||||
|
||||
|
@ -715,3 +706,30 @@ class RealFilePartitioningTestCase(tests_base.TestCase):
|
|||
self.assertEqual([6, 3], sizes[:2],
|
||||
"unexpected partitioning %s" % part_table)
|
||||
self.assertIn(sizes[2], (9, 10))
|
||||
|
||||
@mock.patch.object(image_cache, 'clean_up_caches')
|
||||
def test_fetch_images(self, mock_clean_up_caches):
|
||||
|
||||
mock_cache = mock.MagicMock(master_dir='master_dir')
|
||||
utils.fetch_images(None, mock_cache, [('uuid', 'path')])
|
||||
mock_clean_up_caches.assert_called_once_with(None, 'master_dir',
|
||||
[('uuid', 'path')])
|
||||
mock_cache.fetch_image.assert_called_once_with('uuid', 'path',
|
||||
ctx=None)
|
||||
|
||||
@mock.patch.object(image_cache, 'clean_up_caches')
|
||||
def test_fetch_images_fail(self, mock_clean_up_caches):
|
||||
|
||||
exc = exception.InsufficientDiskSpace(path='a',
|
||||
required=2,
|
||||
actual=1)
|
||||
|
||||
mock_cache = mock.MagicMock(master_dir='master_dir')
|
||||
mock_clean_up_caches.side_effect = [exc]
|
||||
self.assertRaises(exception.InstanceDeployFailure,
|
||||
utils.fetch_images,
|
||||
None,
|
||||
mock_cache,
|
||||
[('uuid', 'path')])
|
||||
mock_clean_up_caches.assert_called_once_with(None, 'master_dir',
|
||||
[('uuid', 'path')])
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Test class for iSCSI deploy mechanism."""
|
||||
|
||||
import mock
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import utils
|
||||
from ironic.db import api as dbapi
|
||||
from ironic.drivers.modules import deploy_utils
|
||||
from ironic.drivers.modules import iscsi_deploy
|
||||
from ironic.openstack.common import context
|
||||
from ironic.openstack.common import fileutils
|
||||
from ironic.tests import base
|
||||
from ironic.tests.conductor import utils as mgr_utils
|
||||
from ironic.tests.db import base as db_base
|
||||
from ironic.tests.db import utils as db_utils
|
||||
from ironic.tests.objects import utils as obj_utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
INST_INFO_DICT = db_utils.get_test_pxe_instance_info()
|
||||
DRV_INFO_DICT = db_utils.get_test_pxe_driver_info()
|
||||
|
||||
|
||||
class IscsiDeployValidateParametersTestCase(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(IscsiDeployValidateParametersTestCase, self).setUp()
|
||||
self.context = context.get_admin_context()
|
||||
self.dbapi = dbapi.get_instance()
|
||||
|
||||
def test_parse_instance_info_good(self):
|
||||
# make sure we get back the expected things
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_pxe',
|
||||
instance_info=INST_INFO_DICT)
|
||||
info = iscsi_deploy.parse_instance_info(node)
|
||||
self.assertIsNotNone(info.get('image_source'))
|
||||
self.assertIsNotNone(info.get('root_gb'))
|
||||
self.assertEqual(0, info.get('ephemeral_gb'))
|
||||
|
||||
def test_parse_instance_info_missing_instance_source(self):
|
||||
# make sure error is raised when info is missing
|
||||
info = dict(INST_INFO_DICT)
|
||||
del info['image_source']
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
self.assertRaises(exception.MissingParameterValue,
|
||||
iscsi_deploy.parse_instance_info,
|
||||
node)
|
||||
|
||||
def test_parse_instance_info_missing_root_gb(self):
|
||||
# make sure error is raised when info is missing
|
||||
info = dict(INST_INFO_DICT)
|
||||
del info['root_gb']
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
self.assertRaises(exception.MissingParameterValue,
|
||||
iscsi_deploy.parse_instance_info,
|
||||
node)
|
||||
|
||||
def test_parse_instance_info_invalid_root_gb(self):
|
||||
info = dict(INST_INFO_DICT)
|
||||
info['root_gb'] = 'foobar'
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
iscsi_deploy.parse_instance_info,
|
||||
node)
|
||||
|
||||
def test_parse_instance_info_valid_ephemeral_gb(self):
|
||||
ephemeral_gb = 10
|
||||
ephemeral_fmt = 'test-fmt'
|
||||
info = dict(INST_INFO_DICT)
|
||||
info['ephemeral_gb'] = ephemeral_gb
|
||||
info['ephemeral_format'] = ephemeral_fmt
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
data = iscsi_deploy.parse_instance_info(node)
|
||||
self.assertEqual(ephemeral_gb, data.get('ephemeral_gb'))
|
||||
self.assertEqual(ephemeral_fmt, data.get('ephemeral_format'))
|
||||
|
||||
def test_parse_instance_info_invalid_ephemeral_gb(self):
|
||||
info = dict(INST_INFO_DICT)
|
||||
info['ephemeral_gb'] = 'foobar'
|
||||
info['ephemeral_format'] = 'exttest'
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
iscsi_deploy.parse_instance_info,
|
||||
node)
|
||||
|
||||
def test_parse_instance_info_valid_ephemeral_missing_format(self):
|
||||
ephemeral_gb = 10
|
||||
ephemeral_fmt = 'test-fmt'
|
||||
info = dict(INST_INFO_DICT)
|
||||
info['ephemeral_gb'] = ephemeral_gb
|
||||
info['ephemeral_format'] = None
|
||||
self.config(default_ephemeral_format=ephemeral_fmt, group='pxe')
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
instance_info = iscsi_deploy.parse_instance_info(node)
|
||||
self.assertEqual(ephemeral_fmt, instance_info['ephemeral_format'])
|
||||
|
||||
def test_parse_instance_info_valid_preserve_ephemeral_true(self):
|
||||
info = dict(INST_INFO_DICT)
|
||||
for _id, opt in enumerate(['true', 'TRUE', 'True', 't',
|
||||
'on', 'yes', 'y', '1']):
|
||||
info['preserve_ephemeral'] = opt
|
||||
node = obj_utils.create_test_node(self.context, id=_id,
|
||||
uuid=utils.generate_uuid(),
|
||||
instance_info=info)
|
||||
data = iscsi_deploy.parse_instance_info(node)
|
||||
self.assertTrue(data.get('preserve_ephemeral'))
|
||||
|
||||
def test_parse_instance_info_valid_preserve_ephemeral_false(self):
|
||||
info = dict(INST_INFO_DICT)
|
||||
for _id, opt in enumerate(['false', 'FALSE', 'False', 'f',
|
||||
'off', 'no', 'n', '0']):
|
||||
info['preserve_ephemeral'] = opt
|
||||
node = obj_utils.create_test_node(self.context, id=_id,
|
||||
uuid=utils.generate_uuid(),
|
||||
instance_info=info)
|
||||
data = iscsi_deploy.parse_instance_info(node)
|
||||
self.assertFalse(data.get('preserve_ephemeral'))
|
||||
|
||||
def test_parse_instance_info_invalid_preserve_ephemeral(self):
|
||||
info = dict(INST_INFO_DICT)
|
||||
info['preserve_ephemeral'] = 'foobar'
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
iscsi_deploy.parse_instance_info,
|
||||
node)
|
||||
|
||||
|
||||
class IscsiDeployPrivateMethodsTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(IscsiDeployPrivateMethodsTestCase, self).setUp()
|
||||
n = {
|
||||
'driver': 'fake_pxe',
|
||||
'instance_info': INST_INFO_DICT,
|
||||
'driver_info': DRV_INFO_DICT,
|
||||
}
|
||||
mgr_utils.mock_the_extension_manager(driver="fake_pxe")
|
||||
self.dbapi = dbapi.get_instance()
|
||||
self.context = context.get_admin_context()
|
||||
self.node = obj_utils.create_test_node(self.context, **n)
|
||||
|
||||
def test__get_image_dir_path(self):
|
||||
self.assertEqual(os.path.join(CONF.pxe.images_path,
|
||||
self.node.uuid),
|
||||
iscsi_deploy._get_image_dir_path(self.node.uuid))
|
||||
|
||||
def test__get_image_file_path(self):
|
||||
self.assertEqual(os.path.join(CONF.pxe.images_path,
|
||||
self.node.uuid,
|
||||
'disk'),
|
||||
iscsi_deploy._get_image_file_path(self.node.uuid))
|
||||
|
||||
|
||||
class IscsiDeployMethodsTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(IscsiDeployMethodsTestCase, self).setUp()
|
||||
n = {
|
||||
'driver': 'fake_pxe',
|
||||
'instance_info': INST_INFO_DICT,
|
||||
'driver_info': DRV_INFO_DICT,
|
||||
}
|
||||
mgr_utils.mock_the_extension_manager(driver="fake_pxe")
|
||||
self.dbapi = dbapi.get_instance()
|
||||
self.context = context.get_admin_context()
|
||||
self.node = obj_utils.create_test_node(self.context, **n)
|
||||
|
||||
@mock.patch.object(deploy_utils, 'fetch_images')
|
||||
def test_cache_instance_images_master_path(self, mock_fetch_image):
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
self.config(images_path=temp_dir, group='pxe')
|
||||
self.config(instance_master_path=os.path.join(temp_dir,
|
||||
'instance_master_path'),
|
||||
group='pxe')
|
||||
fileutils.ensure_tree(CONF.pxe.instance_master_path)
|
||||
|
||||
(uuid, image_path) = iscsi_deploy.cache_instance_image(None, self.node)
|
||||
mock_fetch_image.assert_called_once_with(None,
|
||||
mock.ANY,
|
||||
[(uuid, image_path)])
|
||||
self.assertEqual('glance://image_uuid', uuid)
|
||||
self.assertEqual(os.path.join(temp_dir,
|
||||
self.node.uuid,
|
||||
'disk'),
|
||||
image_path)
|
|
@ -35,7 +35,7 @@ from ironic.conductor import task_manager
|
|||
from ironic.conductor import utils as manager_utils
|
||||
from ironic.db import api as dbapi
|
||||
from ironic.drivers.modules import deploy_utils
|
||||
from ironic.drivers.modules import image_cache
|
||||
from ironic.drivers.modules import iscsi_deploy
|
||||
from ironic.drivers.modules import pxe
|
||||
from ironic.openstack.common import context
|
||||
from ironic.openstack.common import fileutils
|
||||
|
@ -46,7 +46,6 @@ from ironic.tests.db import base as db_base
|
|||
from ironic.tests.db import utils as db_utils
|
||||
from ironic.tests.objects import utils as obj_utils
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
INST_INFO_DICT = db_utils.get_test_pxe_instance_info()
|
||||
|
@ -100,103 +99,6 @@ class PXEValidateParametersTestCase(base.TestCase):
|
|||
self.assertIsNotNone(info.get('deploy_ramdisk'))
|
||||
self.assertIsNotNone(info.get('deploy_kernel'))
|
||||
|
||||
def test__parse_instance_info_good(self):
|
||||
# make sure we get back the expected things
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_pxe',
|
||||
instance_info=INST_INFO_DICT)
|
||||
info = pxe._parse_instance_info(node)
|
||||
self.assertIsNotNone(info.get('image_source'))
|
||||
self.assertIsNotNone(info.get('root_gb'))
|
||||
self.assertEqual(0, info.get('ephemeral_gb'))
|
||||
|
||||
def test__parse_instance_info_missing_instance_source(self):
|
||||
# make sure error is raised when info is missing
|
||||
info = dict(INST_INFO_DICT)
|
||||
del info['image_source']
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
self.assertRaises(exception.MissingParameterValue,
|
||||
pxe._parse_instance_info,
|
||||
node)
|
||||
|
||||
def test__parse_instance_info_missing_root_gb(self):
|
||||
# make sure error is raised when info is missing
|
||||
info = dict(INST_INFO_DICT)
|
||||
del info['root_gb']
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
self.assertRaises(exception.MissingParameterValue,
|
||||
pxe._parse_instance_info,
|
||||
node)
|
||||
|
||||
def test__parse_instance_info_invalid_root_gb(self):
|
||||
info = dict(INST_INFO_DICT)
|
||||
info['root_gb'] = 'foobar'
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
pxe._parse_instance_info,
|
||||
node)
|
||||
|
||||
def test__parse_instance_info_valid_ephemeral_gb(self):
|
||||
ephemeral_gb = 10
|
||||
ephemeral_fmt = 'test-fmt'
|
||||
info = dict(INST_INFO_DICT)
|
||||
info['ephemeral_gb'] = ephemeral_gb
|
||||
info['ephemeral_format'] = ephemeral_fmt
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
data = pxe._parse_instance_info(node)
|
||||
self.assertEqual(ephemeral_gb, data.get('ephemeral_gb'))
|
||||
self.assertEqual(ephemeral_fmt, data.get('ephemeral_format'))
|
||||
|
||||
def test__parse_instance_info_invalid_ephemeral_gb(self):
|
||||
info = dict(INST_INFO_DICT)
|
||||
info['ephemeral_gb'] = 'foobar'
|
||||
info['ephemeral_format'] = 'exttest'
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
pxe._parse_instance_info,
|
||||
node)
|
||||
|
||||
def test__parse_instance_info_valid_ephemeral_missing_format(self):
|
||||
ephemeral_gb = 10
|
||||
ephemeral_fmt = 'test-fmt'
|
||||
info = dict(INST_INFO_DICT)
|
||||
info['ephemeral_gb'] = ephemeral_gb
|
||||
info['ephemeral_format'] = None
|
||||
self.config(default_ephemeral_format=ephemeral_fmt, group='pxe')
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
instance_info = pxe._parse_instance_info(node)
|
||||
self.assertEqual(ephemeral_fmt, instance_info['ephemeral_format'])
|
||||
|
||||
def test__parse_instance_info_valid_preserve_ephemeral_true(self):
|
||||
info = dict(INST_INFO_DICT)
|
||||
for _id, opt in enumerate(['true', 'TRUE', 'True', 't',
|
||||
'on', 'yes', 'y', '1']):
|
||||
info['preserve_ephemeral'] = opt
|
||||
node = obj_utils.create_test_node(self.context, id=_id,
|
||||
uuid=utils.generate_uuid(),
|
||||
instance_info=info)
|
||||
data = pxe._parse_instance_info(node)
|
||||
self.assertTrue(data.get('preserve_ephemeral'))
|
||||
|
||||
def test__parse_instance_info_valid_preserve_ephemeral_false(self):
|
||||
info = dict(INST_INFO_DICT)
|
||||
for _id, opt in enumerate(['false', 'FALSE', 'False', 'f',
|
||||
'off', 'no', 'n', '0']):
|
||||
info['preserve_ephemeral'] = opt
|
||||
node = obj_utils.create_test_node(self.context, id=_id,
|
||||
uuid=utils.generate_uuid(),
|
||||
instance_info=info)
|
||||
data = pxe._parse_instance_info(node)
|
||||
self.assertFalse(data.get('preserve_ephemeral'))
|
||||
|
||||
def test__parse_instance_info_invalid_preserve_ephemeral(self):
|
||||
info = dict(INST_INFO_DICT)
|
||||
info['preserve_ephemeral'] = 'foobar'
|
||||
node = obj_utils.create_test_node(self.context, instance_info=info)
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
pxe._parse_instance_info,
|
||||
node)
|
||||
|
||||
|
||||
class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
||||
|
||||
|
@ -260,6 +162,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
|||
self.config(pxe_append_params='test_param', group='pxe')
|
||||
# NOTE: right '/' should be removed from url string
|
||||
self.config(api_url='http://192.168.122.184:6385/', group='conductor')
|
||||
self.config(disk_devices='sda', group='pxe')
|
||||
|
||||
fake_key = '0123456789ABCDEFGHIJKLMNOPQRSTUV'
|
||||
random_alnum_mock.return_value = fake_key
|
||||
|
@ -290,7 +193,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
|||
expected_options = {
|
||||
'deployment_key': '0123456789ABCDEFGHIJKLMNOPQRSTUV',
|
||||
'ari_path': ramdisk,
|
||||
'deployment_iscsi_iqn': u'iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33'
|
||||
'iscsi_target_iqn': u'iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33'
|
||||
u'c123',
|
||||
'deployment_ari_path': deploy_ramdisk,
|
||||
'pxe_append_params': 'test_param',
|
||||
|
@ -298,6 +201,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
|||
'deployment_id': u'1be26c0b-03f2-4d2e-ae87-c02d7f33c123',
|
||||
'ironic_api_url': 'http://192.168.122.184:6385',
|
||||
'deployment_aki_path': deploy_kernel,
|
||||
'disk': 'sda'
|
||||
}
|
||||
|
||||
image_info = {'deploy_kernel': ('deploy_kernel',
|
||||
|
@ -335,22 +239,12 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
|||
def test__build_pxe_config_options_ipxe(self):
|
||||
self._test_build_pxe_config_options(ipxe_enabled=True)
|
||||
|
||||
def test__get_image_dir_path(self):
|
||||
self.assertEqual(os.path.join(CONF.pxe.images_path, self.node.uuid),
|
||||
pxe._get_image_dir_path(self.node.uuid))
|
||||
|
||||
def test__get_image_file_path(self):
|
||||
self.assertEqual(os.path.join(CONF.pxe.images_path,
|
||||
self.node.uuid,
|
||||
'disk'),
|
||||
pxe._get_image_file_path(self.node.uuid))
|
||||
|
||||
def test_get_token_file_path(self):
|
||||
node_uuid = self.node.uuid
|
||||
self.assertEqual('/tftpboot/token-' + node_uuid,
|
||||
pxe._get_token_file_path(node_uuid))
|
||||
|
||||
@mock.patch.object(pxe, '_fetch_images')
|
||||
@mock.patch.object(deploy_utils, 'fetch_images')
|
||||
def test__cache_tftp_images_master_path(self, mock_fetch_image):
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
self.config(tftp_root=temp_dir, group='pxe')
|
||||
|
@ -369,29 +263,9 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
|||
[('deploy_kernel',
|
||||
image_path)])
|
||||
|
||||
@mock.patch.object(pxe, '_fetch_images')
|
||||
def test__cache_instance_images_master_path(self, mock_fetch_image):
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
self.config(images_path=temp_dir, group='pxe')
|
||||
self.config(instance_master_path=os.path.join(temp_dir,
|
||||
'instance_master_path'),
|
||||
group='pxe')
|
||||
fileutils.ensure_tree(CONF.pxe.instance_master_path)
|
||||
|
||||
(uuid, image_path) = pxe._cache_instance_image(None,
|
||||
self.node)
|
||||
mock_fetch_image.assert_called_once_with(None,
|
||||
mock.ANY,
|
||||
[(uuid, image_path)])
|
||||
self.assertEqual('glance://image_uuid', uuid)
|
||||
self.assertEqual(os.path.join(temp_dir,
|
||||
self.node.uuid,
|
||||
'disk'),
|
||||
image_path)
|
||||
|
||||
@mock.patch.object(pxe, 'TFTPImageCache', lambda: None)
|
||||
@mock.patch.object(fileutils, 'ensure_tree')
|
||||
@mock.patch.object(pxe, '_fetch_images')
|
||||
@mock.patch.object(deploy_utils, 'fetch_images')
|
||||
def test__cache_ramdisk_kernel(self, mock_fetch_image, mock_ensure_tree):
|
||||
self.config(ipxe_enabled=False, group='pxe')
|
||||
fake_pxe_info = {'foo': 'bar'}
|
||||
|
@ -404,7 +278,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
|||
|
||||
@mock.patch.object(pxe, 'TFTPImageCache', lambda: None)
|
||||
@mock.patch.object(fileutils, 'ensure_tree')
|
||||
@mock.patch.object(pxe, '_fetch_images')
|
||||
@mock.patch.object(deploy_utils, 'fetch_images')
|
||||
def test__cache_ramdisk_kernel_ipxe(self, mock_fetch_image,
|
||||
mock_ensure_tree):
|
||||
self.config(ipxe_enabled=True, group='pxe')
|
||||
|
@ -416,33 +290,6 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
|||
mock_fetch_image.assert_called_once_with(self.context, mock.ANY,
|
||||
fake_pxe_info.values())
|
||||
|
||||
@mock.patch.object(image_cache, 'clean_up_caches')
|
||||
def test__fetch_images(self, mock_clean_up_caches):
|
||||
|
||||
mock_cache = mock.MagicMock(master_dir='master_dir')
|
||||
pxe._fetch_images(None, mock_cache, [('uuid', 'path')])
|
||||
mock_clean_up_caches.assert_called_once_with(None, 'master_dir',
|
||||
[('uuid', 'path')])
|
||||
mock_cache.fetch_image.assert_called_once_with('uuid', 'path',
|
||||
ctx=None)
|
||||
|
||||
@mock.patch.object(image_cache, 'clean_up_caches')
|
||||
def test__fetch_images_fail(self, mock_clean_up_caches):
|
||||
|
||||
exc = exception.InsufficientDiskSpace(path='a',
|
||||
required=2,
|
||||
actual=1)
|
||||
|
||||
mock_cache = mock.MagicMock(master_dir='master_dir')
|
||||
mock_clean_up_caches.side_effect = [exc]
|
||||
self.assertRaises(exception.InstanceDeployFailure,
|
||||
pxe._fetch_images,
|
||||
None,
|
||||
mock_cache,
|
||||
[('uuid', 'path')])
|
||||
mock_clean_up_caches.assert_called_once_with(None, 'master_dir',
|
||||
[('uuid', 'path')])
|
||||
|
||||
|
||||
class PXEDriverTestCase(db_base.DbTestCase):
|
||||
|
||||
|
@ -622,8 +469,8 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
|||
task.node, None)
|
||||
|
||||
@mock.patch.object(deploy_utils, 'get_image_mb')
|
||||
@mock.patch.object(pxe, '_get_image_file_path')
|
||||
@mock.patch.object(pxe, '_cache_instance_image')
|
||||
@mock.patch.object(iscsi_deploy, '_get_image_file_path')
|
||||
@mock.patch.object(iscsi_deploy, 'cache_instance_image')
|
||||
@mock.patch.object(neutron, 'update_neutron')
|
||||
@mock.patch.object(manager_utils, 'node_power_action')
|
||||
@mock.patch.object(manager_utils, 'node_set_boot_device')
|
||||
|
@ -655,8 +502,8 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
|||
self.assertEqual(self.context.auth_token, token)
|
||||
|
||||
@mock.patch.object(deploy_utils, 'get_image_mb')
|
||||
@mock.patch.object(pxe, '_get_image_file_path')
|
||||
@mock.patch.object(pxe, '_cache_instance_image')
|
||||
@mock.patch.object(iscsi_deploy, '_get_image_file_path')
|
||||
@mock.patch.object(iscsi_deploy, 'cache_instance_image')
|
||||
def test_deploy_image_too_large(self, mock_cache_instance_image,
|
||||
mock_get_image_file_path,
|
||||
mock_get_image_mb):
|
||||
|
@ -690,15 +537,20 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
|||
update_neutron_mock.assert_called_once_with(
|
||||
task, dhcp_opts)
|
||||
|
||||
@mock.patch.object(pxe, 'InstanceImageCache')
|
||||
def test_continue_deploy_good(self, mock_image_cache):
|
||||
@mock.patch.object(deploy_utils, 'notify_deploy_complete')
|
||||
@mock.patch.object(deploy_utils, 'switch_pxe_config')
|
||||
@mock.patch.object(iscsi_deploy, 'InstanceImageCache')
|
||||
def test_continue_deploy_good(self, mock_image_cache, mock_switch_config,
|
||||
notify_mock):
|
||||
token_path = self._create_token_file()
|
||||
self.node.power_state = states.POWER_ON
|
||||
self.node.provision_state = states.DEPLOYWAIT
|
||||
self.node.save()
|
||||
|
||||
root_uuid = "12345678-1234-1234-1234-1234567890abcxyz"
|
||||
|
||||
def fake_deploy(**kwargs):
|
||||
pass
|
||||
return root_uuid
|
||||
|
||||
self.useFixture(fixtures.MonkeyPatch(
|
||||
'ironic.drivers.modules.deploy_utils.deploy',
|
||||
|
@ -715,8 +567,11 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
|||
self.assertFalse(os.path.exists(token_path))
|
||||
mock_image_cache.assert_called_once_with()
|
||||
mock_image_cache.return_value.clean_up.assert_called_once_with()
|
||||
pxe_config_path = pxe_utils.get_pxe_config_file_path(self.node.uuid)
|
||||
mock_switch_config.assert_called_once_with(pxe_config_path, root_uuid)
|
||||
notify_mock.assert_called_once_with('123456')
|
||||
|
||||
@mock.patch.object(pxe, 'InstanceImageCache')
|
||||
@mock.patch.object(iscsi_deploy, 'InstanceImageCache')
|
||||
def test_continue_deploy_fail(self, mock_image_cache):
|
||||
token_path = self._create_token_file()
|
||||
self.node.power_state = states.POWER_ON
|
||||
|
@ -742,7 +597,7 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
|||
mock_image_cache.assert_called_once_with()
|
||||
mock_image_cache.return_value.clean_up.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(pxe, 'InstanceImageCache')
|
||||
@mock.patch.object(iscsi_deploy, 'InstanceImageCache')
|
||||
def test_continue_deploy_ramdisk_fails(self, mock_image_cache):
|
||||
token_path = self._create_token_file()
|
||||
self.node.power_state = states.POWER_ON
|
||||
|
@ -867,6 +722,7 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
|||
assert_false_path = [config_path, deploy_kernel_path, image_path,
|
||||
pxe_mac_path, image_dir, instance_dir,
|
||||
token_path]
|
||||
|
||||
for path in assert_false_path:
|
||||
self.assertFalse(os.path.exists(path))
|
||||
|
||||
|
@ -882,8 +738,8 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
|||
|
||||
master_d_kernel_path = os.path.join(CONF.pxe.tftp_master_path,
|
||||
'deploy_kernel_uuid')
|
||||
master_instance_path = os.path.join(CONF.pxe.instance_master_path,
|
||||
'image_uuid')
|
||||
instance_master_path = CONF.pxe.instance_master_path
|
||||
master_instance_path = os.path.join(instance_master_path, 'image_uuid')
|
||||
|
||||
self.assertTrue(os.path.exists(master_d_kernel_path))
|
||||
self.assertTrue(os.path.exists(master_instance_path))
|
||||
|
|
|
@ -41,8 +41,8 @@ class TestPXEUtils(db_base.DbTestCase):
|
|||
'deployment_key': '0123456789ABCDEFGHIJKLMNOPQRSTUV',
|
||||
'ari_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/'
|
||||
u'ramdisk',
|
||||
'deployment_iscsi_iqn': u'iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33'
|
||||
u'c123',
|
||||
'iscsi_target_iqn': u'iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33'
|
||||
u'c123',
|
||||
'deployment_ari_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7'
|
||||
u'f33c123/deploy_ramdisk',
|
||||
'pxe_append_params': 'test_param',
|
||||
|
@ -51,7 +51,8 @@ class TestPXEUtils(db_base.DbTestCase):
|
|||
'deployment_id': u'1be26c0b-03f2-4d2e-ae87-c02d7f33c123',
|
||||
'ironic_api_url': 'http://192.168.122.184:6385',
|
||||
'deployment_aki_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-'
|
||||
u'c02d7f33c123/deploy_kernel'
|
||||
u'c02d7f33c123/deploy_kernel',
|
||||
'disk': 'cciss/c0d0,sda,hda,vda'
|
||||
}
|
||||
self.node = object_utils.create_test_node(self.context)
|
||||
|
||||
|
|
Loading…
Reference in New Issue