diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index d8fb0b2ea3..68b5dde544 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -293,6 +293,23 @@ r_conductor_group: in: query required: false type: string +r_conductor_groups: + description: | + Filter the list of returned ports or portgroups, and only return those with + the specified ``conductor_group`` or an empty set if none found. List of + case-insensitive strings up to 255 characters, containing ``a-z``, ``0-9``, + ``_``, ``-``, and ``.``. This cannot be used if ``node``, ``node_uuid``, + ``portgroup`` or ``address`` is specified. + + For example, the following request returns only the ports for nodes + in conductor groups ``bear`` and ``metal``: + + :: + + GET /v1/ports?conductor_groups=bear,metal + in: query + required: false + type: array r_description_contains: description: | Filter the list of returned nodes, and only return those containing diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst index 6624f1a484..5c74691208 100644 --- a/doc/source/contributor/webapi-version-history.rst +++ b/doc/source/contributor/webapi-version-history.rst @@ -2,6 +2,12 @@ REST API Version History ======================== +1.99 (Flamingo) +----------------------- + +Add ability to filter on node conductor group when listing ports and +portgroups. + 1.98 (Flamingo) Add support for patching object attributes with keys containing ~ or /. diff --git a/ironic/api/controllers/v1/port.py b/ironic/api/controllers/v1/port.py index 3a33c623a7..929d7e6a70 100644 --- a/ironic/api/controllers/v1/port.py +++ b/ironic/api/controllers/v1/port.py @@ -233,7 +233,8 @@ class PortsController(rest.RestController): def _get_ports_collection(self, node_ident, address, portgroup_ident, shard, marker, limit, sort_key, sort_dir, resource_url=None, fields=None, detail=None, - project=None, description_contains=None): + project=None, description_contains=None, + conductor_groups=None): """Retrieve a collection of ports. :param node_ident: UUID or name of a node, to get only ports for that @@ -259,6 +260,7 @@ class PortsController(rest.RestController): :param description_contains: Optional string value to get only ports with description field contains matching value. + :param conductor_groups: conductor groups, to filter the request by. :returns: a list of ports. """ @@ -290,6 +292,11 @@ class PortsController(rest.RestController): if description_contains: filters['description_contains'] = description_contains + if conductor_groups and (node_ident or portgroup_ident or address): + raise exception.Invalid( + _("Filtering by conductor_groups cannot be combined with " + "node_ident, portgroup_ident, or node address filters.")) + if portgroup_ident: # FIXME: Since all we need is the portgroup ID, we can # make this more efficient by only querying @@ -327,6 +334,7 @@ class PortsController(rest.RestController): ports = objects.Port.list(api.request.context, limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir, project=project, + conductor_groups=conductor_groups, filters=filters) parameters = {} @@ -405,11 +413,12 @@ class PortsController(rest.RestController): limit=args.integer, sort_key=args.string, sort_dir=args.string, fields=args.string_list, portgroup=args.uuid_or_name, detail=args.boolean, - shard=args.string_list, description_contains=args.string) + shard=args.string_list, description_contains=args.string, + conductor_groups=args.string_list) def get_all(self, node=None, node_uuid=None, address=None, marker=None, limit=None, sort_key='id', sort_dir='asc', fields=None, portgroup=None, detail=None, shard=None, - description_contains=None): + description_contains=None, conductor_groups=None): """Retrieve a list of ports. Note that the 'node_uuid' interface is deprecated in favour @@ -437,6 +446,7 @@ class PortsController(rest.RestController): :param description_contains: Optional string value to get only ports with description field contains matching value. + :param conductor_groups: conductor groups, to filter the request by. :raises: NotAcceptable, HTTPNotFound """ project = api_utils.check_port_list_policy( @@ -476,7 +486,9 @@ class PortsController(rest.RestController): sort_key, sort_dir, resource_url='ports', fields=fields, detail=detail, - project=project, **extra_args) + project=project, + conductor_groups=conductor_groups, + **extra_args) @METRICS.timer('PortsController.detail') @method.expose() @@ -484,10 +496,11 @@ class PortsController(rest.RestController): address=args.mac_address, marker=args.uuid, limit=args.integer, sort_key=args.string, sort_dir=args.string, portgroup=args.uuid_or_name, - shard=args.string_list, description_contains=args.string) + shard=args.string_list, description_contains=args.string, + conductor_groups=args.string_list) def detail(self, node=None, node_uuid=None, address=None, marker=None, limit=None, sort_key='id', sort_dir='asc', portgroup=None, - shard=None, description_contains=None): + shard=None, description_contains=None, conductor_groups=None): """Retrieve a list of ports with detail. Note that the 'node_uuid' interface is deprecated in favour @@ -513,6 +526,7 @@ class PortsController(rest.RestController): :param description_contains: Optional string value to get only ports with description field contains matching value. + :param conductor_groups: conductor groups, to filter the request by. :raises: NotAcceptable, HTTPNotFound """ project = api_utils.check_port_list_policy( @@ -543,7 +557,9 @@ class PortsController(rest.RestController): portgroup, shard, marker, limit, sort_key, sort_dir, resource_url='ports/detail', - project=project, **extra_args) + project=project, + conductor_groups=conductor_groups, + **extra_args) @METRICS.timer('PortsController.get_one') @method.expose() diff --git a/ironic/api/controllers/v1/portgroup.py b/ironic/api/controllers/v1/portgroup.py index ec1035e112..52c536e5e1 100644 --- a/ironic/api/controllers/v1/portgroup.py +++ b/ironic/api/controllers/v1/portgroup.py @@ -166,7 +166,8 @@ class PortgroupsController(pecan.rest.RestController): def _get_portgroups_collection(self, node_ident, address, marker, limit, sort_key, sort_dir, resource_url=None, fields=None, - detail=None, project=None): + detail=None, project=None, + conductor_groups=None): """Return portgroups collection. :param node_ident: UUID or name of a node. @@ -179,6 +180,7 @@ class PortgroupsController(pecan.rest.RestController): :param fields: Optional, a list with a specified set of fields of the resource to be returned. :param project: Optional, project ID to filter the request by. + :param conductor_groups: conductor groups, to filter the request by. """ limit = api_utils.validate_limit(limit) sort_dir = api_utils.validate_sort_dir(sort_dir) @@ -195,6 +197,11 @@ class PortgroupsController(pecan.rest.RestController): node_ident = self.parent_node_ident or node_ident + if conductor_groups and (node_ident or address): + raise exception.Invalid( + _("Filtering by conductor_groups and node_ident/address " + "simultaneously is not supported.")) + if node_ident: # FIXME: Since all we need is the node ID, we can # make this more efficient by only querying @@ -209,10 +216,10 @@ class PortgroupsController(pecan.rest.RestController): portgroups = self._get_portgroups_by_address(address, project=project) else: - portgroups = objects.Portgroup.list(api.request.context, limit, - marker_obj, sort_key=sort_key, - sort_dir=sort_dir, - project=project) + portgroups = objects.Portgroup.list( + api.request.context, limit, + marker_obj, sort_key=sort_key, sort_dir=sort_dir, + project=project, conductor_groups=conductor_groups) parameters = {} if detail is not None: parameters['detail'] = detail @@ -246,10 +253,10 @@ class PortgroupsController(pecan.rest.RestController): @args.validate(node=args.uuid_or_name, address=args.mac_address, marker=args.uuid, limit=args.integer, sort_key=args.string, sort_dir=args.string, fields=args.string_list, - detail=args.boolean) + detail=args.boolean, conductor_groups=args.string_list) def get_all(self, node=None, address=None, marker=None, limit=None, sort_key='id', sort_dir='asc', fields=None, - detail=None): + detail=None, conductor_groups=None): """Retrieve a list of portgroups. :param node: UUID or name of a node, to get only portgroups for that @@ -265,6 +272,7 @@ class PortgroupsController(pecan.rest.RestController): :param sort_dir: direction to sort. "asc" or "desc". Default: asc. :param fields: Optional, a list with a specified set of fields of the resource to be returned. + :param conductor_groups: conductor groups, to filter the request by. """ if not api_utils.allow_portgroups(): raise exception.NotFound() @@ -284,21 +292,22 @@ class PortgroupsController(pecan.rest.RestController): fields = api_utils.get_request_return_fields(fields, detail, _DEFAULT_RETURN_FIELDS) - return self._get_portgroups_collection(node, address, - marker, limit, - sort_key, sort_dir, - fields=fields, - resource_url='portgroups', - detail=detail, - project=project) + return self._get_portgroups_collection( + node, address, marker, limit, sort_key, sort_dir, + fields=fields, + resource_url='portgroups', + detail=detail, + project=project, + conductor_groups=conductor_groups) @METRICS.timer('PortgroupsController.detail') @method.expose() @args.validate(node=args.uuid_or_name, address=args.mac_address, marker=args.uuid, limit=args.integer, sort_key=args.string, - sort_dir=args.string) + sort_dir=args.string, conductor_groups=args.string_list) def detail(self, node=None, address=None, marker=None, - limit=None, sort_key='id', sort_dir='asc'): + limit=None, sort_key='id', sort_dir='asc', + conductor_groups=None): """Retrieve a list of portgroups with detail. :param node: UUID or name of a node, to get only portgroups for that @@ -312,6 +321,7 @@ class PortgroupsController(pecan.rest.RestController): max_limit resources will be returned. :param sort_key: column to sort results by. Default: id. :param sort_dir: direction to sort. "asc" or "desc". Default: asc. + :param conductor_groups: conductor groups, to filter the request by. """ if not api_utils.allow_portgroups(): raise exception.NotFound() @@ -334,7 +344,8 @@ class PortgroupsController(pecan.rest.RestController): return self._get_portgroups_collection( node, address, marker, limit, sort_key, sort_dir, - resource_url='portgroups/detail', project=project) + resource_url='portgroups/detail', project=project, + conductor_groups=conductor_groups) @METRICS.timer('PortgroupsController.get_one') @method.expose() diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index a3e73855c3..44fd535eca 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -136,6 +136,7 @@ BASE_VERSION = 1 # v1.96: Migrate inspection rules from Inspector # v1.97: Add description field to port. # v1.98: Add support for object attributes with keys containing ~ or /. +# v1.99: Add conductor group filtering to port and portgroup list MINOR_0_JUNO = 0 MINOR_1_INITIAL_VERSION = 1 @@ -236,6 +237,7 @@ MINOR_95_DISABLE_POWER_OFF = 95 MINOR_96_INSPECTION_RULES = 96 MINOR_97_PORT_DESCRIPTION = 97 MINOR_98_SUPPORT_SPECIAL_CHAR_IN_ATTRIBUTES = 98 +MINOR_99_PORT_PORTGROUP_CONDUCTOR_GROUP_FILTER = 99 # When adding another version, update: # - MINOR_MAX_VERSION @@ -243,7 +245,8 @@ MINOR_98_SUPPORT_SPECIAL_CHAR_IN_ATTRIBUTES = 98 # explanation of what changed in the new version # - common/release_mappings.py, RELEASE_MAPPING['master']['api'] -MINOR_MAX_VERSION = MINOR_98_SUPPORT_SPECIAL_CHAR_IN_ATTRIBUTES + +MINOR_MAX_VERSION = MINOR_99_PORT_PORTGROUP_CONDUCTOR_GROUP_FILTER # String representations of the minor and maximum versions _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py index 3a03ca2a4d..487c1bd462 100644 --- a/ironic/common/release_mappings.py +++ b/ironic/common/release_mappings.py @@ -545,7 +545,7 @@ RELEASE_MAPPING = { 'Deployment': ['1.0'], 'DeployTemplate': ['1.1'], 'Port': ['1.11'], - 'Portgroup': ['1.4'], + 'Portgroup': ['1.5'], 'Trait': ['1.0'], 'TraitList': ['1.0'], 'VolumeConnector': ['1.0'], @@ -848,7 +848,7 @@ RELEASE_MAPPING = { # make it below. To release, we will preserve a version matching # the release as a separate block of text, like above. 'master': { - 'api': '1.98', + 'api': '1.99', 'rpc': '1.61', 'objects': { 'Allocation': ['1.1'], diff --git a/ironic/db/api.py b/ironic/db/api.py index 08c1a561b0..10d2dcd1d4 100644 --- a/ironic/db/api.py +++ b/ironic/db/api.py @@ -285,7 +285,8 @@ class Connection(object, metaclass=abc.ABCMeta): @abc.abstractmethod def get_port_list(self, limit=None, marker=None, - sort_key=None, sort_dir=None, filters=None): + sort_key=None, sort_dir=None, + conductor_groups=None, filters=None): """Return a list of ports. :param limit: Maximum number of ports to return. @@ -294,6 +295,8 @@ class Connection(object, metaclass=abc.ABCMeta): :param sort_key: Attribute by which results should be sorted. :param sort_dir: direction in which results should be sorted. (asc, desc) + :param conductor_groups: A list of conductor groups to filter by, + defaults to None :param filters: Filters to apply, defaults to None """ @@ -400,7 +403,8 @@ class Connection(object, metaclass=abc.ABCMeta): @abc.abstractmethod def get_portgroup_list(self, limit=None, marker=None, sort_key=None, sort_dir=None, - project=None): + project=None, conductor_groups=None, + filters=None): """Return a list of portgroups. :param limit: Maximum number of portgroups to return. @@ -410,6 +414,9 @@ class Connection(object, metaclass=abc.ABCMeta): :param sort_dir: Direction in which results should be sorted. (asc, desc) :param project: A node owner or lessee to filter by. + :param conductor_groups: A list of conductor groups to filter by, + defaults to None + :param filters: Filters to apply, defaults to None :returns: A list of portgroups. """ diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py index f4708dc9e1..51e3fcabb5 100644 --- a/ironic/db/sqlalchemy/api.py +++ b/ironic/db/sqlalchemy/api.py @@ -337,6 +337,24 @@ def add_port_filter_by_portgroup(query, value): return query.filter(models.Portgroup.uuid == value) +def add_port_filter_by_node_conductor_groups(query, conductor_groups): + if conductor_groups: + query = query.join(models.Node, + models.Port.node_id == models.Node.id) + query = query.filter( + models.Node.conductor_group.in_(conductor_groups)) + return query + + +def add_portgroup_filter_by_node_conductor_groups(query, conductor_groups): + if conductor_groups: + query = query.join(models.Node, + models.Portgroup.node_id == models.Node.id) + query = query.filter( + models.Node.conductor_group.in_(conductor_groups)) + return query + + def add_node_filter_by_chassis(query, value): if strutils.is_int_like(value): return query.filter_by(chassis_id=value) @@ -1080,13 +1098,16 @@ class Connection(api.Connection): def get_port_list(self, limit=None, marker=None, sort_key=None, sort_dir=None, owner=None, - project=None, filters=None): + project=None, conductor_groups=None, filters=None): query = sa.select(models.Port) + if owner: query = add_port_filter_by_node_owner(query, owner) elif project: query = add_port_filter_by_node_project(query, project) query = add_port_filter_description_contains(query, filters) + query = add_port_filter_by_node_conductor_groups(query, + conductor_groups) return _paginate_query(models.Port, limit, marker, sort_key, sort_dir, query) @@ -1223,8 +1244,11 @@ class Connection(api.Connection): return res def get_portgroup_list(self, limit=None, marker=None, - sort_key=None, sort_dir=None, project=None): + sort_key=None, sort_dir=None, project=None, + conductor_groups=None, filters=None): query = sa.select(models.Portgroup) + query = add_portgroup_filter_by_node_conductor_groups( + query, conductor_groups) if project: query = add_portgroup_filter_by_node_project(query, project) return _paginate_query(models.Portgroup, limit, marker, diff --git a/ironic/objects/port.py b/ironic/objects/port.py index 092b37416e..5706121492 100644 --- a/ironic/objects/port.py +++ b/ironic/objects/port.py @@ -266,7 +266,8 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): # @object_base.remotable_classmethod @classmethod def list(cls, context, limit=None, marker=None, sort_key=None, - sort_dir=None, owner=None, project=None, filters=None): + sort_dir=None, owner=None, project=None, conductor_groups=None, + filters=None): """Return a list of Port objects. :param context: Security context. @@ -276,6 +277,8 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): :param sort_dir: direction to sort. "asc" or "desc". :param owner: DEPRECATED a node owner to match against :param project: a node owner or lessee to match against + :param conductor_groups: A list of conductor groups to filter by, + defaults to None :param filters: Filters to apply, defaults to None :returns: a list of :class:`Port` object. :raises: InvalidParameterValue @@ -288,7 +291,8 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat): sort_key=sort_key, sort_dir=sort_dir, project=project, - filters=filters) + filters=filters, + conductor_groups=conductor_groups) return cls._from_db_object_list(context, db_ports) @classmethod diff --git a/ironic/objects/portgroup.py b/ironic/objects/portgroup.py index fa24b9a13b..0fac096a39 100644 --- a/ironic/objects/portgroup.py +++ b/ironic/objects/portgroup.py @@ -195,7 +195,8 @@ class Portgroup(base.IronicObject, object_base.VersionedObjectDictCompat): # @object_base.remotable_classmethod @classmethod def list(cls, context, limit=None, marker=None, - sort_key=None, sort_dir=None, project=None): + sort_key=None, sort_dir=None, project=None, + conductor_groups=None, filters=None): """Return a list of Portgroup objects. :param cls: the :class:`Portgroup` @@ -205,15 +206,21 @@ class Portgroup(base.IronicObject, object_base.VersionedObjectDictCompat): :param sort_key: Column to sort results by. :param sort_dir: Direction to sort. "asc" or "desc". :param project: a node owner or lessee to match against. + :param conductor_groups: A list of conductor groups to filter by, + defaults to None + :param filters: Filters to apply, defaults to None :returns: A list of :class:`Portgroup` object. :raises: InvalidParameterValue """ - db_portgroups = cls.dbapi.get_portgroup_list(limit=limit, - marker=marker, - sort_key=sort_key, - sort_dir=sort_dir, - project=project) + db_portgroups = cls.dbapi.get_portgroup_list( + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir, + project=project, + filters=filters, + conductor_groups=conductor_groups) return cls._from_db_object_list(context, db_portgroups) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable diff --git a/ironic/tests/unit/api/controllers/v1/test_port.py b/ironic/tests/unit/api/controllers/v1/test_port.py index 2f56d12a69..01bf80e69c 100644 --- a/ironic/tests/unit/api/controllers/v1/test_port.py +++ b/ironic/tests/unit/api/controllers/v1/test_port.py @@ -197,7 +197,22 @@ class TestPortsController__GetPortsCollection(base.TestCase): resource_url='ports') mock_list.assert_called_once_with('fake-context', 1000, None, project=None, sort_dir='asc', - sort_key=None, filters={}) + sort_key=None, conductor_groups=None, + filters={}) + + def test__get_ports_collection_conductor_groups(self, mock_request, + mock_list): + mock_request.context = 'fake-context' + mock_list.return_value = [] + self.controller._get_ports_collection(None, None, None, None, None, + None, None, 'asc', + resource_url='ports', + conductor_groups=['foo']) + mock_list.assert_called_once_with('fake-context', 1000, None, + project=None, sort_dir='asc', + sort_key=None, + conductor_groups=['foo'], + filters={}) @mock.patch.object(objects.Port, 'get_by_address', autospec=True) @@ -1171,6 +1186,44 @@ class TestListPortsByShard(test_api_base.BaseApiTest): self.assertNotEqual(res['ports'][0]['address'], bad_shard_address) self.assertNotEqual(res['ports'][1]['address'], bad_shard_address) + def test_get_all_by_conductor_groups(self): + # Get /v1/ports with filter on conductor group + node_a = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + conductor_group='group_a') + node_b = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + conductor_group='group_b') + uuids_group_a = [] + uuids_group_b = [] + for i in range(0, 2): + port = obj_utils.create_test_port(self.context, + node_id=node_a.id, + uuid=uuidutils.generate_uuid(), + address='52:54:00:cf:2d:3%s' % i) + uuids_group_a.append(port.uuid) + for i in range(3, 6): + port = obj_utils.create_test_port(self.context, + node_id=node_b.id, + uuid=uuidutils.generate_uuid(), + address='52:54:00:cf:2d:3%s' % i) + uuids_group_b.append(port.uuid) + data = self.get_json( + '/ports?conductor_groups=%s' % 'group_a,group_b', + headers={api_base.Version.string: str(api_v1.max_version())}) + self.assertEqual(5, len(data['ports'])) + self.assertJsonEqual(uuids_group_a + uuids_group_b, + [x['uuid'] for x in data['ports']]) + data = self.get_json( + '/ports?conductor_groups=%s' % 'group_b', + headers={api_base.Version.string: str(api_v1.max_version())}) + self.assertEqual(3, len(data['ports'])) + self.assertJsonEqual(uuids_group_b, [x['uuid'] for x in data['ports']]) + data = self.get_json( + '/ports?conductor_groups=%s' % 'no_such_group', + headers={api_base.Version.string: str(api_v1.max_version())}) + self.assertEqual([], data['ports']) + @mock.patch.object(rpcapi.ConductorAPI, 'update_port', autospec=True, side_effect=_rpcapi_update_port) diff --git a/ironic/tests/unit/api/controllers/v1/test_portgroup.py b/ironic/tests/unit/api/controllers/v1/test_portgroup.py index 29b85f8c5b..a57ea8e22d 100644 --- a/ironic/tests/unit/api/controllers/v1/test_portgroup.py +++ b/ironic/tests/unit/api/controllers/v1/test_portgroup.py @@ -596,6 +596,49 @@ class TestListPortgroups(test_api_base.BaseApiTest): self.assertEqual(portgroup.uuid, data['portgroups'][0]['uuid']) self.assertEqual(self.node.uuid, data['portgroups'][0]['node_uuid']) + def test_get_all_by_conductor_groups(self): + # Get /v1/portgroups with filter on conductor group + node_a = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + conductor_group='group_a') + node_b = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + conductor_group='group_b') + uuids_group_a = [] + uuids_group_b = [] + for i in range(0, 2): + portgroup = obj_utils.create_test_portgroup( + self.context, + node_id=node_a.id, + uuid=uuidutils.generate_uuid(), + name='foo-%s' % i, + address='52:54:00:cf:2d:3%s' % i) + uuids_group_a.append(portgroup.uuid) + for i in range(3, 6): + portgroup = obj_utils.create_test_portgroup( + self.context, + node_id=node_b.id, + uuid=uuidutils.generate_uuid(), + name='foo-%s' % i, + address='52:54:00:cf:2d:3%s' % i) + uuids_group_b.append(portgroup.uuid) + data = self.get_json( + '/portgroups?conductor_groups=%s' % 'group_a,group_b', + headers={api_base.Version.string: str(api_v1.max_version())}) + self.assertEqual(5, len(data['portgroups'])) + self.assertJsonEqual(uuids_group_a + uuids_group_b, + [x['uuid'] for x in data['portgroups']]) + data = self.get_json( + '/portgroups?conductor_groups=%s' % 'group_b', + headers={api_base.Version.string: str(api_v1.max_version())}) + self.assertEqual(3, len(data['portgroups'])) + self.assertJsonEqual(uuids_group_b, + [x['uuid'] for x in data['portgroups']]) + data = self.get_json( + '/portgroups?conductor_groups=%s' % 'no_such_group', + headers={api_base.Version.string: str(api_v1.max_version())}) + self.assertEqual([], data['portgroups']) + @mock.patch.object(rpcapi.ConductorAPI, 'update_portgroup', autospec=True) class TestPatch(test_api_base.BaseApiTest): diff --git a/ironic/tests/unit/db/test_portgroups.py b/ironic/tests/unit/db/test_portgroups.py index c39bef62ad..fc7835d214 100644 --- a/ironic/tests/unit/db/test_portgroups.py +++ b/ironic/tests/unit/db/test_portgroups.py @@ -111,6 +111,41 @@ class DbportgroupTestCase(base.DbTestCase): def test_get_portgroups_by_node_id_that_does_not_exist(self): self.assertEqual([], self.dbapi.get_portgroups_by_node_id(99)) + def test_get_portgoups_by_conductor_groups(self): + group_a_node = db_utils.create_test_node( + uuid=uuidutils.generate_uuid(), + conductor_group='group_a') + group_b_node = db_utils.create_test_node( + uuid=uuidutils.generate_uuid(), + conductor_group='group_b') + + group_a_uuids = [] + group_b_uuids = [] + for i in range(1, 3): + port = db_utils.create_test_portgroup( + uuid=uuidutils.generate_uuid(), node_id=group_a_node.id, + address='52:54:00:cf:2d:4%s' % i, name='group_a_node_pg%s' % i) + group_a_uuids.append(str(port.uuid)) + for i in range(7, 9): + port = db_utils.create_test_portgroup( + uuid=uuidutils.generate_uuid(), node_id=group_b_node.id, + address='52:54:00:cf:2d:4%s' % i, name='group_b_node_pg%s' % i) + group_b_uuids.append(str(port.uuid)) + for i in range(4, 6): + port = db_utils.create_test_portgroup( + uuid=uuidutils.generate_uuid(), node_id=self.node.id, + address='52:54:00:cf:2d:4%s' % i, name='SetUp_node_pg%s' % i) + res = self.dbapi.get_portgroup_list(conductor_groups=['group_a']) + res_uuids = [r.uuid for r in res] + self.assertJsonEqual(group_a_uuids, res_uuids) + res = self.dbapi.get_portgroup_list(conductor_groups=['group_b']) + res_uuids = [r.uuid for r in res] + self.assertJsonEqual(group_b_uuids, res_uuids) + res = self.dbapi.get_portgroup_list( + conductor_groups=['group_a', 'group_b']) + res_uuids = [r.uuid for r in res] + self.assertJsonEqual(group_a_uuids + group_b_uuids, res_uuids) + def test_destroy_portgroup(self): self.dbapi.destroy_portgroup(self.portgroup.id) self.assertRaises(exception.PortgroupNotFound, diff --git a/ironic/tests/unit/db/test_ports.py b/ironic/tests/unit/db/test_ports.py index 54901d4390..dded9aa8ee 100644 --- a/ironic/tests/unit/db/test_ports.py +++ b/ironic/tests/unit/db/test_ports.py @@ -165,6 +165,39 @@ class DbPortTestCase(base.DbTestCase): res_uuids = [r.uuid for r in res] self.assertCountEqual(uuids, res_uuids) + def test_get_port_list_filter_by_conductor_groups(self): + group_a_node = db_utils.create_test_node( + uuid=uuidutils.generate_uuid(), conductor_group='group_a') + group_b_node = db_utils.create_test_node( + uuid=uuidutils.generate_uuid(), conductor_group='group_b') + + group_a_uuids = [] + group_b_uuids = [] + for i in range(1, 3): + port = db_utils.create_test_port(uuid=uuidutils.generate_uuid(), + node_id=group_a_node.id, + address='52:54:00:cf:2d:4%s' % i) + group_a_uuids.append(str(port.uuid)) + for i in range(7, 9): + port = db_utils.create_test_port(uuid=uuidutils.generate_uuid(), + node_id=group_b_node.id, + address='52:54:00:cf:2d:4%s' % i) + group_b_uuids.append(str(port.uuid)) + for i in range(4, 6): + port = db_utils.create_test_port(uuid=uuidutils.generate_uuid(), + node_id=self.node.id, + address='52:54:00:cf:2d:4%s' % i) + res = self.dbapi.get_port_list(conductor_groups=['group_a']) + res_uuids = [r.uuid for r in res] + self.assertJsonEqual(group_a_uuids, res_uuids) + res = self.dbapi.get_port_list(conductor_groups=['group_b']) + res_uuids = [r.uuid for r in res] + self.assertJsonEqual(group_b_uuids, res_uuids) + res = self.dbapi.get_port_list( + conductor_groups=['group_a', 'group_b']) + res_uuids = [r.uuid for r in res] + self.assertJsonEqual(group_a_uuids + group_b_uuids, res_uuids) + def test_get_ports_by_node_id(self): res = self.dbapi.get_ports_by_node_id(self.node.id) self.assertEqual(self.port.address, res[0].address) diff --git a/ironic/tests/unit/objects/test_port.py b/ironic/tests/unit/objects/test_port.py index 0d347537dd..eddea00b59 100644 --- a/ironic/tests/unit/objects/test_port.py +++ b/ironic/tests/unit/objects/test_port.py @@ -167,7 +167,7 @@ class TestPortObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): self.assertEqual(self.context, ports[0]._context) mock_get_list.assert_called_once_with( limit=None, marker=None, project=None, sort_dir=None, - sort_key=None, filters=None) + sort_key=None, conductor_groups=None, filters=None) def test_list_deprecated_owner(self): with mock.patch.object(self.dbapi, 'get_port_list', @@ -180,7 +180,7 @@ class TestPortObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): self.assertEqual(self.context, ports[0]._context) mock_get_list.assert_called_once_with( limit=None, marker=None, project='12345', sort_dir=None, - sort_key=None, filters=None) + sort_key=None, conductor_groups=None, filters=None) @mock.patch.object(obj_base.IronicObject, 'supports_version', spec_set=types.FunctionType) diff --git a/releasenotes/notes/add-port-portgroup-conductor-group-filter-2fe67c18f5a99265.yaml b/releasenotes/notes/add-port-portgroup-conductor-group-filter-2fe67c18f5a99265.yaml new file mode 100644 index 0000000000..739ed28c81 --- /dev/null +++ b/releasenotes/notes/add-port-portgroup-conductor-group-filter-2fe67c18f5a99265.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + It is now possible to filter by conductor groups when listing ports and + portgroups. For example, the following request returns only the ports for + nodes in conductor groups ``bear`` and ``metal``:: + + GET /v1/ports?conductor_groups=bear,metal +