Skip to content

Commit d326641

Browse files
ghugoDean Troyer
authored andcommitted
Add project tags functionality
This change adds tags functionality for projects in keystone. A user can add a single tag with "--tag", chain "--tag" to add multiple tags, or clear tags with "--no-tag". Change-Id: I31cfef3e76dcefe299dacb00c11bb1a10a252628 Partially-Implements: bp project-tags
1 parent 8c5f755 commit d326641

7 files changed

Lines changed: 278 additions & 5 deletions

File tree

doc/source/cli/command-objects/project.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Create new project
1919
[--enable | --disable]
2020
[--property <key=value>]
2121
[--or-show]
22+
[--tag <tag>]
2223
<name>
2324
2425
.. option:: --domain <domain>
@@ -56,6 +57,13 @@ Create new project
5657

5758
If the project already exists return the existing project data and do not fail.
5859

60+
.. option:: --tag
61+
62+
Add a tag to the project
63+
(repeat option to set multiple tags)
64+
65+
.. versionadded:: 3
66+
5967
.. _project_create-name:
6068
.. describe:: <name>
6169

@@ -98,6 +106,8 @@ List projects
98106
[--my-projects]
99107
[--long]
100108
[--sort <key>[:<direction>,<key>:<direction>,..]]
109+
[--tags <tag>[,<tag>,...]] [--tags-any <tag>[,<tag>,...]]
110+
[--not-tags <tag>[,<tag>,...]] [--not-tags-any <tag>[,<tag>,...]]
101111
102112
.. option:: --domain <domain>
103113
@@ -127,6 +137,30 @@ List projects
127137
multiple keys and directions can be specified --sort
128138
<key>[:<direction>,<key>:<direction>,..]
129139
140+
.. option:: --tags <tag>[,<tag>,...]
141+
142+
List projects which have all given tag(s)
143+
144+
.. versionadded:: 3
145+
146+
.. option:: --tags-any <tag>[,<tag>,...]
147+
148+
List projects which have any given tag(s)
149+
150+
.. versionadded:: 3
151+
152+
.. option:: --not-tags <tag>[,<tag>,...]
153+
154+
Exclude projects which have all given tag(s)
155+
156+
.. versionadded:: 3
157+
158+
.. option:: --not-tags-any <tag>[,<tag>,...]
159+
160+
Exclude projects which have any given tag(s)
161+
162+
.. versionadded:: 3
163+
130164
project set
131165
-----------
132166
@@ -141,6 +175,7 @@ Set project properties
141175
[--description <description>]
142176
[--enable | --disable]
143177
[--property <key=value>]
178+
[--tag <tag> | --clear-tags | --remove-tags <tag>]
144179
<project>
145180
146181
.. option:: --name <name>

openstackclient/identity/v3/project.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
from openstackclient.i18n import _
2828
from openstackclient.identity import common
29-
29+
from openstackclient.identity.v3 import tag
3030

3131
LOG = logging.getLogger(__name__)
3232

@@ -79,6 +79,7 @@ def get_parser(self, prog_name):
7979
action='store_true',
8080
help=_('Return existing project'),
8181
)
82+
tag.add_tag_option_to_parser_for_create(parser, _('project'))
8283
return parser
8384

8485
def take_action(self, parsed_args):
@@ -102,6 +103,7 @@ def take_action(self, parsed_args):
102103
kwargs = {}
103104
if parsed_args.property:
104105
kwargs = parsed_args.property.copy()
106+
kwargs['tags'] = list(set(parsed_args.tags))
105107

106108
try:
107109
project = identity_client.projects.create(
@@ -207,6 +209,7 @@ def get_parser(self, prog_name):
207209
'(default: asc), repeat this option to specify multiple '
208210
'keys and directions.'),
209211
)
212+
tag.add_tag_filtering_option_to_parser(parser, _('projects'))
210213
return parser
211214

212215
def take_action(self, parsed_args):
@@ -234,6 +237,8 @@ def take_action(self, parsed_args):
234237

