Skip to content

Commit 5e40321

Browse files
author
Doug Hellmann
committed
Fix "help" command and implement "list server" and "show server"
blueprint client-manager blueprint nova-client bug 992841 Move the authentication logic into a new ClientManager class so that only commands that need to authenticate will trigger that code. Implement "list server" and "show server" commands as examples of using the ClientManager, Lister, and ShowOne classes. Change-Id: I9845b70b33bae4b193dbe41871bf0ca8e286a727
1 parent b5a809d commit 5e40321

7 files changed

Lines changed: 324 additions & 106 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
*.log
2+
*.log.*
23
*.pyc
34
*.swp
45
*~
@@ -10,3 +11,4 @@ dist
1011
python_openstackclient.egg-info
1112
.tox/
1213
ChangeLog
14+
TAGS
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Manage access to the clients, including authenticating when needed.
2+
"""
3+
4+
import logging
5+
6+
from openstackclient.common import exceptions as exc
7+
from openstackclient.compute import client as compute_client
8+
9+
from keystoneclient.v2_0 import client as keystone_client
10+
11+
LOG = logging.getLogger(__name__)
12+
13+
14+
class ClientCache(object):
15+
"""Descriptor class for caching created client handles.
16+
"""
17+
18+
def __init__(self, factory):
19+
self.factory = factory
20+
self._handle = None
21+
22+
def __get__(self, instance, owner):
23+
# Tell the ClientManager to login to keystone
24+
if self._handle is None:
25+
instance.init_token()
26+
self._handle = self.factory(instance)
27+
return self._handle
28+
29+
30+
class ClientManager(object):
31+
"""Manages access to API clients, including authentication.
32+
"""
33+
34+
compute = ClientCache(compute_client.make_client)
35+
36+
def __init__(self, token=None, url=None,
37+
auth_url=None,
38+
tenant_name=None, tenant_id=None,
39+
username=None, password=None,
40+
region_name=None,
41+
identity_api_version=None,
42+
compute_api_version=None,
43+
image_api_version=None,
44+
):
45+
self._token = token
46+
self._url = url
47+
self._auth_url = auth_url
48+
self._tenant_name = tenant_name
49+
self._tenant_id = tenant_id
50+
self._username = username
51+
self._password = password
52+
self._region_name = region_name
53+
self._identity_api_version = identity_api_version
54+
self._compute_api_version = compute_api_version
55+
self._image_api_version = image_api_version
56+
57+
def init_token(self):
58+
"""Return the auth token and endpoint.
59+
"""
60+
if self._token:
61+
LOG.debug('using existing auth token')
62+
return
63+
64+
LOG.debug('validating authentication options')
65+
if not self._username:
66+
raise exc.CommandError(
67+
"You must provide a username via"
68+
" either --os-username or env[OS_USERNAME]")
69+
70+
if not self._password:
71+
raise exc.CommandError(
72+
"You must provide a password via"
73+
" either --os-password or env[OS_PASSWORD]")
74+
75+
if not (self._tenant_id or self._tenant_name):
76+
raise exc.CommandError(
77+
"You must provide a tenant_id via"
78+
" either --os-tenant-id or via env[OS_TENANT_ID]")
79+
80+
if not self._auth_url:
81+
raise exc.CommandError(
82+
"You must provide an auth url via"
83+
" either --os-auth-url or via env[OS_AUTH_URL]")
84+
85+
kwargs = {
86+
'username': self._username,
87+
'password': self._password,
88+
'tenant_id': self._tenant_id,
89+
'tenant_name': self._tenant_name,
90+
'auth_url': self._auth_url
91+
}
92+
self._auth_client = keystone_client.Client(**kwargs)
93+
self._token = self._auth_client.auth_token
94+
return
95+
96+
def get_endpoint_for_service_type(self, service_type):
97+
"""Return the endpoint URL for the service type.
98+
"""
99+
# See if we are using password flow auth, i.e. we have a
100+
# service catalog to select endpoints from
101+
if self._auth_client and self._auth_client.service_catalog:
102+
endpoint = self._auth_client.service_catalog.url_for(
103+
service_type=service_type)
104+
else:
105+
# Hope we were given the correct URL.
106+
endpoint = self._url
107+
return endpoint

openstackclient/compute/client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import logging
2+
3+
from novaclient import client as nova_client
4+
5+
LOG = logging.getLogger(__name__)
6+
7+
8+
def make_client(instance):
9+
"""Returns a compute service client.
10+
"""
11+
LOG.debug('instantiating compute client')
12+
# FIXME(dhellmann): Where is the endpoint value used?
13+
# url = instance.get_endpoint_for_service_type('compute')
14+
client = nova_client.Client(
15+
version=instance._compute_api_version,
16+
username=instance._username,
17+
api_key=instance._password,
18+
project_id=instance._tenant_name,
19+
auth_url=instance._auth_url,
20+
# FIXME(dhellmann): add constructor argument for this
21+
insecure=False,
22+
region_name=instance._region_name,
23+
# FIXME(dhellmann): get endpoint_type from option?
24+
endpoint_type='publicURL',
25+
# FIXME(dhellmann): add extension discovery
26+
extensions=[],
27+
service_type='compute',
28+
# FIXME(dhellmann): what is service_name?
29+
service_name='',
30+
)
31+
client.authenticate()
32+
return client

openstackclient/compute/v2/server.py

Lines changed: 143 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -20,45 +20,55 @@
2020
"""
2121

