Fix requesting specific fields from ironic

Currently we try sending a Python dict in the query, which obviously
does not work. Convert fields to a comma-separated list first.

For chassis specify that the required API version 1.8 is supported.

Change-Id: Ie1c3230f4fd14a59237a55bab2e91f04d50529a7
This commit is contained in:
Dmitry Tantsur
2018-12-05 20:49:00 +01:00
parent 660d193623
commit 79dff76bef
12 changed files with 110 additions and 16 deletions

View File

@@ -84,3 +84,10 @@ class ListMixin(object):
base_path += '/detail' base_path += '/detail'
return super(ListMixin, cls).list(session, paginated=True, return super(ListMixin, cls).list(session, paginated=True,
base_path=base_path, **params) base_path=base_path, **params)
def comma_separated_list(value):
if value is None:
return None
else:
return ','.join(value)

View File

@@ -19,6 +19,9 @@ class Chassis(_common.ListMixin, resource.Resource):
resources_key = 'chassis' resources_key = 'chassis'
base_path = '/chassis' base_path = '/chassis'
# Specifying fields became possible in 1.8.
_max_microversion = '1.8'
# capabilities # capabilities
allow_create = True allow_create = True
allow_fetch = True allow_fetch = True
@@ -29,7 +32,7 @@ class Chassis(_common.ListMixin, resource.Resource):
commit_jsonpatch = True commit_jsonpatch = True
_query_mapping = resource.QueryParameters( _query_mapping = resource.QueryParameters(
'fields' fields={'name': 'fields', 'type': _common.comma_separated_list},
) )
#: Timestamp at which the chassis was created. #: Timestamp at which the chassis was created.

View File

@@ -49,8 +49,9 @@ class Node(_common.ListMixin, resource.Resource):
commit_jsonpatch = True commit_jsonpatch = True
_query_mapping = resource.QueryParameters( _query_mapping = resource.QueryParameters(
'associated', 'conductor_group', 'driver', 'fault', 'fields', 'associated', 'conductor_group', 'driver', 'fault',
'provision_state', 'resource_class', 'provision_state', 'resource_class',
fields={'name': 'fields', 'type': _common.comma_separated_list},
instance_id='instance_uuid', instance_id='instance_uuid',
is_maintenance='maintenance', is_maintenance='maintenance',
) )

View File

@@ -29,7 +29,8 @@ class Port(_common.ListMixin, resource.Resource):
commit_jsonpatch = True commit_jsonpatch = True
_query_mapping = resource.QueryParameters( _query_mapping = resource.QueryParameters(
'address', 'fields', 'node', 'portgroup', 'address', 'node', 'portgroup',
fields={'name': 'fields', 'type': _common.comma_separated_list},
node_id='node_uuid', node_id='node_uuid',
) )

View File

@@ -29,7 +29,8 @@ class PortGroup(_common.ListMixin, resource.Resource):
commit_jsonpatch = True commit_jsonpatch = True
_query_mapping = resource.QueryParameters( _query_mapping = resource.QueryParameters(
'node', 'address', 'fields', 'node', 'address',
fields={'name': 'fields', 'type': _common.comma_separated_list},
) )
# The mode and properties field introduced in 1.26. # The mode and properties field introduced in 1.26.

View File

@@ -273,13 +273,18 @@ class QueryParameters(object):
:param mappings: Key-value pairs where the key is the client-side :param mappings: Key-value pairs where the key is the client-side
name we'll accept here and the value is the name name we'll accept here and the value is the name
the server expects, e.g, changes_since=changes-since the server expects, e.g, changes_since=changes-since.
Additionally, a value can be a dict with optional keys
name - server-side name,
type - callable to convert from client to server
representation.
By default, both limit and marker are included in the initial mapping By default, both limit and marker are included in the initial mapping
as they're the most common query parameters used for listing resources. as they're the most common query parameters used for listing resources.
""" """
self._mapping = {"limit": "limit", "marker": "marker"} self._mapping = {"limit": "limit", "marker": "marker"}
self._mapping.update(dict({name: name for name in names}, **mappings)) self._mapping.update({name: name for name in names})
self._mapping.update(mappings)
def _validate(self, query, base_path=None): def _validate(self, query, base_path=None):
"""Check that supplied query keys match known query mappings """Check that supplied query keys match known query mappings
@@ -290,7 +295,9 @@ class QueryParameters(object):
the resource. the resource.
""" """
expected_params = list(self._mapping.keys()) expected_params = list(self._mapping.keys())
expected_params += self._mapping.values() expected_params.extend(
value['name'] if isinstance(value, dict) else value
for value in self._mapping.values())
if base_path: if base_path:
expected_params += utils.get_string_format_keys(base_path) expected_params += utils.get_string_format_keys(base_path)
@@ -312,11 +319,25 @@ class QueryParameters(object):
server side name. server side name.
""" """
result = {} result = {}
for key, value in self._mapping.items(): for client_side, server_side in self._mapping.items():
if key in query: if isinstance(server_side, dict):
result[value] = query[key] name = server_side['name']
elif value in query: type_ = server_side.get('type')
result[value] = query[value] else:
name = server_side
type_ = None
if client_side in query:
value = query[client_side]
elif name in query:
value = query[name]
else:
continue
if type_ is not None:
result[name] = type_(value)
else:
result[name] = value
return result return result

View File

