Skip to content

Commit fbc412e

Browse files
author
Dean Troyer
committed
Multiple API version support
* Use multiple entry point groups to represent each API+version combination supported * Add some tests Try it out: * Right now only '* user' commands have multiple overlapping versions; you can see the selection between v2.0 and v3 by looking at the command help output for 'tenant' vs 'project': os --os-identity-api-version=2.0 help set user os --os-identity-api-version=3 help set user Change-Id: I7114fd246843df0243d354a7cce697810bb7de62
1 parent b26cb5b commit fbc412e

10 files changed

Lines changed: 540 additions & 27 deletions

File tree

HACKING

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ Human Alphabetical Order Examples
3939
import logging
4040
import random
4141
import StringIO
42+
import testtools
4243
import time
43-
import unittest
4444

4545
from nova import flags
4646
from nova import test

doc/source/commands.rst

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
========
2+
Commands
3+
========
4+
5+
Command Structure
6+
=================
7+
8+
OpenStack Client uses a command form ``verb object``.
9+
10+
Note that 'object' here refers to the target of a command's action. In coding
11+
discussions 'object' has its usual Python meaning. Go figure.
12+
13+
Commands take the form::
14+
15+
openstack [<global-options>] <verb> <object> [<command-local-arguments>]
16+
17+
Command Arguments
18+
-----------------
19+
20+
* All long option names use two dashes ('--') as the prefix and a single dash
21+
('-') as the interpolation character. Some common options also have the
22+
traditional single letter name prefixed by a single dash ('-').
23+
* Global options generally have a corresponding environment variable that
24+
may also be used to set the value. If both are present, the command-line
25+
option takes priority. The environment variable names can be derived from
26+
the option name by dropping the leading '--', converting all embedded dashes
27+
('-') to underscores ('_'), and converting the name to upper case.
28+
* Positional arguments trail command options. In commands that require two or
29+
more objects be acted upon, such as 'attach A to B', both objects appear
30+
as positional arguments. If they also appear in the command object they are
31+
in the same order.
32+
33+
34+
Implementation
35+
==============
36+
37+
The command structure is designed to support seamless addition of extension
38+
command modules via entry points. The extensions are assumed to be subclasses
39+
of Cliff's command.Command object.
40+
41+
Command Entry Points
42+
--------------------
43+
44+
Commands are added to the client using distribute's entry points in ``setup.py``.
45+
There is a single common group ``openstack.cli`` for commands that are not versioned,
46+
and a group for each combination of OpenStack API and version that is
47+
supported. For example, to support Identity API v3 there is a group called
48+
``openstack.identity.v3`` that contains the individual commands. The command
49+
entry points have the form::
50+
51+
"verb_object=fully.qualified.module.vXX.object:VerbObject"
52+
53+
For example, the 'list user' command fir the Identity API is identified in
54+
``setup.py`` with::
55+
56+
'openstack.identity.v3': [
57+
# ...
58+
'list_user=openstackclient.identity.v3.user:ListUser',
59+
# ...
60+
],
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2012-2013 OpenStack, LLC.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
#
15+
16+
"""Modify Cliff's CommandManager"""
17+
18+
import logging
19+
import pkg_resources
20+
21+
import cliff.commandmanager
22+
23+
24+
LOG = logging.getLogger(__name__)
25+
26+
27+
class CommandManager(cliff.commandmanager.CommandManager):
28+
"""Alters Cliff's default CommandManager behaviour to load additiona
29+
command groups after initialization.
30+
"""
31+
def _load_commands(self, group=None):
32+
if not group:
33+
group = self.namespace
34+
for ep in pkg_resources.iter_entry_points(group):
35+
LOG.debug('found command %r' % ep.name)
36+
self.commands[ep.name.replace('_', ' ')] = ep
37+
return
38+
39+
def add_command_group(self, group=None):
40+
"""Adds another group of command entrypoints"""
41+
if group:
42+
self._load_commands(group)

