diff options
-rw-r--r-- | doc/source/deploy/install-guide.rst | 49 | ||||
-rw-r--r-- | doc/source/deploy/upgrade-guide.rst | 36 | ||||
-rw-r--r-- | doc/source/index.rst | 1 | ||||
-rw-r--r-- | etc/ironic/ironic.conf.sample | 5 | ||||
-rw-r--r-- | ironic/common/grub_conf.template | 10 | ||||
-rw-r--r-- | ironic/common/pxe_utils.py | 33 | ||||
-rw-r--r-- | ironic/drivers/modules/agent.py | 45 | ||||
-rw-r--r-- | ironic/drivers/modules/boot.ipxe | 2 | ||||
-rw-r--r-- | ironic/drivers/modules/deploy_utils.py | 19 | ||||
-rw-r--r-- | ironic/drivers/modules/ilo/common.py | 2 | ||||
-rw-r--r-- | ironic/drivers/modules/ilo/deploy.py | 4 | ||||
-rw-r--r-- | ironic/drivers/modules/ipmitool.py | 29 | ||||
-rw-r--r-- | ironic/drivers/modules/iscsi_deploy.py | 11 | ||||
-rw-r--r-- | ironic/tests/drivers/test_agent.py | 69 | ||||
-rw-r--r-- | ironic/tests/drivers/test_ipmitool.py | 10 | ||||
-rw-r--r-- | ironic/tests/drivers/test_iscsi_deploy.py | 19 | ||||
-rw-r--r-- | ironic/tests/test_images.py | 11 | ||||
-rw-r--r-- | ironic/tests/test_pxe_utils.py | 37 |
18 files changed, 309 insertions, 83 deletions
diff --git a/doc/source/deploy/install-guide.rst b/doc/source/deploy/install-guide.rst index 2907ad54b..871e75313 100644 --- a/doc/source/deploy/install-guide.rst +++ b/doc/source/deploy/install-guide.rst @@ -4,8 +4,9 @@ Bare Metal Service Installation Guide ===================================== -This document pertains to the Juno (2014.2) release of OpenStack. Users of -earlier releases may encounter some differences in configuration of services. +This document pertains to the Kilo (2015.1) release of OpenStack Ironic. Users +of earlier releases may encounter differences, and are encouraged to look at +earlier versions of this document for guidance. Service Overview @@ -14,23 +15,33 @@ Service Overview The Bare Metal Service is a collection of components that provides support to manage and provision physical machines. -Also known as the ``ironic`` project, the Bare Metal Service interacts with -several other OpenStack services such as: +Also known as the ``Ironic`` project, the Bare Metal Service may, depending +upon configuration, interact with several other OpenStack services. This +includes: -- the Identity Service (keystone) for request authentication and to +- the Telemetry (Ceilometer) for consuming the IPMI metrics +- the Identity Service (Keystone) for request authentication and to locate other OpenStack services -- the Image Service (glance) from which to retrieve images -- the Networking Service (neutron) for DHCP and network configuration -- the Compute Service (nova), which leverages the Bare Metal Service to - manage compute instances on bare metal. +- the Image Service (Glance) from which to retrieve images and image meta-data +- the Networking Service (Neutron) for DHCP and network configuration +- the Compute Service (Nova) works with Ironic and acts as a user-facing API + for instance management, while Ironic provides the admin/operator API for + hardware management. Nova also provides scheduling facilities (matching + flavors <-> images <-> hardware), tenant quotas, IP assignment, and other + services which Ironic does not, in and of itself, provide. + +- the Block Storage (Cinder) will provide volumes, but the aspect is not yet available. The Bare Metal Service includes the following components: -- ironic-api. A RESTful API that processes application requests by sending +- ironic-api: A RESTful API that processes application requests by sending them to the ironic-conductor over RPC. -- ironic-conductor. Adds/edits/deletes nodes; powers on/off nodes with +- ironic-conductor: Adds/edits/deletes nodes; powers on/off nodes with ipmi or ssh; provisions/deploys/decommissions bare metal nodes. -- Ironic client. A command-line interface (CLI) for interacting with +- ironic-python-agent: A python service which is run in a temporary ramdisk to + provide ironic-conductor service(s) with remote access and in-band hardware + control. +- python-ironicclient: A command-line interface (CLI) for interacting with the Bare Metal Service. Additionally, the Bare Metal Service has certain external dependencies, which are @@ -44,6 +55,20 @@ very similar to other OpenStack Services: - A queue. A central hub for passing messages. It should use the same implementation as that of the Compute Service (typically RabbitMQ). +Optionally, one may wish to utilize the following associated projects for +additional functionality: + +- ironic-discoverd_; An associated service which performs in-band hardware + introspection by PXE booting unregistered hardware into a "discovery ramdisk". +- diskimage-builder_; May be used to customize machine images, create and + discovery deploy ramdisks, if necessary. +.. _ironic-discoverd: https://github.com/stackforge/ironic-discoverd +.. _diskimage-builder: https://github.com/openstack/diskimage-builder + + +.. todo: include coreos-image-builder reference here, once the split is done + + Install and Configure Prerequisites =================================== diff --git a/doc/source/deploy/upgrade-guide.rst b/doc/source/deploy/upgrade-guide.rst new file mode 100644 index 000000000..2ce9e0952 --- /dev/null +++ b/doc/source/deploy/upgrade-guide.rst @@ -0,0 +1,36 @@ +.. _upgrade-guide: + +===================================== +Bare Metal Service Upgrade Guide +===================================== + +This document outlines various steps and notes for operators to consider when +upgrading their Ironic-driven clouds from previous versions of OpenStack. + +The Ironic service is tightly coupled with the Ironic driver that is shipped +with Nova. Currently, some special considerations must be taken into account +when upgrading your cloud from previous versions of OpenStack. + +Upgrading from Juno to Kilo +=========================== + +When upgrading a cloud from Juno to Kilo, users must ensure the Nova +service is upgraded prior to upgrading the Ironic service. Additionally, +users need to set a special config flag in Nova prior to upgrading to ensure +the newer version of Nova is not attempting to take advantage of new Ironic +features until the Ironic service has been upgraded. The steps for upgrading +your Nova and Ironic services are as follows: + +- Edit nova.conf and ensure force_config_drive=False is set in the [DEFAULT] + group. Restart nova-compute if necessary. +- Install new Nova code, run database migrations +- Install new python-ironicclient code. +- Restart Nova services. +- Install new Ironic code, run database migrations, restart Ironic services. +- Edit nova.conf and set force_config_drive to your liking, restaring + nova-compute if necessary. + +Note that during the period between Nova's upgrade and Ironic's upgrades, +instances can still be provisioned to nodes, however, any attempt by users +to specify a config drive for an instance will cause error until Ironic's +upgrade has completed. diff --git a/doc/source/index.rst b/doc/source/index.rst index a3339207d..7ea533fdb 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -57,6 +57,7 @@ Overview deploy/user-guide deploy/install-guide + deploy/upgrade-guide deploy/drivers deploy/cleaning diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index a8a1501a6..ccf368f0d 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -360,6 +360,11 @@ # set to 0, will not run during cleaning. (integer value) #agent_erase_devices_priority=<None> +# Whether Ironic will manage TFTP files for the deploy +# ramdisks. If set to False, you will need to configure your +# own TFTP server that allows booting the deploy ramdisks. +# (boolean value) +#manage_tftp=true # # Options defined in ironic.drivers.modules.agent_base_vendor diff --git a/ironic/common/grub_conf.template b/ironic/common/grub_conf.template index 746a43d97..2a979d2d6 100644 --- a/ironic/common/grub_conf.template +++ b/ironic/common/grub_conf.template @@ -1,4 +1,8 @@ -menuentry "install" { -linux {{ linux }} {{ kernel_params }} -- -initrd {{ initrd }} +set default=0 +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "boot_partition" { +linuxefi {{ linux }} {{ kernel_params }} -- +initrdefi {{ initrd }} } diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index a125da034..09528a218 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -80,12 +80,20 @@ def _link_mac_pxe_configs(task): :param task: A TaskManager instance. """ - pxe_config_file_path = get_pxe_config_file_path(task.node.uuid) - for mac in driver_utils.get_node_mac_addresses(task): - mac_path = _get_pxe_mac_path(mac) + + def create_link(mac_path): utils.unlink_without_raise(mac_path) utils.create_link_without_raise(pxe_config_file_path, mac_path) + pxe_config_file_path = get_pxe_config_file_path(task.node.uuid) + for mac in driver_utils.get_node_mac_addresses(task): + create_link(_get_pxe_mac_path(mac)) + # TODO(lucasagomes): Backward compatibility with :hexraw, + # to be removed in M. + # see: https://bugs.launchpad.net/ironic/+bug/1441710 + if CONF.pxe.ipxe_enabled: + create_link(_get_pxe_mac_path(mac, delimiter='')) + def _link_ip_address_pxe_configs(task): """Link each IP address with the PXE configuration file. @@ -110,17 +118,20 @@ def _link_ip_address_pxe_configs(task): ip_address_path) -def _get_pxe_mac_path(mac): +def _get_pxe_mac_path(mac, delimiter=None): """Convert a MAC address into a PXE config file name. :param mac: A MAC address string in the format xx:xx:xx:xx:xx:xx. + :param delimiter: The MAC address delimiter. Defaults to dash ('-'). :returns: the path to the config file. """ - if CONF.pxe.ipxe_enabled: - mac_file_name = mac.replace(':', '').lower() - else: - mac_file_name = "01-" + mac.replace(":", "-").lower() + if delimiter is None: + delimiter = '-' + + mac_file_name = mac.replace(':', delimiter).lower() + if not CONF.pxe.ipxe_enabled: + mac_file_name = '01-' + mac_file_name return os.path.join(get_root_dir(), PXE_CFG_DIR_NAME, mac_file_name) @@ -221,6 +232,12 @@ def clean_up_pxe_config(task): else: for mac in driver_utils.get_node_mac_addresses(task): utils.unlink_without_raise(_get_pxe_mac_path(mac)) + # TODO(lucasagomes): Backward compatibility with :hexraw, + # to be removed in M. + # see: https://bugs.launchpad.net/ironic/+bug/1441710 + if CONF.pxe.ipxe_enabled: + utils.unlink_without_raise(_get_pxe_mac_path(mac, + delimiter='')) utils.rmtree_without_raise(os.path.join(get_root_dir(), task.node.uuid)) diff --git a/ironic/drivers/modules/agent.py b/ironic/drivers/modules/agent.py index c257d2c77..6a590c8c4 100644 --- a/ironic/drivers/modules/agent.py +++ b/ironic/drivers/modules/agent.py @@ -57,7 +57,14 @@ agent_opts = [ 'Python Agent ramdisk. If unset, will use the priority ' 'set in the ramdisk (defaults to 10 for the ' 'GenericHardwareManager). If set to 0, will not run ' - 'during cleaning.') + 'during cleaning.'), + cfg.BoolOpt('manage_tftp', + default=True, + help='Whether Ironic will manage TFTP files for the deploy ' + 'ramdisks. If set to False, you will need to configure ' + 'your own TFTP server that allows booting the deploy ' + 'ramdisks.' + ), ] CONF = cfg.CONF @@ -196,12 +203,13 @@ def build_instance_info_for_deploy(task): def _prepare_pxe_boot(task): """Prepare the files required for PXE booting the agent.""" - pxe_info = _get_tftp_image_info(task.node) - pxe_options = _build_pxe_config_options(task.node, pxe_info) - pxe_utils.create_pxe_config(task, - pxe_options, - CONF.agent.agent_pxe_config_template) - _cache_tftp_images(task.context, task.node, pxe_info) + if CONF.agent.manage_tftp: + pxe_info = _get_tftp_image_info(task.node) + pxe_options = _build_pxe_config_options(task.node, pxe_info) + pxe_utils.create_pxe_config(task, + pxe_options, + CONF.agent.agent_pxe_config_template) + _cache_tftp_images(task.context, task.node, pxe_info) def _do_pxe_boot(task, ports=None): @@ -220,13 +228,13 @@ def _do_pxe_boot(task, ports=None): def _clean_up_pxe(task): """Clean up left over PXE and DHCP files.""" - pxe_info = _get_tftp_image_info(task.node) - for label in pxe_info: - path = pxe_info[label][1] - utils.unlink_without_raise(path) - AgentTFTPImageCache().clean_up() - - pxe_utils.clean_up_pxe_config(task) + if CONF.agent.manage_tftp: + pxe_info = _get_tftp_image_info(task.node) + for label in pxe_info: + path = pxe_info[label][1] + utils.unlink_without_raise(path) + AgentTFTPImageCache().clean_up() + pxe_utils.clean_up_pxe_config(task) class AgentDeploy(base.DeployInterface): @@ -251,10 +259,11 @@ class AgentDeploy(base.DeployInterface): """ node = task.node params = {} - params['driver_info.deploy_kernel'] = node.driver_info.get( - 'deploy_kernel') - params['driver_info.deploy_ramdisk'] = node.driver_info.get( - 'deploy_ramdisk') + if CONF.agent.manage_tftp: + params['driver_info.deploy_kernel'] = node.driver_info.get( + 'deploy_kernel') + params['driver_info.deploy_ramdisk'] = node.driver_info.get( + 'deploy_ramdisk') image_source = node.instance_info.get('image_source') params['instance_info.image_source'] = image_source error_msg = _('Node %s failed to validate deploy image info. Some ' diff --git a/ironic/drivers/modules/boot.ipxe b/ironic/drivers/modules/boot.ipxe index 25a0ea8dc..3567dc029 100644 --- a/ironic/drivers/modules/boot.ipxe +++ b/ironic/drivers/modules/boot.ipxe @@ -1,7 +1,7 @@ #!ipxe # load the MAC-specific file or fail if it's not found -chain --autofree pxelinux.cfg/${mac:hexraw} || goto error_no_config +chain --autofree pxelinux.cfg/${mac:hexhyp} || goto error_no_config :error_no_config echo PXE boot failed. No configuration found for MAC ${mac} diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 79fcaecd3..e6c2c48ef 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -1046,24 +1046,22 @@ def is_secure_boot_requested(node): def get_boot_mode_for_deploy(node): """Returns the boot mode that would be used for deploy. - This method returns boot mode to used for deploy using following order: + This method returns boot mode to be used for deploy. It returns 'uefi' if 'secure_boot' is set to 'true' in 'instance_info/capabilities' of node. - It returns value of 'boot_mode' in 'properties/capabilities' of node. - It returns boot mode specified in 'instance_info/deploy_boot_mode' of - node. + Otherwise it returns value of 'boot_mode' in 'properties/capabilities' + of node if set. If that is not set, it returns boot mode in + 'instance_info/deploy_boot_mode' for the node. It would return None if boot mode is present neither in 'capabilities' of - node 'properties' nor in node's 'instance_info'. + node 'properties' nor in node's 'instance_info' (which could also be None). :param node: an ironic node object. :returns: 'bios', 'uefi' or None """ if is_secure_boot_requested(node): - boot_mode = 'uefi' - LOG.debug('Deploy boot mode is %(boot_mode)s for %(node)s.', - {'boot_mode': boot_mode, 'node': node.uuid}) - return boot_mode + LOG.debug('Deploy boot mode is uefi for %s.', node.uuid) + return 'uefi' boot_mode = driver_utils.get_node_capability(node, 'boot_mode') if boot_mode is None: @@ -1072,4 +1070,5 @@ def get_boot_mode_for_deploy(node): LOG.debug('Deploy boot mode is %(boot_mode)s for %(node)s.', {'boot_mode': boot_mode, 'node': node.uuid}) - return boot_mode + + return boot_mode.lower() if boot_mode else boot_mode diff --git a/ironic/drivers/modules/ilo/common.py b/ironic/drivers/modules/ilo/common.py index 02085ea5d..1d1363d0d 100644 --- a/ironic/drivers/modules/ilo/common.py +++ b/ironic/drivers/modules/ilo/common.py @@ -348,7 +348,7 @@ def update_boot_mode(task): if boot_mode is not None: LOG.debug("Node %(uuid)s boot mode is being set to %(boot_mode)s", {'uuid': node.uuid, 'boot_mode': boot_mode}) - set_boot_mode(node, boot_mode.lower()) + set_boot_mode(node, boot_mode) return LOG.debug("Check pending boot mode for node %s.", node.uuid) diff --git a/ironic/drivers/modules/ilo/deploy.py b/ironic/drivers/modules/ilo/deploy.py index dbd9d7340..86dc0c729 100644 --- a/ironic/drivers/modules/ilo/deploy.py +++ b/ironic/drivers/modules/ilo/deploy.py @@ -335,8 +335,8 @@ def _prepare_node_for_deploy(task): if change_boot_mode: ilo_common.update_boot_mode(task) else: - # Need to update boot mode that would used during deploy, if one is not - # provided. + # Need to update boot mode that will be used during deploy, if one is + # not provided. # Since secure boot was disabled, we are in 'uefi' boot mode. if deploy_utils.get_boot_mode_for_deploy(task.node) is None: instance_info = task.node.instance_info diff --git a/ironic/drivers/modules/ipmitool.py b/ironic/drivers/modules/ipmitool.py index 55fc1fa2d..ddebd68ef 100644 --- a/ironic/drivers/modules/ipmitool.py +++ b/ironic/drivers/modules/ipmitool.py @@ -111,7 +111,11 @@ ipmitool_command_options = { 'dual_bridge': ['ipmitool', '-m', '0', '-b', '0', '-t', '0', '-B', '0', '-T', '0', '-h']} -# Note(TheJulia): This string is hardcoded in ipmitool's lanplus driver. +# Note(TheJulia): This string is hardcoded in ipmitool's lanplus driver +# and is substituted in return for the error code received from the IPMI +# controller. As of 1.8.15, no internationalization support appears to +# be in ipmitool which means the string should always be returned in this +# form regardless of locale. IPMITOOL_RETRYABLE_FAILURES = ['insufficient resources for session'] @@ -348,14 +352,14 @@ def _exec_ipmitool(driver_info, command): args.append('-N') args.append(str(CONF.ipmi.min_command_interval)) - end_time = (_time() + CONF.ipmi.retry_timeout) + end_time = (time.time() + CONF.ipmi.retry_timeout) while True: num_tries = num_tries - 1 # NOTE(deva): ensure that no communications are sent to a BMC more # often than once every min_command_interval seconds. time_till_next_poll = CONF.ipmi.min_command_interval - ( - _time() - LAST_CMD_TIME.get(driver_info['address'], 0)) + time.time() - LAST_CMD_TIME.get(driver_info['address'], 0)) if time_till_next_poll > 0: time.sleep(time_till_next_poll) # Resetting the list that will be utilized so the password arguments @@ -377,21 +381,21 @@ def _exec_ipmitool(driver_info, command): with excutils.save_and_reraise_exception() as ctxt: err_list = [x for x in IPMITOOL_RETRYABLE_FAILURES if x in e.message] - if ((_time() > end_time) or + if ((time.time() > end_time) or (num_tries == 0) or not err_list): - LOG.error(_LE('IPMI Error attempting to execute ' + LOG.error(_LE('IPMI Error while attempting ' '"%(cmd)s" for node %(node)s. ' 'Error: %(error)s'), { - 'node': driver_info['uuid'], - 'cmd': e.cmd, - 'error': e + 'node': driver_info['uuid'], + 'cmd': e.cmd, + 'error': e }) else: ctxt.reraise = False LOG.warning(_LW('IPMI Error encountered, retrying ' - '"%(cmd)s" for node %(node)s ' + '"%(cmd)s" for node %(node)s. ' 'Error: %(error)s'), { 'node': driver_info['uuid'], @@ -399,7 +403,7 @@ def _exec_ipmitool(driver_info, command): 'error': e }) finally: - LAST_CMD_TIME[driver_info['address']] = _time() + LAST_CMD_TIME[driver_info['address']] = time.time() def _sleep_time(iter): @@ -626,11 +630,6 @@ def send_raw(task, raw_bytes): raise exception.IPMIFailure(cmd=cmd) -def _time(): - """Wrapper for time.time() enabling simplified unit testing.""" - return time.time() - - class IPMIPower(base.PowerInterface): def __init__(self): diff --git a/ironic/drivers/modules/iscsi_deploy.py b/ironic/drivers/modules/iscsi_deploy.py index 57df4a7d6..ac7315d33 100644 --- a/ironic/drivers/modules/iscsi_deploy.py +++ b/ironic/drivers/modules/iscsi_deploy.py @@ -422,7 +422,7 @@ def _get_boot_mode(node): """ boot_mode = deploy_utils.get_boot_mode_for_deploy(node) if boot_mode: - return boot_mode.lower() + return boot_mode return "bios" @@ -447,13 +447,20 @@ def build_deploy_ramdisk_options(node): node.instance_info = i_info node.save() + # XXX(jroll) DIB relies on boot_option=local to decide whether or not to + # lay down a bootloader. Hack this for now; fix it for real in Liberty. + # See also bug #1441556. + boot_option = get_boot_option(node) + if node.driver_internal_info.get('is_whole_disk_image'): + boot_option = 'netboot' + deploy_options = { 'deployment_id': node['uuid'], 'deployment_key': deploy_key, 'iscsi_target_iqn': "iqn-%s" % node.uuid, 'ironic_api_url': ironic_api, 'disk': CONF.pxe.disk_devices, - 'boot_option': get_boot_option(node), + 'boot_option': boot_option, 'boot_mode': _get_boot_mode(node), # NOTE: The below entry is a temporary workaround for bug/1433812 'coreos.configdrive': 0, diff --git a/ironic/tests/drivers/test_agent.py b/ironic/tests/drivers/test_agent.py index b9ea638e0..bb9675f3f 100644 --- a/ironic/tests/drivers/test_agent.py +++ b/ironic/tests/drivers/test_agent.py @@ -164,6 +164,14 @@ class TestAgentDeploy(db_base.DbTestCase): self.assertIn('driver_info.deploy_ramdisk', str(e)) self.assertIn('driver_info.deploy_kernel', str(e)) + def test_validate_driver_info_manage_tftp_false(self): + self.config(manage_tftp=False, group='agent') + self.node.driver_info = {} + self.node.save() + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + self.driver.validate(task) + def test_validate_instance_info_missing_params(self): self.node.instance_info = {} self.node.save() @@ -199,6 +207,37 @@ class TestAgentDeploy(db_base.DbTestCase): self.assertRaises(exception.InvalidParameterValue, task.driver.deploy.validate, task) + @mock.patch.object(agent, '_cache_tftp_images') + @mock.patch.object(pxe_utils, 'create_pxe_config') + @mock.patch.object(agent, '_build_pxe_config_options') + @mock.patch.object(agent, '_get_tftp_image_info') + def test__prepare_pxe_boot(self, pxe_info_mock, options_mock, + create_mock, cache_mock): + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + agent._prepare_pxe_boot(task) + pxe_info_mock.assert_called_once_with(task.node) + options_mock.assert_called_once_with(task.node, mock.ANY) + create_mock.assert_called_once_with( + task, mock.ANY, CONF.agent.agent_pxe_config_template) + cache_mock.assert_called_once_with(task.context, task.node, + mock.ANY) + + @mock.patch.object(agent, '_cache_tftp_images') + @mock.patch.object(pxe_utils, 'create_pxe_config') + @mock.patch.object(agent, '_build_pxe_config_options') + @mock.patch.object(agent, '_get_tftp_image_info') + def test__prepare_pxe_boot_manage_tftp_false( + self, pxe_info_mock, options_mock, create_mock, cache_mock): + self.config(manage_tftp=False, group='agent') + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + agent._prepare_pxe_boot(task) + self.assertFalse(pxe_info_mock.called) + self.assertFalse(options_mock.called) + self.assertFalse(create_mock.called) + self.assertFalse(cache_mock.called) + @mock.patch.object(dhcp_factory.DHCPFactory, 'update_dhcp') @mock.patch('ironic.conductor.utils.node_set_boot_device') @mock.patch('ironic.conductor.utils.node_power_action') @@ -221,6 +260,36 @@ class TestAgentDeploy(db_base.DbTestCase): power_mock.assert_called_once_with(task, states.POWER_OFF) self.assertEqual(driver_return, states.DELETED) + @mock.patch.object(pxe_utils, 'clean_up_pxe_config') + @mock.patch.object(agent, 'AgentTFTPImageCache') + @mock.patch('ironic.common.utils.unlink_without_raise') + @mock.patch.object(agent, '_get_tftp_image_info') + def test__clean_up_pxe(self, info_mock, unlink_mock, cache_mock, + clean_mock): + info_mock.return_value = {'label': ['fake1', 'fake2']} + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + agent._clean_up_pxe(task) + info_mock.assert_called_once_with(task.node) + unlink_mock.assert_called_once_with('fake2') + clean_mock.assert_called_once_with(task) + + @mock.patch.object(pxe_utils, 'clean_up_pxe_config') + @mock.patch.object(agent.AgentTFTPImageCache, 'clean_up') + @mock.patch('ironic.common.utils.unlink_without_raise') + @mock.patch.object(agent, '_get_tftp_image_info') + def test__clean_up_pxe_manage_tftp_false( + self, info_mock, unlink_mock, cache_mock, clean_mock): + self.config(manage_tftp=False, group='agent') + info_mock.return_value = {'label': ['fake1', 'fake2']} + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + agent._clean_up_pxe(task) + self.assertFalse(info_mock.called) + self.assertFalse(unlink_mock.called) + self.assertFalse(cache_mock.called) + self.assertFalse(clean_mock.called) + @mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.delete_cleaning_ports') @mock.patch('ironic.dhcp.neutron.NeutronDHCPApi.create_cleaning_ports') @mock.patch('ironic.drivers.modules.agent._do_pxe_boot') diff --git a/ironic/tests/drivers/test_ipmitool.py b/ironic/tests/drivers/test_ipmitool.py index 0828f2c60..2e0097fba 100644 --- a/ironic/tests/drivers/test_ipmitool.py +++ b/ironic/tests/drivers/test_ipmitool.py @@ -891,21 +891,21 @@ class IPMIToolPrivateMethodTestCase(db_base.DbTestCase): @mock.patch.object(ipmi, '_is_option_supported', autospec=True) @mock.patch.object(utils, 'execute', autospec=True) - def test__exec_ipmitool_exception_retry_failure_unhandable(self, + def test__exec_ipmitool_exception_non_retryable_failure(self, mock_exec, mock_support, mock_sleep): ipmi.LAST_CMD_TIME = {} mock_support.return_value = False - # Return a retryable error, then a error that cannot - # be retryable thus resulting in a single retry - # attempt by _exec_ipmitool that is successful. + # Return a retryable error, then an error that cannot + # be retried thus resulting in a single retry + # attempt by _exec_ipmitool. mock_exec.side_effect = iter([ processutils.ProcessExecutionError( stderr="insufficient resources for session" ), processutils.ProcessExecutionError( - "Unknown" + stderr="Unknown" ), ]) diff --git a/ironic/tests/drivers/test_iscsi_deploy.py b/ironic/tests/drivers/test_iscsi_deploy.py index a0b1f40ba..4d3803542 100644 --- a/ironic/tests/drivers/test_iscsi_deploy.py +++ b/ironic/tests/drivers/test_iscsi_deploy.py @@ -487,6 +487,25 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase): self._test_build_deploy_ramdisk_options(mock_alnum, fake_api_url, expected_boot_option=expected) + @mock.patch.object(keystone, 'get_service_url', autospec=True) + @mock.patch.object(utils, 'random_alnum', autospec=True) + def test_build_deploy_ramdisk_options_whole_disk_image(self, mock_alnum, + mock_get_url): + """Tests a hack to boot_option for whole disk images. + + This hack is in place to fix bug #1441556. + """ + self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} + dii = self.node.driver_internal_info + dii['is_whole_disk_image'] = True + self.node.driver_internal_info = dii + self.node.save() + expected = 'netboot' + fake_api_url = 'http://127.0.0.1:6385' + self.config(api_url=fake_api_url, group='conductor') + self._test_build_deploy_ramdisk_options(mock_alnum, fake_api_url, + expected_boot_option=expected) + def test_get_boot_option(self): self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} result = iscsi_deploy.get_boot_option(self.node) diff --git a/ironic/tests/test_images.py b/ironic/tests/test_images.py index fb1da9dbc..b610d4b0a 100644 --- a/ironic/tests/test_images.py +++ b/ironic/tests/test_images.py @@ -424,12 +424,15 @@ class FsImageTestCase(base.TestCase): self.assertEqual(expected_cfg, cfg) def test__generate_grub_cfg(self): - kernel_params = ['key1=value1', 'key2'] options = {'linux': '/vmlinuz', 'initrd': '/initrd'} - expected_cfg = ("menuentry \"install\" {\n" - "linux /vmlinuz key1=value1 key2 --\n" - "initrd /initrd\n" + expected_cfg = ("set default=0\n" + "set timeout=5\n" + "set hidden_timeout_quiet=false\n" + "\n" + "menuentry \"boot_partition\" {\n" + "linuxefi /vmlinuz key1=value1 key2 --\n" + "initrdefi /initrd\n" "}") cfg = images._generate_cfg(kernel_params, diff --git a/ironic/tests/test_pxe_utils.py b/ironic/tests/test_pxe_utils.py index f16cd3e1b..1792d840a 100644 --- a/ironic/tests/test_pxe_utils.py +++ b/ironic/tests/test_pxe_utils.py @@ -144,7 +144,40 @@ class TestPXEUtils(db_base.DbTestCase): ] unlink_calls = [ mock.call('/tftpboot/pxelinux.cfg/01-00-11-22-33-44-55-66'), - mock.call('/tftpboot/pxelinux.cfg/01-00-11-22-33-44-55-67') + mock.call('/tftpboot/pxelinux.cfg/01-00-11-22-33-44-55-67'), + ] + with task_manager.acquire(self.context, self.node.uuid) as task: + pxe_utils._link_mac_pxe_configs(task) + + 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', autospec=True) + @mock.patch('ironic.common.utils.unlink_without_raise', autospec=True) + @mock.patch('ironic.drivers.utils.get_node_mac_addresses', autospec=True) + def test__write_mac_ipxe_configs(self, get_macs_mock, unlink_mock, + create_link_mock): + self.config(ipxe_enabled=True, group='pxe') + macs = [ + '00:11:22:33:44:55:66', + '00:11:22:33:44:55:67' + ] + get_macs_mock.return_value = macs + create_link_calls = [ + mock.call(u'/httpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/config', + '/httpboot/pxelinux.cfg/00-11-22-33-44-55-66'), + mock.call(u'/httpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/config', + '/httpboot/pxelinux.cfg/00112233445566'), + mock.call(u'/httpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/config', + '/httpboot/pxelinux.cfg/00-11-22-33-44-55-67'), + mock.call(u'/httpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/config', + '/httpboot/pxelinux.cfg/00112233445567'), + ] + unlink_calls = [ + mock.call('/httpboot/pxelinux.cfg/00-11-22-33-44-55-66'), + mock.call('/httpboot/pxelinux.cfg/00112233445566'), + mock.call('/httpboot/pxelinux.cfg/00-11-22-33-44-55-67'), + mock.call('/httpboot/pxelinux.cfg/00112233445567'), ] with task_manager.acquire(self.context, self.node.uuid) as task: pxe_utils._link_mac_pxe_configs(task) @@ -218,7 +251,7 @@ class TestPXEUtils(db_base.DbTestCase): self.config(ipxe_enabled=True, group='pxe') self.config(http_root='/httpboot', group='pxe') mac = '00:11:22:33:AA:BB:CC' - self.assertEqual('/httpboot/pxelinux.cfg/00112233aabbcc', + self.assertEqual('/httpboot/pxelinux.cfg/00-11-22-33-aa-bb-cc', pxe_utils._get_pxe_mac_path(mac)) def test__get_pxe_ip_address_path(self): |