Merge "Add UEFI based deployment support in Ironic"
This commit is contained in:
commit
1c03cb20a3
@ -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