Merge "Factoring out PXE and TFTP functions"

This commit is contained in:
Jenkins 2014-06-16 19:49:20 +00:00 committed by Gerrit Code Review
commit ebca58dc00
7 changed files with 526 additions and 291 deletions

View File

@ -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

View File

@ -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
View 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}
]

View File

@ -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']),

View File

@ -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')

View File

@ -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
View 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))