Merge "Add UEFI based deployment support in Ironic"

This commit is contained in:
Jenkins 2014-09-03 20:01:39 +00:00 committed by Gerrit Code Review
commit 1c03cb20a3
15 changed files with 585 additions and 29 deletions

View File

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

View File

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

View File

@ -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,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 = _build_pxe_config(pxe_options, template)
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):
@ -159,15 +208,31 @@ def clean_up_pxe_config(task):
"""
LOG.debug("Cleaning up PXE config for node %s", task.node.uuid)
for mac in driver_utils.get_node_mac_addresses(task):
utils.unlink_without_raise(_get_pxe_mac_path(mac))
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))
utils.rmtree_without_raise(os.path.join(get_root_dir(),
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)

View File

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

View File

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

View File

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

View File

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

View File

@ -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 \}\}')
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:
for line in lines:
line = rre.sub(root, line)

View File

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

View File

@ -41,11 +41,19 @@ from ironic.openstack.common import fileutils
from ironic.openstack.common import log as logging
_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)
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)
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'])

View File

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

View File

@ -57,9 +57,9 @@ class TestAgentDeploy(db_base.DbTestCase):
@mock.patch('ironic.conductor.utils.node_set_boot_device')
@mock.patch('ironic.conductor.utils.node_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)

View File

@ -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,31 +420,48 @@ 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()
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.close(fd)
self.addCleanup(os.unlink, fname)
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):

View File

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

View File

@ -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}]
self.assertEqual(sorted(expected_info),
sorted(pxe_utils.dhcp_options_for_instance()))
with task_manager.acquire(self.context, self.node.uuid) as task:
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))