Initial checkin for new CLI and client package

Copied mostly from python-keystoneclient with
some Glance-specific stuff. README.rst shows what
WILL be the way to do things, not what is currently coded :)
This commit is contained in:
Jay Pipes
2012-02-29 16:42:26 -05:00
commit 972677fc3d
24 changed files with 2206 additions and 0 deletions
View File
+195
View File
@@ -0,0 +1,195 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Base utilities to build API operation managers and objects on top of.
"""
from glanceclient import exceptions
# Python 2.4 compat
try:
all
except NameError:
def all(iterable):
return True not in (not x for x in iterable)
def getid(obj):
"""
Abstracts the common pattern of allowing both an object or an object's ID
(UUID) as a parameter when dealing with relationships.
"""
# Try to return the object's UUID first, if we have a UUID.
try:
if obj.uuid:
return obj.uuid
except AttributeError:
pass
try:
return obj.id
except AttributeError:
return obj
class Manager(object):
"""
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
def _list(self, url, response_key, obj_class=None, body=None):
resp = None
if body:
resp, body = self.api.post(url, body=body)
else:
resp, body = self.api.get(url)
if obj_class is None:
obj_class = self.resource_class
data = body[response_key]
return [obj_class(self, res, loaded=True) for res in data if res]
def _get(self, url, response_key):
resp, body = self.api.get(url)
return self.resource_class(self, body[response_key])
def _create(self, url, body, response_key, return_raw=False):
resp, body = self.api.post(url, body=body)
if return_raw:
return body[response_key]
return self.resource_class(self, body[response_key])
def _delete(self, url):
resp, body = self.api.delete(url)
def _update(self, url, body, response_key=None, method="PUT"):
methods = {"PUT": self.api.put,
"POST": self.api.post}
try:
resp, body = methods[method](url, body=body)
except KeyError:
raise exceptions.ClientException("Invalid update method: %s"
% method)
# PUT requests may not return a body
if body:
return self.resource_class(self, body[response_key])
class ManagerWithFind(Manager):
"""
Like a `Manager`, but with additional `find()`/`findall()` methods.
"""
def find(self, **kwargs):
"""
Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
rl = self.findall(**kwargs)
try:
return rl[0]
except IndexError:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(404, msg)
def findall(self, **kwargs):
"""
Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class Resource(object):
"""
A resource represents a particular instance of an object (tenant, user,
etc). This is pretty much just a bag for attributes.
:param manager: Manager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
def __init__(self, manager, info, loaded=False):
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
def _add_details(self, info):
for (k, v) in info.iteritems():
setattr(self, k, v)
def __getattr__(self, k):
if k not in self.__dict__:
#NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
def get(self):
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val
+175
View File
@@ -0,0 +1,175 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack LLC.
# Copyright 2011 Piston Cloud Computing, Inc.
# Copyright 2011 Nebula, Inc.
# All Rights Reserved.
"""
OpenStack Client interface. Handles the REST calls and responses.
"""
import copy
import logging
import os
import time
import urllib
import urlparse
import httplib2
try:
import json
except ImportError:
import simplejson as json
# Python 2.5 compat fix
if not hasattr(urlparse, 'parse_qsl'):
import cgi
urlparse.parse_qsl = cgi.parse_qsl
from glanceclient import exceptions
_logger = logging.getLogger(__name__)
class HTTPClient(httplib2.Http):
USER_AGENT = 'python-glanceclient'
def __init__(self, username=None, tenant_id=None, tenant_name=None,
password=None, auth_url=None, region_name=None, timeout=None,
endpoint=None, token=None):
super(HTTPClient, self).__init__(timeout=timeout)
self.username = username
self.tenant_id = tenant_id
self.tenant_name = tenant_name
self.password = password
self.auth_url = auth_url.rstrip('/') if auth_url else None
self.version = 'v2.0'
self.region_name = region_name
self.auth_token = token
self.management_url = endpoint
# httplib2 overrides
self.force_exception_to_status_code = True
def authenticate(self):
""" Authenticate against the keystone API.
Not implemented here because auth protocols should be API
version-specific.
"""
raise NotImplementedError
def _extract_service_catalog(self, url, body):
""" Set the client's service catalog from the response data.
Not implemented here because data returned may be API
version-specific.
"""
raise NotImplementedError
def http_log(self, args, kwargs, resp, body):
if os.environ.get('GLANCECLIENT_DEBUG', False):
ch = logging.StreamHandler()
_logger.setLevel(logging.DEBUG)
_logger.addHandler(ch)
elif not _logger.isEnabledFor(logging.DEBUG):
return
string_parts = ['curl -i']
for element in args:
if element in ('GET', 'POST'):
string_parts.append(' -X %s' % element)
else:
string_parts.append(' %s' % element)
for element in kwargs['headers']:
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
string_parts.append(header)
_logger.debug("REQ: %s\n" % "".join(string_parts))
if 'body' in kwargs:
_logger.debug("REQ BODY: %s\n" % (kwargs['body']))
_logger.debug("RESP: %s\nRESP BODY: %s\n", resp, body)
def request(self, url, method, **kwargs):
""" Send an http request with the specified characteristics.
Wrapper around httplib2.Http.request to handle tasks such as
setting headers, JSON encoding/decoding, and error handling.
"""
# Copy the kwargs so we can reuse the original in case of redirects
request_kwargs = copy.copy(kwargs)
request_kwargs.setdefault('headers', kwargs.get('headers', {}))
request_kwargs['headers']['User-Agent'] = self.USER_AGENT
if 'body' in kwargs:
request_kwargs['headers']['Content-Type'] = 'application/json'
request_kwargs['body'] = json.dumps(kwargs['body'])
resp, body = super(HTTPClient, self).request(url,
method,
**request_kwargs)
self.http_log((url, method,), request_kwargs, resp, body)
if body:
try:
body = json.loads(body)
except ValueError, e:
_logger.debug("Could not decode JSON from body: %s" % body)
else:
_logger.debug("No body was returned.")
body = None
if resp.status in (400, 401, 403, 404, 408, 409, 413, 500, 501):
_logger.exception("Request returned failure status.")
raise exceptions.from_response(resp, body)
elif resp.status in (301, 302, 305):
# Redirected. Reissue the request to the new location.
return self.request(resp['location'], method, **kwargs)
return resp, body
def _cs_request(self, url, method, **kwargs):
if not self.management_url:
self.authenticate()
kwargs.setdefault('headers', {})
if self.auth_token:
kwargs['headers']['X-Auth-Token'] = self.auth_token
# Perform the request once. If we get a 401 back then it
# might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail.
try:
resp, body = self.request(self.management_url + url, method,
**kwargs)
return resp, body
except exceptions.Unauthorized:
try:
if getattr(self, '_failures', 0) < 1:
self._failures = getattr(self, '_failures', 0) + 1
self.authenticate()
resp, body = self.request(self.management_url + url,
method, **kwargs)
return resp, body
else:
raise
except exceptions.Unauthorized:
raise
def get(self, url, **kwargs):
return self._cs_request(url, 'GET', **kwargs)
def post(self, url, **kwargs):
return self._cs_request(url, 'POST', **kwargs)
def put(self, url, **kwargs):
return self._cs_request(url, 'PUT', **kwargs)
def delete(self, url, **kwargs):
return self._cs_request(url, 'DELETE', **kwargs)
+132
View File
@@ -0,0 +1,132 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Nebula, Inc.
"""
Exception definitions.
"""
class CommandError(Exception):
pass
class AuthorizationFailure(Exception):
pass
class NoTokenLookupException(Exception):
"""This form of authentication does not support looking up
endpoints from an existing token."""
pass
class EndpointNotFound(Exception):
"""Could not find Service or Region in Service Catalog."""
pass
class ClientException(Exception):
"""
The base exception class for all exceptions this library raises.
"""
def __init__(self, code, message=None, details=None):
self.code = code
self.message = message or self.__class__.message
self.details = details
def __str__(self):
return "%s (HTTP %s)" % (self.message, self.code)
class BadRequest(ClientException):
"""
HTTP 400 - Bad request: you sent some malformed data.
"""
http_status = 400
message = "Bad request"
class Unauthorized(ClientException):
"""
HTTP 401 - Unauthorized: bad credentials.
"""
http_status = 401
message = "Unauthorized"
class Forbidden(ClientException):
"""
HTTP 403 - Forbidden: your credentials don't give you access to this
resource.
"""
http_status = 403
message = "Forbidden"
class NotFound(ClientException):
"""
HTTP 404 - Not found
"""
http_status = 404
message = "Not found"
class Conflict(ClientException):
"""
HTTP 409 - Conflict
"""
http_status = 409
message = "Conflict"
class OverLimit(ClientException):
"""
HTTP 413 - Over limit: you're over the API limits for this time period.
"""
http_status = 413
message = "Over limit"
# NotImplemented is a python keyword.
class HTTPNotImplemented(ClientException):
"""
HTTP 501 - Not Implemented: the server does not support this operation.
"""
http_status = 501
message = "Not Implemented"
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
# so we can do this:
# _code_map = dict((c.http_status, c)
# for c in ClientException.__subclasses__())
#
# Instead, we have to hardcode it:
_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
Forbidden, NotFound, OverLimit, HTTPNotImplemented])
def from_response(response, body):
"""
Return an instance of an ClientException or subclass
based on an httplib2 response.
Usage::
resp, body = http.request(...)
if resp.status != 200:
raise exception_from_response(resp, body)
"""
cls = _code_map.get(response.status, ClientException)
if body:
if hasattr(body, 'keys'):
error = body[body.keys()[0]]
message = error.get('message', None)
details = error.get('details', None)
else:
# If we didn't get back a properly formed error message we
# probably couldn't communicate with Keystone at all.
message = "Unable to communicate with identity service: %s." % body
details = None
return cls(code=response.status, message=message, details=details)
else:
return cls(code=response.status)
View File
+205
View File
@@ -0,0 +1,205 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import urlparse
from glanceclient import client
from glanceclient import exceptions
_logger = logging.getLogger(__name__)
class Client(client.HTTPClient):
"""Client for the OpenStack Images pre-version calls API.
:param string endpoint: A user-supplied endpoint URL for the glance
service.
:param integer timeout: Allows customization of the timeout for client
http requests. (optional)
Example::
>>> from glanceclient.generic import client
>>> root = client.Client(auth_url=KEYSTONE_URL)
>>> versions = root.discover()
...
>>> from glanceclient.v1_1 import client as v11client
>>> glance = v11client.Client(auth_url=versions['v1.1']['url'])
...
>>> image = glance.images.get(IMAGE_ID)
>>> image.delete()
"""
def __init__(self, endpoint=None, **kwargs):
""" Initialize a new client for the Glance v2.0 API. """
super(Client, self).__init__(endpoint=endpoint, **kwargs)
self.endpoint = endpoint
def discover(self, url=None):
""" Discover Glance servers and return API versions supported.
:param url: optional url to test (without version)
Returns::
{
'message': 'Glance found at http://127.0.0.1:5000/',
'v2.0': {
'status': 'beta',
'url': 'http://127.0.0.1:5000/v2.0/',
'id': 'v2.0'
},
}
"""
if url:
return self._check_glance_versions(url)
else:
return self._local_glance_exists()
def _local_glance_exists(self):
""" Checks if Glance is available on default local port 9292 """
return self._check_glance_versions("http://localhost:9292")
def _check_glance_versions(self, url):
""" Calls Glance URL and detects the available API versions """
try:
httpclient = client.HTTPClient()
resp, body = httpclient.request(url, "GET",
headers={'Accept': 'application/json'})
if resp.status in (300): # Glance returns a 300 Multiple Choices
try:
results = {}
if 'version' in body:
results['message'] = "Glance found at %s" % url
version = body['version']
# Stable/diablo incorrect format
id, status, version_url = self._get_version_info(
version, url)
results[str(id)] = {"id": id,
"status": status,
"url": version_url}
return results
elif 'versions' in body:
# Correct format
results['message'] = "Glance found at %s" % url
for version in body['versions']['values']:
id, status, version_url = self._get_version_info(
version, url)
results[str(id)] = {"id": id,
"status": status,
"url": version_url}
return results
else:
results['message'] = "Unrecognized response from %s" \
% url
return results
except KeyError:
raise exceptions.AuthorizationFailure()
elif resp.status == 305:
return self._check_glance_versions(resp['location'])
else:
raise exceptions.from_response(resp, body)
except Exception as e:
_logger.exception(e)
def discover_extensions(self, url=None):
""" Discover Glance extensions supported.
:param url: optional url to test (should have a version in it)
Returns::
{
'message': 'Glance extensions at http://127.0.0.1:35357/v2',
'OS-KSEC2': 'OpenStack EC2 Credentials Extension',
}
"""
if url:
return self._check_glance_extensions(url)
def _check_glance_extensions(self, url):
""" Calls Glance URL and detects the available extensions """
try:
httpclient = client.HTTPClient()
if not url.endswith("/"):
url += '/'
resp, body = httpclient.request("%sextensions" % url, "GET",
headers={'Accept': 'application/json'})
if resp.status in (200, 204): # in some cases we get No Content
try:
results = {}
if 'extensions' in body:
if 'values' in body['extensions']:
# Parse correct format (per contract)
for extension in body['extensions']['values']:
alias, name = self._get_extension_info(
extension['extension'])
results[alias] = name
return results
else:
# Support incorrect, but prevalent format
for extension in body['extensions']:
alias, name = self._get_extension_info(
extension)
results[alias] = name
return results
else:
results['message'] = "Unrecognized extensions" \
" response from %s" % url
return results
except KeyError:
raise exceptions.AuthorizationFailure()
elif resp.status == 305:
return self._check_glance_extensions(resp['location'])
else:
raise exceptions.from_response(resp, body)
except Exception as e:
_logger.exception(e)
@staticmethod
def _get_version_info(version, root_url):
""" Parses version information
:param version: a dict of a Glance version response
:param root_url: string url used to construct
the version if no URL is provided.
:returns: tuple - (verionId, versionStatus, versionUrl)
"""
id = version['id']
status = version['status']
ref = urlparse.urljoin(root_url, id)
if 'links' in version:
for link in version['links']:
if link['rel'] == 'self':
ref = link['href']
break
return (id, status, ref)
@staticmethod
def _get_extension_info(extension):
""" Parses extension information
:param extension: a dict of a Glance extension response
:returns: tuple - (alias, name)
"""
alias = extension['alias']
name = extension['name']
return (alias, name)
+57
View File
@@ -0,0 +1,57 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from glanceclient import utils
from glanceclient.generic import client
CLIENT_CLASS = client.Client
@utils.unauthenticated
def do_discover(cs, args):
"""
Discover Keystone servers and show authentication protocols and
extensions supported.
Usage::
$ glance discover
Image Service found at http://localhost:9292
- supports version v1.0 (DEPRECATED) here http://localhost:9292/v1.0
- supports version v1.1 (CURRENT) here http://localhost:9292/v1.1
- supports version v2.0 (BETA) here http://localhost:9292/v2.0
- and RAX-KSKEY: Rackspace API Key Authentication Admin Extension
- and RAX-KSGRP: Rackspace Keystone Group Extensions
"""
if cs.auth_url:
versions = cs.discover(cs.auth_url)
else:
versions = cs.discover()
if versions:
if 'message' in versions:
print versions['message']
for key, version in versions.iteritems():
if key != 'message':
print " - supports version %s (%s) here %s" % \
(version['id'], version['status'], version['url'])
extensions = cs.discover_extensions(version['url'])
if extensions:
for key, extension in extensions.iteritems():
if key != 'message':
print " - and %s: %s" % \
(key, extension)
else:
print "No Glance-compatible endpoint found"
+81
View File
@@ -0,0 +1,81 @@
# Copyright 2011 OpenStack LLC.
# Copyright 2011, Piston Cloud Computing, Inc.
# Copyright 2011 Nebula, Inc.
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from glanceclient import exceptions
class ServiceCatalog(object):
"""
Helper methods for dealing with an OpenStack Identity
Service Catalog.
"""
def __init__(self, resource_dict):
self.catalog = resource_dict
def get_token(self):
"""Fetch token details fron service catalog"""
token = {'id': self.catalog['token']['id'],
'expires': self.catalog['token']['expires']}
try:
token['tenant'] = self.catalog['token']['tenant']['id']
except:
# just leave the tenant out if it doesn't exist
pass
return token
def url_for(self, attr=None, filter_value=None,
service_type='image', endpoint_type='publicURL'):
"""Fetch an endpoint from the service catalog.
Fetch the specified endpoint from the service catalog for
a particular endpoint attribute. If no attribute is given, return
the first endpoint of the specified type.
See tests for a sample service catalog.
"""
catalog = self.catalog.get('serviceCatalog', [])
for service in catalog:
if service['type'] != service_type:
continue
endpoints = service['endpoints']
for endpoint in endpoints:
if not filter_value or endpoint.get(attr) == filter_value:
return endpoint[endpoint_type]
raise exceptions.EndpointNotFound('Endpoint not found.')
def get_endpoints(self, service_type=None, endpoint_type=None):
"""Fetch and filter endpoints for the specified service(s)
Returns endpoints for the specified service (or all) and
that contain the specified type (or all).
"""
sc = {}
for service in self.catalog.get('serviceCatalog', []):
if service_type and service_type != service['type']:
continue
sc[service['type']] = []
for endpoint in service['endpoints']:
if endpoint_type and endpoint_type not in endpoint.keys():
continue
sc[service['type']].append(endpoint)
return sc
+246
View File
@@ -0,0 +1,246 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Command-line interface to the OpenStack Images API.
"""
import argparse
import httplib2
import os
import sys
from glanceclient import exceptions as exc
from glanceclient import utils
from glanceclient.v2_0 import shell as shell_v2_0
from glanceclient.generic import shell as shell_generic
def env(*vars, **kwargs):
"""Search for the first defined of possibly many env vars
Returns the first environment variable defined in vars, or
returns the default defined in kwargs.
"""
for v in vars:
value = os.environ.get(v, None)
if value:
return value
return kwargs.get('default', '')
class OpenStackImagesShell(object):
def get_base_parser(self):
parser = argparse.ArgumentParser(
prog='glance',
description=__doc__.strip(),
epilog='See "glance help COMMAND" '\
'for help on a specific command.',
add_help=False,
formatter_class=OpenStackHelpFormatter,
)
# Global arguments
parser.add_argument('-h', '--help',
action='store_true',
help=argparse.SUPPRESS,
)
parser.add_argument('--debug',
default=False,
action='store_true',
help=argparse.SUPPRESS)
parser.add_argument('--username',
default=env('OS_USERNAME'),
help='Defaults to env[OS_USERNAME]')
parser.add_argument('--password',
default=env('OS_PASSWORD'),
help='Defaults to env[OS_PASSWORD]')
parser.add_argument('--tenant_name',
default=env('OS_TENANT_NAME'),
help='Defaults to env[OS_TENANT_NAME]')
parser.add_argument('--tenant_id',
default=env('OS_TENANT_ID'), dest='os_tenant_id',
help='Defaults to env[OS_TENANT_ID]')
parser.add_argument('--auth_url',
default=env('OS_AUTH_URL'),
help='Defaults to env[OS_AUTH_URL]')
parser.add_argument('--region_name',
default=env('OS_REGION_NAME'),
help='Defaults to env[OS_REGION_NAME]')
parser.add_argument('--identity_api_version',
default=env('OS_IDENTITY_API_VERSION', 'KEYSTONE_VERSION'),
help='Defaults to env[OS_IDENTITY_API_VERSION] or 2.0')
return parser
def get_subcommand_parser(self, version):
parser = self.get_base_parser()
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
try:
actions_module = {
'2.0': shell_v2_0,
}[version]
except KeyError:
actions_module = shell_v2_0
self._find_actions(subparsers, actions_module)
self._find_actions(subparsers, shell_generic)
self._find_actions(subparsers, self)
return parser
def _find_actions(self, subparsers, actions_module):
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
# I prefer to be hypen-separated instead of underscores.
command = attr[3:].replace('_', '-')
callback = getattr(actions_module, attr)
desc = callback.__doc__ or ''
help = desc.strip().split('\n')[0]
arguments = getattr(callback, 'arguments', [])
subparser = subparsers.add_parser(command,
help=help,
description=desc,
add_help=False,
formatter_class=OpenStackHelpFormatter
)
subparser.add_argument('-h', '--help',
action='help',
help=argparse.SUPPRESS,
)
self.subcommands[command] = subparser
for (args, kwargs) in arguments:
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=callback)
def main(self, argv):
# Parse args once to find version
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
# build available subcommands based on version
api_version = options.identity_api_version
subcommand_parser = self.get_subcommand_parser(api_version)
self.parser = subcommand_parser
# Handle top-level --help/-h before attempting to parse
# a command off the command line
if options.help:
self.do_help(options)
return 0
# Parse args again and call whatever callback was selected
args = subcommand_parser.parse_args(argv)
# Deal with global arguments
if args.debug:
httplib2.debuglevel = 1
# Short-circuit and deal with help command right away.
if args.func == self.do_help:
self.do_help(args)
return 0
#FIXME(usrleon): Here should be restrict for project id same as
# for username or apikey but for compatibility it is not.
if not utils.isunauthenticated(args.func):
if not args.username:
raise exc.CommandError("You must provide a username "
"via either --username or env[OS_USERNAME]")
if not args.password:
raise exc.CommandError("You must provide a password "
"via either --password or env[OS_PASSWORD]")
if not args.auth_url:
raise exc.CommandError("You must provide an auth url "
"via either --auth_url or via env[OS_AUTH_URL]")
if utils.isunauthenticated(args.func):
self.cs = shell_generic.CLIENT_CLASS(endpoint=args.auth_url)
else:
api_version = options.identity_api_version
self.cs = self.get_api_class(api_version)(
username=args.username,
tenant_name=args.tenant_name,
tenant_id=args.os_tenant_id,
password=args.password,
auth_url=args.auth_url,
region_name=args.region_name)
try:
args.func(self.cs, args)
except exc.Unauthorized:
raise exc.CommandError("Invalid OpenStack Identity credentials.")
except exc.AuthorizationFailure:
raise exc.CommandError("Unable to authorize user")
def get_api_class(self, version):
try:
return {
"2.0": shell_v2_0.CLIENT_CLASS,
}[version]
except KeyError:
return shell_v2_0.CLIENT_CLASS
@utils.arg('command', metavar='<subcommand>', nargs='?',
help='Display help for <subcommand>')
def do_help(self, args):
"""
Display help about this program or one of its subcommands.
"""
if getattr(args, 'command', None):
if args.command in self.subcommands:
self.subcommands[args.command].print_help()
else:
raise exc.CommandError("'%s' is not a valid subcommand" %
args.command)
else:
self.parser.print_help()
# I'm picky about my shell help.
class OpenStackHelpFormatter(argparse.HelpFormatter):
def start_section(self, heading):
# Title-case the headings
heading = '%s%s' % (heading[0].upper(), heading[1:])
super(OpenStackHelpFormatter, self).start_section(heading)
def main():
try:
OpenStackImagesShell().main(sys.argv[1:])
except Exception, e:
if httplib2.debuglevel == 1:
raise # dump stack.
else:
print >> sys.stderr, e
sys.exit(1)
+94
View File
@@ -0,0 +1,94 @@
import uuid
import prettytable
from glanceclient import exceptions
# Decorator for cli-args
def arg(*args, **kwargs):
def _decorator(func):
# Because of the sematics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
return func
return _decorator
def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
def print_list(objs, fields, formatters={}):
pt = prettytable.PrettyTable([f for f in fields], caching=False)
pt.aligns = ['l' for f in fields]
for o in objs:
row = []
for field in fields:
if field in formatters:
row.append(formatters[field](o))
else:
field_name = field.lower().replace(' ', '_')
data = getattr(o, field_name, '')
row.append(data)
pt.add_row(row)
pt.printt(sortby=fields[0])
def print_dict(d):
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
pt.aligns = ['l', 'l']
[pt.add_row(list(r)) for r in d.iteritems()]
pt.printt(sortby='Property')
def find_resource(manager, name_or_id):
"""Helper for the _find_* methods."""
# first try to get entity as integer id
try:
if isinstance(name_or_id, int) or name_or_id.isdigit():
return manager.get(int(name_or_id))
except exceptions.NotFound:
pass
# now try to get entity as uuid
try:
uuid.UUID(str(name_or_id))
return manager.get(name_or_id)
except (ValueError, exceptions.NotFound):
pass
# finally try to find entity by name
try:
return manager.find(name=name_or_id)
except exceptions.NotFound:
msg = "No %s with a name or ID of '%s' exists." % \
(manager.resource_class.__name__.lower(), name_or_id)
raise exceptions.CommandError(msg)
def unauthenticated(f):
""" Adds 'unauthenticated' attribute to decorated function.
Usage:
@unauthenticated
def mymethod(f):
...
"""
f.unauthenticated = True
return f
def isunauthenticated(f):
"""
Checks to see if the function is marked as not requiring authentication
with the @unauthenticated decorator. Returns True if decorator is
set to True, False otherwise.
"""
return getattr(f, 'unauthenticated', False)
def string_to_bool(arg):
return arg.strip().lower() in ('t', 'true', 'yes', '1')
+1
View File
@@ -0,0 +1 @@
from keystoneclient.v2_0.client import Client
+113
View File
@@ -0,0 +1,113 @@
# Copyright 2011 Nebula, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from glanceclient import client
from glanceclient import exceptions
from glanceclient import service_catalog
from glanceclient.v1_1 import images
_logger = logging.getLogger(__name__)
class Client(client.HTTPClient):
"""Client for the OpenStack Images v1.1 API.
:param string username: Username for authentication. (optional)
:param string password: Password for authentication. (optional)
:param string token: Token for authentication. (optional)
:param string tenant_name: Tenant id. (optional)
:param string tenant_id: Tenant name. (optional)
:param string auth_url: Keystone service endpoint for authorization.
:param string region_name: Name of a region to select when choosing an
endpoint from the service catalog.
:param string endpoint: A user-supplied endpoint URL for the glance
service. Lazy-authentication is possible for API
service calls if endpoint is set at
instantiation.(optional)
:param integer timeout: Allows customization of the timeout for client
http requests. (optional)
Example::
>>> from glanceclient.v1_1 import client
>>> glance = client.Client(username=USER,
password=PASS,
tenant_name=TENANT_NAME,
auth_url=KEYSTONE_URL)
>>> glance.images.list()
...
>>> image = glance.images.get(IMAGE_ID)
>>> image.delete()
"""
def __init__(self, endpoint=None, **kwargs):
""" Initialize a new client for the Images v1.1 API. """
super(Client, self).__init__(endpoint=endpoint, **kwargs)
self.images = images.ImageManager(self)
# NOTE(gabriel): If we have a pre-defined endpoint then we can
# get away with lazy auth. Otherwise auth immediately.
if endpoint is None:
self.authenticate()
else:
self.management_url = endpoint
def authenticate(self):
""" Authenticate against the Keystone API.
Uses the data provided at instantiation to authenticate against
the Keystone server. This may use either a username and password
or token for authentication. If a tenant id was provided
then the resulting authenticated client will be scoped to that
tenant and contain a service catalog of available endpoints.
Returns ``True`` if authentication was successful.
"""
self.management_url = self.auth_url
try:
raw_token = self.tokens.authenticate(username=self.username,
tenant_id=self.tenant_id,
tenant_name=self.tenant_name,
password=self.password,
token=self.auth_token,
return_raw=True)
self._extract_service_catalog(self.auth_url, raw_token)
return True
except (exceptions.AuthorizationFailure, exceptions.Unauthorized):
raise
except Exception, e:
_logger.exception("Authorization Failed.")
raise exceptions.AuthorizationFailure("Authorization Failed: "
"%s" % e)
def _extract_service_catalog(self, url, body):
""" Set the client's service catalog from the response data. """
self.service_catalog = service_catalog.ServiceCatalog(body)
try:
self.auth_token = self.service_catalog.get_token()['id']
except KeyError:
raise exceptions.AuthorizationFailure()
# FIXME(ja): we should be lazy about setting managment_url.
# in fact we should rewrite the client to support the service
# catalog (api calls should be directable to any endpoints)
try:
self.management_url = self.service_catalog.url_for(attr='region',
filter_value=self.region_name, endpoint_type='adminURL')
except:
# Unscoped tokens don't return a service catalog
_logger.exception("unable to retrieve service catalog with token")
+88
View File
@@ -0,0 +1,88 @@
# Copyright 2011 OpenStack LLC.
# Copyright 2011 Nebula, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import urllib
from glanceclient import base
class Image(base.Resource):
def __repr__(self):
return "<Image %s>" % self._info
def delete(self):
return self.manager.delete(self)
def list_roles(self, tenant=None):
return self.manager.list_roles(self.id, base.getid(tenant))
class ImageManager(base.ManagerWithFind):
resource_class = Image
def get(self, image):
return self._get("/images/%s" % base.getid(image), "image")
def update(self, image, **kwargs):
"""
Update image data.
Supported arguments include ``name`` and ``is_public``.
"""
params = {"image": kwargs}
params['image']['id'] = base.getid(image)
url = "/images/%s" % base.getid(image)
return self._update(url, params, "image")
def create(self, name, is_public=True):
"""
Create an image.
"""
params = {
"image": {
"name": name,
"is_public": is_public
}
}
return self._create('/images', params, "image")
def delete(self, image):
"""
Delete a image.
"""
return self._delete("/images/%s" % base.getid(image))
def list(self, limit=None, marker=None):
"""
Get a list of images (optionally limited to a tenant)
:rtype: list of :class:`Image`
"""
params = {}
if limit:
params['limit'] = int(limit)
if marker:
params['marker'] = int(marker)
query = ""
if params:
query = "?" + urllib.urlencode(params)
return self._list("/images%s" % query, "images")
def list_members(self, image):
return self.api.members.members_for_image(base.getid(image))
+77
View File
@@ -0,0 +1,77 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack LLC.
# Copyright 2011 Nebula, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from glanceclient.v1_1 import client
from glanceclient import utils
CLIENT_CLASS = client.Client
@utils.arg('tenant', metavar='<tenant-id>', nargs='?', default=None,
help='Tenant ID (Optional); lists all images if not specified')
def do_image_list(gc, args):
"""List images"""
images = gc.images.list(tenant_id=args.tenant)
utils.print_list(images, ['id', 'is_public', 'email', 'name'])
@utils.arg('--name', metavar='<image-name>', required=True,
help='New image name (must be unique)')
@utils.arg('--is-public', metavar='<true|false>', default=True,
help='Initial image is_public status (default true)')
def do_image_create(gc, args):
"""Create new image"""
image = gc.images.create(args.name, args.passwd, args.email,
tenant_id=args.tenant_id, is_public=args.is_public)
utils.print_dict(image._info)
@utils.arg('--name', metavar='<image-name>',
help='Desired new image name')
@utils.arg('--is-public', metavar='<true|false>',
help='Enable or disable image')
@utils.arg('id', metavar='<image-id>', help='Image ID to update')
def do_image_update(gc, args):
"""Update image's name, email, and is_public status"""
kwargs = {}
if args.name:
kwargs['name'] = args.name
if args.email:
kwargs['email'] = args.email
if args.is_public:
kwargs['is_public'] = utils.string_to_bool(args.is_public)
if not len(kwargs):
print "User not updated, no arguments present."
return
try:
gc.images.update(args.id, **kwargs)
print 'User has been updated.'
except Exception, e:
print 'Unable to update image: %s' % e
@utils.arg('id', metavar='<image-id>', help='User ID to delete')
def do_image_delete(gc, args):
"""Delete image"""
gc.images.delete(args.id)
def do_token_get(gc, args):
"""Display the current user's token"""
utils.print_dict(gc.service_catalog.get_token())