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:
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -0,0 +1 @@
|
||||
from keystoneclient.v2_0.client import Client
|
||||
@@ -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")
|
||||
@@ -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))
|
||||
Executable
+77
@@ -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())
|
||||
Reference in New Issue
Block a user