@@ -49,3 +49,15 @@ class TestBareMetalChassis(base.BaseBaremetalTest):
ignore_missing=False) ignore_missing=False)
self.assertIsNone(self.conn.baremetal.find_chassis(uuid)) self.assertIsNone(self.conn.baremetal.find_chassis(uuid))
self.assertIsNone(self.conn.baremetal.delete_chassis(uuid)) self.assertIsNone(self.conn.baremetal.delete_chassis(uuid))
class TestBareMetalChassisFields(base.BaseBaremetalTest):
min_microversion = '1.8'
def test_chassis_fields(self):
self.create_chassis(description='something')
result = self.conn.baremetal.chassis(fields=['uuid', 'extra'])
for ch in result:
self.assertIsNotNone(ch.id)
self.assertIsNone(ch.description)

View File

@@ -142,6 +142,18 @@ class TestBareMetalNode(base.BaseBaremetalTest):
self.assertIsNone(self.conn.baremetal.delete_node(uuid)) self.assertIsNone(self.conn.baremetal.delete_node(uuid))
class TestBareMetalNodeFields(base.BaseBaremetalTest):
min_microversion = '1.8'
def test_node_fields(self):
self.create_node()
result = self.conn.baremetal.nodes(fields=['uuid', 'name'])
for item in result:
self.assertIsNotNone(item.id)
self.assertIsNone(item.driver)
class TestBareMetalVif(base.BaseBaremetalTest): class TestBareMetalVif(base.BaseBaremetalTest):
min_microversion = '1.28' min_microversion = '1.28'

View File

@@ -92,3 +92,16 @@ class TestBareMetalPort(base.BaseBaremetalTest):
pxe_enabled=True) pxe_enabled=True)
self.assertIsNone(self.conn.baremetal.find_port(uuid)) self.assertIsNone(self.conn.baremetal.find_port(uuid))
self.assertIsNone(self.conn.baremetal.delete_port(uuid)) self.assertIsNone(self.conn.baremetal.delete_port(uuid))
class TestBareMetalPortFields(base.BaseBaremetalTest):
min_microversion = '1.8'
def test_port_fields(self):
self.create_node()
self.create_port(address='11:22:33:44:55:66')
result = self.conn.baremetal.ports(fields=['uuid'])
for item in result:
self.assertIsNotNone(item.id)
self.assertIsNone(item.address)

View File

@@ -84,3 +84,11 @@ class TestBareMetalPortGroup(base.BaseBaremetalTest):
ignore_missing=False) ignore_missing=False)
self.assertIsNone(self.conn.baremetal.find_port_group(uuid)) self.assertIsNone(self.conn.baremetal.find_port_group(uuid))
self.assertIsNone(self.conn.baremetal.delete_port_group(uuid)) self.assertIsNone(self.conn.baremetal.delete_port_group(uuid))
def test_port_group_fields(self):
self.create_node()
self.create_port_group(address='11:22:33:44:55:66')
result = self.conn.baremetal.port_groups(fields=['uuid', 'name'])
for item in result:
self.assertIsNotNone(item.id)
self.assertIsNone(item.address)

View File

@@ -360,32 +360,43 @@ class TestQueryParameters(base.TestCase):
def test_create(self): def test_create(self):
location = "location" location = "location"
mapping = {"first_name": "first-name"} mapping = {"first_name": "first-name",
"second_name": {"name": "second-name"},
"third_name": {"name": "third", "type": int}}
sot = resource.QueryParameters(location, **mapping) sot = resource.QueryParameters(location, **mapping)
self.assertEqual({"location": "location", self.assertEqual({"location": "location",
"first_name": "first-name", "first_name": "first-name",
"second_name": {"name": "second-name"},
"third_name": {"name": "third", "type": int},
"limit": "limit", "limit": "limit",
"marker": "marker"}, "marker": "marker"},
sot._mapping) sot._mapping)
def test_transpose_unmapped(self): def test_transpose_unmapped(self):
location = "location" location = "location"
mapping = {"first_name": "first-name"} mapping = {"first_name": "first-name",
"pet_name": {"name": "pet"},
"answer": {"name": "answer", "type": int}}
sot = resource.QueryParameters(location, **mapping) sot = resource.QueryParameters(location, **mapping)
result = sot._transpose({"location": "Brooklyn", result = sot._transpose({"location": "Brooklyn",
"first_name": "Brian", "first_name": "Brian",
"pet_name": "Meow",
"answer": "42",
"last_name": "Curtin"}) "last_name": "Curtin"})
# last_name isn't mapped and shouldn't be included # last_name isn't mapped and shouldn't be included
self.assertEqual({"location": "Brooklyn", "first-name": "Brian"}, self.assertEqual({"location": "Brooklyn", "first-name": "Brian",
"pet": "Meow", "answer": 42},
result) result)
def test_transpose_not_in_query(self): def test_transpose_not_in_query(self):
location = "location" location = "location"
mapping = {"first_name": "first-name"} mapping = {"first_name": "first-name",
"pet_name": {"name": "pet"},
"answer": {"name": "answer", "type": int}}
sot = resource.QueryParameters(location, **mapping) sot = resource.QueryParameters(location, **mapping)
result = sot._transpose({"location": "Brooklyn"}) result = sot._transpose({"location": "Brooklyn"})

View File

@@ -0,0 +1,4 @@
---
fixes:
- |
Fixes specifying fields when listing bare metal resources.