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:
		
							
								
								
									
										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
 | 
			
		||||
		Reference in New Issue
	
	Block a user