Merge "Convert iPXE boot script to Jinja template"

This commit is contained in:
Jenkins 2016-11-22 16:01:09 +00:00 committed by Gerrit Code Review
commit ab79b3eff2
8 changed files with 241 additions and 116 deletions

View File

@ -17,7 +17,6 @@
import os import os
from ironic_lib import utils as ironic_utils from ironic_lib import utils as ironic_utils
import jinja2
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import fileutils 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)) 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): def _link_mac_pxe_configs(task):
"""Link each MAC address with the PXE configuration file. """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_root_tag = '{{ ROOT }}'
pxe_config_disk_ident = '{{ DISK_IDENTIFIER }}' pxe_config_disk_ident = '{{ DISK_IDENTIFIER }}'
pxe_config = _build_pxe_config(pxe_options, template, pxe_config_root_tag, params = {'pxe_options': pxe_options,
pxe_config_disk_ident) '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) utils.write_to_file(pxe_config_file_path, pxe_config)
if is_uefi_boot_mode and not CONF.pxe.ipxe_enabled: if is_uefi_boot_mode and not CONF.pxe.ipxe_enabled:

View File

@ -27,6 +27,7 @@ import re
import shutil import shutil
import tempfile import tempfile
import jinja2
from oslo_concurrency import processutils from oslo_concurrency import processutils
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import netutils from oslo_utils import netutils
@ -283,6 +284,22 @@ def hash_file(file_like_object, hash_algo='md5'):
return checksum.hexdigest() 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 @contextlib.contextmanager
def tempdir(**kwargs): def tempdir(**kwargs):
tempfile.tempdir = CONF.tempdir tempfile.tempdir = CONF.tempdir
@ -500,3 +517,22 @@ def validate_network_port(port, port_name="Port"):
'numbers must be between 1 and 65535.') % 'numbers must be between 1 and 65535.') %
{'port_name': port_name, 'port': port}) {'port_name': port_name, 'port': port})
return 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)

View File

@ -5,10 +5,10 @@
# https://bugs.launchpad.net/ironic/+bug/1504482 # https://bugs.launchpad.net/ironic/+bug/1504482
set netid:int32 -1 set netid:int32 -1
:loop :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 isset ${net${netid}/mac} || goto loop_done
echo Attempting to boot from MAC ${net${netid}/mac:hexhyp} 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 :loop_done
echo PXE boot failed! No configuration found for any of the present NICs. echo PXE boot failed! No configuration found for any of the present NICs.

View File

@ -15,9 +15,7 @@
PXE Boot Interface PXE Boot Interface
""" """
import filecmp
import os import os
import shutil
from ironic_lib import metrics_utils from ironic_lib import metrics_utils
from ironic_lib import utils as ironic_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 images
from ironic.common import pxe_utils from ironic.common import pxe_utils
from ironic.common import states from ironic.common import states
from ironic.common import utils
from ironic.conf import CONF from ironic.conf import CONF
from ironic.drivers import base from ironic.drivers import base
from ironic.drivers.modules import deploy_utils from ironic.drivers.modules import deploy_utils
@ -382,13 +381,20 @@ class PXEBoot(base.BootInterface):
node = task.node node = task.node
if CONF.pxe.ipxe_enabled: 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( bootfile_path = os.path.join(
CONF.deploy.http_root, CONF.deploy.http_root,
os.path.basename(CONF.pxe.ipxe_boot_script)) 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 if (not os.path.isfile(bootfile_path) or
not filecmp.cmp(CONF.pxe.ipxe_boot_script, bootfile_path)): not utils.file_has_content(bootfile_path, boot_script)):
shutil.copyfile(CONF.pxe.ipxe_boot_script, bootfile_path) utils.write_to_file(bootfile_path, boot_script)
dhcp_opts = pxe_utils.dhcp_options_for_instance(task) dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
provider = dhcp_factory.DHCPFactory() provider = dhcp_factory.DHCPFactory()

View File

