Browse Source

Basic get/list operations work

* 'glance image-list' and 'glance image-show' work
* Set up tests, pep8, venv
changes/11/6211/1
Brian Waldon 9 years ago
parent
commit
c530de6389
  1. 1
      .gitignore
  2. 34
      LICENSE
  3. 1
      MANIFEST.in
  4. 22
      README.rst
  5. 175
      glanceclient/client.py
  6. 0
      glanceclient/common/__init__.py
  7. 46
      glanceclient/common/base.py
  8. 4
      glanceclient/common/exceptions.py
  9. 121
      glanceclient/common/http.py
  10. 55
      glanceclient/common/utils.py
  11. 205
      glanceclient/generic/client.py
  12. 57
      glanceclient/generic/shell.py
  13. 81
      glanceclient/service_catalog.py
  14. 155
      glanceclient/shell.py
  15. 1
      glanceclient/v1/__init__.py
  16. 38
      glanceclient/v1/client.py
  17. 70
      glanceclient/v1/images.py
  18. 38
      glanceclient/v1/shell.py
  19. 1
      glanceclient/v1_1/__init__.py
  20. 113
      glanceclient/v1_1/client.py
  21. 88
      glanceclient/v1_1/images.py
  22. 77
      glanceclient/v1_1/shell.py
  23. 360
      run_tests.py
  24. 96
      run_tests.sh
  25. 42
      setup.py
  26. 5
      tests/test_test.py
  27. 153
      tools/install_venv.py
  28. 2
      tools/pip-requires
  29. 3
      tools/test-requires
  30. 4
      tools/with_venv.sh

1
.gitignore

@ -10,3 +10,4 @@ build
dist
python_glanceclient.egg-info
ChangeLog
run_tests.err.log

34
LICENSE

@ -1,8 +1,3 @@
Copyright (c) 2009 Jacob Kaplan-Moss - initial codebase (< v2.1)
Copyright (c) 2011 Rackspace - OpenStack extensions (>= v2.1)
Copyright (c) 2011 Nebula, Inc - Keystone refactor (>= v2.7)
All rights reserved.
Apache License
Version 2.0, January 2004
@ -178,32 +173,3 @@ All rights reserved.
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
--- License for python-keystoneclient versions prior to 2.1 ---
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of this project nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1
MANIFEST.in

@ -1,4 +1,3 @@
include README.rst
include LICENSE
recursive-include docs *
recursive-include tests *

22
README.rst

@ -28,7 +28,7 @@ Python API
By way of a quick-start::
# use v2.0 auth with http://example.com:5000/v2.0")
>>> from glanceclient.v2_0 import client
>>> from glanceclient.v1 import client
>>> glance = client.Client(username=USERNAME, password=PASSWORD, tenant_name=TENANT, auth_url=KEYSTONE_URL)
>>> glance.images.list()
>>> image = glance.images.create(name="My Test Image")
@ -50,20 +50,15 @@ Command-line API
Installing this package gets you a command-line tool, ``glance``, that you
can use to interact with Glance's Identity API.
You'll need to provide your OpenStack tenant, username and password. You can do this
with the ``tenant_name``, ``--username`` and ``--password`` params, but it's
easier to just set them as environment variables::
You'll need to provide your OpenStack username, password, tenant, and auth
endpoint. You can do this with the ``--tenant_id``, ``--username``,
``--password``, and ``--auth_url`` params, but it's easier to just set them
as environment variables::
export OS_TENANT_NAME=project
export OS_TENANT_id=
export OS_USERNAME=user
export OS_PASSWORD=pass
You will also need to define the authentication url with ``--auth_url`` and the
version of the API with ``--identity_api_version``. Or set them as an environment
variables as well::
export OS_AUTH_URL=http://example.com:5000/v2.0
export OS_IDENTITY_API_VERSION=2.0
Since the Identity service that Glance uses can return multiple regional image
endpoints in the Service Catalog, you can specify the one you want with
@ -74,9 +69,8 @@ You'll find complete documentation on the shell by running
``glance help``::
usage: glance [--username USERNAME] [--password PASSWORD]
[--tenant_name TENANT_NAME | --tenant_id TENANT_ID]
[--tenant_id TENANT_id]
[--auth_url AUTH_URL] [--region_name REGION_NAME]
[--identity_api_version IDENTITY_API_VERSION]
<subcommand> ...
Command-line interface to the OpenStack Identity API.
@ -103,7 +97,5 @@ You'll find complete documentation on the shell by running
--auth_url AUTH_URL Defaults to env[OS_AUTH_URL]
--region_name REGION_NAME
Defaults to env[OS_REGION_NAME]
--identity_api_version IDENTITY_API_VERSION
Defaults to env[OS_IDENTITY_API_VERSION] or 2.0
See "glance help COMMAND" for help on a specific command.

