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
This commit is contained in:
Yibo Cai 2016-05-23 17:58:28 +08:00
parent 85750a1913
commit f8ff1b26be
8 changed files with 216 additions and 12 deletions

View File

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

View File

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

View File

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

View File

@ -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 <None> 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.')),

View File

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

View File

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

View File

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

View File

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