Initial split from python-novaclient.
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_novaclient.egg-info
 | 
			
		||||
							
								
								
									
										4
									
								
								.gitreview
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.gitreview
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
[gerrit]
 | 
			
		||||
host=review.openstack.org
 | 
			
		||||
port=29418
 | 
			
		||||
project=openstack/python-cinderclient.git
 | 
			
		||||
							
								
								
									
										15
									
								
								.mailmap
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.mailmap
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
Antony Messerli <amesserl@rackspace.com> root <root@debian.ohthree.com>
 | 
			
		||||
<amesserl@rackspace.com> <root@debian.ohthree.com>
 | 
			
		||||
<brian.waldon@rackspace.com> <bcwaldon@gmail.com>
 | 
			
		||||
Chris Behrens <cbehrens+github@codestud.com> comstud <cbehrens+github@codestud.com>
 | 
			
		||||
<cbehrens+github@codestud.com> <cbehrens@codestud.com>
 | 
			
		||||
Johannes Erdfelt <johannes.erdfelt@rackspace.com> jerdfelt <johannes@erdfelt.com>
 | 
			
		||||
<johannes.erdfelt@rackspace.com> <johannes@erdfelt.com>
 | 
			
		||||
<josh@jk0.org> <jkearney@nova.(none)>
 | 
			
		||||
<sandy@darksecretsoftware.com> <sandy.walsh@rackspace.com>
 | 
			
		||||
<sandy@darksecretsoftware.com> <sandy@sandywalsh.com>
 | 
			
		||||
Andy Smith <github@anarkystic.com> termie <github@anarkystic.com>
 | 
			
		||||
<chmouel.boudjnah@rackspace.co.uk> <chmouel@chmouel.com>
 | 
			
		||||
<matt.dietz@rackspace.com> <matthew.dietz@gmail.com>
 | 
			
		||||
Nikolay Sokolov <nsokolov@griddynamics.com> Nokolay Sokolov <nsokolov@griddynamics.com>
 | 
			
		||||
Nikolay Sokolov <nsokolov@griddynamics.com> Nokolay Sokolov <chemikadze@gmail.com>
 | 
			
		||||
							
								
								
									
										60
									
								
								AUTHORS
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								AUTHORS
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
Aaron Lee <aaron.lee@rackspace.com>
 | 
			
		||||
Alex Meade <alex.meade@rackspace.com>
 | 
			
		||||
Alvaro Lopez Garcia <aloga@ifca.unican.es>
 | 
			
		||||
Andrey Brindeyev <abrindeyev@griddynamics.com>
 | 
			
		||||
Andy Smith <github@anarkystic.com>
 | 
			
		||||
Anthony Young <sleepsonthefloor@gmail.com>
 | 
			
		||||
Antony Messerli <amesserl@rackspace.com>
 | 
			
		||||
Armando Migliaccio <Armando.Migliaccio@eu.citrix.com>
 | 
			
		||||
Brian Lamar <brian.lamar@rackspace.com>
 | 
			
		||||
Brian Waldon <brian.waldon@rackspace.com>
 | 
			
		||||
Chmouel Boudjnah <chmouel.boudjnah@rackspace.co.uk>
 | 
			
		||||
Chris Behrens <cbehrens+github@codestud.com>
 | 
			
		||||
Christian Berendt <berendt@b1-systems.de>
 | 
			
		||||
Christopher MacGown <ignoti+github@gmail.com>
 | 
			
		||||
Chuck Thier <cthier@gmail.com>
 | 
			
		||||
Cole Robinson <crobinso@redhat.com>
 | 
			
		||||
Dan Prince <dprince@redhat.com>
 | 
			
		||||
Dan Wendlandt <dan@nicira.com>
 | 
			
		||||
Dave Walker <Dave.Walker@canonical.com>
 | 
			
		||||
Dean Troyer <dtroyer@gmail.com>
 | 
			
		||||
Ed Leafe <ed@leafe.com>
 | 
			
		||||
Edouard Thuleau <edouard1.thuleau@orange.com>
 | 
			
		||||
Eldar Nugaev <eldr@ya.ru>
 | 
			
		||||
François Charlier <francois.charlier@ecindernce.com>
 | 
			
		||||
Gabriel Hurley <gabriel@strikeawe.com>
 | 
			
		||||
Gaurav Gupta <gaurav@denali-systems.com>
 | 
			
		||||
Hengqing Hu <hudayou@hotmail.com>
 | 
			
		||||
Ilya Alekseyev <ilyaalekseyev@acm.org>
 | 
			
		||||
Jake Dahn <admin@jakedahn.com>
 | 
			
		||||
James E. Blair <james.blair@rackspace.com>
 | 
			
		||||
Jason Kölker <jason@koelker.net>
 | 
			
		||||
Jason Straw <jason.straw@rackspace.com>
 | 
			
		||||
Jay Pipes <jaypipes@gmail.com>
 | 
			
		||||
Jesse Andrews <anotherjesse@gmail.com>
 | 
			
		||||
Johannes Erdfelt <johannes.erdfelt@rackspace.com>
 | 
			
		||||
John Garbutt <john.garbutt@citrix.com>
 | 
			
		||||
Josh Kearney <josh@jk0.org>
 | 
			
		||||
Juan G. Hernando Rivero <ghe.rivero@stackops.com>
 | 
			
		||||
Kevin L. Mitchell <kevin.mitchell@rackspace.com>
 | 
			
		||||
Kiall Mac Innes <kiall@managedit.ie>
 | 
			
		||||
Kirill Shileev <kshileev@griddynamics.com>
 | 
			
		||||
Lvov Maxim <mlvov@mirantis.com>
 | 
			
		||||
Matt Dietz <matt.dietz@rackspace.com>
 | 
			
		||||
Matt Stephenson <mattstep@mattstep.net>
 | 
			
		||||
Michael Basnight <mbasnight@gmail.com>
 | 
			
		||||
Nicholas Mistry <nmistry@gmail.com>
 | 
			
		||||
Nikolay Sokolov <nsokolov@griddynamics.com>
 | 
			
		||||
Pádraig Brady <pbrady@redhat.com>
 | 
			
		||||
Pavel Shkitin <pshkitin@griddynamics.com>
 | 
			
		||||
Peng Yong <ppyy@pubyun.com>
 | 
			
		||||
Rick Harris <rconradharris@gmail.com>
 | 
			
		||||
Robie Basak <robie.basak@canonical.com>
 | 
			
		||||
Russell Bryant <rbryant@redhat.com>
 | 
			
		||||
Sandy Walsh <sandy@darksecretsoftware.com>
 | 
			
		||||
Unmesh Gurjar <unmesh.gurjar@vertex.co.in>
 | 
			
		||||
William Wolf <throughnothing@gmail.com>
 | 
			
		||||
Yaguang Tang <heut2008@gmail.com>
 | 
			
		||||
Zhongyue Luo <lzyeval@gmail.com>
 | 
			
		||||
Scott Moser <smoser@ubuntu.com>
 | 
			
		||||
Paul Voccio <paul@substation9.com>
 | 
			
		||||
							
								
								
									
										115
									
								
								HACKING
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								HACKING
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,115 @@
 | 
			
		||||
Nova 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
 | 
			
		||||
 | 
			
		||||
Imports
 | 
			
		||||
-------
 | 
			
		||||
- thou shalt not import objects, only modules
 | 
			
		||||
- thou shalt not import more than one module per line
 | 
			
		||||
- thou shalt not make relative imports
 | 
			
		||||
- thou shalt organize your imports according to the following template
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
  # vim: tabstop=4 shiftwidth=4 softtabstop=4
 | 
			
		||||
  {{stdlib imports in human alphabetical order}}
 | 
			
		||||
  \n
 | 
			
		||||
  {{cinder imports in human alphabetical order}}
 | 
			
		||||
  \n
 | 
			
		||||
  \n
 | 
			
		||||
  {{begin your code}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
General
 | 
			
		||||
-------
 | 
			
		||||
- thou shalt put two newlines twixt toplevel code (funcs, classes, etc)
 | 
			
		||||
- thou shalt put one newline twixt methods in classes and anywhere else
 | 
			
		||||
- thou shalt not write "except:", use "except Exception:" at the very least
 | 
			
		||||
- thou shalt include your name with TODOs as in "TODO(termie)"
 | 
			
		||||
- thou shalt not name anything the same name as a builtin or reserved word
 | 
			
		||||
- thou shalt not violate causality in our time cone, or else
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Human Alphabetical Order Examples
 | 
			
		||||
---------------------------------
 | 
			
		||||
::
 | 
			
		||||
  import httplib
 | 
			
		||||
  import logging
 | 
			
		||||
  import random
 | 
			
		||||
  import StringIO
 | 
			
		||||
  import time
 | 
			
		||||
  import unittest
 | 
			
		||||
 | 
			
		||||
  from cinder import flags
 | 
			
		||||
  from cinder import test
 | 
			
		||||
  from cinder.auth import users
 | 
			
		||||
  from cinder.endpoint import api
 | 
			
		||||
  from cinder.endpoint import cloud
 | 
			
		||||
 | 
			
		||||
Docstrings
 | 
			
		||||
----------
 | 
			
		||||
  """A one line docstring looks like this and ends in a period."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  """A multiline docstring has a one-line summary, less than 80 characters.
 | 
			
		||||
 | 
			
		||||
  Then a new paragraph after a newline that explains in more detail any
 | 
			
		||||
  general information about the function, class or method. Example usages
 | 
			
		||||
  are also great to have here if it is a complex class for function. After
 | 
			
		||||
  you have finished your descriptions add an extra newline and close the
 | 
			
		||||
  quotations.
 | 
			
		||||
 | 
			
		||||
  When writing the docstring for a class, an extra line should be placed
 | 
			
		||||
  after the closing quotations. For more in-depth explanations for these
 | 
			
		||||
  decisions see http://www.python.org/dev/peps/pep-0257/
 | 
			
		||||
 | 
			
		||||
  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: description of the return value
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
Text encoding
 | 
			
		||||
----------
 | 
			
		||||
- All text within python code should be of type 'unicode'.
 | 
			
		||||
 | 
			
		||||
    WRONG:
 | 
			
		||||
 | 
			
		||||
    >>> s = 'foo'
 | 
			
		||||
    >>> s
 | 
			
		||||
    'foo'
 | 
			
		||||
    >>> type(s)
 | 
			
		||||
    <type 'str'>
 | 
			
		||||
 | 
			
		||||
    RIGHT:
 | 
			
		||||
 | 
			
		||||
    >>> u = u'foo'
 | 
			
		||||
    >>> u
 | 
			
		||||
    u'foo'
 | 
			
		||||
    >>> type(u)
 | 
			
		||||
    <type 'unicode'>
 | 
			
		||||
 | 
			
		||||
- Transitions between internal unicode and external strings should always
 | 
			
		||||
  be immediately and explicitly encoded or decoded.
 | 
			
		||||
 | 
			
		||||
- All external text that is not explicitly encoded (database storage,
 | 
			
		||||
  commandline arguments, etc.) should be presumed to be encoded as utf-8.
 | 
			
		||||
 | 
			
		||||
    WRONG:
 | 
			
		||||
 | 
			
		||||
    mystring = infile.readline()
 | 
			
		||||
    myreturnstring = do_some_magic_with(mystring)
 | 
			
		||||
    outfile.write(myreturnstring)
 | 
			
		||||
 | 
			
		||||
    RIGHT:
 | 
			
		||||
 | 
			
		||||
    mystring = infile.readline()
 | 
			
		||||
    mytext = s.decode('utf-8')
 | 
			
		||||
    returntext = do_some_magic_with(mytext)
 | 
			
		||||
    returnstring = returntext.encode('utf-8')
 | 
			
		||||
    outfile.write(returnstring)
 | 
			
		||||
							
								
								
									
										208
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,208 @@
 | 
			
		||||
Copyright (c) 2009 Jacob Kaplan-Moss - initial codebase (< v2.1)
 | 
			
		||||
Copyright (c) 2011 Rackspace - OpenStack extensions (>= v2.1)
 | 
			
		||||
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-cinderclient 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.
 | 
			
		||||
							
								
								
									
										8
									
								
								MANIFEST.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								MANIFEST.in
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
include AUTHORS
 | 
			
		||||
include HACKING
 | 
			
		||||
include LICENSE
 | 
			
		||||
include README.rst
 | 
			
		||||
include run_tests.sh tox.ini
 | 
			
		||||
recursive-include docs *
 | 
			
		||||
recursive-include tests *
 | 
			
		||||
recursive-include tools *
 | 
			
		||||
							
								
								
									
										155
									
								
								README.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								README.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
			
		||||
Python bindings to the OpenStack Volume API
 | 
			
		||||
===========================================
 | 
			
		||||
 | 
			
		||||
This is a client for the OpenStack Volume API. There's a Python API (the
 | 
			
		||||
``cinderclient`` module), and a command-line script (``cinder``). Each
 | 
			
		||||
implements 100% of the OpenStack Volume API.
 | 
			
		||||
 | 
			
		||||
[PENDING] `Full documentation is available`__.
 | 
			
		||||
 | 
			
		||||
__ http://packages.python.org/python-cinderclient/
 | 
			
		||||
 | 
			
		||||
You'll also probably want to read `OpenStack Compute Developer Guide API`__ --
 | 
			
		||||
the first bit, at least -- to get an idea of the concepts. Rackspace is doing
 | 
			
		||||
the cloud hosting thing a bit differently from Amazon, and if you get the
 | 
			
		||||
concepts this library should make more sense.
 | 
			
		||||
 | 
			
		||||
__ http://docs.openstack.org/api/
 | 
			
		||||
 | 
			
		||||
The project is hosted on `Launchpad`_, where bugs can be filed. The code is
 | 
			
		||||
hosted on `Github`_. Patches must be submitted using `Gerrit`_, *not* Github
 | 
			
		||||
pull requests.
 | 
			
		||||
 | 
			
		||||
.. _Github: https://github.com/openstack/python-cinderclient
 | 
			
		||||
.. _Launchpad: https://launchpad.net/python-cinderclient
 | 
			
		||||
.. _Gerrit: http://wiki.openstack.org/GerritWorkflow
 | 
			
		||||
 | 
			
		||||
This code a fork of `Jacobian's python-cloudservers`__ If you need API support
 | 
			
		||||
for the Rackspace API solely or the BSD license, you should use that repository.
 | 
			
		||||
python-client is licensed under the Apache License like the rest of OpenStack.
 | 
			
		||||
 | 
			
		||||
__ http://github.com/jacobian/python-cloudservers
 | 
			
		||||
 | 
			
		||||
.. contents:: Contents:
 | 
			
		||||
   :local:
 | 
			
		||||
 | 
			
		||||
Command-line API
 | 
			
		||||
----------------
 | 
			
		||||
 | 
			
		||||
Installing this package gets you a shell command, ``cinder``, that you
 | 
			
		||||
can use to interact with any Rackspace compatible API (including OpenStack).
 | 
			
		||||
 | 
			
		||||
You'll need to provide your OpenStack username and password. You can do this
 | 
			
		||||
with the ``--os_username``, ``--os_password`` and  ``--os_tenant_name``
 | 
			
		||||
params, but it's easier to just set them as environment variables::
 | 
			
		||||
 | 
			
		||||
    export OS_USERNAME=openstack
 | 
			
		||||
    export OS_PASSWORD=yadayada
 | 
			
		||||
    export OS_TENANT_NAME=myproject
 | 
			
		||||
 | 
			
		||||
You will also need to define the authentication url with ``--os_auth_url``
 | 
			
		||||
and the version of the API with ``--version``.  Or set them as an environment
 | 
			
		||||
variables as well::
 | 
			
		||||
 | 
			
		||||
    export OS_AUTH_URL=http://example.com:8774/v1.1/
 | 
			
		||||
    export OS_COMPUTE_API_VERSION=1.1
 | 
			
		||||
 | 
			
		||||
If you are using Keystone, you need to set the CINDER_URL to the keystone
 | 
			
		||||
endpoint::
 | 
			
		||||
 | 
			
		||||
    export OS_AUTH_URL=http://example.com:5000/v2.0/
 | 
			
		||||
 | 
			
		||||
Since Keystone can return multiple regions in the Service Catalog, you
 | 
			
		||||
can specify the one you want with ``--os_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
 | 
			
		||||
``cinder help``::
 | 
			
		||||
 | 
			
		||||
    usage: cinder [--debug] [--os_username OS_USERNAME] [--os_password OS_PASSWORD]
 | 
			
		||||
                [--os_tenant_name OS_TENANT_NAME] [--os_auth_url OS_AUTH_URL]
 | 
			
		||||
                [--os_region_name OS_REGION_NAME] [--service_type SERVICE_TYPE]
 | 
			
		||||
                [--service_name SERVICE_NAME] [--endpoint_type ENDPOINT_TYPE]
 | 
			
		||||
                [--version VERSION] [--username USERNAME]
 | 
			
		||||
                [--region_name REGION_NAME] [--apikey APIKEY]
 | 
			
		||||
                [--projectid PROJECTID] [--url URL]
 | 
			
		||||
                <subcommand> ...
 | 
			
		||||
 | 
			
		||||
    Command-line interface to the OpenStack Nova API.
 | 
			
		||||
 | 
			
		||||
    Positional arguments:
 | 
			
		||||
      <subcommand>
 | 
			
		||||
        create              Add a new volume.
 | 
			
		||||
        credentials         Show user credentials returned from auth
 | 
			
		||||
        delete              Remove a volume.
 | 
			
		||||
        endpoints           Discover endpoints that get returned from the
 | 
			
		||||
                            authenticate services
 | 
			
		||||
        list                List all the volumes.
 | 
			
		||||
        show                Show details about a volume.
 | 
			
		||||
        snapshot-create     Add a new snapshot.
 | 
			
		||||
        snapshot-delete     Remove a snapshot.
 | 
			
		||||
        snapshot-list       List all the snapshots.
 | 
			
		||||
        snapshot-show       Show details about a snapshot.
 | 
			
		||||
        type-create         Create a new volume type.
 | 
			
		||||
        type-delete         Delete a specific flavor
 | 
			
		||||
        type-list           Print a list of available 'volume types'.
 | 
			
		||||
        bash-completion     Prints all of the commands and options to stdout so
 | 
			
		||||
                            that the
 | 
			
		||||
        help                Display help about this program or one of its
 | 
			
		||||
                            subcommands.
 | 
			
		||||
 | 
			
		||||
    Optional arguments:
 | 
			
		||||
      --debug               Print debugging output
 | 
			
		||||
      --os_username OS_USERNAME
 | 
			
		||||
                            Defaults to env[OS_USERNAME].
 | 
			
		||||
      --os_password OS_PASSWORD
 | 
			
		||||
                            Defaults to env[OS_PASSWORD].
 | 
			
		||||
      --os_tenant_name OS_TENANT_NAME
 | 
			
		||||
                            Defaults to env[OS_TENANT_NAME].
 | 
			
		||||
      --os_auth_url OS_AUTH_URL
 | 
			
		||||
                            Defaults to env[OS_AUTH_URL].
 | 
			
		||||
      --os_region_name OS_REGION_NAME
 | 
			
		||||
                            Defaults to env[OS_REGION_NAME].
 | 
			
		||||
      --service_type SERVICE_TYPE
 | 
			
		||||
                            Defaults to compute for most actions
 | 
			
		||||
      --service_name SERVICE_NAME
 | 
			
		||||
                            Defaults to env[CINDER_SERVICE_NAME]
 | 
			
		||||
      --endpoint_type ENDPOINT_TYPE
 | 
			
		||||
                            Defaults to env[CINDER_ENDPOINT_TYPE] or publicURL.
 | 
			
		||||
      --os_compute_api_version VERSION
 | 
			
		||||
                            Accepts 1.1, defaults to env[OS_COMPUTE_API_VERSION].
 | 
			
		||||
      --username USERNAME   Deprecated
 | 
			
		||||
      --region_name REGION_NAME
 | 
			
		||||
                            Deprecated
 | 
			
		||||
      --apikey APIKEY, --password APIKEY
 | 
			
		||||
                            Deprecated
 | 
			
		||||
      --projectid PROJECTID, --tenant_name PROJECTID
 | 
			
		||||
                            Deprecated
 | 
			
		||||
      --url URL, --auth_url URL
 | 
			
		||||
                            Deprecated
 | 
			
		||||
 | 
			
		||||
    See "cinder help COMMAND" for help on a specific command.
 | 
			
		||||
 | 
			
		||||
Python API
 | 
			
		||||
----------
 | 
			
		||||
 | 
			
		||||
[PENDING] There's also a `complete Python API`__.
 | 
			
		||||
 | 
			
		||||
__ http://packages.python.org/python-cinderclient/
 | 
			
		||||
 | 
			
		||||
Quick-start using keystone::
 | 
			
		||||
 | 
			
		||||
    # use v2.0 auth with http://example.com:5000/v2.0/")
 | 
			
		||||
    >>> from cinderclient.v1 import client
 | 
			
		||||
    >>> nt = client.Client(USER, PASS, TENANT, AUTH_URL, service_type="compute")
 | 
			
		||||
    >>> nt.flavors.list()
 | 
			
		||||
    [...]
 | 
			
		||||
    >>> nt.servers.list()
 | 
			
		||||
    [...]
 | 
			
		||||
    >>> nt.keypairs.list()
 | 
			
		||||
    [...]
 | 
			
		||||
 | 
			
		||||
What's new?
 | 
			
		||||
-----------
 | 
			
		||||
 | 
			
		||||
[PENDING] See `the release notes <http://packages.python.org/python-cinderclient/releases.html>`_.
 | 
			
		||||
							
								
								
									
										0
									
								
								cinderclient/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cinderclient/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										293
									
								
								cinderclient/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										293
									
								
								cinderclient/base.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,293 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import contextlib
 | 
			
		||||
import hashlib
 | 
			
		||||
import os
 | 
			
		||||
from cinderclient import exceptions
 | 
			
		||||
from cinderclient import utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
    as a parameter when dealing with relationships.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        return obj.id
 | 
			
		||||
    except AttributeError:
 | 
			
		||||
        return obj
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Manager(utils.HookableMixin):
 | 
			
		||||
    """
 | 
			
		||||
    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.client.post(url, body=body)
 | 
			
		||||
        else:
 | 
			
		||||
            resp, body = self.api.client.get(url)
 | 
			
		||||
 | 
			
		||||
        if obj_class is None:
 | 
			
		||||
            obj_class = self.resource_class
 | 
			
		||||
 | 
			
		||||
        data = body[response_key]
 | 
			
		||||
        # NOTE(ja): keystone returns values as list as {'values': [ ... ]}
 | 
			
		||||
        #           unlike other services which just return the list...
 | 
			
		||||
        if isinstance(data, dict):
 | 
			
		||||
            try:
 | 
			
		||||
                data = data['values']
 | 
			
		||||
            except KeyError:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
        with self.completion_cache('human_id', obj_class, mode="w"):
 | 
			
		||||
            with self.completion_cache('uuid', obj_class, mode="w"):
 | 
			
		||||
                return [obj_class(self, res, loaded=True)
 | 
			
		||||
                        for res in data if res]
 | 
			
		||||
 | 
			
		||||
    @contextlib.contextmanager
 | 
			
		||||
    def completion_cache(self, cache_type, obj_class, mode):
 | 
			
		||||
        """
 | 
			
		||||
        The completion cache store items that can be used for bash
 | 
			
		||||
        autocompletion, like UUIDs or human-friendly IDs.
 | 
			
		||||
 | 
			
		||||
        A resource listing will clear and repopulate the cache.
 | 
			
		||||
 | 
			
		||||
        A resource create will append to the cache.
 | 
			
		||||
 | 
			
		||||
        Delete is not handled because listings are assumed to be performed
 | 
			
		||||
        often enough to keep the cache reasonably up-to-date.
 | 
			
		||||
        """
 | 
			
		||||
        base_dir = utils.env('CINDERCLIENT_UUID_CACHE_DIR',
 | 
			
		||||
                             default="~/.cinderclient")
 | 
			
		||||
 | 
			
		||||
        # NOTE(sirp): Keep separate UUID caches for each username + endpoint
 | 
			
		||||
        # pair
 | 
			
		||||
        username = utils.env('OS_USERNAME', 'CINDER_USERNAME')
 | 
			
		||||
        url = utils.env('OS_URL', 'CINDER_URL')
 | 
			
		||||
        uniqifier = hashlib.md5(username + url).hexdigest()
 | 
			
		||||
 | 
			
		||||
        cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            os.makedirs(cache_dir, 0755)
 | 
			
		||||
        except OSError:
 | 
			
		||||
            # NOTE(kiall): This is typicaly either permission denied while
 | 
			
		||||
            #              attempting to create the directory, or the directory
 | 
			
		||||
            #              already exists. Either way, don't fail.
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        resource = obj_class.__name__.lower()
 | 
			
		||||
        filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-'))
 | 
			
		||||
        path = os.path.join(cache_dir, filename)
 | 
			
		||||
 | 
			
		||||
        cache_attr = "_%s_cache" % cache_type
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            setattr(self, cache_attr, open(path, mode))
 | 
			
		||||
        except IOError:
 | 
			
		||||
            # NOTE(kiall): This is typicaly a permission denied while
 | 
			
		||||
            #              attempting to write the cache file.
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            yield
 | 
			
		||||
        finally:
 | 
			
		||||
            cache = getattr(self, cache_attr, None)
 | 
			
		||||
            if cache:
 | 
			
		||||
                cache.close()
 | 
			
		||||
                delattr(self, cache_attr)
 | 
			
		||||
 | 
			
		||||
    def write_to_completion_cache(self, cache_type, val):
 | 
			
		||||
        cache = getattr(self, "_%s_cache" % cache_type, None)
 | 
			
		||||
        if cache:
 | 
			
		||||
            cache.write("%s\n" % val)
 | 
			
		||||
 | 
			
		||||
    def _get(self, url, response_key=None):
 | 
			
		||||
        resp, body = self.api.client.get(url)
 | 
			
		||||
        if response_key:
 | 
			
		||||
            return self.resource_class(self, body[response_key], loaded=True)
 | 
			
		||||
        else:
 | 
			
		||||
            return self.resource_class(self, body, loaded=True)
 | 
			
		||||
 | 
			
		||||
    def _create(self, url, body, response_key, return_raw=False, **kwargs):
 | 
			
		||||
        self.run_hooks('modify_body_for_create', body, **kwargs)
 | 
			
		||||
        resp, body = self.api.client.post(url, body=body)
 | 
			
		||||
        if return_raw:
 | 
			
		||||
            return body[response_key]
 | 
			
		||||
 | 
			
		||||
        with self.completion_cache('human_id', self.resource_class, mode="a"):
 | 
			
		||||
            with self.completion_cache('uuid', self.resource_class, mode="a"):
 | 
			
		||||
                return self.resource_class(self, body[response_key])
 | 
			
		||||
 | 
			
		||||
    def _delete(self, url):
 | 
			
		||||
        resp, body = self.api.client.delete(url)
 | 
			
		||||
 | 
			
		||||
    def _update(self, url, body, **kwargs):
 | 
			
		||||
        self.run_hooks('modify_body_for_update', body, **kwargs)
 | 
			
		||||
        resp, body = self.api.client.put(url, body=body)
 | 
			
		||||
        return body
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
        """
 | 
			
		||||
        matches = self.findall(**kwargs)
 | 
			
		||||
        num_matches = len(matches)
 | 
			
		||||
        if num_matches == 0:
 | 
			
		||||
            msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
 | 
			
		||||
            raise exceptions.NotFound(404, msg)
 | 
			
		||||
        elif num_matches > 1:
 | 
			
		||||
            raise exceptions.NoUniqueMatch
 | 
			
		||||
        else:
 | 
			
		||||
            return matches[0]
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
    def list(self):
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Resource(object):
 | 
			
		||||
    """
 | 
			
		||||
    A resource represents a particular instance of an object (server, flavor,
 | 
			
		||||
    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
 | 
			
		||||
    """
 | 
			
		||||
    HUMAN_ID = False
 | 
			
		||||
 | 
			
		||||
    def __init__(self, manager, info, loaded=False):
 | 
			
		||||
        self.manager = manager
 | 
			
		||||
        self._info = info
 | 
			
		||||
        self._add_details(info)
 | 
			
		||||
        self._loaded = loaded
 | 
			
		||||
 | 
			
		||||
        # NOTE(sirp): ensure `id` is already present because if it isn't we'll
 | 
			
		||||
        # enter an infinite loop of __getattr__ -> get -> __init__ ->
 | 
			
		||||
        # __getattr__ -> ...
 | 
			
		||||
        if 'id' in self.__dict__ and len(str(self.id)) == 36:
 | 
			
		||||
            self.manager.write_to_completion_cache('uuid', self.id)
 | 
			
		||||
 | 
			
		||||
        human_id = self.human_id
 | 
			
		||||
        if human_id:
 | 
			
		||||
            self.manager.write_to_completion_cache('human_id', human_id)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def human_id(self):
 | 
			
		||||
        """Subclasses may override this provide a pretty ID which can be used
 | 
			
		||||
        for bash completion.
 | 
			
		||||
        """
 | 
			
		||||
        if 'name' in self.__dict__ and self.HUMAN_ID:
 | 
			
		||||
            return utils.slugify(self.name)
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def _add_details(self, info):
 | 
			
		||||
        for (k, v) in info.iteritems():
 | 
			
		||||
            try:
 | 
			
		||||
                setattr(self, k, v)
 | 
			
		||||
            except AttributeError:
 | 
			
		||||
                # In this case we already defined the attribute on the class
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
							
								
								
									
										330
									
								
								cinderclient/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								cinderclient/client.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,330 @@
 | 
			
		||||
# Copyright 2010 Jacob Kaplan-Moss
 | 
			
		||||
# Copyright 2011 OpenStack LLC.
 | 
			
		||||
