Add UEFI based deployment support in Ironic

Most of the new hardware comes with UEFI boot mode, which has several
technical advantages over the traditional BIOS system.

Operator inform the boot mode to ironic using the "capabilities" property
of the node.  The operator may add a new capability "boot_mode=uefi" or
"boot_mode=bios" in "capabilities" within "properties" of the node.

Add new pxe config options:
  - "uefi_pxe_bootfile_name": specify the efi bootloader to be used.
  - "uefi_pxe_config_template": specify the respective efi bootloader config
    template.

As of now only elilo.efi bootloader is supported.

elilo.efi bootloader requires the configuration file to be named after the
ip-address assigned by the DHCP server.

Implements: blueprint uefi-boot-for-ironic
Co-Authored-By: Jim Rollenhagen <jim@jimrollenhagen.com>
Change-Id: I0ad399b2207d7c66f6887e56470ba553b3c87b53
This commit is contained in:
Faizan Barmawer 2014-08-14 16:55:44 -04:00 committed by Jim Rollenhagen
parent b56db42aa3
commit 1718a5f32e
15 changed files with 585 additions and 29 deletions

View File

@ -1113,6 +1113,10 @@
# Template file for PXE configuration. (string value) # Template file for PXE configuration. (string value)
#pxe_config_template=$pybasedir/drivers/modules/pxe_config.template #pxe_config_template=$pybasedir/drivers/modules/pxe_config.template
# Template file for PXE configuration for UEFI boot loader.
# (string value)
#uefi_pxe_config_template=$pybasedir/drivers/modules/elilo_efi_pxe_config.template
# IP address of Ironic compute node's tftp server. (string # IP address of Ironic compute node's tftp server. (string
# value) # value)
#tftp_server=$my_ip #tftp_server=$my_ip
@ -1127,6 +1131,9 @@
# Bootfile DHCP parameter. (string value) # Bootfile DHCP parameter. (string value)
#pxe_bootfile_name=pxelinux.0 #pxe_bootfile_name=pxelinux.0
# Bootfile DHCP parameter for UEFI boot mode. (string value)
#uefi_pxe_bootfile_name=elilo.efi
# Ironic compute node's HTTP server URL. Example: # Ironic compute node's HTTP server URL. Example:
# http://192.1.2.3:8080 (string value) # http://192.1.2.3:8080 (string value)
#http_url=<None> #http_url=<None>

View File

@ -226,6 +226,14 @@ class FailedToUpdateDHCPOptOnPort(IronicException):
message = _("Update DHCP options on port: %(port_id)s failed.") message = _("Update DHCP options on port: %(port_id)s failed.")
class FailedToGetIPAddressOnPort(IronicException):
message = _("Retrieve IP address on port: %(port_id)s failed.")
class InvalidIPv4Address(IronicException):
message = _("Invalid IPv4 address %(ip_address)s.")
class FailedToUpdateMacOnPort(IronicException): class FailedToUpdateMacOnPort(IronicException):
message = _("Update MAC address on port: %(port_id)s failed.") message = _("Update MAC address on port: %(port_id)s failed.")

View File

