Promote agent vendor passthru to core API
Introduces new /v1/lookup and /v1/heartbeat/<UUID> endpoints (and associated controllers). This change does not deprecate the old passthru endpoints, it should be done after IPA switches to using the new ones. Change-Id: I9080c07b03103cd7a323e2fc01be821733b07eea Partial-Bug: #1570841
This commit is contained in:
parent
bc106b56bb
commit
8bdd538c0c
@ -682,7 +682,7 @@ function configure_ironic_conductor {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if is_deployed_by_agent; then
|
if is_deployed_by_agent; then
|
||||||
iniset $IRONIC_CONF_FILE agent heartbeat_timeout 30
|
iniset $IRONIC_CONF_FILE api ramdisk_heartbeat_timeout 30
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# FIXME: this really needs to be tested in the gate. For now, any
|
# FIXME: this really needs to be tested in the gate. For now, any
|
||||||
|
@ -32,6 +32,10 @@ always requests the newest supported API version.
|
|||||||
API Versions History
|
API Versions History
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
**1.22**
|
||||||
|
|
||||||
|
Added endpoints for deployment ramdisks.
|
||||||
|
|
||||||
**1.21**
|
**1.21**
|
||||||
|
|
||||||
Add node ``resource_class`` field.
|
Add node ``resource_class`` field.
|
||||||
|
@ -400,10 +400,6 @@
|
|||||||
# be set to True. Defaults to True. (boolean value)
|
# be set to True. Defaults to True. (boolean value)
|
||||||
#stream_raw_images = true
|
#stream_raw_images = true
|
||||||
|
|
||||||
# Maximum interval (in seconds) for agent heartbeats. (integer
|
|
||||||
# value)
|
|
||||||
#heartbeat_timeout = 300
|
|
||||||
|
|
||||||
# Number of times to retry getting power state to check if
|
# Number of times to retry getting power state to check if
|
||||||
# bare metal node has been powered off after a soft power off.
|
# bare metal node has been powered off after a soft power off.
|
||||||
# (integer value)
|
# (integer value)
|
||||||
@ -486,6 +482,15 @@
|
|||||||
# 'public_endpoint' option. (boolean value)
|
# 'public_endpoint' option. (boolean value)
|
||||||
#enable_ssl_api = false
|
#enable_ssl_api = false
|
||||||
|
|
||||||
|
# Whether to restrict the lookup API to only nodes in certain
|
||||||
|
# states. (boolean value)
|
||||||
|
#restrict_lookup = true
|
||||||
|
|
||||||
|
# Maximum interval (in seconds) for agent heartbeats. (integer
|
||||||
|
# value)
|
||||||
|
# Deprecated group/name - [agent]/heartbeat_timeout
|
||||||
|
#ramdisk_heartbeat_timeout = 300
|
||||||
|
|
||||||
|
|
||||||
[audit]
|
[audit]
|
||||||
|
|
||||||
|
@ -30,6 +30,9 @@ app = {
|
|||||||
'/',
|
'/',
|
||||||
'/v1',
|
'/v1',
|
||||||
# IPA ramdisk methods
|
# IPA ramdisk methods
|
||||||
|
'/v1/lookup',
|
||||||
|
'/v1/heartbeat/[a-z0-9\-]+',
|
||||||
|
# Old IPA ramdisk methods - will be removed in the Ocata release
|
||||||
'/v1/drivers/[a-z0-9_]*/vendor_passthru/lookup',
|
'/v1/drivers/[a-z0-9_]*/vendor_passthru/lookup',
|
||||||
'/v1/nodes/[a-z0-9\-]+/vendor_passthru/heartbeat',
|
'/v1/nodes/[a-z0-9\-]+/vendor_passthru/heartbeat',
|
||||||
],
|
],
|
||||||
|
@ -29,6 +29,8 @@ from ironic.api.controllers.v1 import chassis
|
|||||||
from ironic.api.controllers.v1 import driver
|
from ironic.api.controllers.v1 import driver
|
||||||
from ironic.api.controllers.v1 import node
|
from ironic.api.controllers.v1 import node
|
||||||
from ironic.api.controllers.v1 import port
|
from ironic.api.controllers.v1 import port
|
||||||
|
from ironic.api.controllers.v1 import ramdisk
|
||||||
|
from ironic.api.controllers.v1 import utils
|
||||||
from ironic.api.controllers.v1 import versions
|
from ironic.api.controllers.v1 import versions
|
||||||
from ironic.api import expose
|
from ironic.api import expose
|
||||||
from ironic.common.i18n import _
|
from ironic.common.i18n import _
|
||||||
@ -78,6 +80,12 @@ class V1(base.APIBase):
|
|||||||
drivers = [link.Link]
|
drivers = [link.Link]
|
||||||
"""Links to the drivers resource"""
|
"""Links to the drivers resource"""
|
||||||
|
|
||||||
|
lookup = [link.Link]
|
||||||
|
"""Links to the lookup resource"""
|
||||||
|
|
||||||
|
heartbeat = [link.Link]
|
||||||
|
"""Links to the heartbeat resource"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def convert():
|
def convert():
|
||||||
v1 = V1()
|
v1 = V1()
|
||||||
@ -120,6 +128,22 @@ class V1(base.APIBase):
|
|||||||
'drivers', '',
|
'drivers', '',
|
||||||
bookmark=True)
|
bookmark=True)
|
||||||
]
|
]
|
||||||
|
if utils.allow_ramdisk_endpoints():
|
||||||
|
v1.lookup = [link.Link.make_link('self', pecan.request.public_url,
|
||||||
|
'lookup', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.public_url,
|
||||||
|
'lookup', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
|
v1.heartbeat = [link.Link.make_link('self',
|
||||||
|
pecan.request.public_url,
|
||||||
|
'heartbeat', ''),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.public_url,
|
||||||
|
'heartbeat', '',
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
return v1
|
return v1
|
||||||
|
|
||||||
|
|
||||||
@ -130,6 +154,8 @@ class Controller(rest.RestController):
|
|||||||
ports = port.PortsController()
|
ports = port.PortsController()
|
||||||
chassis = chassis.ChassisController()
|
chassis = chassis.ChassisController()
|
||||||
drivers = driver.DriversController()
|
drivers = driver.DriversController()
|
||||||
|
lookup = ramdisk.LookupController()
|
||||||
|
heartbeat = ramdisk.HeartbeatController()
|
||||||
|
|
||||||
@expose.expose(V1)
|
@expose.expose(V1)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
150
ironic/api/controllers/v1/ramdisk.py
Normal file
150
ironic/api/controllers/v1/ramdisk.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# 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 oslo_config import cfg
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
from six.moves import http_client
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from ironic.api.controllers import base
|
||||||
|
from ironic.api.controllers.v1 import node as node_ctl
|
||||||
|
from ironic.api.controllers.v1 import types
|
||||||
|
from ironic.api.controllers.v1 import utils as api_utils
|
||||||
|
from ironic.api import expose
|
||||||
|
from ironic.common import exception
|
||||||
|
from ironic.common import policy
|
||||||
|
from ironic.common import states
|
||||||
|
from ironic import objects
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
_LOOKUP_RETURN_FIELDS = ('uuid', 'properties', 'instance_info',
|
||||||
|
'driver_internal_info')
|
||||||
|
_LOOKUP_ALLOWED_STATES = {states.DEPLOYING, states.DEPLOYWAIT,
|
||||||
|
states.CLEANING, states.CLEANWAIT,
|
||||||
|
states.INSPECTING}
|
||||||
|
|
||||||
|
|
||||||
|
def config():
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LookupResult(base.APIBase):
|
||||||
|
"""API representation of the node lookup result."""
|
||||||
|
|
||||||
|
node = node_ctl.Node
|
||||||
|
"""The short node representation."""
|
||||||
|
|
||||||
|
config = {wtypes.text: types.jsontype}
|
||||||
|
"""The configuration to pass to the ramdisk."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
return cls(node=node_ctl.Node.sample(),
|
||||||
|
config={'heartbeat_timeout': 600})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, node):
|
||||||
|
node = node_ctl.Node.convert_with_links(node, _LOOKUP_RETURN_FIELDS)
|
||||||
|
return cls(node=node, config=config())
|
||||||
|
|
||||||
|
|
||||||
|
class LookupController(rest.RestController):
|
||||||
|
"""Controller handling node lookup for a deploy ramdisk."""
|
||||||
|
|
||||||
|
@expose.expose(LookupResult, types.list_of_macaddress, types.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.
|
||||||
|
"""
|
||||||
|
if not api_utils.allow_ramdisk_endpoints():
|
||||||
|
raise exception.NotFound()
|
||||||
|
|
||||||
|
cdict = pecan.request.context.to_dict()
|
||||||
|
policy.authorize('baremetal:driver:ipa_lookup', cdict, cdict)
|
||||||
|
|
||||||
|
if not addresses and not node_uuid:
|
||||||
|
raise exception.IncompleteLookup()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if node_uuid:
|
||||||
|
node = objects.Node.get_by_uuid(
|
||||||
|
pecan.request.context, node_uuid)
|
||||||
|
else:
|
||||||
|
node = objects.Node.get_by_port_addresses(
|
||||||
|
pecan.request.context, 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 _LOOKUP_ALLOWED_STATES):
|
||||||
|
raise exception.NotFound()
|
||||||
|
|
||||||
|
return LookupResult.convert_with_links(node)
|
||||||
|
|
||||||
|
|
||||||
|
class HeartbeatController(rest.RestController):
|
||||||
|
"""Controller handling heartbeats from deploy ramdisk."""
|
||||||
|
|
||||||
|
@expose.expose(None, types.uuid_or_name, wtypes.text,
|
||||||
|
status_code=http_client.ACCEPTED)
|
||||||
|
def post(self, node_ident, callback_url):
|
||||||
|
"""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.
|
||||||
|
"""
|
||||||
|
if not api_utils.allow_ramdisk_endpoints():
|
||||||
|
raise exception.NotFound()
|
||||||
|
|
||||||
|
cdict = pecan.request.context.to_dict()
|
||||||
|
policy.authorize('baremetal:node:ipa_heartbeat', cdict, cdict)
|
||||||
|
|
||||||
|
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||||
|
|
||||||
|
try:
|
||||||
|
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||||
|
except exception.NoValidHost as e:
|
||||||
|
e.code = http_client.BAD_REQUEST
|
||||||
|
raise
|
||||||
|
|
||||||
|
pecan.request.rpcapi.heartbeat(pecan.request.context,
|
||||||
|
rpc_node.uuid, callback_url,
|
||||||
|
topic=topic)
|
@ -176,6 +176,26 @@ class ListType(wtypes.UserType):
|
|||||||
return ListType.validate(value)
|
return ListType.validate(value)
|
||||||
|
|
||||||
|
|
||||||
|
class ListOfMacAddressesType(ListType):
|
||||||
|
"""List of MAC addresses."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value):
|
||||||
|
"""Validate and convert the input to a ListOfMacAddressesType.
|
||||||
|
|
||||||
|
:param value: A comma separated string of MAC addresses.
|
||||||
|
:returns: A list of unique MACs, whose order is not guaranteed.
|
||||||
|
"""
|
||||||
|
items = ListType.validate(value)
|
||||||
|
return [MacAddressType.validate(item) for item in items]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def frombasetype(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return ListOfMacAddressesType.validate(value)
|
||||||
|
|
||||||
|
|
||||||
macaddress = MacAddressType()
|
macaddress = MacAddressType()
|
||||||
uuid_or_name = UuidOrNameType()
|
uuid_or_name = UuidOrNameType()
|
||||||
name = NameType()
|
name = NameType()
|
||||||
@ -184,6 +204,7 @@ boolean = BooleanType()
|
|||||||
listtype = ListType()
|
listtype = ListType()
|
||||||
# Can't call it 'json' because that's the name of the stdlib module
|
# Can't call it 'json' because that's the name of the stdlib module
|
||||||
jsontype = JsonType()
|
jsontype = JsonType()
|
||||||
|
list_of_macaddress = ListOfMacAddressesType()
|
||||||
|
|
||||||
|
|
||||||
class JsonPatchType(wtypes.Base):
|
class JsonPatchType(wtypes.Base):
|
||||||
|
@ -383,6 +383,14 @@ def allow_resource_class():
|
|||||||
versions.MINOR_21_RESOURCE_CLASS)
|
versions.MINOR_21_RESOURCE_CLASS)
|
||||||
|
|
||||||
|
|
||||||
|
def allow_ramdisk_endpoints():
|
||||||
|
"""Check if heartbeat and lookup endpoints are allowed.
|
||||||
|
|
||||||
|
Version 1.22 of the API introduced them.
|
||||||
|
"""
|
||||||
|
return pecan.request.version.minor >= versions.MINOR_22_LOOKUP_HEARTBEAT
|
||||||
|
|
||||||
|
|
||||||
def get_controller_reserved_names(cls):
|
def get_controller_reserved_names(cls):
|
||||||
"""Get reserved names for a given controller.
|
"""Get reserved names for a given controller.
|
||||||
|
|
||||||
|
@ -51,6 +51,7 @@ BASE_VERSION = 1
|
|||||||
# v1.19: Add port.local_link_connection and port.pxe_enabled.
|
# v1.19: Add port.local_link_connection and port.pxe_enabled.
|
||||||
# v1.20: Add node.network_interface
|
# v1.20: Add node.network_interface
|
||||||
# v1.21: Add node.resource_class
|
# v1.21: Add node.resource_class
|
||||||
|
# v1.22: Ramdisk lookup and heartbeat endpoints.
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -74,11 +75,12 @@ MINOR_18_PORT_INTERNAL_INFO = 18
|
|||||||
MINOR_19_PORT_ADVANCED_NET_FIELDS = 19
|
MINOR_19_PORT_ADVANCED_NET_FIELDS = 19
|
||||||
MINOR_20_NETWORK_INTERFACE = 20
|
MINOR_20_NETWORK_INTERFACE = 20
|
||||||
MINOR_21_RESOURCE_CLASS = 21
|
MINOR_21_RESOURCE_CLASS = 21
|
||||||
|
MINOR_22_LOOKUP_HEARTBEAT = 22
|
||||||
|
|
||||||
# When adding another version, update MINOR_MAX_VERSION and also update
|
# When adding another version, update MINOR_MAX_VERSION and also update
|
||||||
# doc/source/webapi/v1.rst with a detailed explanation of what the version has
|
# doc/source/webapi/v1.rst with a detailed explanation of what the version has
|
||||||
# changed.
|
# changed.
|
||||||
MINOR_MAX_VERSION = MINOR_21_RESOURCE_CLASS
|
MINOR_MAX_VERSION = MINOR_22_LOOKUP_HEARTBEAT
|
||||||
|
|
||||||
# String representations of the minor and maximum versions
|
# String representations of the minor and maximum versions
|
||||||
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||||
|
@ -606,3 +606,8 @@ class NodeTagNotFound(IronicException):
|
|||||||
|
|
||||||
class NetworkError(IronicException):
|
class NetworkError(IronicException):
|
||||||
_msg_fmt = _("Network operation failure.")
|
_msg_fmt = _("Network operation failure.")
|
||||||
|
|
||||||
|
|
||||||
|
class IncompleteLookup(Invalid):
|
||||||
|
_msg_fmt = _("At least one of 'addresses' and 'node_uuid' parameters "
|
||||||
|
"is required")
|
||||||
|
@ -44,9 +44,6 @@ opts = [
|
|||||||
'to the disk. Unless the disk where the image will be '
|
'to the disk. Unless the disk where the image will be '
|
||||||
'copied to is really slow, this option should be set '
|
'copied to is really slow, this option should be set '
|
||||||
'to True. Defaults to True.')),
|
'to True. Defaults to True.')),
|
||||||
cfg.IntOpt('heartbeat_timeout',
|
|
||||||
default=300,
|
|
||||||
help=_('Maximum interval (in seconds) for agent heartbeats.')),
|
|
||||||
cfg.IntOpt('post_deploy_get_power_state_retries',
|
cfg.IntOpt('post_deploy_get_power_state_retries',
|
||||||
default=6,
|
default=6,
|
||||||
help=_('Number of times to retry getting power state to check '
|
help=_('Number of times to retry getting power state to check '
|
||||||
|
@ -49,6 +49,14 @@ opts = [
|
|||||||
"the service, this option should be False; note, you "
|
"the service, this option should be False; note, you "
|
||||||
"will want to change public API endpoint to represent "
|
"will want to change public API endpoint to represent "
|
||||||
"SSL termination URL with 'public_endpoint' option.")),
|
"SSL termination URL with 'public_endpoint' option.")),
|
||||||
|
cfg.BoolOpt('restrict_lookup',
|
||||||
|
default=True,
|
||||||
|
help=_('Whether to restrict the lookup API to only nodes '
|
||||||
|
'in certain states.')),
|
||||||
|
cfg.IntOpt('ramdisk_heartbeat_timeout',
|
||||||
|
default=300,
|
||||||
|
deprecated_group='agent', deprecated_name='heartbeat_timeout',
|
||||||
|
help=_('Maximum interval (in seconds) for agent heartbeats.')),
|
||||||
]
|
]
|
||||||
|
|
||||||
opt_group = cfg.OptGroup(name='api',
|
opt_group = cfg.OptGroup(name='api',
|
||||||
|
@ -26,6 +26,7 @@ from oslo_utils import strutils
|
|||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
import retrying
|
import retrying
|
||||||
|
|
||||||
|
from ironic.api.controllers.v1 import ramdisk
|
||||||
from ironic.common import boot_devices
|
from ironic.common import boot_devices
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common.i18n import _
|
from ironic.common.i18n import _
|
||||||
@ -789,23 +790,9 @@ class BaseAgentVendor(AgentDeployMixin, base.VendorInterface):
|
|||||||
# config namespace. Instead of a separate deprecation,
|
# config namespace. Instead of a separate deprecation,
|
||||||
# this will die when the vendor_passthru version of
|
# this will die when the vendor_passthru version of
|
||||||
# lookup goes away.
|
# lookup goes away.
|
||||||
'heartbeat_timeout': CONF.agent.heartbeat_timeout,
|
'heartbeat_timeout': CONF.api.ramdisk_heartbeat_timeout,
|
||||||
'node': ndict,
|
'node': ndict,
|
||||||
'config': {
|
'config': ramdisk.config(),
|
||||||
'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.agent.heartbeat_timeout
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_interfaces(self, inventory):
|
def _get_interfaces(self, inventory):
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from ironic.api.controllers import base as api_base
|
||||||
from ironic.api.controllers.v1 import versions
|
from ironic.api.controllers.v1 import versions
|
||||||
from ironic.tests.unit.api import base
|
from ironic.tests.unit.api import base
|
||||||
|
|
||||||
@ -51,3 +52,20 @@ class TestV1Root(base.BaseApiTest):
|
|||||||
|
|
||||||
self.assertIn({'type': 'application/vnd.openstack.ironic.v1+json',
|
self.assertIn({'type': 'application/vnd.openstack.ironic.v1+json',
|
||||||
'base': 'application/json'}, data['media_types'])
|
'base': 'application/json'}, data['media_types'])
|
||||||
|
|
||||||
|
def test_get_v1_root_version_1_22(self):
|
||||||
|
headers = {api_base.Version.string: '1.22'}
|
||||||
|
data = self.get_json('/', headers=headers)
|
||||||
|
self.assertEqual('v1', data['id'])
|
||||||
|
# Check fields are not empty
|
||||||
|
for f in data:
|
||||||
|
self.assertNotIn(f, ['', []])
|
||||||
|
# Check if all known resources are present and there are no extra ones.
|
||||||
|
not_resources = ('id', 'links', 'media_types')
|
||||||
|
actual_resources = tuple(set(data.keys()) - set(not_resources))
|
||||||
|
expected_resources = ('chassis', 'drivers', 'heartbeat',
|
||||||
|
'lookup', 'nodes', 'ports')
|
||||||
|
self.assertEqual(sorted(expected_resources), sorted(actual_resources))
|
||||||
|
|
||||||
|
self.assertIn({'type': 'application/vnd.openstack.ironic.v1+json',
|
||||||
|
'base': 'application/json'}, data['media_types'])
|
||||||
|
172
ironic/tests/unit/api/v1/test_ramdisk.py
Normal file
172
ironic/tests/unit/api/v1/test_ramdisk.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
Tests for the API /lookup/ methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
from six.moves import http_client
|
||||||
|
|
||||||
|
from ironic.api.controllers import base as api_base
|
||||||
|
from ironic.api.controllers import v1 as api_v1
|
||||||
|
from ironic.api.controllers.v1 import ramdisk
|
||||||
|
from ironic.conductor import rpcapi
|
||||||
|
from ironic.tests.unit.api import base as test_api_base
|
||||||
|
from ironic.tests.unit.objects import utils as obj_utils
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class TestLookup(test_api_base.BaseApiTest):
|
||||||
|
addresses = ['11:22:33:44:55:66', '66:55:44:33:22:11']
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestLookup, self).setUp()
|
||||||
|
self.node = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
provision_state='deploying')
|
||||||
|
self.node2 = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
provision_state='available')
|
||||||
|
CONF.set_override('agent_backend', 'statsd', 'metrics')
|
||||||
|
|
||||||
|
def _check_config(self, data):
|
||||||
|
expected_metrics = {
|
||||||
|
'metrics': {
|
||||||
|
'backend': 'statsd',
|
||||||
|
'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
|
||||||
|
}
|
||||||
|
self.assertEqual(expected_metrics, data['config'])
|
||||||
|
|
||||||
|
def test_nothing_provided(self):
|
||||||
|
response = self.get_json(
|
||||||
|
'/lookup',
|
||||||
|
headers={api_base.Version.string: str(api_v1.MAX_VER)},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||||
|
|
||||||
|
def test_not_found(self):
|
||||||
|
response = self.get_json(
|
||||||
|
'/lookup?addresses=%s' % ','.join(self.addresses),
|
||||||
|
headers={api_base.Version.string: str(api_v1.MAX_VER)},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||||
|
|
||||||
|
def test_old_api_version(self):
|
||||||
|
obj_utils.create_test_port(self.context,
|
||||||
|
node_id=self.node.id,
|
||||||
|
address=self.addresses[1])
|
||||||
|
|
||||||
|
response = self.get_json(
|
||||||
|
'/lookup?addresses=%s' % ','.join(self.addresses),
|
||||||
|
headers={api_base.Version.string: str(api_v1.MIN_VER)},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||||
|
|
||||||
|
def test_found_by_addresses(self):
|
||||||
|
obj_utils.create_test_port(self.context,
|
||||||
|
node_id=self.node.id,
|
||||||
|
address=self.addresses[1])
|
||||||
|
|
||||||
|
data = self.get_json(
|
||||||
|
'/lookup?addresses=%s' % ','.join(self.addresses),
|
||||||
|
headers={api_base.Version.string: str(api_v1.MAX_VER)})
|
||||||
|
self.assertEqual(self.node.uuid, data['node']['uuid'])
|
||||||
|
self.assertEqual(set(ramdisk._LOOKUP_RETURN_FIELDS) | {'links'},
|
||||||
|
set(data['node']))
|
||||||
|
self._check_config(data)
|
||||||
|
|
||||||
|
def test_found_by_uuid(self):
|
||||||
|
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_VER)})
|
||||||
|
self.assertEqual(self.node.uuid, data['node']['uuid'])
|
||||||
|
self.assertEqual(set(ramdisk._LOOKUP_RETURN_FIELDS) | {'links'},
|
||||||
|
set(data['node']))
|
||||||
|
self._check_config(data)
|
||||||
|
|
||||||
|
def test_found_by_only_uuid(self):
|
||||||
|
data = self.get_json(
|
||||||
|
'/lookup?node_uuid=%s' % self.node.uuid,
|
||||||
|
headers={api_base.Version.string: str(api_v1.MAX_VER)})
|
||||||
|
self.assertEqual(self.node.uuid, data['node']['uuid'])
|
||||||
|
self.assertEqual(set(ramdisk._LOOKUP_RETURN_FIELDS) | {'links'},
|
||||||
|
set(data['node']))
|
||||||
|
self._check_config(data)
|
||||||
|
|
||||||
|
def test_restrict_lookup(self):
|
||||||
|
response = self.get_json(
|
||||||
|
'/lookup?addresses=%s&node_uuid=%s' %
|
||||||
|
(','.join(self.addresses), self.node2.uuid),
|
||||||
|
headers={api_base.Version.string: str(api_v1.MAX_VER)},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||||
|
|
||||||
|
def test_no_restrict_lookup(self):
|
||||||
|
CONF.set_override('restrict_lookup', False, 'api')
|
||||||
|
data = self.get_json(
|
||||||
|
'/lookup?addresses=%s&node_uuid=%s' %
|
||||||
|
(','.join(self.addresses), self.node2.uuid),
|
||||||
|
headers={api_base.Version.string: str(api_v1.MAX_VER)})
|
||||||
|
self.assertEqual(self.node2.uuid, data['node']['uuid'])
|
||||||
|
self.assertEqual(set(ramdisk._LOOKUP_RETURN_FIELDS) | {'links'},
|
||||||
|
set(data['node']))
|
||||||
|
self._check_config(data)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for',
|
||||||
|
lambda *n: 'test-topic')
|
||||||
|
class TestHeartbeat(test_api_base.BaseApiTest):
|
||||||
|
def test_old_api_version(self):
|
||||||
|
response = self.post_json(
|
||||||
|
'/heartbeat/%s' % uuidutils.generate_uuid(),
|
||||||
|
{'callback_url': 'url'},
|
||||||
|
headers={api_base.Version.string: str(api_v1.MIN_VER)},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||||
|
|
||||||
|
def test_node_not_found(self):
|
||||||
|
response = self.post_json(
|
||||||
|
'/heartbeat/%s' % uuidutils.generate_uuid(),
|
||||||
|
{'callback_url': 'url'},
|
||||||
|
headers={api_base.Version.string: str(api_v1.MAX_VER)},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.NOT_FOUND, response.status_int)
|
||||||
|
|
||||||
|
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||||
|
def test_ok(self, mock_heartbeat):
|
||||||
|
node = obj_utils.create_test_node(self.context)
|
||||||
|
response = self.post_json(
|
||||||
|
'/heartbeat/%s' % node.uuid,
|
||||||
|
{'callback_url': 'url'},
|
||||||
|
headers={api_base.Version.string: str(api_v1.MAX_VER)})
|
||||||
|
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',
|
||||||
|
topic='test-topic')
|
@ -41,6 +41,27 @@ class TestMacAddressType(base.TestCase):
|
|||||||
types.MacAddressType.validate, 'invalid-mac')
|
types.MacAddressType.validate, 'invalid-mac')
|
||||||
|
|
||||||
|
|
||||||
|
class TestListOfMacAddressesType(base.TestCase):
|
||||||
|
|
||||||
|
def test_valid_mac_addr(self):
|
||||||
|
test_mac = 'aa:bb:cc:11:22:33'
|
||||||
|
self.assertEqual([test_mac],
|
||||||
|
types.ListOfMacAddressesType.validate(test_mac))
|
||||||
|
|
||||||
|
def test_valid_list(self):
|
||||||
|
test_mac = 'aa:bb:cc:11:22:33,11:22:33:44:55:66'
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(test_mac.split(',')),
|
||||||
|
sorted(types.ListOfMacAddressesType.validate(test_mac)))
|
||||||
|
|
||||||
|
def test_invalid_mac_addr(self):
|
||||||
|
self.assertRaises(exception.InvalidMAC,
|
||||||
|
types.ListOfMacAddressesType.validate, 'invalid-mac')
|
||||||
|
self.assertRaises(exception.InvalidMAC,
|
||||||
|
types.ListOfMacAddressesType.validate,
|
||||||
|
'aa:bb:cc:11:22:33,invalid-mac')
|
||||||
|
|
||||||
|
|
||||||
class TestUuidType(base.TestCase):
|
class TestUuidType(base.TestCase):
|
||||||
|
|
||||||
def test_valid_uuid(self):
|
def test_valid_uuid(self):
|
||||||
|
@ -132,7 +132,7 @@ class TestBaseAgentVendor(db_base.DbTestCase):
|
|||||||
'statsd_host': CONF.metrics_statsd.agent_statsd_host,
|
'statsd_host': CONF.metrics_statsd.agent_statsd_host,
|
||||||
'statsd_port': CONF.metrics_statsd.agent_statsd_port
|
'statsd_port': CONF.metrics_statsd.agent_statsd_port
|
||||||
},
|
},
|
||||||
'heartbeat_timeout': CONF.agent.heartbeat_timeout
|
'heartbeat_timeout': CONF.api.ramdisk_heartbeat_timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
find_mock.return_value = self.node
|
find_mock.return_value = self.node
|
||||||
|
17
releasenotes/notes/lookup-heartbeat-f9772521d12a0549.yaml
Normal file
17
releasenotes/notes/lookup-heartbeat-f9772521d12a0549.yaml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- New API endpoint for deploy ramdisk lookup ``/v1/lookup``.
|
||||||
|
This endpoint is not authenticated to allow ramdisks to access it without
|
||||||
|
passing the credentials to them.
|
||||||
|
- New API endpoint for deploy ramdisk heartbeat ``/v1/heartbeat/<NODE>``.
|
||||||
|
This endpoint is not authenticated to allow ramdisks to access it without
|
||||||
|
passing the credentials to them.
|
||||||
|
deprecations:
|
||||||
|
- The configuration option ``[agent]heartbeat_timeout`` was renamed to
|
||||||
|
``[api]ramdisk_heartbeat_timeout``. The old variant is deprecated.
|
||||||
|
upgrade:
|
||||||
|
- A new configuration option ``[api]restrict_lookup`` is added, which
|
||||||
|
restricts the lookup API (normally only used by ramdisks) to only work when
|
||||||
|
the node is in specific states used by the ramdisk, and defaults to True.
|
||||||
|
Operators that need this endpoint to work in any state may set this to
|
||||||
|
False, though this is insecure and should not be used in normal operation.
|
Loading…
x
Reference in New Issue
Block a user