From f8ff1b26beb7c386429d1ddb2f869a225cc71d8d Mon Sep 17 00:00:00 2001 From: Yibo Cai Date: Mon, 23 May 2016 17:58:28 +0800 Subject: [PATCH] Support multi arch deployment Ironic is flexible for x86/x86_64 servers by supporting BIOS and UEFI. But to deploy servers of other architectures, such as aarch64 or ppc64, configuration(PXE boot file and config template) must be modified, which means one Ironic conductor can only deploy baremetal machines of one architecture. This patch adds multi arch deployment support. For example, to deploy x86_64 and aarch64 servers by one Ironic conductor. Closes-Bug: #1582964 Change-Id: I628320aeb44b232a262d0843bc726a68d297e1f8 --- etc/ironic/ironic.conf.sample | 9 ++ install-guide/source/setup-drivers.rst | 49 ++++++++++ ironic/common/pxe_utils.py | 9 +- ironic/conf/pxe.py | 12 ++- ironic/drivers/modules/deploy_utils.py | 42 +++++++++ ironic/drivers/modules/pxe.py | 5 +- .../unit/drivers/modules/test_deploy_utils.py | 89 +++++++++++++++++++ .../multi-arch-deploy-bcf840107fc94bef.yaml | 13 +++ 8 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/multi-arch-deploy-bcf840107fc94bef.yaml 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 c72b451713..57bbde1a8e 100644 --- a/install-guide/source/setup-drivers.rst +++ b/install-guide/source/setup-drivers.rst @@ -306,6 +306,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 683d60b465..d88d096c40 100644 --- a/ironic/tests/unit/drivers/modules/test_deploy_utils.py +++ b/ironic/tests/unit/drivers/modules/test_deploy_utils.py @@ -1247,6 +1247,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.