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
|
dist
|
||||||
python_glanceclient.egg-info
|
python_glanceclient.egg-info
|
||||||
ChangeLog
|
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
|
Apache License
|
||||||
Version 2.0, January 2004
|
Version 2.0, January 2004
|
||||||
@ -178,32 +173,3 @@ All rights reserved.
|
|||||||
defend, and hold each Contributor harmless for any liability
|
defend, and hold each Contributor harmless for any liability
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
of your accepting any such warranty or additional liability.
|
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 README.rst
|
||||||
include LICENSE
|
include LICENSE
|
||||||
recursive-include docs *
|
|
||||||
recursive-include tests *
|
recursive-include tests *
|
||||||
|
22
README.rst
22
README.rst
@ -28,7 +28,7 @@ Python API
|
|||||||
By way of a quick-start::
|
By way of a quick-start::
|
||||||
|
|
||||||
# use v2.0 auth with http://example.com:5000/v2.0")
|
# 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 = client.Client(username=USERNAME, password=PASSWORD, tenant_name=TENANT, auth_url=KEYSTONE_URL)
|
||||||
>>> glance.images.list()
|
>>> glance.images.list()
|
||||||
>>> image = glance.images.create(name="My Test Image")
|
>>> 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
|
Installing this package gets you a command-line tool, ``glance``, that you
|
||||||
can use to interact with Glance's Identity API.
|
can use to interact with Glance's Identity API.
|
||||||
|
|
||||||
You'll need to provide your OpenStack tenant, username and password. You can do this
|
You'll need to provide your OpenStack username, password, tenant, and auth
|
||||||
with the ``tenant_name``, ``--username`` and ``--password`` params, but it's
|
endpoint. You can do this with the ``--tenant_id``, ``--username``,
|
||||||
easier to just set them as environment variables::
|
``--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_USERNAME=user
|
||||||
export OS_PASSWORD=pass
|
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_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
|
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
|
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``::
|
``glance help``::
|
||||||
|
|
||||||
usage: glance [--username USERNAME] [--password PASSWORD]
|
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]
|
[--auth_url AUTH_URL] [--region_name REGION_NAME]
|
||||||
[--identity_api_version IDENTITY_API_VERSION]
|
|
||||||
<subcommand> ...
|
<subcommand> ...
|
||||||
|
|
||||||
Command-line interface to the OpenStack Identity API.
|
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]
|
--auth_url AUTH_URL Defaults to env[OS_AUTH_URL]
|
||||||
--region_name REGION_NAME
|
--region_name REGION_NAME
|
||||||
Defaults to env[OS_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.
|
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 2012 OpenStack LLC.
|
||||||
# Copyright 2011 OpenStack LLC.
|
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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.
|
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
|
# Python 2.4 compat
|
||||||
@ -81,7 +80,7 @@ class Manager(object):
|
|||||||
return self.resource_class(self, body[response_key])
|
return self.resource_class(self, body[response_key])
|
||||||
|
|
||||||
def _delete(self, url):
|
def _delete(self, url):
|
||||||
resp, body = self.api.delete(url)
|
self.api.delete(url)
|
||||||
|
|
||||||
def _update(self, url, body, response_key=None, method="PUT"):
|
def _update(self, url, body, response_key=None, method="PUT"):
|
||||||
methods = {"PUT": self.api.put,
|
methods = {"PUT": self.api.put,
|
||||||
@ -96,45 +95,6 @@ class Manager(object):
|
|||||||
return self.resource_class(self, body[response_key])
|
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):
|
class Resource(object):
|
||||||
"""
|
"""
|
||||||
A resource represents a particular instance of an object (tenant, user,
|
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.
|
Exception definitions.
|
||||||
"""
|
"""
|
||||||
@ -125,7 +123,7 @@ def from_response(response, body):
|
|||||||
else:
|
else:
|
||||||
# If we didn't get back a properly formed error message we
|
# If we didn't get back a properly formed error message we
|
||||||
# probably couldn't communicate with Keystone at all.
|
# 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
|
details = None
|
||||||
return cls(code=response.status, message=message, details=details)
|
return cls(code=response.status, message=message, details=details)
|
||||||
else:
|
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 uuid
|
||||||
|
|
||||||
import prettytable
|
import prettytable
|
||||||
|
|
||||||
from glanceclient import exceptions
|
from glanceclient.common import exceptions
|
||||||
|
|
||||||
|
|
||||||
# Decorator for cli-args
|
# Decorator for cli-args
|
||||||
@ -69,26 +85,33 @@ def find_resource(manager, name_or_id):
|
|||||||
raise exceptions.CommandError(msg)
|
raise exceptions.CommandError(msg)
|
||||||
|
|
||||||
|
|
||||||
def unauthenticated(f):
|
def skip_authentication(f):
|
||||||
""" Adds 'unauthenticated' attribute to decorated function.
|
"""Function decorator used to indicate a caller may be unauthenticated."""
|
||||||
|
f.require_authentication = False
|
||||||
Usage:
|
|
||||||
@unauthenticated
|
|
||||||
def mymethod(f):
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
f.unauthenticated = True
|
|
||||||
return f
|
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
|
return getattr(f, 'require_authentication', True)
|
||||||
with the @unauthenticated decorator. Returns True if decorator is
|
|
||||||
set to True, False otherwise.
|
|
||||||
"""
|
|
||||||
return getattr(f, 'unauthenticated', False)
|
|
||||||
|
|
||||||
|
|
||||||
def string_to_bool(arg):
|
def string_to_bool(arg):
|
||||||
return arg.strip().lower() in ('t', 'true', 'yes', '1')
|
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 2012 OpenStack LLC.
|
||||||
# Copyright 2011 OpenStack LLC.
|
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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 argparse
|
||||||
import httplib2
|
import httplib2
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from glanceclient import exceptions as exc
|
from keystoneclient.v2_0 import client as ksclient
|
||||||
from glanceclient import utils
|
|
||||||
from glanceclient.v2_0 import shell as shell_v2_0
|
|
||||||
from glanceclient.generic import shell as shell_generic
|
|
||||||
|
|
||||||
|
from glanceclient.common import exceptions as exc
|
||||||
def env(*vars, **kwargs):
|
from glanceclient.common import utils
|
||||||
"""Search for the first defined of possibly many env vars
|
from glanceclient.v1 import shell as shell_v1
|
||||||
|
from glanceclient.v1 import client as client_v1
|
||||||
Returns the first environment variable defined in vars, or
|
|
||||||
returns the default defined in kwargs.
|
|
||||||
|
|
||||||
"""
|
|
||||||
for v in vars:
|
|
||||||
value = os.environ.get(v, None)
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
return kwargs.get('default', '')
|
|
||||||
|
|
||||||
|
|
||||||
class OpenStackImagesShell(object):
|
class OpenStackImagesShell(object):
|
||||||
@ -52,7 +38,7 @@ class OpenStackImagesShell(object):
|
|||||||
epilog='See "glance help COMMAND" '\
|
epilog='See "glance help COMMAND" '\
|
||||||
'for help on a specific command.',
|
'for help on a specific command.',
|
||||||
add_help=False,
|
add_help=False,
|
||||||
formatter_class=OpenStackHelpFormatter,
|
formatter_class=HelpFormatter,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Global arguments
|
# Global arguments
|
||||||
@ -66,33 +52,33 @@ class OpenStackImagesShell(object):
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help=argparse.SUPPRESS)
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument('--username',
|
parser.add_argument('--os-username',
|
||||||
default=env('OS_USERNAME'),
|
default=utils.env('OS_USERNAME'),
|
||||||
help='Defaults to env[OS_USERNAME]')
|
help='Defaults to env[OS_USERNAME]')
|
||||||
|
|
||||||
parser.add_argument('--password',
|
parser.add_argument('--os-password',
|
||||||
default=env('OS_PASSWORD'),
|
default=utils.env('OS_PASSWORD'),
|
||||||
help='Defaults to env[OS_PASSWORD]')
|
help='Defaults to env[OS_PASSWORD]')
|
||||||
|
|
||||||
parser.add_argument('--tenant_name',
|
parser.add_argument('--os-tenant-id',
|
||||||
default=env('OS_TENANT_NAME'),
|
default=utils.env('OS_TENANT_ID'),
|
||||||
help='Defaults to env[OS_TENANT_NAME]')
|
|
||||||
|
|
||||||
parser.add_argument('--tenant_id',
|
|
||||||
default=env('OS_TENANT_ID'), dest='os_tenant_id',
|
|
||||||
help='Defaults to env[OS_TENANT_ID]')
|
help='Defaults to env[OS_TENANT_ID]')
|
||||||
|
|
||||||
parser.add_argument('--auth_url',
|
parser.add_argument('--os-auth-url',
|
||||||
default=env('OS_AUTH_URL'),
|
default=utils.env('OS_AUTH_URL'),
|
||||||
help='Defaults to env[OS_AUTH_URL]')
|
help='Defaults to env[OS_AUTH_URL]')
|
||||||
|
|
||||||
parser.add_argument('--region_name',
|
parser.add_argument('--os-region-name',
|
||||||
default=env('OS_REGION_NAME'),
|
default=utils.env('OS_REGION_NAME'),
|
||||||
help='Defaults to env[OS_REGION_NAME]')
|
help='Defaults to env[OS_REGION_NAME]')
|
||||||
|
|
||||||
parser.add_argument('--identity_api_version',
|
parser.add_argument('--os-auth-token',
|
||||||
default=env('OS_IDENTITY_API_VERSION', 'KEYSTONE_VERSION'),
|
default=utils.env('OS_AUTH_TOKEN'),
|
||||||
help='Defaults to env[OS_IDENTITY_API_VERSION] or 2.0')
|
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
|
return parser
|
||||||
|
|
||||||
@ -101,16 +87,7 @@ class OpenStackImagesShell(object):
|
|||||||
|
|
||||||
self.subcommands = {}
|
self.subcommands = {}
|
||||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||||
|
self._find_actions(subparsers, shell_v1)
|
||||||
try:
|
|
||||||
actions_module = {
|
|
||||||
'2.0': shell_v2_0,
|
|
||||||
}[version]
|
|
||||||
except KeyError:
|
|
||||||
actions_module = shell_v2_0
|
|
||||||
|
|
||||||
self._find_actions(subparsers, actions_module)
|
|
||||||
self._find_actions(subparsers, shell_generic)
|
|
||||||
self._find_actions(subparsers, self)
|
self._find_actions(subparsers, self)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
@ -128,7 +105,7 @@ class OpenStackImagesShell(object):
|
|||||||
help=help,
|
help=help,
|
||||||
description=desc,
|
description=desc,
|
||||||
add_help=False,
|
add_help=False,
|
||||||
formatter_class=OpenStackHelpFormatter
|
formatter_class=HelpFormatter
|
||||||
)
|
)
|
||||||
subparser.add_argument('-h', '--help',
|
subparser.add_argument('-h', '--help',
|
||||||
action='help',
|
action='help',
|
||||||
@ -139,19 +116,28 @@ class OpenStackImagesShell(object):
|
|||||||
subparser.add_argument(*args, **kwargs)
|
subparser.add_argument(*args, **kwargs)
|
||||||
subparser.set_defaults(func=callback)
|
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):
|
def main(self, argv):
|
||||||
# Parse args once to find version
|
# Parse args once to find version
|
||||||
parser = self.get_base_parser()
|
parser = self.get_base_parser()
|
||||||
(options, args) = parser.parse_known_args(argv)
|
(options, args) = parser.parse_known_args(argv)
|
||||||
|
|
||||||
# build available subcommands based on version
|
# build available subcommands based on version
|
||||||
api_version = options.identity_api_version
|
api_version = '1'
|
||||||
subcommand_parser = self.get_subcommand_parser(api_version)
|
subcommand_parser = self.get_subcommand_parser(api_version)
|
||||||
self.parser = subcommand_parser
|
self.parser = subcommand_parser
|
||||||
|
|
||||||
# Handle top-level --help/-h before attempting to parse
|
# Handle top-level --help/-h before attempting to parse
|
||||||
# a command off the command line
|
# a command off the command line
|
||||||
if options.help:
|
if options.help or not argv:
|
||||||
self.do_help(options)
|
self.do_help(options)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@ -167,51 +153,45 @@ class OpenStackImagesShell(object):
|
|||||||
self.do_help(args)
|
self.do_help(args)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
#FIXME(usrleon): Here should be restrict for project id same as
|
auth_reqd = (utils.is_authentication_required(args.func) or
|
||||||
# for username or apikey but for compatibility it is not.
|
not (args.os_auth_token and args.os_image_url))
|
||||||
|
|
||||||
if not utils.isunauthenticated(args.func):
|
if not auth_reqd:
|
||||||
if not args.username:
|
endpoint = args.os_image_url
|
||||||
raise exc.CommandError("You must provide a username "
|
token = args.os_auth_token
|
||||||
"via either --username or env[OS_USERNAME]")
|
|
||||||
|
|
||||||
if not args.password:
|
|
||||||
raise exc.CommandError("You must provide a password "
|
|
||||||
"via either --password or env[OS_PASSWORD]")
|
|
||||||
|
|
||||||
if not args.auth_url:
|
|
||||||
raise exc.CommandError("You must provide an auth url "
|
|
||||||
"via either --auth_url or via env[OS_AUTH_URL]")
|
|
||||||
|
|
||||||
if utils.isunauthenticated(args.func):
|
|
||||||
self.cs = shell_generic.CLIENT_CLASS(endpoint=args.auth_url)
|
|
||||||
else:
|
else:
|
||||||
api_version = options.identity_api_version
|
if not args.os_username:
|
||||||
self.cs = self.get_api_class(api_version)(
|
raise exc.CommandError("You must provide a username via"
|
||||||
username=args.username,
|
" either --os-username or env[OS_USERNAME]")
|
||||||
tenant_name=args.tenant_name,
|
|
||||||
tenant_id=args.os_tenant_id,
|
if not args.os_password:
|
||||||
password=args.password,
|
raise exc.CommandError("You must provide a password via"
|
||||||
auth_url=args.auth_url,
|
" either --os-password or env[OS_PASSWORD]")
|
||||||
region_name=args.region_name)
|
|
||||||
|
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:
|
try:
|
||||||
args.func(self.cs, args)
|
args.func(image_service, args)
|
||||||
except exc.Unauthorized:
|
except exc.Unauthorized:
|
||||||
raise exc.CommandError("Invalid OpenStack Identity credentials.")
|
raise exc.CommandError("Invalid OpenStack Identity credentials.")
|
||||||
except exc.AuthorizationFailure:
|
except exc.AuthorizationFailure:
|
||||||
raise exc.CommandError("Unable to authorize user")
|
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='?',
|
@utils.arg('command', metavar='<subcommand>', nargs='?',
|
||||||
help='Display help for <subcommand>')
|
help='Display help for <subcommand>')
|
||||||
def do_help(self, args):
|
def do_help(self, args):
|
||||||
"""
|
"""
|
||||||
Display help about this program or one of its subcommands.
|
Display help about this program or one of its subcommands.
|
||||||
@ -226,12 +206,11 @@ class OpenStackImagesShell(object):
|
|||||||
self.parser.print_help()
|
self.parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
# I'm picky about my shell help.
|
class HelpFormatter(argparse.HelpFormatter):
|
||||||
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
|
||||||
def start_section(self, heading):
|
def start_section(self, heading):
|
||||||
# Title-case the headings
|
# Title-case the headings
|
||||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||||
super(OpenStackHelpFormatter, self).start_section(heading)
|
super(HelpFormatter, self).start_section(heading)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -240,7 +219,7 @@ def main():
|
|||||||
|
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
if httplib2.debuglevel == 1:
|
if httplib2.debuglevel == 1:
|
||||||
raise # dump stack.
|
raise
|
||||||
else:
|
else:
|
||||||
print >> sys.stderr, e
|
print >> sys.stderr, e
|
||||||
sys.exit(1)
|
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
|
#!/bin/bash
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
function usage {
|
function usage {
|
||||||
echo "Usage: $0 [OPTION]..."
|
echo "Usage: $0 [OPTION]..."
|
||||||
echo "Run python-glanceclient test suite"
|
echo "Run python-glanceclient's test suite(s)"
|
||||||
echo ""
|
echo ""
|
||||||
echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
|
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 " -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 " -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, --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 " -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 ""
|
||||||
echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
|
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 "
|
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 {
|
function process_option {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h|--help) usage;;
|
-h|--help) usage;;
|
||||||
-V|--virtual-env) always_venv=1; never_venv=0;;
|
-V|--virtual-env) let always_venv=1; let never_venv=0;;
|
||||||
-N|--no-virtual-env) always_venv=0; never_venv=1;;
|
-N|--no-virtual-env) let always_venv=0; let never_venv=1;;
|
||||||
-s|--no-site-packages) no_site_packages=1;;
|
-p|--pep8) let just_pep8=1;;
|
||||||
-f|--force) force=1;;
|
-f|--force) let force=1;;
|
||||||
-p|--pep8) just_pep8=1;;
|
|
||||||
-P|--no-pep8) no_pep8=1;;
|
|
||||||
-c|--coverage) coverage=1;;
|
|
||||||
-*) noseopts="$noseopts $1";;
|
|
||||||
*) noseargs="$noseargs $1"
|
*) noseargs="$noseargs $1"
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@ -43,61 +33,29 @@ with_venv=tools/with_venv.sh
|
|||||||
always_venv=0
|
always_venv=0
|
||||||
never_venv=0
|
never_venv=0
|
||||||
force=0
|
force=0
|
||||||
no_site_packages=0
|
|
||||||
installvenvopts=
|
|
||||||
noseargs=
|
noseargs=
|
||||||
noseopts=
|
|
||||||
wrapper=""
|
wrapper=""
|
||||||
just_pep8=0
|
just_pep8=0
|
||||||
no_pep8=0
|
|
||||||
coverage=0
|
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
process_option $arg
|
process_option $arg
|
||||||
done
|
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 {
|
function run_tests {
|
||||||
# Just run the test suites in current environment
|
# Just run the test suites in current environment
|
||||||
${wrapper} $NOSETESTS
|
${wrapper} rm -f tests.sqlite
|
||||||
# If we get some short import error right away, print the error log directly
|
${wrapper} $NOSETESTS 2> run_tests.err.log
|
||||||
RESULT=$?
|
|
||||||
return $RESULT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function run_pep8 {
|
function run_pep8 {
|
||||||
echo "Running pep8 ..."
|
echo "Running pep8..."
|
||||||
srcfiles="keystoneclient tests"
|
PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat"
|
||||||
# Just run PEP8 in current environment
|
PEP8_INCLUDE="glanceclient/* setup.py run_tests.py tools/install_venv.py"
|
||||||
#
|
${wrapper} pep8 $PEP8_OPTIONS $PEP8_INCLUDE
|
||||||
# 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}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NOSETESTS="nosetests $noseopts $noseargs"
|
|
||||||
|
NOSETESTS="python run_tests.py $noseargs"
|
||||||
|
|
||||||
if [ $never_venv -eq 0 ]
|
if [ $never_venv -eq 0 ]
|
||||||
then
|
then
|
||||||
@ -111,43 +69,27 @@ then
|
|||||||
else
|
else
|
||||||
if [ $always_venv -eq 1 ]; then
|
if [ $always_venv -eq 1 ]; then
|
||||||
# Automatically install the virtualenv
|
# Automatically install the virtualenv
|
||||||
python tools/install_venv.py $installvenvopts
|
python tools/install_venv.py
|
||||||
wrapper="${with_venv}"
|
wrapper="${with_venv}"
|
||||||
else
|
else
|
||||||
echo -e "No virtual environment found...create one? (Y/n) \c"
|
echo -e "No virtual environment found...create one? (Y/n) \c"
|
||||||
read use_ve
|
read use_ve
|
||||||
if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
|
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
|
# Install the virtualenv and run the test suite in it
|
||||||
python tools/install_venv.py $installvenvopts
|
python tools/install_venv.py
|
||||||
wrapper=${with_venv}
|
wrapper=${with_venv}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
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
|
if [ $just_pep8 -eq 1 ]; then
|
||||||
run_pep8
|
run_pep8
|
||||||
exit
|
exit
|
||||||
fi
|
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 [ -z "$noseargs" ]; then
|
||||||
if [ $no_pep8 -eq 0 ]; then
|
run_pep8
|
||||||
run_pep8
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $coverage -eq 1 ]; then
|
|
||||||
echo "Generating coverage report in covhtml/"
|
|
||||||
${wrapper} coverage html -d covhtml -i
|
|
||||||
fi
|
fi
|
||||||
|
42
setup.py
42
setup.py
@ -1,14 +1,15 @@
|
|||||||
import os
|
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_requirements
|
||||||
from glanceclient.openstack.common.setup import parse_dependency_links
|
from glanceclient.openstack.common.setup import parse_dependency_links
|
||||||
from glanceclient.openstack.common.setup import write_requirements
|
from glanceclient.openstack.common.setup import write_requirements
|
||||||
from glanceclient.openstack.common.setup import write_git_changelog
|
from glanceclient.openstack.common.setup import write_git_changelog
|
||||||
|
|
||||||
|
|
||||||
requires = parse_requirements()
|
requires = parse_requirements()
|
||||||
depend_links = parse_dependency_links()
|
dependency_links = parse_dependency_links()
|
||||||
write_requirements()
|
write_requirements()
|
||||||
write_git_changelog()
|
write_git_changelog()
|
||||||
|
|
||||||
@ -16,17 +17,17 @@ write_git_changelog()
|
|||||||
def read(fname):
|
def read(fname):
|
||||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||||
|
|
||||||
setup(
|
setuptools.setup(
|
||||||
name = "python-glanceclient",
|
name="python-glanceclient",
|
||||||
version = "2012.1",
|
version="2012.1",
|
||||||
description = "Client library for OpenStack Glance API",
|
description="Client library for OpenStack Glance API",
|
||||||
long_description = read('README.rst'),
|
long_description=read('README.rst'),
|
||||||
url = 'https://github.com/openstack/python-glanceclient',
|
url='https://github.com/openstack/python-glanceclient',
|
||||||
license = 'Apache',
|
license='Apache',
|
||||||
author = 'Jay Pipes, based on work by Rackspace and Jacob Kaplan-Moss',
|
author='OpenStack Glance Contributors',
|
||||||
author_email = 'jay.pipes@gmail.com',
|
author_email='glance@example.com',
|
||||||
packages = find_packages(exclude=['tests', 'tests.*']),
|
packages=setuptools.find_packages(exclude=['tests', 'tests.*']),
|
||||||
classifiers = [
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 4 - Beta',
|
||||||
'Environment :: Console',
|
'Environment :: Console',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
@ -35,12 +36,9 @@ setup(
|
|||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
],
|
],
|
||||||
install_requires=requires,
|
#install_requires=requires,
|
||||||
dependency_links=depend_links,
|
install_requires=[],
|
||||||
|
dependency_links=dependency_links,
|
||||||
test_suite = "nose.collector",
|
test_suite="nose.collector",
|
||||||
|
entry_points={'console_scripts': ['glance = glanceclient.shell:main']},
|
||||||
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
|
httplib2
|
||||||
prettytable
|
prettytable
|
||||||
simplejson
|
simplejson
|
||||||
|
|
||||||
|
-e git://github.com/openstack/python-keystoneclient.git#egg=keystoneclient
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
distribute>=0.6.24
|
distribute>=0.6.24
|
||||||
|
|
||||||
coverage
|
|
||||||
mock>=0.7.1
|
|
||||||
mox
|
mox
|
||||||
nose
|
nose
|
||||||
|
nose-exclude
|
||||||
nosexcover
|
nosexcover
|
||||||
openstack.nose_plugin
|
openstack.nose_plugin
|
||||||
pep8==0.6.1
|
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