openstackclient/identity/v2_0/user.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# under the License.
1414
#
1515

16-
"""User action implementations"""
16+
"""Identity v2.0 User action implementations"""
1717

1818
import logging
1919

@@ -126,7 +126,7 @@ def get_parser(self, prog_name):
126126
def take_action(self, parsed_args):
127127
self.log.debug('take_action(%s)' % parsed_args)
128128
if parsed_args.long:
129-
columns = ('ID', 'Name', 'TenantId', 'Email', 'Enabled')
129+
columns = ('ID', 'Name', 'Tenant Id', 'Email', 'Enabled')
130130
else:
131131
columns = ('ID', 'Name')
132132
data = self.app.client_manager.identity.users.list()
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# Copyright 2012-2013 OpenStack, LLC.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
#
15+
16+
"""Identity v3 User action implementations"""
17+
18+
import logging
19+
20+
from cliff import command
21+
from cliff import lister
22+
from cliff import show
23+
24+
from openstackclient.common import utils
25+
26+
27+
class CreateUser(show.ShowOne):
28+
"""Create user command"""
29+
30+
api = 'identity'
31+
log = logging.getLogger(__name__ + '.CreateUser')
32+
33+
def get_parser(self, prog_name):
34+
parser = super(CreateUser, self).get_parser(prog_name)
35+
parser.add_argument(
36+
'name',
37+
metavar='<user-name>',
38+
help='New user name',
39+
)
40+
parser.add_argument(
41+
'--password',
42+
metavar='<user-password>',
43+
help='New user password',
44+
)
45+
parser.add_argument(
46+
'--email',
47+
metavar='<user-email>',
48+
help='New user email address',
49+
)
50+
parser.add_argument(
51+
'--project',
52+
metavar='<project>',
53+
help='New default project name or ID',
54+
)
55+
enable_group = parser.add_mutually_exclusive_group()
56+
enable_group.add_argument(
57+
'--enable',
58+
dest='enabled',
59+
action='store_true',
60+
default=True,
61+
help='Enable user',
62+
)
63+
enable_group.add_argument(
64+
'--disable',
65+
dest='enabled',
66+
action='store_false',
67+
help='Disable user',
68+
)
69+
return parser
70+
71+
def take_action(self, parsed_args):
72+
self.log.debug('take_action(%s)' % parsed_args)
73+
identity_client = self.app.client_manager.identity
74+
if parsed_args.project:
75+
project_id = utils.find_resource(
76+
identity_client.projects, parsed_args.project).id
77+
else:
78+
project_id = None
79+
user = identity_client.users.create(
80+
parsed_args.name,
81+
parsed_args.password,
82+
parsed_args.email,
83+
project_id=project_id,
84+
enabled=parsed_args.enabled,
85+
)
86+
87+
info = {}
88+
info.update(user._info)
89+
return zip(*sorted(info.iteritems()))
90+
91+
92+
class DeleteUser(command.Command):
93+
"""Delete user command"""
94+
95+
api = 'identity'
96+
log = logging.getLogger(__name__ + '.DeleteUser')
97+
98+
def get_parser(self, prog_name):
99+
parser = super(DeleteUser, self).get_parser(prog_name)
100+
parser.add_argument(
101+
'user',
102+
metavar='<user>',
103+
help='Name or ID of user to delete',
104+
)
105+
return parser
106+
107+
def take_action(self, parsed_args):
108+
self.log.debug('take_action(%s)' % parsed_args)
109+
identity_client = self.app.client_manager.identity
110+
user = utils.find_resource(
111+
identity_client.users, parsed_args.user)
112+
identity_client.users.delete(user.id)
113+
return
114+
115+
116+
class ListUser(lister.Lister):
117+
"""List user command"""
118+
119+
api = 'identity'
120+
log = logging.getLogger(__name__ + '.ListUser')
121+
122+
def get_parser(self, prog_name):
123+
parser = super(ListUser, self).get_parser(prog_name)
124+
parser.add_argument(
125+
'--project',
126+
metavar='<project>',
127+
help='Name or ID of project to filter users',
128+
)
129+
parser.add_argument(
130+
'--long',
131+
action='store_true',
132+
default=False,
133+
help='Additional fields are listed in output',
134+
)
135+
return parser
136+
137+
def take_action(self, parsed_args):
138+
self.log.debug('take_action(%s)' % parsed_args)
139+
if parsed_args.long:
140+
columns = ('ID', 'Name', 'Project Id', 'Email', 'Enabled')
141+
else:
142+
columns = ('ID', 'Name')
143+
data = self.app.client_manager.identity.users.list()
144+
return (columns,
145+
(utils.get_item_properties(
146+
s, columns,
147+
formatters={},
148+
) for s in data))
149+
150+
151+
class SetUser(command.Command):
152+
"""Set user command"""
153+
154+
api = 'identity'
155+
log = logging.getLogger(__name__ + '.SetUser')
156+
157+
def get_parser(self, prog_name):
158+
parser = super(SetUser, self).get_parser(prog_name)
159+
parser.add_argument(
160+
'user',
161+
metavar='<user>',
162+
help='Name or ID of user to change',
163+
)
164+
parser.add_argument(
165+
'--name',
166+
metavar='<new-user-name>',
167+
help='New user name',
168+
)
169+
parser.add_argument(
170+
'--password',
171+
metavar='<user-password>',
172+
help='New user password',
173+
)
174+
parser.add_argument(
175+
'--email',
176+
metavar='<user-email>',
177+
help='New user email address',
178+
)
179+
parser.add_argument(
180+
'--project',
181+
metavar='<project>',
182+
help='New default project name or ID',
183+
)
184+
enable_group = parser.add_mutually_exclusive_group()
185+
enable_group.add_argument(
186+
'--enable',
187+
dest='enabled',
188+
action='store_true',
189+
default=True,
190+
help='Enable user (default)',
191+
)
192+
enable_group.add_argument(
193+
'--disable',
194+
dest='enabled',
195+
action='store_false',
196+
help='Disable user',
197+
)
198+
return parser
199+
200+
def take_action(self, parsed_args):
201+
self.log.debug('take_action(%s)' % parsed_args)
202+
identity_client = self.app.client_manager.identity
203+
user = utils.find_resource(
204+
identity_client.users, parsed_args.user)
205+
kwargs = {}
206+
if parsed_args.name:
207+
kwargs['name'] = parsed_args.name
208+
if parsed_args.email:
209+
kwargs['email'] = parsed_args.email
210+
if parsed_args.project:
211+
project_id = utils.find_resource(
212+
identity_client.projects, parsed_args.project).id
213+
kwargs['projectId'] = project_id
214+
if 'enabled' in parsed_args:
215+
kwargs['enabled'] = parsed_args.enabled
216+
217+
if not len(kwargs):
218+
stdout.write("User not updated, no arguments present")
219+
return
220+
identity_client.users.update(user.id, **kwargs)
221+
return
222+
223+
224+
class ShowUser(show.ShowOne):
225+
"""Show user command"""
226+
227+
api = 'identity'
228+
log = logging.getLogger(__name__ + '.ShowUser')
229+
230+
def get_parser(self, prog_name):
231+
parser = super(ShowUser, self).get_parser(prog_name)
232+
parser.add_argument(
233+
'user',
234+
metavar='<user>',
235+
help='Name or ID of user to display',
236+
)
237+
return parser
238+
239+
def take_action(self, parsed_args):
240+
self.log.debug('take_action(%s)' % parsed_args)
241+
identity_client = self.app.client_manager.identity
242+
user = utils.find_resource(
243+
identity_client.users, parsed_args.user)
244+
245+
info = {}
246+
info.update(user._info)
247+
return zip(*sorted(info.iteritems()))

0 commit comments

Comments
 (0)