2222
import logging
23+
import os
24+
25+
from cliff import lister
26+
from cliff import show
2327

2428
from openstackclient.common import command
2529
from openstackclient.common import utils
2630

2731

28-
def _find_server(cs, server):
29-
"""Get a server by name or ID."""
30-
return utils.find_resource(cs.servers, server)
31-
32-
33-
def _print_server(cs, server):
34-
# By default when searching via name we will do a
35-
# findall(name=blah) and due a REST /details which is not the same
36-
# as a .get() and doesn't get the information about flavors and
37-
# images. This fix it as we redo the call with the id which does a
38-
# .get() to get all informations.
39-
if not 'flavor' in server._info:
40-
server = _find_server(cs, server.id)
41-
42-
networks = server.networks
43-
info = server._info.copy()
44-
for network_label, address_list in networks.items():
45-
info['%s network' % network_label] = ', '.join(address_list)
46-
47-
flavor = info.get('flavor', {})
48-
flavor_id = flavor.get('id', '')
49-
info['flavor'] = _find_flavor(cs, flavor_id).name
50-
51-
image = info.get('image', {})
52-
image_id = image.get('id', '')
53-
info['image'] = _find_image(cs, image_id).name
54-
55-
info.pop('links', None)
56-
info.pop('addresses', None)
57-
58-
utils.print_dict(info)
59-
60-
61-
class List_Server(command.OpenStackCommand):
32+
def _format_servers_list_networks(server):
33+
"""Return a string containing the networks a server is attached to.
34+
35+
:param server: a single Server resource
36+
"""
37+
output = []
38+
for (network, addresses) in server.networks.items():
39+
if not addresses:
40+
continue
41+
addresses_csv = ', '.join(addresses)
42+
group = "%s=%s" % (network, addresses_csv)
43+
output.append(group)
44+
return '; '.join(output)
45+
46+
47+
def get_server_properties(server, fields, formatters={}):
48+
"""Return a tuple containing the server properties.
49+
50+
:param server: a single Server resource
51+
:param fields: tuple of strings with the desired field names
52+
:param formatters: dictionary mapping field names to callables
53+
to format the values
54+
"""
55+
row = []
56+
mixed_case_fields = ['serverId']
57+
58+
for field in fields:
59+
if field in formatters:
60+
row.append(formatters[field](server))
61+
else:
62+
if field in mixed_case_fields:
63+
field_name = field.replace(' ', '_')
64+
else:
65+
field_name = field.lower().replace(' ', '_')
66+
data = getattr(server, field_name, '')
67+
row.append(data)
68+
return tuple(row)
69+
70+
71+
class List_Server(command.OpenStackCommand, lister.Lister):
6272
"List server command."
6373