@ -22,6 +22,7 @@ from oslo_utils import uuidutils
import six import six
from ironic.common import pxe_utils from ironic.common import pxe_utils
from ironic.common import utils
from ironic.conductor import task_manager from ironic.conductor import task_manager
from ironic.tests.unit.conductor import mgr_utils from ironic.tests.unit.conductor import mgr_utils
from ironic.tests.unit.db import base as db_base 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) 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( rendered_template = utils.render_template(
self.pxe_options, CONF.pxe.pxe_config_template, CONF.pxe.pxe_config_template,
'{{ ROOT }}', '{{ DISK_IDENTIFIER }}') {'pxe_options': self.pxe_options,
'ROOT': '{{ ROOT }}',
'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'})
expected_template = open( expected_template = open(
'ironic/tests/unit/drivers/pxe_config.template').read().rstrip() 'ironic/tests/unit/drivers/pxe_config.template').read().rstrip()
self.assertEqual(six.text_type(expected_template), rendered_template) 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, # NOTE(lucasagomes): iPXE is just an extension of the PXE driver,
# it doesn't have it's own configuration option for template. # it doesn't have it's own configuration option for template.
# More info: # More info:
@ -86,16 +99,18 @@ class TestPXEUtils(db_base.DbTestCase):
group='pxe' group='pxe'
) )
self.config(http_url='http://1.2.3.4:1234', group='deploy') self.config(http_url='http://1.2.3.4:1234', group='deploy')
rendered_template = pxe_utils._build_pxe_config( rendered_template = utils.render_template(
self.ipxe_options, CONF.pxe.pxe_config_template, CONF.pxe.pxe_config_template,
'{{ ROOT }}', '{{ DISK_IDENTIFIER }}') {'pxe_options': self.ipxe_options,
'ROOT': '{{ ROOT }}',
'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'})
expected_template = open( expected_template = open(
'ironic/tests/unit/drivers/ipxe_config.template').read().rstrip() 'ironic/tests/unit/drivers/ipxe_config.template').read().rstrip()
self.assertEqual(six.text_type(expected_template), rendered_template) 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, # NOTE(lucasagomes): iPXE is just an extension of the PXE driver,
# it doesn't have it's own configuration option for template. # it doesn't have it's own configuration option for template.
# More info: # More info:
@ -105,16 +120,18 @@ class TestPXEUtils(db_base.DbTestCase):
group='pxe' group='pxe'
) )
self.config(http_url='http://1.2.3.4:1234', group='deploy') self.config(http_url='http://1.2.3.4:1234', group='deploy')
rendered_template = pxe_utils._build_pxe_config( rendered_template = utils.render_template(
self.ipxe_options_timeout, CONF.pxe.pxe_config_template, CONF.pxe.pxe_config_template,
'{{ ROOT }}', '{{ DISK_IDENTIFIER }}') {'pxe_options': self.ipxe_options_timeout,
'ROOT': '{{ ROOT }}',
'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'})
tpl_file = 'ironic/tests/unit/drivers/ipxe_config_timeout.template' tpl_file = 'ironic/tests/unit/drivers/ipxe_config_timeout.template'
expected_template = open(tpl_file).read().rstrip() expected_template = open(tpl_file).read().rstrip()
self.assertEqual(six.text_type(expected_template), rendered_template) 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 = self.pxe_options
pxe_opts['boot_mode'] = 'uefi' pxe_opts['boot_mode'] = 'uefi'
self.config( self.config(
@ -122,9 +139,11 @@ class TestPXEUtils(db_base.DbTestCase):
'elilo_efi_pxe_config.template'), 'elilo_efi_pxe_config.template'),
group='pxe' group='pxe'
) )
rendered_template = pxe_utils._build_pxe_config( rendered_template = utils.render_template(
pxe_opts, CONF.pxe.uefi_pxe_config_template, CONF.pxe.uefi_pxe_config_template,
'{{ ROOT }}', '{{ DISK_IDENTIFIER }}') {'pxe_options': pxe_opts,
'ROOT': '{{ ROOT }}',
'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'})
expected_template = open( expected_template = open(
'ironic/tests/unit/drivers/elilo_efi_pxe_config.template' '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) 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 = self.pxe_options
pxe_opts['boot_mode'] = 'uefi' pxe_opts['boot_mode'] = 'uefi'
pxe_opts['tftp_server'] = '192.0.2.1' pxe_opts['tftp_server'] = '192.0.2.1'
rendered_template = pxe_utils._build_pxe_config( rendered_template = utils.render_template(
pxe_opts, CONF.pxe.uefi_pxe_config_template, CONF.pxe.uefi_pxe_config_template,
'(( ROOT ))', '(( DISK_IDENTIFIER ))') {'pxe_options': pxe_opts,
'ROOT': '(( ROOT ))',
'DISK_IDENTIFIER': '(( DISK_IDENTIFIER ))'})
template_file = 'ironic/tests/unit/drivers/pxe_grub_config.template' template_file = 'ironic/tests/unit/drivers/pxe_grub_config.template'
expected_template = open(template_file).read().rstrip() 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) create_link_mock.assert_has_calls(create_link_calls)
@mock.patch('ironic.common.utils.write_to_file', autospec=True) @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) @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): write_mock):
build_mock.return_value = self.pxe_options
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
pxe_utils.create_pxe_config(task, self.pxe_options, pxe_utils.create_pxe_config(task, self.pxe_options,
CONF.pxe.pxe_config_template) CONF.pxe.pxe_config_template)
build_mock.assert_called_with(self.pxe_options, render_mock.assert_called_with(
CONF.pxe.pxe_config_template, CONF.pxe.pxe_config_template,
'{{ ROOT }}', {'pxe_options': self.pxe_options,
'{{ DISK_IDENTIFIER }}') 'ROOT': '{{ ROOT }}',
'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'}
)
ensure_calls = [ ensure_calls = [
mock.call(os.path.join(CONF.pxe.tftp_root, self.node.uuid)), mock.call(os.path.join(CONF.pxe.tftp_root, self.node.uuid)),
mock.call(os.path.join(CONF.pxe.tftp_root, 'pxelinux.cfg')) 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) ensure_tree_mock.assert_has_calls(ensure_calls)
pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) 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', @mock.patch('ironic.common.pxe_utils._link_ip_address_pxe_configs',
autospec=True) autospec=True)
@mock.patch('ironic.common.utils.write_to_file', 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) @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): write_mock, link_ip_configs_mock):
self.config( self.config(
uefi_pxe_config_template=('ironic/drivers/modules/' uefi_pxe_config_template=('ironic/drivers/modules/'
'elilo_efi_pxe_config.template'), 'elilo_efi_pxe_config.template'),
group='pxe' group='pxe'
) )
build_mock.return_value = self.pxe_options
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.properties['capabilities'] = 'boot_mode:uefi' task.node.properties['capabilities'] = 'boot_mode:uefi'
pxe_utils.create_pxe_config(task, self.pxe_options, 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')) mock.call(os.path.join(CONF.pxe.tftp_root, 'pxelinux.cfg'))
] ]
ensure_tree_mock.assert_has_calls(ensure_calls) ensure_tree_mock.assert_has_calls(ensure_calls)
build_mock.assert_called_with(self.pxe_options, render_mock.assert_called_with(
CONF.pxe.uefi_pxe_config_template, CONF.pxe.uefi_pxe_config_template,
'{{ ROOT }}', {'pxe_options': self.pxe_options,
'{{ DISK_IDENTIFIER }}') 'ROOT': '{{ ROOT }}',
'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'})
link_ip_configs_mock.assert_called_once_with(task, True) link_ip_configs_mock.assert_called_once_with(task, True)
pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) 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', @mock.patch('ironic.common.pxe_utils._link_ip_address_pxe_configs',
autospec=True) autospec=True)
@mock.patch('ironic.common.utils.write_to_file', 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) @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): write_mock, link_ip_configs_mock):
build_mock.return_value = self.pxe_options
grub_tmplte = "ironic/drivers/modules/pxe_grub_config.template" grub_tmplte = "ironic/drivers/modules/pxe_grub_config.template"
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.properties['capabilities'] = 'boot_mode:uefi' 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')) mock.call(os.path.join(CONF.pxe.tftp_root, 'pxelinux.cfg'))
] ]
ensure_tree_mock.assert_has_calls(ensure_calls) ensure_tree_mock.assert_has_calls(ensure_calls)
build_mock.assert_called_with(self.pxe_options, render_mock.assert_called_with(
grub_tmplte, grub_tmplte,
'(( ROOT ))', {'pxe_options': self.pxe_options,
'(( DISK_IDENTIFIER ))') 'ROOT': '(( ROOT ))',
'DISK_IDENTIFIER': '(( DISK_IDENTIFIER ))'})
link_ip_configs_mock.assert_called_once_with(task, False) link_ip_configs_mock.assert_called_once_with(task, False)
pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) 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', @mock.patch('ironic.common.pxe_utils._link_mac_pxe_configs',
autospec=True) autospec=True)
@mock.patch('ironic.common.utils.write_to_file', 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) @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): write_mock, link_mac_pxe_mock):
self.config(ipxe_enabled=True, group='pxe') self.config(ipxe_enabled=True, group='pxe')
build_mock.return_value = self.ipxe_options
ipxe_template = "ironic/drivers/modules/ipxe_config.template" ipxe_template = "ironic/drivers/modules/ipxe_config.template"
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.properties['capabilities'] = 'boot_mode:uefi' 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')) mock.call(os.path.join(CONF.deploy.http_root, 'pxelinux.cfg'))
] ]
ensure_tree_mock.assert_has_calls(ensure_calls) ensure_tree_mock.assert_has_calls(ensure_calls)
build_mock.assert_called_with(self.ipxe_options, render_mock.assert_called_with(
ipxe_template, ipxe_template,
'{{ ROOT }}', {'pxe_options': self.ipxe_options,
'{{ DISK_IDENTIFIER }}') 'ROOT': '{{ ROOT }}',
'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'})
link_mac_pxe_mock.assert_called_once_with(task) link_mac_pxe_mock.assert_called_once_with(task)
pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid)
write_mock.assert_called_with(pxe_cfg_file_path, 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.common.utils.rmtree_without_raise', autospec=True)
@mock.patch('ironic_lib.utils.unlink_without_raise', autospec=True) @mock.patch('ironic_lib.utils.unlink_without_raise', autospec=True)

View File

@ -21,6 +21,7 @@ import os.path
import shutil import shutil
import tempfile import tempfile
import jinja2
import mock import mock
from oslo_concurrency import processutils from oslo_concurrency import processutils
from oslo_config import cfg from oslo_config import cfg
@ -248,6 +249,20 @@ class GenericUtilsTestCase(base.TestCase):
self.assertRaises(exception.InvalidParameterValue, utils.hash_file, self.assertRaises(exception.InvalidParameterValue, utils.hash_file,
file_like_object, 'hickory-dickory-dock') 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): def test_is_valid_datapath_id(self):
self.assertTrue(utils.is_valid_datapath_id("525400cf2d319fdf")) self.assertTrue(utils.is_valid_datapath_id("525400cf2d319fdf"))
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.', 'Port "invalid" is not a valid integer.',
utils.validate_network_port, utils.validate_network_port,
'invalid') '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')

