diff --git a/doc/source/deploy/install-guide.rst b/doc/source/deploy/install-guide.rst index 0cef347b7a..8dec80b506 100644 --- a/doc/source/deploy/install-guide.rst +++ b/doc/source/deploy/install-guide.rst @@ -724,8 +724,10 @@ node(s) where ``ironic-conductor`` is running. #. Create a map file in the tftp boot directory (``/tftpboot``):: - echo 'r ^([^/]) /tftpboot/\1' > /tftpboot/map-file - echo 'r ^(/tftpboot/) /tftpboot/\2' >> /tftpboot/map-file + echo 're ^(/tftpboot/) /tftpboot/\2' > /tftpboot/map-file + echo 're ^/tftpboot/ /tftpboot/' >> /tftpboot/map-file + echo 're ^(^/) /tftpboot/\1' >> /tftpboot/map-file + echo 're ^([^/]) /tftpboot/\1' >> /tftpboot/map-file #. Enable tftp map file, modify ``/etc/xinetd.d/tftp`` as below and restart xinetd service:: @@ -754,6 +756,67 @@ steps on the Ironic conductor node to configure PXE UEFI environment. sudo cp ./elilo-3.16-x86_64.efi /tftpboot/elilo.efi +#. Grub2 is an alternate UEFI bootloader supported in Ironic. Install grub2 and + shim packages:: + + Ubuntu: (14.04LTS and later) + sudo apt-get install grub-efi-amd64-signed shim-signed + + Fedora: (21 and later) + CentOS: (7 and later) + sudo yum install grub2-efi shim + +#. Copy grub and shim boot loader images to ``/tftpboot`` directory:: + + Ubuntu: (14.04LTS and later) + sudo cp /usr/lib/shim/shim.efi.signed /tftpboot/bootx64.efi + sudo cp /usr/lib/grub/x86_64-efi-signed/grubnetx64.efi.signed \ + /tftpboot/grubx64.efi + + Fedora: (21 and later) + sudo cp /boot/efi/EFI/fedora/shim.efi /tftpboot/bootx64.efi + sudo cp /boot/efi/EFI/fedora/grubx64.efi /tftpboot/grubx64.efi + + CentOS: (7 and later) + sudo cp /boot/efi/EFI/centos/shim.efi /tftpboot/bootx64.efi + sudo cp /boot/efi/EFI/centos/grubx64.efi /tftpboot/grubx64.efi + +#. Create master grub.cfg:: + + Ubuntu: Create grub.cfg under ``/tftpboot/grub`` directory. + GRUB_DIR=/tftpboot/grub + + Fedora: Create grub.cfg under ``/tftpboot/EFI/fedora`` directory. + GRUB_DIR=/tftpboot/EFI/fedora + + CentOS: Create grub.cfg under ``/tftpboot/EFI/centos`` directory. + GRUB_DIR=/tftpboot/EFI/centos + + Create directory GRUB_DIR + sudo mkdir $GRUB_DIR + + This file is used to redirect grub to baremetal node specific config file. + It redirects it to specific grub config file based on DHCP IP assigned to + baremetal node. + + .. literalinclude:: ../../../ironic/drivers/modules/master_grub_cfg.txt + + Change the permission of grub.cfg:: + + sudo chmod 644 $GRUB_DIR/grub.cfg + +#. Update bootfile and template file configuration parameters for UEFI PXE boot + in the Bare Metal Service's configuration file (/etc/ironic/ironic.conf):: + + [pxe] + + # Bootfile DHCP parameter for UEFI boot mode. (string value) + uefi_pxe_bootfile_name=bootx64.efi + + # Template file for PXE configuration for UEFI boot loader. + # (string value) + uefi_pxe_config_template=$pybasedir/drivers/modules/pxe_grub_config.template + #. Update the Ironic node with ``boot_mode`` capability in node's properties field:: diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index 10c5217f1e..fe5ed1cdcd 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -54,7 +54,7 @@ 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): +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 @@ -62,6 +62,8 @@ def _build_pxe_config(pxe_options, template): :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. """ @@ -69,8 +71,8 @@ def _build_pxe_config(pxe_options, template): env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path)) template = env.get_template(tmpl_file) return template.render({'pxe_options': pxe_options, - 'ROOT': '{{ ROOT }}', - 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}', + 'ROOT': root_tag, + 'DISK_IDENTIFIER': disk_ident_tag, }) @@ -95,10 +97,12 @@ def _link_mac_pxe_configs(task): create_link(_get_pxe_mac_path(mac, delimiter='')) -def _link_ip_address_pxe_configs(task): +def _link_ip_address_pxe_configs(task, hex_form): """Link each IP address with the PXE configuration file. :param task: A TaskManager instance. + :param hex_form: Boolean value indicating if the conf file name should be + hexadecimal equivalent of supplied ipv4 address. :raises: FailedToGetIPAddressOnPort :raises: InvalidIPv4Address @@ -112,7 +116,8 @@ def _link_ip_address_pxe_configs(task): "Failed to get IP address for any port on node %s.") % task.node.uuid) for port_ip_address in ip_addrs: - ip_address_path = _get_pxe_ip_address_path(port_ip_address) + ip_address_path = _get_pxe_ip_address_path(port_ip_address, + hex_form) utils.unlink_without_raise(ip_address_path) utils.create_link_without_raise(pxe_config_file_path, ip_address_path) @@ -136,18 +141,23 @@ def _get_pxe_mac_path(mac, delimiter=None): return os.path.join(get_root_dir(), PXE_CFG_DIR_NAME, mac_file_name) -def _get_pxe_ip_address_path(ip_address): +def _get_pxe_ip_address_path(ip_address, hex_form): """Convert an ipv4 address into a PXE config file name. :param ip_address: A valid IPv4 address string in the format 'n.n.n.n'. + :param hex_form: Boolean value indicating if the conf file name should be + hexadecimal equivalent of supplied ipv4 address. :returns: the path to the config file. """ - ip = ip_address.split('.') - hex_ip = '{0:02X}{1:02X}{2:02X}{3:02X}'.format(*map(int, ip)) + # elilo bootloader needs hex based config file name. + if hex_form: + ip = ip_address.split('.') + ip_address = '{0:02X}{1:02X}{2:02X}{3:02X}'.format(*map(int, ip)) + # grub2 bootloader needs ip based config file name. return os.path.join( - CONF.pxe.tftp_root, hex_ip + ".conf" + CONF.pxe.tftp_root, ip_address + ".conf" ) @@ -181,9 +191,14 @@ def create_pxe_config(task, pxe_options, template=None): This method will generate the PXE configuration file for the task's node under a directory named with the UUID of that node. For each - MAC address (port) of that node, a symlink for the configuration file - will be created under the PXE configuration directory, so regardless - of which port boots first they'll get the same PXE configuration. + MAC address or DHCP IP address (port) of that node, a symlink for + the configuration file will be created under the PXE configuration + directory, so regardless of which port boots first they'll get the + same PXE configuration. + If elilo is the bootloader in use, then its configuration file will + be created based on hex form of DHCP IP address. + If grub2 bootloader is in use, then its configuration will be created + based on DHCP IP address in the form nn.nn.nn.nn. :param task: A TaskManager instance. :param pxe_options: A dictionary with the PXE configuration @@ -200,11 +215,32 @@ def create_pxe_config(task, pxe_options, template=None): _ensure_config_dirs_exist(task.node.uuid) pxe_config_file_path = get_pxe_config_file_path(task.node.uuid) - pxe_config = _build_pxe_config(pxe_options, template) + is_uefi_boot_mode = (deploy_utils.get_boot_mode_for_deploy(task.node) == + 'uefi') + + # grub bootloader panics with '{}' around any of its tags in its + # config file. To overcome that 'ROOT' and 'DISK_IDENTIFIER' are enclosed + # with '(' and ')' in uefi boot mode. + # These changes do not have any impact on elilo bootloader. + hex_form = True + if is_uefi_boot_mode and utils.is_regex_string_in_file(template, + '^menuentry'): + hex_form = False + pxe_config_root_tag = '(( ROOT ))' + pxe_config_disk_ident = '(( DISK_IDENTIFIER ))' + else: + # TODO(stendulker): We should use '(' ')' as the delimiters for all our + # config files so that we do not need special handling for each of the + # bootloaders. Should be removed once the M release starts. + 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) utils.write_to_file(pxe_config_file_path, pxe_config) - if deploy_utils.get_boot_mode_for_deploy(task.node) == 'uefi': - _link_ip_address_pxe_configs(task) + if is_uefi_boot_mode: + _link_ip_address_pxe_configs(task, hex_form) else: _link_mac_pxe_configs(task) @@ -225,10 +261,18 @@ def clean_up_pxe_config(task): for port_ip_address in ip_addresses: try: - ip_address_path = _get_pxe_ip_address_path(port_ip_address) + # Get xx.xx.xx.xx based grub config file + ip_address_path = _get_pxe_ip_address_path(port_ip_address, + False) + # Get 0AOAOAOA based elilo config file + hex_ip_path = _get_pxe_ip_address_path(port_ip_address, + True) except exception.InvalidIPv4Address: continue + # Cleaning up config files created for grub2. utils.unlink_without_raise(ip_address_path) + # Cleaning up config files created for elilo. + utils.unlink_without_raise(hex_ip_path) else: for mac in driver_utils.get_node_mac_addresses(task): utils.unlink_without_raise(_get_pxe_mac_path(mac)) diff --git a/ironic/common/utils.py b/ironic/common/utils.py index 4e1576f66f..756702461a 100644 --- a/ironic/common/utils.py +++ b/ironic/common/utils.py @@ -653,3 +653,8 @@ def get_updated_capabilities(current_capabilities, new_capabilities): cap_dict.update(new_capabilities) return ','.join('%(key)s:%(value)s' % {'key': key, 'value': value} for key, value in six.iteritems(cap_dict)) + + +def is_regex_string_in_file(path, string): + with open(path, 'r') as inf: + return any(re.search(string, line) for line in inf.readlines()) diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 87079700f2..fb802b8254 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -364,7 +364,7 @@ def _replace_lines_in_file(path, regex_pattern, replacement): def _replace_root_uuid(path, root_uuid): root = 'UUID=%s' % root_uuid - pattern = r'\{\{ ROOT \}\}' + pattern = r'(\(\(|\{\{) ROOT (\)\)|\}\})' _replace_lines_in_file(path, pattern, root) @@ -378,8 +378,8 @@ def _replace_boot_line(path, boot_mode, is_whole_disk_image, boot_disk_type = 'boot_partition' if boot_mode == 'uefi': - pattern = '^default=.*$' - boot_line = 'default=%s' % boot_disk_type + pattern = '^((set )?default)=.*$' + boot_line = '\\1=%s' % boot_disk_type else: pxe_cmd = 'goto' if CONF.pxe.ipxe_enabled else 'default' pattern = '^%s .*$' % pxe_cmd @@ -389,7 +389,7 @@ def _replace_boot_line(path, boot_mode, is_whole_disk_image, def _replace_disk_identifier(path, disk_identifier): - pattern = r'\{\{ DISK_IDENTIFIER \}\}' + pattern = r'(\(\(|\{\{) DISK_IDENTIFIER (\)\)|\}\})' _replace_lines_in_file(path, pattern, disk_identifier) diff --git a/ironic/drivers/modules/master_grub_cfg.txt b/ironic/drivers/modules/master_grub_cfg.txt new file mode 100644 index 0000000000..14d506d477 --- /dev/null +++ b/ironic/drivers/modules/master_grub_cfg.txt @@ -0,0 +1,7 @@ +set default=master +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "master" { +configfile /tftpboot/$net_default_ip.conf +} diff --git a/ironic/drivers/modules/pxe_grub_config.template b/ironic/drivers/modules/pxe_grub_config.template new file mode 100644 index 0000000000..bee7920f99 --- /dev/null +++ b/ironic/drivers/modules/pxe_grub_config.template @@ -0,0 +1,17 @@ +set default=deploy +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "deploy" { + linuxefi {{ pxe_options.deployment_aki_path }} selinux=0 troubleshoot=0 text disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} {{ pxe_options.pxe_append_params|default("", true) }} boot_server={{pxe_options.tftp_server}} {% if pxe_options.root_device %}root_device={{ pxe_options.root_device }}{% endif %} ipa-api-url={{ pxe_options['ipa-api-url'] }} ipa-driver-name={{ pxe_options['ipa-driver-name'] }} boot_option={{ pxe_options.boot_option }} boot_mode={{ pxe_options['boot_mode'] }} coreos.configdrive=0 + initrdefi {{ pxe_options.deployment_ari_path }} +} + +menuentry "boot_partition" { + linuxefi {{ pxe_options.aki_path }} root={{ ROOT }} ro text {{ pxe_options.pxe_append_params|default("", true) }} boot_server={{pxe_options.tftp_server}} + initrdefi {{ pxe_options.ari_path }} +} + +menuentry "boot_whole_disk" { + linuxefi chain.c32 mbr:{{ DISK_IDENTIFIER }} +} diff --git a/ironic/tests/drivers/pxe_grub_config.template b/ironic/tests/drivers/pxe_grub_config.template new file mode 100644 index 0000000000..96444c1253 --- /dev/null +++ b/ironic/tests/drivers/pxe_grub_config.template @@ -0,0 +1,18 @@ +set default=deploy +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "deploy" { + linuxefi /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/deploy_kernel selinux=0 troubleshoot=0 text disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn=iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_id=1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_key=0123456789ABCDEFGHIJKLMNOPQRSTUV ironic_api_url=http://192.168.122.184:6385 test_param boot_server=192.0.2.1 root_device=vendor=fake,size=123 ipa-api-url=http://192.168.122.184:6385 ipa-driver-name=pxe_ssh boot_option=netboot boot_mode=uefi coreos.configdrive=0 + initrdefi /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/deploy_ramdisk +} + +menuentry "boot_partition" { + linuxefi /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/kernel root=(( ROOT )) ro text test_param boot_server=192.0.2.1 + initrdefi /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk +} + +menuentry "boot_whole_disk" { + linuxefi chain.c32 mbr:(( DISK_IDENTIFIER )) +} + diff --git a/ironic/tests/drivers/test_deploy_utils.py b/ironic/tests/drivers/test_deploy_utils.py index 6fd9f3d486..3268bb9a52 100644 --- a/ironic/tests/drivers/test_deploy_utils.py +++ b/ironic/tests/drivers/test_deploy_utils.py @@ -262,6 +262,66 @@ image=chain.c32 append="mbr:0x12345678" """ +_UEFI_PXECONF_DEPLOY_GRUB = b""" +set default=deploy +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "deploy" { + linuxefi deploy_kernel "ro text" + initrdefi deploy_ramdisk +} + +menuentry "boot_partition" { + linuxefi kernel "root=(( ROOT ))" + initrdefi ramdisk +} + +menuentry "boot_whole_disk" { + linuxefi chain.c32 mbr:(( DISK_IDENTIFIER )) +} +""" + +_UEFI_PXECONF_BOOT_PARTITION_GRUB = """ +set default=boot_partition +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "deploy" { + linuxefi deploy_kernel "ro text" + initrdefi deploy_ramdisk +} + +menuentry "boot_partition" { + linuxefi kernel "root=UUID=12345678-1234-1234-1234-1234567890abcdef" + initrdefi ramdisk +} + +menuentry "boot_whole_disk" { + linuxefi chain.c32 mbr:(( DISK_IDENTIFIER )) +} +""" + +_UEFI_PXECONF_BOOT_WHOLE_DISK_GRUB = """ +set default=boot_whole_disk +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "deploy" { + linuxefi deploy_kernel "ro text" + initrdefi deploy_ramdisk +} + +menuentry "boot_partition" { + linuxefi kernel "root=(( ROOT ))" + initrdefi ramdisk +} + +menuentry "boot_whole_disk" { + linuxefi chain.c32 mbr:0x12345678 +} +""" + @mock.patch.object(time, 'sleep', lambda seconds: None) class PhysicalWorkTestCase(tests_base.TestCase): @@ -922,10 +982,13 @@ class PhysicalWorkTestCase(tests_base.TestCase): class SwitchPxeConfigTestCase(tests_base.TestCase): - def _create_config(self, ipxe=False, boot_mode=None): + def _create_config(self, ipxe=False, boot_mode=None, boot_loader='elilo'): (fd, fname) = tempfile.mkstemp() if boot_mode == 'uefi': - pxe_cfg = _UEFI_PXECONF_DEPLOY + if boot_loader == 'grub': + pxe_cfg = _UEFI_PXECONF_DEPLOY_GRUB + else: + pxe_cfg = _UEFI_PXECONF_DEPLOY else: pxe_cfg = _IPXECONF_DEPLOY if ipxe else _PXECONF_DEPLOY os.write(fd, pxe_cfg) @@ -990,7 +1053,7 @@ class SwitchPxeConfigTestCase(tests_base.TestCase): pxeconf = f.read() self.assertEqual(_IPXECONF_BOOT_WHOLE_DISK, pxeconf) - def test_switch_uefi_pxe_config_partition_image(self): + def test_switch_uefi_elilo_pxe_config_partition_image(self): boot_mode = 'uefi' fname = self._create_config(boot_mode=boot_mode) utils.switch_pxe_config(fname, @@ -1001,7 +1064,7 @@ class SwitchPxeConfigTestCase(tests_base.TestCase): pxeconf = f.read() self.assertEqual(_UEFI_PXECONF_BOOT_PARTITION, pxeconf) - def test_switch_uefi_config_whole_disk_image(self): + def test_switch_uefi_elilo_config_whole_disk_image(self): boot_mode = 'uefi' fname = self._create_config(boot_mode=boot_mode) utils.switch_pxe_config(fname, @@ -1012,6 +1075,28 @@ class SwitchPxeConfigTestCase(tests_base.TestCase): pxeconf = f.read() self.assertEqual(_UEFI_PXECONF_BOOT_WHOLE_DISK, pxeconf) + def test_switch_uefi_grub_pxe_config_partition_image(self): + boot_mode = 'uefi' + fname = self._create_config(boot_mode=boot_mode, boot_loader='grub') + utils.switch_pxe_config(fname, + '12345678-1234-1234-1234-1234567890abcdef', + boot_mode, + False) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_UEFI_PXECONF_BOOT_PARTITION_GRUB, pxeconf) + + def test_switch_uefi_grub_config_whole_disk_image(self): + boot_mode = 'uefi' + fname = self._create_config(boot_mode=boot_mode, boot_loader='grub') + utils.switch_pxe_config(fname, + '0x12345678', + boot_mode, + True) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_UEFI_PXECONF_BOOT_WHOLE_DISK_GRUB, pxeconf) + @mock.patch('time.sleep', lambda sec: None) class OtherFunctionTestCase(db_base.DbTestCase): diff --git a/ironic/tests/test_pxe_utils.py b/ironic/tests/test_pxe_utils.py index 45d6d4bdfe..db8d866bea 100644 --- a/ironic/tests/test_pxe_utils.py +++ b/ironic/tests/test_pxe_utils.py @@ -58,10 +58,19 @@ class TestPXEUtils(db_base.DbTestCase): 'disk': 'cciss/c0d0,sda,hda,vda', 'boot_option': 'netboot', 'ipa-driver-name': 'pxe_ssh', - 'boot_mode': 'bios', } self.pxe_options.update(common_pxe_options) + self.pxe_options_bios = { + 'boot_mode': 'bios', + } + self.pxe_options_bios.update(self.pxe_options) + + self.pxe_options_uefi = { + 'boot_mode': 'uefi', + } + self.pxe_options_uefi.update(self.pxe_options) + self.agent_pxe_options = { 'ipa-driver-name': 'agent_ipmitool', } @@ -80,7 +89,8 @@ class TestPXEUtils(db_base.DbTestCase): def test__build_pxe_config(self): rendered_template = pxe_utils._build_pxe_config( - self.pxe_options, CONF.pxe.pxe_config_template) + self.pxe_options_bios, CONF.pxe.pxe_config_template, + '{{ ROOT }}', '{{ DISK_IDENTIFIER }}') expected_template = open( 'ironic/tests/drivers/pxe_config.template').read().rstrip() @@ -90,7 +100,8 @@ class TestPXEUtils(db_base.DbTestCase): def test__build_pxe_config_with_agent(self): rendered_template = pxe_utils._build_pxe_config( - self.agent_pxe_options, CONF.agent.agent_pxe_config_template) + self.agent_pxe_options, CONF.agent.agent_pxe_config_template, + '{{ ROOT }}', '{{ DISK_IDENTIFIER }}') expected_template = open( 'ironic/tests/drivers/agent_pxe_config.template').read().rstrip() @@ -108,7 +119,8 @@ class TestPXEUtils(db_base.DbTestCase): ) 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) + self.ipxe_options, CONF.pxe.pxe_config_template, + '{{ ROOT }}', '{{ DISK_IDENTIFIER }}') expected_template = open( 'ironic/tests/drivers/ipxe_config.template').read().rstrip() @@ -119,7 +131,8 @@ class TestPXEUtils(db_base.DbTestCase): pxe_opts = self.pxe_options pxe_opts['boot_mode'] = 'uefi' rendered_template = pxe_utils._build_pxe_config( - pxe_opts, CONF.pxe.uefi_pxe_config_template) + pxe_opts, CONF.pxe.uefi_pxe_config_template, + '{{ ROOT }}', '{{ DISK_IDENTIFIER }}') expected_template = open( 'ironic/tests/drivers/elilo_efi_pxe_config.template' @@ -127,6 +140,19 @@ class TestPXEUtils(db_base.DbTestCase): self.assertEqual(six.text_type(expected_template), rendered_template) + def test__build_grub_config(self): + pxe_opts = self.pxe_options + pxe_opts['boot_mode'] = 'uefi' + pxe_opts['tftp_server'] = '192.0.2.1' + grub_tmplte = "ironic/drivers/modules/pxe_grub_config.template" + rendered_template = pxe_utils._build_pxe_config( + pxe_opts, grub_tmplte, '(( ROOT ))', '(( DISK_IDENTIFIER ))') + + expected_template = open( + 'ironic/tests/drivers/pxe_grub_config.template').read().rstrip() + + self.assertEqual(six.text_type(expected_template), rendered_template) + @mock.patch('ironic.common.utils.create_link_without_raise', autospec=True) @mock.patch('ironic.common.utils.unlink_without_raise', autospec=True) @mock.patch('ironic.drivers.utils.get_node_mac_addresses', autospec=True) @@ -200,12 +226,12 @@ class TestPXEUtils(db_base.DbTestCase): provider_mock.get_ip_addresses.return_value = [ip_address] create_link_calls = [ mock.call(u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/config', - u'/tftpboot/0A0A0001.conf'), + u'/tftpboot/10.10.0.1.conf'), ] with task_manager.acquire(self.context, self.node.uuid) as task: - pxe_utils._link_ip_address_pxe_configs(task) + pxe_utils._link_ip_address_pxe_configs(task, False) - unlink_mock.assert_called_once_with('/tftpboot/0A0A0001.conf') + unlink_mock.assert_called_once_with('/tftpboot/10.10.0.1.conf') create_link_mock.assert_has_calls(create_link_calls) @mock.patch('ironic.common.utils.write_to_file', autospec=True) @@ -213,12 +239,14 @@ class TestPXEUtils(db_base.DbTestCase): @mock.patch('oslo_utils.fileutils.ensure_tree', autospec=True) def test_create_pxe_config(self, ensure_tree_mock, build_mock, write_mock): - build_mock.return_value = self.pxe_options + build_mock.return_value = self.pxe_options_bios 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_bios, CONF.pxe.pxe_config_template) - build_mock.assert_called_with(self.pxe_options, - CONF.pxe.pxe_config_template) + build_mock.assert_called_with(self.pxe_options_bios, + CONF.pxe.pxe_config_template, + '{{ ROOT }}', + '{{ 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')) @@ -226,7 +254,62 @@ 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, self.pxe_options_bios) + + @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('oslo_utils.fileutils.ensure_tree', autospec=True) + def test_create_pxe_config_uefi_elilo(self, ensure_tree_mock, build_mock, + write_mock, link_ip_configs_mock): + build_mock.return_value = self.pxe_options_uefi + 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_uefi, + CONF.pxe.uefi_pxe_config_template) + + 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')) + ] + ensure_tree_mock.assert_has_calls(ensure_calls) + build_mock.assert_called_with(self.pxe_options_uefi, + CONF.pxe.uefi_pxe_config_template, + '{{ ROOT }}', + '{{ 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_uefi) + + @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('oslo_utils.fileutils.ensure_tree', autospec=True) + def test_create_pxe_config_uefi_grub(self, ensure_tree_mock, build_mock, + write_mock, link_ip_configs_mock): + build_mock.return_value = self.pxe_options_uefi + 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' + pxe_utils.create_pxe_config(task, self.pxe_options_uefi, + grub_tmplte) + + 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')) + ] + ensure_tree_mock.assert_has_calls(ensure_calls) + build_mock.assert_called_with(self.pxe_options_uefi, + grub_tmplte, + '(( ROOT ))', + '(( 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_uefi) @mock.patch('ironic.common.utils.rmtree_without_raise', autospec=True) @mock.patch('ironic.common.utils.unlink_without_raise', autospec=True) @@ -257,8 +340,8 @@ class TestPXEUtils(db_base.DbTestCase): def test__get_pxe_ip_address_path(self): ipaddress = '10.10.0.1' - self.assertEqual('/tftpboot/0A0A0001.conf', - pxe_utils._get_pxe_ip_address_path(ipaddress)) + self.assertEqual('/tftpboot/10.10.0.1.conf', + pxe_utils._get_pxe_ip_address_path(ipaddress, False)) def test_get_root_dir(self): expected_dir = '/tftproot' @@ -381,7 +464,11 @@ class TestPXEUtils(db_base.DbTestCase): task.node.properties = properties pxe_utils.clean_up_pxe_config(task) - unlink_mock.assert_called_once_with('/tftpboot/0A0A0001.conf') + unlink_calls = [ + mock.call('/tftpboot/10.10.0.1.conf'), + mock.call('/tftpboot/0A0A0001.conf') + ] + unlink_mock.assert_has_calls(unlink_calls) rmtree_mock.assert_called_once_with( os.path.join(CONF.pxe.tftp_root, self.node.uuid)) @@ -402,6 +489,10 @@ class TestPXEUtils(db_base.DbTestCase): task.node.instance_info['deploy_boot_mode'] = 'uefi' pxe_utils.clean_up_pxe_config(task) - unlink_mock.assert_called_once_with('/tftpboot/0A0A0001.conf') + unlink_calls = [ + mock.call('/tftpboot/10.10.0.1.conf'), + mock.call('/tftpboot/0A0A0001.conf') + ] + unlink_mock.assert_has_calls(unlink_calls) rmtree_mock.assert_called_once_with( os.path.join(CONF.pxe.tftp_root, self.node.uuid))