Skip to content

Commit 1eae301

Browse files
josecastroleonDean Troyer
authored andcommitted
Add support for endpoint group commands
Implements the commands for endpoint group filter management. Includes the CRUD management of the endpoint groups and the association management between them and the projects that are using this method. Implements: blueprint keystone-endpoint-filter Change-Id: I4265f7f8598d028191e90d76781b7b6ece6fef64
1 parent 7505831 commit 1eae301

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
@@ -223,6 +223,20 @@
223223
'links': base_url + 'endpoints/' + endpoint_id,
224224
}
225225

226+
endpoint_group_id = 'eg-123'
227+
endpoint_group_description = 'eg 123 description'
228+
endpoint_group_filters = {
229+
'service_id': service_id,
230+
'region_id': endpoint_region,
231+
}
232+
233+
ENDPOINT_GROUP = {
234+
'id': endpoint_group_id,
235+
'filters': endpoint_group_filters,
236+
'description': endpoint_group_description,
237+
'links': base_url + 'endpoint_groups/' + endpoint_group_id,
238+
}
239+
226240
user_id = 'bbbbbbb-aaaa-aaaa-aaaa-bbbbbbbaaaa'
227241
user_name = 'paul'
228242
user_description = 'Sir Paul'
@@ -495,6 +509,8 @@ def __init__(self, **kwargs):
495509
self.endpoints.resource_class = fakes.FakeResource(None, {})
496510
self.endpoint_filter = mock.Mock()
497511
self.endpoint_filter.resource_class = fakes.FakeResource(None, {})
512+
self.endpoint_groups = mock.Mock()
513+
self.endpoint_groups.resource_class = fakes.FakeResource(None, {})
498514
self.groups = mock.Mock()
499515
self.groups.resource_class = fakes.FakeResource(None, {})
500516
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)