Add iPXE support for Ironic

As the size of our deploy ramdisk would continue to increase (Ironic
Python Agent) we need a more reliable way to transfer such data via
the network without relying on TFTP. The problem with TFTP is that it's
unreliable and any transmission error will result consequently in boot
problems. This patch adds support for iPXE so that we have the ability
to transfer data through HTTP which is a reliable protocol.

New configuration options added to the 'pxe' group:

- ipxe_enabled: Whether iPXE is enabled or not
- ipxe_boot_script: The path to the main iPXE script file
- http_url: The HTTP server URL
- http_root: The HTTP root path

Two functions from pxe.py were renamed because they are not related only
to tftp anymore:

- _get_tftp_image_info renamed to _get_image_info

- _cache_tftp_images renamed to _cache_ramdisk_kernel (because that's
what this function is about, it fetchs the kernels and ramdisks associated
with the image, not the image itself)

Implements: blueprint ipxe-boot
Change-Id: I8dc7640a19374a9c4d687877ea6c0ff1ebc13979
This commit is contained in:
Lucas Alvares Gomes 2014-07-02 10:04:27 +01:00
parent 435c20bd01
commit d8a0cf5815
9 changed files with 332 additions and 73 deletions

View File

@ -1100,6 +1100,19 @@
# (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>
# Ironic compute node's HTTP root path. (string value)
#http_root=/httpboot
# Enable iPXE boot. (boolean value)
#ipxe_enabled=false
# The path to the main iPXE script file. (string value)
#ipxe_boot_script=$pybasedir/drivers/modules/boot.ipxe
[seamicro]

View File

