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)
|
# 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>
|
||||||
|
@ -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.")
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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 []
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
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
|
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'])
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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')
|
||||||
|
@ -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))
|
||||||
|
Loading…
Reference in New Issue
Block a user