diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index a1eeb3e5ce..1eb794679a 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -2723,6 +2723,11 @@ # configuration for UEFI boot loader. (string value) #uefi_pxe_config_template = $pybasedir/drivers/modules/pxe_grub_config.template +# On ironic-conductor node, template file for PXE +# configuration per node architecture. For example: +# aarch64:/opt/share/grubaa64_pxe_config.template (dict value) +#pxe_config_template_by_arch = + # IP address of ironic-conductor node's TFTP server. (string # value) #tftp_server = $my_ip @@ -2742,6 +2747,10 @@ # Bootfile DHCP parameter for UEFI boot mode. (string value) #uefi_pxe_bootfile_name = bootx64.efi +# Bootfile DHCP parameter per node architecture. For example: +# aarch64:grubaa64.efi (dict value) +#pxe_bootfile_name_by_arch = + # Enable iPXE boot. (boolean value) #ipxe_enabled = false diff --git a/install-guide/source/setup-drivers.rst b/install-guide/source/setup-drivers.rst index 6aa8e52084..c02c2e57e7 100644 --- a/install-guide/source/setup-drivers.rst +++ b/install-guide/source/setup-drivers.rst @@ -354,6 +354,55 @@ on the Bare Metal service node(s) where ``ironic-conductor`` is running. sudo service ironic-conductor restart +PXE Multi-Arch setup +-------------------- + +It is possible to deploy servers of different architecture by one conductor. + +To support this feature, architecture specific boot and template files must +be configured correctly in the options listed below: + +* ``pxe_bootfile_name_by_arch`` +* ``pxe_config_template_by_arch`` + +These two options are dictionary values. Node's ``cpu_arch`` property is used +as the key to find according boot file and template. If according ``cpu_arch`` +is not found in the dictionary, ``pxe_bootfile_name``, ``pxe_config_template``, +``uefi_pxe_bootfile_name`` and ``uefi_pxe_config_template`` are referenced as +usual. + +In below example, x86 and x86_64 nodes will be deployed by bootf1 or bootf2 +based on ``boot_mode`` capability('bios' or 'uefi') as there's no 'x86' or +'x86_64' keys available in ``pxe_bootfile_name_by_arch``. While aarch64 nodes +will be deployed by bootf3, and ppc64 nodes by bootf4:: + + pxe_bootfile_name = bootf1 + uefi_pxe_bootfile_name = bootf2 + pxe_bootfile_name_by_arch = aarch64:bootf3,ppc64:bootf4 + +Following example assumes you are provisioning x86_64 and aarch64 servers, both +in UEFI boot mode. + +Update bootfile and template file configuration parameters 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 + + # Bootfile DHCP parameter per node architecture. (dictionary value) + pxe_bootfile_name_by_arch=aarch64:grubaa64.efi + + # Template file for PXE configuration per node architecture. + # (dictionary value) + pxe_config_template_by_arch=aarch64:pxe_grubaa64_config.template + + Networking service configuration -------------------------------- diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index 5056d67788..69b52738fa 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -206,13 +206,13 @@ def create_pxe_config(task, pxe_options, template=None): :param pxe_options: A dictionary with the PXE configuration parameters. :param template: The PXE configuration template. If no template is - given the CONF.pxe.pxe_config_template will be used. + given the node specific template will be used. """ LOG.debug("Building PXE config for node %s", task.node.uuid) if template is None: - template = CONF.pxe.pxe_config_template + template = deploy_utils.get_pxe_config_template(task.node) _ensure_config_dirs_exist(task.node.uuid) @@ -294,10 +294,7 @@ def dhcp_options_for_instance(task): """ dhcp_opts = [] - if deploy_utils.get_boot_mode_for_deploy(task.node) == 'uefi': - boot_file = CONF.pxe.uefi_pxe_bootfile_name - else: - boot_file = CONF.pxe.pxe_bootfile_name + boot_file = deploy_utils.get_pxe_boot_file(task.node) if CONF.pxe.ipxe_enabled: script_name = os.path.basename(CONF.pxe.ipxe_boot_script) diff --git a/ironic/conf/pxe.py b/ironic/conf/pxe.py index 586e3439c1..355c45f104 100644 --- a/ironic/conf/pxe.py +++ b/ironic/conf/pxe.py @@ -58,6 +58,12 @@ opts = [ 'drivers/modules/pxe_grub_config.template'), help=_('On ironic-conductor node, template file for PXE ' 'configuration for UEFI boot loader.')), + cfg.DictOpt('pxe_config_template_by_arch', + default={}, + help=_('On ironic-conductor node, template file for PXE ' + 'configuration per node architecture. ' + 'For example: ' + 'aarch64:/opt/share/grubaa64_pxe_config.template')), cfg.StrOpt('tftp_server', default='$my_ip', help=_("IP address of ironic-conductor node's TFTP server.")), @@ -71,14 +77,16 @@ opts = [ help=_('On ironic-conductor node, directory where master TFTP ' 'images are stored on disk. ' 'Setting to disables image caching.')), - # NOTE(dekehn): Additional boot files options may be created in the event - # other architectures require different boot files. cfg.StrOpt('pxe_bootfile_name', default='pxelinux.0', help=_('Bootfile DHCP parameter.')), cfg.StrOpt('uefi_pxe_bootfile_name', default='bootx64.efi', help=_('Bootfile DHCP parameter for UEFI boot mode.')), + cfg.DictOpt('pxe_bootfile_name_by_arch', + default={}, + help=_('Bootfile DHCP parameter per node architecture. ' + 'For example: aarch64:grubaa64.efi')), cfg.BoolOpt('ipxe_enabled', default=False, help=_('Enable iPXE boot.')), diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 32bb16deb1..8734517781 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -819,6 +819,48 @@ def get_boot_mode_for_deploy(node): return boot_mode.lower() if boot_mode else boot_mode +def get_pxe_boot_file(node): + """Return the PXE boot file name requested for deploy. + + This method returns PXE boot file name to be used for deploy. + Architecture specific boot file is searched first. BIOS/UEFI + boot file is used if no valid architecture specific file found. + + :param node: A single Node. + :returns: The PXE boot file name. + """ + cpu_arch = node.properties.get('cpu_arch') + boot_file = CONF.pxe.pxe_bootfile_name_by_arch.get(cpu_arch) + if boot_file is None: + if get_boot_mode_for_deploy(node) == 'uefi': + boot_file = CONF.pxe.uefi_pxe_bootfile_name + else: + boot_file = CONF.pxe.pxe_bootfile_name + + return boot_file + + +def get_pxe_config_template(node): + """Return the PXE config template file name requested for deploy. + + This method returns PXE config template file to be used for deploy. + Architecture specific template file is searched first. BIOS/UEFI + template file is used if no valid architecture specific file found. + + :param node: A single Node. + :returns: The PXE config template file name. + """ + cpu_arch = node.properties.get('cpu_arch') + config_template = CONF.pxe.pxe_config_template_by_arch.get(cpu_arch) + if config_template is None: + if get_boot_mode_for_deploy(node) == 'uefi': + config_template = CONF.pxe.uefi_pxe_config_template + else: + config_template = CONF.pxe.pxe_config_template + + return config_template + + def validate_capabilities(node): """Validates that specified supported capabilities have valid value diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index 1860a8a435..ae94378c9f 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -399,10 +399,7 @@ class PXEBoot(base.BootInterface): pxe_options = _build_pxe_config_options(task, pxe_info) pxe_options.update(ramdisk_params) - if deploy_utils.get_boot_mode_for_deploy(node) == 'uefi': - pxe_config_template = CONF.pxe.uefi_pxe_config_template - else: - pxe_config_template = CONF.pxe.pxe_config_template + pxe_config_template = deploy_utils.get_pxe_config_template(node) pxe_utils.create_pxe_config(task, pxe_options, pxe_config_template) diff --git a/ironic/tests/unit/drivers/modules/test_deploy_utils.py b/ironic/tests/unit/drivers/modules/test_deploy_utils.py index 5f109adc4e..5bd0eba6ee 100644 --- a/ironic/tests/unit/drivers/modules/test_deploy_utils.py +++ b/ironic/tests/unit/drivers/modules/test_deploy_utils.py @@ -1281,6 +1281,95 @@ class SwitchPxeConfigTestCase(tests_base.TestCase): self.assertEqual(_IPXECONF_BOOT_WHOLE_DISK, pxeconf) +class GetPxeBootConfigTestCase(db_base.DbTestCase): + + def setUp(self): + super(GetPxeBootConfigTestCase, self).setUp() + self.node = obj_utils.get_test_node(self.context, driver='fake') + self.config(pxe_bootfile_name='bios-bootfile', group='pxe') + self.config(uefi_pxe_bootfile_name='uefi-bootfile', group='pxe') + self.config(pxe_config_template='bios-template', group='pxe') + self.config(uefi_pxe_config_template='uefi-template', group='pxe') + self.bootfile_by_arch = {'aarch64': 'aarch64-bootfile', + 'ppc64': 'ppc64-bootfile'} + self.template_by_arch = {'aarch64': 'aarch64-template', + 'ppc64': 'ppc64-template'} + + def test_get_pxe_boot_file_bios_without_by_arch(self): + properties = {'cpu_arch': 'x86', 'capabilities': 'boot_mode:bios'} + self.node.properties = properties + self.config(pxe_bootfile_name_by_arch={}, group='pxe') + result = utils.get_pxe_boot_file(self.node) + self.assertEqual('bios-bootfile', result) + + def test_get_pxe_config_template_bios_without_by_arch(self): + properties = {'cpu_arch': 'x86', 'capabilities': 'boot_mode:bios'} + self.node.properties = properties + self.config(pxe_config_template_by_arch={}, group='pxe') + result = utils.get_pxe_config_template(self.node) + self.assertEqual('bios-template', result) + + def test_get_pxe_boot_file_uefi_without_by_arch(self): + properties = {'cpu_arch': 'x86_64', 'capabilities': 'boot_mode:uefi'} + self.node.properties = properties + self.config(pxe_bootfile_name_by_arch={}, group='pxe') + result = utils.get_pxe_boot_file(self.node) + self.assertEqual('uefi-bootfile', result) + + def test_get_pxe_config_template_uefi_without_by_arch(self): + properties = {'cpu_arch': 'x86_64', 'capabilities': 'boot_mode:uefi'} + self.node.properties = properties + self.config(pxe_config_template_by_arch={}, group='pxe') + result = utils.get_pxe_config_template(self.node) + self.assertEqual('uefi-template', result) + + def test_get_pxe_boot_file_cpu_not_in_by_arch(self): + properties = {'cpu_arch': 'x86', 'capabilities': 'boot_mode:bios'} + self.node.properties = properties + self.config(pxe_bootfile_name_by_arch=self.bootfile_by_arch, + group='pxe') + result = utils.get_pxe_boot_file(self.node) + self.assertEqual('bios-bootfile', result) + + def test_get_pxe_config_template_cpu_not_in_by_arch(self): + properties = {'cpu_arch': 'x86', 'capabilities': 'boot_mode:bios'} + self.node.properties = properties + self.config(pxe_config_template_by_arch=self.template_by_arch, + group='pxe') + result = utils.get_pxe_config_template(self.node) + self.assertEqual('bios-template', result) + + def test_get_pxe_boot_file_cpu_in_by_arch(self): + properties = {'cpu_arch': 'aarch64', 'capabilities': 'boot_mode:uefi'} + self.node.properties = properties + self.config(pxe_bootfile_name_by_arch=self.bootfile_by_arch, + group='pxe') + result = utils.get_pxe_boot_file(self.node) + self.assertEqual('aarch64-bootfile', result) + + def test_get_pxe_config_template_cpu_in_by_arch(self): + properties = {'cpu_arch': 'aarch64', 'capabilities': 'boot_mode:uefi'} + self.node.properties = properties + self.config(pxe_config_template_by_arch=self.template_by_arch, + group='pxe') + result = utils.get_pxe_config_template(self.node) + self.assertEqual('aarch64-template', result) + + def test_get_pxe_boot_file_emtpy_property(self): + self.node.properties = {} + self.config(pxe_bootfile_name_by_arch=self.bootfile_by_arch, + group='pxe') + result = utils.get_pxe_boot_file(self.node) + self.assertEqual('bios-bootfile', result) + + def test_get_pxe_config_template_emtpy_property(self): + self.node.properties = {} + self.config(pxe_config_template_by_arch=self.template_by_arch, + group='pxe') + result = utils.get_pxe_config_template(self.node) + self.assertEqual('bios-template', result) + + @mock.patch('time.sleep', lambda sec: None) class OtherFunctionTestCase(db_base.DbTestCase): diff --git a/releasenotes/notes/multi-arch-deploy-bcf840107fc94bef.yaml b/releasenotes/notes/multi-arch-deploy-bcf840107fc94bef.yaml new file mode 100644 index 0000000000..ff5b4a44cd --- /dev/null +++ b/releasenotes/notes/multi-arch-deploy-bcf840107fc94bef.yaml @@ -0,0 +1,13 @@ +--- +features: + - Support multi architecture deployment. E.g., to + deploy x86_64, aarch64 servers by one ironic conductor. + Two new config options, ``pxe_config_template_by_arch`` + and ``pxe_bootfile_name_by_arch``, are introduced to + support multi architecture deployment. They are + dictionary values to hold pxe config templates and + boot files for multiple architectures, with cpu_arch + property per node as the key. If cpu_arch is not found + in dictionary, options ``pxe_config_template``, + ``pxe_bootfile_name``, ``uefi_pxe_config_template``, + ``uefi_pxe_bootfile_name`` will be used as usual.