@ -19,11 +19,18 @@ import os
import jinja2 import jinja2
from oslo.config import cfg from oslo.config import cfg
from ironic.common import dhcp_factory
from ironic.common import exception
from ironic.common import i18n
from ironic.common.i18n import _
from ironic.common import utils from ironic.common import utils
from ironic.drivers import utils as driver_utils from ironic.drivers import utils as driver_utils
from ironic.openstack.common import fileutils from ironic.openstack.common import fileutils
from ironic.openstack.common import log as logging from ironic.openstack.common import log as logging
_LW = i18n._LW
_LE = i18n._LE
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -81,6 +88,29 @@ def _link_mac_pxe_configs(task):
utils.create_link_without_raise(pxe_config_file_path, mac_path) utils.create_link_without_raise(pxe_config_file_path, mac_path)
def _link_ip_address_pxe_configs(task):
"""Link each IP address with the PXE configuration file.
:param task: A TaskManager instance.
:raises: FailedToGetIPAddressOnPort
:raises: InvalidIPv4Address
"""
pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
api = dhcp_factory.DHCPFactory().provider
ip_addrs = api.get_ip_addresses(task)
if not ip_addrs:
raise exception.FailedToGetIPAddressOnPort(_(
"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)
utils.unlink_without_raise(ip_address_path)
utils.create_link_without_raise(pxe_config_file_path,
ip_address_path)
def _get_pxe_mac_path(mac): def _get_pxe_mac_path(mac):
"""Convert a MAC address into a PXE config file name. """Convert a MAC address into a PXE config file name.
@ -96,6 +126,21 @@ def _get_pxe_mac_path(mac):
return os.path.join(get_root_dir(), PXE_CFG_DIR_NAME, mac_file_name) return os.path.join(get_root_dir(), PXE_CFG_DIR_NAME, mac_file_name)
def _get_pxe_ip_address_path(ip_address):
"""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'.
: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))
return os.path.join(
CONF.pxe.tftp_root, hex_ip + ".conf"
)
def get_deploy_kr_info(node_uuid, driver_info): def get_deploy_kr_info(node_uuid, driver_info):
"""Get uuid and tftp path for deploy kernel and ramdisk. """Get uuid and tftp path for deploy kernel and ramdisk.
@ -148,7 +193,11 @@ def create_pxe_config(task, pxe_options, template=None):
pxe_config_file_path = get_pxe_config_file_path(task.node.uuid) pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
pxe_config = _build_pxe_config(pxe_options, template) pxe_config = _build_pxe_config(pxe_options, template)
utils.write_to_file(pxe_config_file_path, pxe_config) utils.write_to_file(pxe_config_file_path, pxe_config)
_link_mac_pxe_configs(task)
if get_node_capability(task.node, 'boot_mode') == 'uefi':
_link_ip_address_pxe_configs(task)
else:
_link_mac_pxe_configs(task)
def clean_up_pxe_config(task): def clean_up_pxe_config(task):
@ -159,15 +208,31 @@ def clean_up_pxe_config(task):
""" """
LOG.debug("Cleaning up PXE config for node %s", task.node.uuid) LOG.debug("Cleaning up PXE config for node %s", task.node.uuid)
for mac in driver_utils.get_node_mac_addresses(task): if get_node_capability(task.node, 'boot_mode') == 'uefi':
utils.unlink_without_raise(_get_pxe_mac_path(mac)) api = dhcp_factory.DHCPFactory().provider
ip_addresses = api.get_ip_addresses(task)
if not ip_addresses:
return
for port_ip_address in ip_addresses:
try:
ip_address_path = _get_pxe_ip_address_path(port_ip_address)
except exception.InvalidIPv4Address:
continue
utils.unlink_without_raise(ip_address_path)
else:
for mac in driver_utils.get_node_mac_addresses(task):
utils.unlink_without_raise(_get_pxe_mac_path(mac))
utils.rmtree_without_raise(os.path.join(get_root_dir(), utils.rmtree_without_raise(os.path.join(get_root_dir(),
task.node.uuid)) task.node.uuid))
def dhcp_options_for_instance(): def dhcp_options_for_instance(task):
"""Retrieves the DHCP PXE boot options.""" """Retrieves the DHCP PXE boot options.
:param task: A TaskManager instance.
"""
dhcp_opts = [] dhcp_opts = []
if CONF.pxe.ipxe_enabled: if CONF.pxe.ipxe_enabled:
script_name = os.path.basename(CONF.pxe.ipxe_boot_script) script_name = os.path.basename(CONF.pxe.ipxe_boot_script)
@ -182,11 +247,55 @@ def dhcp_options_for_instance():
dhcp_opts.append({'opt_name': 'bootfile-name', dhcp_opts.append({'opt_name': 'bootfile-name',
'opt_value': ipxe_script_url}) 'opt_value': ipxe_script_url})
else: else:
if get_node_capability(task.node, 'boot_mode') == 'uefi':
boot_file = CONF.pxe.uefi_pxe_bootfile_name
else:
boot_file = CONF.pxe.pxe_bootfile_name
dhcp_opts.append({'opt_name': 'bootfile-name', dhcp_opts.append({'opt_name': 'bootfile-name',
'opt_value': CONF.pxe.pxe_bootfile_name}) 'opt_value': boot_file})
dhcp_opts.append({'opt_name': 'server-ip-address', dhcp_opts.append({'opt_name': 'server-ip-address',
'opt_value': CONF.pxe.tftp_server}) 'opt_value': CONF.pxe.tftp_server})
dhcp_opts.append({'opt_name': 'tftp-server', dhcp_opts.append({'opt_name': 'tftp-server',
'opt_value': CONF.pxe.tftp_server}) 'opt_value': CONF.pxe.tftp_server})
return dhcp_opts return dhcp_opts
def get_node_capability(node, capability):
"""Returns 'capability' value from node's 'capabilities' property.
:param node: Node object.
:param capability: Capability key.
:return: Capability value.
If capability is not present, then return "None"
"""
capabilities = node.properties.get('capabilities')
if not capabilities:
return
for node_capability in str(capabilities).split(','):
parts = node_capability.split(':')
if len(parts) == 2 and parts[0] and parts[1]:
if parts[0] == capability:
return parts[1]
else:
LOG.warn(_LW("Ignoring malformed capability '%s'. "
"Format should be 'key:val'."), node_capability)
def validate_boot_mode_capability(node):
"""Validate the boot_mode capability set in node property.
:param node: an ironic node object.
:raises: InvalidParameterValue, if 'boot_mode' capability is set
other than 'bios' or 'uefi' or None.
"""
boot_mode = get_node_capability(node, 'boot_mode')
if boot_mode and boot_mode not in ['bios', 'uefi']:
raise exception.InvalidParameterValue(_("Invalid boot_mode "
"parameter '%s'.") % boot_mode)

View File

@ -67,3 +67,11 @@ class BaseDHCP(object):
:raises: FailedToUpdateDHCPOptOnPort :raises: FailedToUpdateDHCPOptOnPort
""" """
@abc.abstractmethod
def get_ip_addresses(self, task):
"""Get IP addresses for all ports in `task`.
:param task: a TaskManager instance.
:returns: List of IP addresses associated with task.ports
"""

View File

@ -25,11 +25,13 @@ from ironic.common import i18n
from ironic.common.i18n import _ from ironic.common.i18n import _
from ironic.common import keystone from ironic.common import keystone
from ironic.common import network from ironic.common import network
from ironic.common import utils
from ironic.dhcp import base from ironic.dhcp import base
from ironic.drivers.modules import ssh from ironic.drivers.modules import ssh
from ironic.openstack.common import log as logging from ironic.openstack.common import log as logging
_LE = i18n._LE
_LW = i18n._LW _LW = i18n._LW
neutron_opts = [ neutron_opts = [
@ -174,3 +176,85 @@ class NeutronDHCPApi(base.BaseDHCP):
if isinstance(task.driver.power, ssh.SSHPower): if isinstance(task.driver.power, ssh.SSHPower):
LOG.debug("Waiting 15 seconds for Neutron.") LOG.debug("Waiting 15 seconds for Neutron.")
time.sleep(15) time.sleep(15)
def _get_fixed_ip_address(self, port_id):
"""Get a port's fixed ip address.
:param port_id: Neutron port id.
:returns: Neutron port ip address.
:raises: FailedToGetIPAddressOnPort
:raises: InvalidIPv4Address
"""
ip_address = None
try:
neutron_port = self.client.show_port(port_id).get('port')
except neutron_client_exc.NeutronClientException:
LOG.exception(_LE("Failed to Get IP address on Neutron port %s."),
port_id)
raise exception.FailedToGetIPAddressOnPort(port_id=port_id)
fixed_ips = neutron_port.get('fixed_ips')
# NOTE(faizan) At present only the first fixed_ip assigned to this
# neutron port will be used, since nova allocates only one fixed_ip
# for the instance.
if fixed_ips:
ip_address = fixed_ips[0].get('ip_address', None)
if ip_address:
if utils.is_valid_ipv4(ip_address):
return ip_address
else:
LOG.error(_LE("Neutron returned invalid IPv4 address %s."),
ip_address)
raise exception.InvalidIPv4Address(ip_address=ip_address)
else:
LOG.error(_LE("No IP address assigned to Neutron port %s."),
port_id)
raise exception.FailedToGetIPAddressOnPort(port_id=port_id)
def _get_port_ip_address(self, task, port_id):
"""Get ip address of ironic port assigned by neutron.
:param task: a TaskManager instance.
:param port_id: ironic Node's port UUID.
:returns: Neutron port ip address associated with Node's port.
:raises: FailedToGetIPAddressOnPort
:raises: InvalidIPv4Address
"""
vifs = network.get_node_vif_ids(task)
if not vifs:
LOG.warning(_LW("No VIFs found for node %(node)s when attempting "
" to get port IP address."),
{'node': task.node.uuid})
raise exception.FailedToGetIPAddressOnPort(port_id=port_id)
port_vif = vifs[port_id]
port_ip_address = self._get_fixed_ip_address(port_vif)
return port_ip_address
def get_ip_addresses(self, task):
"""Get IP addresses for all ports in `task`.
:param task: a TaskManager instance.
:returns: List of IP addresses associated with task.ports.
"""
failures = []
ip_addresses = []
for port in task.ports:
try:
port_ip_address = self._get_port_ip_address(task, port.uuid)
ip_addresses.append(port_ip_address)
except (exception.FailedToGetIPAddressOnPort,
exception.InvalidIPv4Address):
failures.append(port.uuid)
if failures:
LOG.warn(_LW("Some errors were encountered on node %(node)s"
" while retrieving IP address on the following"
" ports: %(ports)s."),
{'node': task.node.uuid, 'ports': failures})
return ip_addresses

View File

@ -26,3 +26,6 @@ class NoneDHCPApi(base.BaseDHCP):
def update_port_address(self, port_id, address): def update_port_address(self, port_id, address):
pass pass
def get_ip_addresses(self, task):
return []

View File

@ -217,7 +217,7 @@ class AgentDeploy(base.DeployInterface):
:param task: a TaskManager instance. :param task: a TaskManager instance.
:returns: status of the deploy. One of ironic.common.states. :returns: status of the deploy. One of ironic.common.states.
""" """
dhcp_opts = pxe_utils.dhcp_options_for_instance() dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
provider = dhcp_factory.DHCPFactory(token=task.context.auth_token) provider = dhcp_factory.DHCPFactory(token=task.context.auth_token)
provider.update_dhcp(task, dhcp_opts) provider.update_dhcp(task, dhcp_opts)
manager_utils.node_set_boot_device(task, 'pxe', persistent=True) manager_utils.node_set_boot_device(task, 'pxe', persistent=True)

View File

@ -160,15 +160,21 @@ def block_uuid(dev):
return out.strip() return out.strip()
def switch_pxe_config(path, root_uuid): def switch_pxe_config(path, root_uuid, boot_mode):
"""Switch a pxe config from deployment mode to service mode.""" """Switch a pxe config from deployment mode to service mode."""
with open(path) as f: with open(path) as f:
lines = f.readlines() lines = f.readlines()
root = 'UUID=%s' % root_uuid root = 'UUID=%s' % root_uuid
pxe_cmd = 'goto' if CONF.pxe.ipxe_enabled else 'default'
rre = re.compile(r'\{\{ ROOT \}\}') rre = re.compile(r'\{\{ ROOT \}\}')
dre = re.compile('^%s .*$' % pxe_cmd)
boot_line = '%s boot' % pxe_cmd if boot_mode == 'uefi':
dre = re.compile('^default=.*$')
boot_line = 'default=boot'
else:
pxe_cmd = 'goto' if CONF.pxe.ipxe_enabled else 'default'
dre = re.compile('^%s .*$' % pxe_cmd)
boot_line = '%s boot' % pxe_cmd
with open(path, 'w') as f: with open(path, 'w') as f:
for line in lines: for line in lines:
line = rre.sub(root, line) line = rre.sub(root, line)

View File

@ -0,0 +1,11 @@
default=deploy
image={{pxe_options.deployment_aki_path}}
label=deploy
initrd={{pxe_options.deployment_ari_path}}
append="rootfstype=ramfs selinux=0 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 }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} ip=%I:{{pxe_options.tftp_server}}:%G:%M:%H::on"
image={{pxe_options.aki_path}}
label=boot
initrd={{pxe_options.ari_path}}
append="root={{ ROOT }} ro text {{ pxe_options.pxe_append_params|default("", true) }} ip=%I:{{pxe_options.tftp_server}}:%G:%M:%H::on"

View File

@ -41,11 +41,19 @@ from ironic.openstack.common import fileutils
from ironic.openstack.common import log as logging from ironic.openstack.common import log as logging
_LE = i18n._LE
_LW = i18n._LW
pxe_opts = [ pxe_opts = [
cfg.StrOpt('pxe_config_template', cfg.StrOpt('pxe_config_template',
default=paths.basedir_def( default=paths.basedir_def(
'drivers/modules/pxe_config.template'), 'drivers/modules/pxe_config.template'),
help='Template file for PXE configuration.'), help='Template file for PXE configuration.'),
cfg.StrOpt('uefi_pxe_config_template',
default=paths.basedir_def(
'drivers/modules/elilo_efi_pxe_config.template'),
help='Template file for PXE configuration for UEFI boot'
' loader.'),
cfg.StrOpt('tftp_server', cfg.StrOpt('tftp_server',
default='$my_ip', default='$my_ip',
help='IP address of Ironic compute node\'s tftp server.'), help='IP address of Ironic compute node\'s tftp server.'),
@ -60,6 +68,9 @@ pxe_opts = [
cfg.StrOpt('pxe_bootfile_name', cfg.StrOpt('pxe_bootfile_name',
default='pxelinux.0', default='pxelinux.0',
help='Bootfile DHCP parameter.'), help='Bootfile DHCP parameter.'),
cfg.StrOpt('uefi_pxe_bootfile_name',
default='elilo.efi',
help='Bootfile DHCP parameter for UEFI boot mode.'),
cfg.StrOpt('http_url', cfg.StrOpt('http_url',
help='Ironic compute node\'s HTTP server URL. ' help='Ironic compute node\'s HTTP server URL. '
'Example: http://192.1.2.3:8080'), 'Example: http://192.1.2.3:8080'),
@ -168,6 +179,7 @@ def _build_pxe_config_options(node, pxe_info, ctx):
'aki_path': kernel, 'aki_path': kernel,
'ari_path': ramdisk, 'ari_path': ramdisk,
'pxe_append_params': CONF.pxe.pxe_append_params, 'pxe_append_params': CONF.pxe.pxe_append_params,
'tftp_server': CONF.pxe.tftp_server
} }
deploy_ramdisk_options = iscsi_deploy.build_deploy_ramdisk_options(node, deploy_ramdisk_options = iscsi_deploy.build_deploy_ramdisk_options(node,
@ -265,11 +277,22 @@ class PXEDeploy(base.DeployInterface):
:raises: InvalidParameterValue. :raises: InvalidParameterValue.
:raises: MissingParameterValue :raises: MissingParameterValue
""" """
# Check the boot_mode capability parameter value.
pxe_utils.validate_boot_mode_capability(task.node)
if CONF.pxe.ipxe_enabled: if CONF.pxe.ipxe_enabled:
if not CONF.pxe.http_url or not CONF.pxe.http_root: if not CONF.pxe.http_url or not CONF.pxe.http_root:
raise exception.MissingParameterValue(_( raise exception.MissingParameterValue(_(
"iPXE boot is enabled but no HTTP URL or HTTP " "iPXE boot is enabled but no HTTP URL or HTTP "
"root was specified.")) "root was specified."))
# iPXE and UEFI should not be configured together.
if pxe_utils.get_node_capability(task.node, 'boot_mode') == 'uefi':
LOG.error(_LE("UEFI boot mode is not supported with "
"iPXE boot enabled."))
raise exception.InvalidParameterValue(_(
"Conflict: iPXE is enabled, but cannot be used with node"
"%(node_uuid)s configured to use UEFI boot") %
{'node_uuid': task.node.uuid})
d_info = _parse_deploy_info(task.node) d_info = _parse_deploy_info(task.node)
@ -299,10 +322,25 @@ class PXEDeploy(base.DeployInterface):
# TODO(yuriyz): more secure way needed for pass auth token # TODO(yuriyz): more secure way needed for pass auth token
# to deploy ramdisk # to deploy ramdisk
_create_token_file(task) _create_token_file(task)
dhcp_opts = pxe_utils.dhcp_options_for_instance() dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
provider = dhcp_factory.DHCPFactory(token=task.context.auth_token) provider = dhcp_factory.DHCPFactory(token=task.context.auth_token)
provider.update_dhcp(task, dhcp_opts) provider.update_dhcp(task, dhcp_opts)
manager_utils.node_set_boot_device(task, 'pxe', persistent=True)
# NOTE(faizan): Under UEFI boot mode, setting of boot device may differ
# between different machines. IPMI does not work for setting boot
# devices in UEFI mode for certain machines.
# Expected IPMI failure for uefi boot mode. Logging a message to
# set the boot device manually and continue with deploy.
try:
manager_utils.node_set_boot_device(task, 'pxe', persistent=True)
except exception.IPMIFailure:
if pxe_utils.get_node_capability(task.node, 'boot_mode') == 'uefi':
LOG.warning(_LW("ipmitool is unable to set boot device while "
"the node is in UEFI boot mode."
"Please set the boot device manually."))
else:
raise
manager_utils.node_power_action(task, states.REBOOT) manager_utils.node_power_action(task, states.REBOOT)
return states.DEPLOYWAIT return states.DEPLOYWAIT
@ -338,8 +376,14 @@ class PXEDeploy(base.DeployInterface):
pxe_info = _get_image_info(task.node, task.context) pxe_info = _get_image_info(task.node, task.context)
pxe_options = _build_pxe_config_options(task.node, pxe_info, pxe_options = _build_pxe_config_options(task.node, pxe_info,
task.context) task.context)
if pxe_utils.get_node_capability(task.node, 'boot_mode') == 'uefi':
pxe_config_template = CONF.pxe.uefi_pxe_config_template
else:
pxe_config_template = CONF.pxe.pxe_config_template
pxe_utils.create_pxe_config(task, pxe_options, pxe_utils.create_pxe_config(task, pxe_options,
CONF.pxe.pxe_config_template) pxe_config_template)
_cache_ramdisk_kernel(task.context, task.node, pxe_info) _cache_ramdisk_kernel(task.context, task.node, pxe_info)
def clean_up(self, task): def clean_up(self, task):
@ -364,7 +408,7 @@ class PXEDeploy(base.DeployInterface):
_destroy_token_file(node) _destroy_token_file(node)
def take_over(self, task): def take_over(self, task):
dhcp_opts = pxe_utils.dhcp_options_for_instance() dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
provider = dhcp_factory.DHCPFactory(token=task.context.auth_token) provider = dhcp_factory.DHCPFactory(token=task.context.auth_token)
provider.update_dhcp(task, dhcp_opts) provider.update_dhcp(task, dhcp_opts)
@ -420,7 +464,8 @@ class VendorPassthru(base.VendorInterface):
try: try:
pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid)
deploy_utils.switch_pxe_config(pxe_config_path, root_uuid) deploy_utils.switch_pxe_config(pxe_config_path, root_uuid,
pxe_utils.get_node_capability(node, 'boot_mode'))
deploy_utils.notify_deploy_complete(kwargs['address']) deploy_utils.notify_deploy_complete(kwargs['address'])

View File

@ -21,6 +21,7 @@ from neutronclient.v2_0 import client
from ironic.common import dhcp_factory from ironic.common import dhcp_factory
from ironic.common import exception from ironic.common import exception
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.dhcp import neutron from ironic.dhcp import neutron
from ironic.openstack.common import context from ironic.openstack.common import context
@ -179,10 +180,10 @@ class TestNeutron(base.TestCase):
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.update_port_dhcp_opts') @mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.update_port_dhcp_opts')
@mock.patch('ironic.common.network.get_node_vif_ids') @mock.patch('ironic.common.network.get_node_vif_ids')
def test_update_dhcp(self, mock_gnvi, mock_updo): def test_update_dhcp(self, mock_gnvi, mock_updo):
opts = pxe_utils.dhcp_options_for_instance()
mock_gnvi.return_value = {'port-uuid': 'vif-uuid'} mock_gnvi.return_value = {'port-uuid': 'vif-uuid'}
with task_manager.acquire(self.context, with task_manager.acquire(self.context,
self.node.uuid) as task: self.node.uuid) as task:
opts = pxe_utils.dhcp_options_for_instance(task)
api = dhcp_factory.DHCPFactory(token=self.context.auth_token) api = dhcp_factory.DHCPFactory(token=self.context.auth_token)
api.update_dhcp(task, self.node) api.update_dhcp(task, self.node)
mock_updo.assertCalleOnceWith('vif-uuid', opts) mock_updo.assertCalleOnceWith('vif-uuid', opts)
@ -224,3 +225,124 @@ class TestNeutron(base.TestCase):
task, self.node) task, self.node)
mock_gnvi.assertCalleOnceWith(task) mock_gnvi.assertCalleOnceWith(task)
self.assertEqual(2, mock_updo.call_count) self.assertEqual(2, mock_updo.call_count)
@mock.patch.object(client.Client, 'show_port')
@mock.patch.object(client.Client, '__init__')
def test_neutron__get_fixed_ip_address(self, mock_client_init,
mock_show_port):
port_id = 'fake-port-id'
expected = "192.168.1.3"
mock_client_init.return_value = None
api = dhcp_factory.DHCPFactory().provider
port_data = {
"id": port_id,
"network_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6",
"admin_state_up": True,
"status": "ACTIVE",
"mac_address": "fa:16:3e:4c:2c:30",
"fixed_ips": [
{
"ip_address": "192.168.1.3",
"subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef"
}
],
"device_id": 'bece68a3-2f8b-4e66-9092-244493d6aba7',
}
port = {'port': port_data}
mock_show_port.return_value = port
result = api._get_fixed_ip_address(port_id)
self.assertEqual(expected, result)
@mock.patch.object(client.Client, 'show_port')
@mock.patch.object(client.Client, '__init__')
def test_neutron__get_fixed_ip_address_invalid_ip(self, mock_client_init,
mock_show_port):
port_id = 'fake-port-id'
mock_client_init.return_value = None
api = dhcp_factory.DHCPFactory().provider
port_data = {
"id": port_id,
"network_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6",
"admin_state_up": True,
"status": "ACTIVE",
"mac_address": "fa:16:3e:4c:2c:30",
"fixed_ips": [
{
"ip_address": "invalid.ip",
"subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef"
}
],
"device_id": 'bece68a3-2f8b-4e66-9092-244493d6aba7',
}
port = {'port': port_data}
mock_show_port.return_value = port
self.assertRaises(exception.InvalidIPv4Address,
api._get_fixed_ip_address,
port_id)
mock_show_port.assert_called_once_with(port_id)
@mock.patch.object(client.Client, 'show_port')
@mock.patch.object(client.Client, '__init__')
def test_neutron__get_fixed_ip_address_with_exception(self,
mock_client_init, mock_show_port):
port_id = 'fake-port-id'
mock_client_init.return_value = None
api = dhcp_factory.DHCPFactory().provider
mock_show_port.side_effect = (
neutron_client_exc.NeutronClientException())
self.assertRaises(exception.FailedToGetIPAddressOnPort,
api._get_fixed_ip_address, port_id)
mock_show_port.assert_called_once_with(port_id)
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi._get_fixed_ip_address')
@mock.patch('ironic.common.network.get_node_vif_ids')
def test__get_port_ip_address(self, mock_gnvi, mock_gfia):
expected = "192.168.1.3"
port = object_utils.create_test_port(self.context,
node_id=self.node.id,
id=6, address='aa:bb:cc',
uuid=utils.generate_uuid(),
extra={'vif_port_id': 'test-vif-A'},
driver='fake')
mock_gnvi.return_value = {port.uuid: 'vif-uuid'}
mock_gfia.return_value = expected
with task_manager.acquire(self.context,
self.node.uuid) as task:
api = dhcp_factory.DHCPFactory(token=task.context.auth_token)
api = api.provider
result = api._get_port_ip_address(task, port.uuid)
self.assertEqual(expected, result)
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi._get_fixed_ip_address')
@mock.patch('ironic.common.network.get_node_vif_ids')
def test__get_port_ip_address_with_exception(self, mock_gnvi, mock_gfia):
expected = "192.168.1.3"
port = object_utils.create_test_port(self.context,
node_id=self.node.id,
id=6, address='aa:bb:cc',
uuid=utils.generate_uuid(),
extra={'vif_port_id': 'test-vif-A'},
driver='fake')
mock_gnvi.return_value = None
mock_gfia.return_value = expected
with task_manager.acquire(self.context,
self.node.uuid) as task:
api = dhcp_factory.DHCPFactory().provider
self.assertRaises(exception.FailedToGetIPAddressOnPort,
api._get_port_ip_address, task, port)
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi._get_port_ip_address')
def test_get_ip_addresses(self, get_ip_mock):
ip_address = '10.10.0.1'
address = "aa:aa:aa:aa:aa:aa"
expected = [ip_address]
object_utils.create_test_port(self.context, node_uuid=self.node.uuid,
address=address)
get_ip_mock.return_value = ip_address
with task_manager.acquire(self.context, self.node.uuid) as task:
api = dhcp_factory.DHCPFactory().provider
result = api.get_ip_addresses(task)
self.assertEqual(expected, result)

View File

@ -57,9 +57,9 @@ class TestAgentDeploy(db_base.DbTestCase):
@mock.patch('ironic.conductor.utils.node_set_boot_device') @mock.patch('ironic.conductor.utils.node_set_boot_device')
@mock.patch('ironic.conductor.utils.node_power_action') @mock.patch('ironic.conductor.utils.node_power_action')
def test_deploy(self, power_mock, bootdev_mock, dhcp_mock): def test_deploy(self, power_mock, bootdev_mock, dhcp_mock):
dhcp_opts = pxe_utils.dhcp_options_for_instance()
with task_manager.acquire( with task_manager.acquire(
self.context, self.node['uuid'], shared=False) as task: self.context, self.node['uuid'], shared=False) as task:
dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
driver_return = self.driver.deploy(task) driver_return = self.driver.deploy(task)
self.assertEqual(driver_return, states.DEPLOYWAIT) self.assertEqual(driver_return, states.DEPLOYWAIT)
dhcp_mock.assert_called_once_with(task, dhcp_opts) dhcp_mock.assert_called_once_with(task, dhcp_opts)

View File

@ -91,6 +91,34 @@ append initrd=ramdisk root=UUID=12345678-1234-1234-1234-1234567890abcdef
boot boot
""" """
_UEFI_PXECONF_DEPLOY = """
default=deploy
image=deploy_kernel
label=deploy
initrd=deploy_ramdisk
append="ro text"
image=kernel
label=boot
initrd=ramdisk
append="root={{ ROOT }}"
"""
_UEFI_PXECONF_BOOT = """
default=boot
image=deploy_kernel
label=deploy
initrd=deploy_ramdisk
append="ro text"
image=kernel
label=boot
initrd=ramdisk
append="root=UUID=12345678-1234-1234-1234-1234567890abcdef"
"""
class PhysicalWorkTestCase(tests_base.TestCase): class PhysicalWorkTestCase(tests_base.TestCase):
def setUp(self): def setUp(self):
@ -392,31 +420,48 @@ class PhysicalWorkTestCase(tests_base.TestCase):
class SwitchPxeConfigTestCase(tests_base.TestCase): class SwitchPxeConfigTestCase(tests_base.TestCase):
def _create_config(self, ipxe=False): def _create_config(self, ipxe=False, boot_mode=None):
(fd, fname) = tempfile.mkstemp() (fd, fname) = tempfile.mkstemp()
pxe_cfg = _IPXECONF_DEPLOY if ipxe else _PXECONF_DEPLOY if boot_mode == 'uefi':
pxe_cfg = _UEFI_PXECONF_DEPLOY
else:
pxe_cfg = _IPXECONF_DEPLOY if ipxe else _PXECONF_DEPLOY
os.write(fd, pxe_cfg) os.write(fd, pxe_cfg)
os.close(fd) os.close(fd)
self.addCleanup(os.unlink, fname) self.addCleanup(os.unlink, fname)
return fname return fname
def test_switch_pxe_config(self): def test_switch_pxe_config(self):
boot_mode = 'bios'
fname = self._create_config() fname = self._create_config()
utils.switch_pxe_config(fname, utils.switch_pxe_config(fname,
'12345678-1234-1234-1234-1234567890abcdef') '12345678-1234-1234-1234-1234567890abcdef',
boot_mode)
with open(fname, 'r') as f: with open(fname, 'r') as f:
pxeconf = f.read() pxeconf = f.read()
self.assertEqual(_PXECONF_BOOT, pxeconf) self.assertEqual(_PXECONF_BOOT, pxeconf)
def test_switch_ipxe_config(self): def test_switch_ipxe_config(self):
boot_mode = 'bios'
cfg.CONF.set_override('ipxe_enabled', True, 'pxe') cfg.CONF.set_override('ipxe_enabled', True, 'pxe')
fname = self._create_config(ipxe=True) fname = self._create_config(ipxe=True)
utils.switch_pxe_config(fname, utils.switch_pxe_config(fname,
'12345678-1234-1234-1234-1234567890abcdef') '12345678-1234-1234-1234-1234567890abcdef',
boot_mode)
with open(fname, 'r') as f: with open(fname, 'r') as f:
pxeconf = f.read() pxeconf = f.read()
self.assertEqual(_IPXECONF_BOOT, pxeconf) self.assertEqual(_IPXECONF_BOOT, pxeconf)
def test_switch_uefi_pxe_config(self):
boot_mode = 'uefi'
fname = self._create_config(boot_mode=boot_mode)
utils.switch_pxe_config(fname,
'12345678-1234-1234-1234-1234567890abcdef',
boot_mode)
with open(fname, 'r') as f:
pxeconf = f.read()
self.assertEqual(_UEFI_PXECONF_BOOT, pxeconf)
class OtherFunctionTestCase(tests_base.TestCase): class OtherFunctionTestCase(tests_base.TestCase):
def test_get_dev(self): def test_get_dev(self):

View File

@ -166,6 +166,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
fake_key = '0123456789ABCDEFGHIJKLMNOPQRSTUV' fake_key = '0123456789ABCDEFGHIJKLMNOPQRSTUV'
random_alnum_mock.return_value = fake_key random_alnum_mock.return_value = fake_key
tftp_server = CONF.pxe.tftp_server
if ipxe_enabled: if ipxe_enabled:
http_url = 'http://192.1.2.3:1234' http_url = 'http://192.1.2.3:1234'
@ -201,7 +202,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
'deployment_id': u'1be26c0b-03f2-4d2e-ae87-c02d7f33c123', 'deployment_id': u'1be26c0b-03f2-4d2e-ae87-c02d7f33c123',
'ironic_api_url': 'http://192.168.122.184:6385', 'ironic_api_url': 'http://192.168.122.184:6385',
'deployment_aki_path': deploy_kernel, 'deployment_aki_path': deploy_kernel,
'disk': 'sda' 'disk': 'sda',
'tftp_server': tftp_server
} }
image_info = {'deploy_kernel': ('deploy_kernel', image_info = {'deploy_kernel': ('deploy_kernel',
@ -342,6 +344,30 @@ class PXEDriverTestCase(db_base.DbTestCase):
self.assertRaises(exception.MissingParameterValue, self.assertRaises(exception.MissingParameterValue,
task.driver.deploy.validate, task) task.driver.deploy.validate, task)
@mock.patch.object(base_image_service.BaseImageService, '_show')
def test_validate_fail_invalid_boot_mode(self, mock_glance):
properties = {'capabilities': 'boot_mode:foo,cap2:value2'}
mock_glance.return_value = {'properties': {'kernel_id': 'fake-kernel',
'ramdisk_id': 'fake-initr'}}
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.node.properties = properties
self.assertRaises(exception.InvalidParameterValue,
task.driver.deploy.validate, task)
@mock.patch.object(base_image_service.BaseImageService, '_show')
def test_validate_fail_invalid_config_uefi_ipxe(self, mock_glance):
properties = {'capabilities': 'boot_mode:uefi,cap2:value2'}
mock_glance.return_value = {'properties': {'kernel_id': 'fake-kernel',
'ramdisk_id': 'fake-initr'}}
self.config(ipxe_enabled=True, group='pxe')
self.config(http_url='dummy_url', group='pxe')
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.node.properties = properties
self.assertRaises(exception.InvalidParameterValue,
task.driver.deploy.validate, task)
def test_validate_fail_no_port(self): def test_validate_fail_no_port(self):
new_node = obj_utils.create_test_node( new_node = obj_utils.create_test_node(
self.context, self.context,
@ -480,10 +506,10 @@ class PXEDriverTestCase(db_base.DbTestCase):
fake_img_path = '/test/path/test.img' fake_img_path = '/test/path/test.img'
mock_get_image_file_path.return_value = fake_img_path mock_get_image_file_path.return_value = fake_img_path
mock_get_image_mb.return_value = 1 mock_get_image_mb.return_value = 1
dhcp_opts = pxe_utils.dhcp_options_for_instance()
with task_manager.acquire(self.context, with task_manager.acquire(self.context,
self.node.uuid, shared=False) as task: self.node.uuid, shared=False) as task:
dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
state = task.driver.deploy.deploy(task) state = task.driver.deploy.deploy(task)
self.assertEqual(state, states.DEPLOYWAIT) self.assertEqual(state, states.DEPLOYWAIT)
mock_cache_instance_image.assert_called_once_with( mock_cache_instance_image.assert_called_once_with(
@ -530,9 +556,9 @@ class PXEDriverTestCase(db_base.DbTestCase):
@mock.patch.object(dhcp_factory.DHCPFactory, 'update_dhcp') @mock.patch.object(dhcp_factory.DHCPFactory, 'update_dhcp')
def test_take_over(self, update_dhcp_mock): def test_take_over(self, update_dhcp_mock):
dhcp_opts = pxe_utils.dhcp_options_for_instance()
with task_manager.acquire( with task_manager.acquire(
self.context, self.node.uuid, shared=True) as task: self.context, self.node.uuid, shared=True) as task:
dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
task.driver.deploy.take_over(task) task.driver.deploy.take_over(task)
update_dhcp_mock.assert_called_once_with( update_dhcp_mock.assert_called_once_with(
task, dhcp_opts) task, dhcp_opts)
@ -548,6 +574,7 @@ class PXEDriverTestCase(db_base.DbTestCase):
self.node.save() self.node.save()
root_uuid = "12345678-1234-1234-1234-1234567890abcxyz" root_uuid = "12345678-1234-1234-1234-1234567890abcxyz"
boot_mode = None
def fake_deploy(**kwargs): def fake_deploy(**kwargs):
return root_uuid return root_uuid
@ -568,7 +595,8 @@ class PXEDriverTestCase(db_base.DbTestCase):
mock_image_cache.assert_called_once_with() mock_image_cache.assert_called_once_with()
mock_image_cache.return_value.clean_up.assert_called_once_with() mock_image_cache.return_value.clean_up.assert_called_once_with()
pxe_config_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) pxe_config_path = pxe_utils.get_pxe_config_file_path(self.node.uuid)
mock_switch_config.assert_called_once_with(pxe_config_path, root_uuid) mock_switch_config.assert_called_once_with(pxe_config_path, root_uuid,
boot_mode)
notify_mock.assert_called_once_with('123456') notify_mock.assert_called_once_with('123456')
@mock.patch.object(iscsi_deploy, 'InstanceImageCache') @mock.patch.object(iscsi_deploy, 'InstanceImageCache')

View File

@ -19,6 +19,7 @@ import os
import mock import mock
from oslo.config import cfg from oslo.config import cfg
from ironic.common import exception
from ironic.common import pxe_utils from ironic.common import pxe_utils
from ironic.conductor import task_manager from ironic.conductor import task_manager
from ironic.db import api as dbapi from ironic.db import api as dbapi
@ -92,6 +93,27 @@ class TestPXEUtils(db_base.DbTestCase):
unlink_mock.assert_has_calls(unlink_calls) unlink_mock.assert_has_calls(unlink_calls)
create_link_mock.assert_has_calls(create_link_calls) create_link_mock.assert_has_calls(create_link_calls)
@mock.patch('ironic.common.utils.create_link_without_raise')
@mock.patch('ironic.common.utils.unlink_without_raise')
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.get_ip_addresses')
def test__link_ip_address_pxe_configs(self, get_ip_mock, unlink_mock,
create_link_mock):
ip_address = '10.10.0.1'
address = "aa:aa:aa:aa:aa:aa"
object_utils.create_test_port(self.context, node_uuid=self.node.uuid,
address=address)
get_ip_mock.return_value = [ip_address]
create_link_calls = [
mock.call(u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/config',
'/tftpboot/0A0A0001.conf'),
]
with task_manager.acquire(self.context, self.node.uuid) as task:
pxe_utils._link_ip_address_pxe_configs(task)
unlink_mock.assert_called_once_with('/tftpboot/0A0A0001.conf')
create_link_mock.assert_has_calls(create_link_calls)
@mock.patch('ironic.common.utils.write_to_file') @mock.patch('ironic.common.utils.write_to_file')
@mock.patch.object(pxe_utils, '_build_pxe_config') @mock.patch.object(pxe_utils, '_build_pxe_config')
@mock.patch('ironic.openstack.common.fileutils.ensure_tree') @mock.patch('ironic.openstack.common.fileutils.ensure_tree')
@ -139,6 +161,11 @@ class TestPXEUtils(db_base.DbTestCase):
self.assertEqual('/httpboot/pxelinux.cfg/00112233aabbcc', self.assertEqual('/httpboot/pxelinux.cfg/00112233aabbcc',
pxe_utils._get_pxe_mac_path(mac)) pxe_utils._get_pxe_mac_path(mac))
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))
def test_get_root_dir(self): def test_get_root_dir(self):
expected_dir = '/tftproot' expected_dir = '/tftproot'
self.config(ipxe_enabled=False, group='pxe') self.config(ipxe_enabled=False, group='pxe')
@ -167,7 +194,9 @@ class TestPXEUtils(db_base.DbTestCase):
{'opt_name': 'tftp-server', {'opt_name': 'tftp-server',
'opt_value': '192.0.2.1'} 'opt_value': '192.0.2.1'}
] ]
self.assertEqual(expected_info, pxe_utils.dhcp_options_for_instance()) with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertEqual(expected_info,
pxe_utils.dhcp_options_for_instance(task))
def _test_get_deploy_kr_info(self, expected_dir): def _test_get_deploy_kr_info(self, expected_dir):
node_uuid = 'fake-node' node_uuid = 'fake-node'
@ -222,5 +251,56 @@ class TestPXEUtils(db_base.DbTestCase):
'opt_value': '192.0.2.1'}, 'opt_value': '192.0.2.1'},
{'opt_name': 'bootfile-name', {'opt_name': 'bootfile-name',
'opt_value': expected_boot_script_url}] 'opt_value': expected_boot_script_url}]
self.assertEqual(sorted(expected_info), with task_manager.acquire(self.context, self.node.uuid) as task:
sorted(pxe_utils.dhcp_options_for_instance())) self.assertEqual(sorted(expected_info),
sorted(pxe_utils.dhcp_options_for_instance(task)))
def test_get_node_capability(self):
properties = {'capabilities': 'cap1:value1,cap2:value2'}
self.node.properties = properties
expected = 'value1'
result = pxe_utils.get_node_capability(self.node, 'cap1')
self.assertEqual(expected, result)
def test_get_node_capability_returns_none(self):
properties = {'capabilities': 'cap1:value1,cap2:value2'}
self.node.properties = properties
result = pxe_utils.get_node_capability(self.node, 'capX')
self.assertIsNone(result)
def test_validate_boot_mode_capability(self):
properties = {'capabilities': 'boot_mode:uefi,cap2:value2'}
self.node.properties = properties
result = pxe_utils.validate_boot_mode_capability(self.node)
self.assertIsNone(result)
def test_validate_boot_mode_capability_with_exception(self):
properties = {'capabilities': 'boot_mode:foo,cap2:value2'}
self.node.properties = properties
self.assertRaises(exception.InvalidParameterValue,
pxe_utils.validate_boot_mode_capability, self.node)
@mock.patch('ironic.common.utils.rmtree_without_raise', autospec=True)
@mock.patch('ironic.common.utils.unlink_without_raise', autospec=True)
@mock.patch('ironic.dhcp.neutron.NeutronDHCPApi._get_port_ip_address')
def test_clean_up_pxe_config_uefi(self, get_ip_mock, unlink_mock,
rmtree_mock):
ip_address = '10.10.0.1'
address = "aa:aa:aa:aa:aa:aa"
properties = {'capabilities': 'boot_mode:uefi'}
object_utils.create_test_port(self.context, node_uuid=self.node.uuid,
address=address)
get_ip_mock.return_value = ip_address
with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.properties = properties
pxe_utils.clean_up_pxe_config(task)
unlink_mock.assert_called_once_with('/tftpboot/0A0A0001.conf')
rmtree_mock.assert_called_once_with(
os.path.join(CONF.pxe.tftp_root, self.node.uuid))