Allocation API: optimize check on candidate nodes

Currently we fetch all nodes one by one when validating the provided
candidate_nodes. This is not overly efficient, so this change introduces
a new database API to specifically validate a list of names/uuids and
convert it to uuids. As a nice side effect, no database query is done
for identities that cannot be a valid node name or uuid.

Change-Id: I7adfe897200b1b05d9e632c68d0d3d046c06a921
Story: #2004341
This commit is contained in:
Dmitry Tantsur 2019-03-04 14:52:14 +01:00
parent 3f6d4c6a78
commit 885bfbda96
5 changed files with 102 additions and 10 deletions

View File

@ -343,16 +343,16 @@ class AllocationsController(pecan.rest.RestController):
if allocation.candidate_nodes: if allocation.candidate_nodes:
# Convert nodes from names to UUIDs and check their validity # Convert nodes from names to UUIDs and check their validity
converted = [] try:
for node in allocation.candidate_nodes: converted = pecan.request.dbapi.check_node_list(
try: allocation.candidate_nodes)
node = api_utils.get_rpc_node(node) except exception.NodeNotFound as exc:
except exception.NodeNotFound as exc: exc.code = http_client.BAD_REQUEST
exc.code = http_client.BAD_REQUEST raise
raise else:
else: # Make sure we keep the ordering of candidate nodes.
converted.append(node.uuid) allocation.candidate_nodes = [
allocation.candidate_nodes = converted converted[ident] for ident in allocation.candidate_nodes]
all_dict = allocation.as_dict() all_dict = allocation.as_dict()

View File

@ -97,6 +97,20 @@ class Connection(object):
(asc, desc) (asc, desc)
""" """
@abc.abstractmethod
def check_node_list(self, idents):
"""Check a list of node identities and map it to UUIDs.
This call takes a list of node names and/or UUIDs and tries to convert
them to UUIDs. It fails early if any identities cannot possible be used
as names or UUIDs.
:param idents: List of identities.
:returns: A mapping from requests identities to node UUIDs.
:raises: NodeNotFound if some identities were not found or cannot be
valid names or UUIDs.
"""
@abc.abstractmethod @abc.abstractmethod
def reserve_node(self, tag, node_id): def reserve_node(self, tag, node_id):
"""Reserve a node. """Reserve a node.

View File

@ -39,6 +39,7 @@ from ironic.common.i18n import _
from ironic.common import profiler from ironic.common import profiler
from ironic.common import release_mappings from ironic.common import release_mappings
from ironic.common import states from ironic.common import states
from ironic.common import utils
from ironic.conf import CONF from ironic.conf import CONF
from ironic.db import api from ironic.db import api
from ironic.db.sqlalchemy import models from ironic.db.sqlalchemy import models
@ -398,6 +399,39 @@ class Connection(api.Connection):
return _paginate_query(models.Node, limit, marker, return _paginate_query(models.Node, limit, marker,
sort_key, sort_dir, query) sort_key, sort_dir, query)
def check_node_list(self, idents):
mapping = {}
if idents:
idents = set(idents)
else:
return mapping
uuids = {i for i in idents if uuidutils.is_uuid_like(i)}
names = {i for i in idents if not uuidutils.is_uuid_like(i)
and utils.is_valid_logical_name(i)}
missing = idents - set(uuids) - set(names)
if missing:
# Such nodes cannot exist, bailing out early
raise exception.NodeNotFound(
_("Nodes cannot be found: %s") % ', '.join(missing))
query = model_query(models.Node.uuid, models.Node.name).filter(
sql.or_(models.Node.uuid.in_(uuids),
models.Node.name.in_(names))
)
for row in query:
if row[0] in idents:
mapping[row[0]] = row[0]
if row[1] and row[1] in idents:
mapping[row[1]] = row[0]
missing = idents - set(mapping)
if missing:
raise exception.NodeNotFound(
_("Nodes cannot be found: %s") % ', '.join(missing))
return mapping
@oslo_db_api.retry_on_deadlock @oslo_db_api.retry_on_deadlock
def reserve_node(self, tag, node_id): def reserve_node(self, tag, node_id):
with _session_for_write(): with _session_for_write():

View File

@ -566,6 +566,15 @@ class TestPost(test_api_base.BaseApiTest):
self.assertEqual(http_client.BAD_REQUEST, response.status_int) self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertTrue(response.json['error_message']) self.assertTrue(response.json['error_message'])
def test_create_allocation_candidate_node_invalid(self):
adict = apiutils.allocation_post_data(
candidate_nodes=['this/is/not a/node/name'])
response = self.post_json('/allocations', adict, expect_errors=True,
headers=self.headers)
self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertTrue(response.json['error_message'])
def test_create_allocation_name_ok(self): def test_create_allocation_name_ok(self):
name = 'foo' name = 'foo'
adict = apiutils.allocation_post_data(name=name) adict = apiutils.allocation_post_data(name=name)

View File

@ -853,3 +853,38 @@ class DbNodeTestCase(base.DbTestCase):
'Multiple nodes', 'Multiple nodes',
self.dbapi.get_node_by_port_addresses, self.dbapi.get_node_by_port_addresses,
addresses) addresses)
def test_check_node_list(self):
node1 = utils.create_test_node(uuid=uuidutils.generate_uuid())
node2 = utils.create_test_node(uuid=uuidutils.generate_uuid(),
name='node_2')
node3 = utils.create_test_node(uuid=uuidutils.generate_uuid(),
name='node_3')
mapping = self.dbapi.check_node_list([node1.uuid, node2.name,
node3.uuid])
self.assertEqual({node1.uuid: node1.uuid,
node2.name: node2.uuid,
node3.uuid: node3.uuid},
mapping)
def test_check_node_list_non_existing(self):
node1 = utils.create_test_node(uuid=uuidutils.generate_uuid())
node2 = utils.create_test_node(uuid=uuidutils.generate_uuid(),
name='node_2')
uuid = uuidutils.generate_uuid()
exc = self.assertRaises(exception.NodeNotFound,
self.dbapi.check_node_list,
[node1.uuid, uuid, 'could-be-a-name',
node2.name])
self.assertIn(uuid, str(exc))
self.assertIn('could-be-a-name', str(exc))
def test_check_node_list_impossible(self):
node1 = utils.create_test_node(uuid=uuidutils.generate_uuid())
exc = self.assertRaises(exception.NodeNotFound,
self.dbapi.check_node_list,
[node1.uuid, 'this/cannot/be/a/name'])
self.assertIn('this/cannot/be/a/name', str(exc))