diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index 1a8122e291..823795e747 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -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= + +# 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] diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index 19685b8f08..a8c4b14e6a 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -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 diff --git a/ironic/drivers/modules/boot.ipxe b/ironic/drivers/modules/boot.ipxe new file mode 100644 index 0000000000..25a0ea8dc2 --- /dev/null +++ b/ironic/drivers/modules/boot.ipxe @@ -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 diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index b63ea9a615..0de005dfcd 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -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) diff --git a/ironic/drivers/modules/ipxe_config.template b/ironic/drivers/modules/ipxe_config.template new file mode 100644 index 0000000000..bcec3e5e4e --- /dev/null +++ b/ironic/drivers/modules/ipxe_config.template @@ -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 diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index 9322bfea01..ad22e7f286 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -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] diff --git a/ironic/tests/drivers/test_deploy_utils.py b/ironic/tests/drivers/test_deploy_utils.py index 062c26e0b1..559fbb381c 100644 --- a/ironic/tests/drivers/test_deploy_utils.py +++ b/ironic/tests/drivers/test_deploy_utils.py @@ -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): diff --git a/ironic/tests/drivers/test_pxe.py b/ironic/tests/drivers/test_pxe.py index f870eca7da..6f8a060d4f 100644 --- a/ironic/tests/drivers/test_pxe.py +++ b/ironic/tests/drivers/test_pxe.py @@ -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] diff --git a/ironic/tests/test_pxe_utils.py b/ironic/tests/test_pxe_utils.py index f8904a8d03..94cb43dcea 100644 --- a/ironic/tests/test_pxe_utils.py +++ b/ironic/tests/test_pxe_utils.py @@ -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()))