From c530de638916d29c609f66194569f57234a68289 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 26 Mar 2012 22:48:48 -0700 Subject: [PATCH] Basic get/list operations work * 'glance image-list' and 'glance image-show' work * Set up tests, pep8, venv --- .gitignore | 1 + LICENSE | 34 -- MANIFEST.in | 1 - README.rst | 22 +- glanceclient/client.py | 175 --------- glanceclient/{generic => common}/__init__.py | 0 glanceclient/{ => common}/base.py | 46 +-- glanceclient/{ => common}/exceptions.py | 4 +- glanceclient/common/http.py | 121 +++++++ glanceclient/{ => common}/utils.py | 55 ++- glanceclient/generic/client.py | 205 ----------- glanceclient/generic/shell.py | 57 --- glanceclient/service_catalog.py | 81 ----- glanceclient/shell.py | 159 ++++---- glanceclient/v1/__init__.py | 1 + glanceclient/v1/client.py | 38 ++ glanceclient/v1/images.py | 70 ++++ glanceclient/v1/shell.py | 38 ++ glanceclient/v1_1/__init__.py | 1 - glanceclient/v1_1/client.py | 113 ------ glanceclient/v1_1/images.py | 88 ----- glanceclient/v1_1/shell.py | 77 ---- run_tests.py | 360 +++++++++++++++++++ run_tests.sh | 96 +---- setup.py | 42 ++- tests/test_test.py | 5 + tools/install_venv.py | 153 ++++++++ tools/pip-requires | 2 + tools/test-requires | 3 +- tools/with_venv.sh | 4 + 30 files changed, 952 insertions(+), 1100 deletions(-) delete mode 100644 glanceclient/client.py rename glanceclient/{generic => common}/__init__.py (100%) rename glanceclient/{ => common}/base.py (78%) rename glanceclient/{ => common}/exceptions.py (95%) create mode 100644 glanceclient/common/http.py rename glanceclient/{ => common}/utils.py (59%) delete mode 100644 glanceclient/generic/client.py delete mode 100644 glanceclient/generic/shell.py delete mode 100644 glanceclient/service_catalog.py create mode 100644 glanceclient/v1/__init__.py create mode 100644 glanceclient/v1/client.py create mode 100644 glanceclient/v1/images.py create mode 100755 glanceclient/v1/shell.py delete mode 100644 glanceclient/v1_1/__init__.py delete mode 100644 glanceclient/v1_1/client.py delete mode 100644 glanceclient/v1_1/images.py delete mode 100755 glanceclient/v1_1/shell.py create mode 100644 run_tests.py create mode 100644 tests/test_test.py create mode 100644 tools/install_venv.py create mode 100755 tools/with_venv.sh diff --git a/.gitignore b/.gitignore index 811f856c..0dfded15 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ build dist python_glanceclient.egg-info ChangeLog +run_tests.err.log diff --git a/LICENSE b/LICENSE index 32b66114..67db8588 100644 --- a/LICENSE +++ b/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. diff --git a/MANIFEST.in b/MANIFEST.in index b023d223..6742c856 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ include README.rst include LICENSE -recursive-include docs * recursive-include tests * diff --git a/README.rst b/README.rst index f865371f..a797e17a 100644 --- a/README.rst +++ b/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] ... 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. diff --git a/glanceclient/client.py b/glanceclient/client.py deleted file mode 100644 index 09755842..00000000 --- a/glanceclient/client.py +++ /dev/null @@ -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) diff --git a/glanceclient/generic/__init__.py b/glanceclient/common/__init__.py similarity index 100% rename from glanceclient/generic/__init__.py rename to glanceclient/common/__init__.py diff --git a/glanceclient/base.py b/glanceclient/common/base.py similarity index 78% rename from glanceclient/base.py rename to glanceclient/common/base.py index ad6fb2e5..f61a1bdb 100644 --- a/glanceclient/base.py +++ b/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, diff --git a/glanceclient/exceptions.py b/glanceclient/common/exceptions.py similarity index 95% rename from glanceclient/exceptions.py rename to glanceclient/common/exceptions.py index f52800d8..e2b71cbf 100644 --- a/glanceclient/exceptions.py +++ b/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: diff --git a/glanceclient/common/http.py b/glanceclient/common/http.py new file mode 100644 index 00000000..01463ce1 --- /dev/null +++ b/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) diff --git a/glanceclient/utils.py b/glanceclient/common/utils.py similarity index 59% rename from glanceclient/utils.py rename to glanceclient/common/utils.py index 9880294e..edd71f95 100644 --- a/glanceclient/utils.py +++ b/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): +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. """ - 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) + 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', '') diff --git a/glanceclient/generic/client.py b/glanceclient/generic/client.py deleted file mode 100644 index 65335728..00000000 --- a/glanceclient/generic/client.py +++ /dev/null @@ -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) diff --git a/glanceclient/generic/shell.py b/glanceclient/generic/shell.py deleted file mode 100644 index caffbba7..00000000 --- a/glanceclient/generic/shell.py +++ /dev/null @@ -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" diff --git a/glanceclient/service_catalog.py b/glanceclient/service_catalog.py deleted file mode 100644 index e302977a..00000000 --- a/glanceclient/service_catalog.py +++ /dev/null @@ -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 diff --git a/glanceclient/shell.py b/glanceclient/shell.py index 2eb9ed16..90645df3 100644 --- a/glanceclient/shell.py +++ b/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='') - - 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 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) + if not auth_reqd: + endpoint = args.os_image_url + token = args.os_auth_token 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) + if not args.os_username: + raise exc.CommandError("You must provide a username via" + " either --os-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.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.os_auth_url: + raise exc.CommandError("You must provide an auth url via" + " either --os-auth-url or via env[OS_AUTH_URL]") + + 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='', nargs='?', - help='Display help for ') + help='Display help for ') 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) diff --git a/glanceclient/v1/__init__.py b/glanceclient/v1/__init__.py new file mode 100644 index 00000000..5ac6b0cf --- /dev/null +++ b/glanceclient/v1/__init__.py @@ -0,0 +1 @@ +from glanceclient.v1.client import Client diff --git a/glanceclient/v1/client.py b/glanceclient/v1/client.py new file mode 100644 index 00000000..bcb5bad4 --- /dev/null +++ b/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) diff --git a/glanceclient/v1/images.py b/glanceclient/v1/images.py new file mode 100644 index 00000000..442b23ec --- /dev/null +++ b/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 "" % 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)) diff --git a/glanceclient/v1/shell.py b/glanceclient/v1/shell.py new file mode 100755 index 00000000..2bb39494 --- /dev/null +++ b/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='', 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) diff --git a/glanceclient/v1_1/__init__.py b/glanceclient/v1_1/__init__.py deleted file mode 100644 index 44ad28e7..00000000 --- a/glanceclient/v1_1/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from keystoneclient.v2_0.client import Client diff --git a/glanceclient/v1_1/client.py b/glanceclient/v1_1/client.py deleted file mode 100644 index 735cecb4..00000000 --- a/glanceclient/v1_1/client.py +++ /dev/null @@ -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") diff --git a/glanceclient/v1_1/images.py b/glanceclient/v1_1/images.py deleted file mode 100644 index b76e560d..00000000 --- a/glanceclient/v1_1/images.py +++ /dev/null @@ -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 "" % 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)) diff --git a/glanceclient/v1_1/shell.py b/glanceclient/v1_1/shell.py deleted file mode 100755 index 7a34f786..00000000 --- a/glanceclient/v1_1/shell.py +++ /dev/null @@ -1,77 +0,0 @@ -# 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='', 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='', required=True, - help='New image name (must be unique)') -@utils.arg('--is-public', metavar='', 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='', - help='Desired new image name') -@utils.arg('--is-public', metavar='', - help='Enable or disable image') -@utils.arg('id', metavar='', 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='', 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()) diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 00000000..ffa35c6b --- /dev/null +++ b/run_tests.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + +# Colorizer Code is borrowed from Twisted: +# Copyright (c) 2001-2010 Twisted Matrix Laboratories. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Unittest runner for Nova. + +To run all tests + python run_tests.py + +To run a single test: + python run_tests.py test_compute:ComputeTestCase.test_run_terminate + +To run a single test module: + python run_tests.py test_compute + + or + + python run_tests.py api.test_wsgi + +""" + +import heapq +import os +import sys +import time +import unittest + +from nose import config +from nose import core +from nose import result + + +class _AnsiColorizer(object): + """ + A colorizer is an object that loosely wraps around a stream, allowing + callers to write text to the stream in a particular color. + + Colorizer classes must implement C{supported()} and C{write(text, color)}. + """ + _colors = dict(black=30, red=31, green=32, yellow=33, + blue=34, magenta=35, cyan=36, white=37) + + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + """ + A class method that returns True if the current platform supports + coloring terminal output using this method. Returns False otherwise. + """ + if not stream.isatty(): + return False # auto color only on TTYs + try: + import curses + except ImportError: + return False + else: + try: + try: + return curses.tigetnum("colors") > 2 + except curses.error: + curses.setupterm() + return curses.tigetnum("colors") > 2 + except: + raise + # guess false in case of error + return False + supported = classmethod(supported) + + def write(self, text, color): + """ + Write the given text to the stream in the given color. + + @param text: Text to be written to the stream. + + @param color: A string label for a color. e.g. 'red', 'white'. + """ + color = self._colors[color] + self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) + + +class _Win32Colorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + from win32console import (GetStdHandle, STD_OUT_HANDLE, + FOREGROUND_RED, FOREGROUND_GREEN, + FOREGROUND_BLUE, FOREGROUND_INTENSITY) + red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN, + FOREGROUND_BLUE, FOREGROUND_INTENSITY) + self.stream = stream + self.screenBuffer = GetStdHandle(STD_OUT_HANDLE) + self._colors = { + 'normal': red | green | blue, + 'red': red | bold, + 'green': green | bold, + 'blue': blue | bold, + 'yellow': red | green | bold, + 'magenta': red | blue | bold, + 'cyan': green | blue | bold, + 'white': red | green | blue | bold + } + + def supported(cls, stream=sys.stdout): + try: + import win32console + screenBuffer = win32console.GetStdHandle( + win32console.STD_OUT_HANDLE) + except ImportError: + return False + import pywintypes + try: + screenBuffer.SetConsoleTextAttribute( + win32console.FOREGROUND_RED | + win32console.FOREGROUND_GREEN | + win32console.FOREGROUND_BLUE) + except pywintypes.error: + return False + else: + return True + supported = classmethod(supported) + + def write(self, text, color): + color = self._colors[color] + self.screenBuffer.SetConsoleTextAttribute(color) + self.stream.write(text) + self.screenBuffer.SetConsoleTextAttribute(self._colors['normal']) + + +class _NullColorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + return True + supported = classmethod(supported) + + def write(self, text, color): + self.stream.write(text) + + +def get_elapsed_time_color(elapsed_time): + if elapsed_time > 1.0: + return 'red' + elif elapsed_time > 0.25: + return 'yellow' + else: + return 'green' + + +class NovaTestResult(result.TextTestResult): + def __init__(self, *args, **kw): + self.show_elapsed = kw.pop('show_elapsed') + result.TextTestResult.__init__(self, *args, **kw) + self.num_slow_tests = 5 + self.slow_tests = [] # this is a fixed-sized heap + self._last_case = None + self.colorizer = None + # NOTE(vish): reset stdout for the terminal check + stdout = sys.stdout + sys.stdout = sys.__stdout__ + for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]: + if colorizer.supported(): + self.colorizer = colorizer(self.stream) + break + sys.stdout = stdout + + # NOTE(lorinh): Initialize start_time in case a sqlalchemy-migrate + # error results in it failing to be initialized later. Otherwise, + # _handleElapsedTime will fail, causing the wrong error message to + # be outputted. + self.start_time = time.time() + + def getDescription(self, test): + return str(test) + + def _handleElapsedTime(self, test): + self.elapsed_time = time.time() - self.start_time + item = (self.elapsed_time, test) + # Record only the n-slowest tests using heap + if len(self.slow_tests) >= self.num_slow_tests: + heapq.heappushpop(self.slow_tests, item) + else: + heapq.heappush(self.slow_tests, item) + + def _writeElapsedTime(self, test): + color = get_elapsed_time_color(self.elapsed_time) + self.colorizer.write(" %.2f" % self.elapsed_time, color) + + def _writeResult(self, test, long_result, color, short_result, success): + if self.showAll: + self.colorizer.write(long_result, color) + if self.show_elapsed and success: + self._writeElapsedTime(test) + self.stream.writeln() + elif self.dots: + self.stream.write(short_result) + self.stream.flush() + + # NOTE(vish): copied from unittest with edit to add color + def addSuccess(self, test): + unittest.TestResult.addSuccess(self, test) + self._handleElapsedTime(test) + self._writeResult(test, 'OK', 'green', '.', True) + + # NOTE(vish): copied from unittest with edit to add color + def addFailure(self, test, err): + unittest.TestResult.addFailure(self, test, err) + self._handleElapsedTime(test) + self._writeResult(test, 'FAIL', 'red', 'F', False) + + # NOTE(vish): copied from nose with edit to add color + def addError(self, test, err): + """Overrides normal addError to add support for + errorClasses. If the exception is a registered class, the + error will be added to the list for that class, not errors. + """ + self._handleElapsedTime(test) + stream = getattr(self, 'stream', None) + ec, ev, tb = err + try: + exc_info = self._exc_info_to_string(err, test) + except TypeError: + # 2.3 compat + exc_info = self._exc_info_to_string(err) + for cls, (storage, label, isfail) in self.errorClasses.items(): + if result.isclass(ec) and issubclass(ec, cls): + if isfail: + test.passed = False + storage.append((test, exc_info)) + # Might get patched into a streamless result + if stream is not None: + if self.showAll: + message = [label] + detail = result._exception_detail(err[1]) + if detail: + message.append(detail) + stream.writeln(": ".join(message)) + elif self.dots: + stream.write(label[:1]) + return + self.errors.append((test, exc_info)) + test.passed = False + if stream is not None: + self._writeResult(test, 'ERROR', 'red', 'E', False) + + def startTest(self, test): + unittest.TestResult.startTest(self, test) + self.start_time = time.time() + current_case = test.test.__class__.__name__ + + if self.showAll: + if current_case != self._last_case: + self.stream.writeln(current_case) + self._last_case = current_case + + self.stream.write( + ' %s' % str(test.test._testMethodName).ljust(60)) + self.stream.flush() + + +class NovaTestRunner(core.TextTestRunner): + def __init__(self, *args, **kwargs): + self.show_elapsed = kwargs.pop('show_elapsed') + core.TextTestRunner.__init__(self, *args, **kwargs) + + def _makeResult(self): + return NovaTestResult(self.stream, + self.descriptions, + self.verbosity, + self.config, + show_elapsed=self.show_elapsed) + + def _writeSlowTests(self, result_): + # Pare out 'fast' tests + slow_tests = [item for item in result_.slow_tests + if get_elapsed_time_color(item[0]) != 'green'] + if slow_tests: + slow_total_time = sum(item[0] for item in slow_tests) + self.stream.writeln("Slowest %i tests took %.2f secs:" + % (len(slow_tests), slow_total_time)) + for elapsed_time, test in sorted(slow_tests, reverse=True): + time_str = "%.2f" % elapsed_time + self.stream.writeln(" %s %s" % (time_str.ljust(10), test)) + + def run(self, test): + result_ = core.TextTestRunner.run(self, test) + if self.show_elapsed: + self._writeSlowTests(result_) + return result_ + + +if __name__ == '__main__': + # If any argument looks like a test name but doesn't have "nova.tests" in + # front of it, automatically add that so we don't have to type as much + show_elapsed = True + argv = [] + for x in sys.argv: + if x.startswith('test_'): + pass + #argv.append('tests.%s' % x) + argv.append(x) + elif x.startswith('--hide-elapsed'): + show_elapsed = False + else: + argv.append(x) + + testdir = os.path.abspath(os.path.join("tests")) + c = config.Config(stream=sys.stdout, + env=os.environ, + verbosity=3, + workingDir=testdir, + plugins=core.DefaultPluginManager()) + + runner = NovaTestRunner(stream=c.stream, + verbosity=c.verbosity, + config=c, + show_elapsed=show_elapsed) + sys.exit(not core.run(config=c, testRunner=runner, argv=argv)) diff --git a/run_tests.sh b/run_tests.sh index 731d2985..d0d36458 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,21 +1,15 @@ #!/bin/bash -set -eu - function usage { echo "Usage: $0 [OPTION]..." - echo "Run python-glanceclient test suite" + echo "Run python-glanceclient's test suite(s)" echo "" echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" - echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment" - echo " -x, --stop Stop running tests after the first error or failure." echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." + echo " --unittests-only Run unit tests only, exclude functional tests." echo " -p, --pep8 Just run pep8" - echo " -P, --no-pep8 Don't run pep8" - echo " -c, --coverage Generate coverage report" echo " -h, --help Print this usage message" - echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list" echo "" echo "Note: with no options specified, the script will try to run the tests in a virtual environment," echo " If no virtualenv is found, the script will ask if you would like to create one. If you " @@ -26,14 +20,10 @@ function usage { function process_option { case "$1" in -h|--help) usage;; - -V|--virtual-env) always_venv=1; never_venv=0;; - -N|--no-virtual-env) always_venv=0; never_venv=1;; - -s|--no-site-packages) no_site_packages=1;; - -f|--force) force=1;; - -p|--pep8) just_pep8=1;; - -P|--no-pep8) no_pep8=1;; - -c|--coverage) coverage=1;; - -*) noseopts="$noseopts $1";; + -V|--virtual-env) let always_venv=1; let never_venv=0;; + -N|--no-virtual-env) let always_venv=0; let never_venv=1;; + -p|--pep8) let just_pep8=1;; + -f|--force) let force=1;; *) noseargs="$noseargs $1" esac } @@ -43,61 +33,29 @@ with_venv=tools/with_venv.sh always_venv=0 never_venv=0 force=0 -no_site_packages=0 -installvenvopts= noseargs= -noseopts= wrapper="" just_pep8=0 -no_pep8=0 -coverage=0 for arg in "$@"; do process_option $arg done -# If enabled, tell nose to collect coverage data -if [ $coverage -eq 1 ]; then - noseopts="$noseopts --with-coverage --cover-package=keystoneclient" -fi - -if [ $no_site_packages -eq 1 ]; then - installvenvopts="--no-site-packages" -fi - function run_tests { # Just run the test suites in current environment - ${wrapper} $NOSETESTS - # If we get some short import error right away, print the error log directly - RESULT=$? - return $RESULT + ${wrapper} rm -f tests.sqlite + ${wrapper} $NOSETESTS 2> run_tests.err.log } function run_pep8 { - echo "Running pep8 ..." - srcfiles="keystoneclient tests" - # Just run PEP8 in current environment - # - # NOTE(sirp): W602 (deprecated 3-arg raise) is being ignored for the - # following reasons: - # - # 1. It's needed to preserve traceback information when re-raising - # exceptions; this is needed b/c Eventlet will clear exceptions when - # switching contexts. - # - # 2. There doesn't appear to be an alternative, "pep8-tool" compatible way of doing this - # in Python 2 (in Python 3 `with_traceback` could be used). - # - # 3. Can find no corroborating evidence that this is deprecated in Python 2 - # other than what the PEP8 tool claims. It is deprecated in Python 3, so, - # perhaps the mistake was thinking that the deprecation applied to Python 2 - # as well. - ${wrapper} pep8 --repeat --show-pep8 --show-source \ - --ignore=E202,W602 \ - ${srcfiles} + echo "Running pep8..." + PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat" + PEP8_INCLUDE="glanceclient/* setup.py run_tests.py tools/install_venv.py" + ${wrapper} pep8 $PEP8_OPTIONS $PEP8_INCLUDE } -NOSETESTS="nosetests $noseopts $noseargs" + +NOSETESTS="python run_tests.py $noseargs" if [ $never_venv -eq 0 ] then @@ -111,43 +69,27 @@ then else if [ $always_venv -eq 1 ]; then # Automatically install the virtualenv - python tools/install_venv.py $installvenvopts + python tools/install_venv.py wrapper="${with_venv}" else echo -e "No virtual environment found...create one? (Y/n) \c" read use_ve if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then # Install the virtualenv and run the test suite in it - python tools/install_venv.py $installvenvopts - wrapper=${with_venv} + python tools/install_venv.py + wrapper=${with_venv} fi fi fi fi -# Delete old coverage data from previous runs -if [ $coverage -eq 1 ]; then - ${wrapper} coverage erase -fi - if [ $just_pep8 -eq 1 ]; then run_pep8 exit fi -run_tests +run_tests || exit -# NOTE(sirp): we only want to run pep8 when we're running the full-test suite, -# not when we're running tests individually. To handle this, we need to -# distinguish between options (noseopts), which begin with a '-', and -# arguments (noseargs). if [ -z "$noseargs" ]; then - if [ $no_pep8 -eq 0 ]; then - run_pep8 - fi -fi - -if [ $coverage -eq 1 ]; then - echo "Generating coverage report in covhtml/" - ${wrapper} coverage html -d covhtml -i + run_pep8 fi diff --git a/setup.py b/setup.py index f989680b..98f04449 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,15 @@ import os -import sys -from setuptools import setup, find_packages + +import setuptools from glanceclient.openstack.common.setup import parse_requirements from glanceclient.openstack.common.setup import parse_dependency_links from glanceclient.openstack.common.setup import write_requirements from glanceclient.openstack.common.setup import write_git_changelog + requires = parse_requirements() -depend_links = parse_dependency_links() +dependency_links = parse_dependency_links() write_requirements() write_git_changelog() @@ -16,17 +17,17 @@ write_git_changelog() def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() -setup( - name = "python-glanceclient", - version = "2012.1", - description = "Client library for OpenStack Glance API", - long_description = read('README.rst'), - url = 'https://github.com/openstack/python-glanceclient', - license = 'Apache', - author = 'Jay Pipes, based on work by Rackspace and Jacob Kaplan-Moss', - author_email = 'jay.pipes@gmail.com', - packages = find_packages(exclude=['tests', 'tests.*']), - classifiers = [ +setuptools.setup( + name="python-glanceclient", + version="2012.1", + description="Client library for OpenStack Glance API", + long_description=read('README.rst'), + url='https://github.com/openstack/python-glanceclient', + license='Apache', + author='OpenStack Glance Contributors', + author_email='glance@example.com', + packages=setuptools.find_packages(exclude=['tests', 'tests.*']), + classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: Developers', @@ -35,12 +36,9 @@ setup( 'Operating System :: OS Independent', 'Programming Language :: Python', ], - install_requires=requires, - dependency_links=depend_links, - - test_suite = "nose.collector", - - entry_points = { - 'console_scripts': ['glance = glanceclient.shell:main'] - } + #install_requires=requires, + install_requires=[], + dependency_links=dependency_links, + test_suite="nose.collector", + entry_points={'console_scripts': ['glance = glanceclient.shell:main']}, ) diff --git a/tests/test_test.py b/tests/test_test.py new file mode 100644 index 00000000..ec6a6e5c --- /dev/null +++ b/tests/test_test.py @@ -0,0 +1,5 @@ +import unittest + +class TestCase(unittest.TestCase): + def test_one(self): + self.assertTrue(True) diff --git a/tools/install_venv.py b/tools/install_venv.py new file mode 100644 index 00000000..ddf0a957 --- /dev/null +++ b/tools/install_venv.py @@ -0,0 +1,153 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2010 OpenStack LLC. +# +# 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. + +""" +Installation script for Glance's development virtualenv +""" + +import os +import subprocess +import sys + + +ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +VENV = os.path.join(ROOT, '.venv') +PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') +TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires') + + +def die(message, *args): + print >> sys.stderr, message % args + sys.exit(1) + + +def run_command(cmd, redirect_output=True, check_exit_code=True): + """ + Runs a command in an out-of-process shell, returning the + output of that command. Working directory is ROOT. + """ + if redirect_output: + stdout = subprocess.PIPE + else: + stdout = None + + proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout) + output = proc.communicate()[0] + if check_exit_code and proc.returncode != 0: + die('Command "%s" failed.\n%s', ' '.join(cmd), output) + return output + + +HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'], + check_exit_code=False).strip()) +HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'], + check_exit_code=False).strip()) + + +def check_dependencies(): + """Make sure virtualenv is in the path.""" + + if not HAS_VIRTUALENV: + print 'not found.' + # Try installing it via easy_install... + if HAS_EASY_INSTALL: + print 'Installing virtualenv via easy_install...', + if not run_command(['which', 'easy_install']): + die('ERROR: virtualenv not found.\n\n' + 'Glance development requires virtualenv, please install' + ' it using your favorite package management tool') + print 'done.' + print 'done.' + + +def create_virtualenv(venv=VENV): + """ + Creates the virtual environment and installs PIP only into the + virtual environment + """ + print 'Creating venv...', + run_command(['virtualenv', '-q', '--no-site-packages', VENV]) + print 'done.' + print 'Installing pip in virtualenv...', + if not run_command(['tools/with_venv.sh', 'easy_install', + 'pip>1.0']).strip(): + die("Failed to install pip.") + print 'done.' + + +def pip_install(*args): + run_command(['tools/with_venv.sh', + 'pip', 'install', '--upgrade'] + list(args), + redirect_output=False) + + +def install_dependencies(venv=VENV): + print 'Installing dependencies with pip (this can take a while)...' + + pip_install('pip') + + pip_install('-r', PIP_REQUIRES) + pip_install('-r', TEST_REQUIRES) + + # Tell the virtual env how to "import glance" + py_ver = _detect_python_version(venv) + pthfile = os.path.join(venv, "lib", py_ver, "site-packages", "glance.pth") + f = open(pthfile, 'w') + f.write("%s\n" % ROOT) + + +def _detect_python_version(venv): + lib_dir = os.path.join(venv, "lib") + for pathname in os.listdir(lib_dir): + if pathname.startswith('python'): + return pathname + raise Exception('Unable to detect Python version') + + +def print_help(): + help = """ + Glance development environment setup is complete. + + Glance development uses virtualenv to track and manage Python dependencies + while in development and testing. + + To activate the Glance virtualenv for the extent of your current shell session + you can run: + + $ source .venv/bin/activate + + Or, if you prefer, you can run commands in the virtualenv on a case by case + basis by running: + + $ tools/with_venv.sh + + Also, make test will automatically use the virtualenv. + """ + print help + + +def main(argv): + check_dependencies() + create_virtualenv() + install_dependencies() + print_help() + +if __name__ == '__main__': + main(sys.argv) diff --git a/tools/pip-requires b/tools/pip-requires index 32cdbc0b..01e5e25c 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -2,3 +2,5 @@ argparse httplib2 prettytable simplejson + +-e git://github.com/openstack/python-keystoneclient.git#egg=keystoneclient diff --git a/tools/test-requires b/tools/test-requires index 7c320d42..572b13d9 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -1,9 +1,8 @@ distribute>=0.6.24 -coverage -mock>=0.7.1 mox nose +nose-exclude nosexcover openstack.nose_plugin pep8==0.6.1 diff --git a/tools/with_venv.sh b/tools/with_venv.sh new file mode 100755 index 00000000..c8d2940f --- /dev/null +++ b/tools/with_venv.sh @@ -0,0 +1,4 @@ +#!/bin/bash +TOOLS=`dirname $0` +VENV=$TOOLS/../.venv +source $VENV/bin/activate && $@