@ -31,15 +31,23 @@ LOG = logging.getLogger(__name__)
PXE_CFG_DIR_NAME = 'pxelinux.cfg'
def get_root_dir():
"""Returns the directory where the config files and images will live."""
if CONF.pxe.ipxe_enabled:
return CONF.pxe.http_root
else:
return CONF.pxe.tftp_root
def _ensure_config_dirs_exist(node_uuid):
"""Ensure that the node's and PXE configuration directories exist.
:param node_uuid: the UUID of the node.
"""
tftp_root = CONF.pxe.tftp_root
fileutils.ensure_tree(os.path.join(tftp_root, node_uuid))
fileutils.ensure_tree(os.path.join(tftp_root, PXE_CFG_DIR_NAME))
root_dir = get_root_dir()
fileutils.ensure_tree(os.path.join(root_dir, node_uuid))
fileutils.ensure_tree(os.path.join(root_dir, PXE_CFG_DIR_NAME))
def _build_pxe_config(pxe_options, template):
@ -80,11 +88,12 @@ def _get_pxe_mac_path(mac):
:returns: the path to the config file.
"""
return os.path.join(
CONF.pxe.tftp_root,
PXE_CFG_DIR_NAME,
"01-" + mac.replace(":", "-").lower()
)
if CONF.pxe.ipxe_enabled:
mac_file_name = mac.replace(':', '').lower()
else:
mac_file_name = "01-" + mac.replace(":", "-").lower()
return os.path.join(get_root_dir(), PXE_CFG_DIR_NAME, mac_file_name)
def get_deploy_kr_info(node_uuid, driver_info):
@ -92,12 +101,13 @@ def get_deploy_kr_info(node_uuid, driver_info):
Note: driver_info should be validated outside of this method.
"""
root_dir = get_root_dir()
image_info = {}
for label in ('deploy_kernel', 'deploy_ramdisk'):
# the values for these keys will look like "glance://image-uuid"
image_info[label] = (
str(driver_info[label]).split('/')[-1],
os.path.join(CONF.pxe.tftp_root, node_uuid, label)
os.path.join(root_dir, node_uuid, label)
)
return image_info
@ -109,7 +119,7 @@ def get_pxe_config_file_path(node_uuid):
:returns: The path to the node's PXE configuration file.
"""
return os.path.join(CONF.pxe.tftp_root, node_uuid, 'config')
return os.path.join(get_root_dir(), node_uuid, 'config')
def create_pxe_config(task, pxe_options, template=None):
@ -152,16 +162,31 @@ def clean_up_pxe_config(task):
for mac in driver_utils.get_node_mac_addresses(task):
utils.unlink_without_raise(_get_pxe_mac_path(mac))
utils.rmtree_without_raise(os.path.join(CONF.pxe.tftp_root,
utils.rmtree_without_raise(os.path.join(get_root_dir(),
task.node.uuid))
def dhcp_options_for_instance():
"""Retrieves the DHCP PXE boot options."""
return [{'opt_name': 'bootfile-name',
'opt_value': CONF.pxe.pxe_bootfile_name},
{'opt_name': 'server-ip-address',
'opt_value': CONF.pxe.tftp_server},
{'opt_name': 'tftp-server',
'opt_value': CONF.pxe.tftp_server}
]
dhcp_opts = []
if CONF.pxe.ipxe_enabled:
script_name = os.path.basename(CONF.pxe.ipxe_boot_script)
ipxe_script_url = '/'.join([CONF.pxe.http_url, script_name])
# if the request comes from dumb firmware send them the iPXE
# boot image. !175 == non-iPXE.
# http://ipxe.org/howto/dhcpd#ipxe-specific_options
dhcp_opts.append({'opt_name': '!175,bootfile-name',
'opt_value': CONF.pxe.pxe_bootfile_name})
# If the request comes from iPXE, direct it to boot from the
# iPXE script
dhcp_opts.append({'opt_name': 'bootfile-name',
'opt_value': ipxe_script_url})
else:
dhcp_opts.append({'opt_name': 'bootfile-name',
'opt_value': CONF.pxe.pxe_bootfile_name})
dhcp_opts.append({'opt_name': 'server-ip-address',
'opt_value': CONF.pxe.tftp_server})
dhcp_opts.append({'opt_name': 'tftp-server',
'opt_value': CONF.pxe.tftp_server})
return dhcp_opts

View File

@ -0,0 +1,10 @@
#!ipxe
# load the MAC-specific file or fail if it's not found
chain --autofree pxelinux.cfg/${mac:hexraw} || goto error_no_config
:error_no_config
echo PXE boot failed. No configuration found for MAC ${mac}
echo Press any key to reboot...
prompt --timeout 180
reboot

View File

@ -20,6 +20,7 @@ import socket
import stat
import time
from oslo.config import cfg
from oslo.utils import excutils
from ironic.common import disk_partitioner
@ -31,6 +32,8 @@ from ironic.openstack.common import processutils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
# All functions are called from deploy() directly or indirectly.
# They are split for stub-out.
@ -166,12 +169,14 @@ def switch_pxe_config(path, root_uuid):
with open(path) as f:
lines = f.readlines()
root = 'UUID=%s' % root_uuid
pxe_cmd = 'goto' if CONF.pxe.ipxe_enabled else 'default'
rre = re.compile(r'\{\{ ROOT \}\}')
dre = re.compile('^default .*$')
dre = re.compile('^%s .*$' % pxe_cmd)
boot_line = '%s boot' % pxe_cmd
with open(path, 'w') as f:
for line in lines:
line = rre.sub(root, line)
line = dre.sub('default boot', line)
line = dre.sub(boot_line, line)
f.write(line)

View File

@ -0,0 +1,15 @@
#!ipxe
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 {{ pxe_options.pxe_append_params|default("", true) }} ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac}
initrd {{ pxe_options.deployment_ari_path }}
boot
:boot
kernel {{ pxe_options.aki_path }} root={{ ROOT }} ro {{ pxe_options.pxe_append_params|default("", true) }}
initrd {{ pxe_options.ari_path }}
boot

View File

@ -18,6 +18,7 @@ PXE Driver and supporting meta-classes.
"""
import os
import shutil
from oslo.config import cfg
from oslo.utils import strutils
@ -84,6 +85,19 @@ pxe_opts = [
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'),
cfg.StrOpt('http_root',
default='/httpboot',
help='Ironic compute node\'s HTTP root path.'),
cfg.BoolOpt('ipxe_enabled',
default=False,
help='Enable iPXE boot.'),
cfg.StrOpt('ipxe_boot_script',
default=paths.basedir_def(
'drivers/modules/boot.ipxe'),
help='The path to the main iPXE script file.'),
]
LOG = logging.getLogger(__name__)
@ -223,14 +237,27 @@ def _build_pxe_config_options(node, pxe_info, ctx):
node.instance_info = i_info
node.save(ctx)
if CONF.pxe.ipxe_enabled:
deploy_kernel = '/'.join([CONF.pxe.http_url, node.uuid,
'deploy_kernel'])
deploy_ramdisk = '/'.join([CONF.pxe.http_url, node.uuid,
'deploy_ramdisk'])
kernel = '/'.join([CONF.pxe.http_url, node.uuid, 'kernel'])
ramdisk = '/'.join([CONF.pxe.http_url, node.uuid, 'ramdisk'])
else:
deploy_kernel = pxe_info['deploy_kernel'][1]
deploy_ramdisk = pxe_info['deploy_ramdisk'][1]
kernel = pxe_info['kernel'][1]
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': 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],
'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,
}
@ -328,10 +355,10 @@ def _fetch_images(ctx, cache, images_info):
cache.fetch_image(uuid, path, ctx=ctx)
def _cache_tftp_images(ctx, node, pxe_info):
def _cache_ramdisk_kernel(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))
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())
@ -367,7 +394,7 @@ def _cache_instance_image(ctx, node):
return (uuid, image_path)
def _get_tftp_image_info(node, ctx):
def _get_image_info(node, ctx):
"""Generate the paths for tftp files for this instance
Raises IronicException if
@ -378,6 +405,7 @@ def _get_tftp_image_info(node, ctx):
"""
d_info = _parse_deploy_info(node)
image_info = {}
root_dir = pxe_utils.get_root_dir()
image_info.update(pxe_utils.get_deploy_kr_info(node.uuid, d_info))
@ -394,7 +422,7 @@ def _get_tftp_image_info(node, ctx):
for label in labels:
image_info[label] = (
i_info[label],
os.path.join(CONF.pxe.tftp_root, node.uuid, label)
os.path.join(root_dir, node.uuid, label)
)
return image_info
@ -489,6 +517,12 @@ class PXEDeploy(base.DeployInterface):
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.InvalidParameterValue(_(
"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
@ -552,12 +586,17 @@ class PXEDeploy(base.DeployInterface):
:param task: a TaskManager instance containing the node to act on.
"""
# TODO(deva): optimize this if rerun on existing files
pxe_info = _get_tftp_image_info(task.node, task.context)
if CONF.pxe.ipxe_enabled:
# Copy the iPXE boot script to HTTP root directory
bootfile_path = os.path.join(CONF.pxe.http_root,
os.path.basename(CONF.pxe.ipxe_boot_script))
shutil.copyfile(CONF.pxe.ipxe_boot_script, bootfile_path)
pxe_info = _get_image_info(task.node, task.context)
pxe_options = _build_pxe_config_options(task.node, pxe_info,
task.context)
pxe_utils.create_pxe_config(task, pxe_options,
CONF.pxe.pxe_config_template)
_cache_tftp_images(task.context, task.node, pxe_info)
_cache_ramdisk_kernel(task.context, task.node, pxe_info)
def clean_up(self, task):
"""Clean up the deployment environment for the task's node.
@ -569,7 +608,7 @@ class PXEDeploy(base.DeployInterface):
:param task: a TaskManager instance containing the node to act on.
"""
node = task.node
pxe_info = _get_tftp_image_info(node, task.context)
pxe_info = _get_image_info(node, task.context)
d_info = _parse_deploy_info(node)
for label in pxe_info:
path = pxe_info[label][1]

View File

@ -20,6 +20,8 @@ import mock
import os
import tempfile
from oslo.config import cfg
from ironic.common import disk_partitioner
from ironic.common import exception
from ironic.common import utils as common_utils
@ -53,6 +55,42 @@ kernel kernel
append initrd=ramdisk root=UUID=12345678-1234-1234-1234-1234567890abcdef
"""
_IPXECONF_DEPLOY = """
#!ipxe
dhcp
goto deploy
:deploy
kernel deploy_kernel
initrd deploy_ramdisk
boot
:boot
kernel kernel
append initrd=ramdisk root={{ ROOT }}
boot
"""
_IPXECONF_BOOT = """
#!ipxe
dhcp
goto boot
:deploy
kernel deploy_kernel
initrd deploy_ramdisk
boot
:boot
kernel kernel
append initrd=ramdisk root=UUID=12345678-1234-1234-1234-1234567890abcdef
boot
"""
class PhysicalWorkTestCase(tests_base.TestCase):
def setUp(self):
@ -363,20 +401,32 @@ class PhysicalWorkTestCase(tests_base.TestCase):
class SwitchPxeConfigTestCase(tests_base.TestCase):
def setUp(self):
super(SwitchPxeConfigTestCase, self).setUp()
(fd, self.fname) = tempfile.mkstemp()
os.write(fd, _PXECONF_DEPLOY)
def _create_config(self, ipxe=False):
(fd, fname) = tempfile.mkstemp()
pxe_cfg = _IPXECONF_DEPLOY if ipxe else _PXECONF_DEPLOY
os.write(fd, pxe_cfg)
os.close(fd)
self.addCleanup(os.unlink, self.fname)
self.addCleanup(os.unlink, fname)
return fname
def test_switch_pxe_config(self):
utils.switch_pxe_config(self.fname,
fname = self._create_config()
utils.switch_pxe_config(fname,
'12345678-1234-1234-1234-1234567890abcdef')
with open(self.fname, 'r') as f:
with open(fname, 'r') as f:
pxeconf = f.read()
self.assertEqual(_PXECONF_BOOT, pxeconf)
def test_switch_ipxe_config(self):
cfg.CONF.set_override('ipxe_enabled', True, 'pxe')
fname = self._create_config(ipxe=True)
utils.switch_pxe_config(fname,
'12345678-1234-1234-1234-1234567890abcdef')
with open(fname, 'r') as f:
pxeconf = f.read()
self.assertEqual(_IPXECONF_BOOT, pxeconf)
class OtherFunctionTestCase(tests_base.TestCase):
def test_get_dev(self):

View File

@ -217,7 +217,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
return self.dbapi.create_port(p)
@mock.patch.object(base_image_service.BaseImageService, '_show')
def test__get_tftp_image_info(self, show_mock):
def test__get_image_info(self, show_mock):
properties = {'properties': {u'kernel_id': u'instance_kernel_uuid',
u'ramdisk_id': u'instance_ramdisk_uuid'}}
@ -242,14 +242,14 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
self.node.uuid,
'deploy_kernel'))}
show_mock.return_value = properties
image_info = pxe._get_tftp_image_info(self.node, self.context)
image_info = pxe._get_image_info(self.node, self.context)
show_mock.assert_called_once_with('glance://image_uuid',
method='get')
self.assertEqual(expected_info, image_info)
# test with saved info
show_mock.reset_mock()
image_info = pxe._get_tftp_image_info(self.node, self.context)
image_info = pxe._get_image_info(self.node, self.context)
self.assertEqual(expected_info, image_info)
self.assertFalse(show_mock.called)
self.assertEqual('instance_kernel_uuid',
@ -259,7 +259,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
@mock.patch.object(utils, 'random_alnum')
@mock.patch.object(pxe_utils, '_build_pxe_config')
def test_build_pxe_config_options(self, build_pxe_mock, random_alnum_mock):
def _test_build_pxe_config_options(self, build_pxe_mock, random_alnum_mock,
ipxe_enabled=False):
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')
@ -269,36 +270,56 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
fake_key = '0123456789ABCDEFGHIJKLMNOPQRSTUV'
random_alnum_mock.return_value = fake_key
if ipxe_enabled:
http_url = 'http://192.1.2.3:1234'
self.config(ipxe_enabled=True, group='pxe')
self.config(http_url=http_url, group='pxe')
deploy_kernel = os.path.join(http_url, self.node.uuid,
'deploy_kernel')
deploy_ramdisk = os.path.join(http_url, self.node.uuid,
'deploy_ramdisk')
kernel = os.path.join(http_url, self.node.uuid, 'kernel')
ramdisk = os.path.join(http_url, self.node.uuid, 'ramdisk')
root_dir = CONF.pxe.http_root
else:
deploy_kernel = os.path.join(CONF.pxe.tftp_root, self.node.uuid,
'deploy_kernel')
deploy_ramdisk = os.path.join(CONF.pxe.tftp_root, self.node.uuid,
'deploy_ramdisk')
kernel = os.path.join(CONF.pxe.tftp_root, self.node.uuid,
'kernel')
ramdisk = os.path.join(CONF.pxe.tftp_root, self.node.uuid,
'ramdisk')
root_dir = CONF.pxe.tftp_root
expected_options = {
'deployment_key': '0123456789ABCDEFGHIJKLMNOPQRSTUV',
'ari_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/'
u'ramdisk',
'ari_path': 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',
'deployment_ari_path': deploy_ramdisk,
'pxe_append_params': 'test_param',
'aki_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/'
u'kernel',
'aki_path': 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'
'deployment_aki_path': deploy_kernel,
}
image_info = {'deploy_kernel': ('deploy_kernel',
os.path.join(CONF.pxe.tftp_root,
os.path.join(root_dir,
self.node.uuid,
'deploy_kernel')),
'deploy_ramdisk': ('deploy_ramdisk',
os.path.join(CONF.pxe.tftp_root,
os.path.join(root_dir,
self.node.uuid,
'deploy_ramdisk')),
'kernel': ('kernel_id',
os.path.join(CONF.pxe.tftp_root,
os.path.join(root_dir,
self.node.uuid,
'kernel')),
'ramdisk': ('ramdisk_id',
os.path.join(CONF.pxe.tftp_root,
os.path.join(root_dir,
self.node.uuid,
'ramdisk'))
}
@ -314,6 +335,12 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
db_key = db_node.instance_info.get('deploy_key')
self.assertEqual(fake_key, db_key)
def test__build_pxe_config_options(self):
self._test_build_pxe_config_options(ipxe_enabled=False)
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))
@ -341,7 +368,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
image_info = {'deploy_kernel': ('deploy_kernel', image_path)}
fileutils.ensure_tree(CONF.pxe.tftp_master_path)
pxe._cache_tftp_images(None, self.node, image_info)
pxe._cache_ramdisk_kernel(None, self.node, image_info)
mock_fetch_image.assert_called_once_with(None,
mock.ANY,
@ -368,6 +395,33 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
'disk'),
image_path)
@mock.patch.object(pxe, 'TFTPImageCache', lambda: None)
@mock.patch.object(fileutils, 'ensure_tree')
@mock.patch.object(pxe, '_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'}
expected_path = os.path.join(CONF.pxe.tftp_root, self.node.uuid)
pxe._cache_ramdisk_kernel(self.context, self.node, fake_pxe_info)
mock_ensure_tree.assert_called_with(expected_path)
mock_fetch_image.assert_called_once_with(self.context, mock.ANY,
fake_pxe_info.values())
@mock.patch.object(pxe, 'TFTPImageCache', lambda: None)
@mock.patch.object(fileutils, 'ensure_tree')
@mock.patch.object(pxe, '_fetch_images')
def test__cache_ramdisk_kernel_ipxe(self, mock_fetch_image,
mock_ensure_tree):
self.config(ipxe_enabled=True, group='pxe')
fake_pxe_info = {'foo': 'bar'}
expected_path = os.path.join(CONF.pxe.http_root, self.node.uuid)
pxe._cache_ramdisk_kernel(self.context, self.node, fake_pxe_info)
mock_ensure_tree.assert_called_with(expected_path)
mock_fetch_image.assert_called_once_with(self.context, mock.ANY,
fake_pxe_info.values())
@mock.patch.object(pxe, 'TFTPImageCache')
@mock.patch.object(pxe, 'InstanceImageCache')
@ -649,25 +703,25 @@ class PXEDriverTestCase(db_base.DbTestCase):
address='123456', iqn='aaa-bbb',
key='fake-12345')
@mock.patch.object(pxe, '_get_tftp_image_info')
@mock.patch.object(pxe, '_cache_tftp_images')
@mock.patch.object(pxe, '_get_image_info')
@mock.patch.object(pxe, '_cache_ramdisk_kernel')
@mock.patch.object(pxe, '_build_pxe_config_options')
@mock.patch.object(pxe_utils, 'create_pxe_config')
def test_prepare(self, mock_pxe_config,
mock_build_pxe, mock_cache_tftp_images,
mock_tftp_img_info):
mock_build_pxe, mock_cache_r_k,
mock_img_info):
mock_build_pxe.return_value = None
mock_tftp_img_info.return_value = None
mock_img_info.return_value = None
mock_pxe_config.return_value = None
mock_cache_tftp_images.return_value = None
mock_cache_r_k.return_value = None
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.deploy.prepare(task)
mock_tftp_img_info.assert_called_once_with(task.node,
self.context)
mock_img_info.assert_called_once_with(task.node,
self.context)
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_cache_r_k.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')
@ -842,8 +896,8 @@ class PXEDriverTestCase(db_base.DbTestCase):
self.assertEqual(1, _continue_deploy_mock.call_count,
"_continue_deploy was not called once.")
@mock.patch.object(pxe, '_get_tftp_image_info')
def clean_up_config(self, get_tftp_image_info_mock, master=None):
@mock.patch.object(pxe, '_get_image_info')
def clean_up_config(self, get_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,
@ -870,7 +924,7 @@ class PXEDriverTestCase(db_base.DbTestCase):
self.node.uuid, 'deploy_kernel')
image_info = {'deploy_kernel': ('deploy_kernel_uuid', d_kernel_path)}
get_tftp_image_info_mock.return_value = image_info
get_image_info_mock.return_value = image_info
pxecfg_dir = os.path.join(CONF.pxe.tftp_root, 'pxelinux.cfg')
os.makedirs(pxecfg_dir)
@ -913,7 +967,7 @@ class PXEDriverTestCase(db_base.DbTestCase):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.driver.deploy.clean_up(task)
get_tftp_image_info_mock.called_once_with(task.node)
get_image_info_mock.called_once_with(task.node)
assert_false_path = [config_path, deploy_kernel_path, image_path,
pxe_mac_path, image_dir, instance_dir,
token_path]

