Merge "Factoring out PXE and TFTP functions"
This commit is contained in:
commit
ebca58dc00
@ -910,13 +910,6 @@
|
||||
# is created. (string value)
|
||||
#default_ephemeral_format=ext4
|
||||
|
||||
# IP address of Ironic compute node's tftp server. (string
|
||||
# value)
|
||||
#tftp_server=$my_ip
|
||||
|
||||
# Ironic compute node's tftp root path. (string value)
|
||||
#tftp_root=/tftpboot
|
||||
|
||||
# Directory where images are stored on disk. (string value)
|
||||
#images_path=/var/lib/ironic/images/
|
||||
|
||||
@ -964,3 +957,17 @@
|
||||
#libvirt_uri=qemu:///system
|
||||
|
||||
|
||||
[tftp]
|
||||
|
||||
#
|
||||
# Options defined in ironic.common.tftp
|
||||
#
|
||||
|
||||
# IP address of Ironic compute node's tftp server. (string
|
||||
# value)
|
||||
#tftp_server=$my_ip
|
||||
|
||||
# Ironic compute node's tftp root path. (string value)
|
||||
#tftp_root=/tftpboot
|
||||
|
||||
|
||||
|
@ -21,6 +21,7 @@ from oslo.config import cfg
|
||||
from ironic.api import acl
|
||||
from ironic.common import exception
|
||||
from ironic.common import keystone
|
||||
from ironic.common import tftp
|
||||
from ironic.openstack.common import log as logging
|
||||
|
||||
|
||||
@ -30,10 +31,12 @@ neutron_opts = [
|
||||
help='URL for connecting to neutron.'),
|
||||
cfg.IntOpt('url_timeout',
|
||||
default=30,
|
||||
help='Timeout value for connecting to neutron in seconds.'),
|
||||
]
|
||||
help='Timeout value for connecting to neutron in seconds.')
|
||||
]
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.import_opt('my_ip', 'ironic.netconf')
|
||||
CONF.register_opts(neutron_opts, group='neutron')
|
||||
acl.register_opts(CONF)
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -105,3 +108,54 @@ class NeutronAPI(object):
|
||||
LOG.exception(_("Failed to update MAC address on Neutron port %s."
|
||||
), port_id)
|
||||
raise exception.FailedToUpdateMacOnPort(port_id=port_id)
|
||||
|
||||
|
||||
def get_node_vif_ids(task):
|
||||
"""Get all Neutron VIF ids for a node.
|
||||
|
||||
This function does not handle multi node operations.
|
||||
|
||||
:param task: a TaskManager instance.
|
||||
:returns: A dict of the Node's port UUIDs and their associated VIFs
|
||||
|
||||
"""
|
||||
port_vifs = {}
|
||||
for port in task.ports:
|
||||
vif = port.extra.get('vif_port_id')
|
||||
if vif:
|
||||
port_vifs[port.uuid] = vif
|
||||
return port_vifs
|
||||
|
||||
|
||||
def update_neutron(task, pxe_bootfile_name):
|
||||
"""Send or update the DHCP BOOT options to Neutron for this node."""
|
||||
options = tftp.dhcp_options_for_instance(pxe_bootfile_name)
|
||||
vifs = get_node_vif_ids(task)
|
||||
if not vifs:
|
||||
LOG.warning(_("No VIFs found for node %(node)s when attempting to "
|
||||
"update Neutron DHCP BOOT options."),
|
||||
{'node': task.node.uuid})
|
||||
return
|
||||
|
||||
# TODO(deva): decouple instantiation of NeutronAPI from task.context.
|
||||
# Try to use the user's task.context.auth_token, but if it
|
||||
# is not present, fall back to a server-generated context.
|
||||
# We don't need to recreate this in every method call.
|
||||
api = NeutronAPI(task.context)
|
||||
failures = []
|
||||
for port_id, port_vif in vifs.iteritems():
|
||||
try:
|
||||
api.update_port_dhcp_opts(port_vif, options)
|
||||
except exception.FailedToUpdateDHCPOptOnPort:
|
||||
failures.append(port_id)
|
||||
|
||||
if failures:
|
||||
if len(failures) == len(vifs):
|
||||
raise exception.FailedToUpdateDHCPOptOnPort(_(
|
||||
"Failed to set DHCP BOOT options for any port on node %s.") %
|
||||
task.node.uuid)
|
||||
else:
|
||||
LOG.warning(_("Some errors were encountered when updating the "
|
||||
"DHCP BOOT options for node %(node)s on the "
|
||||
"following ports: %(ports)s."),
|
||||
{'node': task.node.uuid, 'ports': failures})
|
||||
|
125
ironic/common/tftp.py
Normal file
125
ironic/common/tftp.py
Normal file
@ -0,0 +1,125 @@
|
||||
#
|
||||
# Copyright 2014 Rackspace, Inc
|
||||
# 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
|
||||
|
||||
import jinja2
|
||||
from oslo.config import cfg
|
||||
|
||||
from ironic.common import utils
|
||||
from ironic.drivers import utils as driver_utils
|
||||
from ironic.openstack.common import fileutils
|
||||
from ironic.openstack.common import log as logging
|
||||
|
||||
|
||||
tftp_opts = [
|
||||
cfg.StrOpt('tftp_server',
|
||||
default='$my_ip',
|
||||
help='IP address of Ironic compute node\'s tftp server.',
|
||||
deprecated_group='pxe'),
|
||||
cfg.StrOpt('tftp_root',
|
||||
default='/tftpboot',
|
||||
help='Ironic compute node\'s tftp root path.',
|
||||
deprecated_group='pxe')
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(tftp_opts, group='tftp')
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_pxe_config(task, pxe_options, pxe_config_template):
|
||||
"""Generate PXE configuration file and MAC symlinks for it."""
|
||||
node = task.node
|
||||
fileutils.ensure_tree(os.path.join(CONF.tftp.tftp_root,
|
||||
node.uuid))
|
||||
fileutils.ensure_tree(os.path.join(CONF.tftp.tftp_root,
|
||||
'pxelinux.cfg'))
|
||||
|
||||
pxe_config_file_path = get_pxe_config_file_path(node.uuid)
|
||||
pxe_config = build_pxe_config(node, pxe_options, pxe_config_template)
|
||||
utils.write_to_file(pxe_config_file_path, pxe_config)
|
||||
_write_mac_pxe_configs(task)
|
||||
|
||||
|
||||
def clean_up_pxe_config(task):
|
||||
"""Clean up the TFTP environment for the task's node."""
|
||||
node = task.node
|
||||
|
||||
utils.unlink_without_raise(get_pxe_config_file_path(node.uuid))
|
||||
for port in driver_utils.get_node_mac_addresses(task):
|
||||
utils.unlink_without_raise(get_pxe_mac_path(port))
|
||||
|
||||
utils.rmtree_without_raise(os.path.join(CONF.tftp.tftp_root, node.uuid))
|
||||
|
||||
|
||||
def _write_mac_pxe_configs(task):
|
||||
"""Create a file in the PXE config directory for each MAC so regardless
|
||||
of which port boots first, they'll get the same PXE config.
|
||||
"""
|
||||
pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
|
||||
for port in driver_utils.get_node_mac_addresses(task):
|
||||
mac_path = get_pxe_mac_path(port)
|
||||
utils.unlink_without_raise(mac_path)
|
||||
utils.create_link_without_raise(pxe_config_file_path, mac_path)
|
||||
|
||||
|
||||
def build_pxe_config(node, pxe_options, pxe_config_template):
|
||||
"""Build the PXE config file for a node
|
||||
|
||||
This method builds the PXE boot configuration file for a node,
|
||||
given all the required parameters.
|
||||
|
||||
:param pxe_options: A dict of values to set on the configuration file
|
||||
:returns: A formatted string with the file content.
|
||||
"""
|
||||
LOG.debug("Building PXE config for deployment %s."), node['id']
|
||||
|
||||
tmpl_path, tmpl_file = os.path.split(pxe_config_template)
|
||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path))
|
||||
template = env.get_template(tmpl_file)
|
||||
return template.render({'pxe_options': pxe_options,
|
||||
'ROOT': '{{ ROOT }}'})
|
||||
|
||||
|
||||
def get_pxe_mac_path(mac):
|
||||
"""Convert a MAC address into a PXE config file name.
|
||||
|
||||
:param mac: A mac address string in the format xx:xx:xx:xx:xx:xx.
|
||||
:returns: the path to the config file.
|
||||
"""
|
||||
return os.path.join(
|
||||
CONF.tftp.tftp_root,
|
||||
'pxelinux.cfg',
|
||||
"01-" + mac.replace(":", "-").lower()
|
||||
)
|
||||
|
||||
|
||||
def get_pxe_config_file_path(node_uuid):
|
||||
"""Generate the path for an instances PXE config file."""
|
||||
return os.path.join(CONF.tftp.tftp_root, node_uuid, 'config')
|
||||
|
||||
|
||||
def dhcp_options_for_instance(pxe_bootfile_name):
|
||||
"""Retrives the DHCP PXE boot options."""
|
||||
return [{'opt_name': 'bootfile-name',
|
||||
'opt_value': pxe_bootfile_name},
|
||||
{'opt_name': 'server-ip-address',
|
||||
'opt_value': CONF.tftp.tftp_server},
|
||||
{'opt_name': 'tftp-server',
|
||||
'opt_value': CONF.tftp.tftp_server}
|
||||
]
|
@ -19,7 +19,6 @@ PXE Driver and supporting meta-classes.
|
||||
|
||||
import os
|
||||
|
||||
import jinja2
|
||||
from oslo.config import cfg
|
||||
|
||||
from ironic.common import exception
|
||||
@ -29,6 +28,7 @@ from ironic.common import keystone
|
||||
from ironic.common import neutron
|
||||
from ironic.common import paths
|
||||
from ironic.common import states
|
||||
from ironic.common import tftp
|
||||
from ironic.common import utils
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.conductor import utils as manager_utils
|
||||
@ -53,12 +53,6 @@ pxe_opts = [
|
||||
default='ext4',
|
||||
help='Default file system format for ephemeral partition, '
|
||||
'if one is created.'),
|
||||
cfg.StrOpt('tftp_server',
|
||||
default='$my_ip',
|
||||
help='IP address of Ironic compute node\'s tftp server.'),
|
||||
cfg.StrOpt('tftp_root',
|
||||
default='/tftpboot',
|
||||
help='Ironic compute node\'s tftp root path.'),
|
||||
cfg.StrOpt('images_path',
|
||||
default='/var/lib/ironic/images/',
|
||||
help='Directory where images are stored on disk.'),
|
||||
@ -147,20 +141,19 @@ def _parse_driver_info(node):
|
||||
return d_info
|
||||
|
||||
|
||||
def _build_pxe_config(node, pxe_info, ctx):
|
||||
"""Build the PXE config file for a node
|
||||
def _build_pxe_config_options(node, pxe_info, ctx):
|
||||
"""Build the PXE config options for a node
|
||||
|
||||
This method builds the PXE boot configuration file for a node,
|
||||
This method builds the PXE boot options for a node,
|
||||
given all the required parameters.
|
||||
|
||||
The resulting file has both a "deploy" and "boot" label, which correspond
|
||||
to the two phases of booting. This may be extended later.
|
||||
The options should then be passed to tftp.create_pxe_config to create
|
||||
the actual config files.
|
||||
|
||||
:param pxe_options: A dict of values to set on the configuarion file
|
||||
:returns: A formated string with the file content.
|
||||
:param pxe_options: A dict of values to set on the configuration file
|
||||
:returns: A dictionary of pxe options to be used in the pxe bootfile
|
||||
template.
|
||||
"""
|
||||
LOG.debug("Building PXE config for deployment %s." % node.uuid)
|
||||
|
||||
# NOTE: we should strip '/' from the end because this is intended for
|
||||
# hardcoded ramdisk script
|
||||
ironic_api = (CONF.conductor.api_url or
|
||||
@ -173,61 +166,18 @@ def _build_pxe_config(node, pxe_info, ctx):
|
||||
node.save(ctx)
|
||||
|
||||
pxe_options = {
|
||||
'deployment_id': node['uuid'],
|
||||
'deployment_key': deploy_key,
|
||||
'deployment_iscsi_iqn': "iqn-%s" % node.uuid,
|
||||
'deployment_aki_path': pxe_info['deploy_kernel'][1],
|
||||
'deployment_ari_path': pxe_info['deploy_ramdisk'][1],
|
||||
'aki_path': pxe_info['kernel'][1],
|
||||
'ari_path': pxe_info['ramdisk'][1],
|
||||
'ironic_api_url': ironic_api,
|
||||
'pxe_append_params': CONF.pxe.pxe_append_params,
|
||||
}
|
||||
|
||||
tmpl_path, tmpl_file = os.path.split(CONF.pxe.pxe_config_template)
|
||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path))
|
||||
template = env.get_template(tmpl_file)
|
||||
return template.render({'pxe_options': pxe_options,
|
||||
'ROOT': '{{ ROOT }}'})
|
||||
|
||||
|
||||
def _get_node_vif_ids(task):
|
||||
"""Get all Neutron VIF ids for a node.
|
||||
This function does not handle multi node operations.
|
||||
|
||||
:param task: a TaskManager instance.
|
||||
:returns: A dict of the Node's port UUIDs and their associated VIFs
|
||||
|
||||
"""
|
||||
port_vifs = {}
|
||||
for port in task.ports:
|
||||
vif = port.extra.get('vif_port_id')
|
||||
if vif:
|
||||
port_vifs[port.uuid] = vif
|
||||
return port_vifs
|
||||
|
||||
|
||||
def _get_pxe_mac_path(mac):
|
||||
"""Convert a MAC address into a PXE config file name.
|
||||
|
||||
:param mac: A mac address string in the format xx:xx:xx:xx:xx:xx.
|
||||
:returns: the path to the config file.
|
||||
"""
|
||||
return os.path.join(
|
||||
CONF.pxe.tftp_root,
|
||||
'pxelinux.cfg',
|
||||
"01-" + mac.replace(":", "-").lower()
|
||||
)
|
||||
|
||||
|
||||
def _get_pxe_config_file_path(node_uuid):
|
||||
"""Generate the path for an instances PXE config file."""
|
||||
return os.path.join(CONF.pxe.tftp_root, node_uuid, 'config')
|
||||
|
||||
|
||||
def _get_pxe_bootfile_name():
|
||||
"""Returns the pxe_bootfile_name option."""
|
||||
return CONF.pxe.pxe_bootfile_name
|
||||
'deployment_id': node['uuid'],
|
||||
'deployment_key': deploy_key,
|
||||
'deployment_iscsi_iqn': "iqn-%s" % node.uuid,
|
||||
'deployment_aki_path': pxe_info['deploy_kernel'][1],
|
||||
'deployment_ari_path': pxe_info['deploy_ramdisk'][1],
|
||||
'aki_path': pxe_info['kernel'][1],
|
||||
'ari_path': pxe_info['ramdisk'][1],
|
||||
'ironic_api_url': ironic_api,
|
||||
'pxe_append_params': CONF.pxe.pxe_append_params,
|
||||
}
|
||||
return pxe_options
|
||||
|
||||
|
||||
def _get_image_dir_path(node_uuid):
|
||||
@ -242,7 +192,7 @@ def _get_image_file_path(node_uuid):
|
||||
|
||||
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)
|
||||
return os.path.join(CONF.tftp.tftp_root, 'token-' + node_uuid)
|
||||
|
||||
|
||||
class PXEImageCache(image_cache.ImageCache):
|
||||
@ -322,8 +272,8 @@ def _fetch_images(ctx, cache, images_info):
|
||||
def _cache_tftp_images(ctx, node, pxe_info):
|
||||
"""Fetch the necessary kernels and ramdisks for the instance."""
|
||||
fileutils.ensure_tree(
|
||||
os.path.join(CONF.pxe.tftp_root, node.uuid))
|
||||
LOG.debug("Fetching kernel and ramdisk for node %s" %
|
||||
os.path.join(CONF.tftp.tftp_root, node.uuid))
|
||||
LOG.debug("Fetching kernel and ramdisk for node %s",
|
||||
node.uuid)
|
||||
_fetch_images(ctx, TFTPImageCache(), pxe_info.values())
|
||||
|
||||
@ -336,7 +286,7 @@ def _cache_instance_image(ctx, node):
|
||||
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.
|
||||
are stored under CONF.tftp.tftp_root.
|
||||
|
||||
At present, the AMI is cached and certain files are injected.
|
||||
Debian/ubuntu-specific assumptions are made regarding the injected
|
||||
@ -373,7 +323,7 @@ def _get_tftp_image_info(node, ctx):
|
||||
for label in ('deploy_kernel', 'deploy_ramdisk'):
|
||||
image_info[label] = (
|
||||
str(d_info[label]).split('/')[-1],
|
||||
os.path.join(CONF.pxe.tftp_root, node.uuid, label)
|
||||
os.path.join(CONF.tftp.tftp_root, node.uuid, label)
|
||||
)
|
||||
|
||||
driver_info = node.driver_info
|
||||
@ -390,7 +340,7 @@ def _get_tftp_image_info(node, ctx):
|
||||
for label in labels:
|
||||
image_info[label] = (
|
||||
driver_info['pxe_' + label],
|
||||
os.path.join(CONF.pxe.tftp_root, node.uuid, label)
|
||||
os.path.join(CONF.tftp.tftp_root, node.uuid, label)
|
||||
)
|
||||
|
||||
return image_info
|
||||
@ -429,69 +379,6 @@ def _remove_internal_attrs(task):
|
||||
task.node.save(task.context)
|
||||
|
||||
|
||||
def _dhcp_options_for_instance():
|
||||
"""Retrives the DHCP PXE boot options."""
|
||||
return [{'opt_name': 'bootfile-name',
|
||||
'opt_value': _get_pxe_bootfile_name()},
|
||||
{'opt_name': 'server-ip-address',
|
||||
'opt_value': CONF.pxe.tftp_server},
|
||||
{'opt_name': 'tftp-server',
|
||||
'opt_value': CONF.pxe.tftp_server}
|
||||
]
|
||||
|
||||
|
||||
def _update_neutron(task):
|
||||
"""Send or update the DHCP BOOT options to Neutron for this node."""
|
||||
options = _dhcp_options_for_instance()
|
||||
vifs = _get_node_vif_ids(task)
|
||||
if not vifs:
|
||||
LOG.warning(_("No VIFs found for node %(node)s when attempting to "
|
||||
"update Neutron DHCP BOOT options."),
|
||||
{'node': task.node.uuid})
|
||||
return
|
||||
|
||||
# TODO(deva): decouple instantiation of NeutronAPI from task.context.
|
||||
# Try to use the user's task.context.auth_token, but if it
|
||||
# is not present, fall back to a server-generated context.
|
||||
# We don't need to recreate this in every method call.
|
||||
api = neutron.NeutronAPI(task.context)
|
||||
failures = []
|
||||
for port_id, port_vif in vifs.iteritems():
|
||||
try:
|
||||
api.update_port_dhcp_opts(port_vif, options)
|
||||
except exception.FailedToUpdateDHCPOptOnPort:
|
||||
failures.append(port_id)
|
||||
|
||||
if failures:
|
||||
if len(failures) == len(vifs):
|
||||
raise exception.FailedToUpdateDHCPOptOnPort(_(
|
||||
"Failed to set DHCP BOOT options for any port on node %s.") %
|
||||
task.node.uuid)
|
||||
else:
|
||||
LOG.warning(_("Some errors were encountered when updating the "
|
||||
"DHCP BOOT options for node %(node)s on the "
|
||||
"following ports: %(ports)s."),
|
||||
{'node': task.node.uuid, 'ports': failures})
|
||||
|
||||
|
||||
def _create_pxe_config(task, pxe_info):
|
||||
"""Generate pxe configuration file and link mac ports to it for
|
||||
tftp booting.
|
||||
"""
|
||||
fileutils.ensure_tree(os.path.join(CONF.pxe.tftp_root,
|
||||
task.node.uuid))
|
||||
fileutils.ensure_tree(os.path.join(CONF.pxe.tftp_root,
|
||||
'pxelinux.cfg'))
|
||||
|
||||
pxe_config_file_path = _get_pxe_config_file_path(task.node.uuid)
|
||||
pxe_config = _build_pxe_config(task.node, pxe_info, task.context)
|
||||
utils.write_to_file(pxe_config_file_path, pxe_config)
|
||||
for port in driver_utils.get_node_mac_addresses(task):
|
||||
mac_path = _get_pxe_mac_path(port)
|
||||
utils.unlink_without_raise(mac_path)
|
||||
utils.create_link_without_raise(pxe_config_file_path, mac_path)
|
||||
|
||||
|
||||
def _check_image_size(task):
|
||||
"""Check if the requested image is larger than the root partition size."""
|
||||
driver_info = _parse_driver_info(task.node)
|
||||
@ -587,7 +474,7 @@ class PXEDeploy(base.DeployInterface):
|
||||
# TODO(yuriyz): more secure way needed for pass auth token
|
||||
# to deploy ramdisk
|
||||
_create_token_file(task)
|
||||
_update_neutron(task)
|
||||
neutron.update_neutron(task, CONF.pxe.pxe_bootfile_name)
|
||||
manager_utils.node_set_boot_device(task, 'pxe', persistent=True)
|
||||
manager_utils.node_power_action(task, states.REBOOT)
|
||||
|
||||
@ -619,7 +506,9 @@ class PXEDeploy(base.DeployInterface):
|
||||
"""
|
||||
# TODO(deva): optimize this if rerun on existing files
|
||||
pxe_info = _get_tftp_image_info(task.node, task.context)
|
||||
_create_pxe_config(task, pxe_info)
|
||||
pxe_options = _build_pxe_config_options(task.node, pxe_info,
|
||||
task.context)
|
||||
tftp.create_pxe_config(task, pxe_options, CONF.pxe.pxe_config_template)
|
||||
_cache_tftp_images(task.context, task.node, pxe_info)
|
||||
|
||||
def clean_up(self, task):
|
||||
@ -639,20 +528,13 @@ class PXEDeploy(base.DeployInterface):
|
||||
utils.unlink_without_raise(path)
|
||||
TFTPImageCache().clean_up()
|
||||
|
||||
utils.unlink_without_raise(_get_pxe_config_file_path(
|
||||
node.uuid))
|
||||
for port in driver_utils.get_node_mac_addresses(task):
|
||||
mac_path = _get_pxe_mac_path(port)
|
||||
utils.unlink_without_raise(mac_path)
|
||||
|
||||
utils.rmtree_without_raise(
|
||||
os.path.join(CONF.pxe.tftp_root, node.uuid))
|
||||
tftp.clean_up_pxe_config(task)
|
||||
|
||||
_destroy_images(d_info, node.uuid)
|
||||
_destroy_token_file(node)
|
||||
|
||||
def take_over(self, task):
|
||||
_update_neutron(task)
|
||||
neutron.update_neutron(task, CONF.pxe.pxe_bootfile_name)
|
||||
|
||||
|
||||
class VendorPassthru(base.VendorInterface):
|
||||
@ -671,8 +553,8 @@ class VendorPassthru(base.VendorInterface):
|
||||
'iqn': kwargs.get('iqn'),
|
||||
'lun': kwargs.get('lun', '1'),
|
||||
'image_path': _get_image_file_path(node.uuid),
|
||||
'pxe_config_path': _get_pxe_config_file_path(
|
||||
node.uuid),
|
||||
'pxe_config_path':
|
||||
tftp.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']),
|
||||
|
@ -30,6 +30,7 @@ from ironic.common import image_service
|
||||
from ironic.common import keystone
|
||||
from ironic.common import neutron
|
||||
from ironic.common import states
|
||||
from ironic.common import tftp
|
||||
from ironic.common import utils
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.conductor import utils as manager_utils
|
||||
@ -175,11 +176,6 @@ class PXEValidateParametersTestCase(base.TestCase):
|
||||
pxe._parse_driver_info,
|
||||
node)
|
||||
|
||||
def test__get_pxe_mac_path(self):
|
||||
mac = '00:11:22:33:44:55:66'
|
||||
self.assertEqual('/tftpboot/pxelinux.cfg/01-00-11-22-33-44-55-66',
|
||||
pxe._get_pxe_mac_path(mac))
|
||||
|
||||
|
||||
class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
||||
|
||||
@ -205,22 +201,22 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
||||
|
||||
expected_info = {'ramdisk':
|
||||
('instance_ramdisk_uuid',
|
||||
os.path.join(CONF.pxe.tftp_root,
|
||||
os.path.join(CONF.tftp.tftp_root,
|
||||
self.node.uuid,
|
||||
'ramdisk')),
|
||||
'kernel':
|
||||
('instance_kernel_uuid',
|
||||
os.path.join(CONF.pxe.tftp_root,
|
||||
os.path.join(CONF.tftp.tftp_root,
|
||||
self.node.uuid,
|
||||
'kernel')),
|
||||
'deploy_ramdisk':
|
||||
('deploy_ramdisk_uuid',
|
||||
os.path.join(CONF.pxe.tftp_root,
|
||||
os.path.join(CONF.tftp.tftp_root,
|
||||
self.node.uuid,
|
||||
'deploy_ramdisk')),
|
||||
'deploy_kernel':
|
||||
('deploy_kernel_uuid',
|
||||
os.path.join(CONF.pxe.tftp_root,
|
||||
os.path.join(CONF.tftp.tftp_root,
|
||||
self.node.uuid,
|
||||
'deploy_kernel'))}
|
||||
show_mock.return_value = properties
|
||||
@ -240,139 +236,62 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
||||
self.node.driver_info.get('pxe_ramdisk'))
|
||||
|
||||
@mock.patch.object(utils, 'random_alnum')
|
||||
def test__build_pxe_config(self, random_alnum_mock):
|
||||
@mock.patch.object(tftp, 'build_pxe_config')
|
||||
def test_build_pxe_config_options(self, build_pxe_mock, random_alnum_mock):
|
||||
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')
|
||||
|
||||
template = 'ironic/tests/drivers/pxe_config.template'
|
||||
pxe_config_template = open(template, 'r').read()
|
||||
pxe_template = 'pxe_config_template'
|
||||
self.config(pxe_config_template=pxe_template, group='pxe')
|
||||
|
||||
fake_key = '0123456789ABCDEFGHIJKLMNOPQRSTUV'
|
||||
random_alnum_mock.return_value = fake_key
|
||||
|
||||
expected_options = {
|
||||
'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',
|
||||
'deployment_ari_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7'
|
||||
u'f33c123/deploy_ramdisk',
|
||||
'pxe_append_params': 'test_param',
|
||||
'aki_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/'
|
||||
u'kernel',
|
||||
'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'
|
||||
}
|
||||
image_info = {'deploy_kernel': ('deploy_kernel',
|
||||
os.path.join(CONF.pxe.tftp_root,
|
||||
os.path.join(CONF.tftp.tftp_root,
|
||||
self.node.uuid,
|
||||
'deploy_kernel')),
|
||||
'deploy_ramdisk': ('deploy_ramdisk',
|
||||
os.path.join(CONF.pxe.tftp_root,
|
||||
os.path.join(CONF.tftp.tftp_root,
|
||||
self.node.uuid,
|
||||
'deploy_ramdisk')),
|
||||
'kernel': ('kernel_id',
|
||||
os.path.join(CONF.pxe.tftp_root,
|
||||
os.path.join(CONF.tftp.tftp_root,
|
||||
self.node.uuid,
|
||||
'kernel')),
|
||||
'ramdisk': ('ramdisk_id',
|
||||
os.path.join(CONF.pxe.tftp_root,
|
||||
os.path.join(CONF.tftp.tftp_root,
|
||||
self.node.uuid,
|
||||
'ramdisk'))
|
||||
}
|
||||
pxe_config = pxe._build_pxe_config(self.node,
|
||||
image_info,
|
||||
self.context)
|
||||
options = pxe._build_pxe_config_options(self.node,
|
||||
image_info,
|
||||
self.context)
|
||||
self.assertEqual(expected_options, options)
|
||||
|
||||
random_alnum_mock.assert_called_once_with(32)
|
||||
self.assertEqual(pxe_config_template, pxe_config)
|
||||
|
||||
# test that deploy_key saved
|
||||
db_node = self.dbapi.get_node_by_uuid(self.node.uuid)
|
||||
db_key = db_node['driver_info'].get('pxe_deploy_key')
|
||||
self.assertEqual(fake_key, db_key)
|
||||
|
||||
def test__get_node_vif_ids_no_ports(self):
|
||||
expected = {}
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = pxe._get_node_vif_ids(task)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test__get_node_vif_ids_one_port(self):
|
||||
port1 = self._create_test_port(node_id=self.node.id, id=6,
|
||||
address='aa:bb:cc',
|
||||
uuid=utils.generate_uuid(),
|
||||
extra={'vif_port_id': 'test-vif-A'})
|
||||
expected = {port1.uuid: 'test-vif-A'}
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = pxe._get_node_vif_ids(task)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test__get_node_vif_ids_two_ports(self):
|
||||
port1 = self._create_test_port(node_id=self.node.id, id=6,
|
||||
address='aa:bb:cc',
|
||||
uuid=utils.generate_uuid(),
|
||||
extra={'vif_port_id': 'test-vif-A'})
|
||||
port2 = self._create_test_port(node_id=self.node.id, id=7,
|
||||
address='dd:ee:ff',
|
||||
uuid=utils.generate_uuid(),
|
||||
extra={'vif_port_id': 'test-vif-B'})
|
||||
expected = {port1.uuid: 'test-vif-A', port2.uuid: 'test-vif-B'}
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = pxe._get_node_vif_ids(task)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@mock.patch.object(pxe, '_get_node_vif_ids')
|
||||
@mock.patch.object(neutron.NeutronAPI, 'update_port_dhcp_opts')
|
||||
def test__update_neutron(self, mock_updo, mock_gnvi):
|
||||
opts = pxe._dhcp_options_for_instance()
|
||||
mock_gnvi.return_value = {'port-uuid': 'vif-uuid'}
|
||||
with task_manager.acquire(self.context,
|
||||
self.node.uuid) as task:
|
||||
pxe._update_neutron(task)
|
||||
mock_updo.assert_called_once_with('vif-uuid', opts)
|
||||
|
||||
@mock.patch.object(pxe, '_get_node_vif_ids')
|
||||
@mock.patch.object(neutron.NeutronAPI, '__init__')
|
||||
def test__update_neutron_no_vif_data(self, mock_init, mock_gnvi):
|
||||
mock_gnvi.return_value = {}
|
||||
with task_manager.acquire(self.context,
|
||||
self.node.uuid) as task:
|
||||
pxe._update_neutron(task)
|
||||
self.assertFalse(mock_init.called)
|
||||
|
||||
@mock.patch.object(pxe, '_get_node_vif_ids')
|
||||
@mock.patch.object(neutron.NeutronAPI, 'update_port_dhcp_opts')
|
||||
def test__update_neutron_some_failures(self, mock_updo, mock_gnvi):
|
||||
# confirm update is called twice, one fails, but no exception raised
|
||||
mock_gnvi.return_value = {'p1': 'v1', 'p2': 'v2'}
|
||||
exc = exception.FailedToUpdateDHCPOptOnPort('fake exception')
|
||||
mock_updo.side_effect = [None, exc]
|
||||
with task_manager.acquire(self.context,
|
||||
self.node.uuid) as task:
|
||||
pxe._update_neutron(task)
|
||||
self.assertEqual(2, mock_updo.call_count)
|
||||
|
||||
@mock.patch.object(pxe, '_get_node_vif_ids')
|
||||
@mock.patch.object(neutron.NeutronAPI, 'update_port_dhcp_opts')
|
||||
def test__update_neutron_fails(self, mock_updo, mock_gnvi):
|
||||
# confirm update is called twice, both fail, and exception is raised
|
||||
mock_gnvi.return_value = {'p1': 'v1', 'p2': 'v2'}
|
||||
exc = exception.FailedToUpdateDHCPOptOnPort('fake exception')
|
||||
mock_updo.side_effect = [exc, exc]
|
||||
with task_manager.acquire(self.context,
|
||||
self.node.uuid) as task:
|
||||
self.assertRaises(exception.FailedToUpdateDHCPOptOnPort,
|
||||
pxe._update_neutron,
|
||||
task)
|
||||
self.assertEqual(2, mock_updo.call_count)
|
||||
|
||||
def test__dhcp_options_for_instance(self):
|
||||
self.config(pxe_bootfile_name='test_pxe_bootfile', group='pxe')
|
||||
self.config(tftp_server='192.0.2.1', group='pxe')
|
||||
expected_info = [{'opt_name': 'bootfile-name',
|
||||
'opt_value': 'test_pxe_bootfile'},
|
||||
{'opt_name': 'server-ip-address',
|
||||
'opt_value': '192.0.2.1'},
|
||||
{'opt_name': 'tftp-server',
|
||||
'opt_value': '192.0.2.1'}
|
||||
]
|
||||
self.assertEqual(expected_info, pxe._dhcp_options_for_instance())
|
||||
|
||||
def test__get_pxe_config_file_path(self):
|
||||
self.assertEqual(os.path.join(CONF.pxe.tftp_root,
|
||||
self.node.uuid,
|
||||
'config'),
|
||||
pxe._get_pxe_config_file_path(self.node.uuid))
|
||||
|
||||
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))
|
||||
@ -391,7 +310,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
||||
@mock.patch.object(pxe, '_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')
|
||||
self.config(tftp_root=temp_dir, group='tftp')
|
||||
self.config(tftp_master_path=os.path.join(temp_dir,
|
||||
'tftp_master_path'),
|
||||
group='pxe')
|
||||
@ -552,7 +471,7 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
self.context = context.get_admin_context()
|
||||
self.context.auth_token = '4562138218392831'
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.config(tftp_root=self.temp_dir, group='pxe')
|
||||
self.config(tftp_root=self.temp_dir, group='tftp')
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.config(images_path=self.temp_dir, group='pxe')
|
||||
mgr_utils.mock_the_extension_manager(driver="fake_pxe")
|
||||
@ -696,9 +615,12 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
|
||||
@mock.patch.object(pxe, '_get_tftp_image_info')
|
||||
@mock.patch.object(pxe, '_cache_tftp_images')
|
||||
@mock.patch.object(pxe, '_create_pxe_config')
|
||||
def test_prepare(self, mock_pxe_config, mock_cache_tftp_images,
|
||||
@mock.patch.object(pxe, '_build_pxe_config_options')
|
||||
@mock.patch.object(tftp, 'create_pxe_config')
|
||||
def test_prepare(self, mock_pxe_config,
|
||||
mock_build_pxe, mock_cache_tftp_images,
|
||||
mock_tftp_img_info):
|
||||
mock_build_pxe.return_value = None
|
||||
mock_tftp_img_info.return_value = None
|
||||
mock_pxe_config.return_value = None
|
||||
mock_cache_tftp_images.return_value = None
|
||||
@ -706,14 +628,15 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
task.driver.deploy.prepare(task)
|
||||
mock_tftp_img_info.assert_called_once_with(task.node,
|
||||
self.context)
|
||||
mock_pxe_config.assert_called_once_with(task, None)
|
||||
mock_pxe_config.assert_called_once_with(
|
||||
task, None, CONF.pxe.pxe_config_template)
|
||||
mock_cache_tftp_images.assert_called_once_with(self.context,
|
||||
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(pxe, '_update_neutron')
|
||||
@mock.patch.object(neutron, 'update_neutron')
|
||||
@mock.patch.object(manager_utils, 'node_power_action')
|
||||
@mock.patch.object(manager_utils, 'node_set_boot_device')
|
||||
def test_deploy(self, mock_node_set_boot, mock_node_power_action,
|
||||
@ -731,7 +654,8 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
self.context, task.node)
|
||||
mock_get_image_file_path.assert_called_once_with(task.node.uuid)
|
||||
mock_get_image_mb.assert_called_once_with(fake_img_path)
|
||||
mock_update_neutron.assert_called_once_with(task)
|
||||
mock_update_neutron.assert_called_once_with(
|
||||
task, CONF.pxe.pxe_bootfile_name)
|
||||
mock_node_set_boot.assert_called_once_with(task, 'pxe',
|
||||
persistent=True)
|
||||
mock_node_power_action.assert_called_once_with(task, states.REBOOT)
|
||||
@ -786,12 +710,13 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
self.assertNotIn('pxe_kernel', self.node.driver_info)
|
||||
self.assertNotIn('pxe_ramdisk', self.node.driver_info)
|
||||
|
||||
@mock.patch.object(pxe, '_update_neutron')
|
||||
@mock.patch.object(neutron, 'update_neutron')
|
||||
def test_take_over(self, update_neutron_mock):
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=True) as task:
|
||||
task.driver.deploy.take_over(task)
|
||||
update_neutron_mock.assert_called_once_with(task)
|
||||
update_neutron_mock.assert_called_once_with(
|
||||
task, CONF.pxe.pxe_bootfile_name)
|
||||
|
||||
@mock.patch.object(pxe, 'InstanceImageCache')
|
||||
def test_continue_deploy_good(self, mock_image_cache):
|
||||
@ -900,8 +825,8 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
@mock.patch.object(pxe, '_get_tftp_image_info')
|
||||
def clean_up_config(self, get_tftp_image_info_mock, master=None):
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
self.config(tftp_root=temp_dir, group='pxe')
|
||||
tftp_master_dir = os.path.join(CONF.pxe.tftp_root,
|
||||
self.config(tftp_root=temp_dir, group='tftp')
|
||||
tftp_master_dir = os.path.join(CONF.tftp.tftp_root,
|
||||
'tftp_master')
|
||||
self.config(tftp_master_path=tftp_master_dir, group='pxe')
|
||||
os.makedirs(tftp_master_dir)
|
||||
@ -921,16 +846,16 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
uuid='bb43dc0b-03f2-4d2e-ae87-c02d7f33cc53',
|
||||
node_id='123')))
|
||||
|
||||
d_kernel_path = os.path.join(CONF.pxe.tftp_root,
|
||||
d_kernel_path = os.path.join(CONF.tftp.tftp_root,
|
||||
self.node.uuid, 'deploy_kernel')
|
||||
image_info = {'deploy_kernel': ('deploy_kernel_uuid', d_kernel_path)}
|
||||
|
||||
get_tftp_image_info_mock.return_value = image_info
|
||||
|
||||
pxecfg_dir = os.path.join(CONF.pxe.tftp_root, 'pxelinux.cfg')
|
||||
pxecfg_dir = os.path.join(CONF.tftp.tftp_root, 'pxelinux.cfg')
|
||||
os.makedirs(pxecfg_dir)
|
||||
|
||||
instance_dir = os.path.join(CONF.pxe.tftp_root,
|
||||
instance_dir = os.path.join(CONF.tftp.tftp_root,
|
||||
self.node.uuid)
|
||||
image_dir = os.path.join(CONF.pxe.images_path, self.node.uuid)
|
||||
os.makedirs(instance_dir)
|
||||
@ -952,7 +877,7 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
os.link(master_deploy_kernel_path, deploy_kernel_path)
|
||||
os.link(master_instance_path, image_path)
|
||||
if master == 'in_use':
|
||||
deploy_kernel_link = os.path.join(CONF.pxe.tftp_root,
|
||||
deploy_kernel_link = os.path.join(CONF.tftp.tftp_root,
|
||||
'deploy_kernel_link')
|
||||
image_link = os.path.join(CONF.pxe.images_path,
|
||||
'image_link')
|
||||
|
@ -15,15 +15,22 @@
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
|
||||
from neutronclient.common import exceptions as neutron_client_exc
|
||||
from neutronclient.v2_0 import client
|
||||
from oslo.config import cfg
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import neutron
|
||||
from ironic.common import tftp
|
||||
from ironic.common import utils
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.db import api as dbapi
|
||||
from ironic.openstack.common import context
|
||||
from ironic.tests import base
|
||||
from ironic.tests.conductor import utils as mgr_utils
|
||||
from ironic.tests.db import utils as db_utils
|
||||
from ironic.tests.objects import utils as object_utils
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
@ -32,6 +39,8 @@ class TestNeutron(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestNeutron, self).setUp()
|
||||
mgr_utils.mock_the_extension_manager(driver='fake')
|
||||
self.config(enabled_drivers=['fake'])
|
||||
self.config(url='test-url',
|
||||
url_timeout=30,
|
||||
group='neutron')
|
||||
@ -42,6 +51,13 @@ class TestNeutron(base.TestCase):
|
||||
admin_password='test-admin-password',
|
||||
auth_uri='test-auth-uri',
|
||||
group='keystone_authtoken')
|
||||
self.dbapi = dbapi.get_instance()
|
||||
self.context = context.get_admin_context()
|
||||
self.node = object_utils.create_test_node(self.context)
|
||||
|
||||
def _create_test_port(self, **kwargs):
|
||||
p = db_utils.get_test_port(**kwargs)
|
||||
return self.dbapi.create_port(p)
|
||||
|
||||
def test_create_with_token(self):
|
||||
token = 'test-token-123'
|
||||
@ -143,3 +159,84 @@ class TestNeutron(base.TestCase):
|
||||
neutron_client_exc.NeutronClientException())
|
||||
self.assertRaises(exception.FailedToUpdateMacOnPort,
|
||||
api.update_port_address, port_id, address)
|
||||
|
||||
def test_get_node_vif_ids_no_ports(self):
|
||||
expected = {}
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = neutron.get_node_vif_ids(task)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test__get_node_vif_ids_one_port(self):
|
||||
port1 = self._create_test_port(node_id=self.node.id,
|
||||
id=6,
|
||||
address='aa:bb:cc',
|
||||
uuid=utils.generate_uuid(),
|
||||
extra={'vif_port_id': 'test-vif-A'},
|
||||
driver='fake')
|
||||
expected = {port1.uuid: 'test-vif-A'}
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = neutron.get_node_vif_ids(task)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test__get_node_vif_ids_two_ports(self):
|
||||
port1 = self._create_test_port(node_id=self.node.id,
|
||||
id=6,
|
||||
address='aa:bb:cc',
|
||||
uuid=utils.generate_uuid(),
|
||||
extra={'vif_port_id': 'test-vif-A'},
|
||||
driver='fake')
|
||||
port2 = self._create_test_port(node_id=self.node.id,
|
||||
id=7,
|
||||
address='dd:ee:ff',
|
||||
uuid=utils.generate_uuid(),
|
||||
extra={'vif_port_id': 'test-vif-B'},
|
||||
driver='fake')
|
||||
expected = {port1.uuid: 'test-vif-A', port2.uuid: 'test-vif-B'}
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = neutron.get_node_vif_ids(task)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@mock.patch('ironic.common.neutron.NeutronAPI.update_port_dhcp_opts')
|
||||
@mock.patch('ironic.common.neutron.get_node_vif_ids')
|
||||
def test_update_neutron(self, mock_gnvi, mock_updo):
|
||||
opts = tftp.dhcp_options_for_instance(CONF.pxe.pxe_bootfile_name)
|
||||
mock_gnvi.return_value = {'port-uuid': 'vif-uuid'}
|
||||
with task_manager.acquire(self.context,
|
||||
self.node.uuid) as task:
|
||||
neutron.update_neutron(task, self.node)
|
||||
mock_updo.assertCalleOnceWith('vif-uuid', opts)
|
||||
|
||||
@mock.patch('ironic.common.neutron.NeutronAPI.__init__')
|
||||
@mock.patch('ironic.common.neutron.get_node_vif_ids')
|
||||
def test_update_neutron_no_vif_data(self, mock_gnvi, mock_init):
|
||||
mock_gnvi.return_value = {}
|
||||
with task_manager.acquire(self.context,
|
||||
self.node.uuid) as task:
|
||||
neutron.update_neutron(task, self.node)
|
||||
mock_init.assert_not_called()
|
||||
|
||||
@mock.patch('ironic.common.neutron.NeutronAPI.update_port_dhcp_opts')
|
||||
@mock.patch('ironic.common.neutron.get_node_vif_ids')
|
||||
def test_update_neutron_some_failures(self, mock_gnvi, mock_updo):
|
||||
# confirm update is called twice, one fails, but no exception raised
|
||||
mock_gnvi.return_value = {'p1': 'v1', 'p2': 'v2'}
|
||||
exc = exception.FailedToUpdateDHCPOptOnPort('fake exception')
|
||||
mock_updo.side_effect = [None, exc]
|
||||
with task_manager.acquire(self.context,
|
||||
self.node.uuid) as task:
|
||||
neutron.update_neutron(task, self.node)
|
||||
self.assertEqual(2, mock_updo.call_count)
|
||||
|
||||
@mock.patch('ironic.common.neutron.NeutronAPI.update_port_dhcp_opts')
|
||||
@mock.patch('ironic.common.neutron.get_node_vif_ids')
|
||||
def test_update_neutron_fails(self, mock_gnvi, mock_updo):
|
||||
# confirm update is called twice, both fail, and exception is raised
|
||||
mock_gnvi.return_value = {'p1': 'v1', 'p2': 'v2'}
|
||||
exc = exception.FailedToUpdateDHCPOptOnPort('fake exception')
|
||||
mock_updo.side_effect = [exc, exc]
|
||||
with task_manager.acquire(self.context,
|
||||
self.node.uuid) as task:
|
||||
self.assertRaises(exception.FailedToUpdateDHCPOptOnPort,
|
||||
neutron.update_neutron,
|
||||
task, self.node)
|
||||
self.assertEqual(2, mock_updo.call_count)
|
||||
|
145
ironic/tests/test_tftp.py
Normal file
145
ironic/tests/test_tftp.py
Normal file
@ -0,0 +1,145 @@
|
||||
#
|
||||
# Copyright 2014 Rackspace, Inc
|
||||
# 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
|
||||
|
||||
import mock
|
||||
from oslo.config import cfg
|
||||
|
||||
from ironic.common import tftp
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.db import api as dbapi
|
||||
from ironic.openstack.common import context
|
||||
from ironic.tests.conductor import utils as mgr_utils
|
||||
from ironic.tests.db import base as db_base
|
||||
from ironic.tests.objects import utils as object_utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class TestNetworkUtils(db_base.DbTestCase):
|
||||
def setUp(self):
|
||||
super(TestNetworkUtils, self).setUp()
|
||||
mgr_utils.mock_the_extension_manager(driver="fake")
|
||||
self.dbapi = dbapi.get_instance()
|
||||
self.context = context.get_admin_context()
|
||||
self.pxe_options = {
|
||||
'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',
|
||||
'deployment_ari_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7'
|
||||
u'f33c123/deploy_ramdisk',
|
||||
'pxe_append_params': 'test_param',
|
||||
'aki_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/'
|
||||
u'kernel',
|
||||
'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'
|
||||
}
|
||||
self.node = object_utils.create_test_node(self.context)
|
||||
|
||||
def test_build_pxe_config(self):
|
||||
|
||||
rendered_template = tftp.build_pxe_config(
|
||||
self.node, self.pxe_options, CONF.pxe.pxe_config_template)
|
||||
|
||||
expected_template = open(
|
||||
'ironic/tests/drivers/pxe_config.template').read()
|
||||
|
||||
self.assertEqual(rendered_template, expected_template)
|
||||
|
||||
@mock.patch('ironic.common.utils.create_link_without_raise')
|
||||
@mock.patch('ironic.common.utils.unlink_without_raise')
|
||||
@mock.patch('ironic.drivers.utils.get_node_mac_addresses')
|
||||
def test__write_mac_pxe_configs(self, get_macs_mock, unlink_mock,
|
||||
create_link_mock):
|
||||
macs = [
|
||||
'00:11:22:33:44:55:66',
|
||||
'00:11:22:33:44:55:67'
|
||||
]
|
||||
get_macs_mock.return_value = macs
|
||||
create_link_calls = [
|
||||
mock.call(u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/config',
|
||||
'/tftpboot/pxelinux.cfg/01-00-11-22-33-44-55-66'),
|
||||
mock.call(u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/config',
|
||||
'/tftpboot/pxelinux.cfg/01-00-11-22-33-44-55-67')
|
||||
]
|
||||
unlink_calls = [
|
||||
mock.call('/tftpboot/pxelinux.cfg/01-00-11-22-33-44-55-66'),
|
||||
mock.call('/tftpboot/pxelinux.cfg/01-00-11-22-33-44-55-67')
|
||||
]
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
tftp._write_mac_pxe_configs(task)
|
||||
|
||||
unlink_mock.assert_has_calls(unlink_calls)
|
||||
create_link_mock.assert_has_calls(create_link_calls)
|
||||
|
||||
@mock.patch('ironic.common.utils.write_to_file')
|
||||
@mock.patch('ironic.common.tftp.build_pxe_config')
|
||||
@mock.patch('ironic.openstack.common.fileutils.ensure_tree')
|
||||
def test_create_pxe_config(self, ensure_tree_mock, build_mock,
|
||||
write_mock):
|
||||
build_mock.return_value = self.pxe_options
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
tftp.create_pxe_config(task, self.pxe_options,
|
||||
CONF.pxe.pxe_config_template)
|
||||
build_mock.assert_called_with(task.node, self.pxe_options,
|
||||
CONF.pxe.pxe_config_template)
|
||||
ensure_calls = [
|
||||
mock.call(os.path.join(CONF.tftp.tftp_root, self.node.uuid)),
|
||||
mock.call(os.path.join(CONF.tftp.tftp_root, 'pxelinux.cfg'))
|
||||
]
|
||||
ensure_tree_mock.has_calls(ensure_calls)
|
||||
|
||||
pxe_config_file_path = tftp.get_pxe_config_file_path(self.node.uuid)
|
||||
write_mock.assert_called_with(pxe_config_file_path, self.pxe_options)
|
||||
|
||||
@mock.patch('ironic.common.utils.rmtree_without_raise', autospec=True)
|
||||
@mock.patch('ironic.common.utils.unlink_without_raise', autospec=True)
|
||||
def test_clean_up_pxe_config(self, unlink_mock, rmtree_mock):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
tftp.clean_up_pxe_config(task)
|
||||
|
||||
unlink_mock.assert_called_once_with(
|
||||
tftp.get_pxe_config_file_path(self.node.uuid))
|
||||
rmtree_mock.assert_called_once_with(
|
||||
os.path.join(CONF.tftp.tftp_root, self.node.uuid))
|
||||
|
||||
def test_get_pxe_mac_path(self):
|
||||
mac = '00:11:22:33:44:55:66'
|
||||
self.assertEqual('/tftpboot/pxelinux.cfg/01-00-11-22-33-44-55-66',
|
||||
tftp.get_pxe_mac_path(mac))
|
||||
|
||||
def test_get_pxe_config_file_path(self):
|
||||
self.assertEqual(os.path.join(CONF.tftp.tftp_root,
|
||||
self.node.uuid,
|
||||
'config'),
|
||||
tftp.get_pxe_config_file_path(self.node.uuid))
|
||||
|
||||
def test_dhcp_options_for_instance(self):
|
||||
self.config(tftp_server='192.0.2.1', group='tftp')
|
||||
expected_info = [{'opt_name': 'bootfile-name',
|
||||
'opt_value': CONF.pxe.pxe_bootfile_name},
|
||||
{'opt_name': 'server-ip-address',
|
||||
'opt_value': '192.0.2.1'},
|
||||
{'opt_name': 'tftp-server',
|
||||
'opt_value': '192.0.2.1'}
|
||||
]
|
||||
self.assertEqual(expected_info, tftp.dhcp_options_for_instance(
|
||||
CONF.pxe.pxe_bootfile_name))
|
Loading…
x
Reference in New Issue
Block a user