fast tracked deployment support
Provides a facility to minimize the power state changes of a baremetal node to save critical time during deployment operations. Story: #2004965 Task: #29408 Depends-On: https://review.openstack.org/636778 Change-Id: I7ebbaddb33b38c87246c10165339ac4bac0ac6fc
This commit is contained in:
parent
9625b8aa71
commit
3f734c8728
@ -449,6 +449,8 @@ IRONIC_DEPLOY_LOGS_STORAGE_BACKEND=${IRONIC_DEPLOY_LOGS_STORAGE_BACKEND:-local}
|
|||||||
# The path to the directory where Ironic should put the logs when IRONIC_DEPLOY_LOGS_STORAGE_BACKEND is set to "local"
|
# The path to the directory where Ironic should put the logs when IRONIC_DEPLOY_LOGS_STORAGE_BACKEND is set to "local"
|
||||||
IRONIC_DEPLOY_LOGS_LOCAL_PATH=${IRONIC_DEPLOY_LOGS_LOCAL_PATH:-$IRONIC_VM_LOG_DIR/deploy_logs}
|
IRONIC_DEPLOY_LOGS_LOCAL_PATH=${IRONIC_DEPLOY_LOGS_LOCAL_PATH:-$IRONIC_VM_LOG_DIR/deploy_logs}
|
||||||
|
|
||||||
|
# Fast track option
|
||||||
|
IRONIC_DEPLOY_FAST_TRACK=${IRONIC_DEPLOY_FAST_TRACK:-False}
|
||||||
|
|
||||||
# Define baremetal min_microversion in tempest config. Default value None is picked from tempest.
|
# Define baremetal min_microversion in tempest config. Default value None is picked from tempest.
|
||||||
TEMPEST_BAREMETAL_MIN_MICROVERSION=${TEMPEST_BAREMETAL_MIN_MICROVERSION:-}
|
TEMPEST_BAREMETAL_MIN_MICROVERSION=${TEMPEST_BAREMETAL_MIN_MICROVERSION:-}
|
||||||
@ -1115,6 +1117,9 @@ function configure_ironic {
|
|||||||
iniset $IRONIC_CONF_FILE DEFAULT rpc_transport $IRONIC_RPC_TRANSPORT
|
iniset $IRONIC_CONF_FILE DEFAULT rpc_transport $IRONIC_RPC_TRANSPORT
|
||||||
iniset $IRONIC_CONF_FILE json_rpc port $IRONIC_JSON_RPC_PORT
|
iniset $IRONIC_CONF_FILE json_rpc port $IRONIC_JSON_RPC_PORT
|
||||||
|
|
||||||
|
# Set fast track options
|
||||||
|
iniset $IRONIC_CONF_FILE deploy fast_track $IRONIC_DEPLOY_FAST_TRACK
|
||||||
|
|
||||||
# Configure Ironic conductor, if it was enabled.
|
# Configure Ironic conductor, if it was enabled.
|
||||||
if is_service_enabled ir-cond; then
|
if is_service_enabled ir-cond; then
|
||||||
configure_ironic_conductor
|
configure_ironic_conductor
|
||||||
@ -2553,6 +2558,8 @@ function ironic_configure_tempest {
|
|||||||
|
|
||||||
# Enabled features
|
# Enabled features
|
||||||
iniset $TEMPEST_CONFIG baremetal_feature_enabled ipxe_enabled $IRONIC_IPXE_ENABLED
|
iniset $TEMPEST_CONFIG baremetal_feature_enabled ipxe_enabled $IRONIC_IPXE_ENABLED
|
||||||
|
iniset $TEMPEST_CONFIG baremetal_feature_enabled fast_track_discovery $IRONIC_DEPLOY_FAST_TRACK
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_ironic_node_prefix {
|
function get_ironic_node_prefix {
|
||||||
|
@ -37,10 +37,6 @@ LOG = log.getLogger(__name__)
|
|||||||
|
|
||||||
_LOOKUP_RETURN_FIELDS = ('uuid', 'properties', 'instance_info',
|
_LOOKUP_RETURN_FIELDS = ('uuid', 'properties', 'instance_info',
|
||||||
'driver_internal_info')
|
'driver_internal_info')
|
||||||
_LOOKUP_ALLOWED_STATES = {states.DEPLOYING, states.DEPLOYWAIT,
|
|
||||||
states.CLEANING, states.CLEANWAIT,
|
|
||||||
states.INSPECTING,
|
|
||||||
states.RESCUING, states.RESCUEWAIT}
|
|
||||||
|
|
||||||
|
|
||||||
def config():
|
def config():
|
||||||
@ -83,6 +79,12 @@ class LookupResult(base.APIBase):
|
|||||||
class LookupController(rest.RestController):
|
class LookupController(rest.RestController):
|
||||||
"""Controller handling node lookup for a deploy ramdisk."""
|
"""Controller handling node lookup for a deploy ramdisk."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lookup_allowed_states(self):
|
||||||
|
if CONF.deploy.fast_track:
|
||||||
|
return states.FASTTRACK_LOOKUP_ALLOWED_STATES
|
||||||
|
return states.LOOKUP_ALLOWED_STATES
|
||||||
|
|
||||||
@expose.expose(LookupResult, types.listtype, types.uuid)
|
@expose.expose(LookupResult, types.listtype, types.uuid)
|
||||||
def get_all(self, addresses=None, node_uuid=None):
|
def get_all(self, addresses=None, node_uuid=None):
|
||||||
"""Look up a node by its MAC addresses and optionally UUID.
|
"""Look up a node by its MAC addresses and optionally UUID.
|
||||||
@ -144,7 +146,7 @@ class LookupController(rest.RestController):
|
|||||||
raise exception.NotFound()
|
raise exception.NotFound()
|
||||||
|
|
||||||
if (CONF.api.restrict_lookup
|
if (CONF.api.restrict_lookup
|
||||||
and node.provision_state not in _LOOKUP_ALLOWED_STATES):
|
and node.provision_state not in self.lookup_allowed_states):
|
||||||
raise exception.NotFound()
|
raise exception.NotFound()
|
||||||
|
|
||||||
return LookupResult.convert_with_links(node)
|
return LookupResult.convert_with_links(node)
|
||||||
|
@ -799,7 +799,12 @@ def build_service_pxe_config(task, instance_image_info,
|
|||||||
# NOTE(pas-ha) if it is takeover of ACTIVE node or node performing
|
# NOTE(pas-ha) if it is takeover of ACTIVE node or node performing
|
||||||
# unrescue operation, first ensure that basic PXE configs and links
|
# unrescue operation, first ensure that basic PXE configs and links
|
||||||
# are in place before switching pxe config
|
# are in place before switching pxe config
|
||||||
if (node.provision_state in [states.ACTIVE, states.UNRESCUING]
|
# NOTE(TheJulia): Also consider deploying a valid state to go ahead
|
||||||
|
# and check things before continuing, as otherwise deployments can
|
||||||
|
# fail if the agent was booted outside the direct actions of the
|
||||||
|
# boot interface.
|
||||||
|
if (node.provision_state in [states.ACTIVE, states.UNRESCUING,
|
||||||
|
states.DEPLOYING]
|
||||||
and not os.path.isfile(pxe_config_path)):
|
and not os.path.isfile(pxe_config_path)):
|
||||||
pxe_options = build_pxe_config_options(task, instance_image_info,
|
pxe_options = build_pxe_config_options(task, instance_image_info,
|
||||||
service=True,
|
service=True,
|
||||||
@ -808,6 +813,7 @@ def build_service_pxe_config(task, instance_image_info,
|
|||||||
create_pxe_config(task, pxe_options, pxe_config_template,
|
create_pxe_config(task, pxe_options, pxe_config_template,
|
||||||
ipxe_enabled=ipxe_enabled)
|
ipxe_enabled=ipxe_enabled)
|
||||||
iwdi = node.driver_internal_info.get('is_whole_disk_image')
|
iwdi = node.driver_internal_info.get('is_whole_disk_image')
|
||||||
|
|
||||||
deploy_utils.switch_pxe_config(
|
deploy_utils.switch_pxe_config(
|
||||||
pxe_config_path, root_uuid_or_disk_id,
|
pxe_config_path, root_uuid_or_disk_id,
|
||||||
boot_mode_utils.get_boot_mode(node),
|
boot_mode_utils.get_boot_mode(node),
|
||||||
|
@ -243,6 +243,20 @@ If a node gets stuck with one of these states for some reason
|
|||||||
to fail state.
|
to fail state.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_LOOKUP_ALLOWED_STATES = (DEPLOYING, DEPLOYWAIT, CLEANING, CLEANWAIT,
|
||||||
|
INSPECTING, RESCUING, RESCUEWAIT)
|
||||||
|
LOOKUP_ALLOWED_STATES = frozenset(_LOOKUP_ALLOWED_STATES)
|
||||||
|
|
||||||
|
"""States when API lookups are normally allowed for nodes."""
|
||||||
|
|
||||||
|
_FASTTRACK_LOOKUP_ALLOWED_STATES = (ENROLL, MANAGEABLE, AVAILABLE,
|
||||||
|
DEPLOYING, DEPLOYWAIT, CLEANING,
|
||||||
|
CLEANWAIT, INSPECTING, RESCUING,
|
||||||
|
RESCUEWAIT)
|
||||||
|
FASTTRACK_LOOKUP_ALLOWED_STATES = frozenset(_FASTTRACK_LOOKUP_ALLOWED_STATES)
|
||||||
|
"""States where API lookups are permitted with fast track enabled."""
|
||||||
|
|
||||||
|
|
||||||
##############
|
##############
|
||||||
# Power states
|
# Power states
|
||||||
##############
|
##############
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import datetime
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from openstack.baremetal import configdrive as os_configdrive
|
from openstack.baremetal import configdrive as os_configdrive
|
||||||
@ -21,6 +22,7 @@ from oslo_log import log
|
|||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
from oslo_service import loopingcall
|
from oslo_service import loopingcall
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
|
from oslo_utils import timeutils
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ironic.common import boot_devices
|
from ironic.common import boot_devices
|
||||||
@ -1408,3 +1410,63 @@ def build_configdrive(node, configdrive):
|
|||||||
LOG.debug('Building a configdrive for node %s', node.uuid)
|
LOG.debug('Building a configdrive for node %s', node.uuid)
|
||||||
return os_configdrive.build(meta_data, user_data=user_data,
|
return os_configdrive.build(meta_data, user_data=user_data,
|
||||||
network_data=configdrive.get('network_data'))
|
network_data=configdrive.get('network_data'))
|
||||||
|
|
||||||
|
|
||||||
|
def fast_track_able(task):
|
||||||
|
"""Checks if the operation can be a streamlined deployment sequence.
|
||||||
|
|
||||||
|
This is mainly focused on ensuring that we are able to quickly sequence
|
||||||
|
through operations if we already have a ramdisk heartbeating through
|
||||||
|
external means.
|
||||||
|
|
||||||
|
:param task: Taskmanager object
|
||||||
|
:returns: True if [deploy]fast_track is set to True, no iSCSI boot
|
||||||
|
configuration is present, and no last_error is present for
|
||||||
|
the node indicating that there was a recent failure.
|
||||||
|
"""
|
||||||
|
return (CONF.deploy.fast_track
|
||||||
|
# TODO(TheJulia): Network model aside, we should be able to
|
||||||
|
# fast-track through initial sequence to complete deployment.
|
||||||
|
# This needs to be validated.
|
||||||
|
# TODO(TheJulia): Do we need a secondary guard? To prevent
|
||||||
|
# driving through this we could query the API endpoint of
|
||||||
|
# the agent with a short timeout such as 10 seconds, which
|
||||||
|
# would help verify if the node is online.
|
||||||
|
# TODO(TheJulia): Should we check the provisioning/deployment
|
||||||
|
# networks match config wise? Do we care? #decisionsdecisions
|
||||||
|
and task.driver.storage.should_write_image(task)
|
||||||
|
and task.node.last_error is None)
|
||||||
|
|
||||||
|
|
||||||
|
def is_fast_track(task):
|
||||||
|
"""Checks a fast track is available.
|
||||||
|
|
||||||
|
This method first ensures that the node and conductor configuration
|
||||||
|
is valid to perform a fast track sequence meaning that we already
|
||||||
|
have a ramdisk running through another means like discovery.
|
||||||
|
If not valid, False is returned.
|
||||||
|
|
||||||
|
The method then checks for the last agent heartbeat, and if it occured
|
||||||
|
within the timeout set by [deploy]fast_track_timeout and the power
|
||||||
|
state for the machine is POWER_ON, then fast track is permitted.
|
||||||
|
|
||||||
|
:param node: A node object.
|
||||||
|
:returns: True if the last heartbeat that was recorded was within
|
||||||
|
the [deploy]fast_track_timeout setting.
|
||||||
|
"""
|
||||||
|
if not fast_track_able(task):
|
||||||
|
return False
|
||||||
|
# use native datetime objects for conversion and compare
|
||||||
|
# slightly odd because py2 compatability :(
|
||||||
|
last = datetime.datetime.strptime(
|
||||||
|
task.node.driver_internal_info.get(
|
||||||
|
'agent_last_heartbeat',
|
||||||
|
'1970-01-01T00:00:00.000000'),
|
||||||
|
"%Y-%m-%dT%H:%M:%S.%f")
|
||||||
|
# If we found nothing, we assume that the time is essentially epoch.
|
||||||
|
time_delta = datetime.timedelta(seconds=CONF.deploy.fast_track_timeout)
|
||||||
|
last_valid = timeutils.utcnow() - time_delta
|
||||||
|
# Checking the power state, because if we find the machine off due to
|
||||||
|
# any action, we can't actually fast track the node. :(
|
||||||
|
return (last_valid <= last
|
||||||
|
and task.driver.power.get_power_state(task) == states.POWER_ON)
|
||||||
|
@ -113,6 +113,27 @@ opts = [
|
|||||||
'images for the direct deploy interface, when local '
|
'images for the direct deploy interface, when local '
|
||||||
'HTTP service is incorporated to provide instance image '
|
'HTTP service is incorporated to provide instance image '
|
||||||
'instead of swift tempurls.')),
|
'instead of swift tempurls.')),
|
||||||
|
cfg.BoolOpt('fast_track',
|
||||||
|
default=False,
|
||||||
|
help=_('Whether to allow deployment agents to perform lookup, '
|
||||||
|
'heartbeat operations during initial states of a '
|
||||||
|
'machine lifecycle and by-pass the normal setup '
|
||||||
|
'procedures for a ramdisk. This feature also enables '
|
||||||
|
'power operations which are part of deployment '
|
||||||
|
'processes to be bypassed if the ramdisk has performed '
|
||||||
|
'a heartbeat operation using the fast_track_timeout '
|
||||||
|
'setting.')),
|
||||||
|
cfg.IntOpt('fast_track_timeout',
|
||||||
|
default=300,
|
||||||
|
min=0,
|
||||||
|
max=300,
|
||||||
|
help=_('Seconds for which the last heartbeat event is to be '
|
||||||
|
'considered valid for the purpose of a fast '
|
||||||
|
'track sequence. This setting should generally be '
|
||||||
|
'less than the number of seconds for "Power-On Self '
|
||||||
|
'Test" and typical ramdisk start-up. This value should '
|
||||||
|
'not exceed the [api]ramdisk_heartbeat_timeout '
|
||||||
|
'setting.')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -455,7 +455,14 @@ class AgentDeploy(AgentDeployMixin, 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.
|
||||||
"""
|
"""
|
||||||
if task.driver.storage.should_write_image(task):
|
if manager_utils.is_fast_track(task):
|
||||||
|
LOG.debug('Performing a fast track deployment for %(node)s.',
|
||||||
|
{'node': task.node.uuid})
|
||||||
|
# Update the database for the API and the task tracking resumes
|
||||||
|
# the state machine state going from DEPLOYWAIT -> DEPLOYING
|
||||||
|
task.process_event('wait')
|
||||||
|
self.continue_deploy(task)
|
||||||
|
elif task.driver.storage.should_write_image(task):
|
||||||
manager_utils.node_power_action(task, states.REBOOT)
|
manager_utils.node_power_action(task, states.REBOOT)
|
||||||
return states.DEPLOYWAIT
|
return states.DEPLOYWAIT
|
||||||
else:
|
else:
|
||||||
@ -521,6 +528,12 @@ class AgentDeploy(AgentDeployMixin, base.DeployInterface):
|
|||||||
:raises: exception.InvalidParameterValue if network validation fails.
|
:raises: exception.InvalidParameterValue if network validation fails.
|
||||||
:raises: any boot interface's prepare_ramdisk exceptions.
|
:raises: any boot interface's prepare_ramdisk exceptions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _update_instance_info():
|
||||||
|
node.instance_info = (
|
||||||
|
deploy_utils.build_instance_info_for_deploy(task))
|
||||||
|
node.save()
|
||||||
|
|
||||||
node = task.node
|
node = task.node
|
||||||
deploy_utils.populate_storage_driver_internal_info(task)
|
deploy_utils.populate_storage_driver_internal_info(task)
|
||||||
if node.provision_state == states.DEPLOYING:
|
if node.provision_state == states.DEPLOYING:
|
||||||
@ -547,25 +560,44 @@ class AgentDeploy(AgentDeployMixin, base.DeployInterface):
|
|||||||
task.driver.network.validate(task)
|
task.driver.network.validate(task)
|
||||||
else:
|
else:
|
||||||
ctx.reraise = True
|
ctx.reraise = True
|
||||||
|
# Determine if this is a fast track sequence
|
||||||
# Adding the node to provisioning network so that the dhcp
|
fast_track_deploy = manager_utils.is_fast_track(task)
|
||||||
# options get added for the provisioning port.
|
if fast_track_deploy:
|
||||||
manager_utils.node_power_action(task, states.POWER_OFF)
|
# The agent has already recently checked in and we are
|
||||||
|
# configured to take that as an indicator that we can
|
||||||
|
# skip ahead.
|
||||||
|
LOG.debug('The agent for node %(node)s has recently checked '
|
||||||
|
'in, and the node power will remain unmodified.',
|
||||||
|
{'node': task.node.uuid})
|
||||||
|
else:
|
||||||
|
# Powering off node to setup networking for port and
|
||||||
|
# ensure that the state is reset if it is inadvertently
|
||||||
|
# on for any unknown reason.
|
||||||
|
manager_utils.node_power_action(task, states.POWER_OFF)
|
||||||
if task.driver.storage.should_write_image(task):
|
if task.driver.storage.should_write_image(task):
|
||||||
# NOTE(vdrok): in case of rebuild, we have tenant network
|
# NOTE(vdrok): in case of rebuild, we have tenant network
|
||||||
# already configured, unbind tenant ports if present
|
# already configured, unbind tenant ports if present
|
||||||
power_state_to_restore = (
|
if not fast_track_deploy:
|
||||||
manager_utils.power_on_node_if_needed(task))
|
power_state_to_restore = (
|
||||||
|
manager_utils.power_on_node_if_needed(task))
|
||||||
|
|
||||||
task.driver.network.unconfigure_tenant_networks(task)
|
task.driver.network.unconfigure_tenant_networks(task)
|
||||||
task.driver.network.add_provisioning_network(task)
|
task.driver.network.add_provisioning_network(task)
|
||||||
manager_utils.restore_power_state_if_needed(
|
if not fast_track_deploy:
|
||||||
task, power_state_to_restore)
|
manager_utils.restore_power_state_if_needed(
|
||||||
|
task, power_state_to_restore)
|
||||||
|
else:
|
||||||
|
# Fast track sequence in progress
|
||||||
|
_update_instance_info()
|
||||||
# Signal to storage driver to attach volumes
|
# Signal to storage driver to attach volumes
|
||||||
task.driver.storage.attach_volumes(task)
|
task.driver.storage.attach_volumes(task)
|
||||||
if not task.driver.storage.should_write_image(task):
|
if (not task.driver.storage.should_write_image(task)
|
||||||
|
or fast_track_deploy):
|
||||||
# We have nothing else to do as this is handled in the
|
# We have nothing else to do as this is handled in the
|
||||||
# backend storage system, and we can return to the caller
|
# backend storage system, and we can return to the caller
|
||||||
# as we do not need to boot the agent to deploy.
|
# as we do not need to boot the agent to deploy.
|
||||||
|
# Alternatively, we could be in a fast track deployment
|
||||||
|
# and again, we should have nothing to do here.
|
||||||
return
|
return
|
||||||
if node.provision_state in (states.ACTIVE, states.UNRESCUING):
|
if node.provision_state in (states.ACTIVE, states.UNRESCUING):
|
||||||
# Call is due to conductor takeover
|
# Call is due to conductor takeover
|
||||||
@ -573,9 +605,7 @@ class AgentDeploy(AgentDeployMixin, base.DeployInterface):
|
|||||||
elif node.provision_state != states.ADOPTING:
|
elif node.provision_state != states.ADOPTING:
|
||||||
if node.provision_state not in (states.RESCUING, states.RESCUEWAIT,
|
if node.provision_state not in (states.RESCUING, states.RESCUEWAIT,
|
||||||
states.RESCUE, states.RESCUEFAIL):
|
states.RESCUE, states.RESCUEFAIL):
|
||||||
node.instance_info = (
|
_update_instance_info()
|
||||||
deploy_utils.build_instance_info_for_deploy(task))
|
|
||||||
node.save()
|
|
||||||
if CONF.agent.manage_agent_boot:
|
if CONF.agent.manage_agent_boot:
|
||||||
deploy_opts = deploy_utils.build_agent_options(node)
|
deploy_opts = deploy_utils.build_agent_options(node)
|
||||||
task.driver.boot.prepare_ramdisk(task, deploy_opts)
|
task.driver.boot.prepare_ramdisk(task, deploy_opts)
|
||||||
|
@ -66,6 +66,19 @@ VENDOR_PROPERTIES = {
|
|||||||
'older deploy ramdisks. Defaults to False. Optional.')
|
'older deploy ramdisks. Defaults to False. Optional.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__HEARTBEAT_RECORD_ONLY = (states.ENROLL, states.MANAGEABLE,
|
||||||
|
states.AVAILABLE)
|
||||||
|
_HEARTBEAT_RECORD_ONLY = frozenset(__HEARTBEAT_RECORD_ONLY)
|
||||||
|
|
||||||
|
_HEARTBEAT_ALLOWED = (states.DEPLOYWAIT, states.CLEANWAIT, states.RESCUEWAIT)
|
||||||
|
HEARTBEAT_ALLOWED = frozenset(_HEARTBEAT_ALLOWED)
|
||||||
|
|
||||||
|
_FASTTRACK_HEARTBEAT_ALLOWED = (states.DEPLOYWAIT, states.CLEANWAIT,
|
||||||
|
states.RESCUEWAIT, states.ENROLL,
|
||||||
|
states.MANAGEABLE, states.AVAILABLE,
|
||||||
|
states.DEPLOYING)
|
||||||
|
FASTTRACK_HEARTBEAT_ALLOWED = frozenset(_FASTTRACK_HEARTBEAT_ALLOWED)
|
||||||
|
|
||||||
|
|
||||||
def _get_client():
|
def _get_client():
|
||||||
client = agent_client.AgentClient()
|
client = agent_client.AgentClient()
|
||||||
@ -260,7 +273,9 @@ class HeartbeatMixin(object):
|
|||||||
@property
|
@property
|
||||||
def heartbeat_allowed_states(self):
|
def heartbeat_allowed_states(self):
|
||||||
"""Define node states where heartbeating is allowed"""
|
"""Define node states where heartbeating is allowed"""
|
||||||
return (states.DEPLOYWAIT, states.CLEANWAIT, states.RESCUEWAIT)
|
if CONF.deploy.fast_track:
|
||||||
|
return FASTTRACK_HEARTBEAT_ALLOWED
|
||||||
|
return HEARTBEAT_ALLOWED
|
||||||
|
|
||||||
@METRICS.timer('HeartbeatMixin.heartbeat')
|
@METRICS.timer('HeartbeatMixin.heartbeat')
|
||||||
def heartbeat(self, task, callback_url, agent_version):
|
def heartbeat(self, task, callback_url, agent_version):
|
||||||
@ -271,7 +286,8 @@ class HeartbeatMixin(object):
|
|||||||
:param agent_version: The version of the agent that is heartbeating
|
:param agent_version: The version of the agent that is heartbeating
|
||||||
"""
|
"""
|
||||||
# NOTE(pas-ha) immediately skip the rest if nothing to do
|
# NOTE(pas-ha) immediately skip the rest if nothing to do
|
||||||
if task.node.provision_state not in self.heartbeat_allowed_states:
|
if (task.node.provision_state not in self.heartbeat_allowed_states
|
||||||
|
and not manager_utils.fast_track_able(task)):
|
||||||
LOG.debug('Heartbeat from node %(node)s in unsupported '
|
LOG.debug('Heartbeat from node %(node)s in unsupported '
|
||||||
'provision state %(state)s, not taking any action.',
|
'provision state %(state)s, not taking any action.',
|
||||||
{'node': task.node.uuid,
|
{'node': task.node.uuid,
|
||||||
@ -288,13 +304,25 @@ class HeartbeatMixin(object):
|
|||||||
|
|
||||||
node = task.node
|
node = task.node
|
||||||
LOG.debug('Heartbeat from node %s', node.uuid)
|
LOG.debug('Heartbeat from node %s', node.uuid)
|
||||||
|
|
||||||
driver_internal_info = node.driver_internal_info
|
driver_internal_info = node.driver_internal_info
|
||||||
driver_internal_info['agent_url'] = callback_url
|
driver_internal_info['agent_url'] = callback_url
|
||||||
driver_internal_info['agent_version'] = agent_version
|
driver_internal_info['agent_version'] = agent_version
|
||||||
|
# Record the last heartbeat event time in UTC, so we can make
|
||||||
|
# decisions about it later. Can be decoded to datetime object with:
|
||||||
|
# datetime.datetime.strptime(var, "%Y-%m-%d %H:%M:%S.%f")
|
||||||
|
driver_internal_info['agent_last_heartbeat'] = str(
|
||||||
|
timeutils.utcnow().isoformat())
|
||||||
node.driver_internal_info = driver_internal_info
|
node.driver_internal_info = driver_internal_info
|
||||||
node.save()
|
node.save()
|
||||||
|
|
||||||
|
if node.provision_state in _HEARTBEAT_RECORD_ONLY:
|
||||||
|
# We shouldn't take any additional action. The agent will
|
||||||
|
# silently continue to heartbeat to ironic until user initiated
|
||||||
|
# state change occurs causing it to match a state below.
|
||||||
|
LOG.debug('Heartbeat from %(node)s recorded to identify the '
|
||||||
|
'node as on-line.', {'node': task.node.uuid})
|
||||||
|
return
|
||||||
|
|
||||||
# Async call backs don't set error state on their own
|
# Async call backs don't set error state on their own
|
||||||
# TODO(jimrollenhagen) improve error messages here
|
# TODO(jimrollenhagen) improve error messages here
|
||||||
msg = _('Failed checking if deploy is done.')
|
msg = _('Failed checking if deploy is done.')
|
||||||
|
@ -899,10 +899,22 @@ def prepare_inband_cleaning(task, manage_boot=True):
|
|||||||
:raises: InvalidParameterValue if cleaning network UUID config option has
|
:raises: InvalidParameterValue if cleaning network UUID config option has
|
||||||
an invalid value.
|
an invalid value.
|
||||||
"""
|
"""
|
||||||
power_state_to_restore = manager_utils.power_on_node_if_needed(task)
|
fast_track = manager_utils.is_fast_track(task)
|
||||||
|
if not fast_track:
|
||||||
|
power_state_to_restore = manager_utils.power_on_node_if_needed(task)
|
||||||
|
|
||||||
|
# WARNING(TheJulia): When fast track is available, trying to plug the
|
||||||
|
# cleaning network is problematic and in practice this may fail if
|
||||||
|
# cleaning/provisioning/discovery all take place on different
|
||||||
|
# networks when..
|
||||||
|
# Translation: Here be a realistically unavoidable footgun
|
||||||
|
# fast track support.
|
||||||
|
# TODO(TheJulia): Lets improve this somehow such that the agent host
|
||||||
|
# gracefully handles these sorts of changes.
|
||||||
task.driver.network.add_cleaning_network(task)
|
task.driver.network.add_cleaning_network(task)
|
||||||
manager_utils.restore_power_state_if_needed(
|
if not fast_track:
|
||||||
task, power_state_to_restore)
|
manager_utils.restore_power_state_if_needed(
|
||||||
|
task, power_state_to_restore)
|
||||||
|
|
||||||
# Append required config parameters to node's driver_internal_info
|
# Append required config parameters to node's driver_internal_info
|
||||||
# to pass to IPA.
|
# to pass to IPA.
|
||||||
@ -912,7 +924,8 @@ def prepare_inband_cleaning(task, manage_boot=True):
|
|||||||
ramdisk_opts = build_agent_options(task.node)
|
ramdisk_opts = build_agent_options(task.node)
|
||||||
task.driver.boot.prepare_ramdisk(task, ramdisk_opts)
|
task.driver.boot.prepare_ramdisk(task, ramdisk_opts)
|
||||||
|
|
||||||
manager_utils.node_power_action(task, states.REBOOT)
|
if not fast_track:
|
||||||
|
manager_utils.node_power_action(task, states.REBOOT)
|
||||||
|
|
||||||
# Tell the conductor we are waiting for the agent to boot.
|
# Tell the conductor we are waiting for the agent to boot.
|
||||||
return states.CLEANWAIT
|
return states.CLEANWAIT
|
||||||
@ -936,14 +949,18 @@ def tear_down_inband_cleaning(task, manage_boot=True):
|
|||||||
:raises: NetworkError, NodeCleaningFailure if the cleaning ports cannot be
|
:raises: NetworkError, NodeCleaningFailure if the cleaning ports cannot be
|
||||||
removed.
|
removed.
|
||||||
"""
|
"""
|
||||||
manager_utils.node_power_action(task, states.POWER_OFF)
|
fast_track = manager_utils.is_fast_track(task)
|
||||||
|
if not fast_track:
|
||||||
|
manager_utils.node_power_action(task, states.POWER_OFF)
|
||||||
|
|
||||||
if manage_boot:
|
if manage_boot:
|
||||||
task.driver.boot.clean_up_ramdisk(task)
|
task.driver.boot.clean_up_ramdisk(task)
|
||||||
|
|
||||||
power_state_to_restore = manager_utils.power_on_node_if_needed(task)
|
power_state_to_restore = manager_utils.power_on_node_if_needed(task)
|
||||||
task.driver.network.remove_cleaning_network(task)
|
task.driver.network.remove_cleaning_network(task)
|
||||||
manager_utils.restore_power_state_if_needed(
|
if not fast_track:
|
||||||
task, power_state_to_restore)
|
manager_utils.restore_power_state_if_needed(
|
||||||
|
task, power_state_to_restore)
|
||||||
|
|
||||||
|
|
||||||
def get_image_instance_info(node):
|
def get_image_instance_info(node):
|
||||||
|
@ -408,13 +408,24 @@ class ISCSIDeploy(AgentDeployMixin, base.DeployInterface):
|
|||||||
:returns: deploy state DEPLOYWAIT.
|
:returns: deploy state DEPLOYWAIT.
|
||||||
"""
|
"""
|
||||||
node = task.node
|
node = task.node
|
||||||
if task.driver.storage.should_write_image(task):
|
if manager_utils.is_fast_track(task):
|
||||||
|
LOG.debug('Performing a fast track deployment for %(node)s.',
|
||||||
|
{'node': task.node.uuid})
|
||||||
|
deploy_utils.cache_instance_image(task.context, node)
|
||||||
|
check_image_size(task)
|
||||||
|
# Update the database for the API and the task tracking resumes
|
||||||
|
# the state machine state going from DEPLOYWAIT -> DEPLOYING
|
||||||
|
task.process_event('wait')
|
||||||
|
self.continue_deploy(task)
|
||||||
|
elif task.driver.storage.should_write_image(task):
|
||||||
|
# Standard deploy process
|
||||||
deploy_utils.cache_instance_image(task.context, node)
|
deploy_utils.cache_instance_image(task.context, node)
|
||||||
check_image_size(task)
|
check_image_size(task)
|
||||||
manager_utils.node_power_action(task, states.REBOOT)
|
manager_utils.node_power_action(task, states.REBOOT)
|
||||||
|
|
||||||
return states.DEPLOYWAIT
|
return states.DEPLOYWAIT
|
||||||
else:
|
else:
|
||||||
|
# Boot to an Storage Volume
|
||||||
|
|
||||||
# TODO(TheJulia): At some point, we should de-dupe this code
|
# TODO(TheJulia): At some point, we should de-dupe this code
|
||||||
# as it is nearly identical to the agent deploy interface.
|
# as it is nearly identical to the agent deploy interface.
|
||||||
# This is not being done now as it is expected to be
|
# This is not being done now as it is expected to be
|
||||||
@ -486,23 +497,38 @@ class ISCSIDeploy(AgentDeployMixin, base.DeployInterface):
|
|||||||
task.driver.boot.prepare_instance(task)
|
task.driver.boot.prepare_instance(task)
|
||||||
else:
|
else:
|
||||||
if node.provision_state == states.DEPLOYING:
|
if node.provision_state == states.DEPLOYING:
|
||||||
# Adding the node to provisioning network so that the dhcp
|
fast_track_deploy = manager_utils.is_fast_track(task)
|
||||||
# options get added for the provisioning port.
|
if fast_track_deploy:
|
||||||
manager_utils.node_power_action(task, states.POWER_OFF)
|
# The agent has already recently checked in and we are
|
||||||
|
# configured to take that as an indicator that we can
|
||||||
|
# skip ahead.
|
||||||
|
LOG.debug('The agent for node %(node)s has recently '
|
||||||
|
'checked in, and the node power will remain '
|
||||||
|
'unmodified.',
|
||||||
|
{'node': task.node.uuid})
|
||||||
|
else:
|
||||||
|
# Adding the node to provisioning network so that the dhcp
|
||||||
|
# options get added for the provisioning port.
|
||||||
|
manager_utils.node_power_action(task, states.POWER_OFF)
|
||||||
# NOTE(vdrok): in case of rebuild, we have tenant network
|
# NOTE(vdrok): in case of rebuild, we have tenant network
|
||||||
# already configured, unbind tenant ports if present
|
# already configured, unbind tenant ports if present
|
||||||
if task.driver.storage.should_write_image(task):
|
if task.driver.storage.should_write_image(task):
|
||||||
power_state_to_restore = (
|
if not fast_track_deploy:
|
||||||
manager_utils.power_on_node_if_needed(task))
|
power_state_to_restore = (
|
||||||
|
manager_utils.power_on_node_if_needed(task))
|
||||||
task.driver.network.unconfigure_tenant_networks(task)
|
task.driver.network.unconfigure_tenant_networks(task)
|
||||||
task.driver.network.add_provisioning_network(task)
|
task.driver.network.add_provisioning_network(task)
|
||||||
manager_utils.restore_power_state_if_needed(
|
if not fast_track_deploy:
|
||||||
task, power_state_to_restore)
|
manager_utils.restore_power_state_if_needed(
|
||||||
|
task, power_state_to_restore)
|
||||||
task.driver.storage.attach_volumes(task)
|
task.driver.storage.attach_volumes(task)
|
||||||
if not task.driver.storage.should_write_image(task):
|
if (not task.driver.storage.should_write_image(task)
|
||||||
|
or fast_track_deploy):
|
||||||
# We have nothing else to do as this is handled in the
|
# We have nothing else to do as this is handled in the
|
||||||
# backend storage system, and we can return to the caller
|
# backend storage system, and we can return to the caller
|
||||||
# as we do not need to boot the agent to deploy.
|
# as we do not need to boot the agent to deploy.
|
||||||
|
# Alternatively, we are in a fast track deployment
|
||||||
|
# and have nothing else to do.
|
||||||
return
|
return
|
||||||
|
|
||||||
deploy_opts = deploy_utils.build_agent_options(node)
|
deploy_opts = deploy_utils.build_agent_options(node)
|
||||||
|
@ -24,6 +24,7 @@ from six.moves import http_client
|
|||||||
from ironic.api.controllers import base as api_base
|
from ironic.api.controllers import base as api_base
|
||||||
from ironic.api.controllers import v1 as api_v1
|
from ironic.api.controllers import v1 as api_v1
|
||||||
from ironic.api.controllers.v1 import ramdisk
|
from ironic.api.controllers.v1 import ramdisk
|
||||||
|
from ironic.common import states
|
||||||
from ironic.conductor import rpcapi
|
from ironic.conductor import rpcapi
|
||||||
from ironic.tests.unit.api import base as test_api_base
|
from ironic.tests.unit.api import base as test_api_base
|
||||||
from ironic.tests.unit.objects import utils as obj_utils
|
from ironic.tests.unit.objects import utils as obj_utils
|
||||||
@ -160,6 +161,17 @@ class TestLookup(test_api_base.BaseApiTest):
|
|||||||
set(data['node']))
|
set(data['node']))
|
||||||
self._check_config(data)
|
self._check_config(data)
|
||||||
|
|
||||||
|
def test_fast_deploy_lookup(self):
|
||||||
|
CONF.set_override('fast_track', True, 'deploy')
|
||||||
|
for provision_state in [states.ENROLL, states.MANAGEABLE,
|
||||||
|
states.AVAILABLE]:
|
||||||
|
self.node.provision_state = provision_state
|
||||||
|
data = self.get_json(
|
||||||
|
'/lookup?addresses=%s&node_uuid=%s' %
|
||||||
|
(','.join(self.addresses), self.node.uuid),
|
||||||
|
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||||
|
self.assertEqual(self.node.uuid, data['node']['uuid'])
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
|
@mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
|
||||||
lambda *n: 'test-topic')
|
lambda *n: 'test-topic')
|
||||||
|
@ -13,6 +13,7 @@ import time
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
from oslo_utils import timeutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
from ironic.common import boot_devices
|
from ironic.common import boot_devices
|
||||||
@ -2537,3 +2538,53 @@ class ValidateDeployTemplatesTestCase(db_base.DbTestCase):
|
|||||||
self.assertRaises(exception.InvalidParameterValue,
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
conductor_utils.validate_deploy_templates, task)
|
conductor_utils.validate_deploy_templates, task)
|
||||||
mock_validated.assert_called_once_with(task)
|
mock_validated.assert_called_once_with(task)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(fake.FakePower, 'get_power_state', autospec=True)
|
||||||
|
class FastTrackTestCase(db_base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(FastTrackTestCase, self).setUp()
|
||||||
|
self.node = obj_utils.create_test_node(
|
||||||
|
self.context, driver='fake-hardware',
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
driver_internal_info={
|
||||||
|
'agent_last_heartbeat': str(timeutils.utcnow().isoformat())})
|
||||||
|
self.config(fast_track=True, group='deploy')
|
||||||
|
|
||||||
|
def test_is_fast_track(self, mock_get_power):
|
||||||
|
mock_get_power.return_value = states.POWER_ON
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
|
self.assertTrue(conductor_utils.is_fast_track(task))
|
||||||
|
|
||||||
|
def test_is_fast_track_config_false(self, mock_get_power):
|
||||||
|
self.config(fast_track=False, group='deploy')
|
||||||
|
mock_get_power.return_value = states.POWER_ON
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
|
self.assertFalse(conductor_utils.is_fast_track(task))
|
||||||
|
|
||||||
|
def test_is_fast_track_power_off_false(self, mock_get_power):
|
||||||
|
mock_get_power.return_value = states.POWER_OFF
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
|
self.assertFalse(conductor_utils.is_fast_track(task))
|
||||||
|
|
||||||
|
def test_is_fast_track_no_heartbeat(self, mock_get_power):
|
||||||
|
mock_get_power.return_value = states.POWER_ON
|
||||||
|
i_info = self.node.driver_internal_info
|
||||||
|
i_info.pop('agent_last_heartbeat')
|
||||||
|
self.node.driver_internal_info = i_info
|
||||||
|
self.node.save()
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
|
self.assertFalse(conductor_utils.is_fast_track(task))
|
||||||
|
|
||||||
|
def test_is_fast_track_error_blocks(self, mock_get_power):
|
||||||
|
mock_get_power.return_value = states.POWER_ON
|
||||||
|
self.node.last_error = "bad things happened"
|
||||||
|
self.node.save()
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
|
self.assertFalse(conductor_utils.is_fast_track(task))
|
||||||
|
@ -337,6 +337,39 @@ class TestAgentDeploy(db_base.DbTestCase):
|
|||||||
self.assertIsNone(driver_return)
|
self.assertIsNone(driver_return)
|
||||||
self.assertTrue(mock_pxe_instance.called)
|
self.assertTrue(mock_pxe_instance.called)
|
||||||
|
|
||||||
|
@mock.patch.object(agent_client.AgentClient, 'prepare_image',
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch('ironic.conductor.utils.is_fast_track', autospec=True)
|
||||||
|
@mock.patch.object(pxe.PXEBoot, 'prepare_instance', autospec=True)
|
||||||
|
@mock.patch('ironic.conductor.utils.node_power_action', autospec=True)
|
||||||
|
def test_deploy_fast_track(self, power_mock, mock_pxe_instance,
|
||||||
|
mock_is_fast_track, prepare_image_mock):
|
||||||
|
mock_is_fast_track.return_value = True
|
||||||
|
self.node.target_provision_state = states.ACTIVE
|
||||||
|
self.node.provision_state = states.DEPLOYING
|
||||||
|
test_temp_url = 'http://image'
|
||||||
|
expected_image_info = {
|
||||||
|
'urls': [test_temp_url],
|
||||||
|
'id': 'fake-image',
|
||||||
|
'node_uuid': self.node.uuid,
|
||||||
|
'checksum': 'checksum',
|
||||||
|
'disk_format': 'qcow2',
|
||||||
|
'container_format': 'bare',
|
||||||
|
'stream_raw_images': CONF.agent.stream_raw_images,
|
||||||
|
}
|
||||||
|
self.node.save()
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=False) as task:
|
||||||
|
self.driver.deploy(task)
|
||||||
|
self.assertFalse(power_mock.called)
|
||||||
|
self.assertFalse(mock_pxe_instance.called)
|
||||||
|
task.node.refresh()
|
||||||
|
prepare_image_mock.assert_called_with(mock.ANY, task.node,
|
||||||
|
expected_image_info)
|
||||||
|
self.assertEqual(states.DEPLOYWAIT, task.node.provision_state)
|
||||||
|
self.assertEqual(states.ACTIVE,
|
||||||
|
task.node.target_provision_state)
|
||||||
|
|
||||||
@mock.patch.object(noop_storage.NoopStorage, 'detach_volumes',
|
@mock.patch.object(noop_storage.NoopStorage, 'detach_volumes',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(flat_network.FlatNetwork,
|
@mock.patch.object(flat_network.FlatNetwork,
|
||||||
@ -778,6 +811,52 @@ class TestAgentDeploy(db_base.DbTestCase):
|
|||||||
build_options_mock.assert_not_called()
|
build_options_mock.assert_not_called()
|
||||||
pxe_prepare_ramdisk_mock.assert_not_called()
|
pxe_prepare_ramdisk_mock.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch('ironic.conductor.utils.is_fast_track', autospec=True)
|
||||||
|
@mock.patch.object(noop_storage.NoopStorage, 'attach_volumes',
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch.object(deploy_utils, 'populate_storage_driver_internal_info')
|
||||||
|
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk')
|
||||||
|
@mock.patch.object(deploy_utils, 'build_agent_options')
|
||||||
|
@mock.patch.object(deploy_utils, 'build_instance_info_for_deploy')
|
||||||
|
@mock.patch.object(flat_network.FlatNetwork, 'add_provisioning_network',
|
||||||
|
spec_set=True, autospec=True)
|
||||||
|
@mock.patch.object(flat_network.FlatNetwork,
|
||||||
|
'unconfigure_tenant_networks',
|
||||||
|
spec_set=True, autospec=True)
|
||||||
|
@mock.patch.object(flat_network.FlatNetwork, 'validate',
|
||||||
|
spec_set=True, autospec=True)
|
||||||
|
def test_prepare_fast_track(
|
||||||
|
self, validate_net_mock,
|
||||||
|
unconfigure_tenant_net_mock, add_provisioning_net_mock,
|
||||||
|
build_instance_info_mock, build_options_mock,
|
||||||
|
pxe_prepare_ramdisk_mock, storage_driver_info_mock,
|
||||||
|
storage_attach_volumes_mock, is_fast_track_mock):
|
||||||
|
# TODO(TheJulia): We should revisit this test. Smartnic
|
||||||
|
# support didn't wire in tightly on testing for power in
|
||||||
|
# these tests, and largely fast_track impacts power operations.
|
||||||
|
node = self.node
|
||||||
|
node.network_interface = 'flat'
|
||||||
|
node.save()
|
||||||
|
is_fast_track_mock.return_value = True
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=False) as task:
|
||||||
|
task.node.provision_state = states.DEPLOYING
|
||||||
|
build_options_mock.return_value = {'a': 'b'}
|
||||||
|
self.driver.prepare(task)
|
||||||
|
storage_driver_info_mock.assert_called_once_with(task)
|
||||||
|
validate_net_mock.assert_called_once_with(mock.ANY, task)
|
||||||
|
add_provisioning_net_mock.assert_called_once_with(mock.ANY, task)
|
||||||
|
unconfigure_tenant_net_mock.assert_called_once_with(mock.ANY, task)
|
||||||
|
self.assertTrue(storage_attach_volumes_mock.called)
|
||||||
|
self.assertTrue(build_instance_info_mock.called)
|
||||||
|
# TODO(TheJulia): We should likely consider executing the
|
||||||
|
# next two methods at some point in order to facilitate
|
||||||
|
# continuity. While not explicitly required for this feature
|
||||||
|
# to work, reboots as part of deployment would need the ramdisk
|
||||||
|
# present and ready.
|
||||||
|
self.assertFalse(build_options_mock.called)
|
||||||
|
self.assertFalse(pxe_prepare_ramdisk_mock.called)
|
||||||
|
|
||||||
@mock.patch('ironic.common.dhcp_factory.DHCPFactory._set_dhcp_provider')
|
@mock.patch('ironic.common.dhcp_factory.DHCPFactory._set_dhcp_provider')
|
||||||
@mock.patch('ironic.common.dhcp_factory.DHCPFactory.clean_dhcp')
|
@mock.patch('ironic.common.dhcp_factory.DHCPFactory.clean_dhcp')
|
||||||
@mock.patch.object(pxe.PXEBoot, 'clean_up_instance')
|
@mock.patch.object(pxe.PXEBoot, 'clean_up_instance')
|
||||||
|
@ -355,8 +355,29 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
|
|||||||
task.node.driver_internal_info['agent_url'])
|
task.node.driver_internal_info['agent_url'])
|
||||||
self.assertEqual('3.2.0',
|
self.assertEqual('3.2.0',
|
||||||
task.node.driver_internal_info['agent_version'])
|
task.node.driver_internal_info['agent_version'])
|
||||||
|
self.assertIsNotNone(
|
||||||
|
task.node.driver_internal_info['agent_last_heartbeat'])
|
||||||
mock_touch.assert_called_once_with(mock.ANY)
|
mock_touch.assert_called_once_with(mock.ANY)
|
||||||
|
|
||||||
|
def test_heartbeat_records_fast_track(self):
|
||||||
|
self.config(fast_track=True, group='deploy')
|
||||||
|
for provision_state in [states.ENROLL, states.MANAGEABLE,
|
||||||
|
states.AVAILABLE]:
|
||||||
|
self.node.driver_internal_info = {}
|
||||||
|
self.node.provision_state = provision_state
|
||||||
|
self.node.save()
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
|
self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '3.2.0')
|
||||||
|
self.assertEqual('http://127.0.0.1:8080',
|
||||||
|
task.node.driver_internal_info['agent_url'])
|
||||||
|
self.assertEqual('3.2.0',
|
||||||
|
task.node.driver_internal_info[
|
||||||
|
'agent_version'])
|
||||||
|
self.assertIsNotNone(
|
||||||
|
task.node.driver_internal_info['agent_last_heartbeat'])
|
||||||
|
self.assertEqual(provision_state, task.node.provision_state)
|
||||||
|
|
||||||
|
|
||||||
class AgentRescueTests(AgentDeployMixinBaseTest):
|
class AgentRescueTests(AgentDeployMixinBaseTest):
|
||||||
|
|
||||||
|
@ -1699,6 +1699,7 @@ class AgentMethodsTestCase(db_base.DbTestCase):
|
|||||||
self.assertEqual(8, task.node.driver_internal_info[
|
self.assertEqual(8, task.node.driver_internal_info[
|
||||||
'disk_erasure_concurrency'])
|
'disk_erasure_concurrency'])
|
||||||
|
|
||||||
|
@mock.patch('ironic.conductor.utils.is_fast_track', autospec=True)
|
||||||
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', autospec=True)
|
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', autospec=True)
|
||||||
@mock.patch('ironic.conductor.utils.node_power_action', autospec=True)
|
@mock.patch('ironic.conductor.utils.node_power_action', autospec=True)
|
||||||
@mock.patch.object(utils, 'build_agent_options', autospec=True)
|
@mock.patch.object(utils, 'build_agent_options', autospec=True)
|
||||||
@ -1707,15 +1708,19 @@ class AgentMethodsTestCase(db_base.DbTestCase):
|
|||||||
def _test_prepare_inband_cleaning(
|
def _test_prepare_inband_cleaning(
|
||||||
self, add_cleaning_network_mock,
|
self, add_cleaning_network_mock,
|
||||||
build_options_mock, power_mock, prepare_ramdisk_mock,
|
build_options_mock, power_mock, prepare_ramdisk_mock,
|
||||||
manage_boot=True):
|
is_fast_track_mock, manage_boot=True, fast_track=False):
|
||||||
build_options_mock.return_value = {'a': 'b'}
|
build_options_mock.return_value = {'a': 'b'}
|
||||||
|
is_fast_track_mock.return_value = fast_track
|
||||||
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:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
states.CLEANWAIT,
|
states.CLEANWAIT,
|
||||||
utils.prepare_inband_cleaning(task, manage_boot=manage_boot))
|
utils.prepare_inband_cleaning(task, manage_boot=manage_boot))
|
||||||
add_cleaning_network_mock.assert_called_once_with(task)
|
add_cleaning_network_mock.assert_called_once_with(task)
|
||||||
power_mock.assert_called_once_with(task, states.REBOOT)
|
if not fast_track:
|
||||||
|
power_mock.assert_called_once_with(task, states.REBOOT)
|
||||||
|
else:
|
||||||
|
self.assertFalse(power_mock.called)
|
||||||
self.assertEqual(1, task.node.driver_internal_info[
|
self.assertEqual(1, task.node.driver_internal_info[
|
||||||
'agent_erase_devices_iterations'])
|
'agent_erase_devices_iterations'])
|
||||||
self.assertIs(True, task.node.driver_internal_info[
|
self.assertIs(True, task.node.driver_internal_info[
|
||||||
@ -1734,17 +1739,26 @@ class AgentMethodsTestCase(db_base.DbTestCase):
|
|||||||
def test_prepare_inband_cleaning_manage_boot_false(self):
|
def test_prepare_inband_cleaning_manage_boot_false(self):
|
||||||
self._test_prepare_inband_cleaning(manage_boot=False)
|
self._test_prepare_inband_cleaning(manage_boot=False)
|
||||||
|
|
||||||
|
def test_prepare_inband_cleaning_fast_track(self):
|
||||||
|
self._test_prepare_inband_cleaning(fast_track=True)
|
||||||
|
|
||||||
|
@mock.patch('ironic.conductor.utils.is_fast_track', autospec=True)
|
||||||
@mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk', autospec=True)
|
@mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk', autospec=True)
|
||||||
@mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.'
|
@mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.'
|
||||||
'remove_cleaning_network')
|
'remove_cleaning_network')
|
||||||
@mock.patch('ironic.conductor.utils.node_power_action', autospec=True)
|
@mock.patch('ironic.conductor.utils.node_power_action', autospec=True)
|
||||||
def _test_tear_down_inband_cleaning(
|
def _test_tear_down_inband_cleaning(
|
||||||
self, power_mock, remove_cleaning_network_mock,
|
self, power_mock, remove_cleaning_network_mock,
|
||||||
clean_up_ramdisk_mock, manage_boot=True):
|
clean_up_ramdisk_mock, is_fast_track_mock,
|
||||||
|
manage_boot=True, fast_track=False):
|
||||||
|
is_fast_track_mock.return_value = fast_track
|
||||||
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:
|
||||||
utils.tear_down_inband_cleaning(task, manage_boot=manage_boot)
|
utils.tear_down_inband_cleaning(task, manage_boot=manage_boot)
|
||||||
power_mock.assert_called_once_with(task, states.POWER_OFF)
|
if not fast_track:
|
||||||
|
power_mock.assert_called_once_with(task, states.POWER_OFF)
|
||||||
|
else:
|
||||||
|
self.assertFalse(power_mock.called)
|
||||||
remove_cleaning_network_mock.assert_called_once_with(task)
|
remove_cleaning_network_mock.assert_called_once_with(task)
|
||||||
if manage_boot:
|
if manage_boot:
|
||||||
clean_up_ramdisk_mock.assert_called_once_with(
|
clean_up_ramdisk_mock.assert_called_once_with(
|
||||||
@ -1758,6 +1772,9 @@ class AgentMethodsTestCase(db_base.DbTestCase):
|
|||||||
def test_tear_down_inband_cleaning_manage_boot_false(self):
|
def test_tear_down_inband_cleaning_manage_boot_false(self):
|
||||||
self._test_tear_down_inband_cleaning(manage_boot=False)
|
self._test_tear_down_inband_cleaning(manage_boot=False)
|
||||||
|
|
||||||
|
def test_tear_down_inband_cleaning_fast_track(self):
|
||||||
|
self._test_tear_down_inband_cleaning(fast_track=True)
|
||||||
|
|
||||||
def test_build_agent_options_conf(self):
|
def test_build_agent_options_conf(self):
|
||||||
self.config(api_url='https://api-url', group='conductor')
|
self.config(api_url='https://api-url', group='conductor')
|
||||||
options = utils.build_agent_options(self.node)
|
options = utils.build_agent_options(self.node)
|
||||||
|
@ -734,6 +734,53 @@ class ISCSIDeployTestCase(db_base.DbTestCase):
|
|||||||
task.driver.storage, task)
|
task.driver.storage, task)
|
||||||
self.assertEqual(2, storage_should_write_mock.call_count)
|
self.assertEqual(2, storage_should_write_mock.call_count)
|
||||||
|
|
||||||
|
@mock.patch('ironic.conductor.utils.is_fast_track', autospec=True)
|
||||||
|
@mock.patch.object(noop_storage.NoopStorage, 'attach_volumes',
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch.object(deploy_utils, 'populate_storage_driver_internal_info')
|
||||||
|
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk')
|
||||||
|
@mock.patch.object(deploy_utils, 'build_agent_options')
|
||||||
|
@mock.patch.object(deploy_utils, 'build_instance_info_for_deploy')
|
||||||
|
@mock.patch.object(flat_network.FlatNetwork, 'add_provisioning_network',
|
||||||
|
spec_set=True, autospec=True)
|
||||||
|
@mock.patch.object(flat_network.FlatNetwork,
|
||||||
|
'unconfigure_tenant_networks',
|
||||||
|
spec_set=True, autospec=True)
|
||||||
|
@mock.patch.object(flat_network.FlatNetwork, 'validate',
|
||||||
|
spec_set=True, autospec=True)
|
||||||
|
def test_prepare_fast_track(
|
||||||
|
self, validate_net_mock,
|
||||||
|
unconfigure_tenant_net_mock, add_provisioning_net_mock,
|
||||||
|
build_instance_info_mock, build_options_mock,
|
||||||
|
pxe_prepare_ramdisk_mock, storage_driver_info_mock,
|
||||||
|
storage_attach_volumes_mock, is_fast_track_mock):
|
||||||
|
# TODO(TheJulia): We should revisit this test. Smartnic
|
||||||
|
# support didn't wire in tightly on testing for power in
|
||||||
|
# these tests, and largely fast_track impacts power operations.
|
||||||
|
node = self.node
|
||||||
|
node.network_interface = 'flat'
|
||||||
|
node.save()
|
||||||
|
is_fast_track_mock.return_value = True
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=False) as task:
|
||||||
|
task.node.provision_state = states.DEPLOYING
|
||||||
|
build_options_mock.return_value = {'a': 'b'}
|
||||||
|
task.driver.deploy.prepare(task)
|
||||||
|
storage_driver_info_mock.assert_called_once_with(task)
|
||||||
|
# NOTE: Validate is the primary difference between agent/iscsi
|
||||||
|
self.assertFalse(validate_net_mock.called)
|
||||||
|
add_provisioning_net_mock.assert_called_once_with(mock.ANY, task)
|
||||||
|
unconfigure_tenant_net_mock.assert_called_once_with(mock.ANY, task)
|
||||||
|
self.assertTrue(storage_attach_volumes_mock.called)
|
||||||
|
self.assertFalse(build_instance_info_mock.called)
|
||||||
|
# TODO(TheJulia): We should likely consider executing the
|
||||||
|
# next two methods at some point in order to facilitate
|
||||||
|
# continuity. While not explicitly required for this feature
|
||||||
|
# to work, reboots as part of deployment would need the ramdisk
|
||||||
|
# present and ready.
|
||||||
|
self.assertFalse(build_options_mock.called)
|
||||||
|
self.assertFalse(pxe_prepare_ramdisk_mock.called)
|
||||||
|
|
||||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||||
@mock.patch.object(iscsi_deploy, 'check_image_size', autospec=True)
|
@mock.patch.object(iscsi_deploy, 'check_image_size', autospec=True)
|
||||||
@mock.patch.object(deploy_utils, 'cache_instance_image', autospec=True)
|
@mock.patch.object(deploy_utils, 'cache_instance_image', autospec=True)
|
||||||
@ -787,6 +834,36 @@ class ISCSIDeployTestCase(db_base.DbTestCase):
|
|||||||
self.assertEqual(2, mock_node_power_action.call_count)
|
self.assertEqual(2, mock_node_power_action.call_count)
|
||||||
self.assertEqual(states.DEPLOYING, task.node.provision_state)
|
self.assertEqual(states.DEPLOYING, task.node.provision_state)
|
||||||
|
|
||||||
|
@mock.patch.object(iscsi_deploy, 'check_image_size', autospec=True)
|
||||||
|
@mock.patch.object(deploy_utils, 'cache_instance_image', autospec=True)
|
||||||
|
@mock.patch.object(iscsi_deploy.ISCSIDeploy, 'continue_deploy',
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch('ironic.conductor.utils.is_fast_track', autospec=True)
|
||||||
|
@mock.patch.object(pxe.PXEBoot, 'prepare_instance', autospec=True)
|
||||||
|
@mock.patch('ironic.conductor.utils.node_power_action', autospec=True)
|
||||||
|
def test_deploy_fast_track(self, power_mock, mock_pxe_instance,
|
||||||
|
mock_is_fast_track, continue_deploy_mock,
|
||||||
|
cache_image_mock, check_image_size_mock):
|
||||||
|
mock_is_fast_track.return_value = True
|
||||||
|
self.node.target_provision_state = states.ACTIVE
|
||||||
|
self.node.provision_state = states.DEPLOYING
|
||||||
|
i_info = self.node.driver_internal_info
|
||||||
|
i_info['agent_url'] = 'http://1.2.3.4:1234'
|
||||||
|
self.node.driver_internal_info = i_info
|
||||||
|
self.node.save()
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node['uuid'], shared=False) as task:
|
||||||
|
task.driver.deploy.deploy(task)
|
||||||
|
self.assertFalse(power_mock.called)
|
||||||
|
self.assertFalse(mock_pxe_instance.called)
|
||||||
|
task.node.refresh()
|
||||||
|
self.assertEqual(states.DEPLOYWAIT, task.node.provision_state)
|
||||||
|
self.assertEqual(states.ACTIVE,
|
||||||
|
task.node.target_provision_state)
|
||||||
|
cache_image_mock.assert_called_with(mock.ANY, task.node)
|
||||||
|
check_image_size_mock.assert_called_with(task)
|
||||||
|
continue_deploy_mock.assert_called_with(mock.ANY, task)
|
||||||
|
|
||||||
@mock.patch.object(noop_storage.NoopStorage, 'detach_volumes',
|
@mock.patch.object(noop_storage.NoopStorage, 'detach_volumes',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(flat_network.FlatNetwork,
|
@mock.patch.object(flat_network.FlatNetwork,
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds a new feature called `fast-track` which allows an operator to
|
||||||
|
optionally configure the Bare Metal API Service and the Bare Metal
|
||||||
|
conductor service to permit lookup and heartbeat for nodes that are
|
||||||
|
in the process of being enrolled and created.
|
||||||
|
|
||||||
|
These nodes can be left online, from a process such as discovery.
|
||||||
|
If ironic-python-agent has communicated with the Bare Metal Service
|
||||||
|
API endpoint with-in the last `300` seconds, then setup steps that
|
||||||
|
are normally involved with preparing to launch a ramdisk on the node,
|
||||||
|
are skipped along with power operations to enable a baremetal node to
|
||||||
|
undergo discovery through to deployment with a single power cycle.
|
||||||
|
Fast track functionality may be enabled through the ``[deploy]fast_track``
|
||||||
|
option.
|
@ -466,3 +466,14 @@
|
|||||||
Run python 3 unit tests with driver dependencies installed.
|
Run python 3 unit tests with driver dependencies installed.
|
||||||
vars:
|
vars:
|
||||||
tox_envlist: unit-with-driver-libs-python3
|
tox_envlist: unit-with-driver-libs-python3
|
||||||
|
|
||||||
|
- job:
|
||||||
|
name: ironic-inspector-tempest-discovery-fast-track
|
||||||
|
description: ironic-inspector-tempest-discovery-fast-track
|
||||||
|
parent: ironic-inspector-tempest-discovery
|
||||||
|
vars:
|
||||||
|
tempest_test_regex: BareMetalFastTrackTest
|
||||||
|
devstack_localrc:
|
||||||
|
IRONIC_INSPECTOR_POWER_OFF: False
|
||||||
|
IRONIC_DEPLOY_FAST_TRACK: True
|
||||||
|
IRONIC_DEPLOY_FAST_TRACK_CLEANING: True
|
||||||
|
Loading…
Reference in New Issue
Block a user