175
glanceclient/client.py

@ -1,175 +0,0 @@
# 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
glanceclient/generic/__init__.py → glanceclient/common/__init__.py

46
glanceclient/base.py → glanceclient/common/base.py

@ -1,5 +1,4 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack LLC.
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -18,7 +17,7 @@
Base utilities to build API operation managers and objects on top of.
"""
from glanceclient import exceptions
from glanceclient.common import exceptions
# Python 2.4 compat
@ -81,7 +80,7 @@ class Manager(object):
return self.resource_class(self, body[response_key])
def _delete(self, url):
resp, body = self.api.delete(url)
self.api.delete(url)
def _update(self, url, body, response_key=None, method="PUT"):
methods = {"PUT": self.api.put,
@ -96,45 +95,6 @@ class Manager(object):
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,

4
glanceclient/exceptions.py → glanceclient/common/exceptions.py

@ -1,5 +1,3 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Nebula, Inc.
"""
Exception definitions.
"""
@ -125,7 +123,7 @@ def from_response(response, body):
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
message = "Unable to communicate with image service: %s." % body
details = None
return cls(code=response.status, message=message, details=details)
else:

121
glanceclient/common/http.py

@ -0,0 +1,121 @@
"""
OpenStack Client interface. Handles the REST calls and responses.
"""
import copy
import logging
import os
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.common import exceptions
logger = logging.getLogger(__name__)
USER_AGENT = 'python-glanceclient'
class HTTPClient(httplib2.Http):
def __init__(self, endpoint, token=None, timeout=600):
super(HTTPClient, self).__init__(timeout=timeout)
self.endpoint = endpoint
self.auth_token = token
# httplib2 overrides
self.force_exception_to_status_code = True
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 _http_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
_kwargs = copy.copy(kwargs)
_kwargs.setdefault('headers', kwargs.get('headers', {}))
_kwargs['headers']['User-Agent'] = USER_AGENT
if 'body' in kwargs:
_kwargs['headers']['Content-Type'] = 'application/json'
_kwargs['body'] = json.dumps(kwargs['body'])
resp, body = super(HTTPClient, self).request(url, method, **_kwargs)
self.http_log((url, method,), _kwargs, resp, body)
if body:
try:
body = json.loads(body)
except ValueError:
logger.debug("Could not decode JSON from body: %s" % body)
else:
logger.debug("No body was returned.")
body = None
if 400 <= resp.status < 600:
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._http_request(resp['location'], method, **kwargs)
return resp, body
def request(self, url, method, **kwargs):
kwargs.setdefault('headers', {})
if self.auth_token:
kwargs['headers']['X-Auth-Token'] = self.auth_token
req_url = self.endpoint + url
resp, body = self._http_request(req_url, method, **kwargs)
return resp, body
def head(self, url, **kwargs):
return self.request(url, 'HEAD', **kwargs)
def get(self, url, **kwargs):
return self.request(url, 'GET', **kwargs)
def post(self, url, **kwargs):
return self.request(url, 'POST', **kwargs)
def put(self, url, **kwargs):
return self.request(url, 'PUT', **kwargs)
def delete(self, url, **kwargs):
return self.request(url, 'DELETE', **kwargs)

55
glanceclient/utils.py → glanceclient/common/utils.py

@ -1,8 +1,24 @@
# Copyright 2012 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 os
import uuid
import prettytable
from glanceclient import exceptions
from glanceclient.common import exceptions
# Decorator for cli-args
@ -69,26 +85,33 @@ def find_resource(manager, name_or_id):
raise exceptions.CommandError(msg)
def unauthenticated(f):
""" Adds 'unauthenticated' attribute to decorated function.
Usage:
@unauthenticated
def mymethod(f):
...
"""
f.unauthenticated = True
def skip_authentication(f):
"""Function decorator used to indicate a caller may be unauthenticated."""
f.require_authentication = False
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.
def is_authentication_required(f):
"""Checks to see if the function requires authentication.
Use the skip_authentication decorator to indicate a caller may
skip the authentication step.
"""
return getattr(f, 'unauthenticated', False)
return getattr(f, 'require_authentication', True)
def string_to_bool(arg):
return arg.strip().lower() in ('t', 'true', 'yes', '1')
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', '')

205
glanceclient/generic/client.py

@ -1,205 +0,0 @@
# 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
glanceclient/generic/shell.py

@ -1,57 +0,0 @@
# 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
glanceclient/service_catalog.py

@ -1,81 +0,0 @@
# 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

155
glanceclient/shell.py

@ -1,5 +1,4 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack LLC.
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -20,27 +19,14 @@ 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
from keystoneclient.v2_0 import client as ksclient
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', '')
from glanceclient.common import exceptions as exc
from glanceclient.common import utils
from glanceclient.v1 import shell as shell_v1
from glanceclient.v1 import client as client_v1
class OpenStackImagesShell(object):
@ -52,7 +38,7 @@ class OpenStackImagesShell(object):
epilog='See "glance help COMMAND" '\
'for help on a specific command.',
add_help=False,
formatter_class=OpenStackHelpFormatter,
formatter_class=HelpFormatter,
)
# Global arguments
@ -66,33 +52,33 @@ class OpenStackImagesShell(object):
action='store_true',
help=argparse.SUPPRESS)
parser.add_argument('--username',
default=env('OS_USERNAME'),
parser.add_argument('--os-username',
default=utils.env('OS_USERNAME'),
help='Defaults to env[OS_USERNAME]')
parser.add_argument('--password',
default=env('OS_PASSWORD'),
parser.add_argument('--os-password',
default=utils.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',
parser.add_argument('--os-tenant-id',
default=utils.env('OS_TENANT_ID'),
help='Defaults to env[OS_TENANT_ID]')
parser.add_argument('--auth_url',
default=env('OS_AUTH_URL'),
parser.add_argument('--os-auth-url',
default=utils.env('OS_AUTH_URL'),
help='Defaults to env[OS_AUTH_URL]')
parser.add_argument('--region_name',
default=env('OS_REGION_NAME'),
parser.add_argument('--os-region-name',
default=utils.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')
parser.add_argument('--os-auth-token',
default=utils.env('OS_AUTH_TOKEN'),
help='Defaults to env[OS_AUTH_TOKEN]')
parser.add_argument('--os-image-url',
default=utils.env('OS_IMAGE_URL'),
help='Defaults to env[OS_IMAGE_URL]')
return parser
@ -101,16 +87,7 @@ class OpenStackImagesShell(object):
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, shell_v1)
self._find_actions(subparsers, self)
return parser
@ -128,7 +105,7 @@ class OpenStackImagesShell(object):
help=help,
description=desc,
add_help=False,
formatter_class=OpenStackHelpFormatter
formatter_class=HelpFormatter
)
subparser.add_argument('-h', '--help',
action='help',
@ -139,19 +116,28 @@ class OpenStackImagesShell(object):
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=callback)
def _authenticate(self, username, password, tenant_id, auth_url):
_ksclient = ksclient.Client(username=username,
password=password,
tenant_id=tenant_id,
auth_url=auth_url)
endpoint = _ksclient.service_catalog.url_for(service_type='image',
endpoint_type='publicURL')
return (endpoint, _ksclient.auth_token)
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
api_version = '1'
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:
if options.help or not argv:
self.do_help(options)
return 0
@ -167,51 +153,45 @@ class OpenStackImagesShell(object):
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.
auth_reqd = (utils.is_authentication_required(args.func) or
not (args.os_auth_token and args.os_image_url))
if not auth_reqd:
endpoint = args.os_image_url
token = args.os_auth_token
else:
if not args.os_username:
raise exc.CommandError("You must provide a username via"
" either --os-username or env[OS_USERNAME]")
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.os_password:
raise exc.CommandError("You must provide a password via"
" either --os-password or env[OS_PASSWORD]")
if not args.password:
raise exc.CommandError("You must provide a password "
"via either --password or env[OS_PASSWORD]")
if not args.os_tenant_id:
raise exc.CommandError("You must provide a tenant_id via"
" either --os-tenant-id or via env[OS_TENANT_ID]")
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 not args.os_auth_url:
raise exc.CommandError("You must provide an auth url via"
" either --os-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)
endpoint, token = self._authenticate(args.os_username,
args.os_password,
args.os_tenant_id,
args.os_auth_url)
image_service = client_v1.Client(endpoint, token)
try:
args.func(self.cs, args)
args.func(image_service, 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>')
help='Display help for <subcommand>')
def do_help(self, args):
"""
Display help about this program or one of its subcommands.
@ -226,12 +206,11 @@ class OpenStackImagesShell(object):
self.parser.print_help()
# I'm picky about my shell help.
class OpenStackHelpFormatter(argparse.HelpFormatter):
class HelpFormatter(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)
super(HelpFormatter, self).start_section(heading)
def main():
@ -240,7 +219,7 @@ def main():
except Exception, e:
if httplib2.debuglevel == 1:
raise # dump stack.
raise
else:
print >> sys.stderr, e
sys.exit(1)