# Copyright 2011 Piston Cloud Computing, Inc.
 | 
			
		||||
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
"""
 | 
			
		||||
OpenStack Client interface. Handles the REST calls and responses.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import httplib2
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import urlparse
 | 
			
		||||
 | 
			
		||||
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 cinderclient import exceptions
 | 
			
		||||
from cinderclient import service_catalog
 | 
			
		||||
from cinderclient import utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_logger = logging.getLogger(__name__)
 | 
			
		||||
if 'CINDERCLIENT_DEBUG' in os.environ and os.environ['CINDERCLIENT_DEBUG']:
 | 
			
		||||
    ch = logging.StreamHandler()
 | 
			
		||||
    _logger.setLevel(logging.DEBUG)
 | 
			
		||||
    _logger.addHandler(ch)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HTTPClient(httplib2.Http):
 | 
			
		||||
 | 
			
		||||
    USER_AGENT = 'python-cinderclient'
 | 
			
		||||
 | 
			
		||||
    def __init__(self, user, password, projectid, auth_url, insecure=False,
 | 
			
		||||
                 timeout=None, proxy_tenant_id=None,
 | 
			
		||||
                 proxy_token=None, region_name=None,
 | 
			
		||||
                 endpoint_type='publicURL', service_type=None,
 | 
			
		||||
                 service_name=None, volume_service_name=None):
 | 
			
		||||
        super(HTTPClient, self).__init__(timeout=timeout)
 | 
			
		||||
        self.user = user
 | 
			
		||||
        self.password = password
 | 
			
		||||
        self.projectid = projectid
 | 
			
		||||
        self.auth_url = auth_url.rstrip('/')
 | 
			
		||||
        self.version = 'v1'
 | 
			
		||||
        self.region_name = region_name
 | 
			
		||||
        self.endpoint_type = endpoint_type
 | 
			
		||||
        self.service_type = service_type
 | 
			
		||||
        self.service_name = service_name
 | 
			
		||||
        self.volume_service_name = volume_service_name
 | 
			
		||||
 | 
			
		||||
        self.management_url = None
 | 
			
		||||
        self.auth_token = None
 | 
			
		||||
        self.proxy_token = proxy_token
 | 
			
		||||
        self.proxy_tenant_id = proxy_tenant_id
 | 
			
		||||
 | 
			
		||||
        # httplib2 overrides
 | 
			
		||||
        self.force_exception_to_status_code = True
 | 
			
		||||
        self.disable_ssl_certificate_validation = insecure
 | 
			
		||||
 | 
			
		||||
    def http_log(self, args, kwargs, resp, body):
 | 
			
		||||
        if 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 %s\n", resp, body)
 | 
			
		||||
 | 
			
		||||
    def request(self, *args, **kwargs):
 | 
			
		||||
        kwargs.setdefault('headers', kwargs.get('headers', {}))
 | 
			
		||||
        kwargs['headers']['User-Agent'] = self.USER_AGENT
 | 
			
		||||
        kwargs['headers']['Accept'] = 'application/json'
 | 
			
		||||
        if 'body' in kwargs:
 | 
			
		||||
            kwargs['headers']['Content-Type'] = 'application/json'
 | 
			
		||||
            kwargs['body'] = json.dumps(kwargs['body'])
 | 
			
		||||
 | 
			
		||||
        resp, body = super(HTTPClient, self).request(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        self.http_log(args, kwargs, resp, body)
 | 
			
		||||
 | 
			
		||||
        if body:
 | 
			
		||||
            try:
 | 
			
		||||
                body = json.loads(body)
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                pass
 | 
			
		||||
        else:
 | 
			
		||||
            body = None
 | 
			
		||||
 | 
			
		||||
        if resp.status >= 400:
 | 
			
		||||
            raise exceptions.from_response(resp, body)
 | 
			
		||||
 | 
			
		||||
        return resp, body
 | 
			
		||||
 | 
			
		||||
    def _cs_request(self, url, method, **kwargs):
 | 
			
		||||
        if not self.management_url:
 | 
			
		||||
            self.authenticate()
 | 
			
		||||
 | 
			
		||||
        # 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:
 | 
			
		||||
            kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
 | 
			
		||||
            if self.projectid:
 | 
			
		||||
                kwargs['headers']['X-Auth-Project-Id'] = self.projectid
 | 
			
		||||
 | 
			
		||||
            resp, body = self.request(self.management_url + url, method,
 | 
			
		||||
                                      **kwargs)
 | 
			
		||||
            return resp, body
 | 
			
		||||
        except exceptions.Unauthorized, ex:
 | 
			
		||||
            try:
 | 
			
		||||
                self.authenticate()
 | 
			
		||||
                resp, body = self.request(self.management_url + url, method,
 | 
			
		||||
                                          **kwargs)
 | 
			
		||||
                return resp, body
 | 
			
		||||
            except exceptions.Unauthorized:
 | 
			
		||||
                raise ex
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
    def _extract_service_catalog(self, url, resp, body, extract_token=True):
 | 
			
		||||
        """See what the auth service told us and process the response.
 | 
			
		||||
        We may get redirected to another site, fail or actually get
 | 
			
		||||
        back a service catalog with a token and our endpoints."""
 | 
			
		||||
 | 
			
		||||
        if resp.status == 200:  # content must always present
 | 
			
		||||
            try:
 | 
			
		||||
                self.auth_url = url
 | 
			
		||||
                self.service_catalog = \
 | 
			
		||||
                    service_catalog.ServiceCatalog(body)
 | 
			
		||||
 | 
			
		||||
                if extract_token:
 | 
			
		||||
                    self.auth_token = self.service_catalog.get_token()
 | 
			
		||||
 | 
			
		||||
                management_url = self.service_catalog.url_for(
 | 
			
		||||
                               attr='region',
 | 
			
		||||
                               filter_value=self.region_name,
 | 
			
		||||
                               endpoint_type=self.endpoint_type,
 | 
			
		||||
                               service_type=self.service_type,
 | 
			
		||||
                               service_name=self.service_name,
 | 
			
		||||
                               volume_service_name=self.volume_service_name,)
 | 
			
		||||
                self.management_url = management_url.rstrip('/')
 | 
			
		||||
                return None
 | 
			
		||||
            except exceptions.AmbiguousEndpoints:
 | 
			
		||||
                print "Found more than one valid endpoint. Use a more " \
 | 
			
		||||
                      "restrictive filter"
 | 
			
		||||
                raise
 | 
			
		||||
            except KeyError:
 | 
			
		||||
                raise exceptions.AuthorizationFailure()
 | 
			
		||||
            except exceptions.EndpointNotFound:
 | 
			
		||||
                print "Could not find any suitable endpoint. Correct region?"
 | 
			
		||||
                raise
 | 
			
		||||
 | 
			
		||||
        elif resp.status == 305:
 | 
			
		||||
            return resp['location']
 | 
			
		||||
        else:
 | 
			
		||||
            raise exceptions.from_response(resp, body)
 | 
			
		||||
 | 
			
		||||
    def _fetch_endpoints_from_auth(self, url):
 | 
			
		||||
        """We have a token, but don't know the final endpoint for
 | 
			
		||||
        the region. We have to go back to the auth service and
 | 
			
		||||
        ask again. This request requires an admin-level token
 | 
			
		||||
        to work. The proxy token supplied could be from a low-level enduser.
 | 
			
		||||
 | 
			
		||||
        We can't get this from the keystone service endpoint, we have to use
 | 
			
		||||
        the admin endpoint.
 | 
			
		||||
 | 
			
		||||
        This will overwrite our admin token with the user token.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        # GET ...:5001/v2.0/tokens/#####/endpoints
 | 
			
		||||
        url = '/'.join([url, 'tokens', '%s?belongsTo=%s'
 | 
			
		||||
                        % (self.proxy_token, self.proxy_tenant_id)])
 | 
			
		||||
        _logger.debug("Using Endpoint URL: %s" % url)
 | 
			
		||||
        resp, body = self.request(url, "GET",
 | 
			
		||||
                                  headers={'X-Auth_Token': self.auth_token})
 | 
			
		||||
        return self._extract_service_catalog(url, resp, body,
 | 
			
		||||
                                             extract_token=False)
 | 
			
		||||
 | 
			
		||||
    def authenticate(self):
 | 
			
		||||
        magic_tuple = urlparse.urlsplit(self.auth_url)
 | 
			
		||||
        scheme, netloc, path, query, frag = magic_tuple
 | 
			
		||||
        port = magic_tuple.port
 | 
			
		||||
        if port is None:
 | 
			
		||||
            port = 80
 | 
			
		||||
        path_parts = path.split('/')
 | 
			
		||||
        for part in path_parts:
 | 
			
		||||
            if len(part) > 0 and part[0] == 'v':
 | 
			
		||||
                self.version = part
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        # TODO(sandy): Assume admin endpoint is 35357 for now.
 | 
			
		||||
        # Ideally this is going to have to be provided by the service catalog.
 | 
			
		||||
        new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,))
 | 
			
		||||
        admin_url = urlparse.urlunsplit(
 | 
			
		||||
                        (scheme, new_netloc, path, query, frag))
 | 
			
		||||
 | 
			
		||||
        auth_url = self.auth_url
 | 
			
		||||
        if self.version == "v2.0":
 | 
			
		||||
            while auth_url:
 | 
			
		||||
                if "CINDER_RAX_AUTH" in os.environ:
 | 
			
		||||
                    auth_url = self._rax_auth(auth_url)
 | 
			
		||||
                else:
 | 
			
		||||
                    auth_url = self._v2_auth(auth_url)
 | 
			
		||||
 | 
			
		||||
            # Are we acting on behalf of another user via an
 | 
			
		||||
            # existing token? If so, our actual endpoints may
 | 
			
		||||
            # be different than that of the admin token.
 | 
			
		||||
            if self.proxy_token:
 | 
			
		||||
                self._fetch_endpoints_from_auth(admin_url)
 | 
			
		||||
                # Since keystone no longer returns the user token
 | 
			
		||||
                # with the endpoints any more, we need to replace
 | 
			
		||||
                # our service account token with the user token.
 | 
			
		||||
                self.auth_token = self.proxy_token
 | 
			
		||||
        else:
 | 
			
		||||
            try:
 | 
			
		||||
                while auth_url:
 | 
			
		||||
                    auth_url = self._v1_auth(auth_url)
 | 
			
		||||
            # In some configurations cinder makes redirection to
 | 
			
		||||
            # v2.0 keystone endpoint. Also, new location does not contain
 | 
			
		||||
            # real endpoint, only hostname and port.
 | 
			
		||||
            except exceptions.AuthorizationFailure:
 | 
			
		||||
                if auth_url.find('v2.0') < 0:
 | 
			
		||||
                    auth_url = auth_url + '/v2.0'
 | 
			
		||||
                self._v2_auth(auth_url)
 | 
			
		||||
 | 
			
		||||
    def _v1_auth(self, url):
 | 
			
		||||
        if self.proxy_token:
 | 
			
		||||
            raise exceptions.NoTokenLookupException()
 | 
			
		||||
 | 
			
		||||
        headers = {'X-Auth-User': self.user,
 | 
			
		||||
                   'X-Auth-Key': self.password}
 | 
			
		||||
        if self.projectid:
 | 
			
		||||
            headers['X-Auth-Project-Id'] = self.projectid
 | 
			
		||||
 | 
			
		||||
        resp, body = self.request(url, 'GET', headers=headers)
 | 
			
		||||
        if resp.status in (200, 204):  # in some cases we get No Content
 | 
			
		||||
            try:
 | 
			
		||||
                mgmt_header = 'x-server-management-url'
 | 
			
		||||
                self.management_url = resp[mgmt_header].rstrip('/')
 | 
			
		||||
                self.auth_token = resp['x-auth-token']
 | 
			
		||||
                self.auth_url = url
 | 
			
		||||
            except KeyError:
 | 
			
		||||
                raise exceptions.AuthorizationFailure()
 | 
			
		||||
        elif resp.status == 305:
 | 
			
		||||
            return resp['location']
 | 
			
		||||
        else:
 | 
			
		||||
            raise exceptions.from_response(resp, body)
 | 
			
		||||
 | 
			
		||||
    def _v2_auth(self, url):
 | 
			
		||||
        """Authenticate against a v2.0 auth service."""
 | 
			
		||||
        body = {"auth": {
 | 
			
		||||
                   "passwordCredentials": {"username": self.user,
 | 
			
		||||
                                           "password": self.password}}}
 | 
			
		||||
 | 
			
		||||
        if self.projectid:
 | 
			
		||||
            body['auth']['tenantName'] = self.projectid
 | 
			
		||||
 | 
			
		||||
        self._authenticate(url, body)
 | 
			
		||||
 | 
			
		||||
    def _rax_auth(self, url):
 | 
			
		||||
        """Authenticate against the Rackspace auth service."""
 | 
			
		||||
        body = {"auth": {
 | 
			
		||||
                "RAX-KSKEY:apiKeyCredentials": {
 | 
			
		||||
                        "username": self.user,
 | 
			
		||||
                        "apiKey": self.password,
 | 
			
		||||
                        "tenantName": self.projectid}}}
 | 
			
		||||
 | 
			
		||||
        self._authenticate(url, body)
 | 
			
		||||
 | 
			
		||||
    def _authenticate(self, url, body):
 | 
			
		||||
        """Authenticate and extract the service catalog."""
 | 
			
		||||
        token_url = url + "/tokens"
 | 
			
		||||
 | 
			
		||||
        # Make sure we follow redirects when trying to reach Keystone
 | 
			
		||||
        tmp_follow_all_redirects = self.follow_all_redirects
 | 
			
		||||
        self.follow_all_redirects = True
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            resp, body = self.request(token_url, "POST", body=body)
 | 
			
		||||
        finally:
 | 
			
		||||
            self.follow_all_redirects = tmp_follow_all_redirects
 | 
			
		||||
 | 
			
		||||
        return self._extract_service_catalog(url, resp, body)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_client_class(version):
 | 
			
		||||
    version_map = {
 | 
			
		||||
        '1': 'cinderclient.v1.client.Client',
 | 
			
		||||
    }
 | 
			
		||||
    try:
 | 
			
		||||
        client_path = version_map[str(version)]
 | 
			
		||||
    except (KeyError, ValueError):
 | 
			
		||||
        msg = "Invalid client version '%s'. must be one of: %s" % (
 | 
			
		||||
              (version, ', '.join(version_map.keys())))
 | 
			
		||||
        raise exceptions.UnsupportedVersion(msg)
 | 
			
		||||
 | 
			
		||||
    return utils.import_class(client_path)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def Client(version, *args, **kwargs):
 | 
			
		||||
    client_class = get_client_class(version)
 | 
			
		||||
    return client_class(*args, **kwargs)
 | 
			
		||||
							
								
								
									
										146
									
								
								cinderclient/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								cinderclient/exceptions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,146 @@
 | 
			
		||||
# Copyright 2010 Jacob Kaplan-Moss
 | 
			
		||||
"""
 | 
			
		||||
Exception definitions.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UnsupportedVersion(Exception):
 | 
			
		||||
    """Indicates that the user is trying to use an unsupported
 | 
			
		||||
    version of the API"""
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommandError(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthorizationFailure(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoUniqueMatch(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 AmbiguousEndpoints(Exception):
 | 
			
		||||
    """Found more than one matching endpoint in Service Catalog."""
 | 
			
		||||
    def __init__(self, endpoints=None):
 | 
			
		||||
        self.endpoints = endpoints
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "AmbiguousEndpoints: %s" % repr(self.endpoints)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientException(Exception):
 | 
			
		||||
    """
 | 
			
		||||
    The base exception class for all exceptions this library raises.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, code, message=None, details=None, request_id=None):
 | 
			
		||||
        self.code = code
 | 
			
		||||
        self.message = message or self.__class__.message
 | 
			
		||||
        self.details = details
 | 
			
		||||
        self.request_id = request_id
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        formatted_string = "%s (HTTP %s)" % (self.message, self.code)
 | 
			
		||||
        if self.request_id:
 | 
			
		||||
            formatted_string += " (Request-ID: %s)" % self.request_id
 | 
			
		||||
 | 
			
		||||
        return formatted_string
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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 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)
 | 
			
		||||
    request_id = response.get('x-compute-request-id')
 | 
			
		||||
    if body:
 | 
			
		||||
        message = "n/a"
 | 
			
		||||
        details = "n/a"
 | 
			
		||||
        if hasattr(body, 'keys'):
 | 
			
		||||
            error = body[body.keys()[0]]
 | 
			
		||||
            message = error.get('message', None)
 | 
			
		||||
            details = error.get('details', None)
 | 
			
		||||
        return cls(code=response.status, message=message, details=details,
 | 
			
		||||
                   request_id=request_id)
 | 
			
		||||
    else:
 | 
			
		||||
        return cls(code=response.status, request_id=request_id)
 | 
			
		||||
							
								
								
									
										39
									
								
								cinderclient/extension.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								cinderclient/extension.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
from cinderclient import base
 | 
			
		||||
from cinderclient import utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Extension(utils.HookableMixin):
 | 
			
		||||
    """Extension descriptor."""
 | 
			
		||||
 | 
			
		||||
    SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
 | 
			
		||||
 | 
			
		||||
    def __init__(self, name, module):
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.module = module
 | 
			
		||||
        self._parse_extension_module()
 | 
			
		||||
 | 
			
		||||
    def _parse_extension_module(self):
 | 
			
		||||
        self.manager_class = None
 | 
			
		||||
        for attr_name, attr_value in self.module.__dict__.items():
 | 
			
		||||
            if attr_name in self.SUPPORTED_HOOKS:
 | 
			
		||||
                self.add_hook(attr_name, attr_value)
 | 
			
		||||
            elif utils.safe_issubclass(attr_value, base.Manager):
 | 
			
		||||
                self.manager_class = attr_value
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return "<Extension '%s'>" % self.name
 | 
			
		||||
							
								
								
									
										77
									
								
								cinderclient/service_catalog.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								cinderclient/service_catalog.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
# Copyright 2011 OpenStack LLC.
 | 
			
		||||
