Merge "Pre-shared agent token"
This commit is contained in:
commit
25879fec84
@ -2,6 +2,13 @@
|
||||
REST API Version History
|
||||
========================
|
||||
|
||||
1.62 (Ussuri, master)
|
||||
---------------------
|
||||
|
||||
This version of the API is to signify capability of an ironic deployment
|
||||
to support the ``agent token`` functionality with the
|
||||
``ironic-python-agent``.
|
||||
|
||||
1.61 (Ussuri, master)
|
||||
---------------------
|
||||
|
||||
|
@ -1245,6 +1245,9 @@ class Node(base.APIBase):
|
||||
if self.instance_info.get('image_url'):
|
||||
self.instance_info['image_url'] = "******"
|
||||
|
||||
if self.driver_internal_info.get('agent_secret_token'):
|
||||
self.driver_internal_info['agent_secret_token'] = "******"
|
||||
|
||||
update_state_in_older_versions(self)
|
||||
hide_fields_in_newer_versions(self)
|
||||
|
||||
|
@ -39,7 +39,7 @@ _LOOKUP_RETURN_FIELDS = ('uuid', 'properties', 'instance_info',
|
||||
'driver_internal_info')
|
||||
|
||||
|
||||
def config():
|
||||
def config(token):
|
||||
return {
|
||||
'metrics': {
|
||||
'backend': CONF.metrics.agent_backend,
|
||||
@ -52,7 +52,8 @@ def config():
|
||||
'statsd_host': CONF.metrics_statsd.agent_statsd_host,
|
||||
'statsd_port': CONF.metrics_statsd.agent_statsd_port
|
||||
},
|
||||
'heartbeat_timeout': CONF.api.ramdisk_heartbeat_timeout
|
||||
'heartbeat_timeout': CONF.api.ramdisk_heartbeat_timeout,
|
||||
'agent_token': token
|
||||
}
|
||||
|
||||
|
||||
@ -72,8 +73,9 @@ class LookupResult(base.APIBase):
|
||||
|
||||
@classmethod
|
||||
def convert_with_links(cls, node):
|
||||
token = node.driver_internal_info.get('agent_secret_token')
|
||||
node = node_ctl.Node.convert_with_links(node, _LOOKUP_RETURN_FIELDS)
|
||||
return cls(node=node, config=config())
|
||||
return cls(node=node, config=config(token))
|
||||
|
||||
|
||||
class LookupController(rest.RestController):
|
||||
@ -149,15 +151,27 @@ class LookupController(rest.RestController):
|
||||
and node.provision_state not in self.lookup_allowed_states):
|
||||
raise exception.NotFound()
|
||||
|
||||
return LookupResult.convert_with_links(node)
|
||||
if api_utils.allow_agent_token() or CONF.require_agent_token:
|
||||
try:
|
||||
topic = api.request.rpcapi.get_topic_for(node)
|
||||
except exception.NoValidHost as e:
|
||||
e.code = http_client.BAD_REQUEST
|
||||
raise
|
||||
|
||||
found_node = api.request.rpcapi.get_node_with_token(
|
||||
api.request.context, node.uuid, topic=topic)
|
||||
else:
|
||||
found_node = node
|
||||
return LookupResult.convert_with_links(found_node)
|
||||
|
||||
|
||||
class HeartbeatController(rest.RestController):
|
||||
"""Controller handling heartbeats from deploy ramdisk."""
|
||||
|
||||
@expose.expose(None, types.uuid_or_name, str,
|
||||
str, status_code=http_client.ACCEPTED)
|
||||
def post(self, node_ident, callback_url, agent_version=None):
|
||||
str, str, status_code=http_client.ACCEPTED)
|
||||
def post(self, node_ident, callback_url, agent_version=None,
|
||||
agent_token=None):
|
||||
"""Process a heartbeat from the deploy ramdisk.
|
||||
|
||||
:param node_ident: the UUID or logical name of a node.
|
||||
@ -197,6 +211,14 @@ class HeartbeatController(rest.RestController):
|
||||
raise exception.Invalid(
|
||||
_('Detected change in ramdisk provided '
|
||||
'"callback_url"'))
|
||||
# NOTE(TheJulia): If tokens are required, lets go ahead and fail the
|
||||
# heartbeat very early on.
|
||||
token_required = CONF.require_agent_token
|
||||
if token_required and agent_token is None:
|
||||
LOG.error('Agent heartbeat received for node %(node)s '
|
||||
'without an agent token.', {'node': node_ident})
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Agent token is required for heartbeat processing.'))
|
||||
|
||||
try:
|
||||
topic = api.request.rpcapi.get_topic_for(rpc_node)
|
||||
@ -206,4 +228,4 @@ class HeartbeatController(rest.RestController):
|
||||
|
||||
api.request.rpcapi.heartbeat(
|
||||
api.request.context, rpc_node.uuid, callback_url,
|
||||
agent_version, topic=topic)
|
||||
agent_version, agent_token, topic=topic)
|
||||
|
@ -1316,3 +1316,8 @@ def allow_allocation_owner():
|
||||
Version 1.60 of the API added the owner field to the allocation object.
|
||||
"""
|
||||
return api.request.version.minor >= versions.MINOR_60_ALLOCATION_OWNER
|
||||
|
||||
|
||||
def allow_agent_token():
|
||||
"""Check if agent token is available."""
|
||||
return api.request.version.minor >= versions.MINOR_62_AGENT_TOKEN
|
||||
|
@ -99,6 +99,7 @@ BASE_VERSION = 1
|
||||
# v1.59: Add support vendor data in configdrives.
|
||||
# v1.60: Add owner to the allocation object.
|
||||
# v1.61: Add retired and retired_reason to the node object.
|
||||
# v1.62: Add agent_token support for agent communication.
|
||||
|
||||
MINOR_0_JUNO = 0
|
||||
MINOR_1_INITIAL_VERSION = 1
|
||||
@ -162,6 +163,7 @@ MINOR_58_ALLOCATION_BACKFILL = 58
|
||||
MINOR_59_CONFIGDRIVE_VENDOR_DATA = 59
|
||||
MINOR_60_ALLOCATION_OWNER = 60
|
||||
MINOR_61_NODE_RETIRED = 61
|
||||
MINOR_62_AGENT_TOKEN = 62
|
||||
|
||||
# When adding another version, update:
|
||||
# - MINOR_MAX_VERSION
|
||||
@ -169,7 +171,7 @@ MINOR_61_NODE_RETIRED = 61
|
||||
# explanation of what changed in the new version
|
||||
# - common/release_mappings.py, RELEASE_MAPPING['master']['api']
|
||||
|
||||
MINOR_MAX_VERSION = MINOR_61_NODE_RETIRED
|
||||
MINOR_MAX_VERSION = MINOR_62_AGENT_TOKEN
|
||||
|
||||
# String representations of the minor and maximum versions
|
||||
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||
|
@ -54,9 +54,21 @@ def warn_about_missing_default_boot_option(conf):
|
||||
'an explicit value for it during the transition period')
|
||||
|
||||
|
||||
def warn_about_agent_token_deprecation(conf):
|
||||
if not conf.require_agent_token:
|
||||
LOG.warning('The ``[DEFAULT]require_agent_token`` option is not '
|
||||
'set and support for ironic-python-agents that do not '
|
||||
'utilize agent tokens, along with the configuration '
|
||||
'option will be removed in the W development cycle. '
|
||||
'Please upgrade your ironic-python-agent version, and '
|
||||
'consider adopting the require_agent_token setting '
|
||||
'during the Victoria development cycle.')
|
||||
|
||||
|
||||
def issue_startup_warnings(conf):
|
||||
warn_about_unsafe_shred_parameters(conf)
|
||||
warn_about_missing_default_boot_option(conf)
|
||||
warn_about_agent_token_deprecation(conf)
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -214,8 +214,8 @@ RELEASE_MAPPING = {
|
||||
}
|
||||
},
|
||||
'master': {
|
||||
'api': '1.61',
|
||||
'rpc': '1.48',
|
||||
'api': '1.62',
|
||||
'rpc': '1.49',
|
||||
'objects': {
|
||||
'Allocation': ['1.1'],
|
||||
'Node': ['1.33', '1.32'],
|
||||
|
@ -213,6 +213,9 @@ def do_next_clean_step(task, step_index):
|
||||
driver_internal_info.pop('clean_step_index', None)
|
||||
driver_internal_info.pop('cleaning_reboot', None)
|
||||
driver_internal_info.pop('cleaning_polling', None)
|
||||
driver_internal_info.pop('agent_secret_token', None)
|
||||
driver_internal_info.pop('agent_secret_token_pregenerated', None)
|
||||
|
||||
# Remove agent_url
|
||||
if not utils.fast_track_able(task):
|
||||
driver_internal_info.pop('agent_url', None)
|
||||
@ -271,6 +274,8 @@ def do_node_clean_abort(task, step_name=None):
|
||||
info.pop('cleaning_polling', None)
|
||||
info.pop('skip_current_clean_step', None)
|
||||
info.pop('agent_url', None)
|
||||
info.pop('agent_secret_token', None)
|
||||
info.pop('agent_secret_token_pregenerated', None)
|
||||
node.driver_internal_info = info
|
||||
node.save()
|
||||
LOG.info(info_message)
|
||||
|
@ -133,7 +133,7 @@ def start_deploy(task, manager, configdrive=None, event='deploy'):
|
||||
def do_node_deploy(task, conductor_id=None, configdrive=None):
|
||||
"""Prepare the environment and deploy a node."""
|
||||
node = task.node
|
||||
|
||||
utils.del_secret_token(node)
|
||||
try:
|
||||
if configdrive:
|
||||
if isinstance(configdrive, dict):
|
||||
@ -392,6 +392,8 @@ def do_next_deploy_step(task, step_index, conductor_id):
|
||||
# Finished executing the steps. Clear deploy_step.
|
||||
node.deploy_step = None
|
||||
driver_internal_info = node.driver_internal_info
|
||||
driver_internal_info.pop('agent_secret_token', None)
|
||||
driver_internal_info.pop('agent_secret_token_pregenerated', None)
|
||||
driver_internal_info['deploy_steps'] = None
|
||||
driver_internal_info.pop('deploy_step_index', None)
|
||||
driver_internal_info.pop('deployment_reboot', None)
|
||||
|
@ -90,7 +90,7 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
# NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's.
|
||||
# NOTE(pas-ha): This also must be in sync with
|
||||
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
||||
RPC_API_VERSION = '1.48'
|
||||
RPC_API_VERSION = '1.49'
|
||||
|
||||
target = messaging.Target(version=RPC_API_VERSION)
|
||||
|
||||
@ -1008,6 +1008,8 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
node.instance_info = {}
|
||||
node.instance_uuid = None
|
||||
driver_internal_info = node.driver_internal_info
|
||||
driver_internal_info.pop('agent_secret_token', None)
|
||||
driver_internal_info.pop('agent_secret_token_pregenerated', None)
|
||||
driver_internal_info.pop('instance', None)
|
||||
driver_internal_info.pop('clean_steps', None)
|
||||
driver_internal_info.pop('root_uuid_or_disk_id', None)
|
||||
@ -2924,7 +2926,8 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
|
||||
@METRICS.timer('ConductorManager.heartbeat')
|
||||
@messaging.expected_exceptions(exception.NoFreeConductorWorker)
|
||||
def heartbeat(self, context, node_id, callback_url, agent_version=None):
|
||||
def heartbeat(self, context, node_id, callback_url, agent_version=None,
|
||||
agent_token=None):
|
||||
"""Process a heartbeat from the ramdisk.
|
||||
|
||||
:param context: request context.
|
||||
@ -2945,10 +2948,43 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
if agent_version is None:
|
||||
agent_version = '3.0.0'
|
||||
|
||||
token_required = CONF.require_agent_token
|
||||
|
||||
# NOTE(dtantsur): we acquire a shared lock to begin with, drivers are
|
||||
# free to promote it to an exclusive one.
|
||||
with task_manager.acquire(context, node_id, shared=True,
|
||||
purpose='heartbeat') as task:
|
||||
|
||||
# NOTE(TheJulia): The "token" line of defense.
|
||||
# either tokens are required and they are present,
|
||||
# or a token is present in general and needs to be
|
||||
# validated.
|
||||
if token_required or utils.is_agent_token_present(task.node):
|
||||
if not utils.is_agent_token_valid(task.node, agent_token):
|
||||
LOG.error('Invalid agent_token receieved for node '
|
||||
'%(node)s', {'node': node_id})
|
||||
raise exception.InvalidParameterValue(
|
||||
'Invalid or missing agent token received.')
|
||||
elif utils.is_agent_token_supported(agent_version):
|
||||
LOG.error('Suspicious activity detected for node %(node)s '
|
||||
'when attempting to heartbeat. Heartbeat '
|
||||
'request has been rejected as the version of '
|
||||
'ironic-python-agent indicated in the heartbeat '
|
||||
'operation should support agent token '
|
||||
'functionality.',
|
||||
{'node': task.node.uuid})
|
||||
raise exception.InvalidParameterValue(
|
||||
'Invalid or missing agent token received.')
|
||||
else:
|
||||
LOG.warning('Out of date agent detected for node '
|
||||
'%(node)s. Agent version %(version) '
|
||||
'reported. Support for this version is '
|
||||
'deprecated.',
|
||||
{'node': task.node.uuid,
|
||||
'version': agent_version})
|
||||
# TODO(TheJulia): raise an exception as of the
|
||||
# ?Victoria? development cycle.
|
||||
|
||||
task.spawn_after(
|
||||
self._spawn_worker, task.driver.deploy.heartbeat,
|
||||
task, callback_url, agent_version)
|
||||
@ -3301,6 +3337,42 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
LOG.exception('Unexpected exception when taking over '
|
||||
'allocation %s', allocation.uuid)
|
||||
|
||||
@METRICS.timer('ConductorManager.get_node_with_token')
|
||||
@messaging.expected_exceptions(exception.NodeLocked,
|
||||
exception.Invalid)
|
||||
def get_node_with_token(self, context, node_id):
|
||||
"""Add secret agent token to node.
|
||||
|
||||
:param context: request context.
|
||||
:param node_id: node ID or UUID.
|
||||
:returns: Secret token set for the node.
|
||||
:raises: NodeLocked, if node has an exclusive lock held on it
|
||||
:raises: Invalid, if the node already has a token set.
|
||||
"""
|
||||
LOG.debug("RPC get_node_with_token called for the node %(node_id)s",
|
||||
{'node_id': node_id})
|
||||
with task_manager.acquire(context, node_id,
|
||||
purpose='generate_token',
|
||||
shared=True) as task:
|
||||
node = task.node
|
||||
if utils.is_agent_token_present(task.node):
|
||||
LOG.warning('An agent token generation request is being '
|
||||
'refused as one is already present for '
|
||||
'node %(node)s',
|
||||
{'node': node_id})
|
||||
# Allow lookup to work by returning a value, it is just an
|
||||
# unusable value that can't be verified against.
|
||||
# This is important if the agent lookup has occured with
|
||||
# pre-generation of tokens with virtual media usage.
|
||||
node.driver_internal_info['agent_secret_token'] = "******"
|
||||
return node
|
||||
task.upgrade_lock()
|
||||
LOG.debug('Generating agent token for node %(node)s',
|
||||
{'node': task.node.uuid})
|
||||
utils.add_secret_token(task.node)
|
||||
task.node.save()
|
||||
return task.node
|
||||
|
||||
|
||||
@METRICS.timer('get_vendor_passthru_metadata')
|
||||
def get_vendor_passthru_metadata(route_dict):
|
||||
|
@ -99,13 +99,15 @@ class ConductorAPI(object):
|
||||
| 1.46 - Added reset_interfaces to update_node
|
||||
| 1.47 - Added support for conductor groups
|
||||
| 1.48 - Added allocation API
|
||||
| 1.49 - Added get_node_with_token and agent_token argument to
|
||||
heartbeat
|
||||
|
||||
"""
|
||||
|
||||
# NOTE(rloo): This must be in sync with manager.ConductorManager's.
|
||||
# NOTE(pas-ha): This also must be in sync with
|
||||
# ironic.common.release_mappings.RELEASE_MAPPING['master']
|
||||
RPC_API_VERSION = '1.48'
|
||||
RPC_API_VERSION = '1.49'
|
||||
|
||||
def __init__(self, topic=None):
|
||||
super(ConductorAPI, self).__init__()
|
||||
@ -811,7 +813,7 @@ class ConductorAPI(object):
|
||||
node_id=node_id, clean_steps=clean_steps)
|
||||
|
||||
def heartbeat(self, context, node_id, callback_url, agent_version,
|
||||
topic=None):
|
||||
agent_token=None, topic=None):
|
||||
"""Process a node heartbeat.
|
||||
|
||||
:param context: request context.
|
||||
@ -825,6 +827,9 @@ class ConductorAPI(object):
|
||||
if self.client.can_send_version('1.42'):
|
||||
version = '1.42'
|
||||
new_kws['agent_version'] = agent_version
|
||||
if self.client.can_send_version('1.49'):
|
||||
version = '1.49'
|
||||
new_kws['agent_token'] = agent_token
|
||||
cctxt = self.client.prepare(topic=topic or self.topic, version=version)
|
||||
return cctxt.call(context, 'heartbeat', node_id=node_id,
|
||||
callback_url=callback_url, **new_kws)
|
||||
@ -1133,3 +1138,16 @@ class ConductorAPI(object):
|
||||
"""
|
||||
cctxt = self.client.prepare(topic=topic or self.topic, version='1.48')
|
||||
return cctxt.call(context, 'destroy_allocation', allocation=allocation)
|
||||
|
||||
def get_node_with_token(self, context, node_id, topic=None):
|
||||
"""Request the node from the conductor with an agent token
|
||||
|
||||
:param context: request context.
|
||||
:param node_id: node ID or UUID.
|
||||
:param topic: RPC topic. Defaults to self.topic.
|
||||
:raises: NodeLocked if node is locked by another conductor.
|
||||
|
||||
:returns: A Node object with agent token.
|
||||
"""
|
||||
cctxt = self.client.prepare(topic=topic or self.topic, version='1.49')
|
||||
return cctxt.call(context, 'get_node_with_token', node_id=node_id)
|
||||
|
@ -14,6 +14,9 @@
|
||||
|
||||
import contextlib
|
||||
import datetime
|
||||
from distutils.version import StrictVersion
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
|
||||
from openstack.baremetal import configdrive as os_configdrive
|
||||
@ -1005,3 +1008,94 @@ def get_node_next_clean_steps(task, skip_current_step=True):
|
||||
def get_node_next_deploy_steps(task, skip_current_step=True):
|
||||
return _get_node_next_steps(task, 'deploy',
|
||||
skip_current_step=skip_current_step)
|
||||
|
||||
|
||||
def add_secret_token(node):
|
||||
"""Adds a secret token to driver_internal_info for IPA verification."""
|
||||
characters = string.ascii_letters + string.digits
|
||||
token = ''.join(
|
||||
random.SystemRandom().choice(characters) for i in range(128))
|
||||
i_info = node.driver_internal_info
|
||||
i_info['agent_secret_token'] = token
|
||||
node.driver_internal_info = i_info
|
||||
|
||||
|
||||
def del_secret_token(node):
|
||||
"""Deletes the IPA agent secret token.
|
||||
|
||||
Removes the agent token secret from the driver_internal_info field
|
||||
from the Node object.
|
||||
|
||||
:param node: Node object
|
||||
"""
|
||||
i_info = node.driver_internal_info
|
||||
i_info.pop('agent_secret_token', None)
|
||||
node.driver_internal_info = i_info
|
||||
|
||||
|
||||
def is_agent_token_present(node):
|
||||
"""Determines if an agent token is present upon a node.
|
||||
|
||||
:param node: Node object
|
||||
:returns: True if an agent_secret_token value is present in a node
|
||||
driver_internal_info field.
|
||||
"""
|
||||
# TODO(TheJulia): we should likely record the time when we add the token
|
||||
# and then compare if it was in the last ?hour? to act as an additional
|
||||
# guard rail, but if we do that we will want to check the last heartbeat
|
||||
# because the heartbeat overrides the age of the token.
|
||||
# We may want to do this elsewhere or nowhere, just a thought for the
|
||||
# future.
|
||||
return node.driver_internal_info.get(
|
||||
'agent_secret_token', None) is not None
|
||||
|
||||
|
||||
def is_agent_token_valid(node, token):
|
||||
"""Validates if a supplied token is valid for the node.
|
||||
|
||||
:param node: Node object
|
||||
:token: A token value to validate against the driver_internal_info field
|
||||
agent_sercret_token.
|
||||
:returns: True if the supplied token matches the token recorded in the
|
||||
supplied node object.
|
||||
"""
|
||||
if token is None:
|
||||
# No token is never valid.
|
||||
return False
|
||||
known_token = node.driver_internal_info.get('agent_secret_token', None)
|
||||
return known_token == token
|
||||
|
||||
|
||||
def is_agent_token_supported(agent_version):
|
||||
# NOTE(TheJulia): This is hoped that 6.x supports
|
||||
# agent token capabilities and realistically needs to be updated
|
||||
# once that version of IPA is out there in some shape or form.
|
||||
# This allows us to gracefully allow older agent's that were
|
||||
# launched via pre-generated agent_tokens, to still work
|
||||
# and could likely be removed at some point down the road.
|
||||
version = str(agent_version).replace('.dev', 'b', 1)
|
||||
return StrictVersion(version) > StrictVersion('6.1.0')
|
||||
|
||||
|
||||
def is_agent_token_pregenerated(node):
|
||||
"""Determines if the token was generated for out of band configuration.
|
||||
|
||||
Ironic supports the ability to provide configuration data to the agent
|
||||
through the a virtual floppy or as part of the virtual media image
|
||||
which is attached to the BMC.
|
||||
|
||||
This method helps us identify WHEN we did so as we don't need to remove
|
||||
records of the token prior to rebooting the token. This is important as
|
||||
tokens provided through out of band means presist in the virtual media
|
||||
image, are loaded as part of the agent ramdisk, and do not require
|
||||
regeneration of the token upon the initial lookup, ultimately making
|
||||
the overall usage of virtual media and pregenerated tokens far more
|
||||
secure.
|
||||
|
||||
:param node: Node Object
|
||||
:returns: True if the token was pregenerated as indicated by the node's
|
||||
driver_internal_info field.
|
||||
False in all other cases.
|
||||
"""
|
||||
return node.driver_internal_info.get(
|
||||
'agent_secret_token_pregenerated', False)
|
||||
|
@ -327,6 +327,15 @@ service_opts = [
|
||||
('json-rpc', _('use JSON RPC transport'))],
|
||||
help=_('Which RPC transport implementation to use between '
|
||||
'conductor and API services')),
|
||||
cfg.BoolOpt('require_agent_token',
|
||||
default=False,
|
||||
help=_('Used to require the use of agent tokens. These '
|
||||
'tokens are used to guard the api lookup endpoint and '
|
||||
'conductor heartbeat processing logic to authenticate '
|
||||
'transactions with the ironic-python-agent. Tokens '
|
||||
'are provided only upon the first lookup of a node '
|
||||
'and may be provided via out of band means through '
|
||||
'the use of virtual media.')),
|
||||
]
|
||||
|
||||
utils_opts = [
|
||||
|
@ -166,6 +166,10 @@ def _cleaning_reboot(task):
|
||||
# Signify that we've rebooted
|
||||
driver_internal_info = task.node.driver_internal_info
|
||||
driver_internal_info['cleaning_reboot'] = True
|
||||
if not driver_internal_info.get('agent_secret_token_pregenerated', False):
|
||||
# Wipes out the existing recorded token because the machine will
|
||||
# need to re-establish the token.
|
||||
driver_internal_info.pop('agent_secret_token', None)
|
||||
task.node.driver_internal_info = driver_internal_info
|
||||
task.node.save()
|
||||
|
||||
|
@ -1486,6 +1486,24 @@ def get_async_step_return_state(node):
|
||||
return states.CLEANWAIT if node.clean_step else states.DEPLOYWAIT
|
||||
|
||||
|
||||
def _check_agent_token_prior_to_agent_reboot(driver_internal_info):
|
||||
"""Removes the agent token if it was not pregenerated.
|
||||
|
||||
Removal of the agent token in cases where it is not pregenerated
|
||||
is a vital action prior to rebooting the agent, as without doing
|
||||
so the agent will be unable to establish communication with
|
||||
the ironic API after the reboot. Effectively locking itself out
|
||||
as in cases where the value is not pregenerated, it is not
|
||||
already included in the payload and must be generated again
|
||||
upon lookup.
|
||||
|
||||
:param driver_internal_info: The driver_interal_info dict object
|
||||
from a Node object.
|
||||
"""
|
||||
if not driver_internal_info.get('agent_secret_token_pregenerated', False):
|
||||
driver_internal_info.pop('agent_secret_token', None)
|
||||
|
||||
|
||||
def set_async_step_flags(node, reboot=None, skip_current_step=None,
|
||||
polling=None):
|
||||
"""Sets appropriate reboot flags in driver_internal_info based on operation
|
||||
@ -1515,6 +1533,10 @@ def set_async_step_flags(node, reboot=None, skip_current_step=None,
|
||||
fields = cleaning if node.clean_step else deployment
|
||||
if reboot is not None:
|
||||
info[fields['reboot']] = reboot
|
||||
if reboot:
|
||||
# If rebooting, we must ensure that we check and remove
|
||||
# an agent token if necessary.
|
||||
_check_agent_token_prior_to_agent_reboot(info)
|
||||
if skip_current_step is not None:
|
||||
info[fields['skip']] = skip_current_step
|
||||
if polling is not None:
|
||||
|
@ -171,6 +171,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||
d.get('driver_info', {}), "******")
|
||||
d['instance_info'] = strutils.mask_dict_password(
|
||||
d.get('instance_info', {}), "******")
|
||||
d['driver_internal_info'] = strutils.mask_dict_password(
|
||||
d.get('driver_internal_info', {}), "******")
|
||||
return d
|
||||
|
||||
def _validate_property_values(self, properties):
|
||||
|
@ -605,6 +605,15 @@ class TestListNodes(test_api_base.BaseApiTest):
|
||||
headers={api_base.Version.string: '1.61'})
|
||||
self.assertIn('retired', response)
|
||||
|
||||
def test_get_one_with_no_agent_secret(self):
|
||||
node = obj_utils.create_test_node(
|
||||
self.context,
|
||||
driver_internal_info={'agent_secret_token': 'abcdefg'})
|
||||
response = self.get_json('/nodes/%s' % (node.uuid),
|
||||
headers={api_base.Version.string: '1.52'})
|
||||
token_value = response['driver_internal_info']['agent_secret_token']
|
||||
self.assertEqual('******', token_value)
|
||||
|
||||
def test_detail(self):
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
chassis_id=self.chassis.id)
|
||||
|
@ -50,6 +50,16 @@ class TestLookup(test_api_base.BaseApiTest):
|
||||
fixtures.MockPatchObject(rpcapi.ConductorAPI, 'get_conductor_for',
|
||||
autospec=True)).mock
|
||||
self.mock_get_conductor_for.return_value = 'fake.conductor'
|
||||
self.mock_get_node_with_token = self.useFixture(
|
||||
fixtures.MockPatchObject(rpcapi.ConductorAPI,
|
||||
'get_node_with_token',
|
||||
autospec=True)).mock
|
||||
|
||||
def _set_secret_mock(self, node, token_value):
|
||||
driver_internal = node.driver_internal_info
|
||||
driver_internal['agent_secret_token'] = token_value
|
||||
node.driver_internal_info = driver_internal
|
||||
self.mock_get_node_with_token.return_value = node
|
||||
|
||||
def _check_config(self, data):
|
||||
expected_metrics = {
|
||||
@ -65,9 +75,12 @@ class TestLookup(test_api_base.BaseApiTest):
|
||||
'statsd_host': CONF.metrics_statsd.agent_statsd_host,
|
||||
'statsd_port': CONF.metrics_statsd.agent_statsd_port
|
||||
},
|
||||
'heartbeat_timeout': CONF.api.ramdisk_heartbeat_timeout
|
||||
'heartbeat_timeout': CONF.api.ramdisk_heartbeat_timeout,
|
||||
'agent_token': mock.ANY
|
||||
}
|
||||
self.assertEqual(expected_metrics, data['config'])
|
||||
self.assertIsNotNone(data['config']['agent_token'])
|
||||
self.assertNotEqual('******', data['config']['agent_token'])
|
||||
|
||||
def test_nothing_provided(self):
|
||||
response = self.get_json(
|
||||
@ -95,6 +108,7 @@ class TestLookup(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||
|
||||
def test_found_by_addresses(self):
|
||||
self._set_secret_mock(self.node, 'some-value')
|
||||
obj_utils.create_test_port(self.context,
|
||||
node_id=self.node.id,
|
||||
address=self.addresses[1])
|
||||
@ -109,6 +123,7 @@ class TestLookup(test_api_base.BaseApiTest):
|
||||
|
||||
@mock.patch.object(ramdisk.LOG, 'warning', autospec=True)
|
||||
def test_ignore_malformed_address(self, mock_log):
|
||||
self._set_secret_mock(self.node, '123456')
|
||||
obj_utils.create_test_port(self.context,
|
||||
node_id=self.node.id,
|
||||
address=self.addresses[1])
|
||||
@ -125,6 +140,7 @@ class TestLookup(test_api_base.BaseApiTest):
|
||||
self.assertTrue(mock_log.called)
|
||||
|
||||
def test_found_by_uuid(self):
|
||||
self._set_secret_mock(self.node, 'this_thing_on?')
|
||||
data = self.get_json(
|
||||
'/lookup?addresses=%s&node_uuid=%s' %
|
||||
(','.join(self.addresses), self.node.uuid),
|
||||
@ -135,6 +151,7 @@ class TestLookup(test_api_base.BaseApiTest):
|
||||
self._check_config(data)
|
||||
|
||||
def test_found_by_only_uuid(self):
|
||||
self._set_secret_mock(self.node, 'xyzabc')
|
||||
data = self.get_json(
|
||||
'/lookup?node_uuid=%s' % self.node.uuid,
|
||||
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||
@ -153,6 +170,7 @@ class TestLookup(test_api_base.BaseApiTest):
|
||||
|
||||
def test_no_restrict_lookup(self):
|
||||
CONF.set_override('restrict_lookup', False, 'api')
|
||||
self._set_secret_mock(self.node2, '234567890')
|
||||
data = self.get_json(
|
||||
'/lookup?addresses=%s&node_uuid=%s' %
|
||||
(','.join(self.addresses), self.node2.uuid),
|
||||
@ -163,6 +181,7 @@ class TestLookup(test_api_base.BaseApiTest):
|
||||
self._check_config(data)
|
||||
|
||||
def test_fast_deploy_lookup(self):
|
||||
self._set_secret_mock(self.node, 'abcxyz')
|
||||
CONF.set_override('fast_track', True, 'deploy')
|
||||
for provision_state in [states.ENROLL, states.MANAGEABLE,
|
||||
states.AVAILABLE]:
|
||||
@ -203,7 +222,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', None,
|
||||
node.uuid, 'url', None, None,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||
@ -216,7 +235,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', None,
|
||||
node.uuid, 'url', None, None,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||
@ -229,7 +248,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', None,
|
||||
node.uuid, 'url', None, None,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||
@ -243,7 +262,7 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', '1.4.1',
|
||||
node.uuid, 'url', '1.4.1', None,
|
||||
topic='test-topic')
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||
@ -268,3 +287,18 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
||||
headers={api_base.Version.string: str(api_v1.max_version())},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||
|
||||
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||
def test_ok_agent_token(self, mock_heartbeat):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
response = self.post_json(
|
||||
'/heartbeat/%s' % node.uuid,
|
||||
{'callback_url': 'url',
|
||||
'agent_token': 'abcdef1'},
|
||||
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||
self.assertEqual(b'', response.body)
|
||||
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
node.uuid, 'url', None,
|
||||
'abcdef1',
|
||||
topic='test-topic')
|
||||
|
@ -565,6 +565,12 @@ class TestCheckAllowFields(base.TestCase):
|
||||
mock_request.version.minor = 54
|
||||
self.assertFalse(utils.allow_deploy_templates())
|
||||
|
||||
def test_allow_agent_token(self, mock_request):
|
||||
mock_request.version.minor = 62
|
||||
self.assertTrue(utils.allow_agent_token())
|
||||
mock_request.version.minor = 61
|
||||
self.assertFalse(utils.allow_agent_token())
|
||||
|
||||
|
||||
@mock.patch.object(api, 'request')
|
||||
class TestNodeIdent(base.TestCase):
|
||||
|
@ -358,7 +358,8 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
expected_calls = [
|
||||
mock.call(node.uuid,
|
||||
{'version': mock.ANY,
|
||||
'instance_info': expected_instance_info}),
|
||||
'instance_info': expected_instance_info,
|
||||
'driver_internal_info': mock.ANY}),
|
||||
mock.call(node.uuid,
|
||||
{'version': mock.ANY,
|
||||
'last_error': mock.ANY}),
|
||||
|
@ -6885,6 +6885,10 @@ class DoNodeTakeOverTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
@mgr_utils.mock_record_keepalive
|
||||
class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
|
||||
def _fake_spawn(self, conductor_obj, func, *args, **kwargs):
|
||||
func(*args, **kwargs)
|
||||
return mock.MagicMock()
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.validate')
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeBoot.validate')
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeConsole.start_console')
|
||||
@ -7031,6 +7035,10 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
self.assertEqual(states.NOSTATE, node.target_provision_state)
|
||||
self.assertIsNone(node.last_error)
|
||||
|
||||
# TODO(TheJulia): We should double check if these heartbeat tests need
|
||||
# to move. I have this strange feeling we were lacking rpc testing of
|
||||
# heartbeat until we did adoption testing....
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
|
||||
@ -7046,10 +7054,7 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
|
||||
mock_spawn.reset_mock()
|
||||
|
||||
def fake_spawn(conductor_obj, func, *args, **kwargs):
|
||||
func(*args, **kwargs)
|
||||
return mock.MagicMock()
|
||||
mock_spawn.side_effect = fake_spawn
|
||||
mock_spawn.side_effect = self._fake_spawn
|
||||
|
||||
self.service.heartbeat(self.context, node.uuid, 'http://callback')
|
||||
mock_heartbeat.assert_called_with(mock.ANY, mock.ANY,
|
||||
@ -7070,16 +7075,191 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
|
||||
mock_spawn.reset_mock()
|
||||
|
||||
def fake_spawn(conductor_obj, func, *args, **kwargs):
|
||||
func(*args, **kwargs)
|
||||
return mock.MagicMock()
|
||||
mock_spawn.side_effect = fake_spawn
|
||||
mock_spawn.side_effect = self._fake_spawn
|
||||
|
||||
self.service.heartbeat(
|
||||
self.context, node.uuid, 'http://callback', '1.4.1')
|
||||
mock_heartbeat.assert_called_with(mock.ANY, mock.ANY,
|
||||
'http://callback', '1.4.1')
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
|
||||
autospec=True)
|
||||
def test_heartbeat_with_no_required_agent_token(self, mock_spawn,
|
||||
mock_heartbeat):
|
||||
"""Tests that we kill the heartbeat attempt very early on."""
|
||||
self.config(require_agent_token=True)
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.DEPLOYING,
|
||||
target_provision_state=states.ACTIVE)
|
||||
|
||||
self._start_service()
|
||||
|
||||
mock_spawn.reset_mock()
|
||||
|
||||
mock_spawn.side_effect = self._fake_spawn
|
||||
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue, self.service.heartbeat,
|
||||
self.context, node.uuid, 'http://callback', agent_token=None)
|
||||
self.assertFalse(mock_heartbeat.called)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
|
||||
autospec=True)
|
||||
def test_heartbeat_with_required_agent_token(self, mock_spawn,
|
||||
mock_heartbeat):
|
||||
"""Test heartbeat works when token matches."""
|
||||
self.config(require_agent_token=True)
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.DEPLOYING,
|
||||
target_provision_state=states.ACTIVE,
|
||||
driver_internal_info={'agent_secret_token': 'a secret'})
|
||||
|
||||
self._start_service()
|
||||
|
||||
mock_spawn.reset_mock()
|
||||
|
||||
mock_spawn.side_effect = self._fake_spawn
|
||||
|
||||
self.service.heartbeat(self.context, node.uuid, 'http://callback',
|
||||
agent_token='a secret')
|
||||
mock_heartbeat.assert_called_with(mock.ANY, mock.ANY,
|
||||
'http://callback', '3.0.0')
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
|
||||
autospec=True)
|
||||
def test_heartbeat_with_agent_token(self, mock_spawn,
|
||||
mock_heartbeat):
|
||||
"""Test heartbeat works when token matches."""
|
||||
self.config(require_agent_token=False)
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.DEPLOYING,
|
||||
target_provision_state=states.ACTIVE,
|
||||
driver_internal_info={'agent_secret_token': 'a secret'})
|
||||
|
||||
self._start_service()
|
||||
|
||||
mock_spawn.reset_mock()
|
||||
|
||||
mock_spawn.side_effect = self._fake_spawn
|
||||
|
||||
self.service.heartbeat(self.context, node.uuid, 'http://callback',
|
||||
agent_token='a secret')
|
||||
mock_heartbeat.assert_called_with(mock.ANY, mock.ANY,
|
||||
'http://callback', '3.0.0')
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
|
||||
autospec=True)
|
||||
def test_heartbeat_invalid_agent_token(self, mock_spawn,
|
||||
mock_heartbeat):
|
||||
"""Heartbeat fails when it does not match."""
|
||||
self.config(require_agent_token=False)
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.DEPLOYING,
|
||||
target_provision_state=states.ACTIVE,
|
||||
driver_internal_info={'agent_secret_token': 'a secret'})
|
||||
|
||||
self._start_service()
|
||||
|
||||
mock_spawn.reset_mock()
|
||||
|
||||
mock_spawn.side_effect = self._fake_spawn
|
||||
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.service.heartbeat, self.context,
|
||||
node.uuid, 'http://callback',
|
||||
agent_token='evil', agent_version='5.0.0b23')
|
||||
self.assertFalse(mock_heartbeat.called)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
|
||||
autospec=True)
|
||||
def test_heartbeat_invalid_agent_token_older_version(
|
||||
self, mock_spawn, mock_heartbeat):
|
||||
"""Heartbeat is rejected if token is received that is invalid."""
|
||||
self.config(require_agent_token=False)
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.DEPLOYING,
|
||||
target_provision_state=states.ACTIVE,
|
||||
driver_internal_info={'agent_secret_token': 'a secret'})
|
||||
|
||||
self._start_service()
|
||||
|
||||
mock_spawn.reset_mock()
|
||||
|
||||
mock_spawn.side_effect = self._fake_spawn
|
||||
# Intentionally sending an older client in case something fishy
|
||||
# occurs.
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.service.heartbeat, self.context,
|
||||
node.uuid, 'http://callback',
|
||||
agent_token='evil', agent_version='4.0.0')
|
||||
self.assertFalse(mock_heartbeat.called)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
|
||||
autospec=True)
|
||||
def test_heartbeat_invalid_when_token_on_file_older_agent(
|
||||
self, mock_spawn, mock_heartbeat):
|
||||
"""Heartbeat rejected if a token is on file."""
|
||||
self.config(require_agent_token=False)
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.DEPLOYING,
|
||||
target_provision_state=states.ACTIVE,
|
||||
driver_internal_info={'agent_secret_token': 'a secret'})
|
||||
|
||||
self._start_service()
|
||||
|
||||
mock_spawn.reset_mock()
|
||||
|
||||
mock_spawn.side_effect = self._fake_spawn
|
||||
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.service.heartbeat, self.context,
|
||||
node.uuid, 'http://callback',
|
||||
agent_token=None, agent_version='4.0.0')
|
||||
self.assertFalse(mock_heartbeat.called)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
|
||||
autospec=True)
|
||||
def test_heartbeat_invalid_newer_version(
|
||||
self, mock_spawn, mock_heartbeat):
|
||||
"""Heartbeat rejected if client should be sending a token."""
|
||||
self.config(require_agent_token=False)
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.DEPLOYING,
|
||||
target_provision_state=states.ACTIVE)
|
||||
|
||||
self._start_service()
|
||||
|
||||
mock_spawn.reset_mock()
|
||||
|
||||
mock_spawn.side_effect = self._fake_spawn
|
||||
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.service.heartbeat, self.context,
|
||||
node.uuid, 'http://callback',
|
||||
agent_token=None, agent_version='6.1.5')
|
||||
self.assertFalse(mock_heartbeat.called)
|
||||
|
||||
|
||||
@mgr_utils.mock_record_keepalive
|
||||
class DestroyVolumeConnectorTestCase(mgr_utils.ServiceSetUpMixin,
|
||||
|
@ -486,7 +486,16 @@ class RPCAPITestCase(db_base.DbTestCase):
|
||||
node_id='fake-node',
|
||||
callback_url='http://ramdisk.url:port',
|
||||
agent_version=None,
|
||||
version='1.42')
|
||||
version='1.49')
|
||||
|
||||
def test_heartbeat_agent_token(self):
|
||||
self._test_rpcapi('heartbeat',
|
||||
'call',
|
||||
node_id='fake-node',
|
||||
callback_url='http://ramdisk.url:port',
|
||||
agent_version=None,
|
||||
agent_token='xyz1',
|
||||
version='1.49')
|
||||
|
||||
def test_destroy_volume_connector(self):
|
||||
fake_volume_connector = db_utils.get_test_volume_connector()
|
||||
@ -555,6 +564,12 @@ class RPCAPITestCase(db_base.DbTestCase):
|
||||
version='1.43',
|
||||
node_id=self.fake_node['uuid'])
|
||||
|
||||
def test_get_node_with_token(self):
|
||||
self._test_rpcapi('get_node_with_token',
|
||||
'call',
|
||||
version='1.49',
|
||||
node_id=self.fake_node['uuid'])
|
||||
|
||||
def _test_can_send_rescue(self, can_send):
|
||||
rpcapi = conductor_rpcapi.ConductorAPI(topic='fake-topic')
|
||||
with mock.patch.object(rpcapi.client,
|
||||
|
@ -2018,3 +2018,37 @@ class GetNodeNextStepsTestCase(db_base.DbTestCase):
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
step_index = conductor_utils.get_node_next_clean_steps(task)
|
||||
self.assertEqual(0, step_index)
|
||||
|
||||
|
||||
class AgentTokenUtilsTestCase(tests_base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(AgentTokenUtilsTestCase, self).setUp()
|
||||
self.node = obj_utils.get_test_node(self.context,
|
||||
driver='fake-hardware')
|
||||
|
||||
def test_add_secret_token(self):
|
||||
self.assertNotIn('agent_secret_token', self.node.driver_internal_info)
|
||||
conductor_utils.add_secret_token(self.node)
|
||||
self.assertEqual(
|
||||
128, len(self.node.driver_internal_info['agent_secret_token']))
|
||||
|
||||
def test_del_secret_token(self):
|
||||
conductor_utils.add_secret_token(self.node)
|
||||
self.assertIn('agent_secret_token', self.node.driver_internal_info)
|
||||
conductor_utils.del_secret_token(self.node)
|
||||
self.assertNotIn('agent_secret_token', self.node.driver_internal_info)
|
||||
|
||||
def test_is_agent_token_present(self):
|
||||
# This should always be False as the token has not been added yet.
|
||||
self.assertFalse(conductor_utils.is_agent_token_present(self.node))
|
||||
conductor_utils.add_secret_token(self.node)
|
||||
self.assertTrue(conductor_utils.is_agent_token_present(self.node))
|
||||
|
||||
def test_is_agent_token_supported(self):
|
||||
self.assertTrue(
|
||||
conductor_utils.is_agent_token_supported('6.1.1.dev39'))
|
||||
self.assertTrue(
|
||||
conductor_utils.is_agent_token_supported('6.2.1'))
|
||||
self.assertFalse(
|
||||
conductor_utils.is_agent_token_supported('6.0.0'))
|
||||
|
@ -1559,11 +1559,35 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
|
||||
def test__cleaning_reboot(self, mock_reboot, mock_prepare, mock_build_opt):
|
||||
with task_manager.acquire(self.context, self.node['uuid'],
|
||||
shared=False) as task:
|
||||
i_info = task.node.driver_internal_info
|
||||
i_info['agent_secret_token'] = 'magicvalue01'
|
||||
task.node.driver_internal_info = i_info
|
||||
agent_base._cleaning_reboot(task)
|
||||
self.assertTrue(mock_build_opt.called)
|
||||
self.assertTrue(mock_prepare.called)
|
||||
mock_reboot.assert_called_once_with(task, states.REBOOT)
|
||||
self.assertTrue(task.node.driver_internal_info['cleaning_reboot'])
|
||||
self.assertNotIn('agent_secret_token',
|
||||
task.node.driver_internal_info)
|
||||
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', spec_set=True,
|
||||
autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
def test__cleaning_reboot_pregenerated_token(
|
||||
self, mock_reboot, mock_prepare, mock_build_opt):
|
||||
with task_manager.acquire(self.context, self.node['uuid'],
|
||||
shared=False) as task:
|
||||
i_info = task.node.driver_internal_info
|
||||
i_info['agent_secret_token'] = 'magicvalue01'
|
||||
i_info['agent_secret_token_pregenerated'] = True
|
||||
task.node.driver_internal_info = i_info
|
||||
agent_base._cleaning_reboot(task)
|
||||
self.assertTrue(mock_build_opt.called)
|
||||
self.assertTrue(mock_prepare.called)
|
||||
mock_reboot.assert_called_once_with(task, states.REBOOT)
|
||||
self.assertIn('agent_secret_token',
|
||||
task.node.driver_internal_info)
|
||||
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', spec_set=True,
|
||||
|
@ -2939,10 +2939,14 @@ class AsyncStepTestCase(db_base.DbTestCase):
|
||||
def test_set_async_step_flags_deploying_set_all(self):
|
||||
self.node.deploy_step = {'step': 'create_configuration',
|
||||
'interface': 'raid'}
|
||||
self.node.driver_internal_info = {}
|
||||
self.node.driver_internal_info = {
|
||||
'agent_secret_token': 'test',
|
||||
'agent_secret_token_pregenerated': True}
|
||||
expected = {'deployment_reboot': True,
|
||||
'deployment_polling': True,
|
||||
'skip_current_deploy_step': True}
|
||||
'skip_current_deploy_step': True,
|
||||
'agent_secret_token': 'test',
|
||||
'agent_secret_token_pregenerated': True}
|
||||
self.node.save()
|
||||
utils.set_async_step_flags(self.node, reboot=True,
|
||||
skip_current_step=True,
|
||||
@ -2957,3 +2961,16 @@ class AsyncStepTestCase(db_base.DbTestCase):
|
||||
utils.set_async_step_flags(self.node, reboot=True)
|
||||
self.assertEqual({'deployment_reboot': True},
|
||||
self.node.driver_internal_info)
|
||||
|
||||
def test_set_async_step_flags_clears_non_pregenerated_token(self):
|
||||
self.node.clean_step = {'step': 'create_configuration',
|
||||
'interface': 'raid'}
|
||||
self.node.driver_internal_info = {'agent_secret_token': 'test'}
|
||||
expected = {'cleaning_reboot': True,
|
||||
'cleaning_polling': True,
|
||||
'skip_current_clean_step': True}
|
||||
self.node.save()
|
||||
utils.set_async_step_flags(self.node, reboot=True,
|
||||
skip_current_step=True,
|
||||
polling=True)
|
||||
self.assertEqual(expected, self.node.driver_internal_info)
|
||||
|
@ -40,18 +40,24 @@ class TestNodeObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
|
||||
def test_as_dict_insecure(self):
|
||||
self.node.driver_info['ipmi_password'] = 'fake'
|
||||
self.node.instance_info['configdrive'] = 'data'
|
||||
self.node.driver_internal_info['agent_secret_token'] = 'abc'
|
||||
d = self.node.as_dict()
|
||||
self.assertEqual('fake', d['driver_info']['ipmi_password'])
|
||||
self.assertEqual('data', d['instance_info']['configdrive'])
|
||||
self.assertEqual('abc',
|
||||
d['driver_internal_info']['agent_secret_token'])
|
||||
# Ensure the node can be serialised.
|
||||
jsonutils.dumps(d)
|
||||
|
||||
def test_as_dict_secure(self):
|
||||
self.node.driver_info['ipmi_password'] = 'fake'
|
||||
self.node.instance_info['configdrive'] = 'data'
|
||||
self.node.driver_internal_info['agent_secret_token'] = 'abc'
|
||||
d = self.node.as_dict(secure=True)
|
||||
self.assertEqual('******', d['driver_info']['ipmi_password'])
|
||||
self.assertEqual('******', d['instance_info']['configdrive'])
|
||||
self.assertEqual('******',
|
||||
d['driver_internal_info']['agent_secret_token'])
|
||||
# Ensure the node can be serialised.
|
||||
jsonutils.dumps(d)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user