1
glanceclient/v1/__init__.py

@ -0,0 +1 @@
from glanceclient.v1.client import Client

38
glanceclient/v1/client.py

@ -0,0 +1,38 @@
# Copyright 2012 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
from glanceclient.common import http
from glanceclient.v1 import images
logger = logging.getLogger(__name__)
class Client(http.HTTPClient):
"""Client for the OpenStack Images v1 API.
:param string endpoint: A user-supplied endpoint URL for the glance
service.
:param string token: Token for authentication.
:param integer timeout: Allows customization of the timeout for client
http requests. (optional)
"""
def __init__(self, endpoint, token=None, timeout=600):
""" Initialize a new client for the Images v1 API. """
super(Client, self).__init__(endpoint, token=token, timeout=timeout)
self.images = images.ImageManager(self)

70
glanceclient/v1/images.py

@ -0,0 +1,70 @@
# Copyright 2012 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 urllib
from glanceclient.common import base
class Image(base.Resource):
def __repr__(self):
return "<Image %s>" % self._info
def delete(self):
return self.manager.delete(self)
class ImageManager(base.Manager):
resource_class = Image
def get(self, image):
"""Get the metadata for a specific image.
:param image: image object or id to look up
:rtype: :class:`Image`
"""
resp, body = self.api.head("/images/%s" % base.getid(image))
meta = {'properties': {}}
for key, value in resp.iteritems():
if key.startswith('x-image-meta-property-'):
_key = key[22:]
meta['properties'][_key] = value
elif key.startswith('x-image-meta-'):
_key = key[13:]
meta[_key] = value
return Image(self, meta)
def list(self, limit=None, marker=None):
"""Get a list of images.
:param limit: maximum number of images to return. Used for pagination.
:param marker: id of image last seen by caller. Used for pagination.
: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/detail%s" % query, "images")
def delete(self, image):
"""Delete an image."""
self._delete("/images/%s" % base.getid(image))

38
glanceclient/v1/shell.py

@ -0,0 +1,38 @@
# Copyright 2012 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 copy
from glanceclient.common import utils
def do_image_list(gc, args):
"""List images."""
images = gc.images.list()
columns = ['ID', 'Name', 'Disk Format', 'Container Format', 'Size']
utils.print_list(images, columns)
@utils.arg('id', metavar='<IMAGE_ID>', help='ID of image to describe.')
def do_image_show(gc, args):
"""Describe a specific image."""
image = gc.images.get(args.id)
# Flatten image properties dict
info = copy.deepcopy(image._info)
for (k, v) in info.pop('properties').iteritems():
info['Property \'%s\'' % k] = v
utils.print_dict(info)

1
glanceclient/v1_1/__init__.py

@ -1 +0,0 @@
from keystoneclient.v2_0.client import Client

113
glanceclient/v1_1/client.py

@ -1,113 +0,0 @@
# 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
glanceclient/v1_1/images.py

@ -1,88 +0,0 @@
# 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 = {