Skip to content

Commit c24451e

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Add support for endpoint group commands"
2 parents d6761f0 + 1eae301 commit c24451e

6 files changed

Lines changed: 384 additions & 0 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
==============
2+
endpoint group
3+
==============
4+
5+
A **endpoint group** is used to create groups of endpoints that then
6+
can be used to filter the endpoints that are available to a project.
7+
Applicable to Identity v3
8+
9+
.. autoprogram-cliff:: openstack.identity.v3
10+
:command: endpoint group add project
11+
12+
.. autoprogram-cliff:: openstack.identity.v3
13+
:command: endpoint group create
14+
15+
.. autoprogram-cliff:: openstack.identity.v3
16+
:command: endpoint group delete
17+
18+
.. autoprogram-cliff:: openstack.identity.v3
19+
:command: endpoint group list
20+
21+
.. autoprogram-cliff:: openstack.identity.v3
22+
:command: endpoint group remove project
23+
24+
.. autoprogram-cliff:: openstack.identity.v3
25+
:command: endpoint group set
26+
27+
.. autoprogram-cliff:: openstack.identity.v3
28+
:command: endpoint group show

doc/source/cli/commands.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ referring to both Compute and Volume quotas.
9191
* ``domain``: (**Identity**) a grouping of projects
9292
* ``ec2 credentials``: (**Identity**) AWS EC2-compatible credentials
9393
* ``endpoint``: (**Identity**) the base URL used to contact a specific service
94+
* ``endpoint group``: (**Identity**) group endpoints to be used as filters
9495
* ``extension``: (**Compute**, **Identity**, **Network**, **Volume**) OpenStack server API extensions
9596
* ``federation protocol``: (**Identity**) the underlying protocol used while federating identities
9697
* ``flavor``: (**Compute**) predefined server configurations: ram, root disk and so on
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
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+
14+
"""Identity v3 Endpoint Group action implementations"""
15+
16+
import json
17+
import logging
18+
19+
from osc_lib.command import command
20+
from osc_lib import exceptions
21+
from osc_lib import utils
22+
import six
23+
24+
from openstackclient.i18n import _
25+
from openstackclient.identity import common
26+
27+
28+
LOG = logging.getLogger(__name__)
29+
30+
31+
class _FiltersReader(object):
32+
_description = _("Helper class capable of reading filters from files")
33+
34+
def _read_filters(self, path):
35+
"""Read and parse rules from path
36+
37+
Expect the file to contain a valid JSON structure.
38+
39+
:param path: path to the file
40+
:return: loaded and valid dictionary with filters
41+
:raises exception.CommandError: In case the file cannot be
42+
accessed or the content is not a valid JSON.
43+
44+
Example of the content of the file:
45+
{
46+
"interface": "admin",
47+
"service_id": "1b501a"
48+
}
49+
"""
50+
blob = utils.read_blob_file_contents(path)
51+
try:
52+
rules = json.loads(blob)
53+
except ValueError as e:
54+
msg = _("An error occurred when reading filters from file "
55+
"%(path)s: %(error)s") % {"path": path, "error": e}
56+
raise exceptions.CommandError(msg)
57+
else:
58+
return rules
59+
60+
61+
class AddProjectToEndpointGroup(command.Command):
62+
_description = _("Add a project to an endpoint group")
63+
64+
def get_parser(self, prog_name):
65+
parser = super(
66+
AddProjectToEndpointGroup, self).get_parser(prog_name)
67+
parser.add_argument(
68+
'endpointgroup',
69+
metavar='<endpoint-group>',
70+
help=_('Endpoint group (name or ID)'),
71+
)
72+
parser.add_argument(
73+
'project',
74+
metavar='<project>',
75+
help=_('Project to associate (name or ID)'),
76+
)
77+
common.add_project_domain_option_to_parser(parser)
78+
return parser
79+
80+
def take_action(self, parsed_args):
81+
client = self.app.client_manager.identity
82+
83+
endpointgroup = utils.find_resource(client.endpoint_groups,
84+
parsed_args.endpointgroup)
85+
86+
project = common.find_project(client,
87+
parsed_args.project,
88+
parsed_args.project_domain)
89+
90+
client.endpoint_filter.add_endpoint_group_to_project(
91+
endpoint_group=endpointgroup.id,
92+
project=project.id)
93+
94+
95+
class CreateEndpointGroup(command.ShowOne, _FiltersReader):
96+
_description = _("Create new endpoint group")
97+
98+
def get_parser(self, prog_name):
99+
parser = super(CreateEndpointGroup, self).get_parser(prog_name)
100+
parser.add_argument(
101+
'name',
102+
metavar='<name>',
103+
help=_('Name of the endpoint group'),
104+
)
105+
parser.add_argument(
106+
'filters',
107+
metavar='<filename>',
108+
help=_('Filename that contains a new set of filters'),
109+
)
110+
parser.add_argument(
111+
'--description',
112+
help=_('Description of the endpoint group'),
113+
)
114+
return parser
115+
116+
def take_action(self, parsed_args):
117+
identity_client = self.app.client_manager.identity
118+
119+
filters = None
120+
if parsed_args.filters:
121+
filters = self._read_filters(parsed_args.filters)
122+
123+
endpoint_group = identity_client.endpoint_groups.create(
124+
name=parsed_args.name,
125+
filters=filters,
126+
description=parsed_args.description
127+
)
128+
129+
info = {}
130+
endpoint_group._info.pop('links')
131+
info.update(endpoint_group._info)
132+
return zip(*sorted(six.iteritems(info)))
133+
134+
135+
class DeleteEndpointGroup(command.Command):
136+
_description = _("Delete endpoint group(s)")
137+
138+
def get_parser(self, prog_name):
139+
parser = super(DeleteEndpointGroup, self).get_parser(prog_name)
140+
parser.add_argument(
141+
'endpointgroup',
142+
metavar='<endpoint-group>',
143+
nargs='+',
144+
help=_('Endpoint group(s) to delete (name or ID)'),
145+
)
146+
return parser
147+
148+
def take_action(self, parsed_args):
149+
identity_client = self.app.client_manager.identity
150+
result = 0
151+
for i in parsed_args.endpointgroup:
152+
try:
153+
endpoint_id = utils.find_resource(
154+
identity_client.endpoint_groups, i).id
155+
identity_client.endpoint_groups.delete(endpoint_id)
156+
except Exception as e:
157+
result += 1
158+
LOG.error(_("Failed to delete endpoint group with "
159+
"ID '%(endpointgroup)s': %(e)s"),
160+
{'endpointgroup': i, 'e': e})
161+
162+
if result > 0:
163+
total = len(parsed_args.endpointgroup)
164+
msg = (_("%(result)s of %(total)s endpointgroups failed "
165+
"to delete.") % {'result': result, 'total': total})
166+
raise exceptions.CommandError(msg)
167+
168+
169+
class ListEndpointGroup(command.Lister):
170+
_description = _("List endpoint groups")
171+
172+
def get_parser(self, prog_name):
173+
parser = super(ListEndpointGroup, self).get_parser(prog_name)
174+
list_group = parser.add_mutually_exclusive_group()
175+
list_group.add_argument(
176+
'--endpointgroup',
177+
metavar='<endpoint-group>',
178+
help=_('Endpoint Group (name or ID)'),
179+
)
180+
list_group.add_argument(
181+
'--project',
182+
metavar='<project>',
183+
help=_('Project (name or ID)'),
184+
)
185+
parser.add_argument(
186+
'--domain',
187+
metavar='<domain>',
188+
help=_('Domain owning <project> (name or ID)'),
189+
)
190+
return parser
191+
192+
def take_action(self, parsed_args):
193+
client = self.app.client_manager.identity
194+
195+
endpointgroup = None
196+
if parsed_args.endpointgroup:
197+
endpointgroup = utils.find_resource(client.endpoint_groups,
198+
parsed_args.endpointgroup)
199+
project = None
200+
if parsed_args.project:
201+
project = common.find_project(client,
202+
parsed_args.project,
203+
parsed_args.domain)
204+
205+
if endpointgroup:
206+
# List projects associated to the endpoint group
207+
columns = ('ID', 'Name')
208+
data = client.endpoint_filter.list_projects_for_endpoint_group(
209+
endpoint_group=endpointgroup.id)
210+
elif project:
211+
columns = ('ID', 'Name')
212+
data = client.endpoint_filter.list_endpoint_groups_for_project(
213+
project=project.id)
214+
else:
215+
columns = ('ID', 'Name', 'Description')
216+
data = client.endpoint_groups.list()
217+
218+
return (columns,
219+
(utils.get_item_properties(
220+
s, columns,
221+
formatters={},
222+
) for s in data))
223+
224+
225+
class RemoveProjectFromEndpointGroup(command.Command):
226+
_description = _("Remove project from endpoint group")
227+
228+
def get_parser(self, prog_name):
229+
parser = super(
230+
RemoveProjectFromEndpointGroup, self).get_parser(prog_name)
231+
parser.add_argument(
232+
'endpointgroup',
233+
metavar='<endpoint-group>',
234+
help=_('Endpoint group (name or ID)'),
235+
)
236+
parser.add_argument(
237+
'project',
238+
metavar='<project>',
239+
help=_('Project to remove (name or ID)'),
240+
)
241+
common.add_project_domain_option_to_parser(parser)
242+
return parser
243+
244+
def take_action(self, parsed_args):
245+
client = self.app.client_manager.identity
246+
247+
endpointgroup = utils.find_resource(client.endpoint_groups,
248+
parsed_args.endpointgroup)
249+
250+
project = common.find_project(client,
251+
parsed_args.project,
252+
parsed_args.project_domain)
253+
254+
client.endpoint_filter.delete_endpoint_group_to_project(
255+
endpoint_group=endpointgroup.id,
256+
project=project.id)
257+
258+
259+
class SetEndpointGroup(command.Command, _FiltersReader):
260+
_description = _("Set endpoint group properties")
261+
262+
def get_parser(self, prog_name):
263+
parser = super(SetEndpointGroup, self).get_parser(prog_name)
264+
parser.add_argument(
265+
'endpointgroup',
266+
metavar='<endpoint-group>',
267+
help=_('Endpoint Group to modify (name or ID)'),
268+
)
269+
parser.add_argument(
270+
'--name',
271+
metavar='<name>',
272+
help=_('New enpoint group name'),
273+
)
274+
parser.add_argument(
275+
'--filters',
276+
metavar='<filename>',
277+
help=_('Filename that contains a new set of filters'),
278+
)
279+
parser.add_argument(
280+
'--description',
281+
metavar='<description>',
282+
default='',
283+
help=_('New endpoint group description'),
284+
)
285+
return parser
286+
287+
def take_action(self, parsed_args):
288+
identity_client = self.app.client_manager.identity
289+
endpointgroup = utils.find_resource(identity_client.endpoint_groups,
290+
parsed_args.endpointgroup)
291+
292+
filters = None
293+
if parsed_args.filters:
294+
filters = self._read_filters(parsed_args.filters)
295+
296+
identity_client.endpoint_groups.update(
297+
endpointgroup.id,
298+
name=parsed_args.name,
299+
filters=filters,
300+
description=parsed_args.description
301+
)
302+
303+
304+
class ShowEndpointGroup(command.ShowOne):
305+
_description = _("Display endpoint group details")
306+
307+
def get_parser(self, prog_name):
308+
parser = super(ShowEndpointGroup, self).get_parser(prog_name)
309+
parser.add_argument(
310+
'endpointgroup',
311+
metavar='<endpointgroup>',
312+
help=_('Endpoint group (name or ID)'),
313+
)
314+
return parser
315+
316+
def take_action(self, parsed_args):
317+
identity_client = self.app.client_manager.identity
318+
endpoint_group = utils.find_resource(identity_client.endpoint_groups,
319+
parsed_args.endpointgroup)
320+
321+
info = {}
322+
endpoint_group._info.pop('links')
323+
info.update(endpoint_group._info)
324+
return zip(*sorted(six.iteritems(info)))

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,20 @@
228228
'links': base_url + 'endpoints/' + endpoint_id,
229229
}
230230

