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:
parent
b56db42aa3
commit
1718a5f32e
@ -1113,6 +1113,10 @@
|
||||
# Template file for PXE configuration. (string value)
|
||||
#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
|
||||
# value)
|
||||
#tftp_server=$my_ip
|
||||
@ -1127,6 +1131,9 @@
|
||||
# Bootfile DHCP parameter. (string value)
|
||||
#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:
|
||||
# http://192.1.2.3:8080 (string value)
|
||||
#http_url=<None>
|
||||
|
@ -226,6 +226,14 @@ class FailedToUpdateDHCPOptOnPort(IronicException):
|
||||
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):
|
||||
message = _("Update MAC address on port: %(port_id)s failed.")
|
||||
|
||||
|
@ -19,11 +19,18 @@ import os
|
||||
import jinja2
|
||||
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.drivers import utils as driver_utils
|
||||
from ironic.openstack.common import fileutils
|
||||
from ironic.openstack.common import log as logging
|
||||
|
||||
_LW = i18n._LW
|
||||
_LE = i18n._LE
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
"""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)
|
||||
|
||||
|
||||
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):
|
||||
"""Get uuid and tftp path for deploy kernel and ramdisk.
|
||||
|
||||
@ -148,6 +193,10 @@ def create_pxe_config(task, pxe_options, template=None):
|
||||
pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
|
||||
pxe_config = _build_pxe_config(pxe_options, template)
|
||||
utils.write_to_file(pxe_config_file_path, pxe_config)
|
||||
|
||||
if get_node_capability(task.node, 'boot_mode') == 'uefi':
|
||||
_link_ip_address_pxe_configs(task)
|
||||
else:
|
||||
_link_mac_pxe_configs(task)
|
||||
|
||||
|
||||
@ -159,6 +208,19 @@ def clean_up_pxe_config(task):
|
||||
"""
|
||||
LOG.debug("Cleaning up PXE config for node %s", task.node.uuid)
|
||||
|
||||
if get_node_capability(task.node, 'boot_mode') == 'uefi':
|
||||
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))
|
||||
|
||||
@ -166,8 +228,11 @@ def clean_up_pxe_config(task):
|
||||
task.node.uuid))
|
||||
|
||||
|
||||
def dhcp_options_for_instance():
|
||||
"""Retrieves the DHCP PXE boot options."""
|
||||
def dhcp_options_for_instance(task):
|
||||
"""Retrieves the DHCP PXE boot options.
|
||||
|
||||
:param task: A TaskManager instance.
|
||||
"""
|
||||
dhcp_opts = []
|
||||
if CONF.pxe.ipxe_enabled:
|
||||
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',
|
||||
'opt_value': ipxe_script_url})
|
||||
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',
|
||||
'opt_value': CONF.pxe.pxe_bootfile_name})
|
||||
'opt_value': boot_file})
|
||||
|
||||
dhcp_opts.append({'opt_name': 'server-ip-address',
|
||||
'opt_value': CONF.pxe.tftp_server})
|
||||
dhcp_opts.append({'opt_name': 'tftp-server',
|
||||
'opt_value': CONF.pxe.tftp_server})
|
||||
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)
|
||||
|
@ -67,3 +67,11 @@ class BaseDHCP(object):
|
||||
|
||||
: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
|
||||
"""
|
||||
|
@ -25,11 +25,13 @@ from ironic.common import i18n
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import keystone
|
||||
from ironic.common import network
|
||||
from ironic.common import utils
|
||||
from ironic.dhcp import base
|
||||
from ironic.drivers.modules import ssh
|
||||
from ironic.openstack.common import log as logging
|
||||
|
||||
|
||||
_LE = i18n._LE
|
||||
_LW = i18n._LW
|
||||
|
||||
neutron_opts = [
|
||||
@ -174,3 +176,85 @@ class NeutronDHCPApi(base.BaseDHCP):
|
||||
if isinstance(task.driver.power, ssh.SSHPower):
|
||||
LOG.debug("Waiting 15 seconds for Neutron.")
|
||||
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
|
||||
|
@ -26,3 +26,6 @@ class NoneDHCPApi(base.BaseDHCP):
|
||||
|
||||
def update_port_address(self, port_id, address):
|
||||
pass
|
||||
|
||||
def get_ip_addresses(self, task):
|
||||
return []
|
||||
|
@ -217,7 +217,7 @@ class AgentDeploy(base.DeployInterface):
|
||||
:param task: a TaskManager instance.
|
||||
: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.update_dhcp(task, dhcp_opts)
|
||||
manager_utils.node_set_boot_device(task, 'pxe', persistent=True)
|
||||
|
@ -160,15 +160,21 @@ def block_uuid(dev):
|
||||
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."""
|
||||
with open(path) as f:
|
||||
lines = f.readlines()
|
||||
root = 'UUID=%s' % root_uuid
|
||||
pxe_cmd = 'goto' if CONF.pxe.ipxe_enabled else 'default'
|
||||
rre = re.compile(r'\{\{ ROOT \}\}')
|
||||
|
||||
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:
|
||||
for line in lines:
|
||||
line = rre.sub(root, line)
|
||||
|
11
ironic/drivers/modules/elilo_efi_pxe_config.template
Normal file
11
ironic/drivers/modules/elilo_efi_pxe_config.template
Normal 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"
|
@ -41,11 +41,19 @@ from ironic.openstack.common import fileutils
|
||||
from ironic.openstack.common import log as logging
|
||||
|
||||
|
||||
_LE = i18n._LE
|
||||
_LW = i18n._LW
|
||||
|
||||
pxe_opts = [
|
||||
cfg.StrOpt('pxe_config_template',
|
||||
default=paths.basedir_def(
|
||||
'drivers/modules/pxe_config.template'),
|
||||
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',
|
||||
default='$my_ip',
|
||||
help='IP address of Ironic compute node\'s tftp server.'),
|
||||
@ -60,6 +68,9 @@ pxe_opts = [
|
||||
cfg.StrOpt('pxe_bootfile_name',
|
||||
default='pxelinux.0',
|
||||
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',
|
||||
help='Ironic compute node\'s HTTP server URL. '
|
||||
'Example: http://192.1.2.3:8080'),
|
||||
@ -168,6 +179,7 @@ def _build_pxe_config_options(node, pxe_info, ctx):
|
||||
'aki_path': kernel,
|
||||
'ari_path': ramdisk,
|
||||
'pxe_append_params': CONF.pxe.pxe_append_params,
|
||||
'tftp_server': CONF.pxe.tftp_server
|
||||
}
|
||||
|
||||
deploy_ramdisk_options = iscsi_deploy.build_deploy_ramdisk_options(node,
|
||||
@ -265,11 +277,22 @@ class PXEDeploy(base.DeployInterface):
|
||||
:raises: InvalidParameterValue.
|
||||
:raises: MissingParameterValue
|
||||
"""
|
||||
# Check the boot_mode capability parameter value.
|
||||
pxe_utils.validate_boot_mode_capability(task.node)
|
||||
|
||||
if CONF.pxe.ipxe_enabled:
|
||||
if not CONF.pxe.http_url or not CONF.pxe.http_root:
|
||||
raise exception.MissingParameterValue(_(
|
||||
"iPXE boot is enabled but no HTTP URL or HTTP "
|
||||
"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)
|
||||
|
||||
@ -299,10 +322,25 @@ class PXEDeploy(base.DeployInterface):
|
||||
# TODO(yuriyz): more secure way needed for pass auth token
|
||||
# to deploy ramdisk
|
||||
_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.update_dhcp(task, dhcp_opts)
|
||||
|
||||
# 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)
|
||||
|
||||
return states.DEPLOYWAIT
|
||||
@ -338,8 +376,14 @@ class PXEDeploy(base.DeployInterface):
|
||||
pxe_info = _get_image_info(task.node, task.context)
|
||||
pxe_options = _build_pxe_config_options(task.node, pxe_info,
|
||||
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,
|
||||
CONF.pxe.pxe_config_template)
|
||||
pxe_config_template)
|
||||
_cache_ramdisk_kernel(task.context, task.node, pxe_info)
|
||||
|
||||
def clean_up(self, task):
|
||||
@ -364,7 +408,7 @@ class PXEDeploy(base.DeployInterface):
|
||||
_destroy_token_file(node)
|
||||
|
||||
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.update_dhcp(task, dhcp_opts)
|
||||
|
||||
@ -420,7 +464,8 @@ class VendorPassthru(base.VendorInterface):
|
||||
|
||||
try:
|
||||
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'])
|
||||
|
||||
|
@ -21,6 +21,7 @@ from neutronclient.v2_0 import client
|
||||
from ironic.common import dhcp_factory
|
||||
from ironic.common import exception
|
||||
from ironic.common import pxe_utils
|
||||
from ironic.common import utils
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.dhcp import neutron
|
||||
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.common.network.get_node_vif_ids')
|
||||
def test_update_dhcp(self, mock_gnvi, mock_updo):
|
||||
opts = pxe_utils.dhcp_options_for_instance()
|
||||
mock_gnvi.return_value = {'port-uuid': 'vif-uuid'}
|
||||
with task_manager.acquire(self.context,
|
||||
self.node.uuid) as task:
|
||||
opts = pxe_utils.dhcp_options_for_instance(task)
|
||||
api = dhcp_factory.DHCPFactory(token=self.context.auth_token)
|
||||
api.update_dhcp(task, self.node)
|
||||
mock_updo.assertCalleOnceWith('vif-uuid', opts)
|
||||
@ -224,3 +225,124 @@ class TestNeutron(base.TestCase):
|
||||
task, self.node)
|
||||
mock_gnvi.assertCalleOnceWith(task)
|
||||
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)
|
||||
|
@ -57,9 +57,9 @@ class TestAgentDeploy(db_base.DbTestCase):
|
||||
@mock.patch('ironic.conductor.utils.node_set_boot_device')
|
||||
@mock.patch('ironic.conductor.utils.node_power_action')
|
||||
def test_deploy(self, power_mock, bootdev_mock, dhcp_mock):
|
||||
dhcp_opts = pxe_utils.dhcp_options_for_instance()
|
||||
with task_manager.acquire(
|
||||
self.context, self.node['uuid'], shared=False) as task:
|
||||
dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
|
||||
driver_return = self.driver.deploy(task)
|
||||
self.assertEqual(driver_return, states.DEPLOYWAIT)
|
||||
dhcp_mock.assert_called_once_with(task, dhcp_opts)
|
||||
|
@ -91,6 +91,34 @@ append initrd=ramdisk root=UUID=12345678-1234-1234-1234-1234567890abcdef
|
||||
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):
|
||||
def setUp(self):
|
||||
@ -392,8 +420,11 @@ class PhysicalWorkTestCase(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()
|
||||
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.close(fd)
|
||||
@ -401,22 +432,36 @@ class SwitchPxeConfigTestCase(tests_base.TestCase):
|
||||
return fname
|
||||
|
||||
def test_switch_pxe_config(self):
|
||||
boot_mode = 'bios'
|
||||
fname = self._create_config()
|
||||
utils.switch_pxe_config(fname,
|
||||
'12345678-1234-1234-1234-1234567890abcdef')
|
||||
'12345678-1234-1234-1234-1234567890abcdef',
|
||||
boot_mode)
|
||||
with open(fname, 'r') as f:
|
||||
pxeconf = f.read()
|
||||
self.assertEqual(_PXECONF_BOOT, pxeconf)
|
||||
|
||||
def test_switch_ipxe_config(self):
|
||||
boot_mode = 'bios'
|
||||
cfg.CONF.set_override('ipxe_enabled', True, 'pxe')
|
||||
fname = self._create_config(ipxe=True)
|
||||
utils.switch_pxe_config(fname,
|
||||
'12345678-1234-1234-1234-1234567890abcdef')
|
||||
'12345678-1234-1234-1234-1234567890abcdef',
|
||||
boot_mode)
|
||||
with open(fname, 'r') as f:
|
||||
pxeconf = f.read()
|
||||
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):
|
||||
def test_get_dev(self):
|
||||
|
@ -166,6 +166,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
|
||||
|
||||
fake_key = '0123456789ABCDEFGHIJKLMNOPQRSTUV'
|
||||
random_alnum_mock.return_value = fake_key
|
||||
tftp_server = CONF.pxe.tftp_server
|
||||
|
||||
if ipxe_enabled:
|
||||
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',
|
||||
'ironic_api_url': 'http://192.168.122.184:6385',
|
||||
'deployment_aki_path': deploy_kernel,
|
||||
'disk': 'sda'
|
||||
'disk': 'sda',
|
||||
'tftp_server': tftp_server
|
||||
}
|
||||
|
||||
image_info = {'deploy_kernel': ('deploy_kernel',
|
||||
@ -342,6 +344,30 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
self.assertRaises(exception.MissingParameterValue,
|
||||
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):
|
||||
new_node = obj_utils.create_test_node(
|
||||
self.context,
|
||||
@ -480,10 +506,10 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
fake_img_path = '/test/path/test.img'
|
||||
mock_get_image_file_path.return_value = fake_img_path
|
||||
mock_get_image_mb.return_value = 1
|
||||
dhcp_opts = pxe_utils.dhcp_options_for_instance()
|
||||
|
||||
with task_manager.acquire(self.context,
|
||||
self.node.uuid, shared=False) as task:
|
||||
dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
|
||||
state = task.driver.deploy.deploy(task)
|
||||
self.assertEqual(state, states.DEPLOYWAIT)
|
||||
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')
|
||||
def test_take_over(self, update_dhcp_mock):
|
||||
dhcp_opts = pxe_utils.dhcp_options_for_instance()
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=True) as task:
|
||||
dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
|
||||
task.driver.deploy.take_over(task)
|
||||
update_dhcp_mock.assert_called_once_with(
|
||||
task, dhcp_opts)
|
||||
@ -548,6 +574,7 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
self.node.save()
|
||||
|
||||
root_uuid = "12345678-1234-1234-1234-1234567890abcxyz"
|
||||
boot_mode = None
|
||||
|
||||
def fake_deploy(**kwargs):
|
||||
return root_uuid
|
||||
@ -568,7 +595,8 @@ class PXEDriverTestCase(db_base.DbTestCase):
|
||||
mock_image_cache.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)
|
||||
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')
|
||||
|
||||
@mock.patch.object(iscsi_deploy, 'InstanceImageCache')
|
||||
|
@ -19,6 +19,7 @@ import os
|
||||
import mock
|
||||
from oslo.config import cfg
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import pxe_utils
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.db import api as dbapi
|
||||
@ -92,6 +93,27 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
unlink_mock.assert_has_calls(unlink_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.object(pxe_utils, '_build_pxe_config')
|
||||
@mock.patch('ironic.openstack.common.fileutils.ensure_tree')
|
||||
@ -139,6 +161,11 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
self.assertEqual('/httpboot/pxelinux.cfg/00112233aabbcc',
|
||||
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):
|
||||
expected_dir = '/tftproot'
|
||||
self.config(ipxe_enabled=False, group='pxe')
|
||||
@ -167,7 +194,9 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
{'opt_name': 'tftp-server',
|
||||
'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):
|
||||
node_uuid = 'fake-node'
|
||||
@ -222,5 +251,56 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
'opt_value': '192.0.2.1'},
|
||||
{'opt_name': 'bootfile-name',
|
||||
'opt_value': expected_boot_script_url}]
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertEqual(sorted(expected_info),
|
||||
sorted(pxe_utils.dhcp_options_for_instance()))
|
||||
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))
|
||||
|
Loading…
Reference in New Issue
Block a user