View File

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

View File

@ -15,9 +15,7 @@
"""Test class for PXE driver.""" """Test class for PXE driver."""
import filecmp
import os import os
import shutil
import tempfile import tempfile
from ironic_lib import utils as ironic_utils 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) image_info = pxe._get_instance_image_info(self.node, self.context)
self.assertEqual({}, image_info) self.assertEqual({}, image_info)
@mock.patch.object(pxe_utils, '_build_pxe_config', autospec=True) @mock.patch('ironic.common.utils.render_template', autospec=True)
def _test_build_pxe_config_options_pxe(self, build_pxe_mock, def _test_build_pxe_config_options_pxe(self, render_mock,
whle_dsk_img=False): whle_dsk_img=False):
self.config(pxe_append_params='test_param', group='pxe') self.config(pxe_append_params='test_param', group='pxe')
# NOTE: right '/' should be removed from url string # NOTE: right '/' should be removed from url string
@ -272,8 +270,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
@mock.patch('ironic.common.image_service.GlanceImageService', @mock.patch('ironic.common.image_service.GlanceImageService',
autospec=True) autospec=True)
@mock.patch.object(pxe_utils, '_build_pxe_config', autospec=True) @mock.patch('ironic.common.utils.render_template', autospec=True)
def _test_build_pxe_config_options_ipxe(self, build_pxe_mock, glance_mock, def _test_build_pxe_config_options_ipxe(self, render_mock, glance_mock,
whle_dsk_img=False, whle_dsk_img=False,
ipxe_timeout=0, ipxe_timeout=0,
ipxe_use_swift=False): ipxe_use_swift=False):
@ -770,46 +768,57 @@ class PXEBootTestCase(db_base.DbTestCase):
self._test_prepare_ramdisk(uefi=True) self._test_prepare_ramdisk(uefi=True)
@mock.patch.object(os.path, 'isfile', autospec=True) @mock.patch.object(os.path, 'isfile', autospec=True)
@mock.patch.object(filecmp, 'cmp', autospec=True) @mock.patch('ironic.common.utils.file_has_content', autospec=True)
@mock.patch.object(shutil, 'copyfile', 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( 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.provision_state = states.DEPLOYING
self.node.save() self.node.save()
self.config(group='pxe', ipxe_enabled=True) self.config(group='pxe', ipxe_enabled=True)
self.config(group='deploy', http_url='http://myserver') self.config(group='deploy', http_url='http://myserver')
isfile_mock.return_value = True isfile_mock.return_value = True
cmp_mock.return_value = False cmp_mock.return_value = False
render_mock.return_value = 'foo'
self._test_prepare_ramdisk() self._test_prepare_ramdisk()
copyfile_mock.assert_called_once_with( write_mock.assert_called_once_with(
CONF.pxe.ipxe_boot_script,
os.path.join( os.path.join(
CONF.deploy.http_root, 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(os.path, 'isfile', autospec=True)
@mock.patch.object(filecmp, 'cmp', autospec=True) @mock.patch('ironic.common.utils.file_has_content', autospec=True)
@mock.patch.object(shutil, 'copyfile', 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( 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.provision_state = states.DEPLOYING
self.node.save() self.node.save()
self.config(group='pxe', ipxe_enabled=True) self.config(group='pxe', ipxe_enabled=True)
self.config(group='deploy', http_url='http://myserver') self.config(group='deploy', http_url='http://myserver')
isfile_mock.return_value = False isfile_mock.return_value = False
render_mock.return_value = 'foo'
self._test_prepare_ramdisk() self._test_prepare_ramdisk()
self.assertFalse(cmp_mock.called) self.assertFalse(cmp_mock.called)
copyfile_mock.assert_called_once_with( write_mock.assert_called_once_with(
CONF.pxe.ipxe_boot_script,
os.path.join( os.path.join(
CONF.deploy.http_root, 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(os.path, 'isfile', autospec=True)
@mock.patch.object(filecmp, 'cmp', autospec=True) @mock.patch('ironic.common.utils.file_has_content', autospec=True)
@mock.patch.object(shutil, 'copyfile', 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( 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.provision_state = states.DEPLOYING
self.node.save() self.node.save()
self.config(group='pxe', ipxe_enabled=True) self.config(group='pxe', ipxe_enabled=True)
@ -817,35 +826,40 @@ class PXEBootTestCase(db_base.DbTestCase):
isfile_mock.return_value = True isfile_mock.return_value = True
cmp_mock.return_value = True cmp_mock.return_value = True
self._test_prepare_ramdisk() self._test_prepare_ramdisk()
self.assertFalse(copyfile_mock.called) self.assertFalse(write_mock.called)
@mock.patch.object(shutil, 'copyfile', autospec=True) @mock.patch('ironic.common.utils.write_to_file', autospec=True)
def test_prepare_ramdisk_ipxe_swift(self, copyfile_mock): @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.provision_state = states.DEPLOYING
self.node.save() self.node.save()
self.config(group='pxe', ipxe_enabled=True) self.config(group='pxe', ipxe_enabled=True)
self.config(group='pxe', ipxe_use_swift=True) self.config(group='pxe', ipxe_use_swift=True)
self.config(group='deploy', http_url='http://myserver') self.config(group='deploy', http_url='http://myserver')
render_mock.return_value = 'foo'
self._test_prepare_ramdisk(ipxe_use_swift=True) self._test_prepare_ramdisk(ipxe_use_swift=True)
copyfile_mock.assert_called_once_with( write_mock.assert_called_once_with(
CONF.pxe.ipxe_boot_script,
os.path.join( os.path.join(
CONF.deploy.http_root, 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) @mock.patch('ironic.common.utils.write_to_file', autospec=True)
def test_prepare_ramdisk_ipxe_swift_whole_disk_image(self, copyfile_mock): @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.provision_state = states.DEPLOYING
self.node.save() self.node.save()
self.config(group='pxe', ipxe_enabled=True) self.config(group='pxe', ipxe_enabled=True)
self.config(group='pxe', ipxe_use_swift=True) self.config(group='pxe', ipxe_use_swift=True)
self.config(group='deploy', http_url='http://myserver') 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) self._test_prepare_ramdisk(ipxe_use_swift=True, whole_disk_image=True)
copyfile_mock.assert_called_once_with( write_mock.assert_called_once_with(
CONF.pxe.ipxe_boot_script,
os.path.join( os.path.join(
CONF.deploy.http_root, 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): def test_prepare_ramdisk_cleaning(self):
self.node.provision_state = states.CLEANING self.node.provision_state = states.CLEANING