View File

@ -133,6 +133,25 @@ class TestPXEUtils(db_base.DbTestCase):
self.assertEqual('/tftpboot/pxelinux.cfg/01-00-11-22-33-44-55-66',
pxe_utils._get_pxe_mac_path(mac))
def test__get_pxe_mac_path_ipxe(self):
self.config(ipxe_enabled=True, group='pxe')
self.config(http_root='/httpboot', group='pxe')
mac = '00:11:22:33:AA:BB:CC'
self.assertEqual('/httpboot/pxelinux.cfg/00112233aabbcc',
pxe_utils._get_pxe_mac_path(mac))
def test_get_root_dir(self):
expected_dir = '/tftproot'
self.config(ipxe_enabled=False, group='pxe')
self.config(tftp_root=expected_dir, group='pxe')
self.assertEqual(expected_dir, pxe_utils.get_root_dir())
def test_get_root_dir_ipxe(self):
expected_dir = '/httpboot'
self.config(ipxe_enabled=True, group='pxe')
self.config(http_root=expected_dir, group='pxe')
self.assertEqual(expected_dir, pxe_utils.get_root_dir())
def test_get_pxe_config_file_path(self):
self.assertEqual(os.path.join(CONF.pxe.tftp_root,
self.node.uuid,
@ -151,8 +170,7 @@ class TestPXEUtils(db_base.DbTestCase):
]
self.assertEqual(expected_info, pxe_utils.dhcp_options_for_instance())
def test_get_deploy_kr_info(self):
self.config(tftp_root='/tftp', group='pxe')
def _test_get_deploy_kr_info(self, expected_dir):
node_uuid = 'fake-node'
driver_info = {
'deploy_kernel': 'glance://deploy-kernel',
@ -161,14 +179,25 @@ class TestPXEUtils(db_base.DbTestCase):
expected = {
'deploy_kernel': ('deploy-kernel',
'/tftp/fake-node/deploy_kernel'),
expected_dir + '/fake-node/deploy_kernel'),
'deploy_ramdisk': ('deploy-ramdisk',
'/tftp/fake-node/deploy_ramdisk'),
expected_dir + '/fake-node/deploy_ramdisk'),
}
kr_info = pxe_utils.get_deploy_kr_info(node_uuid, driver_info)
self.assertEqual(expected, kr_info)
def test_get_deploy_kr_info(self):
expected_dir = '/tftp'
self.config(tftp_root=expected_dir, group='pxe')
self._test_get_deploy_kr_info(expected_dir)
def test_get_deploy_kr_info_ipxe(self):
expected_dir = '/http'
self.config(ipxe_enabled=True, group='pxe')
self.config(http_root=expected_dir, group='pxe')
self._test_get_deploy_kr_info(expected_dir)
def test_get_deploy_kr_info_bad_driver_info(self):
self.config(tftp_root='/tftp', group='pxe')
node_uuid = 'fake-node'
@ -177,3 +206,22 @@ class TestPXEUtils(db_base.DbTestCase):
pxe_utils.get_deploy_kr_info,
node_uuid,
driver_info)
def test_dhcp_options_for_instance_ipxe(self):
self.config(tftp_server='192.0.2.1', group='pxe')
self.config(pxe_bootfile_name='fake-bootfile', group='pxe')
self.config(ipxe_enabled=True, group='pxe')
self.config(http_url='http://192.0.3.2:1234', group='pxe')
self.config(ipxe_boot_script='/test/boot.ipxe', group='pxe')
expected_boot_script_url = 'http://192.0.3.2:1234/boot.ipxe'
expected_info = [{'opt_name': '!175,bootfile-name',
'opt_value': 'fake-bootfile'},
{'opt_name': 'server-ip-address',
'opt_value': '192.0.2.1'},
{'opt_name': 'tftp-server',
'opt_value': '192.0.2.1'},
{'opt_name': 'bootfile-name',
'opt_value': expected_boot_script_url}]
self.assertEqual(sorted(expected_info),
sorted(pxe_utils.dhcp_options_for_instance()))