231+
endpoint_group_id = 'eg-123'
232+
endpoint_group_description = 'eg 123 description'
233+
endpoint_group_filters = {
234+
'service_id': service_id,
235+
'region_id': endpoint_region,
236+
}
237+
238+
ENDPOINT_GROUP = {
239+
'id': endpoint_group_id,
240+
'filters': endpoint_group_filters,
241+
'description': endpoint_group_description,
242+
'links': base_url + 'endpoint_groups/' + endpoint_group_id,
243+
}
244+
231245
user_id = 'bbbbbbb-aaaa-aaaa-aaaa-bbbbbbbaaaa'
232246
user_name = 'paul'
233247
user_description = 'Sir Paul'
@@ -500,6 +514,8 @@ def __init__(self, **kwargs):
500514
self.endpoints.resource_class = fakes.FakeResource(None, {})
501515
self.endpoint_filter = mock.Mock()
502516
self.endpoint_filter.resource_class = fakes.FakeResource(None, {})
517+
self.endpoint_groups = mock.Mock()
518+
self.endpoint_groups.resource_class = fakes.FakeResource(None, {})
503519
self.groups = mock.Mock()
504520
self.groups.resource_class = fakes.FakeResource(None, {})
505521
self.oauth1 = mock.Mock()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
features:
3+
- |
4+
Add endpoint group commands: ``endpoint group add project``, ``endpoint group create``,
5+
``endpoint group delete``, ``endpoint group list``, ``endpoint group remove project``,
6+
``endpoint group set`` and ``endpoint group show``.
7+
[Blueprint `keystone-endpoint-filter <https://blueprints.launchpad.net/python-openstackclient/+spec/keystone-endpoint-filter>`_]

0 commit comments

Comments
 (0)