235238
kwargs['user'] = user_id
236239

240+
tag.get_tag_filtering_args(parsed_args, kwargs)
241+
237242
if parsed_args.my_projects:
238243
# NOTE(adriant): my-projects supersedes all the other filters.
239244
kwargs = {'user': self.app.client_manager.auth_ref.user_id}
@@ -303,6 +308,7 @@ def get_parser(self, prog_name):
303308
help=_('Set a property on <project> '
304309
'(repeat option to set multiple properties)'),
305310
)
311+
tag.add_tag_option_to_parser_for_set(parser, _('project'))
306312
return parser
307313

308314
def take_action(self, parsed_args):
@@ -323,6 +329,7 @@ def take_action(self, parsed_args):
323329
kwargs['enabled'] = False
324330
if parsed_args.property:
325331
kwargs.update(parsed_args.property)
332+
tag.update_tags_in_args(parsed_args, project, kwargs)
326333

327334
identity_client.projects.update(project.id, **kwargs)
328335

openstackclient/identity/v3/tag.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
import argparse
14+
15+
from openstackclient.i18n import _
16+
17+
18+
class _CommaListAction(argparse.Action):
19+
20+
def __call__(self, parser, namespace, values, option_string=None):
21+
setattr(namespace, self.dest, values.split(','))
22+
23+
24+
def add_tag_filtering_option_to_parser(parser, collection_name):
25+
parser.add_argument(
26+
'--tags',
27+
metavar='<tag>[,<tag>,...]',
28+
action=_CommaListAction,
29+
help=_('List %s which have all given tag(s) '
30+
'(Comma-separated list of tags)') % collection_name
31+
)
32+
parser.add_argument(
33+
'--tags-any',
34+
metavar='<tag>[,<tag>,...]',
35+
action=_CommaListAction,
36+
help=_('List %s which have any given tag(s) '
37+
'(Comma-separated list of tags)') % collection_name
38+
)
39+
parser.add_argument(
40+
'--not-tags',
41+
metavar='<tag>[,<tag>,...]',
42+
action=_CommaListAction,
43+
help=_('Exclude %s which have all given tag(s) '
44+
'(Comma-separated list of tags)') % collection_name
45+
)
46+
parser.add_argument(
47+
'--not-tags-any',
48+
metavar='<tag>[,<tag>,...]',
49+
action=_CommaListAction,
50+
help=_('Exclude %s which have any given tag(s) '
51+
'(Comma-separated list of tags)') % collection_name
52+
)
53+
54+
55+
def get_tag_filtering_args(parsed_args, args):
56+
if parsed_args.tags:
57+
args['tags'] = ','.join(parsed_args.tags)
58+
if parsed_args.tags_any:
59+
args['tags-any'] = ','.join(parsed_args.tags_any)
60+
if parsed_args.not_tags:
61+
args['not-tags'] = ','.join(parsed_args.not_tags)
62+
if parsed_args.not_tags_any:
63+
args['not-tags-any'] = ','.join(parsed_args.not_tags_any)
64+
65+
66+
def add_tag_option_to_parser_for_create(parser, resource_name):
67+
tag_group = parser.add_mutually_exclusive_group()
68+
tag_group.add_argument(
69+
'--tag',
70+
action='append',
71+
dest='tags',
72+
metavar='<tag>',
73+
default=[],
74+
help=_('Tag to be added to the %s '
75+
'(repeat option to set multiple tags)') % resource_name
76+
)
77+
78+
79+
def add_tag_option_to_parser_for_set(parser, resource_name):
80+
parser.add_argument(
81+
'--tag',
82+
action='append',
83+
dest='tags',
84+
metavar='<tag>',
85+
default=[],
86+
help=_('Tag to be added to the %s '
87+
'(repeat option to set multiple tags)') % resource_name
88+
)
89+
parser.add_argument(
90+
'--clear-tags',
91+
action='store_true',
92+
help=_('Clear tags associated with the %s. Specify '
93+
'both --tag and --clear-tags to overwrite '
94+
'current tags') % resource_name
95+
)
96+
parser.add_argument(
97+
'--remove-tag',
98+
metavar='<tag>',
99+
default=[],
100+
help=_('Tag to be deleted from the %s '
101+
'(repeat option to delete multiple tags)') % resource_name
102+
)
103+
104+
105+
def update_tags_in_args(parsed_args, obj, args):
106+
if parsed_args.clear_tags:
107+
args['tags'] = []
108+
obj.tags = []
109+
if parsed_args.remove_tag:
110+
if parsed_args.remove_tag in obj.tags:
111+
obj.tags.remove(parsed_args.remove_tag)
112+
args['tags'] = list(set(obj.tags))
113+
return
114+
if parsed_args.tags:
115+
args['tags'] = list(set(obj.tags).union(
116+
set(parsed_args.tags)))

