Initial checkin for new CLI and client package
Copied mostly from python-keystoneclient with some Glance-specific stuff. README.rst shows what WILL be the way to do things, not what is currently coded :)
This commit is contained in:
commit
972677fc3d
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.coverage
|
||||||
|
.venv
|
||||||
|
*,cover
|
||||||
|
cover
|
||||||
|
*.pyc
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
python_keystoneclient.egg-info
|
186
HACKING.rst
Normal file
186
HACKING.rst
Normal file
@ -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.
|
209
LICENSE
Normal file
209
LICENSE
Normal file
@ -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.
|
4
MANIFEST.in
Normal file
4
MANIFEST.in
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
include README.rst
|
||||||
|
include LICENSE
|
||||||
|
recursive-include docs *
|
||||||
|
recursive-include tests *
|
109
README.rst
Normal file
109
README.rst
Normal file
@ -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.
|
0
glanceclient/__init__.py
Normal file
0
glanceclient/__init__.py
Normal file
195
glanceclient/base.py
Normal file
195
glanceclient/base.py
Normal file
@ -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
|
175
glanceclient/client.py
Normal file
175
glanceclient/client.py
Normal file
@ -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)
|
132
glanceclient/exceptions.py
Normal file
132
glanceclient/exceptions.py
Normal file
@ -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)
|
0
glanceclient/generic/__init__.py
Normal file
0
glanceclient/generic/__init__.py
Normal file
205
glanceclient/generic/client.py
Normal file
205
glanceclient/generic/client.py
Normal file
@ -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)
|
57
glanceclient/generic/shell.py
Normal file
57
glanceclient/generic/shell.py
Normal file
@ -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"
|
81
glanceclient/service_catalog.py
Normal file
81
glanceclient/service_catalog.py
Normal file
@ -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
|
246
glanceclient/shell.py
Normal file
246
glanceclient/shell.py
Normal file
@ -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)
|
94
glanceclient/utils.py
Normal file
94
glanceclient/utils.py
Normal file
@ -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')
|
1
glanceclient/v1_1/__init__.py
Normal file
1
glanceclient/v1_1/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from keystoneclient.v2_0.client import Client
|
113
glanceclient/v1_1/client.py
Normal file
113
glanceclient/v1_1/client.py
Normal file
@ -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")
|
88
glanceclient/v1_1/images.py
Normal file
88
glanceclient/v1_1/images.py
Normal file
@ -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))
|
77
glanceclient/v1_1/shell.py
Executable file
77
glanceclient/v1_1/shell.py
Executable file
@ -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())
|
153
run_tests.sh
Executable file
153
run_tests.sh
Executable file
@ -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
|
13
setup.cfg
Normal file
13
setup.cfg
Normal file
@ -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
|
42
setup.py
Normal file
42
setup.py
Normal file
@ -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']
|
||||||
|
}
|
||||||
|
)
|
14
tox.ini
Normal file
14
tox.ini
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user