Basic get/list operations work
* 'glance image-list' and 'glance image-show' work * Set up tests, pep8, venv
This commit is contained in:
parent
b5847df3e2
commit
c530de6389
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,3 +10,4 @@ build
|
||||
dist
|
||||
python_glanceclient.egg-info
|
||||
ChangeLog
|
||||
run_tests.err.log
|
||||
|
34
LICENSE
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,4 +1,3 @@
|
||||
include README.rst
|
||||
include LICENSE
|
||||
recursive-include docs *
|
||||
recursive-include tests *
|
||||
|
22
README.rst
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.
|
||||
|
@ -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)
|
@ -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,
|
@ -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
Normal file
121
glanceclient/common/http.py
Normal file
@ -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)
|
@ -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', '')
|
@ -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)
|
@ -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"
|
@ -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
|
@ -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 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='<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
Normal file
1
glanceclient/v1/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from glanceclient.v1.client import Client
|
38
glanceclient/v1/client.py
Normal file
38
glanceclient/v1/client.py
Normal file
@ -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
Normal file
70
glanceclient/v1/images.py
Normal file
@ -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
Executable file
38
glanceclient/v1/shell.py
Executable file
@ -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 +0,0 @@
|
||||
from keystoneclient.v2_0.client import Client
|
@ -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")
|
@ -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 = {
|
||||
"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))
|
@ -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='<tenant-id>', nargs='?', default=None,
|
||||
help='Tenant ID (Optional); lists all images if not specified')
|
||||
def do_image_list(gc, args):
|
||||
"""List images"""
|
||||
images = gc.images.list(tenant_id=args.tenant)
|
||||
utils.print_list(images, ['id', 'is_public', 'email', 'name'])
|
||||
|
||||
|
||||
@utils.arg('--name', metavar='<image-name>', required=True,
|
||||
help='New image name (must be unique)')
|
||||
@utils.arg('--is-public', metavar='<true|false>', default=True,
|
||||
help='Initial image is_public status (default true)')
|
||||
def do_image_create(gc, args):
|
||||
"""Create new image"""
|
||||
image = gc.images.create(args.name, args.passwd, args.email,
|
||||
tenant_id=args.tenant_id, is_public=args.is_public)
|
||||
utils.print_dict(image._info)
|
||||
|
||||
|
||||
@utils.arg('--name', metavar='<image-name>',
|
||||
help='Desired new image name')
|
||||
@utils.arg('--is-public', metavar='<true|false>',
|
||||
help='Enable or disable image')
|
||||
@utils.arg('id', metavar='<image-id>', help='Image ID to update')
|
||||
def do_image_update(gc, args):
|
||||
"""Update image's name, email, and is_public status"""
|
||||
kwargs = {}
|
||||
if args.name:
|
||||
kwargs['name'] = args.name
|
||||
if args.email:
|
||||
kwargs['email'] = args.email
|
||||
if args.is_public:
|
||||
kwargs['is_public'] = utils.string_to_bool(args.is_public)
|
||||
|
||||
if not len(kwargs):
|
||||
print "User not updated, no arguments present."
|
||||
return
|
||||
|
||||
try:
|
||||
gc.images.update(args.id, **kwargs)
|
||||
print 'User has been updated.'
|
||||
except Exception, e:
|
||||
print 'Unable to update image: %s' % e
|
||||
|
||||
|
||||
@utils.arg('id', metavar='<image-id>', help='User ID to delete')
|
||||
def do_image_delete(gc, args):
|
||||
"""Delete image"""
|
||||
gc.images.delete(args.id)
|
||||
|
||||
|
||||
def do_token_get(gc, args):
|
||||
"""Display the current user's token"""
|
||||
utils.print_dict(gc.service_catalog.get_token())
|
360
run_tests.py
Normal file
360
run_tests.py
Normal file
@ -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))
|
96
run_tests.sh
96
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
|
||||
|
42
setup.py
42
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']},
|
||||
)
|
||||
|
5
tests/test_test.py
Normal file
5
tests/test_test.py
Normal file
@ -0,0 +1,5 @@
|
||||
import unittest
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
def test_one(self):
|
||||
self.assertTrue(True)
|
153
tools/install_venv.py
Normal file
153
tools/install_venv.py
Normal file
@ -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 <your command>
|
||||
|
||||
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)
|
@ -2,3 +2,5 @@ argparse
|
||||
httplib2
|
||||
prettytable
|
||||
simplejson
|
||||
|
||||
-e git://github.com/openstack/python-keystoneclient.git#egg=keystoneclient
|
||||
|
@ -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
|
||||
|
4
tools/with_venv.sh
Executable file
4
tools/with_venv.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
TOOLS=`dirname $0`
|
||||
VENV=$TOOLS/../.venv
|
||||
source $VENV/bin/activate && $@
|
Loading…
Reference in New Issue
Block a user