openstackclient/tests/unit/identity/v3/fakes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
'name': domain_name,
3535
'description': domain_description,
3636
'enabled': True,
37+
'tags': [],
3738
'links': base_url + 'domains/' + domain_id,
3839
}
3940

@@ -115,6 +116,7 @@
115116
'description': project_description,
116117
'enabled': True,
117118
'domain_id': domain_id,
119+
'tags': [],
118120
'links': base_url + 'projects/' + project_id,
119121
}
120122

@@ -124,6 +126,7 @@
124126
'description': project_description + 'plus four more',
125127
'enabled': True,
126128
'domain_id': domain_id,
129+
'tags': [],
127130
'links': base_url + 'projects/' + project_id,
128131
}
129132

@@ -145,6 +148,7 @@
145148
'enabled': True,
146149
'domain_id': domain_id,
147150
'parent_id': project_id,
151+
'tags': [],
148152
'links': base_url + 'projects/' + (project_id + '-with-parent'),
149153
}
150154

@@ -155,6 +159,7 @@
155159
'enabled': True,
156160
'domain_id': domain_id,
157161
'parent_id': PROJECT_WITH_PARENT['id'],
162+
'tags': [],
158163
'links': base_url + 'projects/' + (project_id + '-with-grandparent'),
159164
}
160165

@@ -619,6 +624,7 @@ def create_one_project(attrs=None):
619624
'is_domain': False,
620625
'domain_id': 'domain-id-' + uuid.uuid4().hex,
621626
'parent_id': 'parent-id-' + uuid.uuid4().hex,
627+
'tags': [],
622628
'links': 'links-' + uuid.uuid4().hex,
623629
}
624630
project_info.update(attrs)
@@ -666,6 +672,7 @@ def create_one_domain(attrs=None):
666672
'name': 'domain-name-' + uuid.uuid4().hex,
667673
'description': 'domain-description-' + uuid.uuid4().hex,
668674
'enabled': True,
675+
'tags': [],
669676
'links': 'links-' + uuid.uuid4().hex,
670677
}
671678
domain_info.update(attrs)

openstackclient/tests/unit/identity/v3/test_domain.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class TestDomainCreate(TestDomain):
3131
'enabled',
3232
'id',
3333
'name',
34+
'tags'
3435
)
3536

3637
def setUp(self):
@@ -43,6 +44,7 @@ def setUp(self):
4344
True,
4445
self.domain.id,
4546
self.domain.name,
47+
self.domain.tags
4648
)
4749

4850
# Get the command object to test
@@ -390,12 +392,13 @@ def test_domain_show(self):
390392
self.domain.id,
391393
)
392394

393-
collist = ('description', 'enabled', 'id', 'name')
395+
collist = ('description', 'enabled', 'id', 'name', 'tags')
394396
self.assertEqual(collist, columns)
395397
datalist = (
396398
self.domain.description,
397399
True,
398400
self.domain.id,
399401
self.domain.name,
402+
self.domain.tags
400403
)
401404
self.assertEqual(datalist, data)

0 commit comments

Comments
 (0)