# Copyright 2011, Piston Cloud Computing, 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 cinderclient.exceptions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ServiceCatalog(object):
 | 
			
		||||
    """Helper methods for dealing with a Keystone Service Catalog."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, resource_dict):
 | 
			
		||||
        self.catalog = resource_dict
 | 
			
		||||
 | 
			
		||||
    def get_token(self):
 | 
			
		||||
        return self.catalog['access']['token']['id']
 | 
			
		||||
 | 
			
		||||
    def url_for(self, attr=None, filter_value=None,
 | 
			
		||||
                    service_type=None, endpoint_type='publicURL',
 | 
			
		||||
                    service_name=None, volume_service_name=None):
 | 
			
		||||
        """Fetch the public URL from the Compute service for
 | 
			
		||||
        a particular endpoint attribute. If none given, return
 | 
			
		||||
        the first. See tests for sample service catalog."""
 | 
			
		||||
        matching_endpoints = []
 | 
			
		||||
        if 'endpoints' in self.catalog:
 | 
			
		||||
            # We have a bastardized service catalog. Treat it special. :/
 | 
			
		||||
            for endpoint in self.catalog['endpoints']:
 | 
			
		||||
                if not filter_value or endpoint[attr] == filter_value:
 | 
			
		||||
                    matching_endpoints.append(endpoint)
 | 
			
		||||
            if not matching_endpoints:
 | 
			
		||||
                raise cinderclient.exceptions.EndpointNotFound()
 | 
			
		||||
 | 
			
		||||
        # We don't always get a service catalog back ...
 | 
			
		||||
        if not 'serviceCatalog' in self.catalog['access']:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        # Full catalog ...
 | 
			
		||||
        catalog = self.catalog['access']['serviceCatalog']
 | 
			
		||||
 | 
			
		||||
        for service in catalog:
 | 
			
		||||
            if service.get("type") != service_type:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if (service_name and service_type == 'compute' and
 | 
			
		||||
                    service.get('name') != service_name):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if (volume_service_name and service_type == 'volume' and
 | 
			
		||||
                    service.get('name') != volume_service_name):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            endpoints = service['endpoints']
 | 
			
		||||
            for endpoint in endpoints:
 | 
			
		||||
                if not filter_value or endpoint.get(attr) == filter_value:
 | 
			
		||||
                    endpoint["serviceName"] = service.get("name")
 | 
			
		||||
                    matching_endpoints.append(endpoint)
 | 
			
		||||
 | 
			
		||||
        if not matching_endpoints:
 | 
			
		||||
            raise cinderclient.exceptions.EndpointNotFound()
 | 
			
		||||
        elif len(matching_endpoints) > 1:
 | 
			
		||||
            raise cinderclient.exceptions.AmbiguousEndpoints(
 | 
			
		||||
                    endpoints=matching_endpoints)
 | 
			
		||||
        else:
 | 
			
		||||
            return matching_endpoints[0][endpoint_type]
 | 
			
		||||
							
								
								
									
										435
									
								
								cinderclient/shell.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										435
									
								
								cinderclient/shell.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,435 @@
 | 
			
		||||
 | 
			
		||||
# 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 Volume API.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import glob
 | 
			
		||||
import httplib2
 | 
			
		||||
import imp
 | 
			
		||||
import itertools
 | 
			
		||||
import os
 | 
			
		||||
import pkgutil
 | 
			
		||||
import sys
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from cinderclient import client
 | 
			
		||||
from cinderclient import exceptions as exc
 | 
			
		||||
import cinderclient.extension
 | 
			
		||||
from cinderclient import utils
 | 
			
		||||
from cinderclient.v1 import shell as shell_v1
 | 
			
		||||
 | 
			
		||||
DEFAULT_OS_VOLUME_API_VERSION = "1"
 | 
			
		||||
DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL'
 | 
			
		||||
DEFAULT_CINDER_SERVICE_TYPE = 'compute'
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CinderClientArgumentParser(argparse.ArgumentParser):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super(CinderClientArgumentParser, self).__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def error(self, message):
 | 
			
		||||
        """error(message: string)
 | 
			
		||||
 | 
			
		||||
        Prints a usage message incorporating the message to stderr and
 | 
			
		||||
        exits.
 | 
			
		||||
        """
 | 
			
		||||
        self.print_usage(sys.stderr)
 | 
			
		||||
        #FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
 | 
			
		||||
        choose_from = ' (choose from'
 | 
			
		||||
        progparts = self.prog.partition(' ')
 | 
			
		||||
        self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'"
 | 
			
		||||
                     " for more information.\n" %
 | 
			
		||||
                     {'errmsg': message.split(choose_from)[0],
 | 
			
		||||
                      'mainp': progparts[0],
 | 
			
		||||
                      'subp': progparts[2]})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OpenStackCinderShell(object):
 | 
			
		||||
 | 
			
		||||
    def get_base_parser(self):
 | 
			
		||||
        parser = CinderClientArgumentParser(
 | 
			
		||||
            prog='cinder',
 | 
			
		||||
            description=__doc__.strip(),
 | 
			
		||||
            epilog='See "cinder 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="Print debugging output")
 | 
			
		||||
 | 
			
		||||
        parser.add_argument('--os_username',
 | 
			
		||||
            default=utils.env('OS_USERNAME', 'CINDER_USERNAME'),
 | 
			
		||||
            help='Defaults to env[OS_USERNAME].')
 | 
			
		||||
 | 
			
		||||
        parser.add_argument('--os_password',
 | 
			
		||||
            default=utils.env('OS_PASSWORD', 'CINDER_PASSWORD'),
 | 
			
		||||
            help='Defaults to env[OS_PASSWORD].')
 | 
			
		||||
 | 
			
		||||
        parser.add_argument('--os_tenant_name',
 | 
			
		||||
            default=utils.env('OS_TENANT_NAME', 'CINDER_PROJECT_ID'),
 | 
			
		||||
            help='Defaults to env[OS_TENANT_NAME].')
 | 
			
		||||
 | 
			
		||||
        parser.add_argument('--os_auth_url',
 | 
			
		||||
            default=utils.env('OS_AUTH_URL', 'CINDER_URL'),
 | 
			
		||||
            help='Defaults to env[OS_AUTH_URL].')
 | 
			
		||||
 | 
			
		||||
        parser.add_argument('--os_region_name',
 | 
			
		||||
            default=utils.env('OS_REGION_NAME', 'CINDER_REGION_NAME'),
 | 
			
		||||
            help='Defaults to env[OS_REGION_NAME].')
 | 
			
		||||
 | 
			
		||||
        parser.add_argument('--service_type',
 | 
			
		||||
            help='Defaults to compute for most actions')
 | 
			
		||||
 | 
			
		||||
        parser.add_argument('--service_name',
 | 
			
		||||
            default=utils.env('CINDER_SERVICE_NAME'),
 | 
			
		||||
            help='Defaults to env[CINDER_SERVICE_NAME]')
 | 
			
		||||
 | 
			
		||||
        parser.add_argument('--volume_service_name',
 | 
			
		||||
            default=utils.env('CINDER_VOLUME_SERVICE_NAME'),
 | 
			
		||||
            help='Defaults to env[CINDER_VOLUME_SERVICE_NAME]')
 | 
			
		||||
 | 
			
		||||
        parser.add_argument('--endpoint_type',
 | 
			
		||||
            default=utils.env('CINDER_ENDPOINT_TYPE',
 | 
			
		||||
                        default=DEFAULT_CINDER_ENDPOINT_TYPE),
 | 
			
		||||
            help='Defaults to env[CINDER_ENDPOINT_TYPE] or '
 | 
			
		||||
                    + DEFAULT_CINDER_ENDPOINT_TYPE + '.')
 | 
			
		||||
 | 
			
		||||
        parser.add_argument('--os_volume_api_version',
 | 
			
		||||
            default=utils.env('OS_VOLUME_API_VERSION',
 | 
			
		||||
            default=DEFAULT_OS_VOLUME_API_VERSION),
 | 
			
		||||
            help='Accepts 1, defaults to env[OS_VOLUME_API_VERSION].')
 | 
			
		||||
 | 
			
		||||
        parser.add_argument('--insecure',
 | 
			
		||||
            default=utils.env('CINDERCLIENT_INSECURE', default=False),
 | 
			
		||||
            action='store_true',
 | 
			
		||||
            help=argparse.SUPPRESS)
 | 
			
		||||
 | 
			
		||||
        # FIXME(dtroyer): The args below are here for diablo compatibility,
 | 
			
		||||
        #                 remove them in folsum cycle
 | 
			
		||||
 | 
			
		||||
        # alias for --os_username, left in for backwards compatibility
 | 
			
		||||
        parser.add_argument('--username',
 | 
			
		||||
            help='Deprecated')
 | 
			
		||||
 | 
			
		||||
        # alias for --os_region_name, left in for backwards compatibility
 | 
			
		||||
        parser.add_argument('--region_name',
 | 
			
		||||
            help='Deprecated')
 | 
			
		||||
 | 
			
		||||
        # alias for --os_password, left in for backwards compatibility
 | 
			
		||||
        parser.add_argument('--apikey', '--password', dest='apikey',
 | 
			
		||||
            default=utils.env('CINDER_API_KEY'),
 | 
			
		||||
            help='Deprecated')
 | 
			
		||||
 | 
			
		||||
        # alias for --os_tenant_name, left in for backward compatibility
 | 
			
		||||
        parser.add_argument('--projectid', '--tenant_name', dest='projectid',
 | 
			
		||||
            default=utils.env('CINDER_PROJECT_ID'),
 | 
			
		||||
            help='Deprecated')
 | 
			
		||||
 | 
			
		||||
        # alias for --os_auth_url, left in for backward compatibility
 | 
			
		||||
        parser.add_argument('--url', '--auth_url', dest='url',
 | 
			
		||||
            default=utils.env('CINDER_URL'),
 | 
			
		||||
            help='Deprecated')
 | 
			
		||||
 | 
			
		||||
        return parser
 | 
			
		||||
 | 
			
		||||
    def get_subcommand_parser(self, version):
 | 
			
		||||
        parser = self.get_base_parser()
 | 
			
		||||
 | 
			
		||||
        self.subcommands = {}
 | 
			
		||||
        subparsers = parser.add_subparsers(metavar='<subcommand>')
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            actions_module = {
 | 
			
		||||
                '1.1': shell_v1,
 | 
			
		||||
                '2': shell_v1,
 | 
			
		||||
            }[version]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            actions_module = shell_v1
 | 
			
		||||
 | 
			
		||||
        self._find_actions(subparsers, actions_module)
 | 
			
		||||
        self._find_actions(subparsers, self)
 | 
			
		||||
 | 
			
		||||
        for extension in self.extensions:
 | 
			
		||||
            self._find_actions(subparsers, extension.module)
 | 
			
		||||
 | 
			
		||||
        self._add_bash_completion_subparser(subparsers)
 | 
			
		||||
 | 
			
		||||
        return parser
 | 
			
		||||
 | 
			
		||||
    def _discover_extensions(self, version):
 | 
			
		||||
        extensions = []
 | 
			
		||||
        for name, module in itertools.chain(
 | 
			
		||||
                self._discover_via_python_path(version),
 | 
			
		||||
                self._discover_via_contrib_path(version)):
 | 
			
		||||
 | 
			
		||||
            extension = cinderclient.extension.Extension(name, module)
 | 
			
		||||
            extensions.append(extension)
 | 
			
		||||
 | 
			
		||||
        return extensions
 | 
			
		||||
 | 
			
		||||
    def _discover_via_python_path(self, version):
 | 
			
		||||
        for (module_loader, name, ispkg) in pkgutil.iter_modules():
 | 
			
		||||
            if name.endswith('python_cinderclient_ext'):
 | 
			
		||||
                if not hasattr(module_loader, 'load_module'):
 | 
			
		||||
                    # Python 2.6 compat: actually get an ImpImporter obj
 | 
			
		||||
                    module_loader = module_loader.find_module(name)
 | 
			
		||||
 | 
			
		||||
                module = module_loader.load_module(name)
 | 
			
		||||
                yield name, module
 | 
			
		||||
 | 
			
		||||
    def _discover_via_contrib_path(self, version):
 | 
			
		||||
        module_path = os.path.dirname(os.path.abspath(__file__))
 | 
			
		||||
        version_str = "v%s" % version.replace('.', '_')
 | 
			
		||||
        ext_path = os.path.join(module_path, version_str, 'contrib')
 | 
			
		||||
        ext_glob = os.path.join(ext_path, "*.py")
 | 
			
		||||
 | 
			
		||||
        for ext_path in glob.iglob(ext_glob):
 | 
			
		||||
            name = os.path.basename(ext_path)[:-3]
 | 
			
		||||
 | 
			
		||||
            if name == "__init__":
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            module = imp.load_source(name, ext_path)
 | 
			
		||||
            yield name, module
 | 
			
		||||
 | 
			
		||||
    def _add_bash_completion_subparser(self, subparsers):
 | 
			
		||||
        subparser = subparsers.add_parser('bash_completion',
 | 
			
		||||
            add_help=False,
 | 
			
		||||
            formatter_class=OpenStackHelpFormatter
 | 
			
		||||
        )
 | 
			
		||||
        self.subcommands['bash_completion'] = subparser
 | 
			
		||||
        subparser.set_defaults(func=self.do_bash_completion)
 | 
			
		||||
 | 
			
		||||
    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 setup_debugging(self, debug):
 | 
			
		||||
        if not debug:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        streamhandler = logging.StreamHandler()
 | 
			
		||||
        streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
 | 
			
		||||
        streamhandler.setFormatter(logging.Formatter(streamformat))
 | 
			
		||||
        logger.setLevel(logging.DEBUG)
 | 
			
		||||
        logger.addHandler(streamhandler)
 | 
			
		||||
 | 
			
		||||
        httplib2.debuglevel = 1
 | 
			
		||||
 | 
			
		||||
    def main(self, argv):
 | 
			
		||||
        # Parse args once to find version
 | 
			
		||||
        parser = self.get_base_parser()
 | 
			
		||||
        (options, args) = parser.parse_known_args(argv)
 | 
			
		||||
        self.setup_debugging(options.debug)
 | 
			
		||||
 | 
			
		||||
        # build available subcommands based on version
 | 
			
		||||
        self.extensions = self._discover_extensions(
 | 
			
		||||
                options.os_volume_api_version)
 | 
			
		||||
        self._run_extension_hooks('__pre_parse_args__')
 | 
			
		||||
 | 
			
		||||
        subcommand_parser = self.get_subcommand_parser(
 | 
			
		||||
                options.os_volume_api_version)
 | 
			
		||||
        self.parser = subcommand_parser
 | 
			
		||||
 | 
			
		||||
        if options.help and len(args) == 0:
 | 
			
		||||
            subcommand_parser.print_help()
 | 
			
		||||
            return 0
 | 
			
		||||
 | 
			
		||||
        args = subcommand_parser.parse_args(argv)
 | 
			
		||||
        self._run_extension_hooks('__post_parse_args__', args)
 | 
			
		||||
 | 
			
		||||
        # Short-circuit and deal with help right away.
 | 
			
		||||
        if args.func == self.do_help:
 | 
			
		||||
            self.do_help(args)
 | 
			
		||||
            return 0
 | 
			
		||||
        elif args.func == self.do_bash_completion:
 | 
			
		||||
            self.do_bash_completion(args)
 | 
			
		||||
            return 0
 | 
			
		||||
 | 
			
		||||
        (os_username, os_password, os_tenant_name, os_auth_url,
 | 
			
		||||
                os_region_name, endpoint_type, insecure,
 | 
			
		||||
                service_type, service_name, volume_service_name,
 | 
			
		||||
                username, apikey, projectid, url, region_name) = (
 | 
			
		||||
                        args.os_username, args.os_password,
 | 
			
		||||
                        args.os_tenant_name, args.os_auth_url,
 | 
			
		||||
                        args.os_region_name, args.endpoint_type,
 | 
			
		||||
                        args.insecure, args.service_type, args.service_name,
 | 
			
		||||
                        args.volume_service_name, args.username,
 | 
			
		||||
                        args.apikey, args.projectid,
 | 
			
		||||
                        args.url, args.region_name)
 | 
			
		||||
 | 
			
		||||
        if not endpoint_type:
 | 
			
		||||
            endpoint_type = DEFAULT_CINDER_ENDPOINT_TYPE
 | 
			
		||||
 | 
			
		||||
        if not service_type:
 | 
			
		||||
            service_type = DEFAULT_CINDER_SERVICE_TYPE
 | 
			
		||||
            service_type = utils.get_service_type(args.func) or service_type
 | 
			
		||||
 | 
			
		||||
        #FIXME(usrleon): Here should be restrict for project id same as
 | 
			
		||||
        # for os_username or os_password but for compatibility it is not.
 | 
			
		||||
 | 
			
		||||
        if not utils.isunauthenticated(args.func):
 | 
			
		||||
            if not os_username:
 | 
			
		||||
                if not username:
 | 
			
		||||
                    raise exc.CommandError("You must provide a username "
 | 
			
		||||
                            "via either --os_username or env[OS_USERNAME]")
 | 
			
		||||
                else:
 | 
			
		||||
                    os_username = username
 | 
			
		||||
 | 
			
		||||
            if not os_password:
 | 
			
		||||
                if not apikey:
 | 
			
		||||
                    raise exc.CommandError("You must provide a password "
 | 
			
		||||
                            "via either --os_password or via "
 | 
			
		||||
                            "env[OS_PASSWORD]")
 | 
			
		||||
                else:
 | 
			
		||||
                    os_password = apikey
 | 
			
		||||
 | 
			
		||||
            if not os_tenant_name:
 | 
			
		||||
                if not projectid:
 | 
			
		||||
                    raise exc.CommandError("You must provide a tenant name "
 | 
			
		||||
                            "via either --os_tenant_name or "
 | 
			
		||||
                            "env[OS_TENANT_NAME]")
 | 
			
		||||
                else:
 | 
			
		||||
                    os_tenant_name = projectid
 | 
			
		||||
 | 
			
		||||
            if not os_auth_url:
 | 
			
		||||
                if not url:
 | 
			
		||||
                    raise exc.CommandError("You must provide an auth url "
 | 
			
		||||
                            "via either --os_auth_url or env[OS_AUTH_URL]")
 | 
			
		||||
                else:
 | 
			
		||||
                    os_auth_url = url
 | 
			
		||||
 | 
			
		||||
            if not os_region_name and region_name:
 | 
			
		||||
                os_region_name = region_name
 | 
			
		||||
 | 
			
		||||
        if not os_tenant_name:
 | 
			
		||||
            raise exc.CommandError("You must provide a tenant name "
 | 
			
		||||
                    "via either --os_tenant_name or env[OS_TENANT_NAME]")
 | 
			
		||||
 | 
			
		||||
        if not os_auth_url:
 | 
			
		||||
            raise exc.CommandError("You must provide an auth url "
 | 
			
		||||
                    "via either --os_auth_url or env[OS_AUTH_URL]")
 | 
			
		||||
 | 
			
		||||
        self.cs = client.Client(options.os_volume_api_version, os_username,
 | 
			
		||||
                os_password, os_tenant_name, os_auth_url, insecure,
 | 
			
		||||
                region_name=os_region_name, endpoint_type=endpoint_type,
 | 
			
		||||
                extensions=self.extensions, service_type=service_type,
 | 
			
		||||
                service_name=service_name,
 | 
			
		||||
                volume_service_name=volume_service_name)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            if not utils.isunauthenticated(args.func):
 | 
			
		||||
                self.cs.authenticate()
 | 
			
		||||
        except exc.Unauthorized:
 | 
			
		||||
            raise exc.CommandError("Invalid OpenStack Nova credentials.")
 | 
			
		||||
        except exc.AuthorizationFailure:
 | 
			
		||||
            raise exc.CommandError("Unable to authorize user")
 | 
			
		||||
 | 
			
		||||
        args.func(self.cs, args)
 | 
			
		||||
 | 
			
		||||
    def _run_extension_hooks(self, hook_type, *args, **kwargs):
 | 
			
		||||
        """Run hooks for all registered extensions."""
 | 
			
		||||
        for extension in self.extensions:
 | 
			
		||||
            extension.run_hooks(hook_type, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def do_bash_completion(self, args):
 | 
			
		||||
        """
 | 
			
		||||
        Prints all of the commands and options to stdout so that the
 | 
			
		||||
        cinder.bash_completion script doesn't have to hard code them.
 | 
			
		||||
        """
 | 
			
		||||
        commands = set()
 | 
			
		||||
        options = set()
 | 
			
		||||
        for sc_str, sc in self.subcommands.items():
 | 
			
		||||
            commands.add(sc_str)
 | 
			
		||||
            for option in sc._optionals._option_string_actions.keys():
 | 
			
		||||
                options.add(option)
 | 
			
		||||
 | 
			
		||||
        commands.remove('bash-completion')
 | 
			
		||||
        commands.remove('bash_completion')
 | 
			
		||||
        print ' '.join(commands | options)
 | 
			
		||||
 | 
			
		||||
    @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 args.command:
 | 
			
		||||
            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:
 | 
			
		||||
        OpenStackCinderShell().main(sys.argv[1:])
 | 
			
		||||
 | 
			
		||||
    except Exception, e:
 | 
			
		||||
        logger.debug(e, exc_info=1)
 | 
			
		||||
        print >> sys.stderr, "ERROR: %s" % str(e)
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										261
									
								
								cinderclient/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								cinderclient/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,261 @@
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import sys
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
import prettytable
 | 
			
		||||
 | 
			
		||||
from cinderclient import exceptions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def arg(*args, **kwargs):
 | 
			
		||||
    """Decorator for CLI args."""
 | 
			
		||||
    def _decorator(func):
 | 
			
		||||
        add_arg(func, *args, **kwargs)
 | 
			
		||||
        return func
 | 
			
		||||
    return _decorator
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def env(*vars, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
    returns the first environment variable set
 | 
			
		||||
    if none are non-empty, defaults to '' or keyword arg default
 | 
			
		||||
    """
 | 
			
		||||
    for v in vars:
 | 
			
		||||
        value = os.environ.get(v, None)
 | 
			
		||||
        if value:
 | 
			
		||||
            return value
 | 
			
		||||
    return kwargs.get('default', '')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_arg(f, *args, **kwargs):
 | 
			
		||||
    """Bind CLI arguments to a shell.py `do_foo` function."""
 | 
			
		||||
 | 
			
		||||
    if not hasattr(f, 'arguments'):
 | 
			
		||||
        f.arguments = []
 | 
			
		||||
 | 
			
		||||
    # NOTE(sirp): avoid dups that can occur when the module is shared across
 | 
			
		||||
    # tests.
 | 
			
		||||
    if (args, kwargs) not in f.arguments:
 | 
			
		||||
        # Because of the sematics of decorator composition if we just append
 | 
			
		||||
        # to the options list positional options will appear to be backwards.
 | 
			
		||||
        f.arguments.insert(0, (args, kwargs))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_resource_manager_extra_kwargs_hook(f, hook):
 | 
			
		||||
    """Adds hook to bind CLI arguments to ResourceManager calls.
 | 
			
		||||
 | 
			
		||||
    The `do_foo` calls in shell.py will receive CLI args and then in turn pass
 | 
			
		||||
    them through to the ResourceManager. Before passing through the args, the
 | 
			
		||||
    hooks registered here will be called, giving us a chance to add extra
 | 
			
		||||
    kwargs (taken from the command-line) to what's passed to the
 | 
			
		||||
    ResourceManager.
 | 
			
		||||
    """
 | 
			
		||||
    if not hasattr(f, 'resource_manager_kwargs_hooks'):
 | 
			
		||||
        f.resource_manager_kwargs_hooks = []
 | 
			
		||||
 | 
			
		||||
    names = [h.__name__ for h in f.resource_manager_kwargs_hooks]
 | 
			
		||||
    if hook.__name__ not in names:
 | 
			
		||||
        f.resource_manager_kwargs_hooks.append(hook)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False):
 | 
			
		||||
    """Return extra_kwargs by calling resource manager kwargs hooks."""
 | 
			
		||||
    hooks = getattr(f, "resource_manager_kwargs_hooks", [])
 | 
			
		||||
    extra_kwargs = {}
 | 
			
		||||
    for hook in hooks:
 | 
			
		||||
        hook_name = hook.__name__
 | 
			
		||||
        hook_kwargs = hook(args)
 | 
			
		||||
 | 
			
		||||
        conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys())
 | 
			
		||||
        if conflicting_keys and not allow_conflicts:
 | 
			
		||||
            raise Exception("Hook '%(hook_name)s' is attempting to redefine"
 | 
			
		||||
                            " attributes '%(conflicting_keys)s'" % locals())
 | 
			
		||||
 | 
			
		||||
        extra_kwargs.update(hook_kwargs)
 | 
			
		||||
 | 
			
		||||
    return extra_kwargs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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 service_type(stype):
 | 
			
		||||
    """
 | 
			
		||||
    Adds 'service_type' attribute to decorated function.
 | 
			
		||||
    Usage:
 | 
			
		||||
        @service_type('volume')
 | 
			
		||||
        def mymethod(f):
 | 
			
		||||
            ...
 | 
			
		||||
    """
 | 
			
		||||
    def inner(f):
 | 
			
		||||
        f.service_type = stype
 | 
			
		||||
        return f
 | 
			
		||||
    return inner
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_service_type(f):
 | 
			
		||||
    """
 | 
			
		||||
    Retrieves service type from function
 | 
			
		||||
    """
 | 
			
		||||
    return getattr(f, 'service_type', None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pretty_choice_list(l):
 | 
			
		||||
    return ', '.join("'%s'" % i for i in l)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def print_list(objs, fields, formatters={}):
 | 
			
		||||
    mixed_case_fields = ['serverId']
 | 
			
		||||
    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:
 | 
			
		||||
                if field in mixed_case_fields:
 | 
			
		||||
                    field_name = field.replace(' ', '_')
 | 
			
		||||
                else:
 | 
			
		||||
                    field_name = field.lower().replace(' ', '_')
 | 
			
		||||
                data = getattr(o, field_name, '')
 | 
			
		||||
                row.append(data)
 | 
			
		||||
        pt.add_row(row)
 | 
			
		||||
 | 
			
		||||
    print pt.get_string(sortby=fields[0])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def print_dict(d, property="Property"):
 | 
			
		||||
    pt = prettytable.PrettyTable([property, 'Value'], caching=False)
 | 
			
		||||
    pt.aligns = ['l', 'l']
 | 
			
		||||
    [pt.add_row(list(r)) for r in d.iteritems()]
 | 
			
		||||
    print pt.get_string(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
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        try:
 | 
			
		||||
            return manager.find(human_id=name_or_id)
 | 
			
		||||
        except exceptions.NotFound:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        # finally try to find entity by name
 | 
			
		||||
        try:
 | 
			
		||||
            return manager.find(name=name_or_id)
 | 
			
		||||
        except exceptions.NotFound:
 | 
			
		||||
            try:
 | 
			
		||||
                # Volumes does not have name, but display_name
 | 
			
		||||
                return manager.find(display_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)
 | 
			
		||||
    except exceptions.NoUniqueMatch:
 | 
			
		||||
        msg = ("Multiple %s matches found for '%s', use an ID to be more"
 | 
			
		||||
               " specific." % (manager.resource_class.__name__.lower(),
 | 
			
		||||
                               name_or_id))
 | 
			
		||||
        raise exceptions.CommandError(msg)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _format_servers_list_networks(server):
 | 
			
		||||
    output = []
 | 
			
		||||
    for (network, addresses) in server.networks.items():
 | 
			
		||||
        if len(addresses) == 0:
 | 
			
		||||
            continue
 | 
			
		||||
        addresses_csv = ', '.join(addresses)
 | 
			
		||||
        group = "%s=%s" % (network, addresses_csv)
 | 
			
		||||
        output.append(group)
 | 
			
		||||
 | 
			
		||||
    return '; '.join(output)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HookableMixin(object):
 | 
			
		||||
    """Mixin so classes can register and run hooks."""
 | 
			
		||||
    _hooks_map = {}
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def add_hook(cls, hook_type, hook_func):
 | 
			
		||||
        if hook_type not in cls._hooks_map:
 | 
			
		||||
            cls._hooks_map[hook_type] = []
 | 
			
		||||
 | 
			
		||||
        cls._hooks_map[hook_type].append(hook_func)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def run_hooks(cls, hook_type, *args, **kwargs):
 | 
			
		||||
        hook_funcs = cls._hooks_map.get(hook_type) or []
 | 
			
		||||
        for hook_func in hook_funcs:
 | 
			
		||||
            hook_func(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def safe_issubclass(*args):
 | 
			
		||||
    """Like issubclass, but will just return False if not a class."""
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        if issubclass(*args):
 | 
			
		||||
            return True
 | 
			
		||||
    except TypeError:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def import_class(import_str):
 | 
			
		||||
    """Returns a class from a string including module and class."""
 | 
			
		||||
    mod_str, _sep, class_str = import_str.rpartition('.')
 | 
			
		||||
    __import__(mod_str)
 | 
			
		||||
    return getattr(sys.modules[mod_str], class_str)
 | 
			
		||||
 | 
			
		||||
_slugify_strip_re = re.compile(r'[^\w\s-]')
 | 
			
		||||
_slugify_hyphenate_re = re.compile(r'[-\s]+')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# http://code.activestate.com/recipes/
 | 
			
		||||
#   577257-slugify-make-a-string-usable-in-a-url-or-filename/
 | 
			
		||||
def slugify(value):
 | 
			
		||||
    """
 | 
			
		||||
    Normalizes string, converts to lowercase, removes non-alpha characters,
 | 
			
		||||
    and converts spaces to hyphens.
 | 
			
		||||
 | 
			
		||||
    From Django's "django/template/defaultfilters.py".
 | 
			
		||||
    """
 | 
			
		||||
    import unicodedata
 | 
			
		||||
    if not isinstance(value, unicode):
 | 
			
		||||
        value = unicode(value)
 | 
			
		||||
    value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
 | 
			
		||||
    value = unicode(_slugify_strip_re.sub('', value).strip().lower())
 | 
			
		||||
    return _slugify_hyphenate_re.sub('-', value)
 | 
			
		||||
							
								
								
									
										17
									
								
								cinderclient/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cinderclient/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
# Copyright (c) 2012 OpenStack, LLC.
 | 
			
		||||
#
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
from cinderclient.v1.client import Client
 | 
			
		||||
							
								
								
									
										71
									
								
								cinderclient/v1/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								cinderclient/v1/client.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
from cinderclient import client
 | 
			
		||||
from cinderclient.v1 import volumes
 | 
			
		||||
from cinderclient.v1 import volume_snapshots
 | 
			
		||||
from cinderclient.v1 import volume_types
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Client(object):
 | 
			
		||||
    """
 | 
			
		||||
    Top-level object to access the OpenStack Compute API.
 | 
			
		||||
 | 
			
		||||
    Create an instance with your creds::
 | 
			
		||||
 | 
			
		||||
        >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL)
 | 
			
		||||
 | 
			
		||||
    Then call methods on its managers::
 | 
			
		||||
 | 
			
		||||
        >>> client.servers.list()
 | 
			
		||||
        ...
 | 
			
		||||
        >>> client.flavors.list()
 | 
			
		||||
        ...
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    # FIXME(jesse): project_id isn't required to authenticate
 | 
			
		||||
    def __init__(self, username, api_key, project_id, auth_url,
 | 
			
		||||
                  insecure=False, timeout=None, proxy_tenant_id=None,
 | 
			
		||||
                  proxy_token=None, region_name=None,
 | 
			
		||||
                  endpoint_type='publicURL', extensions=None,
 | 
			
		||||
                  service_type='compute', service_name=None,
 | 
			
		||||
                  volume_service_name=None):
 | 
			
		||||
        # FIXME(comstud): Rename the api_key argument above when we
 | 
			
		||||
        # know it's not being used as keyword argument
 | 
			
		||||
        password = api_key
 | 
			
		||||
 | 
			
		||||
        # extensions
 | 
			
		||||
        self.volumes = volumes.VolumeManager(self)
 | 
			
		||||
        self.volume_snapshots = volume_snapshots.SnapshotManager(self)
 | 
			
		||||
        self.volume_types = volume_types.VolumeTypeManager(self)
 | 
			
		||||
 | 
			
		||||
        # Add in any extensions...
 | 
			
		||||
        if extensions:
 | 
			
		||||
            for extension in extensions:
 | 
			
		||||
                if extension.manager_class:
 | 
			
		||||
                    setattr(self, extension.name,
 | 
			
		||||
                            extension.manager_class(self))
 | 
			
		||||
 | 
			
		||||
        self.client = client.HTTPClient(username,
 | 
			
		||||
                                    password,
 | 
			
		||||
                                    project_id,
 | 
			
		||||
                                    auth_url,
 | 
			
		||||
                                    insecure=insecure,
 | 
			
		||||
                                    timeout=timeout,
 | 
			
		||||
                                    proxy_token=proxy_token,
 | 
			
		||||
                                    proxy_tenant_id=proxy_tenant_id,
 | 
			
		||||
                                    region_name=region_name,
 | 
			
		||||
                                    endpoint_type=endpoint_type,
 | 
			
		||||
                                    service_type=service_type,
 | 
			
		||||
                                    service_name=service_name,
 | 
			
		||||
                                    volume_service_name=volume_service_name)
 | 
			
		||||
 | 
			
		||||
    def authenticate(self):
 | 
			
		||||
        """
 | 
			
		||||
        Authenticate against the server.
 | 
			
		||||
 | 
			
		||||
        Normally this is called automatically when you first access the API,
 | 
			
		||||
        but you can call this method to force authentication right now.
 | 
			
		||||
 | 
			
		||||
        Returns on success; raises :exc:`exceptions.Unauthorized` if the
 | 
			
		||||
        credentials are wrong.
 | 
			
		||||
        """
 | 
			
		||||
        self.client.authenticate()
 | 
			
		||||
							
								
								
									
										0
									
								
								cinderclient/v1/contrib/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cinderclient/v1/contrib/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										241
									
								
								cinderclient/v1/shell.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								cinderclient/v1/shell.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,241 @@
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from cinderclient import utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
 | 
			
		||||
                     poll_period=5, show_progress=True):
 | 
			
		||||
    """Block while an action is being performed, periodically printing
 | 
			
		||||
    progress.
 | 
			
		||||
    """
 | 
			
		||||
    def print_progress(progress):
 | 
			
		||||
        if show_progress:
 | 
			
		||||
            msg = ('\rInstance %(action)s... %(progress)s%% complete'
 | 
			
		||||
                   % dict(action=action, progress=progress))
 | 
			
		||||
        else:
 | 
			
		||||
            msg = '\rInstance %(action)s...' % dict(action=action)
 | 
			
		||||
 | 
			
		||||
        sys.stdout.write(msg)
 | 
			
		||||
        sys.stdout.flush()
 | 
			
		||||
 | 
			
		||||
    print
 | 
			
		||||
    while True:
 | 
			
		||||
        obj = poll_fn(obj_id)
 | 
			
		||||
        status = obj.status.lower()
 | 
			
		||||
        progress = getattr(obj, 'progress', None) or 0
 | 
			
		||||
        if status in final_ok_states:
 | 
			
		||||
            print_progress(100)
 | 
			
		||||
            print "\nFinished"
 | 
			
		||||
            break
 | 
			
		||||
        elif status == "error":
 | 
			
		||||
            print "\nError %(action)s instance" % locals()
 | 
			
		||||
            break
 | 
			
		||||
        else:
 | 
			
		||||
            print_progress(progress)
 | 
			
		||||
            time.sleep(poll_period)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _find_volume(cs, volume):
 | 
			
		||||
    """Get a volume by ID."""
 | 
			
		||||
    return utils.find_resource(cs.volumes, volume)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _find_volume_snapshot(cs, snapshot):
 | 
			
		||||
    """Get a volume snapshot by ID."""
 | 
			
		||||
    return utils.find_resource(cs.volume_snapshots, snapshot)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _print_volume(cs, volume):
 | 
			
		||||
    utils.print_dict(volume._info)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _print_volume_snapshot(cs, snapshot):
 | 
			
		||||
    utils.print_dict(snapshot._info)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _translate_volume_keys(collection):
 | 
			
		||||
    convert = [('displayName', 'display_name'), ('volumeType', 'volume_type')]
 | 
			
		||||
    for item in collection:
 | 
			
		||||
        keys = item.__dict__.keys()
 | 
			
		||||
        for from_key, to_key in convert:
 | 
			
		||||
            if from_key in keys and to_key not in keys:
 | 
			
		||||
                setattr(item, to_key, item._info[from_key])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _translate_volume_snapshot_keys(collection):
 | 
			
		||||
    convert = [('displayName', 'display_name'), ('volumeId', 'volume_id')]
 | 
			
		||||
    for item in collection:
 | 
			
		||||
        keys = item.__dict__.keys()
 | 
			
		||||
        for from_key, to_key in convert:
 | 
			
		||||
            if from_key in keys and to_key not in keys:
 | 
			
		||||
                setattr(item, to_key, item._info[from_key])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@utils.service_type('volume')
 | 
			
		||||
def do_list(cs, args):
 | 
			
		||||
    """List all the volumes."""
 | 
			
		||||
    volumes = cs.volumes.list()
 | 
			
		||||
    _translate_volume_keys(volumes)
 | 
			
		||||
 | 
			
		||||
    # Create a list of servers to which the volume is attached
 | 
			
		||||
    for vol in volumes:
 | 
			
		||||
        servers = [s.get('server_id') for s in vol.attachments]
 | 
			
		||||
        setattr(vol, 'attached_to', ','.join(map(str, servers)))
 | 
			
		||||
    utils.print_list(volumes, ['ID', 'Status', 'Display Name',
 | 
			
		||||
                        'Size', 'Volume Type', 'Attached to'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@utils.arg('volume', metavar='<volume>', help='ID of the volume.')
 | 
			
		||||
@utils.service_type('volume')
 | 
			
		||||
def do_show(cs, args):
 | 
			
		||||
    """Show details about a volume."""
 | 
			
		||||
    volume = _find_volume(cs, args.volume)
 | 
			
		||||
    _print_volume(cs, volume)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@utils.arg('size',
 | 
			
		||||
    metavar='<size>',
 | 
			
		||||
    type=int,
 | 
			
		||||
    help='Size of volume in GB')
 | 
			
		||||
@utils.arg('--snapshot_id',
 | 
			
		||||
    metavar='<snapshot_id>',
 | 
			
		||||
    help='Optional snapshot id to create the volume from. (Default=None)',
 | 
			
		||||
    default=None)
 | 
			
		||||
@utils.arg('--display_name', metavar='<display_name>',
 | 
			
		||||
            help='Optional volume name. (Default=None)',
 | 
			
		||||
            default=None)
 | 
			
		||||
@utils.arg('--display_description', metavar='<display_description>',
 | 
			
		||||
            help='Optional volume description. (Default=None)',
 | 
			
		||||
            default=None)
 | 
			
		||||
@utils.arg('--volume_type',
 | 
			
		||||
    metavar='<volume_type>',
 | 
			
		||||
    help='Optional volume type. (Default=None)',
 | 
			
		||||
    default=None)
 | 
			
		||||
@utils.service_type('volume')
 | 
			
		||||
def do_create(cs, args):
 | 
			
		||||
    """Add a new volume."""
 | 
			
		||||
    cs.volumes.create(args.size,
 | 
			
		||||
                        args.snapshot_id,
 | 
			
		||||
                        args.display_name,
 | 
			
		||||
                        args.display_description,
 | 
			
		||||
                        args.volume_type)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@utils.arg('volume', metavar='<volume>', help='ID of the volume to delete.')
 | 
			
		||||
@utils.service_type('volume')
 | 
			
		||||
def do_delete(cs, args):
 | 
			
		||||
    """Remove a volume."""
 | 
			
		||||
    volume = _find_volume(cs, args.volume)
 | 
			
		||||
    volume.delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@utils.service_type('volume')
 | 
			
		||||
def do_snapshot_list(cs, args):
 | 
			
		||||
    """List all the snapshots."""
 | 
			
		||||
    snapshots = cs.volume_snapshots.list()
 | 
			
		||||
    _translate_volume_snapshot_keys(snapshots)
 | 
			
		||||
    utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Display Name',
 | 
			
		||||
                        'Size'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@utils.arg('snapshot', metavar='<snapshot>', help='ID of the snapshot.')
 | 
			
		||||
@utils.service_type('volume')
 | 
			
		||||
def do_snapshot_show(cs, args):
 | 
			
		||||
    """Show details about a snapshot."""
 | 
			
		||||
    snapshot = _find_volume_snapshot(cs, args.snapshot)
 | 
			
		||||
    _print_volume_snapshot(cs, snapshot)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@utils.arg('volume_id',
 | 
			
		||||
    metavar='<volume_id>',
 | 
			
		||||
    help='ID of the volume to snapshot')
 | 
			
		||||
@utils.arg('--force',
 | 
			
		||||
    metavar='<True|False>',
 | 
			
		||||
    help='Optional flag to indicate whether to snapshot a volume even if its '
 | 
			
		||||
        'attached to an instance. (Default=False)',
 | 
			
		||||
    default=False)
 | 
			
		||||
@utils.arg('--display_name', metavar='<display_name>',
 | 
			
		||||
            help='Optional snapshot name. (Default=None)',
 | 
			
		||||
            default=None)
 | 
			
		||||
@utils.arg('--display_description', metavar='<display_description>',
 | 
			
		||||
            help='Optional snapshot description. (Default=None)',
 | 
			
		||||
            default=None)
 | 
			
		||||
@utils.service_type('volume')
 | 
			
		||||
def do_snapshot_create(cs, args):
 | 
			
		||||
    """Add a new snapshot."""
 | 
			
		||||
    cs.volume_snapshots.create(args.volume_id,
 | 
			
		||||
                        args.force,
 | 
			
		||||
                        args.display_name,
 | 
			
		||||
                        args.display_description)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@utils.arg('snapshot_id',
 | 
			
		||||
    metavar='<snapshot_id>',
 | 
			
		||||
    help='ID of the snapshot to delete.')
 | 
			
		||||
@utils.service_type('volume')
 | 
			
		||||
def do_snapshot_delete(cs, args):
 | 
			
		||||
    """Remove a snapshot."""
 | 
			
		||||
    snapshot = _find_volume_snapshot(cs, args.snapshot_id)
 | 
			
		||||
    snapshot.delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _print_volume_type_list(vtypes):
 | 
			
		||||
    utils.print_list(vtypes, ['ID', 'Name'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@utils.service_type('volume')
 | 
			
		||||
def do_type_list(cs, args):
 | 
			
		||||
    """Print a list of available 'volume types'."""
 | 
			
		||||
    vtypes = cs.volume_types.list()
 | 
			
		||||
    _print_volume_type_list(vtypes)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@utils.arg('name',
 | 
			
		||||
     metavar='<name>',
 | 
			
		||||
     help="Name of the new flavor")
 | 
			
		||||
@utils.service_type('volume')
 | 
			
		||||
def do_type_create(cs, args):
 | 
			
		||||
    """Create a new volume type."""
 | 
			
		||||
    vtype = cs.volume_types.create(args.name)
 | 
			
		||||
    _print_volume_type_list([vtype])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@utils.arg('id',
 | 
			
		||||
     metavar='<id>',
 | 
			
		||||
     help="Unique ID of the volume type to delete")
 | 
			
		||||
@utils.service_type('volume')
 | 
			
		||||
def do_type_delete(cs, args):
 | 
			
		||||
    """Delete a specific flavor"""
 | 
			
		||||
    cs.volume_types.delete(args.id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def do_endpoints(cs, args):
 | 
			
		||||
    """Discover endpoints that get returned from the authenticate services"""
 | 
			
		||||
    catalog = cs.client.service_catalog.catalog
 | 
			
		||||
    for e in catalog['access']['serviceCatalog']:
 | 
			
		||||
        utils.print_dict(e['endpoints'][0], e['name'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def do_credentials(cs, args):
 | 
			
		||||
    """Show user credentials returned from auth"""
 | 
			
		||||
    catalog = cs.client.service_catalog.catalog
 | 
			
		||||
    utils.print_dict(catalog['access']['user'], "User Credentials")
 | 
			
		||||
    utils.print_dict(catalog['access']['token'], "Token")
 | 
			
		||||
							
								
								
									
										88
									
								
								cinderclient/v1/volume_snapshots.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								cinderclient/v1/volume_snapshots.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
# Copyright 2011 Denali Systems, 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.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Volume snapshot interface (1.1 extension).
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from cinderclient import base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Snapshot(base.Resource):
 | 
			
		||||
    """
 | 
			
		||||
    A Snapshot is a point-in-time snapshot of an openstack volume.
 | 
			
		||||
    """
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return "<Snapshot: %s>" % self.id
 | 
			
		||||
 | 
			
		||||
    def delete(self):
 | 
			
		||||
        """
 | 
			
		||||
        Delete this snapshot.
 | 
			
		||||
        """
 | 
			
		||||
        self.manager.delete(self)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SnapshotManager(base.ManagerWithFind):
 | 
			
		||||
    """
 | 
			
		||||
    Manage :class:`Snapshot` resources.
 | 
			
		||||
    """
 | 
			
		||||
    resource_class = Snapshot
 | 
			
		||||
 | 
			
		||||
    def create(self, volume_id, force=False,
 | 
			
		||||
                    display_name=None, display_description=None):
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        Create a snapshot of the given volume.
 | 
			
		||||
 | 
			
		||||
        :param volume_id: The ID of the volume to snapshot.
 | 
			
		||||
        :param force: If force is True, create a snapshot even if the volume is
 | 
			
		||||
        attached to an instance. Default is False.
 | 
			
		||||
        :param display_name: Name of the snapshot
 | 
			
		||||
        :param display_description: Description of the snapshot
 | 
			
		||||
        :rtype: :class:`Snapshot`
 | 
			
		||||
        """
 | 
			
		||||
        body = {'snapshot': {'volume_id': volume_id,
 | 
			
		||||
                            'force': force,
 | 
			
		||||
                            'display_name': display_name,
 | 
			
		||||
                            'display_description': display_description}}
 | 
			
		||||
        return self._create('/snapshots', body, 'snapshot')
 | 
			
		||||
 | 
			
		||||
    def get(self, snapshot_id):
 | 
			
		||||
        """
 | 
			
		||||
        Get a snapshot.
 | 
			
		||||
 | 
			
		||||
        :param snapshot_id: The ID of the snapshot to get.
 | 
			
		||||
        :rtype: :class:`Snapshot`
 | 
			
		||||
        """
 | 
			
		||||
        return self._get("/snapshots/%s" % snapshot_id, "snapshot")
 | 
			
		||||
 | 
			
		||||
    def list(self, detailed=True):
 | 
			
		||||
        """
 | 
			
		||||
        Get a list of all snapshots.
 | 
			
		||||
 | 
			
		||||
        :rtype: list of :class:`Snapshot`
 | 
			
		||||
        """
 | 
			
		||||
        if detailed is True:
 | 
			
		||||
            return self._list("/snapshots/detail", "snapshots")
 | 
			
		||||
        else:
 | 
			
		||||
            return self._list("/snapshots", "snapshots")
 | 
			
		||||
 | 
			
		||||
    def delete(self, snapshot):
 | 
			
		||||
        """
 | 
			
		||||
        Delete a snapshot.
 | 
			
		||||
 | 
			
		||||
        :param snapshot: The :class:`Snapshot` to delete.
 | 
			
		||||
        """
 | 
			
		||||
        self._delete("/snapshots/%s" % base.getid(snapshot))
 | 
			
		||||
							
								
								
									
										77
									
								
								cinderclient/v1/volume_types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								cinderclient/v1/volume_types.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
# Copyright (c) 2011 Rackspace US, Inc.
 | 
			
		||||
#
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Volume Type interface.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from cinderclient import base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VolumeType(base.Resource):
 | 
			
		||||
    """
 | 
			
		||||
    A Volume Type is the type of volume to be created
 | 
			
		||||
    """
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return "<Volume Type: %s>" % self.name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VolumeTypeManager(base.ManagerWithFind):
 | 
			
		||||
    """
 | 
			
		||||
    Manage :class:`VolumeType` resources.
 | 
			
		||||
    """
 | 
			
		||||
    resource_class = VolumeType
 | 
			
		||||
 | 
			
		||||
    def list(self):
 | 
			
		||||
        """
 | 
			
		||||
        Get a list of all volume types.
 | 
			
		||||
 | 
			
		||||
        :rtype: list of :class:`VolumeType`.
 | 
			
		||||
        """
 | 
			
		||||
        return self._list("/types", "volume_types")
 | 
			
		||||
 | 
			
		||||
    def get(self, volume_type):
 | 
			
		||||
        """
 | 
			
		||||
        Get a specific volume type.
 | 
			
		||||
 | 
			
		||||
        :param volume_type: The ID of the :class:`VolumeType` to get.
 | 
			
		||||
        :rtype: :class:`VolumeType`
 | 
			
		||||
        """
 | 
			
		||||
        return self._get("/types/%s" % base.getid(volume_type), "volume_type")
 | 
			
		||||
 | 
			
		||||
    def delete(self, volume_type):
 | 
			
		||||
        """
 | 
			
		||||
        Delete a specific volume_type.
 | 
			
		||||
 | 
			
		||||
        :param volume_type: The ID of the :class:`VolumeType` to get.
 | 
			
		||||
        """
 | 
			
		||||
        self._delete("/types/%s" % base.getid(volume_type))
 | 
			
		||||
 | 
			
		||||
    def create(self, name):
 | 
			
		||||
        """
 | 
			
		||||
        Create a volume type.
 | 
			
		||||
 | 
			
		||||
        :param name: Descriptive name of the volume type
 | 
			
		||||
        :rtype: :class:`VolumeType`
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        body = {
 | 
			
		||||
            "volume_type": {
 | 
			
		||||
                "name": name,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return self._create("/types", body, "volume_type")
 | 
			
		||||
							
								
								
									
										135
									
								
								cinderclient/v1/volumes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								cinderclient/v1/volumes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
# Copyright 2011 Denali Systems, 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.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Volume interface (1.1 extension).
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from cinderclient import base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Volume(base.Resource):
 | 
			
		||||
    """
 | 
			
		||||
    A volume is an extra block level storage to the OpenStack instances.
 | 
			
		||||
    """
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return "<Volume: %s>" % self.id
 | 
			
		||||
 | 
			
		||||
    def delete(self):
 | 
			
		||||
        """
 | 
			
		||||
        Delete this volume.
 | 
			
		||||
        """
 | 
			
		||||
        self.manager.delete(self)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VolumeManager(base.ManagerWithFind):
 | 
			
		||||
    """
 | 
			
		||||
    Manage :class:`Volume` resources.
 | 
			
		||||
    """
 | 
			
		||||
    resource_class = Volume
 | 
			
		||||
 | 
			
		||||
    def create(self, size, snapshot_id=None,
 | 
			
		||||
                    display_name=None, display_description=None,
 | 
			
		||||
                    volume_type=None):
 | 
			
		||||
        """
 | 
			
		||||
        Create a volume.
 | 
			
		||||
 | 
			
		||||
        :param size: Size of volume in GB
 | 
			
		||||
        :param snapshot_id: ID of the snapshot
 | 
			
		||||
        :param display_name: Name of the volume
 | 
			
		||||
        :param display_description: Description of the volume
 | 
			
		||||
        :param volume_type: Type of volume
 | 
			
		||||
        :rtype: :class:`Volume`
 | 
			
		||||
        """
 | 
			
		||||
        body = {'volume': {'size': size,
 | 
			
		||||
                            'snapshot_id': snapshot_id,
 | 
			
		||||
                            'display_name': display_name,
 | 
			
		||||
                            'display_description': display_description,
 | 
			
		||||
                            'volume_type': volume_type}}
 | 
			
		||||
        return self._create('/volumes', body, 'volume')
 | 
			
		||||
 | 
			
		||||
    def get(self, volume_id):
 | 
			
		||||
        """
 | 
			
		||||
        Get a volume.
 | 
			
		||||
 | 
			
		||||
        :param volume_id: The ID of the volume to delete.
 | 
			
		||||
        :rtype: :class:`Volume`
 | 
			
		||||
        """
 | 
			
		||||
        return self._get("/volumes/%s" % volume_id, "volume")
 | 
			
		||||
 | 
			
		||||
    def list(self, detailed=True):
 | 
			
		||||
        """
 | 
			
		||||
        Get a list of all volumes.
 | 
			
		||||
 | 
			
		||||
        :rtype: list of :class:`Volume`
 | 
			
		||||
        """
 | 
			
		||||
        if detailed is True:
 | 
			
		||||
            return self._list("/volumes/detail", "volumes")
 | 
			
		||||
        else:
 | 
			
		||||
            return self._list("/volumes", "volumes")
 | 
			
		||||
 | 
			
		||||
    def delete(self, volume):
 | 
			
		||||
        """
 | 
			
		||||
        Delete a volume.
 | 
			
		||||
 | 
			
		||||
        :param volume: The :class:`Volume` to delete.
 | 
			
		||||
        """
 | 
			
		||||
        self._delete("/volumes/%s" % base.getid(volume))
 | 
			
		||||
 | 
			
		||||
    def create_server_volume(self, server_id, volume_id, device):
 | 
			
		||||
        """
 | 
			
		||||
        Attach a volume identified by the volume ID to the given server ID
 | 
			
		||||
 | 
			
		||||
        :param server_id: The ID of the server
 | 
			
		||||
        :param volume_id: The ID of the volume to attach.
 | 
			
		||||
        :param device: The device name
 | 
			
		||||
        :rtype: :class:`Volume`
 | 
			
		||||
        """
 | 
			
		||||
        body = {'volumeAttachment': {'volumeId': volume_id,
 | 
			
		||||
                            'device': device}}
 | 
			
		||||
        return self._create("/servers/%s/os-volume_attachments" % server_id,
 | 
			
		||||
            body, "volumeAttachment")
 | 
			
		||||
 | 
			
		||||
    def get_server_volume(self, server_id, attachment_id):
 | 
			
		||||
        """
 | 
			
		||||
        Get the volume identified by the attachment ID, that is attached to
 | 
			
		||||
        the given server ID
 | 
			
		||||
 | 
			
		||||
        :param server_id: The ID of the server
 | 
			
		||||
        :param attachment_id: The ID of the attachment
 | 
			
		||||
        :rtype: :class:`Volume`
 | 
			
		||||
        """
 | 
			
		||||
        return self._get("/servers/%s/os-volume_attachments/%s" % (server_id,
 | 
			
		||||
            attachment_id,), "volumeAttachment")
 | 
			
		||||
 | 
			
		||||
    def get_server_volumes(self, server_id):
 | 
			
		||||
        """
 | 
			
		||||
        Get a list of all the attached volumes for the given server ID
 | 
			
		||||
 | 
			
		||||
        :param server_id: The ID of the server
 | 
			
		||||
        :rtype: list of :class:`Volume`
 | 
			
		||||
        """
 | 
			
		||||
        return self._list("/servers/%s/os-volume_attachments" % server_id,
 | 
			
		||||
            "volumeAttachments")
 | 
			
		||||
 | 
			
		||||
    def delete_server_volume(self, server_id, attachment_id):
 | 
			
		||||
        """
 | 
			
		||||
        Detach a volume identified by the attachment ID from the given server
 | 
			
		||||
 | 
			
		||||
        :param server_id: The ID of the server
 | 
			
		||||
        :param attachment_id: The ID of the attachment
 | 
			
		||||
        """
 | 
			
		||||
        self._delete("/servers/%s/os-volume_attachments/%s" %
 | 
			
		||||
                                        (server_id, attachment_id,))
 | 
			
		||||
							
								
								
									
										1
									
								
								docs/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								docs/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
_build/
 | 
			
		||||
							
								
								
									
										89
									
								
								docs/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								docs/Makefile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
# Makefile for Sphinx documentation
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
# You can set these variables from the command line.
 | 
			
		||||
SPHINXOPTS    =
 | 
			
		||||
SPHINXBUILD   = sphinx-build
 | 
			
		||||
PAPER         =
 | 
			
		||||
BUILDDIR      = _build
 | 
			
		||||
 | 
			
		||||
# Internal variables.
 | 
			
		||||
PAPEROPT_a4     = -D latex_paper_size=a4
 | 
			
		||||
PAPEROPT_letter = -D latex_paper_size=letter
 | 
			
		||||
ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
 | 
			
		||||
 | 
			
		||||
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
 | 
			
		||||
 | 
			
		||||
help:
 | 
			
		||||
	@echo "Please use \`make <target>' where <target> is one of"
 | 
			
		||||
	@echo "  html      to make standalone HTML files"
 | 
			
		||||
	@echo "  dirhtml   to make HTML files named index.html in directories"
 | 
			
		||||
	@echo "  pickle    to make pickle files"
 | 
			
		||||
	@echo "  json      to make JSON files"
 | 
			
		||||
	@echo "  htmlhelp  to make HTML files and a HTML help project"
 | 
			
		||||
	@echo "  qthelp    to make HTML files and a qthelp project"
 | 
			
		||||
	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
 | 
			
		||||
	@echo "  changes   to make an overview of all changed/added/deprecated items"
 | 
			
		||||
	@echo "  linkcheck to check all external links for integrity"
 | 
			
		||||
	@echo "  doctest   to run all doctests embedded in the documentation (if enabled)"
 | 
			
		||||
 | 
			
		||||
clean:
 | 
			
		||||
	-rm -rf $(BUILDDIR)/*
 | 
			
		||||
 | 
			
		||||
html:
 | 
			
		||||
	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
 | 
			
		||||
 | 
			
		||||
dirhtml:
 | 
			
		||||
	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
 | 
			
		||||
 | 
			
		||||
pickle:
 | 
			
		||||
	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "Build finished; now you can process the pickle files."
 | 
			
		||||
 | 
			
		||||
json:
 | 
			
		||||
	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "Build finished; now you can process the JSON files."
 | 
			
		||||
 | 
			
		||||
htmlhelp:
 | 
			
		||||
	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "Build finished; now you can run HTML Help Workshop with the" \
 | 
			
		||||
	      ".hhp project file in $(BUILDDIR)/htmlhelp."
 | 
			
		||||
 | 
			
		||||
qthelp:
 | 
			
		||||
	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
 | 
			
		||||
	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
 | 
			
		||||
	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-cinderclient.qhcp"
 | 
			
		||||
	@echo "To view the help file:"
 | 
			
		||||
	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-cinderclient.qhc"
 | 
			
		||||
 | 
			
		||||
latex:
 | 
			
		||||
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
 | 
			
		||||
	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
 | 
			
		||||
	      "run these through (pdf)latex."
 | 
			
		||||
 | 
			
		||||
changes:
 | 
			
		||||
	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "The overview file is in $(BUILDDIR)/changes."
 | 
			
		||||
 | 
			
		||||
linkcheck:
 | 
			
		||||
	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "Link check complete; look for any errors in the above output " \
 | 
			
		||||
	      "or in $(BUILDDIR)/linkcheck/output.txt."
 | 
			
		||||
 | 
			
		||||
doctest:
 | 
			
		||||
	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
 | 
			
		||||
	@echo "Testing of doctests in the sources finished, look at the " \
 | 
			
		||||
	      "results in $(BUILDDIR)/doctest/output.txt."
 | 
			
		||||
							
								
								
									
										67
									
								
								docs/api.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								docs/api.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
The :mod:`cinderclient` Python API
 | 
			
		||||
==================================
 | 
			
		||||
 | 
			
		||||
.. module:: cinderclient
 | 
			
		||||
   :synopsis: A client for the OpenStack Nova API.
 | 
			
		||||
 | 
			
		||||
.. currentmodule:: cinderclient
 | 
			
		||||
 | 
			
		||||
Usage
 | 
			
		||||
-----
 | 
			
		||||
 | 
			
		||||
First create an instance of :class:`OpenStack` with your credentials::
 | 
			
		||||
 | 
			
		||||
    >>> from cinderclient import OpenStack
 | 
			
		||||
    >>> cinder = OpenStack(USERNAME, PASSWORD, AUTH_URL)
 | 
			
		||||
 | 
			
		||||
Then call methods on the :class:`OpenStack` object:
 | 
			
		||||
 | 
			
		||||
.. class:: OpenStack
 | 
			
		||||
 | 
			
		||||
    .. attribute:: backup_schedules
 | 
			
		||||
 | 
			
		||||
        A :class:`BackupScheduleManager` -- manage automatic backup images.
 | 
			
		||||
 | 
			
		||||
    .. attribute:: flavors
 | 
			
		||||
 | 
			
		||||
        A :class:`FlavorManager` -- query available "flavors" (hardware
 | 
			
		||||
        configurations).
 | 
			
		||||
 | 
			
		||||
    .. attribute:: images
 | 
			
		||||
 | 
			
		||||
        An :class:`ImageManager` -- query and create server disk images.
 | 
			
		||||
 | 
			
		||||
    .. attribute:: ipgroups
 | 
			
		||||
 | 
			
		||||
        A :class:`IPGroupManager` -- manage shared public IP addresses.
 | 
			
		||||
 | 
			
		||||
    .. attribute:: servers
 | 
			
		||||
 | 
			
		||||
        A :class:`ServerManager` -- start, stop, and manage virtual machines.
 | 
			
		||||
 | 
			
		||||
    .. automethod:: authenticate
 | 
			
		||||
 | 
			
		||||
For example::
 | 
			
		||||
 | 
			
		||||
    >>> cinder.servers.list()
 | 
			
		||||
    [<Server: buildslave-ubuntu-9.10>]
 | 
			
		||||
 | 
			
		||||
    >>> cinder.flavors.list()
 | 
			
		||||
    [<Flavor: 256 server>,
 | 
			
		||||
     <Flavor: 512 server>,
 | 
			
		||||
     <Flavor: 1GB server>,
 | 
			
		||||
     <Flavor: 2GB server>,
 | 
			
		||||
     <Flavor: 4GB server>,
 | 
			
		||||
     <Flavor: 8GB server>,
 | 
			
		||||
     <Flavor: 15.5GB server>]
 | 
			
		||||
 | 
			
		||||
    >>> fl = cinder.flavors.find(ram=512)
 | 
			
		||||
    >>> cinder.servers.create("my-server", flavor=fl)
 | 
			
		||||
    <Server: my-server>
 | 
			
		||||
 | 
			
		||||
For more information, see the reference:
 | 
			
		||||
 | 
			
		||||
.. toctree::
 | 
			
		||||
   :maxdepth: 2
 | 
			
		||||
 | 
			
		||||
   ref/index
 | 
			
		||||
							
								
								
									
										198
									
								
								docs/conf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								docs/conf.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,198 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
# python-cinderclient documentation build configuration file, created by
 | 
			
		||||
# sphinx-quickstart on Sun Dec  6 14:19:25 2009.
 | 
			
		||||
#
 | 
			
		||||
# This file is execfile()d with the current directory set to its containing dir.
 | 
			
		||||
#
 | 
			
		||||
# Note that not all possible configuration values are present in this
 | 
			
		||||
# autogenerated file.
 | 
			
		||||
#
 | 
			
		||||
# All configuration values have a default; values that are commented out
 | 
			
		||||
# serve to show the default.
 | 
			
		||||
 | 
			
		||||
import sys, os
 | 
			
		||||
 | 
			
		||||
# If extensions (or modules to document with autodoc) are in another directory,
 | 
			
		||||
# add these directories to sys.path here. If the directory is relative to the
 | 
			
		||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
 | 
			
		||||
#sys.path.append(os.path.abspath('.'))
 | 
			
		||||
 | 
			
		||||
# -- General configuration -----------------------------------------------------
 | 
			
		||||
 | 
			
		||||
# Add any Sphinx extension module names here, as strings. They can be extensions
 | 
			
		||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 | 
			
		||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
 | 
			
		||||
 | 
			
		||||
# Add any paths that contain templates here, relative to this directory.
 | 
			
		||||
templates_path = ['_templates']
 | 
			
		||||
 | 
			
		||||
# The suffix of source filenames.
 | 
			
		||||
source_suffix = '.rst'
 | 
			
		||||
 | 
			
		||||
# The encoding of source files.
 | 
			
		||||
#source_encoding = 'utf-8'
 | 
			
		||||
 | 
			
		||||
# The master toctree document.
 | 
			
		||||
master_doc = 'index'
 | 
			
		||||
 | 
			
		||||
# General information about the project.
 | 
			
		||||
project = u'python-cinderclient'
 | 
			
		||||
copyright = u'Rackspace, based on work by Jacob Kaplan-Moss'
 | 
			
		||||
 | 
			
		||||
# The version info for the project you're documenting, acts as replacement for
 | 
			
		||||
# |version| and |release|, also used in various other places throughout the
 | 
			
		||||
# built documents.
 | 
			
		||||
#
 | 
			
		||||
# The short X.Y version.
 | 
			
		||||
version = '2.6'
 | 
			
		||||
# The full version, including alpha/beta/rc tags.
 | 
			
		||||
release = '2.6.10'
 | 
			
		||||
 | 
			
		||||
# The language for content autogenerated by Sphinx. Refer to documentation
 | 
			
		||||
# for a list of supported languages.
 | 
			
		||||
#language = None
 | 
			
		||||
 | 
			
		||||
# There are two options for replacing |today|: either, you set today to some
 | 
			
		||||
# non-false value, then it is used:
 | 
			
		||||
#today = ''
 | 
			
		||||
# Else, today_fmt is used as the format for a strftime call.
 | 
			
		||||
#today_fmt = '%B %d, %Y'
 | 
			
		||||
 | 
			
		||||
# List of documents that shouldn't be included in the build.
 | 
			
		||||
#unused_docs = []
 | 
			
		||||
 | 
			
		||||
# List of directories, relative to source directory, that shouldn't be searched
 | 
			
		||||
# for source files.
 | 
			
		||||
exclude_trees = ['_build']
 | 
			
		||||
 | 
			
		||||
# The reST default role (used for this markup: `text`) to use for all documents.
 | 
			
		||||
#default_role = None
 | 
			
		||||
 | 
			
		||||
# If true, '()' will be appended to :func: etc. cross-reference text.
 | 
			
		||||
add_function_parentheses = True
 | 
			
		||||
 | 
			
		||||
# If true, the current module name will be prepended to all description
 | 
			
		||||
# unit titles (such as .. function::).
 | 
			
		||||
add_module_names = True
 | 
			
		||||
 | 
			
		||||
# If true, sectionauthor and moduleauthor directives will be shown in the
 | 
			
		||||
# output. They are ignored by default.
 | 
			
		||||
#show_authors = False
 | 
			
		||||
 | 
			
		||||
# The name of the Pygments (syntax highlighting) style to use.
 | 
			
		||||
pygments_style = 'sphinx'
 | 
			
		||||
 | 
			
		||||
# A list of ignored prefixes for module index sorting.
 | 
			
		||||
#modindex_common_prefix = []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# -- Options for HTML output ---------------------------------------------------
 | 
			
		||||
 | 
			
		||||
# The theme to use for HTML and HTML Help pages.  Major themes that come with
 | 
			
		||||
# Sphinx are currently 'default' and 'sphinxdoc'.
 | 
			
		||||
html_theme = 'nature'
 | 
			
		||||
 | 
			
		||||
# Theme options are theme-specific and customize the look and feel of a theme
 | 
			
		||||
# further.  For a list of options available for each theme, see the
 | 
			
		||||
# documentation.
 | 
			
		||||
#html_theme_options = {}
 | 
			
		||||
 | 
			
		||||
# Add any paths that contain custom themes here, relative to this directory.
 | 
			
		||||
#html_theme_path = []
 | 
			
		||||
 | 
			
		||||
# The name for this set of Sphinx documents.  If None, it defaults to
 | 
			
		||||
# "<project> v<release> documentation".
 | 
			
		||||
#html_title = None
 | 
			
		||||
 | 
			
		||||
# A shorter title for the navigation bar.  Default is the same as html_title.
 | 
			
		||||
#html_short_title = None
 | 
			
		||||
 | 
			
		||||
# The name of an image file (relative to this directory) to place at the top
 | 
			
		||||
# of the sidebar.
 | 
			
		||||
#html_logo = None
 | 
			
		||||
 | 
			
		||||
# The name of an image file (within the static path) to use as favicon of the
 | 
			
		||||
# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 | 
			
		||||
# pixels large.
 | 
			
		||||
#html_favicon = None
 | 
			
		||||
 | 
			
		||||
# Add any paths that contain custom static files (such as style sheets) here,
 | 
			
		||||
# relative to this directory. They are copied after the builtin static files,
 | 
			
		||||
# so a file named "default.css" will overwrite the builtin "default.css".
 | 
			
		||||
html_static_path = ['_static']
 | 
			
		||||
 | 
			
		||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 | 
			
		||||
# using the given strftime format.
 | 
			
		||||
#html_last_updated_fmt = '%b %d, %Y'
 | 
			
		||||
 | 
			
		||||
# If true, SmartyPants will be used to convert quotes and dashes to
 | 
			
		||||
# typographically correct entities.
 | 
			
		||||
#html_use_smartypants = True
 | 
			
		||||
 | 
			
		||||
# Custom sidebar templates, maps document names to template names.
 | 
			
		||||
#html_sidebars = {}
 | 
			
		||||
 | 
			
		||||
# Additional templates that should be rendered to pages, maps page names to
 | 
			
		||||
# template names.
 | 
			
		||||
#html_additional_pages = {}
 | 
			
		||||
 | 
			
		||||
# If false, no module index is generated.
 | 
			
		||||
#html_use_modindex = True
 | 
			
		||||
 | 
			
		||||
# If false, no index is generated.
 | 
			
		||||
#html_use_index = True
 | 
			
		||||
 | 
			
		||||
# If true, the index is split into individual pages for each letter.
 | 
			
		||||
#html_split_index = False
 | 
			
		||||
 | 
			
		||||
# If true, links to the reST sources are added to the pages.
 | 
			
		||||
#html_show_sourcelink = True
 | 
			
		||||
 | 
			
		||||
# If true, an OpenSearch description file will be output, and all pages will
 | 
			
		||||
# contain a <link> tag referring to it.  The value of this option must be the
 | 
			
		||||
# base URL from which the finished HTML is served.
 | 
			
		||||
#html_use_opensearch = ''
 | 
			
		||||
 | 
			
		||||
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
 | 
			
		||||
#html_file_suffix = ''
 | 
			
		||||
 | 
			
		||||
# Output file base name for HTML help builder.
 | 
			
		||||
htmlhelp_basename = 'python-cinderclientdoc'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# -- Options for LaTeX output --------------------------------------------------
 | 
			
		||||
 | 
			
		||||
# The paper size ('letter' or 'a4').
 | 
			
		||||
#latex_paper_size = 'letter'
 | 
			
		||||
 | 
			
		||||
# The font size ('10pt', '11pt' or '12pt').
 | 
			
		||||
#latex_font_size = '10pt'
 | 
			
		||||
 | 
			
		||||
# Grouping the document tree into LaTeX files. List of tuples
 | 
			
		||||
# (source start file, target name, title, author, documentclass [howto/manual]).
 | 
			
		||||
latex_documents = [
 | 
			
		||||
  ('index', 'python-cinderclient.tex', u'python-cinderclient Documentation',
 | 
			
		||||
   u'Rackspace - based on work by Jacob Kaplan-Moss', 'manual'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# The name of an image file (relative to this directory) to place at the top of
 | 
			
		||||
# the title page.
 | 
			
		||||
#latex_logo = None
 | 
			
		||||
 | 
			
		||||
# For "manual" documents, if this is true, then toplevel headings are parts,
 | 
			
		||||
# not chapters.
 | 
			
		||||
#latex_use_parts = False
 | 
			
		||||
 | 
			
		||||
# Additional stuff for the LaTeX preamble.
 | 
			
		||||
#latex_preamble = ''
 | 
			
		||||
 | 
			
		||||
# Documents to append as an appendix to all manuals.
 | 
			
		||||
#latex_appendices = []
 | 
			
		||||
 | 
			
		||||
# If false, no module index is generated.
 | 
			
		||||
#latex_use_modindex = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Example configuration for intersphinx: refer to the Python standard library.
 | 
			
		||||
intersphinx_mapping = {'http://docs.python.org/': None}
 | 
			
		||||
							
								
								
									
										45
									
								
								docs/index.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								docs/index.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
Python bindings to the OpenStack Nova API
 | 
			
		||||
==================================================
 | 
			
		||||
 | 
			
		||||
This is a client for OpenStack Nova API. There's :doc:`a Python API
 | 
			
		||||
<api>` (the :mod:`cinderclient` module), and a :doc:`command-line script
 | 
			
		||||
<shell>` (installed as :program:`cinder`). Each implements the entire
 | 
			
		||||
OpenStack Nova API.
 | 
			
		||||
 | 
			
		||||
You'll need an `OpenStack Nova` account, which you can get by using `cinder-manage`.
 | 
			
		||||
 | 
			
		||||
.. seealso::
 | 
			
		||||
 | 
			
		||||
    You may want to read `Rackspace's API guide`__ (PDF) -- the first bit, at
 | 
			
		||||
    least -- to get an idea of the concepts. Rackspace is doing the cloud
 | 
			
		||||
    hosting thing a bit differently from Amazon, and if you get the concepts
 | 
			
		||||
    this library should make more sense.
 | 
			
		||||
 | 
			
		||||
    __ http://docs.rackspacecloud.com/servers/api/cs-devguide-latest.pdf
 | 
			
		||||
 | 
			
		||||
Contents:
 | 
			
		||||
 | 
			
		||||
.. toctree::
 | 
			
		||||
   :maxdepth: 2
 | 
			
		||||
 | 
			
		||||
   shell
 | 
			
		||||
   api
 | 
			
		||||
   ref/index
 | 
			
		||||
   releases
 | 
			
		||||
 | 
			
		||||
Contributing
 | 
			
		||||
============
 | 
			
		||||
 | 
			
		||||
Development takes place `on GitHub`__; please file bugs/pull requests there.
 | 
			
		||||
 | 
			
		||||
__ https://github.com/rackspace/python-cinderclient
 | 
			
		||||
 | 
			
		||||
Run tests with ``python setup.py test``.
 | 
			
		||||
 | 
			
		||||
Indices and tables
 | 
			
		||||
==================
 | 
			
		||||
 | 
			
		||||
* :ref:`genindex`
 | 
			
		||||
* :ref:`modindex`
 | 
			
		||||
* :ref:`search`
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										60
									
								
								docs/ref/backup_schedules.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								docs/ref/backup_schedules.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
Backup schedules
 | 
			
		||||
================
 | 
			
		||||
 | 
			
		||||
.. currentmodule:: cinderclient
 | 
			
		||||
 | 
			
		||||
Rackspace allows scheduling of weekly and/or daily backups for virtual
 | 
			
		||||
servers. You can access these backup schedules either off the API object as
 | 
			
		||||
:attr:`OpenStack.backup_schedules`, or directly off a particular
 | 
			
		||||
:class:`Server` instance as :attr:`Server.backup_schedule`.
 | 
			
		||||
 | 
			
		||||
Classes
 | 
			
		||||
-------
 | 
			
		||||
 | 
			
		||||
.. autoclass:: BackupScheduleManager
 | 
			
		||||
   :members: create, delete, update, get
 | 
			
		||||
   
 | 
			
		||||
.. autoclass:: BackupSchedule
 | 
			
		||||
   :members: update, delete
 | 
			
		||||
   
 | 
			
		||||
   .. attribute:: enabled
 | 
			
		||||
   
 | 
			
		||||
        Is this backup enabled? (boolean)
 | 
			
		||||
        
 | 
			
		||||
   .. attribute:: weekly
 | 
			
		||||
   
 | 
			
		||||
        The day of week upon which to perform a weekly backup.
 | 
			
		||||
   
 | 
			
		||||
   .. attribute:: daily
 | 
			
		||||
   
 | 
			
		||||
        The daily time period during which to perform a daily backup.
 | 
			
		||||
        
 | 
			
		||||
Constants
 | 
			
		||||
---------
 | 
			
		||||
 | 
			
		||||
Constants for selecting weekly backup days:
 | 
			
		||||
 | 
			
		||||
    .. data:: BACKUP_WEEKLY_DISABLED
 | 
			
		||||
    .. data:: BACKUP_WEEKLY_SUNDAY
 | 
			
		||||
    .. data:: BACKUP_WEEKLY_MONDAY  
 | 
			
		||||
    .. data:: BACKUP_WEEKLY_TUESDAY 
 | 
			
		||||
    .. data:: BACKUP_WEEKLY_WEDNESDA
 | 
			
		||||
    .. data:: BACKUP_WEEKLY_THURSDAY
 | 
			
		||||
    .. data:: BACKUP_WEEKLY_FRIDAY  
 | 
			
		||||
    .. data:: BACKUP_WEEKLY_SATURDAY
 | 
			
		||||
    
 | 
			
		||||
Constants for selecting hourly backup windows:
 | 
			
		||||
 | 
			
		||||
    .. data:: BACKUP_DAILY_DISABLED   
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_0000_0200
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_0200_0400
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_0400_0600
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_0600_0800
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_0800_1000
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_1000_1200
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_1200_1400
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_1400_1600
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_1600_1800
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_1800_2000
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_2000_2200
 | 
			
		||||
    .. data:: BACKUP_DAILY_H_2200_0000
 | 
			
		||||
							
								
								
									
										14
									
								
								docs/ref/exceptions.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								docs/ref/exceptions.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
Exceptions
 | 
			
		||||
==========
 | 
			
		||||
 | 
			
		||||
.. currentmodule:: cinderclient
 | 
			
		||||
 | 
			
		||||
Exceptions
 | 
			
		||||
----------
 | 
			
		||||
 | 
			
		||||
Exceptions that the API might throw:
 | 
			
		||||
 | 
			
		||||
.. automodule:: cinderclient
 | 
			
		||||
   :members: OpenStackException, BadRequest, Unauthorized, Forbidden, 
 | 
			
		||||
             NotFound, OverLimit
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										35
									
								
								docs/ref/flavors.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								docs/ref/flavors.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
Flavors
 | 
			
		||||
=======
 | 
			
		||||
 | 
			
		||||
From Rackspace's API documentation:
 | 
			
		||||
 | 
			
		||||
    A flavor is an available hardware configuration for a server. Each flavor
 | 
			
		||||
    has a unique combination of disk space, memory capacity and priority for
 | 
			
		||||
    CPU time.
 | 
			
		||||
    
 | 
			
		||||
Classes
 | 
			
		||||
-------
 | 
			
		||||
 | 
			
		||||
.. currentmodule:: cinderclient
 | 
			
		||||
 | 
			
		||||
.. autoclass:: FlavorManager
 | 
			
		||||
   :members: get, list, find, findall
 | 
			
		||||
 | 
			
		||||
.. autoclass:: Flavor
 | 
			
		||||
   :members:
 | 
			
		||||
   
 | 
			
		||||
   .. attribute:: id
 | 
			
		||||
   
 | 
			
		||||
       This flavor's ID.
 | 
			
		||||
       
 | 
			
		||||
   .. attribute:: name
 | 
			
		||||
   
 | 
			
		||||
       A human-readable name for this flavor.
 | 
			
		||||
   
 | 
			
		||||
   .. attribute:: ram
 | 
			
		||||
   
 | 
			
		||||
       The amount of RAM this flavor has, in MB.
 | 
			
		||||
       
 | 
			
		||||
   .. attribute:: disk
 | 
			
		||||
   
 | 
			
		||||
       The amount of disk space this flavor has, in MB
 | 
			
		||||
							
								
								
									
										54
									
								
								docs/ref/images.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								docs/ref/images.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
Images
 | 
			
		||||
======
 | 
			
		||||
 | 
			
		||||
.. currentmodule:: cinderclient
 | 
			
		||||
 | 
			
		||||
An "image" is a snapshot from which you can create new server instances.
 | 
			
		||||
 | 
			
		||||
From Rackspace's own API documentation:
 | 
			
		||||
 | 
			
		||||
    An image is a collection of files used to create or rebuild a server.
 | 
			
		||||
    Rackspace provides a number of pre-built OS images by default. You may
 | 
			
		||||
    also create custom images from cloud servers you have launched. These
 | 
			
		||||
    custom images are useful for backup purposes or for producing "gold"
 | 
			
		||||
    server images if you plan to deploy a particular server configuration
 | 
			
		||||
    frequently.
 | 
			
		||||
 | 
			
		||||
Classes
 | 
			
		||||
-------
 | 
			
		||||
 | 
			
		||||
.. autoclass:: ImageManager
 | 
			
		||||
   :members: get, list, find, findall, create, delete
 | 
			
		||||
 | 
			
		||||
.. autoclass:: Image
 | 
			
		||||
   :members: delete
 | 
			
		||||
      
 | 
			
		||||
   .. attribute:: id
 | 
			
		||||
   
 | 
			
		||||
       This image's ID.
 | 
			
		||||
       
 | 
			
		||||
   .. attribute:: name
 | 
			
		||||
   
 | 
			
		||||
       This image's name.
 | 
			
		||||
       
 | 
			
		||||
   .. attribute:: created
 | 
			
		||||
   
 | 
			
		||||
       The date/time this image was created.
 | 
			
		||||
       
 | 
			
		||||
   .. attribute:: updated
 | 
			
		||||
   
 | 
			
		||||
       The date/time this instance was updated.
 | 
			
		||||
       
 | 
			
		||||
   .. attribute:: status
 | 
			
		||||
   
 | 
			
		||||
       The status of this image (usually ``"SAVING"`` or ``ACTIVE``).
 | 
			
		||||
       
 | 
			
		||||
   .. attribute:: progress
 | 
			
		||||
   
 | 
			
		||||
       During saving of an image this'll be set to something between
 | 
			
		||||
       0 and 100, representing a rough percentage done.
 | 
			
		||||
       
 | 
			
		||||
   .. attribute:: serverId
 | 
			
		||||
   
 | 
			
		||||
       If this image was created from a :class:`Server` then this attribute
 | 
			
		||||
       will be set to the ID of the server whence this image came.
 | 
			
		||||
							
								
								
									
										12
									
								
								docs/ref/index.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								docs/ref/index.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
API Reference
 | 
			
		||||
=============
 | 
			
		||||
 | 
			
		||||
.. toctree::
 | 
			
		||||
   :maxdepth: 1
 | 
			
		||||
   
 | 
			
		||||
   backup_schedules
 | 
			
		||||
   exceptions
 | 
			
		||||
   flavors
 | 
			
		||||
   images
 | 
			
		||||
   ipgroups
 | 
			
		||||
   servers
 | 
			
		||||
							
								
								
									
										46
									
								
								docs/ref/ipgroups.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								docs/ref/ipgroups.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
Shared IP addresses
 | 
			
		||||
===================
 | 
			
		||||
 | 
			
		||||
From the Rackspace API guide:
 | 
			
		||||
 | 
			
		||||
    Public IP addresses can be shared across multiple servers for use in
 | 
			
		||||
    various high availability scenarios. When an IP address is shared to
 | 
			
		||||
    another server, the cloud network restrictions are modified to allow each
 | 
			
		||||
    server to listen to and respond on that IP address (you may optionally
 | 
			
		||||
    specify that the target server network configuration be modified). Shared
 | 
			
		||||
    IP addresses can be used with many standard heartbeat facilities (e.g.
 | 
			
		||||
    ``keepalived``) that monitor for failure and manage IP failover.
 | 
			
		||||
 | 
			
		||||
    A shared IP group is a collection of servers that can share IPs with other
 | 
			
		||||
    members of the group. Any server in a group can share one or more public
 | 
			
		||||
    IPs with any other server in the group. With the exception of the first
 | 
			
		||||
    server in a shared IP group, servers must be launched into shared IP
 | 
			
		||||
    groups. A server may only be a member of one shared IP group.
 | 
			
		||||
 | 
			
		||||
.. seealso::
 | 
			
		||||
 | 
			
		||||
    Use :meth:`Server.share_ip` and `Server.unshare_ip` to share and unshare
 | 
			
		||||
    IPs in a group.
 | 
			
		||||
 | 
			
		||||
Classes
 | 
			
		||||
-------
 | 
			
		||||
 | 
			
		||||
.. currentmodule:: cinderclient
 | 
			
		||||
 | 
			
		||||
.. autoclass:: IPGroupManager
 | 
			
		||||
   :members: get, list, find, findall, create, delete
 | 
			
		||||
   
 | 
			
		||||
.. autoclass:: IPGroup
 | 
			
		||||
   :members: delete
 | 
			
		||||
   
 | 
			
		||||
   .. attribute:: id
 | 
			
		||||
   
 | 
			
		||||
        Shared group ID.
 | 
			
		||||
   
 | 
			
		||||
   .. attribute:: name
 | 
			
		||||
   
 | 
			
		||||
        Name of the group.
 | 
			
		||||
   
 | 
			
		||||
   .. attribute:: servers
 | 
			
		||||
   
 | 
			
		||||
        A list of server IDs in this group.
 | 
			
		||||
							
								
								
									
										73
									
								
								docs/ref/servers.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								docs/ref/servers.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
Servers
 | 
			
		||||
=======
 | 
			
		||||
 | 
			
		||||
A virtual machine instance.
 | 
			
		||||
 | 
			
		||||
Classes
 | 
			
		||||
-------
 | 
			
		||||
 | 
			
		||||
.. currentmodule:: cinderclient
 | 
			
		||||
 | 
			
		||||
.. autoclass:: ServerManager
 | 
			
		||||
   :members: get, list, find, findall, create, update, delete, share_ip,
 | 
			
		||||
             unshare_ip, reboot, rebuild, resize, confirm_resize,
 | 
			
		||||
             revert_resize
 | 
			
		||||
 | 
			
		||||
.. autoclass:: Server
 | 
			
		||||
   :members: update, delete, share_ip, unshare_ip, reboot, rebuild, resize,
 | 
			
		||||
             confirm_resize, revert_resize
 | 
			
		||||
 | 
			
		||||
   .. attribute:: id
 | 
			
		||||
 | 
			
		||||
        This server's ID.
 | 
			
		||||
 | 
			
		||||
   .. attribute:: name
 | 
			
		||||
 | 
			
		||||
        The name you gave the server when you booted it.
 | 
			
		||||
 | 
			
		||||
   .. attribute:: imageId
 | 
			
		||||
 | 
			
		||||
        The :class:`Image` this server was booted with.
 | 
			
		||||
 | 
			
		||||
   .. attribute:: flavorId
 | 
			
		||||
 | 
			
		||||
        This server's current :class:`Flavor`.
 | 
			
		||||
 | 
			
		||||
   .. attribute:: hostId
 | 
			
		||||
 | 
			
		||||
        Rackspace doesn't document this value. It appears to be SHA1 hash.
 | 
			
		||||
 | 
			
		||||
   .. attribute:: status
 | 
			
		||||
 | 
			
		||||
        The server's status (``BOOTING``, ``ACTIVE``, etc).
 | 
			
		||||
 | 
			
		||||
   .. attribute:: progress
 | 
			
		||||
 | 
			
		||||
        When booting, resizing, updating, etc., this will be set to a
 | 
			
		||||
        value between 0 and 100 giving a rough estimate of the progress
 | 
			
		||||
        of the current operation.
 | 
			
		||||
 | 
			
		||||
   .. attribute:: addresses
 | 
			
		||||
 | 
			
		||||
        The public and private IP addresses of this server. This'll be a dict
 | 
			
		||||
        of the form::
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
              "public" : ["67.23.10.138"],
 | 
			
		||||
              "private" : ["10.176.42.19"]
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        You *can* get more than one public/private IP provisioned, but not
 | 
			
		||||
        directly from the API; you'll need to open a support ticket.
 | 
			
		||||
 | 
			
		||||
   .. attribute:: metadata
 | 
			
		||||
 | 
			
		||||
        The metadata dict you gave when creating the server.
 | 
			
		||||
 | 
			
		||||
Constants
 | 
			
		||||
---------
 | 
			
		||||
 | 
			
		||||
Reboot types:
 | 
			
		||||
 | 
			
		||||
.. data:: REBOOT_SOFT
 | 
			
		||||
.. data:: REBOOT_HARD
 | 
			
		||||
							
								
								
									
										99
									
								
								docs/releases.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								docs/releases.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
			
		||||
=============
 | 
			
		||||
Release notes
 | 
			
		||||
=============
 | 
			
		||||
 | 
			
		||||
2.5.8 (July 11, 2011)
 | 
			
		||||
=====================
 | 
			
		||||
* returns all public/private ips, not just first one
 | 
			
		||||
* better 'cinder list' search options
 | 
			
		||||
 | 
			
		||||
2.5.7 - 2.5.6 = minor tweaks
 | 
			
		||||
 | 
			
		||||
2.5.5 (June 21, 2011)
 | 
			
		||||
=====================
 | 
			
		||||
* zone-boot min/max instance count added thanks to comstud
 | 
			
		||||
* create for user added thanks to cerberus
 | 
			
		||||
* fixed tests
 | 
			
		||||
 | 
			
		||||
2.5.3 (June 15, 2011)
 | 
			
		||||
=====================
 | 
			
		||||
* ProjectID can be None for backwards compatability.
 | 
			
		||||
* README/docs updated for projectId thanks to usrleon
 | 
			
		||||
 | 
			
		||||
2.5.1 (June 10, 2011)
 | 
			
		||||
=====================
 | 
			
		||||
* ProjectID now part of authentication
 | 
			
		||||
 | 
			
		||||
2.5.0 (June 3, 2011)
 | 
			
		||||
=================
 | 
			
		||||
 | 
			
		||||
* better logging thanks to GridDynamics
 | 
			
		||||
 | 
			
		||||
2.4.4 (June 1, 2011)
 | 
			
		||||
=================
 | 
			
		||||
 | 
			
		||||
* added support for GET /servers with reservation_id (and /servers/detail)
 | 
			
		||||
 | 
			
		||||
2.4.3 (May 27, 2011)
 | 
			
		||||
=================
 | 
			
		||||
 | 
			
		||||
* added support for POST /zones/select (client only, not cmdline)
 | 
			
		||||
 | 
			
		||||
2.4 (March 7, 2011)
 | 
			
		||||
=================
 | 
			
		||||
 | 
			
		||||
* added Jacob Kaplan-Moss copyright notices to older/untouched files.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
2.3 (March 2, 2011)
 | 
			
		||||
=================
 | 
			
		||||
 | 
			
		||||
* package renamed to python-cinderclient. Module to cinderclient
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
2.2 (March 1, 2011)
 | 
			
		||||
=================
 | 
			
		||||
 | 
			
		||||
* removed some license/copywrite notices from source that wasn't
 | 
			
		||||
  significantly changed.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
2.1 (Feb 28, 2011)
 | 
			
		||||
=================
 | 
			
		||||
 | 
			
		||||
* shell renamed to cinder from cindertools
 | 
			
		||||
 | 
			
		||||
* license changed from BSD to Apache
 | 
			
		||||
 | 
			
		||||
2.0 (Feb 7, 2011)
 | 
			
		||||
=================
 | 
			
		||||
 | 
			
		||||
* Forked from https://github.com/jacobian/python-cloudservers
 | 
			
		||||
 | 
			
		||||
* Rebranded to python-cindertools
 | 
			
		||||
 | 
			
		||||
* Auth URL support
 | 
			
		||||
 | 
			
		||||
* New OpenStack specific commands added (pause, suspend, etc)
 | 
			
		||||
 | 
			
		||||
1.2 (August 15, 2010)
 | 
			
		||||
=====================
 | 
			
		||||
 | 
			
		||||
* Support for Python 2.4 - 2.7.
 | 
			
		||||
 | 
			
		||||
* Improved output of :program:`cloudservers ipgroup-list`.
 | 
			
		||||
 | 
			
		||||
* Made ``cloudservers boot --ipgroup <name>`` work (as well as ``--ipgroup
 | 
			
		||||
  <id>``).
 | 
			
		||||
 | 
			
		||||
1.1 (May 6, 2010)
 | 
			
		||||
=================
 | 
			
		||||
 | 
			
		||||
* Added a ``--files`` option to :program:`cloudservers boot` supporting
 | 
			
		||||
  the upload of (up to five) files at boot time.
 | 
			
		||||
 | 
			
		||||
* Added a ``--key`` option to :program:`cloudservers boot` to key the server
 | 
			
		||||
  with an SSH public key at boot time. This is just a shortcut for ``--files``,
 | 
			
		||||
  but it's a useful shortcut.
 | 
			
		||||
 | 
			
		||||
* Changed the default server image to Ubuntu 10.04 LTS.
 | 
			
		||||
							
								
								
									
										52
									
								
								docs/shell.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								docs/shell.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
The :program:`cinder` shell utility
 | 
			
		||||
=========================================
 | 
			
		||||
 | 
			
		||||
.. program:: cinder
 | 
			
		||||
.. highlight:: bash
 | 
			
		||||
 | 
			
		||||
The :program:`cinder` shell utility interacts with OpenStack Nova API
 | 
			
		||||
from the command line. It supports the entirety of the OpenStack Nova API.
 | 
			
		||||
 | 
			
		||||
First, you'll need an OpenStack Nova account and an API key. You get this
 | 
			
		||||
by using the `cinder-manage` command in OpenStack Nova.
 | 
			
		||||
 | 
			
		||||
You'll need to provide :program:`cinder` with your OpenStack username and
 | 
			
		||||
API key. You can do this with the :option:`--os_username`, :option:`--os_password`
 | 
			
		||||
and :option:`--os_tenant_id` options, but it's easier to just set them as
 | 
			
		||||
environment variables by setting two environment variables:
 | 
			
		||||
 | 
			
		||||
.. envvar:: OS_USERNAME
 | 
			
		||||
 | 
			
		||||
    Your OpenStack Nova username.
 | 
			
		||||
 | 
			
		||||
.. envvar:: OS_PASSWORD
 | 
			
		||||
 | 
			
		||||
    Your password.
 | 
			
		||||
 | 
			
		||||
.. envvar:: OS_TENANT_NAME
 | 
			
		||||
 | 
			
		||||
    Project for work.
 | 
			
		||||
 | 
			
		||||
.. envvar:: OS_AUTH_URL
 | 
			
		||||
 | 
			
		||||
    The OpenStack API server URL.
 | 
			
		||||
 | 
			
		||||
.. envvar:: OS_COMPUTE_API_VERSION
 | 
			
		||||
 | 
			
		||||
    The OpenStack API version.
 | 
			
		||||
 | 
			
		||||
For example, in Bash you'd use::
 | 
			
		||||
 | 
			
		||||
    export OS_USERNAME=yourname
 | 
			
		||||
    export OS_PASSWORD=yadayadayada
 | 
			
		||||
    export OS_TENANT_NAME=myproject
 | 
			
		||||
    export OS_AUTH_URL=http://...
 | 
			
		||||
    export OS_COMPUTE_API_VERSION=1.1
 | 
			
		||||
    
 | 
			
		||||
From there, all shell commands take the form::
 | 
			
		||||
    
 | 
			
		||||
    cinder <command> [arguments...]
 | 
			
		||||
 | 
			
		||||
Run :program:`cinder help` to get a full list of all possible commands,
 | 
			
		||||
and run :program:`cinder help <command>` to get detailed help for that
 | 
			
		||||
command.
 | 
			
		||||
							
								
								
									
										154
									
								
								run_tests.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										154
									
								
								run_tests.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,154 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
set -eu
 | 
			
		||||
 | 
			
		||||
function usage {
 | 
			
		||||
  echo "Usage: $0 [OPTION]..."
 | 
			
		||||
  echo "Run python-cinderclient 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=cinderclient"
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ $no_site_packages -eq 1 ]; then
 | 
			
		||||
  installvenvopts="--no-site-packages"
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
function run_tests {
 | 
			
		||||
  # Cleanup *.pyc
 | 
			
		||||
  ${wrapper} find . -type f -name "*.pyc" -delete
 | 
			
		||||
  # 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="cinderclient 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.
 | 
			
		||||
  pep8_opts="--ignore=E202,W602 --repeat"
 | 
			
		||||
  ${wrapper} pep8 ${pep8_opts} ${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 = cinderclient
 | 
			
		||||
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
 | 
			
		||||
							
								
								
									
										56
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
# Copyright 2011 OpenStack, LLC
 | 
			
		||||
#
 | 
			
		||||
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
# you may not use this file except in compliance with the License.
 | 
			
		||||
# You may obtain a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
# http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
# Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
# See the License for the specific language governing permissions and
 | 
			
		||||
# limitations under the License.
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import setuptools
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
requirements = ["httplib2", "prettytable"]
 | 
			
		||||
if sys.version_info < (2, 6):
 | 
			
		||||
    requirements.append("simplejson")
 | 
			
		||||
if sys.version_info < (2, 7):
 | 
			
		||||
    requirements.append("argparse")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def read_file(file_name):
 | 
			
		||||
    return open(os.path.join(os.path.dirname(__file__), file_name)).read()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
setuptools.setup(
 | 
			
		||||
    name="python-cinderclient",
 | 
			
		||||
    version="2012.2",
 | 
			
		||||
    author="Rackspace, based on work by Jacob Kaplan-Moss",
 | 
			
		||||
    author_email="github@racklabs.com",
 | 
			
		||||
    description="Client library for OpenStack Nova API.",
 | 
			
		||||
    long_description=read_file("README.rst"),
 | 
			
		||||
    license="Apache License, Version 2.0",
 | 
			
		||||
    url="https://github.com/openstack/python-cinderclient",
 | 
			
		||||
    packages=setuptools.find_packages(exclude=['tests', 'tests.*']),
 | 
			
		||||
    install_requires=requirements,
 | 
			
		||||
    tests_require=["nose", "mock"],
 | 
			
		||||
    test_suite="nose.collector",
 | 
			
		||||
    classifiers=[
 | 
			
		||||
        "Development Status :: 5 - Production/Stable",
 | 
			
		||||
        "Environment :: Console",
 | 
			
		||||
        "Intended Audience :: Developers",
 | 
			
		||||
        "Intended Audience :: Information Technology",
 | 
			
		||||
        "License :: OSI Approved :: Apache Software License",
 | 
			
		||||
        "Operating System :: OS Independent",
 | 
			
		||||
        "Programming Language :: Python"
 | 
			
		||||
    ],
 | 
			
		||||
    entry_points={
 | 
			
		||||
        "console_scripts": ["cinder = cinderclient.shell:main"]
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										0
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										71
									
								
								tests/fakes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								tests/fakes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
"""
 | 
			
		||||
A fake server that "responds" to API methods with pre-canned responses.
 | 
			
		||||
 | 
			
		||||
All of these responses come from the spec, so if for some reason the spec's
 | 
			
		||||
wrong the tests might raise AssertionError. I've indicated in comments the
 | 
			
		||||
places where actual behavior differs from the spec.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def assert_has_keys(dict, required=[], optional=[]):
 | 
			
		||||
    keys = dict.keys()
 | 
			
		||||
    for k in required:
 | 
			
		||||
        try:
 | 
			
		||||
            assert k in keys
 | 
			
		||||
        except AssertionError:
 | 
			
		||||
            extra_keys = set(keys).difference(set(required + optional))
 | 
			
		||||
            raise AssertionError("found unexpected keys: %s" %
 | 
			
		||||
                    list(extra_keys))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FakeClient(object):
 | 
			
		||||
 | 
			
		||||
    def assert_called(self, method, url, body=None, pos=-1):
 | 
			
		||||
        """
 | 
			
		||||
        Assert than an API method was just called.
 | 
			
		||||
        """
 | 
			
		||||
        expected = (method, url)
 | 
			
		||||
        called = self.client.callstack[pos][0:2]
 | 
			
		||||
 | 
			
		||||
        assert self.client.callstack, \
 | 
			
		||||
                       "Expected %s %s but no calls were made." % expected
 | 
			
		||||
 | 
			
		||||
        assert expected == called, 'Expected %s %s; got %s %s' % \
 | 
			
		||||
                                               (expected + called)
 | 
			
		||||
 | 
			
		||||
        if body is not None:
 | 
			
		||||
            assert self.client.callstack[pos][2] == body
 | 
			
		||||
 | 
			
		||||
    def assert_called_anytime(self, method, url, body=None):
 | 
			
		||||
        """
 | 
			
		||||
        Assert than an API method was called anytime in the test.
 | 
			
		||||
        """
 | 
			
		||||
        expected = (method, url)
 | 
			
		||||
 | 
			
		||||
        assert self.client.callstack, \
 | 
			
		||||
                       "Expected %s %s but no calls were made." % expected
 | 
			
		||||
 | 
			
		||||
        found = False
 | 
			
		||||
        for entry in self.client.callstack:
 | 
			
		||||
            if expected == entry[0:2]:
 | 
			
		||||
                found = True
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        assert found, 'Expected %s %s; got %s' % \
 | 
			
		||||
                              (expected, self.client.callstack)
 | 
			
		||||
        if body is not None:
 | 
			
		||||
            try:
 | 
			
		||||
                assert entry[2] == body
 | 
			
		||||
            except AssertionError:
 | 
			
		||||
                print entry[2]
 | 
			
		||||
                print "!="
 | 
			
		||||
                print body
 | 
			
		||||
                raise
 | 
			
		||||
 | 
			
		||||
        self.client.callstack = []
 | 
			
		||||
 | 
			
		||||
    def clear_callstack(self):
 | 
			
		||||
        self.client.callstack = []
 | 
			
		||||
 | 
			
		||||
    def authenticate(self):
 | 
			
		||||
        pass
 | 
			
		||||
							
								
								
									
										48
									
								
								tests/test_base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								tests/test_base.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
from cinderclient import base
 | 
			
		||||
from cinderclient import exceptions
 | 
			
		||||
from cinderclient.v1 import volumes
 | 
			
		||||
from tests import utils
 | 
			
		||||
from tests.v1 import fakes
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
cs = fakes.FakeClient()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseTest(utils.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_resource_repr(self):
 | 
			
		||||
        r = base.Resource(None, dict(foo="bar", baz="spam"))
 | 
			
		||||
        self.assertEqual(repr(r), "<Resource baz=spam, foo=bar>")
 | 
			
		||||
 | 
			
		||||
    def test_getid(self):
 | 
			
		||||
        self.assertEqual(base.getid(4), 4)
 | 
			
		||||
 | 
			
		||||
        class TmpObject(object):
 | 
			
		||||
            id = 4
 | 
			
		||||
        self.assertEqual(base.getid(TmpObject), 4)
 | 
			
		||||
 | 
			
		||||
    def test_eq(self):
 | 
			
		||||
        # Two resources of the same type with the same id: equal
 | 
			
		||||
        r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
 | 
			
		||||
        r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
 | 
			
		||||
        self.assertEqual(r1, r2)
 | 
			
		||||
 | 
			
		||||
        # Two resoruces of different types: never equal
 | 
			
		||||
        r1 = base.Resource(None, {'id': 1})
 | 
			
		||||
        r2 = volumes.Volume(None, {'id': 1})
 | 
			
		||||
        self.assertNotEqual(r1, r2)
 | 
			
		||||
 | 
			
		||||
        # Two resources with no ID: equal if their info is equal
 | 
			
		||||
        r1 = base.Resource(None, {'name': 'joe', 'age': 12})
 | 
			
		||||
        r2 = base.Resource(None, {'name': 'joe', 'age': 12})
 | 
			
		||||
        self.assertEqual(r1, r2)
 | 
			
		||||
 | 
			
		||||
    def test_findall_invalid_attribute(self):
 | 
			
		||||
        # Make sure findall with an invalid attribute doesn't cause errors.
 | 
			
		||||
        # The following should not raise an exception.
 | 
			
		||||
        cs.volumes.findall(vegetable='carrot')
 | 
			
		||||
 | 
			
		||||
        # However, find() should raise an error
 | 
			
		||||
        self.assertRaises(exceptions.NotFound,
 | 
			
		||||
                          cs.volumes.find,
 | 
			
		||||
                          vegetable='carrot')
 | 
			
		||||
							
								
								
									
										18
									
								
								tests/test_client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								tests/test_client.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
 | 
			
		||||
import cinderclient.client
 | 
			
		||||
import cinderclient.v1.client
 | 
			
		||||
from tests import utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientTest(utils.TestCase):
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_get_client_class_v1(self):
 | 
			
		||||
        output = cinderclient.client.get_client_class('1')
 | 
			
		||||
        self.assertEqual(output, cinderclient.v1.client.Client)
 | 
			
		||||
 | 
			
		||||
    def test_get_client_class_unknown(self):
 | 
			
		||||
        self.assertRaises(cinderclient.exceptions.UnsupportedVersion,
 | 
			
		||||
                          cinderclient.client.get_client_class, '0')
 | 
			
		||||
							
								
								
									
										74
									
								
								tests/test_http.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								tests/test_http.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
import httplib2
 | 
			
		||||
import mock
 | 
			
		||||
 | 
			
		||||
from cinderclient import client
 | 
			
		||||
from cinderclient import exceptions
 | 
			
		||||
from tests import utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
fake_response = httplib2.Response({"status": 200})
 | 
			
		||||
fake_body = '{"hi": "there"}'
 | 
			
		||||
mock_request = mock.Mock(return_value=(fake_response, fake_body))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_client():
 | 
			
		||||
    cl = client.HTTPClient("username", "password",
 | 
			
		||||
                           "project_id", "auth_test")
 | 
			
		||||
    return cl
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_authed_client():
 | 
			
		||||
    cl = get_client()
 | 
			
		||||
    cl.management_url = "http://example.com"
 | 
			
		||||
    cl.auth_token = "token"
 | 
			
		||||
    return cl
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientTest(utils.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_get(self):
 | 
			
		||||
        cl = get_authed_client()
 | 
			
		||||
 | 
			
		||||
        @mock.patch.object(httplib2.Http, "request", mock_request)
 | 
			
		||||
        @mock.patch('time.time', mock.Mock(return_value=1234))
 | 
			
		||||
        def test_get_call():
 | 
			
		||||
            resp, body = cl.get("/hi")
 | 
			
		||||
            headers = {"X-Auth-Token": "token",
 | 
			
		||||
                       "X-Auth-Project-Id": "project_id",
 | 
			
		||||
                       "User-Agent": cl.USER_AGENT,
 | 
			
		||||
                       'Accept': 'application/json',
 | 
			
		||||
            }
 | 
			
		||||
            mock_request.assert_called_with("http://example.com/hi",
 | 
			
		||||
                                            "GET", headers=headers)
 | 
			
		||||
            # Automatic JSON parsing
 | 
			
		||||
            self.assertEqual(body, {"hi": "there"})
 | 
			
		||||
 | 
			
		||||
        test_get_call()
 | 
			
		||||
 | 
			
		||||
    def test_post(self):
 | 
			
		||||
        cl = get_authed_client()
 | 
			
		||||
 | 
			
		||||
        @mock.patch.object(httplib2.Http, "request", mock_request)
 | 
			
		||||
        def test_post_call():
 | 
			
		||||
            cl.post("/hi", body=[1, 2, 3])
 | 
			
		||||
            headers = {
 | 
			
		||||
                "X-Auth-Token": "token",
 | 
			
		||||
                "X-Auth-Project-Id": "project_id",
 | 
			
		||||
                "Content-Type": "application/json",
 | 
			
		||||
                'Accept': 'application/json',
 | 
			
		||||
                "User-Agent": cl.USER_AGENT
 | 
			
		||||
            }
 | 
			
		||||
            mock_request.assert_called_with("http://example.com/hi", "POST",
 | 
			
		||||
                                            headers=headers, body='[1, 2, 3]')
 | 
			
		||||
 | 
			
		||||
        test_post_call()
 | 
			
		||||
 | 
			
		||||
    def test_auth_failure(self):
 | 
			
		||||
        cl = get_client()
 | 
			
		||||
 | 
			
		||||
        # response must not have x-server-management-url header
 | 
			
		||||
        @mock.patch.object(httplib2.Http, "request", mock_request)
 | 
			
		||||
        def test_auth_call():
 | 
			
		||||
            self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate)
 | 
			
		||||
 | 
			
		||||
        test_auth_call()
 | 
			
		||||
							
								
								
									
										127
									
								
								tests/test_service_catalog.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								tests/test_service_catalog.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
			
		||||
from cinderclient import exceptions
 | 
			
		||||
from cinderclient import service_catalog
 | 
			
		||||
from tests import utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Taken directly from keystone/content/common/samples/auth.json
 | 
			
		||||
# Do not edit this structure. Instead, grab the latest from there.
 | 
			
		||||
 | 
			
		||||
SERVICE_CATALOG = {
 | 
			
		||||
    "access": {
 | 
			
		||||
        "token": {
 | 
			
		||||
            "id": "ab48a9efdfedb23ty3494",
 | 
			
		||||
            "expires": "2010-11-01T03:32:15-05:00",
 | 
			
		||||
            "tenant": {
 | 
			
		||||
                "id": "345",
 | 
			
		||||
                "name": "My Project"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "user": {
 | 
			
		||||
            "id": "123",
 | 
			
		||||
            "name": "jqsmith",
 | 
			
		||||
            "roles": [
 | 
			
		||||
                {
 | 
			
		||||
                    "id": "234",
 | 
			
		||||
                    "name": "compute:admin",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "id": "235",
 | 
			
		||||
                    "name": "object-store:admin",
 | 
			
		||||
                    "tenantId": "1",
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "roles_links": [],
 | 
			
		||||
        },
 | 
			
		||||
        "serviceCatalog": [
 | 
			
		||||
            {
 | 
			
		||||
                "name": "Cloud Servers",
 | 
			
		||||
                "type": "compute",
 | 
			
		||||
                "endpoints": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "tenantId": "1",
 | 
			
		||||
                        "publicURL": "https://compute1.host/v1/1234",
 | 
			
		||||
                        "internalURL": "https://compute1.host/v1/1234",
 | 
			
		||||
                        "region": "North",
 | 
			
		||||
                        "versionId": "1.0",
 | 
			
		||||
                        "versionInfo": "https://compute1.host/v1/",
 | 
			
		||||
                        "versionList": "https://compute1.host/"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "tenantId": "2",
 | 
			
		||||
                        "publicURL": "https://compute1.host/v1/3456",
 | 
			
		||||
                        "internalURL": "https://compute1.host/v1/3456",
 | 
			
		||||
                        "region": "North",
 | 
			
		||||
                        "versionId": "1.1",
 | 
			
		||||
                        "versionInfo": "https://compute1.host/v1/",
 | 
			
		||||
                        "versionList": "https://compute1.host/"
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
                "endpoints_links": [],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "name": "Nova Volumes",
 | 
			
		||||
                "type": "volume",
 | 
			
		||||
                "endpoints": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "tenantId": "1",
 | 
			
		||||
                        "publicURL": "https://volume1.host/v1/1234",
 | 
			
		||||
                        "internalURL": "https://volume1.host/v1/1234",
 | 
			
		||||
                        "region": "South",
 | 
			
		||||
                        "versionId": "1.0",
 | 
			
		||||
                        "versionInfo": "uri",
 | 
			
		||||
                        "versionList": "uri"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "tenantId": "2",
 | 
			
		||||
                        "publicURL": "https://volume1.host/v1/3456",
 | 
			
		||||
                        "internalURL": "https://volume1.host/v1/3456",
 | 
			
		||||
                        "region": "South",
 | 
			
		||||
                        "versionId": "1.1",
 | 
			
		||||
                        "versionInfo": "https://volume1.host/v1/",
 | 
			
		||||
                        "versionList": "https://volume1.host/"
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
                "endpoints_links": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "rel": "next",
 | 
			
		||||
                        "href": "https://identity1.host/v2.0/endpoints"
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
        "serviceCatalog_links": [
 | 
			
		||||
            {
 | 
			
		||||
                "rel": "next",
 | 
			
		||||
                "href": "https://identity.host/v2.0/endpoints?session=2hfh8Ar",
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ServiceCatalogTest(utils.TestCase):
 | 
			
		||||
    def test_building_a_service_catalog(self):
 | 
			
		||||
        sc = service_catalog.ServiceCatalog(SERVICE_CATALOG)
 | 
			
		||||
 | 
			
		||||
        self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for,
 | 
			
		||||
                          service_type='compute')
 | 
			
		||||
        self.assertEquals(sc.url_for('tenantId', '1', service_type='compute'),
 | 
			
		||||
                            "https://compute1.host/v1/1234")
 | 
			
		||||
        self.assertEquals(sc.url_for('tenantId', '2', service_type='compute'),
 | 
			
		||||
                            "https://compute1.host/v1/3456")
 | 
			
		||||
 | 
			
		||||
        self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
 | 
			
		||||
                          "region", "South", service_type='compute')
 | 
			
		||||
 | 
			
		||||
    def test_alternate_service_type(self):
 | 
			
		||||
        sc = service_catalog.ServiceCatalog(SERVICE_CATALOG)
 | 
			
		||||
 | 
			
		||||
        self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for,
 | 
			
		||||
                          service_type='volume')
 | 
			
		||||
        self.assertEquals(sc.url_for('tenantId', '1', service_type='volume'),
 | 
			
		||||
                            "https://volume1.host/v1/1234")
 | 
			
		||||
        self.assertEquals(sc.url_for('tenantId', '2', service_type='volume'),
 | 
			
		||||
                            "https://volume1.host/v1/3456")
 | 
			
		||||
 | 
			
		||||
        self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
 | 
			
		||||
                          "region", "North", service_type='volume')
 | 
			
		||||
							
								
								
									
										75
									
								
								tests/test_shell.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								tests/test_shell.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
import cStringIO
 | 
			
		||||
import os
 | 
			
		||||
import httplib2
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
from cinderclient import exceptions
 | 
			
		||||
import cinderclient.shell
 | 
			
		||||
from tests import utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShellTest(utils.TestCase):
 | 
			
		||||
 | 
			
		||||
    # Patch os.environ to avoid required auth info.
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        global _old_env
 | 
			
		||||
        fake_env = {
 | 
			
		||||
            'OS_USERNAME': 'username',
 | 
			
		||||
            'OS_PASSWORD': 'password',
 | 
			
		||||
            'OS_TENANT_NAME': 'tenant_name',
 | 
			
		||||
            'OS_AUTH_URL': 'http://no.where',
 | 
			
		||||
        }
 | 
			
		||||
        _old_env, os.environ = os.environ, fake_env.copy()
 | 
			
		||||
 | 
			
		||||
    def shell(self, argstr):
 | 
			
		||||
        orig = sys.stdout
 | 
			
		||||
        try:
 | 
			
		||||
            sys.stdout = cStringIO.StringIO()
 | 
			
		||||
            _shell = cinderclient.shell.OpenStackCinderShell()
 | 
			
		||||
            _shell.main(argstr.split())
 | 
			
		||||
        except SystemExit:
 | 
			
		||||
            exc_type, exc_value, exc_traceback = sys.exc_info()
 | 
			
		||||
            self.assertEqual(exc_value.code, 0)
 | 
			
		||||
        finally:
 | 
			
		||||
            out = sys.stdout.getvalue()
 | 
			
		||||
            sys.stdout.close()
 | 
			
		||||
            sys.stdout = orig
 | 
			
		||||
 | 
			
		||||
        return out
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        global _old_env
 | 
			
		||||
        os.environ = _old_env
 | 
			
		||||
 | 
			
		||||
    def test_help_unknown_command(self):
 | 
			
		||||
        self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo')
 | 
			
		||||
 | 
			
		||||
    def test_debug(self):
 | 
			
		||||
        httplib2.debuglevel = 0
 | 
			
		||||
        self.shell('--debug help')
 | 
			
		||||
        assert httplib2.debuglevel == 1
 | 
			
		||||
 | 
			
		||||
    def test_help(self):
 | 
			
		||||
        required = [
 | 
			
		||||
            '^usage: ',
 | 
			
		||||
            '(?m)^\s+create\s+Add a new volume.',
 | 
			
		||||
            '(?m)^See "cinder help COMMAND" for help on a specific command',
 | 
			
		||||
        ]
 | 
			
		||||
        for argstr in ['--help', 'help']:
 | 
			
		||||
            help_text = self.shell(argstr)
 | 
			
		||||
            for r in required:
 | 
			
		||||
                self.assertRegexpMatches(help_text, r)
 | 
			
		||||
 | 
			
		||||
    def test_help_on_subcommand(self):
 | 
			
		||||
        required = [
 | 
			
		||||
            '^usage: cinder list',
 | 
			
		||||
            '(?m)^List all the volumes.',
 | 
			
		||||
        ]
 | 
			
		||||
        argstrings = [
 | 
			
		||||
            'list --help',
 | 
			
		||||
            'help list',
 | 
			
		||||
        ]
 | 
			
		||||
        for argstr in argstrings:
 | 
			
		||||
            help_text = self.shell(argstr)
 | 
			
		||||
            for r in required:
 | 
			
		||||
                self.assertRegexpMatches(help_text, r)
 | 
			
		||||
							
								
								
									
										74
									
								
								tests/test_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								tests/test_utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
 | 
			
		||||
from cinderclient import exceptions
 | 
			
		||||
from cinderclient import utils
 | 
			
		||||
from cinderclient import base
 | 
			
		||||
from tests import utils as test_utils
 | 
			
		||||
 | 
			
		||||
UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FakeResource(object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, _id, properties):
 | 
			
		||||
        self.id = _id
 | 
			
		||||
        try:
 | 
			
		||||
            self.name = properties['name']
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            pass
 | 
			
		||||
        try:
 | 
			
		||||
            self.display_name = properties['display_name']
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FakeManager(base.ManagerWithFind):
 | 
			
		||||
 | 
			
		||||
    resource_class = FakeResource
 | 
			
		||||
 | 
			
		||||
    resources = [
 | 
			
		||||
        FakeResource('1234', {'name': 'entity_one'}),
 | 
			
		||||
        FakeResource(UUID, {'name': 'entity_two'}),
 | 
			
		||||
        FakeResource('4242', {'display_name': 'entity_three'}),
 | 
			
		||||
        FakeResource('5678', {'name': '9876'})
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def get(self, resource_id):
 | 
			
		||||
        for resource in self.resources:
 | 
			
		||||
            if resource.id == str(resource_id):
 | 
			
		||||
                return resource
 | 
			
		||||
        raise exceptions.NotFound(resource_id)
 | 
			
		||||
 | 
			
		||||
    def list(self):
 | 
			
		||||
        return self.resources
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FindResourceTestCase(test_utils.TestCase):
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.manager = FakeManager(None)
 | 
			
		||||
 | 
			
		||||
    def test_find_none(self):
 | 
			
		||||
        self.assertRaises(exceptions.CommandError,
 | 
			
		||||
                          utils.find_resource,
 | 
			
		||||
                          self.manager,
 | 
			
		||||
                          'asdf')
 | 
			
		||||
 | 
			
		||||
    def test_find_by_integer_id(self):
 | 
			
		||||
        output = utils.find_resource(self.manager, 1234)
 | 
			
		||||
        self.assertEqual(output, self.manager.get('1234'))
 | 
			
		||||
 | 
			
		||||
    def test_find_by_str_id(self):
 | 
			
		||||
        output = utils.find_resource(self.manager, '1234')
 | 
			
		||||
        self.assertEqual(output, self.manager.get('1234'))
 | 
			
		||||
 | 
			
		||||
    def test_find_by_uuid(self):
 | 
			
		||||
        output = utils.find_resource(self.manager, UUID)
 | 
			
		||||
        self.assertEqual(output, self.manager.get(UUID))
 | 
			
		||||
 | 
			
		||||
    def test_find_by_str_name(self):
 | 
			
		||||
        output = utils.find_resource(self.manager, 'entity_one')
 | 
			
		||||
        self.assertEqual(output, self.manager.get('1234'))
 | 
			
		||||
 | 
			
		||||
    def test_find_by_str_displayname(self):
 | 
			
		||||
        output = utils.find_resource(self.manager, 'entity_three')
 | 
			
		||||
        self.assertEqual(output, self.manager.get('4242'))
 | 
			
		||||
							
								
								
									
										5
									
								
								tests/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								tests/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
import unittest2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCase(unittest2.TestCase):
 | 
			
		||||
    pass
 | 
			
		||||
							
								
								
									
										0
									
								
								tests/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										765
									
								
								tests/v1/fakes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										765
									
								
								tests/v1/fakes.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,765 @@
 | 
			
		||||
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
 | 
			
		||||
# Copyright 2011 OpenStack, LLC
 | 
			
		||||
#
 | 
			
		||||
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
# you may not use this file except in compliance with the License.
 | 
			
		||||
# You may obtain a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
# http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
# Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
# See the License for the specific language governing permissions and
 | 
			
		||||
# limitations under the License.
 | 
			
		||||
 | 
			
		||||
import httplib2
 | 
			
		||||
import urlparse
 | 
			
		||||
 | 
			
		||||
from cinderclient import client as base_client
 | 
			
		||||
from cinderclient.v1 import client
 | 
			
		||||
from tests import fakes
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FakeClient(fakes.FakeClient, client.Client):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        client.Client.__init__(self, 'username', 'password',
 | 
			
		||||
                               'project_id', 'auth_url')
 | 
			
		||||
        self.client = FakeHTTPClient(**kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FakeHTTPClient(base_client.HTTPClient):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        self.username = 'username'
 | 
			
		||||
        self.password = 'password'
 | 
			
		||||
        self.auth_url = 'auth_url'
 | 
			
		||||
        self.callstack = []
 | 
			
		||||
 | 
			
		||||
    def _cs_request(self, url, method, **kwargs):
 | 
			
		||||
        # Check that certain things are called correctly
 | 
			
		||||
        if method in ['GET', 'DELETE']:
 | 
			
		||||
            assert 'body' not in kwargs
 | 
			
		||||
        elif method == 'PUT':
 | 
			
		||||
            assert 'body' in kwargs
 | 
			
		||||
 | 
			
		||||
        # Call the method
 | 
			
		||||
        args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
 | 
			
		||||
        kwargs.update(args)
 | 
			
		||||
        munged_url = url.rsplit('?', 1)[0]
 | 
			
		||||
        munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
 | 
			
		||||
        munged_url = munged_url.replace('-', '_')
 | 
			
		||||
 | 
			
		||||
        callback = "%s_%s" % (method.lower(), munged_url)
 | 
			
		||||
 | 
			
		||||
        if not hasattr(self, callback):
 | 
			
		||||
            raise AssertionError('Called unknown API method: %s %s, '
 | 
			
		||||
                                 'expected fakes method name: %s' %
 | 
			
		||||
                                 (method, url, callback))
 | 
			
		||||
 | 
			
		||||
        # Note the call
 | 
			
		||||
        self.callstack.append((method, url, kwargs.get('body', None)))
 | 
			
		||||
 | 
			
		||||
        status, body = getattr(self, callback)(**kwargs)
 | 
			
		||||
        if hasattr(status, 'items'):
 | 
			
		||||
            return httplib2.Response(status), body
 | 
			
		||||
        else:
 | 
			
		||||
            return httplib2.Response({"status": status}), body
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Limits
 | 
			
		||||
    #
 | 
			
		||||
 | 
			
		||||
    def get_limits(self, **kw):
 | 
			
		||||
        return (200, {"limits": {
 | 
			
		||||
            "rate": [
 | 
			
		||||
                {
 | 
			
		||||
                    "uri": "*",
 | 
			
		||||
                    "regex": ".*",
 | 
			
		||||
                    "limit": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "value": 10,
 | 
			
		||||
                            "verb": "POST",
 | 
			
		||||
                            "remaining": 2,
 | 
			
		||||
                            "unit": "MINUTE",
 | 
			
		||||
                            "next-available": "2011-12-15T22:42:45Z"
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            "value": 10,
 | 
			
		||||
                            "verb": "PUT",
 | 
			
		||||
                            "remaining": 2,
 | 
			
		||||
                            "unit": "MINUTE",
 | 
			
		||||
                            "next-available": "2011-12-15T22:42:45Z"
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            "value": 100,
 | 
			
		||||
                            "verb": "DELETE",
 | 
			
		||||
                            "remaining": 100,
 | 
			
		||||
                            "unit": "MINUTE",
 | 
			
		||||
                            "next-available": "2011-12-15T22:42:45Z"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "uri": "*/servers",
 | 
			
		||||
                    "regex": "^/servers",
 | 
			
		||||
                    "limit": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "verb": "POST",
 | 
			
		||||
                            "value": 25,
 | 
			
		||||
                            "remaining": 24,
 | 
			
		||||
                            "unit": "DAY",
 | 
			
		||||
                            "next-available": "2011-12-15T22:42:45Z"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "absolute": {
 | 
			
		||||
                "maxTotalRAMSize": 51200,
 | 
			
		||||
                "maxServerMeta": 5,
 | 
			
		||||
                "maxImageMeta": 5,
 | 
			
		||||
                "maxPersonality": 5,
 | 
			
		||||
                "maxPersonalitySize": 10240
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Servers
 | 
			
		||||
    #
 | 
			
		||||
 | 
			
		||||
    def get_volumes(self, **kw):
 | 
			
		||||
        return (200, {"volumes": [
 | 
			
		||||
            {'id': 1234, 'name': 'sample-volume'},
 | 
			
		||||
            {'id': 5678, 'name': 'sample-volume2'}
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    # TODO(jdg): This will need to change
 | 
			
		||||
    # at the very least it's not complete
 | 
			
		||||
    def get_volumes_detail(self, **kw):
 | 
			
		||||
        return (200, {"volumes": [
 | 
			
		||||
            {'id': 1234,
 | 
			
		||||
             'name': 'sample-volume',
 | 
			
		||||
             'attachments': [{'server_id': 1234}]
 | 
			
		||||
            },
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    def get_volumes_1234(self, **kw):
 | 
			
		||||
        r = {'volume': self.get_volumes_detail()[1]['volumes'][0]}
 | 
			
		||||
        return (200, r)
 | 
			
		||||
 | 
			
		||||
    def post_servers(self, body, **kw):
 | 
			
		||||
        assert set(body.keys()) <= set(['server', 'os:scheduler_hints'])
 | 
			
		||||
        fakes.assert_has_keys(body['server'],
 | 
			
		||||
                        required=['name', 'imageRef', 'flavorRef'],
 | 
			
		||||
                        optional=['metadata', 'personality'])
 | 
			
		||||
        if 'personality' in body['server']:
 | 
			
		||||
            for pfile in body['server']['personality']:
 | 
			
		||||
                fakes.assert_has_keys(pfile, required=['path', 'contents'])
 | 
			
		||||
        return (202, self.get_servers_1234()[1])
 | 
			
		||||
 | 
			
		||||
    def get_servers_1234(self, **kw):
 | 
			
		||||
        r = {'server': self.get_servers_detail()[1]['servers'][0]}
 | 
			
		||||
        return (200, r)
 | 
			
		||||
 | 
			
		||||
    def get_servers_5678(self, **kw):
 | 
			
		||||
        r = {'server': self.get_servers_detail()[1]['servers'][1]}
 | 
			
		||||
        return (200, r)
 | 
			
		||||
 | 
			
		||||
    def put_servers_1234(self, body, **kw):
 | 
			
		||||
        assert body.keys() == ['server']
 | 
			
		||||
        fakes.assert_has_keys(body['server'], optional=['name', 'adminPass'])
 | 
			
		||||
        return (204, None)
 | 
			
		||||
 | 
			
		||||
    def delete_servers_1234(self, **kw):
 | 
			
		||||
        return (202, None)
 | 
			
		||||
 | 
			
		||||
    def delete_volumes_1234(self, **kw):
 | 
			
		||||
        return (202, None)
 | 
			
		||||
 | 
			
		||||
    def delete_servers_1234_metadata_test_key(self, **kw):
 | 
			
		||||
        return (204, None)
 | 
			
		||||
 | 
			
		||||
    def delete_servers_1234_metadata_key1(self, **kw):
 | 
			
		||||
        return (204, None)
 | 
			
		||||
 | 
			
		||||
    def delete_servers_1234_metadata_key2(self, **kw):
 | 
			
		||||
        return (204, None)
 | 
			
		||||
 | 
			
		||||
    def post_servers_1234_metadata(self, **kw):
 | 
			
		||||
        return (204, {'metadata': {'test_key': 'test_value'}})
 | 
			
		||||
 | 
			
		||||
    def get_servers_1234_diagnostics(self, **kw):
 | 
			
		||||
        return (200, {'data': 'Fake diagnostics'})
 | 
			
		||||
 | 
			
		||||
    def get_servers_1234_actions(self, **kw):
 | 
			
		||||
        return (200, {'actions': [
 | 
			
		||||
            {
 | 
			
		||||
                'action': 'rebuild',
 | 
			
		||||
                'error': None,
 | 
			
		||||
                'created_at': '2011-12-30 11:45:36'
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                'action': 'reboot',
 | 
			
		||||
                'error': 'Failed!',
 | 
			
		||||
                'created_at': '2011-12-30 11:40:29'
 | 
			
		||||
            },
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Server Addresses
 | 
			
		||||
    #
 | 
			
		||||
 | 
			
		||||
    def get_servers_1234_ips(self, **kw):
 | 
			
		||||
        return (200, {'addresses':
 | 
			
		||||
                      self.get_servers_1234()[1]['server']['addresses']})
 | 
			
		||||
 | 
			
		||||
    def get_servers_1234_ips_public(self, **kw):
 | 
			
		||||
        return (200, {'public':
 | 
			
		||||
                      self.get_servers_1234_ips()[1]['addresses']['public']})
 | 
			
		||||
 | 
			
		||||
    def get_servers_1234_ips_private(self, **kw):
 | 
			
		||||
        return (200, {'private':
 | 
			
		||||
                      self.get_servers_1234_ips()[1]['addresses']['private']})
 | 
			
		||||
 | 
			
		||||
    def delete_servers_1234_ips_public_1_2_3_4(self, **kw):
 | 
			
		||||
        return (202, None)
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Server actions
 | 
			
		||||
    #
 | 
			
		||||
 | 
			
		||||
    def post_servers_1234_action(self, body, **kw):
 | 
			
		||||
        _body = None
 | 
			
		||||
        resp = 202
 | 
			
		||||
        assert len(body.keys()) == 1
 | 
			
		||||
        action = body.keys()[0]
 | 
			
		||||
        if action == 'reboot':
 | 
			
		||||
            assert body[action].keys() == ['type']
 | 
			
		||||
            assert body[action]['type'] in ['HARD', 'SOFT']
 | 
			
		||||
        elif action == 'rebuild':
 | 
			
		||||
            keys = body[action].keys()
 | 
			
		||||
            if 'adminPass' in keys:
 | 
			
		||||
                keys.remove('adminPass')
 | 
			
		||||
            assert keys == ['imageRef']
 | 
			
		||||
            _body = self.get_servers_1234()[1]
 | 
			
		||||
        elif action == 'resize':
 | 
			
		||||
            assert body[action].keys() == ['flavorRef']
 | 
			
		||||
        elif action == 'confirmResize':
 | 
			
		||||
            assert body[action] is None
 | 
			
		||||
            # This one method returns a different response code
 | 
			
		||||
            return (204, None)
 | 
			
		||||
        elif action == 'revertResize':
 | 
			
		||||
            assert body[action] is None
 | 
			
		||||
        elif action == 'migrate':
 | 
			
		||||
            assert body[action] is None
 | 
			
		||||
        elif action == 'rescue':
 | 
			
		||||
            assert body[action] is None
 | 
			
		||||
        elif action == 'unrescue':
 | 
			
		||||
            assert body[action] is None
 | 
			
		||||
        elif action == 'lock':
 | 
			
		||||
            assert body[action] is None
 | 
			
		||||
        elif action == 'unlock':
 | 
			
		||||
            assert body[action] is None
 | 
			
		||||
        elif action == 'addFixedIp':
 | 
			
		||||
            assert body[action].keys() == ['networkId']
 | 
			
		||||
        elif action == 'removeFixedIp':
 | 
			
		||||
            assert body[action].keys() == ['address']
 | 
			
		||||
        elif action == 'addFloatingIp':
 | 
			
		||||
            assert body[action].keys() == ['address']
 | 
			
		||||
        elif action == 'removeFloatingIp':
 | 
			
		||||
            assert body[action].keys() == ['address']
 | 
			
		||||
        elif action == 'createImage':
 | 
			
		||||
            assert set(body[action].keys()) == set(['name', 'metadata'])
 | 
			
		||||
            resp = dict(status=202, location="http://blah/images/456")
 | 
			
		||||
        elif action == 'changePassword':
 | 
			
		||||
            assert body[action].keys() == ['adminPass']
 | 
			
		||||
        elif action == 'os-getConsoleOutput':
 | 
			
		||||
            assert body[action].keys() == ['length']
 | 
			
		||||
            return (202, {'output': 'foo'})
 | 
			
		||||
        elif action == 'os-getVNCConsole':
 | 
			
		||||
            assert body[action].keys() == ['type']
 | 
			
		||||
        elif action == 'os-migrateLive':
 | 
			
		||||
            assert set(body[action].keys()) == set(['host',
 | 
			
		||||
                                                    'block_migration',
 | 
			
		||||
                                                    'disk_over_commit'])
 | 
			
		||||
        else:
 | 
			
		||||
            raise AssertionError("Unexpected server action: %s" % action)
 | 
			
		||||
        return (resp, _body)
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Cloudpipe
 | 
			
		||||
    #
 | 
			
		||||
 | 
			
		||||
    def get_os_cloudpipe(self, **kw):
 | 
			
		||||
        return (200, {'cloudpipes': [
 | 
			
		||||
            {'project_id':1}
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    def post_os_cloudpipe(self, **ks):
 | 
			
		||||
        return (202, {'instance_id': '9d5824aa-20e6-4b9f-b967-76a699fc51fd'})
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Flavors
 | 
			
		||||
    #
 | 
			
		||||
 | 
			
		||||
    def get_flavors(self, **kw):
 | 
			
		||||
        return (200, {'flavors': [
 | 
			
		||||
            {'id': 1, 'name': '256 MB Server'},
 | 
			
		||||
            {'id': 2, 'name': '512 MB Server'}
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    def get_flavors_detail(self, **kw):
 | 
			
		||||
        return (200, {'flavors': [
 | 
			
		||||
            {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10,
 | 
			
		||||
             'OS-FLV-EXT-DATA:ephemeral': 10},
 | 
			
		||||
            {'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20,
 | 
			
		||||
             'OS-FLV-EXT-DATA:ephemeral': 20}
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    def get_flavors_1(self, **kw):
 | 
			
		||||
        return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][0]})
 | 
			
		||||
 | 
			
		||||
    def get_flavors_2(self, **kw):
 | 
			
		||||
        return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][1]})
 | 
			
		||||
 | 
			
		||||
    def get_flavors_3(self, **kw):
 | 
			
		||||
        # Diablo has no ephemeral
 | 
			
		||||
        return (200, {'flavor': {'id': 3, 'name': '256 MB Server',
 | 
			
		||||
                                 'ram': 256, 'disk': 10}})
 | 
			
		||||
 | 
			
		||||
    def delete_flavors_flavordelete(self, **kw):
 | 
			
		||||
        return (202, None)
 | 
			
		||||
 | 
			
		||||
    def post_flavors(self, body, **kw):
 | 
			
		||||
        return (202, {'flavor': self.get_flavors_detail()[1]['flavors'][0]})
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Floating ips
 | 
			
		||||
    #
 | 
			
		||||
 | 
			
		||||
    def get_os_floating_ip_pools(self):
 | 
			
		||||
        return (200, {'floating_ip_pools': [{'name': 'foo', 'name': 'bar'}]})
 | 
			
		||||
 | 
			
		||||
    def get_os_floating_ips(self, **kw):
 | 
			
		||||
        return (200, {'floating_ips': [
 | 
			
		||||
            {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'},
 | 
			
		||||
            {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'},
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    def get_os_floating_ips_1(self, **kw):
 | 
			
		||||
        return (200, {'floating_ip':
 | 
			
		||||
            {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    def post_os_floating_ips(self, body, **kw):
 | 
			
		||||
        return (202, self.get_os_floating_ips_1()[1])
 | 
			
		||||
 | 
			
		||||
    def post_os_floating_ips(self, body):
 | 
			
		||||
        if body.get('pool'):
 | 
			
		||||
            return (200, {'floating_ip':
 | 
			
		||||
                {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1',
 | 
			
		||||
                                                            'pool': 'cinder'}})
 | 
			
		||||
        else:
 | 
			
		||||
            return (200, {'floating_ip':
 | 
			
		||||
                {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1',
 | 
			
		||||
                                                            'pool': None}})
 | 
			
		||||
 | 
			
		||||
    def delete_os_floating_ips_1(self, **kw):
 | 
			
		||||
        return (204, None)
 | 
			
		||||
 | 
			
		||||
    def get_os_floating_ip_dns(self, **kw):
 | 
			
		||||
        return (205, {'domain_entries':
 | 
			
		||||
                      [{'domain': 'example.org'},
 | 
			
		||||
                       {'domain': 'example.com'}]})
 | 
			
		||||
 | 
			
		||||
    def get_os_floating_ip_dns_testdomain_entries(self, **kw):
 | 
			
		||||
        if kw.get('ip'):
 | 
			
		||||
            return (205, {'dns_entries':
 | 
			
		||||
                          [{'dns_entry':
 | 
			
		||||
                             {'ip': kw.get('ip'),
 | 
			
		||||
                              'name': "host1",
 | 
			
		||||
                              'type': "A",
 | 
			
		||||
                              'domain': 'testdomain'}},
 | 
			
		||||
                           {'dns_entry':
 | 
			
		||||
                             {'ip': kw.get('ip'),
 | 
			
		||||
                              'name': "host2",
 | 
			
		||||
                              'type': "A",
 | 
			
		||||
                              'domain': 'testdomain'}}]})
 | 
			
		||||
        else:
 | 
			
		||||
            return (404, None)
 | 
			
		||||
 | 
			
		||||
    def get_os_floating_ip_dns_testdomain_entries_testname(self, **kw):
 | 
			
		||||
        return (205, {'dns_entry':
 | 
			
		||||
                        {'ip': "10.10.10.10",
 | 
			
		||||
                         'name': 'testname',
 | 
			
		||||
                         'type': "A",
 | 
			
		||||
                         'domain': 'testdomain'}})
 | 
			
		||||
 | 
			
		||||
    def put_os_floating_ip_dns_testdomain(self, body, **kw):
 | 
			
		||||
        if body['domain_entry']['scope'] == 'private':
 | 
			
		||||
            fakes.assert_has_keys(body['domain_entry'],
 | 
			
		||||
                            required=['availability_zone', 'scope'])
 | 
			
		||||
        elif body['domain_entry']['scope'] == 'public':
 | 
			
		||||
            fakes.assert_has_keys(body['domain_entry'],
 | 
			
		||||
                            required=['project', 'scope'])
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            fakes.assert_has_keys(body['domain_entry'],
 | 
			
		||||
                            required=['project', 'scope'])
 | 
			
		||||
        return (205, None)
 | 
			
		||||
 | 
			
		||||
    def put_os_floating_ip_dns_testdomain_entries_testname(self, body, **kw):
 | 
			
		||||
        fakes.assert_has_keys(body['dns_entry'],
 | 
			
		||||
                        required=['ip', 'dns_type'])
 | 
			
		||||
        return (205, None)
 | 
			
		||||
 | 
			
		||||
    def delete_os_floating_ip_dns_testdomain(self, **kw):
 | 
			
		||||
        return (200, None)
 | 
			
		||||
 | 
			
		||||
    def delete_os_floating_ip_dns_testdomain_entries_testname(self, **kw):
 | 
			
		||||
        return (200, None)
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Images
 | 
			
		||||
    #
 | 
			
		||||
    def get_images(self, **kw):
 | 
			
		||||
        return (200, {'images': [
 | 
			
		||||
            {'id': 1, 'name': 'CentOS 5.2'},
 | 
			
		||||
            {'id': 2, 'name': 'My Server Backup'}
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    def get_images_detail(self, **kw):
 | 
			
		||||
        return (200, {'images': [
 | 
			
		||||
            {
 | 
			
		||||
                'id': 1,
 | 
			
		||||
                'name': 'CentOS 5.2',
 | 
			
		||||
                "updated": "2010-10-10T12:00:00Z",
 | 
			
		||||
                "created": "2010-08-10T12:00:00Z",
 | 
			
		||||
                "status": "ACTIVE",
 | 
			
		||||
                "metadata": {
 | 
			
		||||
                    "test_key": "test_value",
 | 
			
		||||
                },
 | 
			
		||||
                "links": {},
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "id": 743,
 | 
			
		||||
                "name": "My Server Backup",
 | 
			
		||||
                "serverId": 1234,
 | 
			
		||||
                "updated": "2010-10-10T12:00:00Z",
 | 
			
		||||
                "created": "2010-08-10T12:00:00Z",
 | 
			
		||||
                "status": "SAVING",
 | 
			
		||||
                "progress": 80,
 | 
			
		||||
                "links": {},
 | 
			
		||||
            }
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    def get_images_1(self, **kw):
 | 
			
		||||
        return (200, {'image': self.get_images_detail()[1]['images'][0]})
 | 
			
		||||
 | 
			
		||||
    def get_images_2(self, **kw):
 | 
			
		||||
        return (200, {'image': self.get_images_detail()[1]['images'][1]})
 | 
			
		||||
 | 
			
		||||
    def post_images(self, body, **kw):
 | 
			
		||||
        assert body.keys() == ['image']
 | 
			
		||||
        fakes.assert_has_keys(body['image'], required=['serverId', 'name'])
 | 
			
		||||
        return (202, self.get_images_1()[1])
 | 
			
		||||
 | 
			
		||||
    def post_images_1_metadata(self, body, **kw):
 | 
			
		||||
        assert body.keys() == ['metadata']
 | 
			
		||||
        fakes.assert_has_keys(body['metadata'],
 | 
			
		||||
                              required=['test_key'])
 | 
			
		||||
        return (200,
 | 
			
		||||
            {'metadata': self.get_images_1()[1]['image']['metadata']})
 | 
			
		||||
 | 
			
		||||
    def delete_images_1(self, **kw):
 | 
			
		||||
        return (204, None)
 | 
			
		||||
 | 
			
		||||
    def delete_images_1_metadata_test_key(self, **kw):
 | 
			
		||||
        return (204, None)
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Keypairs
 | 
			
		||||
    #
 | 
			
		||||
    def get_os_keypairs(self, *kw):
 | 
			
		||||
        return (200, {"keypairs": [
 | 
			
		||||
            {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'}
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    def delete_os_keypairs_test(self, **kw):
 | 
			
		||||
        return (202, None)
 | 
			
		||||
 | 
			
		||||
    def post_os_keypairs(self, body, **kw):
 | 
			
		||||
        assert body.keys() == ['keypair']
 | 
			
		||||
        fakes.assert_has_keys(body['keypair'],
 | 
			
		||||
                              required=['name'])
 | 
			
		||||
        r = {'keypair': self.get_os_keypairs()[1]['keypairs'][0]}
 | 
			
		||||
        return (202, r)
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Virtual Interfaces
 | 
			
		||||
    #
 | 
			
		||||
    def get_servers_1234_os_virtual_interfaces(self, **kw):
 | 
			
		||||
        return (200, {"virtual_interfaces": [
 | 
			
		||||
            {'id': 'fakeid', 'mac_address': 'fakemac'}
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Quotas
 | 
			
		||||
    #
 | 
			
		||||
 | 
			
		||||
    def get_os_quota_sets_test(self, **kw):
 | 
			
		||||
        return (200, {'quota_set': {
 | 
			
		||||
                      'tenant_id': 'test',
 | 
			
		||||
                      'metadata_items': [],
 | 
			
		||||
                      'injected_file_content_bytes': 1,
 | 
			
		||||
                      'volumes': 1,
 | 
			
		||||
                      'gigabytes': 1,
 | 
			
		||||
                      'ram': 1,
 | 
			
		||||
                      'floating_ips': 1,
 | 
			
		||||
                      'instances': 1,
 | 
			
		||||
                      'injected_files': 1,
 | 
			
		||||
                      'cores': 1}})
 | 
			
		||||
 | 
			
		||||
    def get_os_quota_sets_test_defaults(self):
 | 
			
		||||
        return (200, {'quota_set': {
 | 
			
		||||
                      'tenant_id': 'test',
 | 
			
		||||
                      'metadata_items': [],
 | 
			
		||||
                      'injected_file_content_bytes': 1,
 | 
			
		||||
                      'volumes': 1,
 | 
			
		||||
                      'gigabytes': 1,
 | 
			
		||||
                      'ram': 1,
 | 
			
		||||
                      'floating_ips': 1,
 | 
			
		||||
                      'instances': 1,
 | 
			
		||||
                      'injected_files': 1,
 | 
			
		||||
                      'cores': 1}})
 | 
			
		||||
 | 
			
		||||
    def put_os_quota_sets_test(self, body, **kw):
 | 
			
		||||
        assert body.keys() == ['quota_set']
 | 
			
		||||
        fakes.assert_has_keys(body['quota_set'],
 | 
			
		||||
                              required=['tenant_id'])
 | 
			
		||||
        return (200, {'quota_set': {
 | 
			
		||||
                      'tenant_id': 'test',
 | 
			
		||||
                      'metadata_items': [],
 | 
			
		||||
                      'injected_file_content_bytes': 1,
 | 
			
		||||
                      'volumes': 2,
 | 
			
		||||
                      'gigabytes': 1,
 | 
			
		||||
                      'ram': 1,
 | 
			
		||||
                      'floating_ips': 1,
 | 
			
		||||
                      'instances': 1,
 | 
			
		||||
                      'injected_files': 1,
 | 
			
		||||
                      'cores': 1}})
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Quota Classes
 | 
			
		||||
    #
 | 
			
		||||
 | 
			
		||||
    def get_os_quota_class_sets_test(self, **kw):
 | 
			
		||||
        return (200, {'quota_class_set': {
 | 
			
		||||
                      'class_name': 'test',
 | 
			
		||||
                      'metadata_items': [],
 | 
			
		||||
                      'injected_file_content_bytes': 1,
 | 
			
		||||
                      'volumes': 1,
 | 
			
		||||
                      'gigabytes': 1,
 | 
			
		||||
                      'ram': 1,
 | 
			
		||||
                      'floating_ips': 1,
 | 
			
		||||
                      'instances': 1,
 | 
			
		||||
                      'injected_files': 1,
 | 
			
		||||
                      'cores': 1}})
 | 
			
		||||
 | 
			
		||||
    def put_os_quota_class_sets_test(self, body, **kw):
 | 
			
		||||
        assert body.keys() == ['quota_class_set']
 | 
			
		||||
        fakes.assert_has_keys(body['quota_class_set'],
 | 
			
		||||
                              required=['class_name'])
 | 
			
		||||
        return (200, {'quota_class_set': {
 | 
			
		||||
                      'class_name': 'test',
 | 
			
		||||
                      'metadata_items': [],
 | 
			
		||||
                      'injected_file_content_bytes': 1,
 | 
			
		||||
                      'volumes': 2,
 | 
			
		||||
                      'gigabytes': 1,
 | 
			
		||||
                      'ram': 1,
 | 
			
		||||
                      'floating_ips': 1,
 | 
			
		||||
                      'instances': 1,
 | 
			
		||||
                      'injected_files': 1,
 | 
			
		||||
                      'cores': 1}})
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Security Groups
 | 
			
		||||
    #
 | 
			
		||||
    def get_os_security_groups(self, **kw):
 | 
			
		||||
        return (200, {"security_groups": [
 | 
			
		||||
                {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP'}
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    def get_os_security_groups_1(self, **kw):
 | 
			
		||||
        return (200, {"security_group":
 | 
			
		||||
                {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP'}
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    def delete_os_security_groups_1(self, **kw):
 | 
			
		||||
        return (202, None)
 | 
			
		||||
 | 
			
		||||
    def post_os_security_groups(self, body, **kw):
 | 
			
		||||
        assert body.keys() == ['security_group']
 | 
			
		||||
        fakes.assert_has_keys(body['security_group'],
 | 
			
		||||
                              required=['name', 'description'])
 | 
			
		||||
        r = {'security_group':
 | 
			
		||||
                self.get_os_security_groups()[1]['security_groups'][0]}
 | 
			
		||||
        return (202, r)
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Security Group Rules
 | 
			
		||||
    #
 | 
			
		||||
    def get_os_security_group_rules(self, **kw):
 | 
			
		||||
        return (200, {"security_group_rules": [
 | 
			
		||||
                {'id': 1, 'parent_group_id': 1, 'group_id': 2,
 | 
			
		||||
                 'ip_protocol': 'TCP', 'from_port': '22', 'to_port': 22,
 | 
			
		||||
                 'cidr': '10.0.0.0/8'}
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    def delete_os_security_group_rules_1(self, **kw):
 | 
			
		||||
        return (202, None)
 | 
			
		||||
 | 
			
		||||
    def post_os_security_group_rules(self, body, **kw):
 | 
			
		||||
        assert body.keys() == ['security_group_rule']
 | 
			
		||||
        fakes.assert_has_keys(body['security_group_rule'],
 | 
			
		||||
            required=['parent_group_id'],
 | 
			
		||||
            optional=['group_id', 'ip_protocol', 'from_port',
 | 
			
		||||
                      'to_port', 'cidr'])
 | 
			
		||||
        r = {'security_group_rule':
 | 
			
		||||
            self.get_os_security_group_rules()[1]['security_group_rules'][0]}
 | 
			
		||||
        return (202, r)
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Tenant Usage
 | 
			
		||||
    #
 | 
			
		||||
    def get_os_simple_tenant_usage(self, **kw):
 | 
			
		||||
        return (200, {u'tenant_usages': [{
 | 
			
		||||
            u'total_memory_mb_usage': 25451.762807466665,
 | 
			
		||||
            u'total_vcpus_usage': 49.71047423333333,
 | 
			
		||||
            u'total_hours': 49.71047423333333,
 | 
			
		||||
            u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869',
 | 
			
		||||
            u'stop': u'2012-01-22 19:48:41.750722',
 | 
			
		||||
            u'server_usages': [{
 | 
			
		||||
                u'hours': 49.71047423333333,
 | 
			
		||||
                u'uptime': 27035, u'local_gb': 0, u'ended_at': None,
 | 
			
		||||
                u'name': u'f15image1',
 | 
			
		||||
                u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869',
 | 
			
		||||
                u'vcpus': 1, u'memory_mb': 512, u'state': u'active',
 | 
			
		||||
                u'flavor': u'm1.tiny',
 | 
			
		||||
                u'started_at': u'2012-01-20 18:06:06.479998'}],
 | 
			
		||||
            u'start': u'2011-12-25 19:48:41.750687',
 | 
			
		||||
            u'total_local_gb_usage': 0.0}]})
 | 
			
		||||
 | 
			
		||||
    def get_os_simple_tenant_usage_tenantfoo(self, **kw):
 | 
			
		||||
        return (200, {u'tenant_usage': {
 | 
			
		||||
            u'total_memory_mb_usage': 25451.762807466665,
 | 
			
		||||
            u'total_vcpus_usage': 49.71047423333333,
 | 
			
		||||
            u'total_hours': 49.71047423333333,
 | 
			
		||||
            u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869',
 | 
			
		||||
            u'stop': u'2012-01-22 19:48:41.750722',
 | 
			
		||||
            u'server_usages': [{
 | 
			
		||||
                u'hours': 49.71047423333333,
 | 
			
		||||
                u'uptime': 27035, u'local_gb': 0, u'ended_at': None,
 | 
			
		||||
                u'name': u'f15image1',
 | 
			
		||||
                u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869',
 | 
			
		||||
                u'vcpus': 1, u'memory_mb': 512, u'state': u'active',
 | 
			
		||||
                u'flavor': u'm1.tiny',
 | 
			
		||||
                u'started_at': u'2012-01-20 18:06:06.479998'}],
 | 
			
		||||
            u'start': u'2011-12-25 19:48:41.750687',
 | 
			
		||||
            u'total_local_gb_usage': 0.0}})
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Certificates
 | 
			
		||||
    #
 | 
			
		||||
    def get_os_certificates_root(self, **kw):
 | 
			
		||||
        return (200, {'certificate': {'private_key': None, 'data': 'foo'}})
 | 
			
		||||
 | 
			
		||||
    def post_os_certificates(self, **kw):
 | 
			
		||||
        return (200, {'certificate': {'private_key': 'foo', 'data': 'bar'}})
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Aggregates
 | 
			
		||||
    #
 | 
			
		||||
    def get_os_aggregates(self, *kw):
 | 
			
		||||
        return (200, {"aggregates": [
 | 
			
		||||
            {'id':'1',
 | 
			
		||||
             'name': 'test',
 | 
			
		||||
             'availability_zone': 'cinder1'},
 | 
			
		||||
            {'id':'2',
 | 
			
		||||
             'name': 'test2',
 | 
			
		||||
             'availability_zone': 'cinder1'},
 | 
			
		||||
        ]})
 | 
			
		||||
 | 
			
		||||
    def _return_aggregate(self):
 | 
			
		||||
        r = {'aggregate': self.get_os_aggregates()[1]['aggregates'][0]}
 | 
			
		||||
        return (200, r)
 | 
			
		||||
 | 
			
		||||
    def get_os_aggregates_1(self, **kw):
 | 
			
		||||
        return self._return_aggregate()
 | 
			
		||||
 | 
			
		||||
    def post_os_aggregates(self, body, **kw):
 | 
			
		||||
        return self._return_aggregate()
 | 
			
		||||
 | 
			
		||||
    def put_os_aggregates_1(self, body, **kw):
 | 
			
		||||
        return self._return_aggregate()
 | 
			
		||||
 | 
			
		||||
    def put_os_aggregates_2(self, body, **kw):
 | 
			
		||||
        return self._return_aggregate()
 | 
			
		||||
 | 
			
		||||
    def post_os_aggregates_1_action(self, body, **kw):
 | 
			
		||||
        return self._return_aggregate()
 | 
			
		||||
 | 
			
		||||
    def post_os_aggregates_2_action(self, body, **kw):
 | 
			
		||||
        return self._return_aggregate()
 | 
			
		||||
 | 
			
		||||
    def delete_os_aggregates_1(self, **kw):
 | 
			
		||||
        return (202, None)
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Hosts
 | 
			
		||||
    #
 | 
			
		||||
    def get_os_hosts_host(self, *kw):
 | 
			
		||||
        return (200, {'host':
 | 
			
		||||
                [{'resource': {'project': '(total)', 'host': 'dummy',
 | 
			
		||||
                  'cpu': 16, 'memory_mb': 32234, 'disk_gb': 128}},
 | 
			
		||||
                 {'resource': {'project': '(used_now)', 'host': 'dummy',
 | 
			
		||||
                  'cpu': 1, 'memory_mb': 2075, 'disk_gb': 45}},
 | 
			
		||||
                 {'resource': {'project': '(used_max)', 'host': 'dummy',
 | 
			
		||||
                  'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}},
 | 
			
		||||
                 {'resource': {'project': 'admin', 'host': 'dummy',
 | 
			
		||||
                  'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}}]})
 | 
			
		||||
 | 
			
		||||
    def get_os_hosts_sample_host(self, *kw):
 | 
			
		||||
        return (200, {'host': [{'resource': {'host': 'sample_host'}}], })
 | 
			
		||||
 | 
			
		||||
    def put_os_hosts_sample_host_1(self, body, **kw):
 | 
			
		||||
        return (200, {'host': 'sample-host_1',
 | 
			
		||||
                      'status': 'enabled'})
 | 
			
		||||
 | 
			
		||||
    def put_os_hosts_sample_host_2(self, body, **kw):
 | 
			
		||||
        return (200, {'host': 'sample-host_2',
 | 
			
		||||
                      'maintenance_mode': 'on_maintenance'})
 | 
			
		||||
 | 
			
		||||
    def put_os_hosts_sample_host_3(self, body, **kw):
 | 
			
		||||
        return (200, {'host': 'sample-host_3',
 | 
			
		||||
                      'status': 'enabled',
 | 
			
		||||
                      'maintenance_mode': 'on_maintenance'})
 | 
			
		||||
 | 
			
		||||
    def get_os_hosts_sample_host_startup(self, **kw):
 | 
			
		||||
        return (200, {'host': 'sample_host',
 | 
			
		||||
                      'power_action': 'startup'})
 | 
			
		||||
 | 
			
		||||
    def get_os_hosts_sample_host_reboot(self, **kw):
 | 
			
		||||
        return (200, {'host': 'sample_host',
 | 
			
		||||
                      'power_action': 'reboot'})
 | 
			
		||||
 | 
			
		||||
    def get_os_hosts_sample_host_shutdown(self, **kw):
 | 
			
		||||
        return (200, {'host': 'sample_host',
 | 
			
		||||
                      'power_action': 'shutdown'})
 | 
			
		||||
 | 
			
		||||
    def put_os_hosts_sample_host(self, body, **kw):
 | 
			
		||||
        result = {'host': 'dummy'}
 | 
			
		||||
        result.update(body)
 | 
			
		||||
        return (200, result)
 | 
			
		||||
							
								
								
									
										297
									
								
								tests/v1/test_auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								tests/v1/test_auth.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,297 @@
 | 
			
		||||
import httplib2
 | 
			
		||||
import json
 | 
			
		||||
import mock
 | 
			
		||||
 | 
			
		||||
from cinderclient.v1 import client
 | 
			
		||||
from cinderclient import exceptions
 | 
			
		||||
from tests import utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_http_response(resp_dict):
 | 
			
		||||
    """Converts dict of response attributes to httplib response."""
 | 
			
		||||
    resp = httplib2.Response(resp_dict)
 | 
			
		||||
    for k, v in resp_dict['headers'].items():
 | 
			
		||||
        resp[k] = v
 | 
			
		||||
    return resp
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthenticateAgainstKeystoneTests(utils.TestCase):
 | 
			
		||||
    def test_authenticate_success(self):
 | 
			
		||||
        cs = client.Client("username", "password", "project_id",
 | 
			
		||||
                           "auth_url/v2.0", service_type='compute')
 | 
			
		||||
        resp = {
 | 
			
		||||
            "access": {
 | 
			
		||||
                "token": {
 | 
			
		||||
                    "expires": "12345",
 | 
			
		||||
                    "id": "FAKE_ID",
 | 
			
		||||
                },
 | 
			
		||||
                "serviceCatalog": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": "compute",
 | 
			
		||||
                        "endpoints": [
 | 
			
		||||
                            {
 | 
			
		||||
                                "region": "RegionOne",
 | 
			
		||||
                                "adminURL": "http://localhost:8774/v1",
 | 
			
		||||
                                "internalURL": "http://localhost:8774/v1",
 | 
			
		||||
                                "publicURL": "http://localhost:8774/v1/",
 | 
			
		||||
                            },
 | 
			
		||||
                        ],
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        auth_response = httplib2.Response({
 | 
			
		||||
            "status": 200,
 | 
			
		||||
            "body": json.dumps(resp),
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
        mock_request = mock.Mock(return_value=(auth_response,
 | 
			
		||||
                                               json.dumps(resp)))
 | 
			
		||||
 | 
			
		||||
        @mock.patch.object(httplib2.Http, "request", mock_request)
 | 
			
		||||
        def test_auth_call():
 | 
			
		||||
            cs.client.authenticate()
 | 
			
		||||
            headers = {
 | 
			
		||||
                'User-Agent': cs.client.USER_AGENT,
 | 
			
		||||
                'Content-Type': 'application/json',
 | 
			
		||||
                'Accept': 'application/json',
 | 
			
		||||
            }
 | 
			
		||||
            body = {
 | 
			
		||||
                'auth': {
 | 
			
		||||
                    'passwordCredentials': {
 | 
			
		||||
                        'username': cs.client.user,
 | 
			
		||||
                        'password': cs.client.password,
 | 
			
		||||
                    },
 | 
			
		||||
                    'tenantName': cs.client.projectid,
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            token_url = cs.client.auth_url + "/tokens"
 | 
			
		||||
            mock_request.assert_called_with(token_url, "POST",
 | 
			
		||||
                                            headers=headers,
 | 
			
		||||
                                            body=json.dumps(body))
 | 
			
		||||
 | 
			
		||||
            endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
 | 
			
		||||
            public_url = endpoints[0]["publicURL"].rstrip('/')
 | 
			
		||||
            self.assertEqual(cs.client.management_url, public_url)
 | 
			
		||||
            token_id = resp["access"]["token"]["id"]
 | 
			
		||||
            self.assertEqual(cs.client.auth_token, token_id)
 | 
			
		||||
 | 
			
		||||
        test_auth_call()
 | 
			
		||||
 | 
			
		||||
    def test_authenticate_failure(self):
 | 
			
		||||
        cs = client.Client("username", "password", "project_id",
 | 
			
		||||
                           "auth_url/v2.0")
 | 
			
		||||
        resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}}
 | 
			
		||||
        auth_response = httplib2.Response({
 | 
			
		||||
            "status": 401,
 | 
			
		||||
            "body": json.dumps(resp),
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
        mock_request = mock.Mock(return_value=(auth_response,
 | 
			
		||||
                                               json.dumps(resp)))
 | 
			
		||||
 | 
			
		||||
        @mock.patch.object(httplib2.Http, "request", mock_request)
 | 
			
		||||
        def test_auth_call():
 | 
			
		||||
            self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
 | 
			
		||||
 | 
			
		||||
        test_auth_call()
 | 
			
		||||
 | 
			
		||||
    def test_auth_redirect(self):
 | 
			
		||||
        cs = client.Client("username", "password", "project_id",
 | 
			
		||||
                           "auth_url/v1", service_type='compute')
 | 
			
		||||
        dict_correct_response = {
 | 
			
		||||
            "access": {
 | 
			
		||||
                "token": {
 | 
			
		||||
                    "expires": "12345",
 | 
			
		||||
                    "id": "FAKE_ID",
 | 
			
		||||
                },
 | 
			
		||||
                "serviceCatalog": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": "compute",
 | 
			
		||||
                        "endpoints": [
 | 
			
		||||
                            {
 | 
			
		||||
                                "adminURL": "http://localhost:8774/v1",
 | 
			
		||||
                                "region": "RegionOne",
 | 
			
		||||
                                "internalURL": "http://localhost:8774/v1",
 | 
			
		||||
                                "publicURL": "http://localhost:8774/v1/",
 | 
			
		||||
                            },
 | 
			
		||||
                        ],
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        correct_response = json.dumps(dict_correct_response)
 | 
			
		||||
        dict_responses = [
 | 
			
		||||
            {"headers": {'location':'http://127.0.0.1:5001'},
 | 
			
		||||
             "status": 305,
 | 
			
		||||
             "body": "Use proxy"},
 | 
			
		||||
            # Configured on admin port, cinder redirects to v2.0 port.
 | 
			
		||||
            # When trying to connect on it, keystone auth succeed by v1.0
 | 
			
		||||
            # protocol (through headers) but tokens are being returned in
 | 
			
		||||
            # body (looks like keystone bug). Leaved for compatibility.
 | 
			
		||||
            {"headers": {},
 | 
			
		||||
             "status": 200,
 | 
			
		||||
             "body": correct_response},
 | 
			
		||||
            {"headers": {},
 | 
			
		||||
             "status": 200,
 | 
			
		||||
             "body": correct_response}
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        responses = [(to_http_response(resp), resp['body']) \
 | 
			
		||||
                        for resp in dict_responses]
 | 
			
		||||
 | 
			
		||||
        def side_effect(*args, **kwargs):
 | 
			
		||||
            return responses.pop(0)
 | 
			
		||||
 | 
			
		||||
        mock_request = mock.Mock(side_effect=side_effect)
 | 
			
		||||
 | 
			
		||||
        @mock.patch.object(httplib2.Http, "request", mock_request)
 | 
			
		||||
        def test_auth_call():
 | 
			
		||||
            cs.client.authenticate()
 | 
			
		||||
            headers = {
 | 
			
		||||
                'User-Agent': cs.client.USER_AGENT,
 | 
			
		||||
                'Content-Type': 'application/json',
 | 
			
		||||
                'Accept': 'application/json',
 | 
			
		||||
            }
 | 
			
		||||
            body = {
 | 
			
		||||
                'auth': {
 | 
			
		||||
                    'passwordCredentials': {
 | 
			
		||||
                        'username': cs.client.user,
 | 
			
		||||
                        'password': cs.client.password,
 | 
			
		||||
                     },
 | 
			
		||||
                     'tenantName': cs.client.projectid,
 | 
			
		||||
                 },
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            token_url = cs.client.auth_url + "/tokens"
 | 
			
		||||
            mock_request.assert_called_with(token_url, "POST",
 | 
			
		||||
                                            headers=headers,
 | 
			
		||||
                                            body=json.dumps(body))
 | 
			
		||||
 | 
			
		||||
            resp = dict_correct_response
 | 
			
		||||
            endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
 | 
			
		||||
            public_url = endpoints[0]["publicURL"].rstrip('/')
 | 
			
		||||
            self.assertEqual(cs.client.management_url, public_url)
 | 
			
		||||
            token_id = resp["access"]["token"]["id"]
 | 
			
		||||
            self.assertEqual(cs.client.auth_token, token_id)
 | 
			
		||||
 | 
			
		||||
        test_auth_call()
 | 
			
		||||
 | 
			
		||||
    def test_ambiguous_endpoints(self):
 | 
			
		||||
        cs = client.Client("username", "password", "project_id",
 | 
			
		||||
                           "auth_url/v2.0", service_type='compute')
 | 
			
		||||
        resp = {
 | 
			
		||||
            "access": {
 | 
			
		||||
                "token": {
 | 
			
		||||
                    "expires": "12345",
 | 
			
		||||
                    "id": "FAKE_ID",
 | 
			
		||||
                },
 | 
			
		||||
                "serviceCatalog": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "adminURL": "http://localhost:8774/v1",
 | 
			
		||||
                        "type": "compute",
 | 
			
		||||
                        "name": "Compute CLoud",
 | 
			
		||||
                        "endpoints": [
 | 
			
		||||
                            {
 | 
			
		||||
                                "region": "RegionOne",
 | 
			
		||||
                                "internalURL": "http://localhost:8774/v1",
 | 
			
		||||
                                "publicURL": "http://localhost:8774/v1/",
 | 
			
		||||
                            },
 | 
			
		||||
                        ],
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "adminURL": "http://localhost:8774/v1",
 | 
			
		||||
                        "type": "compute",
 | 
			
		||||
                        "name": "Hyper-compute Cloud",
 | 
			
		||||
                        "endpoints": [
 | 
			
		||||
                            {
 | 
			
		||||
                                "internalURL": "http://localhost:8774/v1",
 | 
			
		||||
                                "publicURL": "http://localhost:8774/v1/",
 | 
			
		||||
                            },
 | 
			
		||||
                        ],
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        auth_response = httplib2.Response({
 | 
			
		||||
            "status": 200,
 | 
			
		||||
            "body": json.dumps(resp),
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
        mock_request = mock.Mock(return_value=(auth_response,
 | 
			
		||||
                                               json.dumps(resp)))
 | 
			
		||||
 | 
			
		||||
        @mock.patch.object(httplib2.Http, "request", mock_request)
 | 
			
		||||
        def test_auth_call():
 | 
			
		||||
            self.assertRaises(exceptions.AmbiguousEndpoints,
 | 
			
		||||
                              cs.client.authenticate)
 | 
			
		||||
 | 
			
		||||
        test_auth_call()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthenticationTests(utils.TestCase):
 | 
			
		||||
    def test_authenticate_success(self):
 | 
			
		||||
        cs = client.Client("username", "password", "project_id", "auth_url")
 | 
			
		||||
        management_url = 'https://servers.api.rackspacecloud.com/v1.1/443470'
 | 
			
		||||
        auth_response = httplib2.Response({
 | 
			
		||||
            'status': 204,
 | 
			
		||||
            'x-server-management-url': management_url,
 | 
			
		||||
            'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
 | 
			
		||||
        })
 | 
			
		||||
        mock_request = mock.Mock(return_value=(auth_response, None))
 | 
			
		||||
 | 
			
		||||
        @mock.patch.object(httplib2.Http, "request", mock_request)
 | 
			
		||||
        def test_auth_call():
 | 
			
		||||
            cs.client.authenticate()
 | 
			
		||||
            headers = {
 | 
			
		||||
                'Accept': 'application/json',
 | 
			
		||||
                'X-Auth-User': 'username',
 | 
			
		||||
                'X-Auth-Key': 'password',
 | 
			
		||||
                'X-Auth-Project-Id': 'project_id',
 | 
			
		||||
                'User-Agent': cs.client.USER_AGENT
 | 
			
		||||
            }
 | 
			
		||||
            mock_request.assert_called_with(cs.client.auth_url, 'GET',
 | 
			
		||||
                                            headers=headers)
 | 
			
		||||
            self.assertEqual(cs.client.management_url,
 | 
			
		||||
                             auth_response['x-server-management-url'])
 | 
			
		||||
            self.assertEqual(cs.client.auth_token,
 | 
			
		||||
                             auth_response['x-auth-token'])
 | 
			
		||||
 | 
			
		||||
        test_auth_call()
 | 
			
		||||
 | 
			
		||||
    def test_authenticate_failure(self):
 | 
			
		||||
        cs = client.Client("username", "password", "project_id", "auth_url")
 | 
			
		||||
        auth_response = httplib2.Response({'status': 401})
 | 
			
		||||
        mock_request = mock.Mock(return_value=(auth_response, None))
 | 
			
		||||
 | 
			
		||||
        @mock.patch.object(httplib2.Http, "request", mock_request)
 | 
			
		||||
        def test_auth_call():
 | 
			
		||||
            self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
 | 
			
		||||
 | 
			
		||||
        test_auth_call()
 | 
			
		||||
 | 
			
		||||
    def test_auth_automatic(self):
 | 
			
		||||
        cs = client.Client("username", "password", "project_id", "auth_url")
 | 
			
		||||
        http_client = cs.client
 | 
			
		||||
        http_client.management_url = ''
 | 
			
		||||
        mock_request = mock.Mock(return_value=(None, None))
 | 
			
		||||
 | 
			
		||||
        @mock.patch.object(http_client, 'request', mock_request)
 | 
			
		||||
        @mock.patch.object(http_client, 'authenticate')
 | 
			
		||||
        def test_auth_call(m):
 | 
			
		||||
            http_client.get('/')
 | 
			
		||||
            m.assert_called()
 | 
			
		||||
            mock_request.assert_called()
 | 
			
		||||
 | 
			
		||||
        test_auth_call()
 | 
			
		||||
 | 
			
		||||
    def test_auth_manual(self):
 | 
			
		||||
        cs = client.Client("username", "password", "project_id", "auth_url")
 | 
			
		||||
 | 
			
		||||
        @mock.patch.object(cs.client, 'authenticate')
 | 
			
		||||
        def test_auth_call(m):
 | 
			
		||||
            cs.authenticate()
 | 
			
		||||
            m.assert_called()
 | 
			
		||||
 | 
			
		||||
        test_auth_call()
 | 
			
		||||
							
								
								
									
										77
									
								
								tests/v1/test_shell.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								tests/v1/test_shell.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from cinderclient import client
 | 
			
		||||
from cinderclient import shell
 | 
			
		||||
from tests.v1 import fakes
 | 
			
		||||
from tests import utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShellTest(utils.TestCase):
 | 
			
		||||
 | 
			
		||||
    # Patch os.environ to avoid required auth info.
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        """Run before each test."""
 | 
			
		||||
        self.old_environment = os.environ.copy()
 | 
			
		||||
        os.environ = {
 | 
			
		||||
            'CINDER_USERNAME': 'username',
 | 
			
		||||
            'CINDER_PASSWORD': 'password',
 | 
			
		||||
            'CINDER_PROJECT_ID': 'project_id',
 | 
			
		||||
            'OS_COMPUTE_API_VERSION': '1.1',
 | 
			
		||||
            'CINDER_URL': 'http://no.where',
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.shell = shell.OpenStackCinderShell()
 | 
			
		||||
 | 
			
		||||
        #HACK(bcwaldon): replace this when we start using stubs
 | 
			
		||||
        self.old_get_client_class = client.get_client_class
 | 
			
		||||
        client.get_client_class = lambda *_: fakes.FakeClient
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        os.environ = self.old_environment
 | 
			
		||||
        # For some method like test_image_meta_bad_action we are
 | 
			
		||||
        # testing a SystemExit to be thrown and object self.shell has
 | 
			
		||||
        # no time to get instantatiated which is OK in this case, so
 | 
			
		||||
        # we make sure the method is there before launching it.
 | 
			
		||||
        if hasattr(self.shell, 'cs'):
 | 
			
		||||
            self.shell.cs.clear_callstack()
 | 
			
		||||
 | 
			
		||||
        #HACK(bcwaldon): replace this when we start using stubs
 | 
			
		||||
        client.get_client_class = self.old_get_client_class
 | 
			
		||||
 | 
			
		||||
    def run_command(self, cmd):
 | 
			
		||||
        self.shell.main(cmd.split())
 | 
			
		||||
 | 
			
		||||
    def assert_called(self, method, url, body=None, **kwargs):
 | 
			
		||||
        return self.shell.cs.assert_called(method, url, body, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def assert_called_anytime(self, method, url, body=None):
 | 
			
		||||
        return self.shell.cs.assert_called_anytime(method, url, body)
 | 
			
		||||
 | 
			
		||||
    def test_list(self):
 | 
			
		||||
        self.run_command('list')
 | 
			
		||||
        # NOTE(jdg): we default to detail currently
 | 
			
		||||
        self.assert_called('GET', '/volumes/detail')
 | 
			
		||||
 | 
			
		||||
    def test_show(self):
 | 
			
		||||
        self.run_command('show 1234')
 | 
			
		||||
        self.assert_called('GET', '/volumes/1234')
 | 
			
		||||
 | 
			
		||||
    def test_delete(self):
 | 
			
		||||
        self.run_command('delete 1234')
 | 
			
		||||
							
								
								
									
										1
									
								
								tests/v1/testfile.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/v1/testfile.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
BLAH
 | 
			
		||||
							
								
								
									
										29
									
								
								tests/v1/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								tests/v1/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
from nose.tools import ok_
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def fail(msg):
 | 
			
		||||
    raise AssertionError(msg)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def assert_in(thing, seq, msg=None):
 | 
			
		||||
    msg = msg or "'%s' not found in %s" % (thing, seq)
 | 
			
		||||
    ok_(thing in seq, msg)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def assert_not_in(thing, seq, msg=None):
 | 
			
		||||
    msg = msg or "unexpected '%s' found in %s" % (thing, seq)
 | 
			
		||||
    ok_(thing not in seq, msg)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def assert_has_keys(dict, required=[], optional=[]):
 | 
			
		||||
    keys = dict.keys()
 | 
			
		||||
    for k in required:
 | 
			
		||||
        assert_in(k, keys, "required key %s missing from %s" % (k, dict))
 | 
			
		||||
    allowed_keys = set(required) | set(optional)
 | 
			
		||||
    extra_keys = set(keys).difference(set(required + optional))
 | 
			
		||||
    if extra_keys:
 | 
			
		||||
        fail("found unexpected keys: %s" % list(extra_keys))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def assert_isinstance(thing, kls):
 | 
			
		||||
    ok_(isinstance(thing, kls), "%s is not an instance of %s" % (thing, kls))
 | 
			
		||||
							
								
								
									
										3
									
								
								tools/generate_authors.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								tools/generate_authors.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
git shortlog -se | cut -c8-
 | 
			
		||||
							
								
								
									
										244
									
								
								tools/install_venv.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								tools/install_venv.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,244 @@
 | 
			
		||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 | 
			
		||||
 | 
			
		||||
# Copyright 2010 United States Government as represented by the
 | 
			
		||||
# Administrator of the National Aeronautics and Space Administration.
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
#
 | 
			
		||||
# Copyright 2010 OpenStack, LLC
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Installation script for Nova's development virtualenv
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import optparse
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
import sys
 | 
			
		||||
import platform
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
 | 
			
		||||
VENV = os.path.join(ROOT, '.venv')
 | 
			
		||||
PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires')
 | 
			
		||||
PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def die(message, *args):
 | 
			
		||||
    print >> sys.stderr, message % args
 | 
			
		||||
    sys.exit(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_python_version():
 | 
			
		||||
    if sys.version_info < (2, 6):
 | 
			
		||||
        die("Need Python Version >= 2.6")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_command_with_code(cmd, redirect_output=True, check_exit_code=True):
 | 
			
		||||
    """
 | 
			
		||||
    Runs a command in an out-of-process shell, returning the
 | 
			
		||||
    output of that command.  Working directory is ROOT.
 | 
			
		||||
    """
 | 
			
		||||
    if redirect_output:
 | 
			
		||||
        stdout = subprocess.PIPE
 | 
			
		||||
    else:
 | 
			
		||||
        stdout = None
 | 
			
		||||
 | 
			
		||||
    proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout)
 | 
			
		||||
    output = proc.communicate()[0]
 | 
			
		||||
    if check_exit_code and proc.returncode != 0:
 | 
			
		||||
        die('Command "%s" failed.\n%s', ' '.join(cmd), output)
 | 
			
		||||
    return (output, proc.returncode)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_command(cmd, redirect_output=True, check_exit_code=True):
 | 
			
		||||
    return run_command_with_code(cmd, redirect_output, check_exit_code)[0]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Distro(object):
 | 
			
		||||
 | 
			
		||||
    def check_cmd(self, cmd):
 | 
			
		||||
        return bool(run_command(['which', cmd], check_exit_code=False).strip())
 | 
			
		||||
 | 
			
		||||
    def install_virtualenv(self):
 | 
			
		||||
        if self.check_cmd('virtualenv'):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if self.check_cmd('easy_install'):
 | 
			
		||||
            print 'Installing virtualenv via easy_install...',
 | 
			
		||||
            if run_command(['easy_install', 'virtualenv']):
 | 
			
		||||
                print 'Succeeded'
 | 
			
		||||
                return
 | 
			
		||||
            else:
 | 
			
		||||
                print 'Failed'
 | 
			
		||||
 | 
			
		||||
        die('ERROR: virtualenv not found.\n\nDevelopment'
 | 
			
		||||
            ' requires virtualenv, please install it using your'
 | 
			
		||||
            ' favorite package management tool')
 | 
			
		||||
 | 
			
		||||
    def post_process(self):
 | 
			
		||||
        """Any distribution-specific post-processing gets done here.
 | 
			
		||||
 | 
			
		||||
        In particular, this is useful for applying patches to code inside
 | 
			
		||||
        the venv."""
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Debian(Distro):
 | 
			
		||||
    """This covers all Debian-based distributions."""
 | 
			
		||||
 | 
			
		||||
    def check_pkg(self, pkg):
 | 
			
		||||
        return run_command_with_code(['dpkg', '-l', pkg],
 | 
			
		||||
                                     check_exit_code=False)[1] == 0
 | 
			
		||||
 | 
			
		||||
    def apt_install(self, pkg, **kwargs):
 | 
			
		||||
        run_command(['sudo', 'apt-get', 'install', '-y', pkg], **kwargs)
 | 
			
		||||
 | 
			
		||||
    def apply_patch(self, originalfile, patchfile):
 | 
			
		||||
        run_command(['patch', originalfile, patchfile])
 | 
			
		||||
 | 
			
		||||
    def install_virtualenv(self):
 | 
			
		||||
        if self.check_cmd('virtualenv'):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if not self.check_pkg('python-virtualenv'):
 | 
			
		||||
            self.apt_install('python-virtualenv', check_exit_code=False)
 | 
			
		||||
 | 
			
		||||
        super(Debian, self).install_virtualenv()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Fedora(Distro):
 | 
			
		||||
    """This covers all Fedora-based distributions.
 | 
			
		||||
 | 
			
		||||
    Includes: Fedora, RHEL, CentOS, Scientific Linux"""
 | 
			
		||||
 | 
			
		||||
    def check_pkg(self, pkg):
 | 
			
		||||
        return run_command_with_code(['rpm', '-q', pkg],
 | 
			
		||||
                                     check_exit_code=False)[1] == 0
 | 
			
		||||
 | 
			
		||||
    def yum_install(self, pkg, **kwargs):
 | 
			
		||||
        run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs)
 | 
			
		||||
 | 
			
		||||
    def apply_patch(self, originalfile, patchfile):
 | 
			
		||||
        run_command(['patch', originalfile, patchfile])
 | 
			
		||||
 | 
			
		||||
    def install_virtualenv(self):
 | 
			
		||||
        if self.check_cmd('virtualenv'):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if not self.check_pkg('python-virtualenv'):
 | 
			
		||||
            self.yum_install('python-virtualenv', check_exit_code=False)
 | 
			
		||||
 | 
			
		||||
        super(Fedora, self).install_virtualenv()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_distro():
 | 
			
		||||
    if os.path.exists('/etc/fedora-release') or \
 | 
			
		||||
       os.path.exists('/etc/redhat-release'):
 | 
			
		||||
        return Fedora()
 | 
			
		||||
    elif os.path.exists('/etc/debian_version'):
 | 
			
		||||
        return Debian()
 | 
			
		||||
    else:
 | 
			
		||||
        return Distro()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_dependencies():
 | 
			
		||||
    get_distro().install_virtualenv()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_virtualenv(venv=VENV, no_site_packages=True):
 | 
			
		||||
    """Creates the virtual environment and installs PIP only into the
 | 
			
		||||
    virtual environment
 | 
			
		||||
    """
 | 
			
		||||
    print 'Creating venv...',
 | 
			
		||||
    if no_site_packages:
 | 
			
		||||
        run_command(['virtualenv', '-q', '--no-site-packages', VENV])
 | 
			
		||||
    else:
 | 
			
		||||
        run_command(['virtualenv', '-q', VENV])
 | 
			
		||||
    print 'done.'
 | 
			
		||||
    print 'Installing pip in virtualenv...',
 | 
			
		||||
    if not run_command(['tools/with_venv.sh', 'easy_install',
 | 
			
		||||
                        'pip>1.0']).strip():
 | 
			
		||||
        die("Failed to install pip.")
 | 
			
		||||
    print 'done.'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pip_install(*args):
 | 
			
		||||
    run_command(['tools/with_venv.sh',
 | 
			
		||||
                 'pip', 'install', '--upgrade'] + list(args),
 | 
			
		||||
                redirect_output=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def install_dependencies(venv=VENV):
 | 
			
		||||
    print 'Installing dependencies with pip (this can take a while)...'
 | 
			
		||||
 | 
			
		||||
    # First things first, make sure our venv has the latest pip and distribute.
 | 
			
		||||
    pip_install('pip')
 | 
			
		||||
    pip_install('distribute')
 | 
			
		||||
 | 
			
		||||
    pip_install('-r', PIP_REQUIRES)
 | 
			
		||||
 | 
			
		||||
    # Tell the virtual env how to "import cinder"
 | 
			
		||||
    pthfile = os.path.join(venv, "lib", PY_VERSION, "site-packages",
 | 
			
		||||
                        "cinderclient.pth")
 | 
			
		||||
    f = open(pthfile, 'w')
 | 
			
		||||
    f.write("%s\n" % ROOT)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def post_process():
 | 
			
		||||
    get_distro().post_process()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def print_help():
 | 
			
		||||
    help = """
 | 
			
		||||
    python-cinderclient development environment setup is complete.
 | 
			
		||||
 | 
			
		||||
    python-cinderclient development uses virtualenv to track and manage Python
 | 
			
		||||
    dependencies while in development and testing.
 | 
			
		||||
 | 
			
		||||
    To activate the python-cinderclient virtualenv for the extent of your current
 | 
			
		||||
    shell session you can run:
 | 
			
		||||
 | 
			
		||||
    $ source .venv/bin/activate
 | 
			
		||||
 | 
			
		||||
    Or, if you prefer, you can run commands in the virtualenv on a case by case
 | 
			
		||||
    basis by running:
 | 
			
		||||
 | 
			
		||||
    $ tools/with_venv.sh <your command>
 | 
			
		||||
 | 
			
		||||
    Also, make test will automatically use the virtualenv.
 | 
			
		||||
    """
 | 
			
		||||
    print help
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_args():
 | 
			
		||||
    """Parse command-line arguments"""
 | 
			
		||||
    parser = optparse.OptionParser()
 | 
			
		||||
    parser.add_option("-n", "--no-site-packages", dest="no_site_packages",
 | 
			
		||||
        default=False, action="store_true",
 | 
			
		||||
        help="Do not inherit packages from global Python install")
 | 
			
		||||
    return parser.parse_args()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main(argv):
 | 
			
		||||
    (options, args) = parse_args()
 | 
			
		||||
    check_python_version()
 | 
			
		||||
    check_dependencies()
 | 
			
		||||
    create_virtualenv(no_site_packages=options.no_site_packages)
 | 
			
		||||
    install_dependencies()
 | 
			
		||||
    post_process()
 | 
			
		||||
    print_help()
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    main(sys.argv)
 | 
			
		||||
							
								
								
									
										15
									
								
								tools/nova.bash_completion
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tools/nova.bash_completion
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
_cinder()
 | 
			
		||||
{
 | 
			
		||||
    local cur prev opts
 | 
			
		||||
    COMPREPLY=()
 | 
			
		||||
    cur="${COMP_WORDS[COMP_CWORD]}"
 | 
			
		||||
    prev="${COMP_WORDS[COMP_CWORD-1]}"
 | 
			
		||||
 | 
			
		||||
    opts="$(cinder bash_completion)"
 | 
			
		||||
 | 
			
		||||
    COMPLETION_CACHE=~/.cinderclient/*/*-cache
 | 
			
		||||
    opts+=" "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ')
 | 
			
		||||
 | 
			
		||||
    COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
 | 
			
		||||
}
 | 
			
		||||
complete -F _cinder cinder
 | 
			
		||||
							
								
								
									
										9
									
								
								tools/pip-requires
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								tools/pip-requires
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
argparse
 | 
			
		||||
coverage
 | 
			
		||||
httplib2
 | 
			
		||||
mock
 | 
			
		||||
nose
 | 
			
		||||
prettytable
 | 
			
		||||
simplejson
 | 
			
		||||
pep8==0.6.1
 | 
			
		||||
unittest2
 | 
			
		||||
							
								
								
									
										145
									
								
								tools/rfc.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										145
									
								
								tools/rfc.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,145 @@
 | 
			
		||||
#!/bin/sh -e
 | 
			
		||||
# Copyright (c) 2010-2011 Gluster, Inc. <http://www.gluster.com>
 | 
			
		||||
# This initial version of this file was taken from the source tree
 | 
			
		||||
# of GlusterFS. It was not directly attributed, but is assumed to be
 | 
			
		||||
# Copyright (c) 2010-2011 Gluster, Inc and release GPLv3
 | 
			
		||||
# Subsequent modifications are Copyright (c) 2011 OpenStack, LLC.
 | 
			
		||||
#
 | 
			
		||||
# GlusterFS is free software; you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU General Public License as published
 | 
			
		||||
# by the Free Software Foundation; either version 3 of the License,
 | 
			
		||||
# or (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# GlusterFS is distributed in the hope that it will be useful, but
 | 
			
		||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | 
			
		||||
# General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with this program.  If not, see
 | 
			
		||||
# <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
branch="master";
 | 
			
		||||
 | 
			
		||||
set_hooks_commit_msg()
 | 
			
		||||
{
 | 
			
		||||
    top_dir=`git rev-parse --show-toplevel`
 | 
			
		||||
    f="${top_dir}/.git/hooks/commit-msg";
 | 
			
		||||
    u="https://review.openstack.org/tools/hooks/commit-msg";
 | 
			
		||||
 | 
			
		||||
    if [ -x "$f" ]; then
 | 
			
		||||
        return;
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
    curl -o $f $u || wget -O $f $u;
 | 
			
		||||
 | 
			
		||||
    chmod +x $f;
 | 
			
		||||
 | 
			
		||||
    GIT_EDITOR=true git commit --amend
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
add_remote()
 | 
			
		||||
{
 | 
			
		||||
    username=$1
 | 
			
		||||
    project=$2
 | 
			
		||||
 | 
			
		||||
    echo "No remote set, testing ssh://$username@review.openstack.org:29418"
 | 
			
		||||
    if project_list=`ssh -p29418 -o StrictHostKeyChecking=no $username@review.openstack.org gerrit ls-projects 2>/dev/null`
 | 
			
		||||
    then
 | 
			
		||||
        echo "$username@review.openstack.org:29418 worked."
 | 
			
		||||
        if echo $project_list | grep $project >/dev/null
 | 
			
		||||
        then
 | 
			
		||||
            echo "Creating a git remote called gerrit that maps to:"
 | 
			
		||||
            echo "  ssh://$username@review.openstack.org:29418/$project"
 | 
			
		||||
            git remote add gerrit ssh://$username@review.openstack.org:29418/$project
 | 
			
		||||
        else
 | 
			
		||||
            echo "The current project name, $project, is not a known project."
 | 
			
		||||
            echo "Please either reclone from github/gerrit or create a"
 | 
			
		||||
            echo "remote named gerrit that points to the intended project."
 | 
			
		||||
            return 1
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        return 0
 | 
			
		||||
    fi
 | 
			
		||||
    return 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
check_remote()
 | 
			
		||||
{
 | 
			
		||||
    if ! git remote | grep gerrit >/dev/null 2>&1
 | 
			
		||||
    then
 | 
			
		||||
        origin_project=`git remote show origin | grep 'Fetch URL' | perl -nle '@fields = split(m|[:/]|); $len = $#fields; print $fields[$len-1], "/", $fields[$len];'`
 | 
			
		||||
        if add_remote $USERNAME $origin_project
 | 
			
		||||
        then
 | 
			
		||||
            return 0
 | 
			
		||||
        else
 | 
			
		||||
            echo "Your local name doesn't work on Gerrit."
 | 
			
		||||
            echo -n "Enter Gerrit username (same as launchpad): "
 | 
			
		||||
            read gerrit_user
 | 
			
		||||
            if add_remote $gerrit_user $origin_project
 | 
			
		||||
            then
 | 
			
		||||
                return 0
 | 
			
		||||
            else
 | 
			
		||||
                echo "Can't infer where gerrit is - please set a remote named"
 | 
			
		||||
                echo "gerrit manually and then try again."
 | 
			
		||||
                echo
 | 
			
		||||
                echo "For more information, please see:"
 | 
			
		||||
                echo "\thttp://wiki.openstack.org/GerritWorkflow"
 | 
			
		||||
                exit 1
 | 
			
		||||
            fi
 | 
			
		||||
        fi
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
rebase_changes()
 | 
			
		||||
{
 | 
			
		||||
    git fetch;
 | 
			
		||||
 | 
			
		||||
    GIT_EDITOR=true git rebase -i origin/$branch || exit $?;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
assert_diverge()
 | 
			
		||||
{
 | 
			
		||||
    if ! git diff origin/$branch..HEAD | grep -q .
 | 
			
		||||
    then
 | 
			
		||||
	echo "No changes between the current branch and origin/$branch."
 | 
			
		||||
	exit 1
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
main()
 | 
			
		||||
{
 | 
			
		||||
    set_hooks_commit_msg;
 | 
			
		||||
 | 
			
		||||
    check_remote;
 | 
			
		||||
 | 
			
		||||
    rebase_changes;
 | 
			
		||||
 | 
			
		||||
    assert_diverge;
 | 
			
		||||
 | 
			
		||||
    bug=$(git show --format='%s %b' | perl -nle 'if (/\b([Bb]ug|[Ll][Pp])\s*[#:]?\s*(\d+)/) {print "$2"; exit}')
 | 
			
		||||
 | 
			
		||||
    bp=$(git show --format='%s %b' | perl -nle 'if (/\b([Bb]lue[Pp]rint|[Bb][Pp])\s*[#:]?\s*([0-9a-zA-Z-_]+)/) {print "$2"; exit}')
 | 
			
		||||
 | 
			
		||||
    if [ "$DRY_RUN" = 1 ]; then
 | 
			
		||||
        drier='echo -e Please use the following command to send your commits to review:\n\n'
 | 
			
		||||
    else
 | 
			
		||||
        drier=
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
    local_branch=`git branch | grep -Ei "\* (.*)" | cut -f2 -d' '`
 | 
			
		||||
    if [ -z "$bug" ]; then
 | 
			
		||||
	if [ -z "$bp" ]; then
 | 
			
		||||
            $drier git push gerrit HEAD:refs/for/$branch/$local_branch;
 | 
			
		||||
	else
 | 
			
		||||
	    $drier git push gerrit HEAD:refs/for/$branch/bp/$bp;
 | 
			
		||||
	fi
 | 
			
		||||
    else
 | 
			
		||||
        $drier git push gerrit HEAD:refs/for/$branch/bug/$bug;
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main "$@"
 | 
			
		||||
							
								
								
									
										4
									
								
								tools/with_venv.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								tools/with_venv.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
TOOLS=`dirname $0`
 | 
			
		||||
VENV=$TOOLS/../.venv
 | 
			
		||||
source $VENV/bin/activate && $@
 | 
			
		||||
							
								
								
									
										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 = coverage
 | 
			
		||||
commands = /bin/bash run_tests.sh -N --coverage
 | 
			
		||||
		Reference in New Issue
	
	Block a user