diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index 4eefef24ab..92efd90777 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -17,7 +17,6 @@ import os from ironic_lib import utils as ironic_utils -import jinja2 from oslo_config import cfg from oslo_log import log as logging from oslo_utils import fileutils @@ -54,28 +53,6 @@ def _ensure_config_dirs_exist(node_uuid): fileutils.ensure_tree(os.path.join(root_dir, PXE_CFG_DIR_NAME)) -def _build_pxe_config(pxe_options, template, root_tag, disk_ident_tag): - """Build the PXE boot configuration file. - - This method builds the PXE boot configuration file by rendering the - template with the given parameters. - - :param pxe_options: A dict of values to set on the configuration file. - :param template: The PXE configuration template. - :param root_tag: Root tag used in the PXE config file. - :param disk_ident_tag: Disk identifier tag used in the PXE config file. - :returns: A formatted string with the file content. - - """ - tmpl_path, tmpl_file = os.path.split(template) - env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path)) - template = env.get_template(tmpl_file) - return template.render({'pxe_options': pxe_options, - 'ROOT': root_tag, - 'DISK_IDENTIFIER': disk_ident_tag, - }) - - def _link_mac_pxe_configs(task): """Link each MAC address with the PXE configuration file. @@ -237,8 +214,11 @@ def create_pxe_config(task, pxe_options, template=None): pxe_config_root_tag = '{{ ROOT }}' pxe_config_disk_ident = '{{ DISK_IDENTIFIER }}' - pxe_config = _build_pxe_config(pxe_options, template, pxe_config_root_tag, - pxe_config_disk_ident) + params = {'pxe_options': pxe_options, + 'ROOT': pxe_config_root_tag, + 'DISK_IDENTIFIER': pxe_config_disk_ident} + + pxe_config = utils.render_template(template, params) utils.write_to_file(pxe_config_file_path, pxe_config) if is_uefi_boot_mode and not CONF.pxe.ipxe_enabled: diff --git a/ironic/common/utils.py b/ironic/common/utils.py index be5a8a1e31..cbd9b1ae0f 100644 --- a/ironic/common/utils.py +++ b/ironic/common/utils.py @@ -27,6 +27,7 @@ import re import shutil import tempfile +import jinja2 from oslo_concurrency import processutils from oslo_log import log as logging from oslo_utils import netutils @@ -283,6 +284,22 @@ def hash_file(file_like_object, hash_algo='md5'): return checksum.hexdigest() +def file_has_content(path, content, hash_algo='md5'): + """Checks that content of the file is the same as provided reference. + + :param path: path to file + :param content: reference content to check against + :param hash_algo: hashing algo from hashlib to use, default is 'md5' + :returns: True if the hash of reference content is the same as + the hash of file's content, False otherwise + """ + with open(path) as existing: + file_hash_hex = hash_file(existing, hash_algo=hash_algo) + ref_hash = _get_hash_object(hash_algo) + ref_hash.update(content) + return file_hash_hex == ref_hash.hexdigest() + + @contextlib.contextmanager def tempdir(**kwargs): tempfile.tempdir = CONF.tempdir @@ -500,3 +517,22 @@ def validate_network_port(port, port_name="Port"): 'numbers must be between 1 and 65535.') % {'port_name': port_name, 'port': port}) return port + + +def render_template(template, params, is_file=True): + """Renders Jinja2 template file with given parameters. + + :param template: full path to the Jinja2 template file + :param params: dictionary with parameters to use when rendering + :param is_file: whether template is file or string with template itself + :returns: the rendered template as a string + """ + if is_file: + tmpl_path, tmpl_name = os.path.split(template) + loader = jinja2.FileSystemLoader(tmpl_path) + else: + tmpl_name = 'template' + loader = jinja2.DictLoader({tmpl_name: template}) + env = jinja2.Environment(loader=loader) + tmpl = env.get_template(tmpl_name) + return tmpl.render(params) diff --git a/ironic/drivers/modules/boot.ipxe b/ironic/drivers/modules/boot.ipxe index aa8ee9e511..4739695bab 100644 --- a/ironic/drivers/modules/boot.ipxe +++ b/ironic/drivers/modules/boot.ipxe @@ -5,10 +5,10 @@ # https://bugs.launchpad.net/ironic/+bug/1504482 set netid:int32 -1 :loop -inc netid || chain pxelinux.cfg/${mac:hexhyp} || goto old_rom +inc netid || chain {{ ipxe_for_mac_uri }}${mac:hexhyp} || goto old_rom isset ${net${netid}/mac} || goto loop_done echo Attempting to boot from MAC ${net${netid}/mac:hexhyp} -chain pxelinux.cfg/${net${netid}/mac:hexhyp} || goto loop +chain {{ ipxe_for_mac_uri }}${net${netid}/mac:hexhyp} || goto loop :loop_done echo PXE boot failed! No configuration found for any of the present NICs. diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index 77ab5190f6..659de2d3ab 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -15,9 +15,7 @@ PXE Boot Interface """ -import filecmp import os -import shutil from ironic_lib import metrics_utils from ironic_lib import utils as ironic_utils @@ -33,6 +31,7 @@ from ironic.common import image_service as service from ironic.common import images from ironic.common import pxe_utils from ironic.common import states +from ironic.common import utils from ironic.conf import CONF from ironic.drivers import base from ironic.drivers.modules import deploy_utils @@ -382,13 +381,20 @@ class PXEBoot(base.BootInterface): node = task.node if CONF.pxe.ipxe_enabled: - # Copy the iPXE boot script to HTTP root directory + # Render the iPXE boot script template and save it + # to HTTP root directory + boot_script = utils.render_template( + CONF.pxe.ipxe_boot_script, + {'ipxe_for_mac_uri': pxe_utils.PXE_CFG_DIR_NAME + '/'}) bootfile_path = os.path.join( CONF.deploy.http_root, os.path.basename(CONF.pxe.ipxe_boot_script)) + # NOTE(pas-ha) to prevent unneeded writes, + # only write to file if its content is different from required, + # which should be rather rare if (not os.path.isfile(bootfile_path) or - not filecmp.cmp(CONF.pxe.ipxe_boot_script, bootfile_path)): - shutil.copyfile(CONF.pxe.ipxe_boot_script, bootfile_path) + not utils.file_has_content(bootfile_path, boot_script)): + utils.write_to_file(bootfile_path, boot_script) dhcp_opts = pxe_utils.dhcp_options_for_instance(task) provider = dhcp_factory.DHCPFactory() diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py index 5250bf248b..037aaae7e4 100644 --- a/ironic/tests/unit/common/test_pxe_utils.py +++ b/ironic/tests/unit/common/test_pxe_utils.py @@ -22,6 +22,7 @@ from oslo_utils import uuidutils import six from ironic.common import pxe_utils +from ironic.common import utils from ironic.conductor import task_manager from ironic.tests.unit.conductor import mgr_utils from ironic.tests.unit.db import base as db_base @@ -65,18 +66,30 @@ class TestPXEUtils(db_base.DbTestCase): self.node = object_utils.create_test_node(self.context) - def test__build_pxe_config(self): + def test_default_pxe_config(self): - rendered_template = pxe_utils._build_pxe_config( - self.pxe_options, CONF.pxe.pxe_config_template, - '{{ ROOT }}', '{{ DISK_IDENTIFIER }}') + rendered_template = utils.render_template( + CONF.pxe.pxe_config_template, + {'pxe_options': self.pxe_options, + 'ROOT': '{{ ROOT }}', + 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'}) expected_template = open( 'ironic/tests/unit/drivers/pxe_config.template').read().rstrip() self.assertEqual(six.text_type(expected_template), rendered_template) - def test__build_ipxe_config(self): + def test_default_ipxe_boot_script(self): + rendered_template = utils.render_template( + CONF.pxe.ipxe_boot_script, + {'ipxe_for_mac_uri': 'pxelinux.cfg/'}) + + expected_template = open( + 'ironic/tests/unit/drivers/boot.ipxe').read().rstrip() + + self.assertEqual(six.text_type(expected_template), rendered_template) + + def test_default_ipxe_config(self): # NOTE(lucasagomes): iPXE is just an extension of the PXE driver, # it doesn't have it's own configuration option for template. # More info: @@ -86,16 +99,18 @@ class TestPXEUtils(db_base.DbTestCase): group='pxe' ) self.config(http_url='http://1.2.3.4:1234', group='deploy') - rendered_template = pxe_utils._build_pxe_config( - self.ipxe_options, CONF.pxe.pxe_config_template, - '{{ ROOT }}', '{{ DISK_IDENTIFIER }}') + rendered_template = utils.render_template( + CONF.pxe.pxe_config_template, + {'pxe_options': self.ipxe_options, + 'ROOT': '{{ ROOT }}', + 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'}) expected_template = open( 'ironic/tests/unit/drivers/ipxe_config.template').read().rstrip() self.assertEqual(six.text_type(expected_template), rendered_template) - def test__build_ipxe_timeout_config(self): + def test_default_ipxe_timeout_config(self): # NOTE(lucasagomes): iPXE is just an extension of the PXE driver, # it doesn't have it's own configuration option for template. # More info: @@ -105,16 +120,18 @@ class TestPXEUtils(db_base.DbTestCase): group='pxe' ) self.config(http_url='http://1.2.3.4:1234', group='deploy') - rendered_template = pxe_utils._build_pxe_config( - self.ipxe_options_timeout, CONF.pxe.pxe_config_template, - '{{ ROOT }}', '{{ DISK_IDENTIFIER }}') + rendered_template = utils.render_template( + CONF.pxe.pxe_config_template, + {'pxe_options': self.ipxe_options_timeout, + 'ROOT': '{{ ROOT }}', + 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'}) tpl_file = 'ironic/tests/unit/drivers/ipxe_config_timeout.template' expected_template = open(tpl_file).read().rstrip() self.assertEqual(six.text_type(expected_template), rendered_template) - def test__build_elilo_config(self): + def test_default_elilo_config(self): pxe_opts = self.pxe_options pxe_opts['boot_mode'] = 'uefi' self.config( @@ -122,9 +139,11 @@ class TestPXEUtils(db_base.DbTestCase): 'elilo_efi_pxe_config.template'), group='pxe' ) - rendered_template = pxe_utils._build_pxe_config( - pxe_opts, CONF.pxe.uefi_pxe_config_template, - '{{ ROOT }}', '{{ DISK_IDENTIFIER }}') + rendered_template = utils.render_template( + CONF.pxe.uefi_pxe_config_template, + {'pxe_options': pxe_opts, + 'ROOT': '{{ ROOT }}', + 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'}) expected_template = open( 'ironic/tests/unit/drivers/elilo_efi_pxe_config.template' @@ -132,13 +151,15 @@ class TestPXEUtils(db_base.DbTestCase): self.assertEqual(six.text_type(expected_template), rendered_template) - def test__build_grub_config(self): + def test_default_grub_config(self): pxe_opts = self.pxe_options pxe_opts['boot_mode'] = 'uefi' pxe_opts['tftp_server'] = '192.0.2.1' - rendered_template = pxe_utils._build_pxe_config( - pxe_opts, CONF.pxe.uefi_pxe_config_template, - '(( ROOT ))', '(( DISK_IDENTIFIER ))') + rendered_template = utils.render_template( + CONF.pxe.uefi_pxe_config_template, + {'pxe_options': pxe_opts, + 'ROOT': '(( ROOT ))', + 'DISK_IDENTIFIER': '(( DISK_IDENTIFIER ))'}) template_file = 'ironic/tests/unit/drivers/pxe_grub_config.template' expected_template = open(template_file).read().rstrip() @@ -254,18 +275,19 @@ class TestPXEUtils(db_base.DbTestCase): create_link_mock.assert_has_calls(create_link_calls) @mock.patch('ironic.common.utils.write_to_file', autospec=True) - @mock.patch.object(pxe_utils, '_build_pxe_config', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) @mock.patch('oslo_utils.fileutils.ensure_tree', autospec=True) - def test_create_pxe_config(self, ensure_tree_mock, build_mock, + def test_create_pxe_config(self, ensure_tree_mock, render_mock, write_mock): - build_mock.return_value = self.pxe_options with task_manager.acquire(self.context, self.node.uuid) as task: pxe_utils.create_pxe_config(task, self.pxe_options, CONF.pxe.pxe_config_template) - build_mock.assert_called_with(self.pxe_options, - CONF.pxe.pxe_config_template, - '{{ ROOT }}', - '{{ DISK_IDENTIFIER }}') + render_mock.assert_called_with( + CONF.pxe.pxe_config_template, + {'pxe_options': self.pxe_options, + 'ROOT': '{{ ROOT }}', + 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'} + ) ensure_calls = [ mock.call(os.path.join(CONF.pxe.tftp_root, self.node.uuid)), mock.call(os.path.join(CONF.pxe.tftp_root, 'pxelinux.cfg')) @@ -273,21 +295,21 @@ class TestPXEUtils(db_base.DbTestCase): ensure_tree_mock.assert_has_calls(ensure_calls) pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) - write_mock.assert_called_with(pxe_cfg_file_path, self.pxe_options) + write_mock.assert_called_with(pxe_cfg_file_path, + render_mock.return_value) @mock.patch('ironic.common.pxe_utils._link_ip_address_pxe_configs', autospec=True) @mock.patch('ironic.common.utils.write_to_file', autospec=True) - @mock.patch('ironic.common.pxe_utils._build_pxe_config', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) @mock.patch('oslo_utils.fileutils.ensure_tree', autospec=True) - def test_create_pxe_config_uefi_elilo(self, ensure_tree_mock, build_mock, + def test_create_pxe_config_uefi_elilo(self, ensure_tree_mock, render_mock, write_mock, link_ip_configs_mock): self.config( uefi_pxe_config_template=('ironic/drivers/modules/' 'elilo_efi_pxe_config.template'), group='pxe' ) - build_mock.return_value = self.pxe_options with task_manager.acquire(self.context, self.node.uuid) as task: task.node.properties['capabilities'] = 'boot_mode:uefi' pxe_utils.create_pxe_config(task, self.pxe_options, @@ -298,23 +320,24 @@ class TestPXEUtils(db_base.DbTestCase): mock.call(os.path.join(CONF.pxe.tftp_root, 'pxelinux.cfg')) ] ensure_tree_mock.assert_has_calls(ensure_calls) - build_mock.assert_called_with(self.pxe_options, - CONF.pxe.uefi_pxe_config_template, - '{{ ROOT }}', - '{{ DISK_IDENTIFIER }}') + render_mock.assert_called_with( + CONF.pxe.uefi_pxe_config_template, + {'pxe_options': self.pxe_options, + 'ROOT': '{{ ROOT }}', + 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'}) link_ip_configs_mock.assert_called_once_with(task, True) pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) - write_mock.assert_called_with(pxe_cfg_file_path, self.pxe_options) + write_mock.assert_called_with(pxe_cfg_file_path, + render_mock.return_value) @mock.patch('ironic.common.pxe_utils._link_ip_address_pxe_configs', autospec=True) @mock.patch('ironic.common.utils.write_to_file', autospec=True) - @mock.patch('ironic.common.pxe_utils._build_pxe_config', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) @mock.patch('oslo_utils.fileutils.ensure_tree', autospec=True) - def test_create_pxe_config_uefi_grub(self, ensure_tree_mock, build_mock, + def test_create_pxe_config_uefi_grub(self, ensure_tree_mock, render_mock, write_mock, link_ip_configs_mock): - build_mock.return_value = self.pxe_options grub_tmplte = "ironic/drivers/modules/pxe_grub_config.template" with task_manager.acquire(self.context, self.node.uuid) as task: task.node.properties['capabilities'] = 'boot_mode:uefi' @@ -326,24 +349,25 @@ class TestPXEUtils(db_base.DbTestCase): mock.call(os.path.join(CONF.pxe.tftp_root, 'pxelinux.cfg')) ] ensure_tree_mock.assert_has_calls(ensure_calls) - build_mock.assert_called_with(self.pxe_options, - grub_tmplte, - '(( ROOT ))', - '(( DISK_IDENTIFIER ))') + render_mock.assert_called_with( + grub_tmplte, + {'pxe_options': self.pxe_options, + 'ROOT': '(( ROOT ))', + 'DISK_IDENTIFIER': '(( DISK_IDENTIFIER ))'}) link_ip_configs_mock.assert_called_once_with(task, False) pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) - write_mock.assert_called_with(pxe_cfg_file_path, self.pxe_options) + write_mock.assert_called_with(pxe_cfg_file_path, + render_mock.return_value) @mock.patch('ironic.common.pxe_utils._link_mac_pxe_configs', autospec=True) @mock.patch('ironic.common.utils.write_to_file', autospec=True) - @mock.patch('ironic.common.pxe_utils._build_pxe_config', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) @mock.patch('oslo_utils.fileutils.ensure_tree', autospec=True) - def test_create_pxe_config_uefi_ipxe(self, ensure_tree_mock, build_mock, + def test_create_pxe_config_uefi_ipxe(self, ensure_tree_mock, render_mock, write_mock, link_mac_pxe_mock): self.config(ipxe_enabled=True, group='pxe') - build_mock.return_value = self.ipxe_options ipxe_template = "ironic/drivers/modules/ipxe_config.template" with task_manager.acquire(self.context, self.node.uuid) as task: task.node.properties['capabilities'] = 'boot_mode:uefi' @@ -355,15 +379,16 @@ class TestPXEUtils(db_base.DbTestCase): mock.call(os.path.join(CONF.deploy.http_root, 'pxelinux.cfg')) ] ensure_tree_mock.assert_has_calls(ensure_calls) - build_mock.assert_called_with(self.ipxe_options, - ipxe_template, - '{{ ROOT }}', - '{{ DISK_IDENTIFIER }}') + render_mock.assert_called_with( + ipxe_template, + {'pxe_options': self.ipxe_options, + 'ROOT': '{{ ROOT }}', + 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'}) link_mac_pxe_mock.assert_called_once_with(task) pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) write_mock.assert_called_with(pxe_cfg_file_path, - self.ipxe_options) + render_mock.return_value) @mock.patch('ironic.common.utils.rmtree_without_raise', autospec=True) @mock.patch('ironic_lib.utils.unlink_without_raise', autospec=True) diff --git a/ironic/tests/unit/common/test_utils.py b/ironic/tests/unit/common/test_utils.py index 861b1ded30..2c6068af37 100644 --- a/ironic/tests/unit/common/test_utils.py +++ b/ironic/tests/unit/common/test_utils.py @@ -21,6 +21,7 @@ import os.path import shutil import tempfile +import jinja2 import mock from oslo_concurrency import processutils from oslo_config import cfg @@ -248,6 +249,20 @@ class GenericUtilsTestCase(base.TestCase): self.assertRaises(exception.InvalidParameterValue, utils.hash_file, file_like_object, 'hickory-dickory-dock') + def test_file_has_content_equal(self): + data = b'Mary had a little lamb, its fleece as white as snow' + ref = data + with mock.patch('ironic.common.utils.open', + mock.mock_open(read_data=data)): + self.assertTrue(utils.file_has_content('foo', ref)) + + def test_file_has_content_differ(self): + data = b'Mary had a little lamb, its fleece as white as snow' + ref = data + b'!' + with mock.patch('ironic.common.utils.open', + mock.mock_open(read_data=data)): + self.assertFalse(utils.file_has_content('foo', ref)) + def test_is_valid_datapath_id(self): self.assertTrue(utils.is_valid_datapath_id("525400cf2d319fdf")) self.assertTrue(utils.is_valid_datapath_id("525400CF2D319FDF")) @@ -613,3 +628,28 @@ class GetUpdatedCapabilitiesTestCase(base.TestCase): 'Port "invalid" is not a valid integer.', utils.validate_network_port, 'invalid') + + +class JinjaTemplatingTestCase(base.TestCase): + + def setUp(self): + super(JinjaTemplatingTestCase, self).setUp() + self.template = '{{ foo }} {{ bar }}' + self.params = {'foo': 'spam', 'bar': 'ham'} + self.expected = 'spam ham' + + def test_render_string(self): + self.assertEqual(self.expected, + utils.render_template(self.template, + self.params, + is_file=False)) + + @mock.patch('ironic.common.utils.jinja2.FileSystemLoader') + def test_render_file(self, jinja_fsl_mock): + path = '/path/to/template.j2' + jinja_fsl_mock.return_value = jinja2.DictLoader( + {'template.j2': self.template}) + self.assertEqual(self.expected, + utils.render_template(path, + self.params)) + jinja_fsl_mock.assert_called_once_with('/path/to') diff --git a/ironic/tests/unit/drivers/boot.ipxe b/ironic/tests/unit/drivers/boot.ipxe new file mode 100644 index 0000000000..aa8ee9e511 --- /dev/null +++ b/ironic/tests/unit/drivers/boot.ipxe @@ -0,0 +1,24 @@ +#!ipxe + +# NOTE(lucasagomes): Loop over all network devices and boot from +# the first one capable of booting. For more information see: +# https://bugs.launchpad.net/ironic/+bug/1504482 +set netid:int32 -1 +:loop +inc netid || chain pxelinux.cfg/${mac:hexhyp} || goto old_rom +isset ${net${netid}/mac} || goto loop_done +echo Attempting to boot from MAC ${net${netid}/mac:hexhyp} +chain pxelinux.cfg/${net${netid}/mac:hexhyp} || goto loop + +:loop_done +echo PXE boot failed! No configuration found for any of the present NICs. +echo Press any key to reboot... +prompt --timeout 180 +reboot + +:old_rom +echo PXE boot failed! No configuration found for NIC ${mac:hexhyp}. +echo Please update your iPXE ROM and retry. +echo Press any key to reboot... +prompt --timeout 180 +reboot diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py index fe39287973..2b7e4f42e5 100644 --- a/ironic/tests/unit/drivers/modules/test_pxe.py +++ b/ironic/tests/unit/drivers/modules/test_pxe.py @@ -15,9 +15,7 @@ """Test class for PXE driver.""" -import filecmp import os -import shutil import tempfile from ironic_lib import utils as ironic_utils @@ -164,8 +162,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): image_info = pxe._get_instance_image_info(self.node, self.context) self.assertEqual({}, image_info) - @mock.patch.object(pxe_utils, '_build_pxe_config', autospec=True) - def _test_build_pxe_config_options_pxe(self, build_pxe_mock, + @mock.patch('ironic.common.utils.render_template', autospec=True) + def _test_build_pxe_config_options_pxe(self, render_mock, whle_dsk_img=False): self.config(pxe_append_params='test_param', group='pxe') # NOTE: right '/' should be removed from url string @@ -272,8 +270,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): @mock.patch('ironic.common.image_service.GlanceImageService', autospec=True) - @mock.patch.object(pxe_utils, '_build_pxe_config', autospec=True) - def _test_build_pxe_config_options_ipxe(self, build_pxe_mock, glance_mock, + @mock.patch('ironic.common.utils.render_template', autospec=True) + def _test_build_pxe_config_options_ipxe(self, render_mock, glance_mock, whle_dsk_img=False, ipxe_timeout=0, ipxe_use_swift=False): @@ -770,46 +768,57 @@ class PXEBootTestCase(db_base.DbTestCase): self._test_prepare_ramdisk(uefi=True) @mock.patch.object(os.path, 'isfile', autospec=True) - @mock.patch.object(filecmp, 'cmp', autospec=True) - @mock.patch.object(shutil, 'copyfile', autospec=True) + @mock.patch('ironic.common.utils.file_has_content', autospec=True) + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) def test_prepare_ramdisk_ipxe_with_copy_file_different( - self, copyfile_mock, cmp_mock, isfile_mock): + self, render_mock, write_mock, cmp_mock, isfile_mock): self.node.provision_state = states.DEPLOYING self.node.save() self.config(group='pxe', ipxe_enabled=True) self.config(group='deploy', http_url='http://myserver') isfile_mock.return_value = True cmp_mock.return_value = False + render_mock.return_value = 'foo' self._test_prepare_ramdisk() - copyfile_mock.assert_called_once_with( - CONF.pxe.ipxe_boot_script, + write_mock.assert_called_once_with( os.path.join( CONF.deploy.http_root, - os.path.basename(CONF.pxe.ipxe_boot_script))) + os.path.basename(CONF.pxe.ipxe_boot_script)), + 'foo') + render_mock.assert_called_once_with( + CONF.pxe.ipxe_boot_script, + {'ipxe_for_mac_uri': 'pxelinux.cfg/'}) @mock.patch.object(os.path, 'isfile', autospec=True) - @mock.patch.object(filecmp, 'cmp', autospec=True) - @mock.patch.object(shutil, 'copyfile', autospec=True) + @mock.patch('ironic.common.utils.file_has_content', autospec=True) + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) def test_prepare_ramdisk_ipxe_with_copy_no_file( - self, copyfile_mock, cmp_mock, isfile_mock): + self, render_mock, write_mock, cmp_mock, isfile_mock): self.node.provision_state = states.DEPLOYING self.node.save() self.config(group='pxe', ipxe_enabled=True) self.config(group='deploy', http_url='http://myserver') isfile_mock.return_value = False + render_mock.return_value = 'foo' self._test_prepare_ramdisk() self.assertFalse(cmp_mock.called) - copyfile_mock.assert_called_once_with( - CONF.pxe.ipxe_boot_script, + write_mock.assert_called_once_with( os.path.join( CONF.deploy.http_root, - os.path.basename(CONF.pxe.ipxe_boot_script))) + os.path.basename(CONF.pxe.ipxe_boot_script)), + 'foo') + render_mock.assert_called_once_with( + CONF.pxe.ipxe_boot_script, + {'ipxe_for_mac_uri': 'pxelinux.cfg/'}) @mock.patch.object(os.path, 'isfile', autospec=True) - @mock.patch.object(filecmp, 'cmp', autospec=True) - @mock.patch.object(shutil, 'copyfile', autospec=True) + @mock.patch('ironic.common.utils.file_has_content', autospec=True) + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) def test_prepare_ramdisk_ipxe_without_copy( - self, copyfile_mock, cmp_mock, isfile_mock): + self, render_mock, write_mock, cmp_mock, isfile_mock): self.node.provision_state = states.DEPLOYING self.node.save() self.config(group='pxe', ipxe_enabled=True) @@ -817,35 +826,40 @@ class PXEBootTestCase(db_base.DbTestCase): isfile_mock.return_value = True cmp_mock.return_value = True self._test_prepare_ramdisk() - self.assertFalse(copyfile_mock.called) + self.assertFalse(write_mock.called) - @mock.patch.object(shutil, 'copyfile', autospec=True) - def test_prepare_ramdisk_ipxe_swift(self, copyfile_mock): + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) + def test_prepare_ramdisk_ipxe_swift(self, render_mock, write_mock): self.node.provision_state = states.DEPLOYING self.node.save() self.config(group='pxe', ipxe_enabled=True) self.config(group='pxe', ipxe_use_swift=True) self.config(group='deploy', http_url='http://myserver') + render_mock.return_value = 'foo' self._test_prepare_ramdisk(ipxe_use_swift=True) - copyfile_mock.assert_called_once_with( - CONF.pxe.ipxe_boot_script, + write_mock.assert_called_once_with( os.path.join( CONF.deploy.http_root, - os.path.basename(CONF.pxe.ipxe_boot_script))) + os.path.basename(CONF.pxe.ipxe_boot_script)), + 'foo') - @mock.patch.object(shutil, 'copyfile', autospec=True) - def test_prepare_ramdisk_ipxe_swift_whole_disk_image(self, copyfile_mock): + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) + def test_prepare_ramdisk_ipxe_swift_whole_disk_image( + self, render_mock, write_mock): self.node.provision_state = states.DEPLOYING self.node.save() self.config(group='pxe', ipxe_enabled=True) self.config(group='pxe', ipxe_use_swift=True) self.config(group='deploy', http_url='http://myserver') + render_mock.return_value = 'foo' self._test_prepare_ramdisk(ipxe_use_swift=True, whole_disk_image=True) - copyfile_mock.assert_called_once_with( - CONF.pxe.ipxe_boot_script, + write_mock.assert_called_once_with( os.path.join( CONF.deploy.http_root, - os.path.basename(CONF.pxe.ipxe_boot_script))) + os.path.basename(CONF.pxe.ipxe_boot_script)), + 'foo') def test_prepare_ramdisk_cleaning(self): self.node.provision_state = states.CLEANING