summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore11
-rw-r--r--AUTHORS1
-rw-r--r--HACKING.rst186
-rw-r--r--LICENSE209
-rw-r--r--MANIFEST.in4
-rw-r--r--README.rst109
-rw-r--r--glanceclient/__init__.py0
-rw-r--r--glanceclient/base.py195
-rw-r--r--glanceclient/client.py175
-rw-r--r--glanceclient/exceptions.py132
-rw-r--r--glanceclient/generic/__init__.py0
-rw-r--r--glanceclient/generic/client.py205
-rw-r--r--glanceclient/generic/shell.py57
-rw-r--r--glanceclient/service_catalog.py81
-rw-r--r--glanceclient/shell.py246
-rw-r--r--glanceclient/utils.py94
-rw-r--r--glanceclient/v1_1/__init__.py1
-rw-r--r--glanceclient/v1_1/client.py113
-rw-r--r--glanceclient/v1_1/images.py88
-rwxr-xr-xglanceclient/v1_1/shell.py77
-rwxr-xr-xrun_tests.sh153
-rw-r--r--setup.cfg13
-rw-r--r--setup.py42
-rw-r--r--tox.ini14
24 files changed, 2206 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..097d208
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+.coverage
+.venv
+*,cover
+cover
+*.pyc
+.idea
+*.swp
+*~
+build
+dist
+python_keystoneclient.egg-info
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..dcd8dc7
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+Jay Pipes <jaypipes@gmail.com>
diff --git a/HACKING.rst b/HACKING.rst
new file mode 100644
index 0000000..b6494bb
--- /dev/null
+++ b/HACKING.rst
@@ -0,0 +1,186 @@
+Glance Style Commandments
+=========================
+
+- Step 1: Read http://www.python.org/dev/peps/pep-0008/
+- Step 2: Read http://www.python.org/dev/peps/pep-0008/ again
+- Step 3: Read on
+
+
+General
+-------
+- Put two newlines between top-level code (funcs, classes, etc)
+- Put one newline between methods in classes and anywhere else
+- Do not write "except:", use "except Exception:" at the very least
+- Include your name with TODOs as in "#TODO(termie)"
+- Do not name anything the same name as a built-in or reserved word
+
+
+Imports
+-------
+- Do not make relative imports
+- Order your imports by the full module path
+- Organize your imports according to the following template
+
+Example::
+
+ # vim: tabstop=4 shiftwidth=4 softtabstop=4
+ {{stdlib imports in human alphabetical order}}
+ \n
+ {{third-party lib imports in human alphabetical order}}
+ \n
+ {{glance imports in human alphabetical order}}
+ \n
+ \n
+ {{begin your code}}
+
+
+Human Alphabetical Order Examples
+---------------------------------
+Example::
+
+ import httplib
+ import logging
+ import random
+ import StringIO
+ import time
+ import unittest
+
+ import eventlet
+ import webob.exc
+
+ import glance.api.middleware
+ from glance.api import images
+ from glance.auth import users
+ import glance.common
+ from glance.endpoint import cloud
+ from glance import test
+
+
+Docstrings
+----------
+
+Docstrings are required for all functions and methods.
+
+Docstrings should ONLY use triple-double-quotes (``"""``)
+
+Single-line docstrings should NEVER have extraneous whitespace
+between enclosing triple-double-quotes.
+
+**INCORRECT** ::
+
+ """ There is some whitespace between the enclosing quotes :( """
+
+**CORRECT** ::
+
+ """There is no whitespace between the enclosing quotes :)"""
+
+Docstrings that span more than one line should look like this:
+
+Example::
+
+ """
+ Start the docstring on the line following the opening triple-double-quote
+
+ If you are going to describe parameters and return values, use Sphinx, the
+ appropriate syntax is as follows.
+
+ :param foo: the foo parameter
+ :param bar: the bar parameter
+ :returns: return_type -- description of the return value
+ :returns: description of the return value
+ :raises: AttributeError, KeyError
+ """
+
+**DO NOT** leave an extra newline before the closing triple-double-quote.
+
+
+Dictionaries/Lists
+------------------
+If a dictionary (dict) or list object is longer than 80 characters, its items
+should be split with newlines. Embedded iterables should have their items
+indented. Additionally, the last item in the dictionary should have a trailing
+comma. This increases readability and simplifies future diffs.
+
+Example::
+
+ my_dictionary = {
+ "image": {
+ "name": "Just a Snapshot",
+ "size": 2749573,
+ "properties": {
+ "user_id": 12,
+ "arch": "x86_64",
+ },
+ "things": [
+ "thing_one",
+ "thing_two",
+ ],
+ "status": "ACTIVE",
+ },
+ }
+
+
+Calling Methods
+---------------
+Calls to methods 80 characters or longer should format each argument with
+newlines. This is not a requirement, but a guideline::
+
+ unnecessarily_long_function_name('string one',
+ 'string two',
+ kwarg1=constants.ACTIVE,
+ kwarg2=['a', 'b', 'c'])
+
+
+Rather than constructing parameters inline, it is better to break things up::
+
+ list_of_strings = [
+ 'what_a_long_string',
+ 'not as long',
+ ]
+
+ dict_of_numbers = {
+ 'one': 1,
+ 'two': 2,
+ 'twenty four': 24,
+ }
+
+ object_one.call_a_method('string three',
+ 'string four',
+ kwarg1=list_of_strings,
+ kwarg2=dict_of_numbers)
+
+
+Internationalization (i18n) Strings
+-----------------------------------
+In order to support multiple languages, we have a mechanism to support
+automatic translations of exception and log strings.
+
+Example::
+
+ msg = _("An error occurred")
+ raise HTTPBadRequest(explanation=msg)
+
+If you have a variable to place within the string, first internationalize the
+template string then do the replacement.
+
+Example::
+
+ msg = _("Missing parameter: %s") % ("flavor",)
+ LOG.error(msg)
+
+If you have multiple variables to place in the string, use keyword parameters.
+This helps our translators reorder parameters when needed.
+
+Example::
+
+ msg = _("The server with id %(s_id)s has no key %(m_key)s")
+ LOG.error(msg % {"s_id": "1234", "m_key": "imageId"})
+
+
+Creating Unit Tests
+-------------------
+For every new feature, unit tests should be created that both test and
+(implicitly) document the usage of said feature. If submitting a patch for a
+bug that had no unit test, a new passing unit test should be added. If a
+submitted bug fix does have a unit test, be sure to add a new one that fails
+without the patch and passes with the patch.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..32b6611
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,209 @@
+Copyright (c) 2009 Jacob Kaplan-Moss - initial codebase (< v2.1)
+Copyright (c) 2011 Rackspace - OpenStack extensions (>= v2.1)
+Copyright (c) 2011 Nebula, Inc - Keystone refactor (>= v2.7)
+All rights reserved.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+--- License for python-keystoneclient versions prior to 2.1 ---
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of this project nor the names of its contributors may
+ be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..b023d22
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
+include README.rst
+include LICENSE
+recursive-include docs *
+recursive-include tests *
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..f865371
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,109 @@
+Python bindings to the OpenStack Glance API
+=============================================
+
+This is a client for the OpenStack Glance API. There's a Python API (the
+``glanceclient`` module), and a command-line script (``glance``). The
+Glance 2.0 API is still a moving target, so this module will remain in
+"Beta" status until the API is finalized and fully implemented.
+
+Development takes place via the usual OpenStack processes as outlined in
+the `OpenStack wiki`_. The master repository is on GitHub__.
+
+__ http://wiki.openstack.org/HowToContribute
+__ http://github.com/openstack/python-glanceclient
+
+This code a fork of `Rackspace's python-novaclient`__ which is in turn a fork of
+`Jacobian's python-cloudservers`__. The python-glanceclient is licensed under
+the Apache License like the rest of OpenStack.
+
+__ http://github.com/rackspace/python-novaclient
+__ http://github.com/jacobian/python-cloudservers
+
+.. contents:: Contents:
+ :local:
+
+Python API
+----------
+
+By way of a quick-start::
+
+ # use v2.0 auth with http://example.com:5000/v2.0")
+ >>> from glanceclient.v2_0 import client
+ >>> glance = client.Client(username=USERNAME, password=PASSWORD, tenant_name=TENANT, auth_url=KEYSTONE_URL)
+ >>> glance.images.list()
+ >>> image = glance.images.create(name="My Test Image")
+ >>> print image.status
+ 'queued'
+ >>> image.upload(open('/tmp/myimage.iso', 'rb'))
+ >>> print image.status
+ 'active'
+ >>> image_file = image.image_file
+ >>> with open('/tmp/copyimage.iso', 'wb') as f:
+ for chunk in image_file:
+ f.write(chunk)
+ >>> image.delete()
+
+
+Command-line API
+----------------
+
+Installing this package gets you a command-line tool, ``glance``, that you
+can use to interact with Glance's Identity API.
+
+You'll need to provide your OpenStack tenant, username and password. You can do this
+with the ``tenant_name``, ``--username`` and ``--password`` params, but it's
+easier to just set them as environment variables::
+
+ export OS_TENANT_NAME=project
+ export OS_USERNAME=user
+ export OS_PASSWORD=pass
+
+You will also need to define the authentication url with ``--auth_url`` and the
+version of the API with ``--identity_api_version``. Or set them as an environment
+variables as well::
+
+ export OS_AUTH_URL=http://example.com:5000/v2.0
+ export OS_IDENTITY_API_VERSION=2.0
+
+Since the Identity service that Glance uses can return multiple regional image
+endpoints in the Service Catalog, you can specify the one you want with
+``--region_name`` (or ``export OS_REGION_NAME``).
+It defaults to the first in the list returned.
+
+You'll find complete documentation on the shell by running
+``glance help``::
+
+ usage: glance [--username USERNAME] [--password PASSWORD]
+ [--tenant_name TENANT_NAME | --tenant_id TENANT_ID]
+ [--auth_url AUTH_URL] [--region_name REGION_NAME]
+ [--identity_api_version IDENTITY_API_VERSION]
+ <subcommand> ...
+
+ Command-line interface to the OpenStack Identity API.
+
+ Positional arguments:
+ <subcommand>
+ catalog List all image services in service catalog
+ image-create Create new image
+ image-delete Delete image
+ image-list List images
+ image-update Update image's name and other properties
+ image-upload Upload an image file
+ image-download Download an image file
+ help Display help about this program or one of its
+ subcommands.
+
+ Optional arguments:
+ --username USERNAME Defaults to env[OS_USERNAME]
+ --password PASSWORD Defaults to env[OS_PASSWORD]
+ --tenant_name TENANT_NAME
+ Defaults to env[OS_TENANT_NAME]
+ --tenant_id TENANT_ID
+ Defaults to env[OS_TENANT_ID]
+ --auth_url AUTH_URL Defaults to env[OS_AUTH_URL]
+ --region_name REGION_NAME
+ Defaults to env[OS_REGION_NAME]
+ --identity_api_version IDENTITY_API_VERSION
+ Defaults to env[OS_IDENTITY_API_VERSION] or 2.0
+
+See "glance help COMMAND" for help on a specific command.
diff --git a/glanceclient/__init__.py b/glanceclient/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glanceclient/__init__.py
diff --git a/glanceclient/base.py b/glanceclient/base.py
new file mode 100644
index 0000000..ad6fb2e
--- /dev/null
+++ b/glanceclient/base.py
@@ -0,0 +1,195 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Base utilities to build API operation managers and objects on top of.
+"""
+
+from glanceclient import exceptions
+
+
+# Python 2.4 compat
+try:
+ all
+except NameError:
+ def all(iterable):
+ return True not in (not x for x in iterable)
+
+
+def getid(obj):
+ """
+ Abstracts the common pattern of allowing both an object or an object's ID
+ (UUID) as a parameter when dealing with relationships.
+ """
+
+ # Try to return the object's UUID first, if we have a UUID.
+ try:
+ if obj.uuid:
+ return obj.uuid
+ except AttributeError:
+ pass
+ try:
+ return obj.id
+ except AttributeError:
+ return obj
+
+
+class Manager(object):
+ """
+ Managers interact with a particular type of API (servers, flavors, images,
+ etc.) and provide CRUD operations for them.
+ """
+ resource_class = None
+
+ def __init__(self, api):
+ self.api = api
+
+ def _list(self, url, response_key, obj_class=None, body=None):
+ resp = None
+ if body:
+ resp, body = self.api.post(url, body=body)
+ else:
+ resp, body = self.api.get(url)
+
+ if obj_class is None:
+ obj_class = self.resource_class
+
+ data = body[response_key]
+ return [obj_class(self, res, loaded=True) for res in data if res]
+
+ def _get(self, url, response_key):
+ resp, body = self.api.get(url)
+ return self.resource_class(self, body[response_key])
+
+ def _create(self, url, body, response_key, return_raw=False):
+ resp, body = self.api.post(url, body=body)
+ if return_raw:
+ return body[response_key]
+ return self.resource_class(self, body[response_key])
+
+ def _delete(self, url):
+ resp, body = self.api.delete(url)
+
+ def _update(self, url, body, response_key=None, method="PUT"):
+ methods = {"PUT": self.api.put,
+ "POST": self.api.post}
+ try:
+ resp, body = methods[method](url, body=body)
+ except KeyError:
+ raise exceptions.ClientException("Invalid update method: %s"
+ % method)
+ # PUT requests may not return a body
+ if body:
+ return self.resource_class(self, body[response_key])
+
+
+class ManagerWithFind(Manager):
+ """
+ Like a `Manager`, but with additional `find()`/`findall()` methods.
+ """
+ def find(self, **kwargs):
+ """
+ Find a single item with attributes matching ``**kwargs``.
+
+ This isn't very efficient: it loads the entire list then filters on
+ the Python side.
+ """
+ rl = self.findall(**kwargs)
+ try:
+ return rl[0]
+ except IndexError:
+ msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
+ raise exceptions.NotFound(404, msg)
+
+ def findall(self, **kwargs):
+ """
+ Find all items with attributes matching ``**kwargs``.
+
+ This isn't very efficient: it loads the entire list then filters on
+ the Python side.
+ """
+ found = []
+ searches = kwargs.items()
+
+ for obj in self.list():
+ try:
+ if all(getattr(obj, attr) == value
+ for (attr, value) in searches):
+ found.append(obj)
+ except AttributeError:
+ continue
+
+ return found
+
+
+class Resource(object):
+ """
+ A resource represents a particular instance of an object (tenant, user,
+ etc). This is pretty much just a bag for attributes.
+
+ :param manager: Manager object
+ :param info: dictionary representing resource attributes
+ :param loaded: prevent lazy-loading if set to True
+ """
+ def __init__(self, manager, info, loaded=False):
+ self.manager = manager
+ self._info = info
+ self._add_details(info)
+ self._loaded = loaded
+
+ def _add_details(self, info):
+ for (k, v) in info.iteritems():
+ setattr(self, k, v)
+
+ def __getattr__(self, k):
+ if k not in self.__dict__:
+ #NOTE(bcwaldon): disallow lazy-loading if already loaded once
+ if not self.is_loaded():
+ self.get()
+ return self.__getattr__(k)
+
+ raise AttributeError(k)
+ else:
+ return self.__dict__[k]
+
+ def __repr__(self):
+ reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
+ k != 'manager')
+ info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
+ return "<%s %s>" % (self.__class__.__name__, info)
+
+ def get(self):
+ # set_loaded() first ... so if we have to bail, we know we tried.
+ self.set_loaded(True)
+ if not hasattr(self.manager, 'get'):
+ return
+
+ new = self.manager.get(self.id)
+ if new:
+ self._add_details(new._info)
+
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return False
+ if hasattr(self, 'id') and hasattr(other, 'id'):
+ return self.id == other.id
+ return self._info == other._info
+
+ def is_loaded(self):
+ return self._loaded
+
+ def set_loaded(self, val):
+ self._loaded = val
diff --git a/glanceclient/client.py b/glanceclient/client.py
new file mode 100644
index 0000000..0975584
--- /dev/null
+++ b/glanceclient/client.py
@@ -0,0 +1,175 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack LLC.
+# Copyright 2011 Piston Cloud Computing, Inc.
+# Copyright 2011 Nebula, Inc.
+
+# All Rights Reserved.
+"""
+OpenStack Client interface. Handles the REST calls and responses.
+"""
+
+import copy
+import logging
+import os
+import time
+import urllib
+import urlparse
+
+import httplib2
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+# Python 2.5 compat fix
+if not hasattr(urlparse, 'parse_qsl'):
+ import cgi
+ urlparse.parse_qsl = cgi.parse_qsl
+
+
+from glanceclient import exceptions
+
+
+_logger = logging.getLogger(__name__)
+
+
+class HTTPClient(httplib2.Http):
+
+ USER_AGENT = 'python-glanceclient'
+
+ def __init__(self, username=None, tenant_id=None, tenant_name=None,
+ password=None, auth_url=None, region_name=None, timeout=None,
+ endpoint=None, token=None):
+ super(HTTPClient, self).__init__(timeout=timeout)
+ self.username = username
+ self.tenant_id = tenant_id
+ self.tenant_name = tenant_name
+ self.password = password
+ self.auth_url = auth_url.rstrip('/') if auth_url else None
+ self.version = 'v2.0'
+ self.region_name = region_name
+ self.auth_token = token
+
+ self.management_url = endpoint
+
+ # httplib2 overrides
+ self.force_exception_to_status_code = True
+
+ def authenticate(self):
+ """ Authenticate against the keystone API.
+
+ Not implemented here because auth protocols should be API
+ version-specific.
+ """
+ raise NotImplementedError
+
+ def _extract_service_catalog(self, url, body):
+ """ Set the client's service catalog from the response data.
+
+ Not implemented here because data returned may be API
+ version-specific.
+ """
+ raise NotImplementedError
+
+ def http_log(self, args, kwargs, resp, body):
+ if os.environ.get('GLANCECLIENT_DEBUG', False):
+ ch = logging.StreamHandler()
+ _logger.setLevel(logging.DEBUG)
+ _logger.addHandler(ch)
+ elif not _logger.isEnabledFor(logging.DEBUG):
+ return
+
+ string_parts = ['curl -i']
+ for element in args:
+ if element in ('GET', 'POST'):
+ string_parts.append(' -X %s' % element)
+ else:
+ string_parts.append(' %s' % element)
+
+ for element in kwargs['headers']:
+ header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
+ string_parts.append(header)
+
+ _logger.debug("REQ: %s\n" % "".join(string_parts))
+ if 'body' in kwargs:
+ _logger.debug("REQ BODY: %s\n" % (kwargs['body']))
+ _logger.debug("RESP: %s\nRESP BODY: %s\n", resp, body)
+
+ def request(self, url, method, **kwargs):
+ """ Send an http request with the specified characteristics.
+
+ Wrapper around httplib2.Http.request to handle tasks such as
+ setting headers, JSON encoding/decoding, and error handling.
+ """
+ # Copy the kwargs so we can reuse the original in case of redirects
+ request_kwargs = copy.copy(kwargs)
+ request_kwargs.setdefault('headers', kwargs.get('headers', {}))
+ request_kwargs['headers']['User-Agent'] = self.USER_AGENT
+ if 'body' in kwargs:
+ request_kwargs['headers']['Content-Type'] = 'application/json'
+ request_kwargs['body'] = json.dumps(kwargs['body'])
+
+ resp, body = super(HTTPClient, self).request(url,
+ method,
+ **request_kwargs)
+
+ self.http_log((url, method,), request_kwargs, resp, body)
+
+ if body:
+ try:
+ body = json.loads(body)
+ except ValueError, e:
+ _logger.debug("Could not decode JSON from body: %s" % body)
+ else:
+ _logger.debug("No body was returned.")
+ body = None
+
+ if resp.status in (400, 401, 403, 404, 408, 409, 413, 500, 501):
+ _logger.exception("Request returned failure status.")
+ raise exceptions.from_response(resp, body)
+ elif resp.status in (301, 302, 305):
+ # Redirected. Reissue the request to the new location.
+ return self.request(resp['location'], method, **kwargs)
+
+ return resp, body
+
+ def _cs_request(self, url, method, **kwargs):
+ if not self.management_url:
+ self.authenticate()
+
+ kwargs.setdefault('headers', {})
+ if self.auth_token:
+ kwargs['headers']['X-Auth-Token'] = self.auth_token
+
+ # Perform the request once. If we get a 401 back then it
+ # might be because the auth token expired, so try to
+ # re-authenticate and try again. If it still fails, bail.
+ try:
+ resp, body = self.request(self.management_url + url, method,
+ **kwargs)
+ return resp, body
+ except exceptions.Unauthorized:
+ try:
+ if getattr(self, '_failures', 0) < 1:
+ self._failures = getattr(self, '_failures', 0) + 1
+ self.authenticate()
+ resp, body = self.request(self.management_url + url,
+ method, **kwargs)
+ return resp, body
+ else:
+ raise
+ except exceptions.Unauthorized:
+ raise
+
+ def get(self, url, **kwargs):
+ return self._cs_request(url, 'GET', **kwargs)
+
+ def post(self, url, **kwargs):
+ return self._cs_request(url, 'POST', **kwargs)
+
+ def put(self, url, **kwargs):
+ return self._cs_request(url, 'PUT', **kwargs)
+
+ def delete(self, url, **kwargs):
+ return self._cs_request(url, 'DELETE', **kwargs)
diff --git a/glanceclient/exceptions.py b/glanceclient/exceptions.py
new file mode 100644
index 0000000..f52800d
--- /dev/null
+++ b/glanceclient/exceptions.py
@@ -0,0 +1,132 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 Nebula, Inc.
+"""
+Exception definitions.
+"""
+
+
+class CommandError(Exception):
+ pass
+
+
+class AuthorizationFailure(Exception):
+ pass
+
+
+class NoTokenLookupException(Exception):
+ """This form of authentication does not support looking up
+ endpoints from an existing token."""
+ pass
+
+
+class EndpointNotFound(Exception):
+ """Could not find Service or Region in Service Catalog."""
+ pass
+
+
+class ClientException(Exception):
+ """
+ The base exception class for all exceptions this library raises.
+ """
+ def __init__(self, code, message=None, details=None):
+ self.code = code
+ self.message = message or self.__class__.message
+ self.details = details
+
+ def __str__(self):
+ return "%s (HTTP %s)" % (self.message, self.code)
+
+
+class BadRequest(ClientException):
+ """
+ HTTP 400 - Bad request: you sent some malformed data.
+ """
+ http_status = 400
+ message = "Bad request"
+
+
+class Unauthorized(ClientException):
+ """
+ HTTP 401 - Unauthorized: bad credentials.
+ """
+ http_status = 401
+ message = "Unauthorized"
+
+
+class Forbidden(ClientException):
+ """
+ HTTP 403 - Forbidden: your credentials don't give you access to this
+ resource.
+ """
+ http_status = 403
+ message = "Forbidden"
+
+
+class NotFound(ClientException):
+ """
+ HTTP 404 - Not found
+ """
+ http_status = 404
+ message = "Not found"
+
+
+class Conflict(ClientException):
+ """
+ HTTP 409 - Conflict
+ """
+ http_status = 409
+ message = "Conflict"
+
+
+class OverLimit(ClientException):
+ """
+ HTTP 413 - Over limit: you're over the API limits for this time period.
+ """
+ http_status = 413
+ message = "Over limit"
+
+
+# NotImplemented is a python keyword.
+class HTTPNotImplemented(ClientException):
+ """
+ HTTP 501 - Not Implemented: the server does not support this operation.
+ """
+ http_status = 501
+ message = "Not Implemented"
+
+
+# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
+# so we can do this:
+# _code_map = dict((c.http_status, c)
+# for c in ClientException.__subclasses__())
+#
+# Instead, we have to hardcode it:
+_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
+ Forbidden, NotFound, OverLimit, HTTPNotImplemented])
+
+
+def from_response(response, body):
+ """
+ Return an instance of an ClientException or subclass
+ based on an httplib2 response.
+
+ Usage::
+
+ resp, body = http.request(...)
+ if resp.status != 200:
+ raise exception_from_response(resp, body)
+ """
+ cls = _code_map.get(response.status, ClientException)
+ if body:
+ if hasattr(body, 'keys'):
+ error = body[body.keys()[0]]
+ message = error.get('message', None)
+ details = error.get('details', None)
+ else:
+ # If we didn't get back a properly formed error message we
+ # probably couldn't communicate with Keystone at all.
+ message = "Unable to communicate with identity service: %s." % body
+ details = None
+ return cls(code=response.status, message=message, details=details)
+ else:
+ return cls(code=response.status)
diff --git a/glanceclient/generic/__init__.py b/glanceclient/generic/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glanceclient/generic/__init__.py
diff --git a/glanceclient/generic/client.py b/glanceclient/generic/client.py
new file mode 100644
index 0000000..6533572
--- /dev/null
+++ b/glanceclient/generic/client.py
@@ -0,0 +1,205 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+import urlparse
+
+from glanceclient import client
+from glanceclient import exceptions
+
+_logger = logging.getLogger(__name__)
+
+
+class Client(client.HTTPClient):
+ """Client for the OpenStack Images pre-version calls API.
+
+ :param string endpoint: A user-supplied endpoint URL for the glance
+ service.
+ :param integer timeout: Allows customization of the timeout for client
+ http requests. (optional)
+
+ Example::
+
+ >>> from glanceclient.generic import client
+ >>> root = client.Client(auth_url=KEYSTONE_URL)
+ >>> versions = root.discover()
+ ...
+ >>> from glanceclient.v1_1 import client as v11client
+ >>> glance = v11client.Client(auth_url=versions['v1.1']['url'])
+ ...
+ >>> image = glance.images.get(IMAGE_ID)
+ >>> image.delete()
+
+ """
+
+ def __init__(self, endpoint=None, **kwargs):
+ """ Initialize a new client for the Glance v2.0 API. """
+ super(Client, self).__init__(endpoint=endpoint, **kwargs)
+ self.endpoint = endpoint
+
+ def discover(self, url=None):
+ """ Discover Glance servers and return API versions supported.
+
+ :param url: optional url to test (without version)
+
+ Returns::
+
+ {
+ 'message': 'Glance found at http://127.0.0.1:5000/',
+ 'v2.0': {
+ 'status': 'beta',
+ 'url': 'http://127.0.0.1:5000/v2.0/',
+ 'id': 'v2.0'
+ },
+ }
+
+ """
+ if url:
+ return self._check_glance_versions(url)
+ else:
+ return self._local_glance_exists()
+
+ def _local_glance_exists(self):
+ """ Checks if Glance is available on default local port 9292 """
+ return self._check_glance_versions("http://localhost:9292")
+
+ def _check_glance_versions(self, url):
+ """ Calls Glance URL and detects the available API versions """
+ try:
+ httpclient = client.HTTPClient()
+ resp, body = httpclient.request(url, "GET",
+ headers={'Accept': 'application/json'})
+ if resp.status in (300): # Glance returns a 300 Multiple Choices
+ try:
+ results = {}
+ if 'version' in body:
+ results['message'] = "Glance found at %s" % url
+ version = body['version']
+ # Stable/diablo incorrect format
+ id, status, version_url = self._get_version_info(
+ version, url)
+ results[str(id)] = {"id": id,
+ "status": status,
+ "url": version_url}
+ return results
+ elif 'versions' in body:
+ # Correct format
+ results['message'] = "Glance found at %s" % url
+ for version in body['versions']['values']:
+ id, status, version_url = self._get_version_info(
+ version, url)
+ results[str(id)] = {"id": id,
+ "status": status,
+ "url": version_url}
+ return results
+ else:
+ results['message'] = "Unrecognized response from %s" \
+ % url
+ return results
+ except KeyError:
+ raise exceptions.AuthorizationFailure()
+ elif resp.status == 305:
+ return self._check_glance_versions(resp['location'])
+ else:
+ raise exceptions.from_response(resp, body)
+ except Exception as e:
+ _logger.exception(e)
+
+ def discover_extensions(self, url=None):
+ """ Discover Glance extensions supported.
+
+ :param url: optional url to test (should have a version in it)
+
+ Returns::
+
+ {
+ 'message': 'Glance extensions at http://127.0.0.1:35357/v2',
+ 'OS-KSEC2': 'OpenStack EC2 Credentials Extension',
+ }
+
+ """
+ if url:
+ return self._check_glance_extensions(url)
+
+ def _check_glance_extensions(self, url):
+ """ Calls Glance URL and detects the available extensions """
+ try:
+ httpclient = client.HTTPClient()
+ if not url.endswith("/"):
+ url += '/'
+ resp, body = httpclient.request("%sextensions" % url, "GET",
+ headers={'Accept': 'application/json'})
+ if resp.status in (200, 204): # in some cases we get No Content
+ try:
+ results = {}
+ if 'extensions' in body:
+ if 'values' in body['extensions']:
+ # Parse correct format (per contract)
+ for extension in body['extensions']['values']:
+ alias, name = self._get_extension_info(
+ extension['extension'])
+ results[alias] = name
+ return results
+ else:
+ # Support incorrect, but prevalent format
+ for extension in body['extensions']:
+ alias, name = self._get_extension_info(
+ extension)
+ results[alias] = name
+ return results
+ else:
+ results['message'] = "Unrecognized extensions" \
+ " response from %s" % url
+ return results
+ except KeyError:
+ raise exceptions.AuthorizationFailure()
+ elif resp.status == 305:
+ return self._check_glance_extensions(resp['location'])
+ else:
+ raise exceptions.from_response(resp, body)
+ except Exception as e:
+ _logger.exception(e)
+
+ @staticmethod
+ def _get_version_info(version, root_url):
+ """ Parses version information
+
+ :param version: a dict of a Glance version response
+ :param root_url: string url used to construct
+ the version if no URL is provided.
+ :returns: tuple - (verionId, versionStatus, versionUrl)
+ """
+ id = version['id']
+ status = version['status']
+ ref = urlparse.urljoin(root_url, id)
+ if 'links' in version:
+ for link in version['links']:
+ if link['rel'] == 'self':
+ ref = link['href']
+ break
+ return (id, status, ref)
+
+ @staticmethod
+ def _get_extension_info(extension):
+ """ Parses extension information
+
+ :param extension: a dict of a Glance extension response
+ :returns: tuple - (alias, name)
+ """
+ alias = extension['alias']
+ name = extension['name']
+ return (alias, name)
diff --git a/glanceclient/generic/shell.py b/glanceclient/generic/shell.py
new file mode 100644
index 0000000..caffbba
--- /dev/null
+++ b/glanceclient/generic/shell.py
@@ -0,0 +1,57 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from glanceclient import utils
+from glanceclient.generic import client
+
+CLIENT_CLASS = client.Client
+
+
+@utils.unauthenticated
+def do_discover(cs, args):
+ """
+ Discover Keystone servers and show authentication protocols and
+ extensions supported.
+
+ Usage::
+ $ glance discover
+ Image Service found at http://localhost:9292
+ - supports version v1.0 (DEPRECATED) here http://localhost:9292/v1.0
+ - supports version v1.1 (CURRENT) here http://localhost:9292/v1.1
+ - supports version v2.0 (BETA) here http://localhost:9292/v2.0
+ - and RAX-KSKEY: Rackspace API Key Authentication Admin Extension
+ - and RAX-KSGRP: Rackspace Keystone Group Extensions
+ """
+ if cs.auth_url:
+ versions = cs.discover(cs.auth_url)
+ else:
+ versions = cs.discover()
+ if versions:
+ if 'message' in versions:
+ print versions['message']
+ for key, version in versions.iteritems():
+ if key != 'message':
+ print " - supports version %s (%s) here %s" % \
+ (version['id'], version['status'], version['url'])
+ extensions = cs.discover_extensions(version['url'])
+ if extensions:
+ for key, extension in extensions.iteritems():
+ if key != 'message':
+ print " - and %s: %s" % \
+ (key, extension)
+ else:
+ print "No Glance-compatible endpoint found"
diff --git a/glanceclient/service_catalog.py b/glanceclient/service_catalog.py
new file mode 100644
index 0000000..e302977
--- /dev/null
+++ b/glanceclient/service_catalog.py
@@ -0,0 +1,81 @@
+# Copyright 2011 OpenStack LLC.
+# Copyright 2011, Piston Cloud Computing, Inc.
+# Copyright 2011 Nebula, Inc.
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from glanceclient import exceptions
+
+
+class ServiceCatalog(object):
+ """
+ Helper methods for dealing with an OpenStack Identity
+ Service Catalog.
+ """
+
+ def __init__(self, resource_dict):
+ self.catalog = resource_dict
+
+ def get_token(self):
+ """Fetch token details fron service catalog"""
+ token = {'id': self.catalog['token']['id'],
+ 'expires': self.catalog['token']['expires']}
+ try:
+ token['tenant'] = self.catalog['token']['tenant']['id']
+ except:
+ # just leave the tenant out if it doesn't exist
+ pass
+ return token
+
+ def url_for(self, attr=None, filter_value=None,
+ service_type='image', endpoint_type='publicURL'):
+ """Fetch an endpoint from the service catalog.
+
+ Fetch the specified endpoint from the service catalog for
+ a particular endpoint attribute. If no attribute is given, return
+ the first endpoint of the specified type.
+
+ See tests for a sample service catalog.
+ """
+ catalog = self.catalog.get('serviceCatalog', [])
+
+ for service in catalog:
+ if service['type'] != service_type:
+ continue
+
+ endpoints = service['endpoints']
+ for endpoint in endpoints:
+ if not filter_value or endpoint.get(attr) == filter_value:
+ return endpoint[endpoint_type]
+
+ raise exceptions.EndpointNotFound('Endpoint not found.')
+
+ def get_endpoints(self, service_type=None, endpoint_type=None):
+ """Fetch and filter endpoints for the specified service(s)
+
+ Returns endpoints for the specified service (or all) and
+ that contain the specified type (or all).
+ """
+ sc = {}
+ for service in self.catalog.get('serviceCatalog', []):
+ if service_type and service_type != service['type']:
+ continue
+ sc[service['type']] = []
+ for endpoint in service['endpoints']:
+ if endpoint_type and endpoint_type not in endpoint.keys():
+ continue
+ sc[service['type']].append(endpoint)
+ return sc
diff --git a/glanceclient/shell.py b/glanceclient/shell.py
new file mode 100644
index 0000000..2eb9ed1
--- /dev/null
+++ b/glanceclient/shell.py
@@ -0,0 +1,246 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Command-line interface to the OpenStack Images API.
+"""
+
+import argparse
+import httplib2
+import os
+import sys
+
+from glanceclient import exceptions as exc
+from glanceclient import utils
+from glanceclient.v2_0 import shell as shell_v2_0
+from glanceclient.generic import shell as shell_generic
+
+
+def env(*vars, **kwargs):
+ """Search for the first defined of possibly many env vars
+
+ Returns the first environment variable defined in vars, or
+ returns the default defined in kwargs.
+
+ """
+ for v in vars:
+ value = os.environ.get(v, None)
+ if value:
+ return value
+ return kwargs.get('default', '')
+
+
+class OpenStackImagesShell(object):
+
+ def get_base_parser(self):
+ parser = argparse.ArgumentParser(
+ prog='glance',
+ description=__doc__.strip(),
+ epilog='See "glance help COMMAND" '\
+ 'for help on a specific command.',
+ add_help=False,
+ formatter_class=OpenStackHelpFormatter,
+ )
+
+ # Global arguments
+ parser.add_argument('-h', '--help',
+ action='store_true',
+ help=argparse.SUPPRESS,
+ )
+
+ parser.add_argument('--debug',
+ default=False,
+ action='store_true',
+ help=argparse.SUPPRESS)
+
+ parser.add_argument('--username',
+ default=env('OS_USERNAME'),
+ help='Defaults to env[OS_USERNAME]')
+
+ parser.add_argument('--password',
+ default=env('OS_PASSWORD'),
+ help='Defaults to env[OS_PASSWORD]')
+
+ parser.add_argument('--tenant_name',
+ default=env('OS_TENANT_NAME'),
+ help='Defaults to env[OS_TENANT_NAME]')
+
+ parser.add_argument('--tenant_id',
+ default=env('OS_TENANT_ID'), dest='os_tenant_id',
+ help='Defaults to env[OS_TENANT_ID]')
+
+ parser.add_argument('--auth_url',
+ default=env('OS_AUTH_URL'),
+ help='Defaults to env[OS_AUTH_URL]')
+
+ parser.add_argument('--region_name',
+ default=env('OS_REGION_NAME'),
+ help='Defaults to env[OS_REGION_NAME]')
+
+ parser.add_argument('--identity_api_version',
+ default=env('OS_IDENTITY_API_VERSION', 'KEYSTONE_VERSION'),
+ help='Defaults to env[OS_IDENTITY_API_VERSION] or 2.0')
+
+ return parser
+
+ def get_subcommand_parser(self, version):
+ parser = self.get_base_parser()
+
+ self.subcommands = {}
+ subparsers = parser.add_subparsers(metavar='<subcommand>')
+
+ try:
+ actions_module = {
+ '2.0': shell_v2_0,
+ }[version]
+ except KeyError:
+ actions_module = shell_v2_0
+
+ self._find_actions(subparsers, actions_module)
+ self._find_actions(subparsers, shell_generic)
+ self._find_actions(subparsers, self)
+
+ return parser
+
+ def _find_actions(self, subparsers, actions_module):
+ for attr in (a for a in dir(actions_module) if a.startswith('do_')):
+ # I prefer to be hypen-separated instead of underscores.
+ command = attr[3:].replace('_', '-')
+ callback = getattr(actions_module, attr)
+ desc = callback.__doc__ or ''
+ help = desc.strip().split('\n')[0]
+ arguments = getattr(callback, 'arguments', [])
+
+ subparser = subparsers.add_parser(command,
+ help=help,
+ description=desc,
+ add_help=False,
+ formatter_class=OpenStackHelpFormatter
+ )
+ subparser.add_argument('-h', '--help',
+ action='help',
+ help=argparse.SUPPRESS,
+ )
+ self.subcommands[command] = subparser
+ for (args, kwargs) in arguments:
+ subparser.add_argument(*args, **kwargs)
+ subparser.set_defaults(func=callback)
+
+ def main(self, argv):
+ # Parse args once to find version
+ parser = self.get_base_parser()
+ (options, args) = parser.parse_known_args(argv)
+
+ # build available subcommands based on version
+ api_version = options.identity_api_version
+ subcommand_parser = self.get_subcommand_parser(api_version)
+ self.parser = subcommand_parser
+
+ # Handle top-level --help/-h before attempting to parse
+ # a command off the command line
+ if options.help:
+ self.do_help(options)
+ return 0
+
+ # Parse args again and call whatever callback was selected
+ args = subcommand_parser.parse_args(argv)
+
+ # Deal with global arguments
+ if args.debug:
+ httplib2.debuglevel = 1
+
+ # Short-circuit and deal with help command right away.
+ if args.func == self.do_help:
+ self.do_help(args)
+ return 0
+
+ #FIXME(usrleon): Here should be restrict for project id same as
+ # for username or apikey but for compatibility it is not.
+
+ if not utils.isunauthenticated(args.func):
+ if not args.username:
+ raise exc.CommandError("You must provide a username "
+ "via either --username or env[OS_USERNAME]")
+
+ if not args.password:
+ raise exc.CommandError("You must provide a password "
+ "via either --password or env[OS_PASSWORD]")
+
+ if not args.auth_url:
+ raise exc.CommandError("You must provide an auth url "
+ "via either --auth_url or via env[OS_AUTH_URL]")
+
+ if utils.isunauthenticated(args.func):
+ self.cs = shell_generic.CLIENT_CLASS(endpoint=args.auth_url)
+ else:
+ api_version = options.identity_api_version
+ self.cs = self.get_api_class(api_version)(
+ username=args.username,
+ tenant_name=args.tenant_name,
+ tenant_id=args.os_tenant_id,
+ password=args.password,
+ auth_url=args.auth_url,
+ region_name=args.region_name)
+
+ try:
+ args.func(self.cs, args)
+ except exc.Unauthorized:
+ raise exc.CommandError("Invalid OpenStack Identity credentials.")
+ except exc.AuthorizationFailure:
+ raise exc.CommandError("Unable to authorize user")
+
+ def get_api_class(self, version):
+ try:
+ return {
+ "2.0": shell_v2_0.CLIENT_CLASS,
+ }[version]
+ except KeyError:
+ return shell_v2_0.CLIENT_CLASS
+
+ @utils.arg('command', metavar='<subcommand>', nargs='?',
+ help='Display help for <subcommand>')
+ def do_help(self, args):
+ """
+ Display help about this program or one of its subcommands.
+ """
+ if getattr(args, 'command', None):
+ if args.command in self.subcommands:
+ self.subcommands[args.command].print_help()
+ else:
+ raise exc.CommandError("'%s' is not a valid subcommand" %
+ args.command)
+ else:
+ self.parser.print_help()
+
+
+# I'm picky about my shell help.
+class OpenStackHelpFormatter(argparse.HelpFormatter):
+ def start_section(self, heading):
+ # Title-case the headings
+ heading = '%s%s' % (heading[0].upper(), heading[1:])
+ super(OpenStackHelpFormatter, self).start_section(heading)
+
+
+def main():
+ try:
+ OpenStackImagesShell().main(sys.argv[1:])
+
+ except Exception, e:
+ if httplib2.debuglevel == 1:
+ raise # dump stack.
+ else:
+ print >> sys.stderr, e
+ sys.exit(1)
diff --git a/glanceclient/utils.py b/glanceclient/utils.py
new file mode 100644
index 0000000..9880294
--- /dev/null
+++ b/glanceclient/utils.py
@@ -0,0 +1,94 @@
+import uuid
+
+import prettytable
+
+from glanceclient import exceptions
+
+
+# Decorator for cli-args
+def arg(*args, **kwargs):
+ def _decorator(func):
+ # Because of the sematics of decorator composition if we just append
+ # to the options list positional options will appear to be backwards.
+ func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
+ return func
+ return _decorator
+
+
+def pretty_choice_list(l):
+ return ', '.join("'%s'" % i for i in l)
+
+
+def print_list(objs, fields, formatters={}):
+ pt = prettytable.PrettyTable([f for f in fields], caching=False)
+ pt.aligns = ['l' for f in fields]
+
+ for o in objs:
+ row = []
+ for field in fields:
+ if field in formatters:
+ row.append(formatters[field](o))
+ else:
+ field_name = field.lower().replace(' ', '_')
+ data = getattr(o, field_name, '')
+ row.append(data)
+ pt.add_row(row)
+
+ pt.printt(sortby=fields[0])
+
+
+def print_dict(d):
+ pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
+ pt.aligns = ['l', 'l']
+ [pt.add_row(list(r)) for r in d.iteritems()]
+ pt.printt(sortby='Property')
+
+
+def find_resource(manager, name_or_id):
+ """Helper for the _find_* methods."""
+ # first try to get entity as integer id
+ try:
+ if isinstance(name_or_id, int) or name_or_id.isdigit():
+ return manager.get(int(name_or_id))
+ except exceptions.NotFound:
+ pass
+
+ # now try to get entity as uuid
+ try:
+ uuid.UUID(str(name_or_id))
+ return manager.get(name_or_id)
+ except (ValueError, exceptions.NotFound):
+ pass
+
+ # finally try to find entity by name
+ try:
+ return manager.find(name=name_or_id)
+ except exceptions.NotFound:
+ msg = "No %s with a name or ID of '%s' exists." % \
+ (manager.resource_class.__name__.lower(), name_or_id)
+ raise exceptions.CommandError(msg)
+
+
+def unauthenticated(f):
+ """ Adds 'unauthenticated' attribute to decorated function.
+
+ Usage:
+ @unauthenticated
+ def mymethod(f):
+ ...
+ """
+ f.unauthenticated = True
+ return f
+
+
+def isunauthenticated(f):
+ """
+ Checks to see if the function is marked as not requiring authentication
+ with the @unauthenticated decorator. Returns True if decorator is
+ set to True, False otherwise.
+ """
+ return getattr(f, 'unauthenticated', False)
+
+
+def string_to_bool(arg):
+ return arg.strip().lower() in ('t', 'true', 'yes', '1')
diff --git a/glanceclient/v1_1/__init__.py b/glanceclient/v1_1/__init__.py
new file mode 100644
index 0000000..44ad28e
--- /dev/null
+++ b/glanceclient/v1_1/__init__.py
@@ -0,0 +1 @@
+from keystoneclient.v2_0.client import Client
diff --git a/glanceclient/v1_1/client.py b/glanceclient/v1_1/client.py
new file mode 100644
index 0000000..735cecb
--- /dev/null
+++ b/glanceclient/v1_1/client.py
@@ -0,0 +1,113 @@
+# Copyright 2011 Nebula, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+import logging
+
+from glanceclient import client
+from glanceclient import exceptions
+from glanceclient import service_catalog
+from glanceclient.v1_1 import images
+
+
+_logger = logging.getLogger(__name__)
+
+
+class Client(client.HTTPClient):
+ """Client for the OpenStack Images v1.1 API.
+
+ :param string username: Username for authentication. (optional)
+ :param string password: Password for authentication. (optional)
+ :param string token: Token for authentication. (optional)
+ :param string tenant_name: Tenant id. (optional)
+ :param string tenant_id: Tenant name. (optional)
+ :param string auth_url: Keystone service endpoint for authorization.
+ :param string region_name: Name of a region to select when choosing an
+ endpoint from the service catalog.
+ :param string endpoint: A user-supplied endpoint URL for the glance
+ service. Lazy-authentication is possible for API
+ service calls if endpoint is set at
+ instantiation.(optional)
+ :param integer timeout: Allows customization of the timeout for client
+ http requests. (optional)
+
+ Example::
+
+ >>> from glanceclient.v1_1 import client
+ >>> glance = client.Client(username=USER,
+ password=PASS,
+ tenant_name=TENANT_NAME,
+ auth_url=KEYSTONE_URL)
+ >>> glance.images.list()
+ ...
+ >>> image = glance.images.get(IMAGE_ID)
+ >>> image.delete()
+
+ """
+
+ def __init__(self, endpoint=None, **kwargs):
+ """ Initialize a new client for the Images v1.1 API. """
+ super(Client, self).__init__(endpoint=endpoint, **kwargs)
+ self.images = images.ImageManager(self)
+ # NOTE(gabriel): If we have a pre-defined endpoint then we can
+ # get away with lazy auth. Otherwise auth immediately.
+ if endpoint is None:
+ self.authenticate()
+ else:
+ self.management_url = endpoint
+
+ def authenticate(self):
+ """ Authenticate against the Keystone API.
+
+ Uses the data provided at instantiation to authenticate against
+ the Keystone server. This may use either a username and password
+ or token for authentication. If a tenant id was provided
+ then the resulting authenticated client will be scoped to that
+ tenant and contain a service catalog of available endpoints.
+
+ Returns ``True`` if authentication was successful.
+ """
+ self.management_url = self.auth_url
+ try:
+ raw_token = self.tokens.authenticate(username=self.username,
+ tenant_id=self.tenant_id,
+ tenant_name=self.tenant_name,
+ password=self.password,
+ token=self.auth_token,
+ return_raw=True)
+ self._extract_service_catalog(self.auth_url, raw_token)
+ return True
+ except (exceptions.AuthorizationFailure, exceptions.Unauthorized):
+ raise
+ except Exception, e:
+ _logger.exception("Authorization Failed.")
+ raise exceptions.AuthorizationFailure("Authorization Failed: "
+ "%s" % e)
+
+ def _extract_service_catalog(self, url, body):
+ """ Set the client's service catalog from the response data. """
+ self.service_catalog = service_catalog.ServiceCatalog(body)
+ try:
+ self.auth_token = self.service_catalog.get_token()['id']
+ except KeyError:
+ raise exceptions.AuthorizationFailure()
+
+ # FIXME(ja): we should be lazy about setting managment_url.
+ # in fact we should rewrite the client to support the service
+ # catalog (api calls should be directable to any endpoints)
+ try:
+ self.management_url = self.service_catalog.url_for(attr='region',
+ filter_value=self.region_name, endpoint_type='adminURL')
+ except:
+ # Unscoped tokens don't return a service catalog
+ _logger.exception("unable to retrieve service catalog with token")
diff --git a/glanceclient/v1_1/images.py b/glanceclient/v1_1/images.py
new file mode 100644
index 0000000..b76e560
--- /dev/null
+++ b/glanceclient/v1_1/images.py
@@ -0,0 +1,88 @@
+# Copyright 2011 OpenStack LLC.
+# Copyright 2011 Nebula, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import urllib
+
+from glanceclient import base
+
+
+class Image(base.Resource):
+ def __repr__(self):
+ return "<Image %s>" % self._info
+
+ def delete(self):
+ return self.manager.delete(self)
+
+ def list_roles(self, tenant=None):
+ return self.manager.list_roles(self.id, base.getid(tenant))
+
+
+class ImageManager(base.ManagerWithFind):
+ resource_class = Image
+
+ def get(self, image):
+ return self._get("/images/%s" % base.getid(image), "image")
+
+ def update(self, image, **kwargs):
+ """
+ Update image data.
+
+ Supported arguments include ``name`` and ``is_public``.
+ """
+ params = {"image": kwargs}
+ params['image']['id'] = base.getid(image)
+ url = "/images/%s" % base.getid(image)
+ return self._update(url, params, "image")
+
+ def create(self, name, is_public=True):
+ """
+ Create an image.
+ """
+ params = {
+ "image": {
+ "name": name,
+ "is_public": is_public
+ }
+ }
+ return self._create('/images', params, "image")
+
+ def delete(self, image):
+ """
+ Delete a image.
+ """
+ return self._delete("/images/%s" % base.getid(image))
+
+ def list(self, limit=None, marker=None):
+ """
+ Get a list of images (optionally limited to a tenant)
+
+ :rtype: list of :class:`Image`
+ """
+
+ params = {}
+ if limit:
+ params['limit'] = int(limit)
+ if marker:
+ params['marker'] = int(marker)
+
+ query = ""
+ if params:
+ query = "?" + urllib.urlencode(params)
+
+ return self._list("/images%s" % query, "images")
+
+ def list_members(self, image):
+ return self.api.members.members_for_image(base.getid(image))
diff --git a/glanceclient/v1_1/shell.py b/glanceclient/v1_1/shell.py
new file mode 100755
index 0000000..7a34f78
--- /dev/null
+++ b/glanceclient/v1_1/shell.py
@@ -0,0 +1,77 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack LLC.
+# Copyright 2011 Nebula, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from glanceclient.v1_1 import client
+from glanceclient import utils
+
+CLIENT_CLASS = client.Client
+
+
+@utils.arg('tenant', metavar='<tenant-id>', nargs='?', default=None,
+ help='Tenant ID (Optional); lists all images if not specified')
+def do_image_list(gc, args):
+ """List images"""
+ images = gc.images.list(tenant_id=args.tenant)
+ utils.print_list(images, ['id', 'is_public', 'email', 'name'])
+
+
+@utils.arg('--name', metavar='<image-name>', required=True,
+ help='New image name (must be unique)')
+@utils.arg('--is-public', metavar='<true|false>', default=True,
+ help='Initial image is_public status (default true)')
+def do_image_create(gc, args):
+ """Create new image"""
+ image = gc.images.create(args.name, args.passwd, args.email,
+ tenant_id=args.tenant_id, is_public=args.is_public)
+ utils.print_dict(image._info)
+
+
+@utils.arg('--name', metavar='<image-name>',
+ help='Desired new image name')
+@utils.arg('--is-public', metavar='<true|false>',
+ help='Enable or disable image')
+@utils.arg('id', metavar='<image-id>', help='Image ID to update')
+def do_image_update(gc, args):
+ """Update image's name, email, and is_public status"""
+ kwargs = {}
+ if args.name:
+ kwargs['name'] = args.name
+ if args.email:
+ kwargs['email'] = args.email
+ if args.is_public:
+ kwargs['is_public'] = utils.string_to_bool(args.is_public)
+
+ if not len(kwargs):
+ print "User not updated, no arguments present."
+ return
+
+ try:
+ gc.images.update(args.id, **kwargs)
+ print 'User has been updated.'
+ except Exception, e:
+ print 'Unable to update image: %s' % e
+
+
+@utils.arg('id', metavar='<image-id>', help='User ID to delete')
+def do_image_delete(gc, args):
+ """Delete image"""
+ gc.images.delete(args.id)
+
+
+def do_token_get(gc, args):
+ """Display the current user's token"""
+ utils.print_dict(gc.service_catalog.get_token())
diff --git a/run_tests.sh b/run_tests.sh
new file mode 100755
index 0000000..4cd3e3f
--- /dev/null
+++ b/run_tests.sh
@@ -0,0 +1,153 @@
+#!/bin/bash
+
+set -eu
+
+function usage {
+ echo "Usage: $0 [OPTION]..."
+ echo "Run python-keystoneclient test suite"
+ echo ""
+ echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
+ echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
+ echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment"
+ echo " -x, --stop Stop running tests after the first error or failure."
+ echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
+ echo " -p, --pep8 Just run pep8"
+ echo " -P, --no-pep8 Don't run pep8"
+ echo " -c, --coverage Generate coverage report"
+ echo " -h, --help Print this usage message"
+ echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list"
+ echo ""
+ echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
+ echo " If no virtualenv is found, the script will ask if you would like to create one. If you "
+ echo " prefer to run tests NOT in a virtual environment, simply pass the -N option."
+ exit
+}
+
+function process_option {
+ case "$1" in
+ -h|--help) usage;;
+ -V|--virtual-env) always_venv=1; never_venv=0;;
+ -N|--no-virtual-env) always_venv=0; never_venv=1;;
+ -s|--no-site-packages) no_site_packages=1;;
+ -f|--force) force=1;;
+ -p|--pep8) just_pep8=1;;
+ -P|--no-pep8) no_pep8=1;;
+ -c|--coverage) coverage=1;;
+ -*) noseopts="$noseopts $1";;
+ *) noseargs="$noseargs $1"
+ esac
+}
+
+venv=.venv
+with_venv=tools/with_venv.sh
+always_venv=0
+never_venv=0
+force=0
+no_site_packages=0
+installvenvopts=
+noseargs=
+noseopts=
+wrapper=""
+just_pep8=0
+no_pep8=0
+coverage=0
+
+for arg in "$@"; do
+ process_option $arg
+done
+
+# If enabled, tell nose to collect coverage data
+if [ $coverage -eq 1 ]; then
+ noseopts="$noseopts --with-coverage --cover-package=keystoneclient"
+fi
+
+if [ $no_site_packages -eq 1 ]; then
+ installvenvopts="--no-site-packages"
+fi
+
+function run_tests {
+ # Just run the test suites in current environment
+ ${wrapper} $NOSETESTS
+ # If we get some short import error right away, print the error log directly
+ RESULT=$?
+ return $RESULT
+}
+
+function run_pep8 {
+ echo "Running pep8 ..."
+ srcfiles="keystoneclient tests"
+ # Just run PEP8 in current environment
+ #
+ # NOTE(sirp): W602 (deprecated 3-arg raise) is being ignored for the
+ # following reasons:
+ #
+ # 1. It's needed to preserve traceback information when re-raising
+ # exceptions; this is needed b/c Eventlet will clear exceptions when
+ # switching contexts.
+ #
+ # 2. There doesn't appear to be an alternative, "pep8-tool" compatible way of doing this
+ # in Python 2 (in Python 3 `with_traceback` could be used).
+ #
+ # 3. Can find no corroborating evidence that this is deprecated in Python 2
+ # other than what the PEP8 tool claims. It is deprecated in Python 3, so,
+ # perhaps the mistake was thinking that the deprecation applied to Python 2
+ # as well.
+ ${wrapper} pep8 --repeat --show-pep8 --show-source \
+ --ignore=E202,W602 \
+ ${srcfiles}
+}
+
+NOSETESTS="nosetests $noseopts $noseargs"
+
+if [ $never_venv -eq 0 ]
+then
+ # Remove the virtual environment if --force used
+ if [ $force -eq 1 ]; then
+ echo "Cleaning virtualenv..."
+ rm -rf ${venv}
+ fi
+ if [ -e ${venv} ]; then
+ wrapper="${with_venv}"
+ else
+ if [ $always_venv -eq 1 ]; then
+ # Automatically install the virtualenv
+ python tools/install_venv.py $installvenvopts
+ wrapper="${with_venv}"
+ else
+ echo -e "No virtual environment found...create one? (Y/n) \c"
+ read use_ve
+ if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
+ # Install the virtualenv and run the test suite in it
+ python tools/install_venv.py $installvenvopts
+ wrapper=${with_venv}
+ fi
+ fi
+ fi
+fi
+
+# Delete old coverage data from previous runs
+if [ $coverage -eq 1 ]; then
+ ${wrapper} coverage erase
+fi
+
+if [ $just_pep8 -eq 1 ]; then
+ run_pep8
+ exit
+fi
+
+run_tests
+
+# NOTE(sirp): we only want to run pep8 when we're running the full-test suite,
+# not when we're running tests individually. To handle this, we need to
+# distinguish between options (noseopts), which begin with a '-', and
+# arguments (noseargs).
+if [ -z "$noseargs" ]; then
+ if [ $no_pep8 -eq 0 ]; then
+ run_pep8
+ fi
+fi
+
+if [ $coverage -eq 1 ]; then
+ echo "Generating coverage report in covhtml/"
+ ${wrapper} coverage html -d covhtml -i
+fi
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..ab2a949
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,13 @@
+[nosetests]
+cover-package = glanceclient
+cover-html = true
+cover-erase = true
+cover-inclusive = true
+
+[build_sphinx]
+source-dir = docs/
+build-dir = docs/_build
+all_files = 1
+
+[upload_sphinx]
+upload-dir = docs/_build/html
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..098d2d9
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,42 @@
+import os
+import sys
+from setuptools import setup, find_packages
+
+
+def read(fname):
+ return open(os.path.join(os.path.dirname(__file__), fname)).read()
+
+requirements = ['httplib2', 'prettytable']
+if sys.version_info < (2, 6):
+ requirements.append('simplejson')
+if sys.version_info < (2, 7):
+ requirements.append('argparse')
+
+setup(
+ name = "python-glanceclient",
+ version = "2012.1",
+ description = "Client library for OpenStack Glance API",
+ long_description = read('README.rst'),
+ url = 'https://github.com/openstack/python-glanceclient',
+ license = 'Apache',
+ author = 'Jay Pipes, based on work by Rackspace and Jacob Kaplan-Moss',
+ author_email = 'jay.pipes@gmail.com',
+ packages = find_packages(exclude=['tests', 'tests.*']),
+ classifiers = [
+ 'Development Status :: 4 - Beta',
+ 'Environment :: Console',
+ 'Intended Audience :: Developers',
+ 'Intended Audience :: Information Technology',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ ],
+ install_requires = requirements,
+
+ tests_require = ["nose", "mock", "mox"],
+ test_suite = "nose.collector",
+
+ entry_points = {
+ 'console_scripts': ['glance = glanceclient.shell:main']
+ }
+)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..c81dd64
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,14 @@
+[tox]
+envlist = py26,py27
+
+[testenv]
+deps = -r{toxinidir}/tools/pip-requires
+commands = /bin/bash run_tests.sh -N
+
+[testenv:pep8]
+deps = pep8
+commands = /bin/bash run_tests.sh -N --pep8
+
+[testenv:coverage]
+deps = pep8
+commands = /bin/bash run_tests.sh -N --with-coverage