6474
api = 'compute'
@@ -67,17 +77,79 @@ class List_Server(command.OpenStackCommand):
6777
def get_parser(self, prog_name):
6878
parser = super(List_Server, self).get_parser(prog_name)
6979
parser.add_argument(
70-
'--long',
80+
'--reservation-id',
81+
help='only return instances that match the reservation',
82+
)
83+
parser.add_argument(
84+
'--ip',
85+
help='regular expression to match IP address',
86+
)
87+
parser.add_argument(
88+
'--ip6',
89+
help='regular expression to match IPv6 address',
90+
)
91+
parser.add_argument(
92+
'--name',
93+
help='regular expression to match name',
94+
)
95+
parser.add_argument(
96+
'--instance-name',
97+
help='regular expression to match instance name',
98+
)
99+
parser.add_argument(
100+
'--status',
101+
help='search by server status',
102+
# FIXME(dhellmann): Add choices?
103+
)
104+
parser.add_argument(
105+
'--flavor',
106+
help='search by flavor ID',
107+
)
108+
parser.add_argument(
109+
'--image',
110+
help='search by image ID',
111+
)
112+
parser.add_argument(
113+
'--host',
114+
metavar='HOSTNAME',
115+
help='search by hostname',
116+
)
117+
parser.add_argument(
118+
'--all-tenants',
71119
action='store_true',
72-
default=False,
73-
help='Additional fields are listed in output')
120+
default=bool(int(os.environ.get("ALL_TENANTS", 0))),
121+
help='display information from all tenants (admin only)',
122+
)
74123
return parser
75124

76-
def run(self, parsed_args):
77-
self.log.info('v2.List_Server.run(%s)' % parsed_args)
78-
79-
80-
class Show_Server(command.OpenStackCommand):
125+
def get_data(self, parsed_args):
126+
self.log.debug('v2.List_Server.run(%s)' % parsed_args)
127+
nova_client = self.app.client_manager.compute
128+
search_opts = {
129+
'all_tenants': parsed_args.all_tenants,
130+
'reservation_id': parsed_args.reservation_id,
131+
'ip': parsed_args.ip,
132+
'ip6': parsed_args.ip6,
133+
'name': parsed_args.name,
134+
'image': parsed_args.image,
135+
'flavor': parsed_args.flavor,
136+
'status': parsed_args.status,
137+
'host': parsed_args.host,
138+
'instance_name': parsed_args.instance_name,
139+
}
140+
self.log.debug('search options: %s', search_opts)
141+
# FIXME(dhellmann): Consider adding other columns
142+
columns = ('ID', 'Name', 'Status', 'Networks')
143+
data = nova_client.servers.list(search_opts=search_opts)
144+
return (columns,
145+
(get_server_properties(
146+
s, columns,
147+
formatters={'Networks': _format_servers_list_networks},
148+
) for s in data),
149+
)
150+
151+
152+
class Show_Server(command.OpenStackCommand, show.ShowOne):
81153
"Show server command."
82154

83155
api = 'compute'
@@ -91,7 +163,32 @@ def get_parser(self, prog_name):
91163
help='Name or ID of server to display')
92164
return parser
93165

94-
def run(self, parsed_args):
95-
self.log.info('v2.Show_Server.run(%s)' % parsed_args)
96-
#s = _find_server(cs, args.server)
97-
#_print_server(cs, s)
166+
def get_data(self, parsed_args):
167+
self.log.debug('v2.Show_Server.run(%s)' % parsed_args)
168+
nova_client = self.app.client_manager.compute
169+
server = utils.find_resource(nova_client.servers, parsed_args.server)
170+
171+
info = {}
172+
info.update(server._info)
173+
174+
# Convert the flavor blob to a name
175+
flavor_info = info.get('flavor', {})
176+
flavor_id = flavor_info.get('id', '')
177+
flavor = utils.find_resource(nova_client.flavors, flavor_id)
178+
info['flavor'] = flavor.name
179+
180+
# Convert the image blob to a name
181+
image_info = info.get('image', {})
182+
image_id = image_info.get('id', '')
183+
image = utils.find_resource(nova_client.images, image_id)
184+
info['image'] = image.name
185+
186+
# Format addresses in a useful way
187+
info['addresses'] = _format_servers_list_networks(server)
188+
189+
# Remove a couple of values that are long and not too useful
190+
info.pop('links', None)
191+
192+
columns = sorted(info.keys())
193+
values = [info[c] for c in columns]
194+
return (columns, values)

0 commit comments

Comments
 (0)