ironic/ironic/api/controllers/v1/ramdisk.py

252 lines
10 KiB
Python

# Copyright 2016 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from http import client as http_client
from oslo_config import cfg
from oslo_log import log
from pecan import rest
from ironic import api
from ironic.api.controllers.v1 import node as node_ctl
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api import method
from ironic.common import args
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import states
from ironic.common import utils
from ironic import objects
CONF = cfg.CONF
LOG = log.getLogger(__name__)
_LOOKUP_RETURN_FIELDS = ['uuid', 'properties', 'instance_info',
'driver_internal_info']
AGENT_VALID_STATES = ['start', 'end', 'error']
def config(token):
return {
'metrics': {
'backend': CONF.metrics.agent_backend,
'prepend_host': CONF.metrics.agent_prepend_host,
'prepend_uuid': CONF.metrics.agent_prepend_uuid,
'prepend_host_reverse': CONF.metrics.agent_prepend_host_reverse,
'global_prefix': CONF.metrics.agent_global_prefix
},
'metrics_statsd': {
'statsd_host': CONF.metrics_statsd.agent_statsd_host,
'statsd_port': CONF.metrics_statsd.agent_statsd_port
},
'heartbeat_timeout': CONF.api.ramdisk_heartbeat_timeout,
'agent_token': token,
# Since this is for the Victoria release, we send this as an
# explicit True statement for newer agents to lock the setting
# and behavior into place.
'agent_token_required': True,
}
def convert_with_links(node):
token = node.driver_internal_info.get('agent_secret_token')
node = node_ctl.node_convert_with_links(node, _LOOKUP_RETURN_FIELDS)
return {'node': node, 'config': config(token)}
class LookupController(rest.RestController):
"""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
@method.expose()
@args.validate(addresses=args.string_list, node_uuid=args.uuid)
def get_all(self, addresses=None, node_uuid=None):
"""Look up a node by its MAC addresses and optionally UUID.
If the "restrict_lookup" option is set to True (the default), limit
the search to nodes in certain transient states (e.g. deploy wait).
:param addresses: list of MAC addresses for a node.
:param node_uuid: UUID of a node.
:raises: NotFound if requested API version does not allow this
endpoint.
:raises: NotFound if suitable node was not found or node's provision
state is not allowed for the lookup.
:raises: IncompleteLookup if neither node UUID nor any valid MAC
address was provided.
"""
if not api_utils.allow_ramdisk_endpoints():
raise exception.NotFound()
api_utils.check_policy('baremetal:driver:ipa_lookup')
# Validate the list of MAC addresses
if addresses is None:
addresses = []
valid_addresses = []
invalid_addresses = []
for addr in addresses:
try:
mac = utils.validate_and_normalize_mac(addr)
valid_addresses.append(mac)
except exception.InvalidMAC:
invalid_addresses.append(addr)
if invalid_addresses:
node_log = ('' if not node_uuid
else '(Node UUID: %s)' % node_uuid)
LOG.warning('The following MAC addresses "%(addrs)s" are '
'invalid and will be ignored by the lookup '
'request %(node)s',
{'addrs': ', '.join(invalid_addresses),
'node': node_log})
if not valid_addresses and not node_uuid:
raise exception.IncompleteLookup()
try:
if node_uuid:
node = objects.Node.get_by_uuid(
api.request.context, node_uuid)
else:
node = objects.Node.get_by_port_addresses(
api.request.context, valid_addresses)
except exception.NotFound:
# NOTE(dtantsur): we are reraising the same exception to make sure
# we don't disclose the difference between nodes that are not found
# at all and nodes in a wrong state by different error messages.
raise exception.NotFound()
if (CONF.api.restrict_lookup
and node.provision_state not in self.lookup_allowed_states):
raise exception.NotFound()
if api_utils.allow_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 convert_with_links(found_node)
class HeartbeatController(rest.RestController):
"""Controller handling heartbeats from deploy ramdisk."""
@method.expose(status_code=http_client.ACCEPTED)
@args.validate(node_ident=args.uuid_or_name, callback_url=args.string,
agent_version=args.string, agent_token=args.string,
agent_verify_ca=args.string, agent_status=args.string,
agent_status_message=args.string)
def post(self, node_ident, callback_url, agent_version=None,
agent_token=None, agent_verify_ca=None, agent_status=None,
agent_status_message=None):
"""Process a heartbeat from the deploy ramdisk.
:param node_ident: the UUID or logical name of a node.
:param callback_url: the URL to reach back to the ramdisk.
:param agent_version: The version of the agent that is heartbeating.
``None`` indicates that the agent that is heartbeating is a version
before sending agent_version was introduced so agent v3.0.0 (the
last release before sending agent_version was introduced) will be
assumed.
:param agent_token: randomly generated validation token.
:param agent_verify_ca: TLS certificate to use to connect to the agent.
:param agent_status: Current status of the heartbeating agent. Used by
anaconda ramdisk to send status back to Ironic. The valid states
are 'start', 'end', 'error'
:param agent_status_message: Optional status message describing current
agent_status
:raises: NodeNotFound if node with provided UUID or name was not found.
:raises: InvalidUuidOrName if node_ident is not valid name or UUID.
:raises: NoValidHost if RPC topic for node could not be retrieved.
:raises: NotFound if requested API version does not allow this
endpoint.
"""
if not api_utils.allow_ramdisk_endpoints():
raise exception.NotFound()
if agent_version and not api_utils.allow_agent_version_in_heartbeat():
raise exception.InvalidParameterValue(
_('Field "agent_version" not recognised'))
if ((agent_status or agent_status_message)
and not api_utils.allow_status_in_heartbeat()):
raise exception.InvalidParameterValue(
_('Fields "agent_status" and "agent_status_message" '
'not recognised.')
)
api_utils.check_policy('baremetal:node:ipa_heartbeat')
if (agent_verify_ca is not None
and not api_utils.allow_verify_ca_in_heartbeat()):
raise exception.InvalidParameterValue(
_('Field "agent_verify_ca" not recognised in this version'))
rpc_node = api_utils.get_rpc_node_with_suffix(node_ident)
dii = rpc_node['driver_internal_info']
agent_url = dii.get('agent_url')
# If we have an agent_url on file, and we get something different
# we should fail because this is unexpected behavior of the agent.
if agent_url is not None and agent_url != callback_url:
LOG.error('Received heartbeat for node %(node)s with '
'callback URL %(url)s. This is not expected, '
'and the heartbeat will not be processed.',
{'node': rpc_node.uuid, 'url': callback_url})
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.
if 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.'))
if agent_status is not None and agent_status not in AGENT_VALID_STATES:
valid_states = ','.join(AGENT_VALID_STATES)
LOG.error('Agent heartbeat received for node %(node)s '
'has an invalid agent status: %(agent_status)s. '
'Valid states are %(valid_states)s ',
{'node': node_ident, 'agent_status': agent_status,
'valid_states': valid_states})
msg = (_('Agent status is invalid. Valid states are %s.') %
valid_states)
raise exception.InvalidParameterValue(msg)
try:
topic = api.request.rpcapi.get_topic_for(rpc_node)
except exception.NoValidHost as e:
e.code = http_client.BAD_REQUEST
raise
api.request.rpcapi.heartbeat(
api.request.context, rpc_node.uuid, callback_url,
agent_version, agent_token, agent_verify_ca, agent_status,
agent_status_message, topic=topic)