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
|
||||
|
||||
if is_deployed_by_agent; then
|
||||
iniset $IRONIC_CONF_FILE agent heartbeat_timeout 30
|
||||
iniset $IRONIC_CONF_FILE api ramdisk_heartbeat_timeout 30
|
||||
fi
|
||||
|
||||
# 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
|
||||
--------------------
|
||||
|
||||
**1.22**
|
||||
|
||||
Added endpoints for deployment ramdisks.
|
||||
|
||||
**1.21**
|
||||
|
||||
Add node ``resource_class`` field.
|
||||
|
@ -400,10 +400,6 @@
|
||||
# be set to True. Defaults to True. (boolean value)
|
||||
#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
|
||||
# bare metal node has been powered off after a soft power off.
|
||||
# (integer value)
|
||||
@ -486,6 +482,15 @@
|
||||
# 'public_endpoint' option. (boolean value)
|
||||
#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]
|
||||
|
||||
|
@ -30,6 +30,9 @@ app = {
|
||||
'/',
|
||||
'/v1',
|
||||
# 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/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 node
|
||||
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 import expose
|
||||
from ironic.common.i18n import _
|
||||
@ -78,6 +80,12 @@ class V1(base.APIBase):
|
||||
drivers = [link.Link]
|
||||
"""Links to the drivers resource"""
|
||||
|
||||
lookup = [link.Link]
|
||||
"""Links to the lookup resource"""
|
||||
|
||||
heartbeat = [link.Link]
|
||||
"""Links to the heartbeat resource"""
|
||||
|
||||
@staticmethod
|
||||
def convert():
|
||||
v1 = V1()
|
||||
@ -120,6 +128,22 @@ class V1(base.APIBase):
|
||||
'drivers', '',
|
||||
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
|
||||
|
||||
|
||||
@ -130,6 +154,8 @@ class Controller(rest.RestController):
|
||||
ports = port.PortsController()
|
||||
chassis = chassis.ChassisController()
|
||||
drivers = driver.DriversController()
|
||||
lookup = ramdisk.LookupController()
|
||||
heartbeat = ramdisk.HeartbeatController()
|
||||
|
||||
@expose.expose(V1)
|
||||
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)
|
||||
|
||||
|
||||
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()
|
||||
uuid_or_name = UuidOrNameType()
|
||||
name = NameType()
|
||||
@ -184,6 +204,7 @@ boolean = BooleanType()
|
||||
listtype = ListType()
|
||||
# Can't call it 'json' because that's the name of the stdlib module
|
||||
jsontype = JsonType()
|
||||
list_of_macaddress = ListOfMacAddressesType()
|
||||
|
||||
|
||||
class JsonPatchType(wtypes.Base):
|
||||
|
@ -383,6 +383,14 @@ def allow_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):
|
||||
"""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.20: Add node.network_interface
|
||||
# v1.21: Add node.resource_class
|
||||
# v1.22: Ramdisk lookup and heartbeat endpoints.
|
||||
|
||||
MINOR_0_JUNO = 0
|
||||
MINOR_1_INITIAL_VERSION = 1
|
||||
@ -74,11 +75,12 @@ MINOR_18_PORT_INTERNAL_INFO = 18
|
||||
MINOR_19_PORT_ADVANCED_NET_FIELDS = 19
|
||||
MINOR_20_NETWORK_INTERFACE = 20
|
||||
MINOR_21_RESOURCE_CLASS = 21
|
||||
MINOR_22_LOOKUP_HEARTBEAT = 22
|
||||
|
||||
# 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
|
||||
# changed.
|
||||
MINOR_MAX_VERSION = MINOR_21_RESOURCE_CLASS
|
||||
MINOR_MAX_VERSION = MINOR_22_LOOKUP_HEARTBEAT
|
||||
|
||||
# String representations of the minor and maximum versions
|
||||
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||
|
@ -606,3 +606,8 @@ class NodeTagNotFound(IronicException):
|
||||
|
||||
class NetworkError(IronicException):
|
||||
_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 '
|
||||
'copied to is really slow, this option should be set '
|
||||
'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',
|
||||
default=6,
|
||||
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 "
|
||||
"will want to change public API endpoint to represent "
|
||||
"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',
|
||||
|
@ -26,6 +26,7 @@ from oslo_utils import strutils
|
||||
from oslo_utils import timeutils
|
||||
import retrying
|
||||
|
||||
from ironic.api.controllers.v1 import ramdisk
|
||||
from ironic.common import boot_devices
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
@ -789,23 +790,9 @@ class BaseAgentVendor(AgentDeployMixin, base.VendorInterface):
|
||||
# config namespace. Instead of a separate deprecation,
|
||||
# this will die when the vendor_passthru version of
|
||||
# lookup goes away.
|
||||
'heartbeat_timeout': CONF.agent.heartbeat_timeout,
|
||||
'heartbeat_timeout': CONF.api.ramdisk_heartbeat_timeout,
|
||||
'node': ndict,
|
||||
'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
|
||||
}
|
||||
'config': ramdisk.config(),
|
||||
}
|
||||
|
||||
def _get_interfaces(self, inventory):
|
||||
|
@ -13,6 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from ironic.api.controllers import base as api_base
|
||||
from ironic.api.controllers.v1 import versions
|
||||
from ironic.tests.unit.api import base
|
||||
|
||||
@ -51,3 +52,20 @@ class TestV1Root(base.BaseApiTest):
|
||||
|
||||
self.assertIn({'type': 'application/vnd.openstack.ironic.v1+json',
|
||||
'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')
|
||||
|
||||
|
||||
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):
|
||||
|
||||
def test_valid_uuid(self):
|
||||
|
@ -132,7 +132,7 @@ class TestBaseAgentVendor(db_base.DbTestCase):
|
||||
'statsd_host': CONF.metrics_statsd.agent_statsd_host,
|
||||
'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
|
||||
|
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…
Reference in New Issue
Block a user