diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index e29ce0f7a8..8dac99d684 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -186,6 +186,10 @@ class StackController(object): 'show_deleted': 'single', 'show_nested': 'single', 'show_hidden': 'single', + 'tags': 'single', + 'tags_any': 'single', + 'not_tags': 'single', + 'not_tags_any': 'single', } params = util.get_allowed_params(req.params, whitelist) filter_params = util.get_allowed_params(req.params, filter_whitelist) @@ -212,6 +216,30 @@ class StackController(object): params[rpc_api.PARAM_SHOW_HIDDEN]) show_hidden = params[rpc_api.PARAM_SHOW_HIDDEN] + tags = None + if rpc_api.PARAM_TAGS in params: + params[rpc_api.PARAM_TAGS] = param_utils.extract_tags( + params[rpc_api.PARAM_TAGS]) + tags = params[rpc_api.PARAM_TAGS] + + tags_any = None + if rpc_api.PARAM_TAGS_ANY in params: + params[rpc_api.PARAM_TAGS_ANY] = param_utils.extract_tags( + params[rpc_api.PARAM_TAGS_ANY]) + tags_any = params[rpc_api.PARAM_TAGS_ANY] + + not_tags = None + if rpc_api.PARAM_NOT_TAGS in params: + params[rpc_api.PARAM_NOT_TAGS] = param_utils.extract_tags( + params[rpc_api.PARAM_NOT_TAGS]) + not_tags = params[rpc_api.PARAM_NOT_TAGS] + + not_tags_any = None + if rpc_api.PARAM_NOT_TAGS_ANY in params: + params[rpc_api.PARAM_NOT_TAGS_ANY] = param_utils.extract_tags( + params[rpc_api.PARAM_NOT_TAGS_ANY]) + not_tags_any = params[rpc_api.PARAM_NOT_TAGS_ANY] + # get the with_count value, if invalid, raise ValueError with_count = False if req.params.get('with_count'): @@ -236,7 +264,11 @@ class StackController(object): tenant_safe=tenant_safe, show_deleted=show_deleted, show_nested=show_nested, - show_hidden=show_hidden) + show_hidden=show_hidden, + tags=tags, + tags_any=tags_any, + not_tags=not_tags, + not_tags_any=not_tags_any) except AttributeError as exc: LOG.warn(_LW("Old Engine Version: %s") % exc) diff --git a/heat/common/param_utils.py b/heat/common/param_utils.py index af94e1d5df..3f2e44d40d 100644 --- a/heat/common/param_utils.py +++ b/heat/common/param_utils.py @@ -53,3 +53,12 @@ def extract_int(name, value, allow_zero=True, allow_negative=False): {'name': name, 'value': value}) return result + + +def extract_tags(subject): + tags = subject.split(',') + for tag in tags: + if len(tag) > 80: + raise ValueError(_('Invalid tag, "%s" is longer than 80 ' + 'characters') % tag) + return tags diff --git a/heat/db/api.py b/heat/db/api.py index d79effaae6..a2bf0378e2 100644 --- a/heat/db/api.py +++ b/heat/db/api.py @@ -142,10 +142,13 @@ def stack_get_by_name(context, stack_name): def stack_get_all(context, limit=None, sort_keys=None, marker=None, sort_dir=None, filters=None, tenant_safe=True, - show_deleted=False, show_nested=False, show_hidden=False): + show_deleted=False, show_nested=False, show_hidden=False, + tags=None, tags_any=None, not_tags=None, + not_tags_any=None): return IMPL.stack_get_all(context, limit, sort_keys, marker, sort_dir, filters, tenant_safe, - show_deleted, show_nested, show_hidden) + show_deleted, show_nested, show_hidden, + tags, tags_any, not_tags, not_tags_any) def stack_get_all_by_owner_id(context, owner_id): @@ -153,12 +156,18 @@ def stack_get_all_by_owner_id(context, owner_id): def stack_count_all(context, filters=None, tenant_safe=True, - show_deleted=False, show_nested=False, show_hidden=False): + show_deleted=False, show_nested=False, show_hidden=False, + tags=None, tags_any=None, not_tags=None, + not_tags_any=None): return IMPL.stack_count_all(context, filters=filters, tenant_safe=tenant_safe, show_deleted=show_deleted, show_nested=show_nested, - show_hidden=show_hidden) + show_hidden=show_hidden, + tags=tags, + tags_any=tags_any, + not_tags=not_tags, + not_tags_any=not_tags_any) def stack_create(context, values): diff --git a/heat/db/sqlalchemy/api.py b/heat/db/sqlalchemy/api.py index 042ce6ca22..9e662a90e0 100644 --- a/heat/db/sqlalchemy/api.py +++ b/heat/db/sqlalchemy/api.py @@ -23,6 +23,7 @@ import osprofiler.sqlalchemy import six import sqlalchemy from sqlalchemy import orm +from sqlalchemy.orm import aliased as orm_aliased from sqlalchemy.orm import session as orm_session from heat.common import crypt @@ -399,7 +400,8 @@ def _paginate_query(context, query, model, limit=None, sort_keys=None, def _query_stack_get_all(context, tenant_safe=True, show_deleted=False, - show_nested=False, show_hidden=False): + show_nested=False, show_hidden=False, tags=None, + tags_any=None, not_tags=None, not_tags_any=None): if show_nested: query = soft_delete_aware_query( context, models.Stack, show_deleted=show_deleted @@ -412,6 +414,33 @@ def _query_stack_get_all(context, tenant_safe=True, show_deleted=False, if tenant_safe: query = query.filter_by(tenant=context.tenant_id) + if tags: + for tag in tags: + tag_alias = orm_aliased(models.StackTag) + query = query.join(tag_alias, models.Stack.tags) + query = query.filter(tag_alias.tag == tag) + + if tags_any: + query = query.filter( + models.Stack.tags.any( + models.StackTag.tag.in_(tags_any))) + + if not_tags: + subquery = soft_delete_aware_query( + context, models.Stack, show_deleted=show_deleted + ) + for tag in not_tags: + tag_alias = orm_aliased(models.StackTag) + subquery = subquery.join(tag_alias, models.Stack.tags) + subquery = subquery.filter(tag_alias.tag == tag) + not_stack_ids = [s.id for s in subquery.all()] + query = query.filter(models.Stack.id.notin_(not_stack_ids)) + + if not_tags_any: + query = query.filter( + ~models.Stack.tags.any( + models.StackTag.tag.in_(not_tags_any))) + if not show_hidden: query = query.filter( ~models.Stack.tags.any( @@ -422,11 +451,15 @@ def _query_stack_get_all(context, tenant_safe=True, show_deleted=False, def stack_get_all(context, limit=None, sort_keys=None, marker=None, sort_dir=None, filters=None, tenant_safe=True, - show_deleted=False, show_nested=False, show_hidden=False): + show_deleted=False, show_nested=False, show_hidden=False, + tags=None, tags_any=None, not_tags=None, + not_tags_any=None): query = _query_stack_get_all(context, tenant_safe, show_deleted=show_deleted, show_nested=show_nested, - show_hidden=show_hidden) + show_hidden=show_hidden, tags=tags, + tags_any=tags_any, not_tags=not_tags, + not_tags_any=not_tags_any) return _filter_and_page_query(context, query, limit, sort_keys, marker, sort_dir, filters).all() @@ -448,11 +481,15 @@ def _filter_and_page_query(context, query, limit=None, sort_keys=None, def stack_count_all(context, filters=None, tenant_safe=True, - show_deleted=False, show_nested=False, show_hidden=False): + show_deleted=False, show_nested=False, show_hidden=False, + tags=None, tags_any=None, not_tags=None, + not_tags_any=None): query = _query_stack_get_all(context, tenant_safe=tenant_safe, show_deleted=show_deleted, show_nested=show_nested, - show_hidden=show_hidden) + show_hidden=show_hidden, tags=tags, + tags_any=tags_any, not_tags=not_tags, + not_tags_any=not_tags_any) query = db_filters.exact_filter(query, models.Stack, filters) return query.count() diff --git a/heat/engine/service.py b/heat/engine/service.py index f6ccbee2d6..93031b5172 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -266,7 +266,7 @@ class EngineService(service.Service): by the RPC caller. """ - RPC_API_VERSION = '1.7' + RPC_API_VERSION = '1.8' def __init__(self, host, topic, manager=None): super(EngineService, self).__init__() @@ -467,7 +467,9 @@ class EngineService(service.Service): @context.request_context def list_stacks(self, cnxt, limit=None, marker=None, sort_keys=None, sort_dir=None, filters=None, tenant_safe=True, - show_deleted=False, show_nested=False, show_hidden=False): + show_deleted=False, show_nested=False, show_hidden=False, + tags=None, tags_any=None, not_tags=None, + not_tags_any=None): """ The list_stacks method returns attributes of all stacks. It supports pagination (``limit`` and ``marker``), sorting (``sort_keys`` and @@ -483,18 +485,31 @@ class EngineService(service.Service): :param show_deleted: if true, show soft-deleted stacks :param show_nested: if true, show nested stacks :param show_hidden: if true, show hidden stacks + :param tags: show stacks containing these tags, combine multiple + tags using the boolean AND expression + :param tags_any: show stacks containing these tags, combine multiple + tags using the boolean OR expression + :param not_tags: show stacks not containing these tags, combine + multiple tags using the boolean AND expression + :param not_tags_any: show stacks not containing these tags, combine + multiple tags using the boolean OR expression :returns: a list of formatted stacks """ stacks = parser.Stack.load_all(cnxt, limit, marker, sort_keys, sort_dir, filters, tenant_safe, show_deleted, resolve_data=False, show_nested=show_nested, - show_hidden=show_hidden) + show_hidden=show_hidden, + tags=tags, tags_any=tags_any, + not_tags=not_tags, + not_tags_any=not_tags_any) return [api.format_stack(stack) for stack in stacks] @context.request_context def count_stacks(self, cnxt, filters=None, tenant_safe=True, - show_deleted=False, show_nested=False, show_hidden=False): + show_deleted=False, show_nested=False, show_hidden=False, + tags=None, tags_any=None, not_tags=None, + not_tags_any=None): """ Return the number of stacks that match the given filters :param cnxt: RPC context. @@ -502,7 +517,15 @@ class EngineService(service.Service): :param tenant_safe: if true, scope the request by the current tenant :param show_deleted: if true, count will include the deleted stacks :param show_nested: if true, count will include nested stacks - :param show_hidden: if true, show hidden stacks + :param show_hidden: if true, count will include hidden stacks + :param tags: count stacks containing these tags, combine multiple tags + using the boolean AND expression + :param tags_any: count stacks containing these tags, combine multiple + tags using the boolean OR expression + :param not_tags: count stacks not containing these tags, combine + multiple tags using the boolean AND expression + :param not_tags_any: count stacks not containing these tags, combine + multiple tags using the boolean OR expression :returns: a integer representing the number of matched stacks """ return stack_object.Stack.count_all( @@ -511,7 +534,11 @@ class EngineService(service.Service): tenant_safe=tenant_safe, show_deleted=show_deleted, show_nested=show_nested, - show_hidden=show_hidden) + show_hidden=show_hidden, + tags=tags, + tags_any=tags_any, + not_tags=not_tags, + not_tags_any=not_tags_any) def _validate_deferred_auth_context(self, cnxt, stack): if cfg.CONF.deferred_auth_method != 'password': diff --git a/heat/engine/stack.py b/heat/engine/stack.py index 08fced4220..13633186a8 100755 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -354,7 +354,8 @@ class Stack(collections.Mapping): def load_all(cls, context, limit=None, marker=None, sort_keys=None, sort_dir=None, filters=None, tenant_safe=True, show_deleted=False, resolve_data=True, - show_nested=False, show_hidden=False): + show_nested=False, show_hidden=False, tags=None, + tags_any=None, not_tags=None, not_tags_any=None): stacks = stack_object.Stack.get_all( context, limit, @@ -365,7 +366,11 @@ class Stack(collections.Mapping): tenant_safe, show_deleted, show_nested, - show_hidden) or [] + show_hidden, + tags, + tags_any, + not_tags, + not_tags_any) or [] for stack in stacks: yield cls._from_db(context, stack, resolve_data=resolve_data) diff --git a/heat/rpc/api.py b/heat/rpc/api.py index 67e2be1444..c7bffb9994 100644 --- a/heat/rpc/api.py +++ b/heat/rpc/api.py @@ -18,12 +18,14 @@ PARAM_KEYS = ( PARAM_TIMEOUT, PARAM_DISABLE_ROLLBACK, PARAM_ADOPT_STACK_DATA, PARAM_SHOW_DELETED, PARAM_SHOW_NESTED, PARAM_EXISTING, PARAM_CLEAR_PARAMETERS, PARAM_GLOBAL_TENANT, PARAM_LIMIT, - PARAM_NESTED_DEPTH, PARAM_TAGS, PARAM_SHOW_HIDDEN + PARAM_NESTED_DEPTH, PARAM_TAGS, PARAM_SHOW_HIDDEN, PARAM_TAGS_ANY, + PARAM_NOT_TAGS, PARAM_NOT_TAGS_ANY ) = ( 'timeout_mins', 'disable_rollback', 'adopt_stack_data', 'show_deleted', 'show_nested', 'existing', 'clear_parameters', 'global_tenant', 'limit', - 'nested_depth', 'tags', 'show_hidden' + 'nested_depth', 'tags', 'show_hidden', 'tags_any', + 'not_tags', 'not_tags_any' ) STACK_KEYS = ( diff --git a/heat/rpc/client.py b/heat/rpc/client.py index 57feb40403..ecfdadc90e 100644 --- a/heat/rpc/client.py +++ b/heat/rpc/client.py @@ -91,7 +91,9 @@ class EngineClient(object): def list_stacks(self, ctxt, limit=None, marker=None, sort_keys=None, sort_dir=None, filters=None, tenant_safe=True, - show_deleted=False, show_nested=False, show_hidden=False): + show_deleted=False, show_nested=False, show_hidden=False, + tags=None, tags_any=None, not_tags=None, + not_tags_any=None): """ The list_stacks method returns attributes of all stacks. It supports pagination (``limit`` and ``marker``), sorting (``sort_keys`` and @@ -107,6 +109,14 @@ class EngineClient(object): :param show_deleted: if true, show soft-deleted stacks :param show_nested: if true, show nested stacks :param show_hidden: if true, show hidden stacks + :param tags: show stacks containing these tags, combine multiple + tags using the boolean AND expression + :param tags_any: show stacks containing these tags, combine multiple + tags using the boolean OR expression + :param not_tags: show stacks not containing these tags, combine + multiple tags using the boolean AND expression + :param not_tags_any: show stacks not containing these tags, combine + multiple tags using the boolean OR expression :returns: a list of stacks """ return self.call(ctxt, @@ -116,10 +126,16 @@ class EngineClient(object): tenant_safe=tenant_safe, show_deleted=show_deleted, show_nested=show_nested, - show_hidden=show_hidden)) + show_hidden=show_hidden, + tags=tags, tags_any=tags_any, + not_tags=not_tags, + not_tags_any=not_tags_any), + version='1.8') def count_stacks(self, ctxt, filters=None, tenant_safe=True, - show_deleted=False, show_nested=False, show_hidden=False): + show_deleted=False, show_nested=False, show_hidden=False, + tags=None, tags_any=None, not_tags=None, + not_tags_any=None): """ Return the number of stacks that match the given filters :param ctxt: RPC context. @@ -127,7 +143,15 @@ class EngineClient(object): :param tenant_safe: if true, scope the request by the current tenant :param show_deleted: if true, count will include the deleted stacks :param show_nested: if true, count will include nested stacks - :param show_hidden: if true, show hidden stacks + :param show_hidden: if true, count will include hidden stacks + :param tags: count stacks containing these tags, combine multiple tags + using the boolean AND expression + :param tags_any: count stacks containing these tags, combine multiple + tags using the boolean OR expression + :param not_tags: count stacks not containing these tags, combine + multiple tags using the boolean AND expression + :param not_tags_any: count stacks not containing these tags, combine + multiple tags using the boolean OR expression :returns: a integer representing the number of matched stacks """ return self.call(ctxt, self.make_msg('count_stacks', @@ -135,7 +159,12 @@ class EngineClient(object): tenant_safe=tenant_safe, show_deleted=show_deleted, show_nested=show_nested, - show_hidden=show_hidden)) + show_hidden=show_hidden, + tags=tags, + tags_any=tags_any, + not_tags=not_tags, + not_tags_any=not_tags_any), + version='1.8') def show_stack(self, ctxt, stack_identity): """ @@ -205,7 +234,7 @@ class EngineClient(object): user_creds_id=user_creds_id, stack_user_project_id=stack_user_project_id, parent_resource_name=parent_resource_name), - version='1.7') + version='1.8') def update_stack(self, ctxt, stack_identity, template, params, files, args): diff --git a/heat/tests/db/test_sqlalchemy_api.py b/heat/tests/db/test_sqlalchemy_api.py index 146bac94d4..0d778e542e 100644 --- a/heat/tests/db/test_sqlalchemy_api.py +++ b/heat/tests/db/test_sqlalchemy_api.py @@ -574,6 +574,113 @@ class SqlAlchemyTest(common.HeatTestCase): for stack in st_db_visible: self.assertNotEqual(stacks[0].id, stack.id) + def test_stack_get_all_by_tags(self): + stacks = [self._setup_test_stack('stack', x)[1] for x in UUIDs] + stacks[0].tags = ['tag1'] + stacks[0].store() + stacks[1].tags = ['tag1', 'tag2'] + stacks[1].store() + stacks[2].tags = ['tag1', 'tag2', 'tag3'] + stacks[2].store() + + st_db = db_api.stack_get_all(self.ctx, tags=['tag2']) + self.assertEqual(2, len(st_db)) + + st_db = db_api.stack_get_all(self.ctx, tags=['tag1', 'tag2']) + self.assertEqual(2, len(st_db)) + + st_db = db_api.stack_get_all(self.ctx, tags=['tag1', 'tag2', 'tag3']) + self.assertEqual(1, len(st_db)) + + def test_stack_get_all_by_tags_any(self): + stacks = [self._setup_test_stack('stack', x)[1] for x in UUIDs] + stacks[0].tags = ['tag2'] + stacks[0].store() + stacks[1].tags = ['tag1', 'tag2'] + stacks[1].store() + stacks[2].tags = ['tag1', 'tag3'] + stacks[2].store() + + st_db = db_api.stack_get_all(self.ctx, tags_any=['tag1']) + self.assertEqual(2, len(st_db)) + + st_db = db_api.stack_get_all(self.ctx, tags_any=['tag1', 'tag2', + 'tag3']) + self.assertEqual(3, len(st_db)) + + def test_stack_get_all_by_not_tags(self): + stacks = [self._setup_test_stack('stack', x)[1] for x in UUIDs] + stacks[0].tags = ['tag1'] + stacks[0].store() + stacks[1].tags = ['tag1', 'tag2'] + stacks[1].store() + stacks[2].tags = ['tag1', 'tag2', 'tag3'] + stacks[2].store() + + st_db = db_api.stack_get_all(self.ctx, not_tags=['tag2']) + self.assertEqual(1, len(st_db)) + + st_db = db_api.stack_get_all(self.ctx, not_tags=['tag1', 'tag2']) + self.assertEqual(1, len(st_db)) + + st_db = db_api.stack_get_all(self.ctx, not_tags=['tag1', 'tag2', + 'tag3']) + self.assertEqual(2, len(st_db)) + + def test_stack_get_all_by_not_tags_any(self): + stacks = [self._setup_test_stack('stack', x)[1] for x in UUIDs] + stacks[0].tags = ['tag2'] + stacks[0].store() + stacks[1].tags = ['tag1', 'tag2'] + stacks[1].store() + stacks[2].tags = ['tag1', 'tag3'] + stacks[2].store() + + st_db = db_api.stack_get_all(self.ctx, not_tags_any=['tag1']) + self.assertEqual(1, len(st_db)) + + st_db = db_api.stack_get_all(self.ctx, not_tags_any=['tag1', 'tag2', + 'tag3']) + self.assertEqual(0, len(st_db)) + + def test_stack_get_all_by_tag_with_pagination(self): + stacks = [self._setup_test_stack('stack', x)[1] for x in UUIDs] + stacks[0].tags = ['tag1'] + stacks[0].store() + stacks[1].tags = ['tag2'] + stacks[1].store() + stacks[2].tags = ['tag1'] + stacks[2].store() + + st_db = db_api.stack_get_all(self.ctx, tags=['tag1']) + self.assertEqual(2, len(st_db)) + + st_db = db_api.stack_get_all(self.ctx, tags=['tag1'], limit=1) + self.assertEqual(1, len(st_db)) + self.assertEqual(stacks[2].id, st_db[0].id) + + st_db = db_api.stack_get_all(self.ctx, tags=['tag1'], limit=1, + marker=stacks[2].id) + self.assertEqual(1, len(st_db)) + self.assertEqual(stacks[0].id, st_db[0].id) + + def test_stack_get_all_by_tag_with_show_hidden(self): + cfg.CONF.set_override('hidden_stack_tags', ['hidden']) + + stacks = [self._setup_test_stack('stack', x)[1] for x in UUIDs] + stacks[0].tags = ['tag1'] + stacks[0].store() + stacks[1].tags = ['hidden', 'tag1'] + stacks[1].store() + + st_db = db_api.stack_get_all(self.ctx, tags=['tag1'], + show_hidden=True) + self.assertEqual(2, len(st_db)) + + st_db = db_api.stack_get_all(self.ctx, tags=['tag1'], + show_hidden=False) + self.assertEqual(1, len(st_db)) + def test_stack_count_all(self): stacks = [self._setup_test_stack('stack', x)[1] for x in UUIDs] @@ -609,6 +716,38 @@ class SqlAlchemyTest(common.HeatTestCase): st_db_visible = db_api.stack_count_all(self.ctx, show_hidden=False) self.assertEqual(2, st_db_visible) + def test_count_all_by_tags(self): + stacks = [self._setup_test_stack('stack', x)[1] for x in UUIDs] + stacks[0].tags = ['tag1'] + stacks[0].store() + stacks[1].tags = ['tag2'] + stacks[1].store() + stacks[2].tags = ['tag2'] + stacks[2].store() + + st_db = db_api.stack_count_all(self.ctx, tags=['tag1']) + self.assertEqual(1, st_db) + + st_db = db_api.stack_count_all(self.ctx, tags=['tag2']) + self.assertEqual(2, st_db) + + def test_count_all_by_tag_with_show_hidden(self): + cfg.CONF.set_override('hidden_stack_tags', ['hidden']) + + stacks = [self._setup_test_stack('stack', x)[1] for x in UUIDs] + stacks[0].tags = ['tag1'] + stacks[0].store() + stacks[1].tags = ['hidden', 'tag1'] + stacks[1].store() + + st_db = db_api.stack_count_all(self.ctx, tags=['tag1'], + show_hidden=True) + self.assertEqual(2, st_db) + + st_db = db_api.stack_count_all(self.ctx, tags=['tag1'], + show_hidden=False) + self.assertEqual(1, st_db) + def test_stack_count_all_with_filters(self): self._setup_test_stack('foo', UUID1) self._setup_test_stack('bar', UUID2) diff --git a/heat/tests/test_api_cfn_v1.py b/heat/tests/test_api_cfn_v1.py index e305318612..d84d27f63a 100644 --- a/heat/tests/test_api_cfn_v1.py +++ b/heat/tests/test_api_cfn_v1.py @@ -162,9 +162,11 @@ class CfnStackControllerTest(common.HeatTestCase): default_args = {'limit': None, 'sort_keys': None, 'marker': None, 'sort_dir': None, 'filters': None, 'tenant_safe': True, 'show_deleted': False, 'show_nested': False, - 'show_hidden': False} + 'show_hidden': False, 'tags': None, + 'tags_any': None, 'not_tags': None, + 'not_tags_any': None} mock_call.assert_called_once_with( - dummy_req.context, ('list_stacks', default_args)) + dummy_req.context, ('list_stacks', default_args), version='1.8') @mock.patch.object(rpc_client.EngineClient, 'call') def test_list_rmt_aterr(self, mock_call): @@ -180,7 +182,7 @@ class CfnStackControllerTest(common.HeatTestCase): result = self.controller.list(dummy_req) self.assertIsInstance(result, exception.HeatInvalidParameterValueError) mock_call.assert_called_once_with( - dummy_req.context, ('list_stacks', mock.ANY)) + dummy_req.context, ('list_stacks', mock.ANY), version='1.8') @mock.patch.object(rpc_client.EngineClient, 'call') def test_list_rmt_interr(self, mock_call): @@ -196,7 +198,7 @@ class CfnStackControllerTest(common.HeatTestCase): result = self.controller.list(dummy_req) self.assertIsInstance(result, exception.HeatInternalFailureError) mock_call.assert_called_once_with( - dummy_req.context, ('list_stacks', mock.ANY)) + dummy_req.context, ('list_stacks', mock.ANY), version='1.8') def test_describe_last_updated_time(self): params = {'Action': 'DescribeStacks'} @@ -508,7 +510,7 @@ class CfnStackControllerTest(common.HeatTestCase): 'user_creds_id': None, 'parent_resource_name': None, 'stack_user_project_id': None}), - version='1.7' + version='1.8' ).AndRaise(failure) def _stub_rpc_create_stack_call_success(self, stack_name, engine_parms, @@ -536,7 +538,7 @@ class CfnStackControllerTest(common.HeatTestCase): 'user_creds_id': None, 'parent_resource_name': None, 'stack_user_project_id': None}), - version='1.7' + version='1.8' ).AndReturn(engine_resp) self.m.ReplayAll() diff --git a/heat/tests/test_api_openstack_v1.py b/heat/tests/test_api_openstack_v1.py index 3fe0240633..ac7811aecd 100644 --- a/heat/tests/test_api_openstack_v1.py +++ b/heat/tests/test_api_openstack_v1.py @@ -386,9 +386,11 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): default_args = {'limit': None, 'sort_keys': None, 'marker': None, 'sort_dir': None, 'filters': None, 'tenant_safe': True, 'show_deleted': False, 'show_nested': False, - 'show_hidden': False} + 'show_hidden': False, 'tags': None, + 'tags_any': None, 'not_tags': None, + 'not_tags_any': None} mock_call.assert_called_once_with( - req.context, ('list_stacks', default_args)) + req.context, ('list_stacks', default_args), version='1.8') @mock.patch.object(rpc_client.EngineClient, 'call') def test_index_whitelists_pagination_params(self, mock_call, mock_enforce): @@ -407,7 +409,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): rpc_call_args, _ = mock_call.call_args engine_args = rpc_call_args[1][1] - self.assertEqual(9, len(engine_args)) + self.assertEqual(13, len(engine_args)) self.assertIn('limit', engine_args) self.assertIn('sort_keys', engine_args) self.assertIn('marker', engine_args) @@ -608,7 +610,11 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): tenant_safe=True, show_deleted=True, show_nested=False, - show_hidden=False) + show_hidden=False, + tags=None, + tags_any=None, + not_tags=None, + not_tags_any=None) @mock.patch.object(rpc_client.EngineClient, 'call') def test_detail(self, mock_call, mock_enforce): @@ -667,9 +673,11 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): default_args = {'limit': None, 'sort_keys': None, 'marker': None, 'sort_dir': None, 'filters': None, 'tenant_safe': True, 'show_deleted': False, 'show_nested': False, - 'show_hidden': False} + 'show_hidden': False, 'tags': None, + 'tags_any': None, 'not_tags': None, + 'not_tags_any': None} mock_call.assert_called_once_with( - req.context, ('list_stacks', default_args)) + req.context, ('list_stacks', default_args), version='1.8') @mock.patch.object(rpc_client.EngineClient, 'call') def test_index_rmt_aterr(self, mock_call, mock_enforce): @@ -685,7 +693,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): self.assertEqual(400, resp.json['code']) self.assertEqual('AttributeError', resp.json['error']['type']) mock_call.assert_called_once_with( - req.context, ('list_stacks', mock.ANY)) + req.context, ('list_stacks', mock.ANY), version='1.8') def test_index_err_denied_policy(self, mock_enforce): self._mock_enforce_setup(mock_enforce, 'index', False) @@ -713,7 +721,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): self.assertEqual(500, resp.json['code']) self.assertEqual('Exception', resp.json['error']['type']) mock_call.assert_called_once_with( - req.context, ('list_stacks', mock.ANY)) + req.context, ('list_stacks', mock.ANY), version='1.8') def test_create(self, mock_enforce): self._mock_enforce_setup(mock_enforce, 'create', True) @@ -743,7 +751,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): 'user_creds_id': None, 'parent_resource_name': None, 'stack_user_project_id': None}), - version='1.7' + version='1.8' ).AndReturn(dict(identity)) self.m.ReplayAll() @@ -806,7 +814,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): 'user_creds_id': None, 'parent_resource_name': None, 'stack_user_project_id': None}), - version='1.7' + version='1.8' ).AndReturn(dict(identity)) self.m.ReplayAll() @@ -893,7 +901,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): 'user_creds_id': None, 'parent_resource_name': None, 'stack_user_project_id': None}), - version='1.7' + version='1.8' ).AndReturn(dict(identity)) self.m.ReplayAll() @@ -937,7 +945,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): 'user_creds_id': None, 'parent_resource_name': None, 'stack_user_project_id': None}), - version='1.7' + version='1.8' ).AndRaise(to_remote_error(AttributeError())) rpc_client.EngineClient.call( req.context, @@ -954,7 +962,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): 'user_creds_id': None, 'parent_resource_name': None, 'stack_user_project_id': None}), - version='1.7' + version='1.8' ).AndRaise(to_remote_error(unknown_parameter)) rpc_client.EngineClient.call( req.context, @@ -971,7 +979,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): 'user_creds_id': None, 'parent_resource_name': None, 'stack_user_project_id': None}), - version='1.7' + version='1.8' ).AndRaise(to_remote_error(missing_parameter)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, @@ -1025,7 +1033,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): 'user_creds_id': None, 'parent_resource_name': None, 'stack_user_project_id': None}), - version='1.7' + version='1.8' ).AndRaise(to_remote_error(error)) self.m.ReplayAll() @@ -1106,7 +1114,7 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): 'user_creds_id': None, 'parent_resource_name': None, 'stack_user_project_id': None}), - version='1.7' + version='1.8' ).AndRaise(to_remote_error(error)) self.m.ReplayAll() diff --git a/heat/tests/test_common_param_utils.py b/heat/tests/test_common_param_utils.py index 29b4970418..c4c6cfb690 100644 --- a/heat/tests/test_common_param_utils.py +++ b/heat/tests/test_common_param_utils.py @@ -88,3 +88,11 @@ class TestExtractInt(common.HeatTestCase): param_utils.extract_int, 'num', 'true') self.assertRaises(ValueError, param_utils.extract_int, 'num', True) + + +class TestExtractTags(common.HeatTestCase): + def test_extract_tags(self): + self.assertRaises(ValueError, param_utils.extract_tags, "aaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaa,a") + self.assertEqual(["foo", "bar"], param_utils.extract_tags('foo,bar')) diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index dc661d7a15..4fdd760ed4 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -1736,7 +1736,7 @@ class StackServiceTest(common.HeatTestCase): def test_make_sure_rpc_version(self): self.assertEqual( - '1.7', + '1.8', service.EngineService.RPC_API_VERSION, ('RPC version is changed, please update this test to new version ' 'and make sure additional test cases are added for RPC APIs ' @@ -2064,6 +2064,10 @@ class StackServiceTest(common.HeatTestCase): mock.ANY, mock.ANY, mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, ) @mock.patch.object(stack_object.Stack, 'get_all') @@ -2080,6 +2084,10 @@ class StackServiceTest(common.HeatTestCase): mock.ANY, mock.ANY, mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, ) @mock.patch.object(stack_object.Stack, 'get_all') @@ -2095,6 +2103,10 @@ class StackServiceTest(common.HeatTestCase): mock.ANY, mock.ANY, mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, ) @mock.patch.object(stack_object.Stack, 'get_all') @@ -2110,6 +2122,10 @@ class StackServiceTest(common.HeatTestCase): mock.ANY, mock.ANY, mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, ) @mock.patch.object(stack_object.Stack, 'get_all') @@ -2125,6 +2141,10 @@ class StackServiceTest(common.HeatTestCase): mock.ANY, True, mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, ) @mock.patch.object(stack_object.Stack, 'get_all') @@ -2140,6 +2160,10 @@ class StackServiceTest(common.HeatTestCase): True, mock.ANY, mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, ) @mock.patch.object(stack_object.Stack, 'get_all') @@ -2155,6 +2179,86 @@ class StackServiceTest(common.HeatTestCase): mock.ANY, mock.ANY, True, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + ) + + @mock.patch.object(stack_object.Stack, 'get_all') + def test_stack_list_tags(self, mock_stack_get_all): + self.eng.list_stacks(self.ctx, tags=['foo', 'bar']) + mock_stack_get_all.assert_called_once_with(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + ['foo', 'bar'], + mock.ANY, + mock.ANY, + mock.ANY, + ) + + @mock.patch.object(stack_object.Stack, 'get_all') + def test_stack_list_tags_any(self, mock_stack_get_all): + self.eng.list_stacks(self.ctx, tags_any=['foo', 'bar']) + mock_stack_get_all.assert_called_once_with(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + ['foo', 'bar'], + mock.ANY, + mock.ANY, + ) + + @mock.patch.object(stack_object.Stack, 'get_all') + def test_stack_list_not_tags(self, mock_stack_get_all): + self.eng.list_stacks(self.ctx, not_tags=['foo', 'bar']) + mock_stack_get_all.assert_called_once_with(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + ['foo', 'bar'], + mock.ANY, + ) + + @mock.patch.object(stack_object.Stack, 'get_all') + def test_stack_list_not_tags_any(self, mock_stack_get_all): + self.eng.list_stacks(self.ctx, not_tags_any=['foo', 'bar']) + mock_stack_get_all.assert_called_once_with(mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + mock.ANY, + ['foo', 'bar'], ) @mock.patch.object(stack_object.Stack, 'count_all') @@ -2165,7 +2269,11 @@ class StackServiceTest(common.HeatTestCase): tenant_safe=mock.ANY, show_deleted=False, show_nested=False, - show_hidden=False) + show_hidden=False, + tags=None, + tags_any=None, + not_tags=None, + not_tags_any=None) @mock.patch.object(stack_object.Stack, 'count_all') def test_count_stacks_tenant_safe_default_true(self, mock_stack_count_all): @@ -2175,7 +2283,11 @@ class StackServiceTest(common.HeatTestCase): tenant_safe=True, show_deleted=False, show_nested=False, - show_hidden=False) + show_hidden=False, + tags=None, + tags_any=None, + not_tags=None, + not_tags_any=None) @mock.patch.object(stack_object.Stack, 'count_all') def test_count_stacks_passes_tenant_safe_info(self, mock_stack_count_all): @@ -2185,7 +2297,11 @@ class StackServiceTest(common.HeatTestCase): tenant_safe=False, show_deleted=False, show_nested=False, - show_hidden=False) + show_hidden=False, + tags=None, + tags_any=None, + not_tags=None, + not_tags_any=None) @mock.patch.object(stack_object.Stack, 'count_all') def test_count_stacks_show_nested(self, mock_stack_count_all): @@ -2195,7 +2311,11 @@ class StackServiceTest(common.HeatTestCase): tenant_safe=True, show_deleted=False, show_nested=True, - show_hidden=False) + show_hidden=False, + tags=None, + tags_any=None, + not_tags=None, + not_tags_any=None) @mock.patch.object(stack_object.Stack, 'count_all') def test_count_stack_show_deleted(self, mock_stack_count_all): @@ -2205,7 +2325,11 @@ class StackServiceTest(common.HeatTestCase): tenant_safe=True, show_deleted=True, show_nested=False, - show_hidden=False) + show_hidden=False, + tags=None, + tags_any=None, + not_tags=None, + not_tags_any=None) @mock.patch.object(stack_object.Stack, 'count_all') def test_count_stack_show_hidden(self, mock_stack_count_all): @@ -2215,7 +2339,11 @@ class StackServiceTest(common.HeatTestCase): tenant_safe=True, show_deleted=False, show_nested=False, - show_hidden=True) + show_hidden=True, + tags=None, + tags_any=None, + not_tags=None, + not_tags_any=None) @stack_context('service_abandon_stack') def test_abandon_stack(self): diff --git a/heat/tests/test_rpc_client.py b/heat/tests/test_rpc_client.py index 5ed750f2e0..676944fb93 100644 --- a/heat/tests/test_rpc_client.py +++ b/heat/tests/test_rpc_client.py @@ -116,6 +116,10 @@ class EngineRpcAPITestCase(common.HeatTestCase): 'show_deleted': mock.ANY, 'show_nested': mock.ANY, 'show_hidden': mock.ANY, + 'tags': mock.ANY, + 'tags_any': mock.ANY, + 'not_tags': mock.ANY, + 'not_tags_any': mock.ANY, } self._test_engine_api('list_stacks', 'call', **default_args) @@ -126,6 +130,10 @@ class EngineRpcAPITestCase(common.HeatTestCase): 'show_deleted': mock.ANY, 'show_nested': mock.ANY, 'show_hidden': mock.ANY, + 'tags': mock.ANY, + 'tags_any': mock.ANY, + 'not_tags': mock.ANY, + 'not_tags_any': mock.ANY, } self._test_engine_api('count_stacks', 'call', **default_args)