Base structure and stuff code
The patch adds a base structure of the client. Co-Authored-By: Andrey Kurilin <akurilin@mirantis.com> bp api-rally-python-client Change-Id: I03fd125a3eab010748908367a5e4e28ddd74824c
This commit is contained in:
		
				
					committed by
					
						
						Andrey Kurilin
					
				
			
			
				
	
			
			
			
						parent
						
							c7342db0aa
						
					
				
				
					commit
					c4ef481a88
				
			
							
								
								
									
										33
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					# Compiled files
 | 
				
			||||||
 | 
					*.py[co]
 | 
				
			||||||
 | 
					*.a
 | 
				
			||||||
 | 
					*.o
 | 
				
			||||||
 | 
					*.so
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Sphinx
 | 
				
			||||||
 | 
					_build
 | 
				
			||||||
 | 
					doc/source/api/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Packages/installer info
 | 
				
			||||||
 | 
					*.egg
 | 
				
			||||||
 | 
					*.egg-info
 | 
				
			||||||
 | 
					dist
 | 
				
			||||||
 | 
					build
 | 
				
			||||||
 | 
					eggs
 | 
				
			||||||
 | 
					parts
 | 
				
			||||||
 | 
					var
 | 
				
			||||||
 | 
					sdist
 | 
				
			||||||
 | 
					develop-eggs
 | 
				
			||||||
 | 
					.installed.cfg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Other
 | 
				
			||||||
 | 
					*.DS_Store
 | 
				
			||||||
 | 
					.testrepository
 | 
				
			||||||
 | 
					.tox
 | 
				
			||||||
 | 
					.venv
 | 
				
			||||||
 | 
					.*.swp
 | 
				
			||||||
 | 
					.coverage
 | 
				
			||||||
 | 
					cover
 | 
				
			||||||
 | 
					AUTHORS
 | 
				
			||||||
 | 
					ChangeLog
 | 
				
			||||||
 | 
					*.sqlite
 | 
				
			||||||
							
								
								
									
										7
									
								
								.testr.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.testr.conf
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					[DEFAULT]
 | 
				
			||||||
 | 
					test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
 | 
				
			||||||
 | 
					             OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
 | 
				
			||||||
 | 
					             OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
 | 
				
			||||||
 | 
					             ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
 | 
				
			||||||
 | 
					test_id_option=--load-list $IDFILE
 | 
				
			||||||
 | 
					test_list_option=--list
 | 
				
			||||||
							
								
								
									
										17
									
								
								CONTRIBUTING.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								CONTRIBUTING.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					If you would like to contribute to the development of OpenStack,
 | 
				
			||||||
 | 
					you must follow the steps in the "If you're a developer, start here"
 | 
				
			||||||
 | 
					section of this page:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   http://wiki.openstack.org/HowToContribute
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Once those steps have been completed, changes to OpenStack
 | 
				
			||||||
 | 
					should be submitted for review via the Gerrit tool, following
 | 
				
			||||||
 | 
					the workflow documented at:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   http://wiki.openstack.org/GerritWorkflow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Pull requests submitted through GitHub will be ignored.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Bugs should be filed on Launchpad, not GitHub:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   https://bugs.launchpad.net/rally
 | 
				
			||||||
							
								
								
									
										4
									
								
								HACKING.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								HACKING.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					Rally Client Style Commandments
 | 
				
			||||||
 | 
					===============================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/
 | 
				
			||||||
							
								
								
									
										175
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,175 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					                                 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.
 | 
				
			||||||
							
								
								
									
										7
									
								
								README.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								README.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					Python bindings to the Rally API
 | 
				
			||||||
 | 
					================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This is a client library for Rally built on the Rally API.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* Free software: Apache license
 | 
				
			||||||
 | 
					* Documentation: http://wiki.openstack.org/wiki/Rally
 | 
				
			||||||
							
								
								
									
										69
									
								
								doc/source/conf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								doc/source/conf.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					# -*- coding: utf-8 -*-
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# -- 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',
 | 
				
			||||||
 | 
					              'sphinx.ext.viewcode',
 | 
				
			||||||
 | 
					              'oslosphinx',
 | 
				
			||||||
 | 
					              ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# autodoc generation is a bit aggressive and a nuisance when doing heavy
 | 
				
			||||||
 | 
					# text edit cycles.
 | 
				
			||||||
 | 
					# execute "export SPHINX_DEBUG=1" in your terminal to disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Add any paths that contain templates here, relative to this directory.
 | 
				
			||||||
 | 
					templates_path = ['_templates']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# The suffix of source filenames.
 | 
				
			||||||
 | 
					source_suffix = '.rst'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# The master toctree document.
 | 
				
			||||||
 | 
					master_doc = 'index'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# General information about the project.
 | 
				
			||||||
 | 
					project = 'python-rallyclient'
 | 
				
			||||||
 | 
					copyright = 'OpenStack Contributors'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# A list of ignored prefixes for module index sorting.
 | 
				
			||||||
 | 
					modindex_common_prefix = ['rallyclient.']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# The name of the Pygments (syntax highlighting) style to use.
 | 
				
			||||||
 | 
					pygments_style = 'sphinx'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# -- 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_path = ["."]
 | 
				
			||||||
 | 
					#html_theme = '_theme'
 | 
				
			||||||
 | 
					#html_static_path = ['_static']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Output file base name for HTML help builder.
 | 
				
			||||||
 | 
					htmlhelp_basename = '%sdoc' % project
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Grouping the document tree into LaTeX files. List of tuples
 | 
				
			||||||
 | 
					# (source start file, target name, title, author, documentclass
 | 
				
			||||||
 | 
					# [howto/manual]).
 | 
				
			||||||
 | 
					latex_documents = [
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        'index',
 | 
				
			||||||
 | 
					        '%s.tex' % project,
 | 
				
			||||||
 | 
					        '%s Documentation' % project,
 | 
				
			||||||
 | 
					        'OpenStack LLC',
 | 
				
			||||||
 | 
					        'manual'
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Example configuration for intersphinx: refer to the Python standard library.
 | 
				
			||||||
 | 
					intersphinx_mapping = {'http://docs.python.org/': None}
 | 
				
			||||||
							
								
								
									
										29
									
								
								doc/source/index.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								doc/source/index.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					..
 | 
				
			||||||
 | 
					      Copyright 2014 Mirantis 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Welcome to Rally Client's documentation!
 | 
				
			||||||
 | 
					==========================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Rally Client is a Python client library and command line tool for Rally
 | 
				
			||||||
 | 
					REST API.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Indices and tables
 | 
				
			||||||
 | 
					==================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* :ref:`genindex`
 | 
				
			||||||
 | 
					* :ref:`modindex`
 | 
				
			||||||
 | 
					* :ref:`search`
 | 
				
			||||||
							
								
								
									
										10
									
								
								openstack-common.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								openstack-common.conf
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					[DEFAULT]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# The list of modules to copy from oslo-incubator.git
 | 
				
			||||||
 | 
					module=apiclient
 | 
				
			||||||
 | 
					module=cliutils
 | 
				
			||||||
 | 
					module=importutils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# The base module to hold the copy of openstack.common
 | 
				
			||||||
 | 
					base=rallyclient
 | 
				
			||||||
							
								
								
									
										21
									
								
								rallyclient/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								rallyclient/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					#   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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__all__ = ['__version__']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pbr.version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					version_info = pbr.version.VersionInfo('python-rallyclient')
 | 
				
			||||||
 | 
					try:
 | 
				
			||||||
 | 
					    __version__ = version_info.version_string()
 | 
				
			||||||
 | 
					except AttributeError:
 | 
				
			||||||
 | 
					    __version__ = None
 | 
				
			||||||
							
								
								
									
										0
									
								
								rallyclient/openstack/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								rallyclient/openstack/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										17
									
								
								rallyclient/openstack/common/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								rallyclient/openstack/common/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					#
 | 
				
			||||||
 | 
					#    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 six
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox'))
 | 
				
			||||||
							
								
								
									
										0
									
								
								rallyclient/openstack/common/apiclient/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								rallyclient/openstack/common/apiclient/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										221
									
								
								rallyclient/openstack/common/apiclient/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								rallyclient/openstack/common/apiclient/auth.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,221 @@
 | 
				
			|||||||
 | 
					# Copyright 2013 OpenStack Foundation
 | 
				
			||||||
 | 
					# Copyright 2013 Spanish National Research Council.
 | 
				
			||||||
 | 
					# 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# E0202: An attribute inherited from %s hide this method
 | 
				
			||||||
 | 
					# pylint: disable=E0202
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import abc
 | 
				
			||||||
 | 
					import argparse
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import six
 | 
				
			||||||
 | 
					from stevedore import extension
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rallyclient.openstack.common.apiclient import exceptions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_discovered_plugins = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def discover_auth_systems():
 | 
				
			||||||
 | 
					    """Discover the available auth-systems.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This won't take into account the old style auth-systems.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    global _discovered_plugins
 | 
				
			||||||
 | 
					    _discovered_plugins = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_plugin(ext):
 | 
				
			||||||
 | 
					        _discovered_plugins[ext.name] = ext.plugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ep_namespace = "rallyclient.openstack.common.apiclient.auth"
 | 
				
			||||||
 | 
					    mgr = extension.ExtensionManager(ep_namespace)
 | 
				
			||||||
 | 
					    mgr.map(add_plugin)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def load_auth_system_opts(parser):
 | 
				
			||||||
 | 
					    """Load options needed by the available auth-systems into a parser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This function will try to populate the parser with options from the
 | 
				
			||||||
 | 
					    available plugins.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    group = parser.add_argument_group("Common auth options")
 | 
				
			||||||
 | 
					    BaseAuthPlugin.add_common_opts(group)
 | 
				
			||||||
 | 
					    for name, auth_plugin in six.iteritems(_discovered_plugins):
 | 
				
			||||||
 | 
					        group = parser.add_argument_group(
 | 
				
			||||||
 | 
					            "Auth-system '%s' options" % name,
 | 
				
			||||||
 | 
					            conflict_handler="resolve")
 | 
				
			||||||
 | 
					        auth_plugin.add_opts(group)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def load_plugin(auth_system):
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        plugin_class = _discovered_plugins[auth_system]
 | 
				
			||||||
 | 
					    except KeyError:
 | 
				
			||||||
 | 
					        raise exceptions.AuthSystemNotFound(auth_system)
 | 
				
			||||||
 | 
					    return plugin_class(auth_system=auth_system)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def load_plugin_from_args(args):
 | 
				
			||||||
 | 
					    """Load required plugin and populate it with options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Try to guess auth system if it is not specified. Systems are tried in
 | 
				
			||||||
 | 
					    alphabetical order.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :type args: argparse.Namespace
 | 
				
			||||||
 | 
					    :raises: AuthPluginOptionsMissing
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    auth_system = args.os_auth_system
 | 
				
			||||||
 | 
					    if auth_system:
 | 
				
			||||||
 | 
					        plugin = load_plugin(auth_system)
 | 
				
			||||||
 | 
					        plugin.parse_opts(args)
 | 
				
			||||||
 | 
					        plugin.sufficient_options()
 | 
				
			||||||
 | 
					        return plugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
 | 
				
			||||||
 | 
					        plugin_class = _discovered_plugins[plugin_auth_system]
 | 
				
			||||||
 | 
					        plugin = plugin_class()
 | 
				
			||||||
 | 
					        plugin.parse_opts(args)
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            plugin.sufficient_options()
 | 
				
			||||||
 | 
					        except exceptions.AuthPluginOptionsMissing:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        return plugin
 | 
				
			||||||
 | 
					    raise exceptions.AuthPluginOptionsMissing(["auth_system"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@six.add_metaclass(abc.ABCMeta)
 | 
				
			||||||
 | 
					class BaseAuthPlugin(object):
 | 
				
			||||||
 | 
					    """Base class for authentication plugins.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    An authentication plugin needs to override at least the authenticate
 | 
				
			||||||
 | 
					    method to be a valid plugin.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    auth_system = None
 | 
				
			||||||
 | 
					    opt_names = []
 | 
				
			||||||
 | 
					    common_opt_names = [
 | 
				
			||||||
 | 
					        "auth_system",
 | 
				
			||||||
 | 
					        "username",
 | 
				
			||||||
 | 
					        "password",
 | 
				
			||||||
 | 
					        "tenant_name",
 | 
				
			||||||
 | 
					        "token",
 | 
				
			||||||
 | 
					        "auth_url",
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, auth_system=None, **kwargs):
 | 
				
			||||||
 | 
					        self.auth_system = auth_system or self.auth_system
 | 
				
			||||||
 | 
					        self.opts = dict((name, kwargs.get(name))
 | 
				
			||||||
 | 
					                         for name in self.opt_names)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def _parser_add_opt(parser, opt):
 | 
				
			||||||
 | 
					        """Add an option to parser in two variants.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param opt: option name (with underscores)
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        dashed_opt = opt.replace("_", "-")
 | 
				
			||||||
 | 
					        env_var = "OS_%s" % opt.upper()
 | 
				
			||||||
 | 
					        arg_default = os.environ.get(env_var, "")
 | 
				
			||||||
 | 
					        arg_help = "Defaults to env[%s]." % env_var
 | 
				
			||||||
 | 
					        parser.add_argument(
 | 
				
			||||||
 | 
					            "--os-%s" % dashed_opt,
 | 
				
			||||||
 | 
					            metavar="<%s>" % dashed_opt,
 | 
				
			||||||
 | 
					            default=arg_default,
 | 
				
			||||||
 | 
					            help=arg_help)
 | 
				
			||||||
 | 
					        parser.add_argument(
 | 
				
			||||||
 | 
					            "--os_%s" % opt,
 | 
				
			||||||
 | 
					            metavar="<%s>" % dashed_opt,
 | 
				
			||||||
 | 
					            help=argparse.SUPPRESS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def add_opts(cls, parser):
 | 
				
			||||||
 | 
					        """Populate the parser with the options for this plugin.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        for opt in cls.opt_names:
 | 
				
			||||||
 | 
					            # use `BaseAuthPlugin.common_opt_names` since it is never
 | 
				
			||||||
 | 
					            # changed in child classes
 | 
				
			||||||
 | 
					            if opt not in BaseAuthPlugin.common_opt_names:
 | 
				
			||||||
 | 
					                cls._parser_add_opt(parser, opt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def add_common_opts(cls, parser):
 | 
				
			||||||
 | 
					        """Add options that are common for several plugins.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        for opt in cls.common_opt_names:
 | 
				
			||||||
 | 
					            cls._parser_add_opt(parser, opt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def get_opt(opt_name, args):
 | 
				
			||||||
 | 
					        """Return option name and value.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param opt_name: name of the option, e.g., "username"
 | 
				
			||||||
 | 
					        :param args: parsed arguments
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return (opt_name, getattr(args, "os_%s" % opt_name, None))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def parse_opts(self, args):
 | 
				
			||||||
 | 
					        """Parse the actual auth-system options if any.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This method is expected to populate the attribute `self.opts` with a
 | 
				
			||||||
 | 
					        dict containing the options and values needed to make authentication.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.opts.update(dict(self.get_opt(opt_name, args)
 | 
				
			||||||
 | 
					                              for opt_name in self.opt_names))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def authenticate(self, http_client):
 | 
				
			||||||
 | 
					        """Authenticate using plugin defined method.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The method usually analyses `self.opts` and performs
 | 
				
			||||||
 | 
					        a request to authentication server.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param http_client: client object that needs authentication
 | 
				
			||||||
 | 
					        :type http_client: HTTPClient
 | 
				
			||||||
 | 
					        :raises: AuthorizationFailure
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.sufficient_options()
 | 
				
			||||||
 | 
					        self._do_authenticate(http_client)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abc.abstractmethod
 | 
				
			||||||
 | 
					    def _do_authenticate(self, http_client):
 | 
				
			||||||
 | 
					        """Protected method for authentication.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def sufficient_options(self):
 | 
				
			||||||
 | 
					        """Check if all required options are present.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :raises: AuthPluginOptionsMissing
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        missing = [opt
 | 
				
			||||||
 | 
					                   for opt in self.opt_names
 | 
				
			||||||
 | 
					                   if not self.opts.get(opt)]
 | 
				
			||||||
 | 
					        if missing:
 | 
				
			||||||
 | 
					            raise exceptions.AuthPluginOptionsMissing(missing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abc.abstractmethod
 | 
				
			||||||
 | 
					    def token_and_endpoint(self, endpoint_type, service_type):
 | 
				
			||||||
 | 
					        """Return token and endpoint.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param service_type: Service type of the endpoint
 | 
				
			||||||
 | 
					        :type service_type: string
 | 
				
			||||||
 | 
					        :param endpoint_type: Type of endpoint.
 | 
				
			||||||
 | 
					                              Possible values: public or publicURL,
 | 
				
			||||||
 | 
					                              internal or internalURL,
 | 
				
			||||||
 | 
					                              admin or adminURL
 | 
				
			||||||
 | 
					        :type endpoint_type: string
 | 
				
			||||||
 | 
					        :returns: tuple of token and endpoint strings
 | 
				
			||||||
 | 
					        :raises: EndpointException
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
							
								
								
									
										525
									
								
								rallyclient/openstack/common/apiclient/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										525
									
								
								rallyclient/openstack/common/apiclient/base.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,525 @@
 | 
				
			|||||||
 | 
					# Copyright 2010 Jacob Kaplan-Moss
 | 
				
			||||||
 | 
					# Copyright 2011 OpenStack Foundation
 | 
				
			||||||
 | 
					# Copyright 2012 Grid Dynamics
 | 
				
			||||||
 | 
					# Copyright 2013 OpenStack Foundation
 | 
				
			||||||
 | 
					# 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.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# E1102: %s is not callable
 | 
				
			||||||
 | 
					# pylint: disable=E1102
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import abc
 | 
				
			||||||
 | 
					import copy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import six
 | 
				
			||||||
 | 
					from six.moves.urllib import parse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rallyclient.openstack.common.apiclient import exceptions
 | 
				
			||||||
 | 
					from rallyclient.openstack.common.gettextutils import _
 | 
				
			||||||
 | 
					from rallyclient.openstack.common import strutils
 | 
				
			||||||
 | 
					from rallyclient.openstack.common import uuidutils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def getid(obj):
 | 
				
			||||||
 | 
					    """Return id if argument is a Resource.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Abstracts the common pattern of allowing both an object or an object's ID
 | 
				
			||||||
 | 
					    (UUID) as a parameter when dealing with relationships.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        if obj.uuid:
 | 
				
			||||||
 | 
					            return obj.uuid
 | 
				
			||||||
 | 
					    except AttributeError:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return obj.id
 | 
				
			||||||
 | 
					    except AttributeError:
 | 
				
			||||||
 | 
					        return obj
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO(aababilov): call run_hooks() in HookableMixin's child classes
 | 
				
			||||||
 | 
					class HookableMixin(object):
 | 
				
			||||||
 | 
					    """Mixin so classes can register and run hooks."""
 | 
				
			||||||
 | 
					    _hooks_map = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def add_hook(cls, hook_type, hook_func):
 | 
				
			||||||
 | 
					        """Add a new hook of specified type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param cls: class that registers hooks
 | 
				
			||||||
 | 
					        :param hook_type: hook type, e.g., '__pre_parse_args__'
 | 
				
			||||||
 | 
					        :param hook_func: hook function
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        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):
 | 
				
			||||||
 | 
					        """Run all hooks of specified type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param cls: class that registers hooks
 | 
				
			||||||
 | 
					        :param hook_type: hook type, e.g., '__pre_parse_args__'
 | 
				
			||||||
 | 
					        :param args: args to be passed to every hook function
 | 
				
			||||||
 | 
					        :param kwargs: kwargs to be passed to every hook function
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        hook_funcs = cls._hooks_map.get(hook_type) or []
 | 
				
			||||||
 | 
					        for hook_func in hook_funcs:
 | 
				
			||||||
 | 
					            hook_func(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BaseManager(HookableMixin):
 | 
				
			||||||
 | 
					    """Basic manager type providing common operations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Managers interact with a particular type of API (servers, flavors, images,
 | 
				
			||||||
 | 
					    etc.) and provide CRUD operations for them.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    resource_class = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, client):
 | 
				
			||||||
 | 
					        """Initializes BaseManager with `client`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param client: instance of BaseClient descendant for HTTP requests
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        super(BaseManager, self).__init__()
 | 
				
			||||||
 | 
					        self.client = client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _list(self, url, response_key, obj_class=None, json=None):
 | 
				
			||||||
 | 
					        """List the collection.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param url: a partial URL, e.g., '/servers'
 | 
				
			||||||
 | 
					        :param response_key: the key to be looked up in response dictionary,
 | 
				
			||||||
 | 
					            e.g., 'servers'
 | 
				
			||||||
 | 
					        :param obj_class: class for constructing the returned objects
 | 
				
			||||||
 | 
					            (self.resource_class will be used by default)
 | 
				
			||||||
 | 
					        :param json: data that will be encoded as JSON and passed in POST
 | 
				
			||||||
 | 
					            request (GET will be sent by default)
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if json:
 | 
				
			||||||
 | 
					            body = self.client.post(url, json=json).json()
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            body = self.client.get(url).json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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...
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            data = data['values']
 | 
				
			||||||
 | 
					        except (KeyError, TypeError):
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [obj_class(self, res, loaded=True) for res in data if res]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get(self, url, response_key):
 | 
				
			||||||
 | 
					        """Get an object from collection.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param url: a partial URL, e.g., '/servers'
 | 
				
			||||||
 | 
					        :param response_key: the key to be looked up in response dictionary,
 | 
				
			||||||
 | 
					            e.g., 'server'
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        body = self.client.get(url).json()
 | 
				
			||||||
 | 
					        return self.resource_class(self, body[response_key], loaded=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _head(self, url):
 | 
				
			||||||
 | 
					        """Retrieve request headers for an object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param url: a partial URL, e.g., '/servers'
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        resp = self.client.head(url)
 | 
				
			||||||
 | 
					        return resp.status_code == 204
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _post(self, url, json, response_key, return_raw=False):
 | 
				
			||||||
 | 
					        """Create an object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param url: a partial URL, e.g., '/servers'
 | 
				
			||||||
 | 
					        :param json: data that will be encoded as JSON and passed in POST
 | 
				
			||||||
 | 
					            request (GET will be sent by default)
 | 
				
			||||||
 | 
					        :param response_key: the key to be looked up in response dictionary,
 | 
				
			||||||
 | 
					            e.g., 'servers'
 | 
				
			||||||
 | 
					        :param return_raw: flag to force returning raw JSON instead of
 | 
				
			||||||
 | 
					            Python object of self.resource_class
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        body = self.client.post(url, json=json).json()
 | 
				
			||||||
 | 
					        if return_raw:
 | 
				
			||||||
 | 
					            return body[response_key]
 | 
				
			||||||
 | 
					        return self.resource_class(self, body[response_key])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _put(self, url, json=None, response_key=None):
 | 
				
			||||||
 | 
					        """Update an object with PUT method.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param url: a partial URL, e.g., '/servers'
 | 
				
			||||||
 | 
					        :param json: data that will be encoded as JSON and passed in POST
 | 
				
			||||||
 | 
					            request (GET will be sent by default)
 | 
				
			||||||
 | 
					        :param response_key: the key to be looked up in response dictionary,
 | 
				
			||||||
 | 
					            e.g., 'servers'
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        resp = self.client.put(url, json=json)
 | 
				
			||||||
 | 
					        # PUT requests may not return a body
 | 
				
			||||||
 | 
					        if resp.content:
 | 
				
			||||||
 | 
					            body = resp.json()
 | 
				
			||||||
 | 
					            if response_key is not None:
 | 
				
			||||||
 | 
					                return self.resource_class(self, body[response_key])
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                return self.resource_class(self, body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _patch(self, url, json=None, response_key=None):
 | 
				
			||||||
 | 
					        """Update an object with PATCH method.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param url: a partial URL, e.g., '/servers'
 | 
				
			||||||
 | 
					        :param json: data that will be encoded as JSON and passed in POST
 | 
				
			||||||
 | 
					            request (GET will be sent by default)
 | 
				
			||||||
 | 
					        :param response_key: the key to be looked up in response dictionary,
 | 
				
			||||||
 | 
					            e.g., 'servers'
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        body = self.client.patch(url, json=json).json()
 | 
				
			||||||
 | 
					        if response_key is not None:
 | 
				
			||||||
 | 
					            return self.resource_class(self, body[response_key])
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return self.resource_class(self, body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _delete(self, url):
 | 
				
			||||||
 | 
					        """Delete an object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param url: a partial URL, e.g., '/servers/my-server'
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.client.delete(url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@six.add_metaclass(abc.ABCMeta)
 | 
				
			||||||
 | 
					class ManagerWithFind(BaseManager):
 | 
				
			||||||
 | 
					    """Manager with additional `find()`/`findall()` methods."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @abc.abstractmethod
 | 
				
			||||||
 | 
					    def list(self):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 %(name)s matching %(args)s.") % {
 | 
				
			||||||
 | 
					                'name': self.resource_class.__name__,
 | 
				
			||||||
 | 
					                'args': kwargs
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            raise exceptions.NotFound(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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CrudManager(BaseManager):
 | 
				
			||||||
 | 
					    """Base manager class for manipulating entities.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Children of this class are expected to define a `collection_key` and `key`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - `collection_key`: Usually a plural noun by convention (e.g. `entities`);
 | 
				
			||||||
 | 
					      used to refer collections in both URL's (e.g.  `/v3/entities`) and JSON
 | 
				
			||||||
 | 
					      objects containing a list of member resources (e.g. `{'entities': [{},
 | 
				
			||||||
 | 
					      {}, {}]}`).
 | 
				
			||||||
 | 
					    - `key`: Usually a singular noun by convention (e.g. `entity`); used to
 | 
				
			||||||
 | 
					      refer to an individual member of the collection.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    collection_key = None
 | 
				
			||||||
 | 
					    key = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def build_url(self, base_url=None, **kwargs):
 | 
				
			||||||
 | 
					        """Builds a resource URL for the given kwargs.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Given an example collection where `collection_key = 'entities'` and
 | 
				
			||||||
 | 
					        `key = 'entity'`, the following URL's could be generated.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        By default, the URL will represent a collection of entities, e.g.::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /entities
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        If kwargs contains an `entity_id`, then the URL will represent a
 | 
				
			||||||
 | 
					        specific member, e.g.::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /entities/{entity_id}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param base_url: if provided, the generated URL will be appended to it
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        url = base_url if base_url is not None else ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        url += '/%s' % self.collection_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # do we have a specific entity?
 | 
				
			||||||
 | 
					        entity_id = kwargs.get('%s_id' % self.key)
 | 
				
			||||||
 | 
					        if entity_id is not None:
 | 
				
			||||||
 | 
					            url += '/%s' % entity_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _filter_kwargs(self, kwargs):
 | 
				
			||||||
 | 
					        """Drop null values and handle ids."""
 | 
				
			||||||
 | 
					        for key, ref in six.iteritems(kwargs.copy()):
 | 
				
			||||||
 | 
					            if ref is None:
 | 
				
			||||||
 | 
					                kwargs.pop(key)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                if isinstance(ref, Resource):
 | 
				
			||||||
 | 
					                    kwargs.pop(key)
 | 
				
			||||||
 | 
					                    kwargs['%s_id' % key] = getid(ref)
 | 
				
			||||||
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def create(self, **kwargs):
 | 
				
			||||||
 | 
					        kwargs = self._filter_kwargs(kwargs)
 | 
				
			||||||
 | 
					        return self._post(
 | 
				
			||||||
 | 
					            self.build_url(**kwargs),
 | 
				
			||||||
 | 
					            {self.key: kwargs},
 | 
				
			||||||
 | 
					            self.key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, **kwargs):
 | 
				
			||||||
 | 
					        kwargs = self._filter_kwargs(kwargs)
 | 
				
			||||||
 | 
					        return self._get(
 | 
				
			||||||
 | 
					            self.build_url(**kwargs),
 | 
				
			||||||
 | 
					            self.key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def head(self, **kwargs):
 | 
				
			||||||
 | 
					        kwargs = self._filter_kwargs(kwargs)
 | 
				
			||||||
 | 
					        return self._head(self.build_url(**kwargs))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def list(self, base_url=None, **kwargs):
 | 
				
			||||||
 | 
					        """List the collection.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param base_url: if provided, the generated URL will be appended to it
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        kwargs = self._filter_kwargs(kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self._list(
 | 
				
			||||||
 | 
					            '%(base_url)s%(query)s' % {
 | 
				
			||||||
 | 
					                'base_url': self.build_url(base_url=base_url, **kwargs),
 | 
				
			||||||
 | 
					                'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            self.collection_key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def put(self, base_url=None, **kwargs):
 | 
				
			||||||
 | 
					        """Update an element.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param base_url: if provided, the generated URL will be appended to it
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        kwargs = self._filter_kwargs(kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self._put(self.build_url(base_url=base_url, **kwargs))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def update(self, **kwargs):
 | 
				
			||||||
 | 
					        kwargs = self._filter_kwargs(kwargs)
 | 
				
			||||||
 | 
					        params = kwargs.copy()
 | 
				
			||||||
 | 
					        params.pop('%s_id' % self.key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self._patch(
 | 
				
			||||||
 | 
					            self.build_url(**kwargs),
 | 
				
			||||||
 | 
					            {self.key: params},
 | 
				
			||||||
 | 
					            self.key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete(self, **kwargs):
 | 
				
			||||||
 | 
					        kwargs = self._filter_kwargs(kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self._delete(
 | 
				
			||||||
 | 
					            self.build_url(**kwargs))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def find(self, base_url=None, **kwargs):
 | 
				
			||||||
 | 
					        """Find a single item with attributes matching ``**kwargs``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param base_url: if provided, the generated URL will be appended to it
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        kwargs = self._filter_kwargs(kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        rl = self._list(
 | 
				
			||||||
 | 
					            '%(base_url)s%(query)s' % {
 | 
				
			||||||
 | 
					                'base_url': self.build_url(base_url=base_url, **kwargs),
 | 
				
			||||||
 | 
					                'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            self.collection_key)
 | 
				
			||||||
 | 
					        num = len(rl)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if num == 0:
 | 
				
			||||||
 | 
					            msg = _("No %(name)s matching %(args)s.") % {
 | 
				
			||||||
 | 
					                'name': self.resource_class.__name__,
 | 
				
			||||||
 | 
					                'args': kwargs
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            raise exceptions.NotFound(404, msg)
 | 
				
			||||||
 | 
					        elif num > 1:
 | 
				
			||||||
 | 
					            raise exceptions.NoUniqueMatch
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return rl[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Extension(HookableMixin):
 | 
				
			||||||
 | 
					    """Extension descriptor."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
 | 
				
			||||||
 | 
					    manager_class = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, name, module):
 | 
				
			||||||
 | 
					        super(Extension, self).__init__()
 | 
				
			||||||
 | 
					        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)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    if issubclass(attr_value, BaseManager):
 | 
				
			||||||
 | 
					                        self.manager_class = attr_value
 | 
				
			||||||
 | 
					                except TypeError:
 | 
				
			||||||
 | 
					                    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self):
 | 
				
			||||||
 | 
					        return "<Extension '%s'>" % self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Resource(object):
 | 
				
			||||||
 | 
					    """Base class for OpenStack resources (tenant, user, etc.).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This is pretty much just a bag for attributes.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    HUMAN_ID = False
 | 
				
			||||||
 | 
					    NAME_ATTR = 'name'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, manager, info, loaded=False):
 | 
				
			||||||
 | 
					        """Populate and bind to a manager.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param manager: BaseManager object
 | 
				
			||||||
 | 
					        :param info: dictionary representing resource attributes
 | 
				
			||||||
 | 
					        :param loaded: prevent lazy-loading if set to True
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.manager = manager
 | 
				
			||||||
 | 
					        self._info = info
 | 
				
			||||||
 | 
					        self._add_details(info)
 | 
				
			||||||
 | 
					        self._loaded = loaded
 | 
				
			||||||
 | 
					        self._init_completion_cache()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _init_completion_cache(self):
 | 
				
			||||||
 | 
					        cache_write = getattr(self.manager, 'write_to_completion_cache', None)
 | 
				
			||||||
 | 
					        if not cache_write:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # 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 uuidutils.is_uuid_like(self.id):
 | 
				
			||||||
 | 
					            cache_write('uuid', self.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.human_id:
 | 
				
			||||||
 | 
					            cache_write('human_id', self.human_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def human_id(self):
 | 
				
			||||||
 | 
					        """Human-readable ID which can be used for bash completion.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.HUMAN_ID:
 | 
				
			||||||
 | 
					            name = getattr(self, self.NAME_ATTR, None)
 | 
				
			||||||
 | 
					            if name is not None:
 | 
				
			||||||
 | 
					                return strutils.to_slug(name)
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _add_details(self, info):
 | 
				
			||||||
 | 
					        for (k, v) in six.iteritems(info):
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                setattr(self, k, v)
 | 
				
			||||||
 | 
					                self._info[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 get(self):
 | 
				
			||||||
 | 
					        """Support for lazy loading details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Some clients, such as novaclient have the option to lazy load the
 | 
				
			||||||
 | 
					        details, details which can be loaded with this function.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        # 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, Resource):
 | 
				
			||||||
 | 
					            return NotImplemented
 | 
				
			||||||
 | 
					        # two resources of different types are not equal
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_dict(self):
 | 
				
			||||||
 | 
					        return copy.deepcopy(self._info)
 | 
				
			||||||
							
								
								
									
										364
									
								
								rallyclient/openstack/common/apiclient/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										364
									
								
								rallyclient/openstack/common/apiclient/client.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,364 @@
 | 
				
			|||||||
 | 
					# Copyright 2010 Jacob Kaplan-Moss
 | 
				
			||||||
 | 
					# Copyright 2011 OpenStack Foundation
 | 
				
			||||||
 | 
					# Copyright 2011 Piston Cloud Computing, Inc.
 | 
				
			||||||
 | 
					# Copyright 2013 Alessio Ababilov
 | 
				
			||||||
 | 
					# Copyright 2013 Grid Dynamics
 | 
				
			||||||
 | 
					# Copyright 2013 OpenStack Foundation
 | 
				
			||||||
 | 
					# 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					OpenStack Client interface. Handles the REST calls and responses.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# E0202: An attribute inherited from %s hide this method
 | 
				
			||||||
 | 
					# pylint: disable=E0202
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try:
 | 
				
			||||||
 | 
					    import simplejson as json
 | 
				
			||||||
 | 
					except ImportError:
 | 
				
			||||||
 | 
					    import json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rallyclient.openstack.common.apiclient import exceptions
 | 
				
			||||||
 | 
					from rallyclient.openstack.common.gettextutils import _
 | 
				
			||||||
 | 
					from rallyclient.openstack.common import importutils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HTTPClient(object):
 | 
				
			||||||
 | 
					    """This client handles sending HTTP requests to OpenStack servers.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Features:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - share authentication information between several clients to different
 | 
				
			||||||
 | 
					      services (e.g., for compute and image clients);
 | 
				
			||||||
 | 
					    - reissue authentication request for expired tokens;
 | 
				
			||||||
 | 
					    - encode/decode JSON bodies;
 | 
				
			||||||
 | 
					    - raise exceptions on HTTP errors;
 | 
				
			||||||
 | 
					    - pluggable authentication;
 | 
				
			||||||
 | 
					    - store authentication information in a keyring;
 | 
				
			||||||
 | 
					    - store time spent for requests;
 | 
				
			||||||
 | 
					    - register clients for particular services, so one can use
 | 
				
			||||||
 | 
					      `http_client.identity` or `http_client.compute`;
 | 
				
			||||||
 | 
					    - log requests and responses in a format that is easy to copy-and-paste
 | 
				
			||||||
 | 
					      into terminal and send the same request with curl.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    user_agent = "rallyclient.openstack.common.apiclient"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self,
 | 
				
			||||||
 | 
					                 auth_plugin,
 | 
				
			||||||
 | 
					                 region_name=None,
 | 
				
			||||||
 | 
					                 endpoint_type="publicURL",
 | 
				
			||||||
 | 
					                 original_ip=None,
 | 
				
			||||||
 | 
					                 verify=True,
 | 
				
			||||||
 | 
					                 cert=None,
 | 
				
			||||||
 | 
					                 timeout=None,
 | 
				
			||||||
 | 
					                 timings=False,
 | 
				
			||||||
 | 
					                 keyring_saver=None,
 | 
				
			||||||
 | 
					                 debug=False,
 | 
				
			||||||
 | 
					                 user_agent=None,
 | 
				
			||||||
 | 
					                 http=None):
 | 
				
			||||||
 | 
					        self.auth_plugin = auth_plugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.endpoint_type = endpoint_type
 | 
				
			||||||
 | 
					        self.region_name = region_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.original_ip = original_ip
 | 
				
			||||||
 | 
					        self.timeout = timeout
 | 
				
			||||||
 | 
					        self.verify = verify
 | 
				
			||||||
 | 
					        self.cert = cert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.keyring_saver = keyring_saver
 | 
				
			||||||
 | 
					        self.debug = debug
 | 
				
			||||||
 | 
					        self.user_agent = user_agent or self.user_agent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.times = []  # [("item", starttime, endtime), ...]
 | 
				
			||||||
 | 
					        self.timings = timings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # requests within the same session can reuse TCP connections from pool
 | 
				
			||||||
 | 
					        self.http = http or requests.Session()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.cached_token = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _http_log_req(self, method, url, kwargs):
 | 
				
			||||||
 | 
					        if not self.debug:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        string_parts = [
 | 
				
			||||||
 | 
					            "curl -i",
 | 
				
			||||||
 | 
					            "-X '%s'" % method,
 | 
				
			||||||
 | 
					            "'%s'" % url,
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for element in kwargs['headers']:
 | 
				
			||||||
 | 
					            header = "-H '%s: %s'" % (element, kwargs['headers'][element])
 | 
				
			||||||
 | 
					            string_parts.append(header)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _logger.debug("REQ: %s" % " ".join(string_parts))
 | 
				
			||||||
 | 
					        if 'data' in kwargs:
 | 
				
			||||||
 | 
					            _logger.debug("REQ BODY: %s\n" % (kwargs['data']))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _http_log_resp(self, resp):
 | 
				
			||||||
 | 
					        if not self.debug:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        _logger.debug(
 | 
				
			||||||
 | 
					            "RESP: [%s] %s\n",
 | 
				
			||||||
 | 
					            resp.status_code,
 | 
				
			||||||
 | 
					            resp.headers)
 | 
				
			||||||
 | 
					        if resp._content_consumed:
 | 
				
			||||||
 | 
					            _logger.debug(
 | 
				
			||||||
 | 
					                "RESP BODY: %s\n",
 | 
				
			||||||
 | 
					                resp.text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def serialize(self, kwargs):
 | 
				
			||||||
 | 
					        if kwargs.get('json') is not None:
 | 
				
			||||||
 | 
					            kwargs['headers']['Content-Type'] = 'application/json'
 | 
				
			||||||
 | 
					            kwargs['data'] = json.dumps(kwargs['json'])
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            del kwargs['json']
 | 
				
			||||||
 | 
					        except KeyError:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_timings(self):
 | 
				
			||||||
 | 
					        return self.times
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def reset_timings(self):
 | 
				
			||||||
 | 
					        self.times = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def request(self, method, url, **kwargs):
 | 
				
			||||||
 | 
					        """Send an http request with the specified characteristics.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Wrapper around `requests.Session.request` to handle tasks such as
 | 
				
			||||||
 | 
					        setting headers, JSON encoding/decoding, and error handling.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param method: method of HTTP request
 | 
				
			||||||
 | 
					        :param url: URL of HTTP request
 | 
				
			||||||
 | 
					        :param kwargs: any other parameter that can be passed to
 | 
				
			||||||
 | 
					             requests.Session.request (such as `headers`) or `json`
 | 
				
			||||||
 | 
					             that will be encoded as JSON and used as `data` argument
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        kwargs.setdefault("headers", kwargs.get("headers", {}))
 | 
				
			||||||
 | 
					        kwargs["headers"]["User-Agent"] = self.user_agent
 | 
				
			||||||
 | 
					        if self.original_ip:
 | 
				
			||||||
 | 
					            kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
 | 
				
			||||||
 | 
					                self.original_ip, self.user_agent)
 | 
				
			||||||
 | 
					        if self.timeout is not None:
 | 
				
			||||||
 | 
					            kwargs.setdefault("timeout", self.timeout)
 | 
				
			||||||
 | 
					        kwargs.setdefault("verify", self.verify)
 | 
				
			||||||
 | 
					        if self.cert is not None:
 | 
				
			||||||
 | 
					            kwargs.setdefault("cert", self.cert)
 | 
				
			||||||
 | 
					        self.serialize(kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self._http_log_req(method, url, kwargs)
 | 
				
			||||||
 | 
					        if self.timings:
 | 
				
			||||||
 | 
					            start_time = time.time()
 | 
				
			||||||
 | 
					        resp = self.http.request(method, url, **kwargs)
 | 
				
			||||||
 | 
					        if self.timings:
 | 
				
			||||||
 | 
					            self.times.append(("%s %s" % (method, url),
 | 
				
			||||||
 | 
					                               start_time, time.time()))
 | 
				
			||||||
 | 
					        self._http_log_resp(resp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if resp.status_code >= 400:
 | 
				
			||||||
 | 
					            _logger.debug(
 | 
				
			||||||
 | 
					                "Request returned failure status: %s",
 | 
				
			||||||
 | 
					                resp.status_code)
 | 
				
			||||||
 | 
					            raise exceptions.from_response(resp, method, url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return resp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def concat_url(endpoint, url):
 | 
				
			||||||
 | 
					        """Concatenate endpoint and final URL.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
 | 
				
			||||||
 | 
					        "http://keystone/v2.0/tokens".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param endpoint: the base URL
 | 
				
			||||||
 | 
					        :param url: the final URL
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def client_request(self, client, method, url, **kwargs):
 | 
				
			||||||
 | 
					        """Send an http request using `client`'s endpoint and specified `url`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        If request was rejected as unauthorized (possibly because the token is
 | 
				
			||||||
 | 
					        expired), issue one authorization attempt and send the request once
 | 
				
			||||||
 | 
					        again.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param client: instance of BaseClient descendant
 | 
				
			||||||
 | 
					        :param method: method of HTTP request
 | 
				
			||||||
 | 
					        :param url: URL of HTTP request
 | 
				
			||||||
 | 
					        :param kwargs: any other parameter that can be passed to
 | 
				
			||||||
 | 
					            `HTTPClient.request`
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        filter_args = {
 | 
				
			||||||
 | 
					            "endpoint_type": client.endpoint_type or self.endpoint_type,
 | 
				
			||||||
 | 
					            "service_type": client.service_type,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        token, endpoint = (self.cached_token, client.cached_endpoint)
 | 
				
			||||||
 | 
					        just_authenticated = False
 | 
				
			||||||
 | 
					        if not (token and endpoint):
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                token, endpoint = self.auth_plugin.token_and_endpoint(
 | 
				
			||||||
 | 
					                    **filter_args)
 | 
				
			||||||
 | 
					            except exceptions.EndpointException:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					            if not (token and endpoint):
 | 
				
			||||||
 | 
					                self.authenticate()
 | 
				
			||||||
 | 
					                just_authenticated = True
 | 
				
			||||||
 | 
					                token, endpoint = self.auth_plugin.token_and_endpoint(
 | 
				
			||||||
 | 
					                    **filter_args)
 | 
				
			||||||
 | 
					                if not (token and endpoint):
 | 
				
			||||||
 | 
					                    raise exceptions.AuthorizationFailure(
 | 
				
			||||||
 | 
					                        _("Cannot find endpoint or token for request"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        old_token_endpoint = (token, endpoint)
 | 
				
			||||||
 | 
					        kwargs.setdefault("headers", {})["X-Auth-Token"] = token
 | 
				
			||||||
 | 
					        self.cached_token = token
 | 
				
			||||||
 | 
					        client.cached_endpoint = endpoint
 | 
				
			||||||
 | 
					        # Perform the request once. If we get Unauthorized, then it
 | 
				
			||||||
 | 
					        # might be because the auth token expired, so try to
 | 
				
			||||||
 | 
					        # re-authenticate and try again. If it still fails, bail.
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return self.request(
 | 
				
			||||||
 | 
					                method, self.concat_url(endpoint, url), **kwargs)
 | 
				
			||||||
 | 
					        except exceptions.Unauthorized as unauth_ex:
 | 
				
			||||||
 | 
					            if just_authenticated:
 | 
				
			||||||
 | 
					                raise
 | 
				
			||||||
 | 
					            self.cached_token = None
 | 
				
			||||||
 | 
					            client.cached_endpoint = None
 | 
				
			||||||
 | 
					            self.authenticate()
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                token, endpoint = self.auth_plugin.token_and_endpoint(
 | 
				
			||||||
 | 
					                    **filter_args)
 | 
				
			||||||
 | 
					            except exceptions.EndpointException:
 | 
				
			||||||
 | 
					                raise unauth_ex
 | 
				
			||||||
 | 
					            if (not (token and endpoint) or
 | 
				
			||||||
 | 
					                    old_token_endpoint == (token, endpoint)):
 | 
				
			||||||
 | 
					                raise unauth_ex
 | 
				
			||||||
 | 
					            self.cached_token = token
 | 
				
			||||||
 | 
					            client.cached_endpoint = endpoint
 | 
				
			||||||
 | 
					            kwargs["headers"]["X-Auth-Token"] = token
 | 
				
			||||||
 | 
					            return self.request(
 | 
				
			||||||
 | 
					                method, self.concat_url(endpoint, url), **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_client(self, base_client_instance):
 | 
				
			||||||
 | 
					        """Add a new instance of :class:`BaseClient` descendant.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        `self` will store a reference to `base_client_instance`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        >>> def test_clients():
 | 
				
			||||||
 | 
					        ...     from keystoneclient.auth import keystone
 | 
				
			||||||
 | 
					        ...     from openstack.common.apiclient import client
 | 
				
			||||||
 | 
					        ...     auth = keystone.KeystoneAuthPlugin(
 | 
				
			||||||
 | 
					        ...         username="user", password="pass", tenant_name="tenant",
 | 
				
			||||||
 | 
					        ...         auth_url="http://auth:5000/v2.0")
 | 
				
			||||||
 | 
					        ...     openstack_client = client.HTTPClient(auth)
 | 
				
			||||||
 | 
					        ...     # create nova client
 | 
				
			||||||
 | 
					        ...     from novaclient.v1_1 import client
 | 
				
			||||||
 | 
					        ...     client.Client(openstack_client)
 | 
				
			||||||
 | 
					        ...     # create keystone client
 | 
				
			||||||
 | 
					        ...     from keystoneclient.v2_0 import client
 | 
				
			||||||
 | 
					        ...     client.Client(openstack_client)
 | 
				
			||||||
 | 
					        ...     # use them
 | 
				
			||||||
 | 
					        ...     openstack_client.identity.tenants.list()
 | 
				
			||||||
 | 
					        ...     openstack_client.compute.servers.list()
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        service_type = base_client_instance.service_type
 | 
				
			||||||
 | 
					        if service_type and not hasattr(self, service_type):
 | 
				
			||||||
 | 
					            setattr(self, service_type, base_client_instance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def authenticate(self):
 | 
				
			||||||
 | 
					        self.auth_plugin.authenticate(self)
 | 
				
			||||||
 | 
					        # Store the authentication results in the keyring for later requests
 | 
				
			||||||
 | 
					        if self.keyring_saver:
 | 
				
			||||||
 | 
					            self.keyring_saver.save(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BaseClient(object):
 | 
				
			||||||
 | 
					    """Top-level object to access the OpenStack API.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
 | 
				
			||||||
 | 
					    will handle a bunch of issues such as authentication.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    service_type = None
 | 
				
			||||||
 | 
					    endpoint_type = None  # "publicURL" will be used
 | 
				
			||||||
 | 
					    cached_endpoint = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, http_client, extensions=None):
 | 
				
			||||||
 | 
					        self.http_client = http_client
 | 
				
			||||||
 | 
					        http_client.add_client(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Add in any extensions...
 | 
				
			||||||
 | 
					        if extensions:
 | 
				
			||||||
 | 
					            for extension in extensions:
 | 
				
			||||||
 | 
					                if extension.manager_class:
 | 
				
			||||||
 | 
					                    setattr(self, extension.name,
 | 
				
			||||||
 | 
					                            extension.manager_class(self))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def client_request(self, method, url, **kwargs):
 | 
				
			||||||
 | 
					        return self.http_client.client_request(
 | 
				
			||||||
 | 
					            self, method, url, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def head(self, url, **kwargs):
 | 
				
			||||||
 | 
					        return self.client_request("HEAD", url, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, url, **kwargs):
 | 
				
			||||||
 | 
					        return self.client_request("GET", url, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post(self, url, **kwargs):
 | 
				
			||||||
 | 
					        return self.client_request("POST", url, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def put(self, url, **kwargs):
 | 
				
			||||||
 | 
					        return self.client_request("PUT", url, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete(self, url, **kwargs):
 | 
				
			||||||
 | 
					        return self.client_request("DELETE", url, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def patch(self, url, **kwargs):
 | 
				
			||||||
 | 
					        return self.client_request("PATCH", url, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def get_class(api_name, version, version_map):
 | 
				
			||||||
 | 
					        """Returns the client class for the requested API version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param api_name: the name of the API, e.g. 'compute', 'image', etc
 | 
				
			||||||
 | 
					        :param version: the requested API version
 | 
				
			||||||
 | 
					        :param version_map: a dict of client classes keyed by version
 | 
				
			||||||
 | 
					        :rtype: a client class for the requested API version
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            client_path = version_map[str(version)]
 | 
				
			||||||
 | 
					        except (KeyError, ValueError):
 | 
				
			||||||
 | 
					            msg = _("Invalid %(api_name)s client version '%(version)s'. "
 | 
				
			||||||
 | 
					                    "Must be one of: %(version_map)s") % {
 | 
				
			||||||
 | 
					                        'api_name': api_name,
 | 
				
			||||||
 | 
					                        'version': version,
 | 
				
			||||||
 | 
					                        'version_map': ', '.join(version_map.keys())
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					            raise exceptions.UnsupportedVersion(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return importutils.import_class(client_path)
 | 
				
			||||||
							
								
								
									
										466
									
								
								rallyclient/openstack/common/apiclient/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										466
									
								
								rallyclient/openstack/common/apiclient/exceptions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,466 @@
 | 
				
			|||||||
 | 
					# Copyright 2010 Jacob Kaplan-Moss
 | 
				
			||||||
 | 
					# Copyright 2011 Nebula, Inc.
 | 
				
			||||||
 | 
					# Copyright 2013 Alessio Ababilov
 | 
				
			||||||
 | 
					# Copyright 2013 OpenStack Foundation
 | 
				
			||||||
 | 
					# 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Exception definitions.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import inspect
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import six
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rallyclient.openstack.common.gettextutils import _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ClientException(Exception):
 | 
				
			||||||
 | 
					    """The base exception class for all exceptions this library raises.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MissingArgs(ClientException):
 | 
				
			||||||
 | 
					    """Supplied arguments are not sufficient for calling a function."""
 | 
				
			||||||
 | 
					    def __init__(self, missing):
 | 
				
			||||||
 | 
					        self.missing = missing
 | 
				
			||||||
 | 
					        msg = _("Missing arguments: %s") % ", ".join(missing)
 | 
				
			||||||
 | 
					        super(MissingArgs, self).__init__(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ValidationError(ClientException):
 | 
				
			||||||
 | 
					    """Error in validation on API client side."""
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UnsupportedVersion(ClientException):
 | 
				
			||||||
 | 
					    """User is trying to use an unsupported version of the API."""
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CommandError(ClientException):
 | 
				
			||||||
 | 
					    """Error in CLI tool."""
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AuthorizationFailure(ClientException):
 | 
				
			||||||
 | 
					    """Cannot authorize API client."""
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ConnectionRefused(ClientException):
 | 
				
			||||||
 | 
					    """Cannot connect to API service."""
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AuthPluginOptionsMissing(AuthorizationFailure):
 | 
				
			||||||
 | 
					    """Auth plugin misses some options."""
 | 
				
			||||||
 | 
					    def __init__(self, opt_names):
 | 
				
			||||||
 | 
					        super(AuthPluginOptionsMissing, self).__init__(
 | 
				
			||||||
 | 
					            _("Authentication failed. Missing options: %s") %
 | 
				
			||||||
 | 
					            ", ".join(opt_names))
 | 
				
			||||||
 | 
					        self.opt_names = opt_names
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AuthSystemNotFound(AuthorizationFailure):
 | 
				
			||||||
 | 
					    """User has specified an AuthSystem that is not installed."""
 | 
				
			||||||
 | 
					    def __init__(self, auth_system):
 | 
				
			||||||
 | 
					        super(AuthSystemNotFound, self).__init__(
 | 
				
			||||||
 | 
					            _("AuthSystemNotFound: %s") % repr(auth_system))
 | 
				
			||||||
 | 
					        self.auth_system = auth_system
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NoUniqueMatch(ClientException):
 | 
				
			||||||
 | 
					    """Multiple entities found instead of one."""
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EndpointException(ClientException):
 | 
				
			||||||
 | 
					    """Something is rotten in Service Catalog."""
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EndpointNotFound(EndpointException):
 | 
				
			||||||
 | 
					    """Could not find requested endpoint in Service Catalog."""
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AmbiguousEndpoints(EndpointException):
 | 
				
			||||||
 | 
					    """Found more than one matching endpoint in Service Catalog."""
 | 
				
			||||||
 | 
					    def __init__(self, endpoints=None):
 | 
				
			||||||
 | 
					        super(AmbiguousEndpoints, self).__init__(
 | 
				
			||||||
 | 
					            _("AmbiguousEndpoints: %s") % repr(endpoints))
 | 
				
			||||||
 | 
					        self.endpoints = endpoints
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HttpError(ClientException):
 | 
				
			||||||
 | 
					    """The base exception class for all HTTP exceptions.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 0
 | 
				
			||||||
 | 
					    message = _("HTTP Error")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, message=None, details=None,
 | 
				
			||||||
 | 
					                 response=None, request_id=None,
 | 
				
			||||||
 | 
					                 url=None, method=None, http_status=None):
 | 
				
			||||||
 | 
					        self.http_status = http_status or self.http_status
 | 
				
			||||||
 | 
					        self.message = message or self.message
 | 
				
			||||||
 | 
					        self.details = details
 | 
				
			||||||
 | 
					        self.request_id = request_id
 | 
				
			||||||
 | 
					        self.response = response
 | 
				
			||||||
 | 
					        self.url = url
 | 
				
			||||||
 | 
					        self.method = method
 | 
				
			||||||
 | 
					        formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
 | 
				
			||||||
 | 
					        if request_id:
 | 
				
			||||||
 | 
					            formatted_string += " (Request-ID: %s)" % request_id
 | 
				
			||||||
 | 
					        super(HttpError, self).__init__(formatted_string)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HTTPRedirection(HttpError):
 | 
				
			||||||
 | 
					    """HTTP Redirection."""
 | 
				
			||||||
 | 
					    message = _("HTTP Redirection")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HTTPClientError(HttpError):
 | 
				
			||||||
 | 
					    """Client-side HTTP error.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Exception for cases in which the client seems to have erred.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    message = _("HTTP Client Error")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HttpServerError(HttpError):
 | 
				
			||||||
 | 
					    """Server-side HTTP error.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Exception for cases in which the server is aware that it has
 | 
				
			||||||
 | 
					    erred or is incapable of performing the request.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    message = _("HTTP Server Error")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MultipleChoices(HTTPRedirection):
 | 
				
			||||||
 | 
					    """HTTP 300 - Multiple Choices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Indicates multiple options for the resource that the client may follow.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http_status = 300
 | 
				
			||||||
 | 
					    message = _("Multiple Choices")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BadRequest(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 400 - Bad Request.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The request cannot be fulfilled due to bad syntax.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 400
 | 
				
			||||||
 | 
					    message = _("Bad Request")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Unauthorized(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 401 - Unauthorized.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Similar to 403 Forbidden, but specifically for use when authentication
 | 
				
			||||||
 | 
					    is required and has failed or has not yet been provided.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 401
 | 
				
			||||||
 | 
					    message = _("Unauthorized")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PaymentRequired(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 402 - Payment Required.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Reserved for future use.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 402
 | 
				
			||||||
 | 
					    message = _("Payment Required")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Forbidden(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 403 - Forbidden.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The request was a valid request, but the server is refusing to respond
 | 
				
			||||||
 | 
					    to it.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 403
 | 
				
			||||||
 | 
					    message = _("Forbidden")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotFound(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 404 - Not Found.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The requested resource could not be found but may be available again
 | 
				
			||||||
 | 
					    in the future.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 404
 | 
				
			||||||
 | 
					    message = _("Not Found")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MethodNotAllowed(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 405 - Method Not Allowed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    A request was made of a resource using a request method not supported
 | 
				
			||||||
 | 
					    by that resource.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 405
 | 
				
			||||||
 | 
					    message = _("Method Not Allowed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NotAcceptable(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 406 - Not Acceptable.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The requested resource is only capable of generating content not
 | 
				
			||||||
 | 
					    acceptable according to the Accept headers sent in the request.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 406
 | 
				
			||||||
 | 
					    message = _("Not Acceptable")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ProxyAuthenticationRequired(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 407 - Proxy Authentication Required.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The client must first authenticate itself with the proxy.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 407
 | 
				
			||||||
 | 
					    message = _("Proxy Authentication Required")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RequestTimeout(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 408 - Request Timeout.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The server timed out waiting for the request.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 408
 | 
				
			||||||
 | 
					    message = _("Request Timeout")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Conflict(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 409 - Conflict.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Indicates that the request could not be processed because of conflict
 | 
				
			||||||
 | 
					    in the request, such as an edit conflict.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 409
 | 
				
			||||||
 | 
					    message = _("Conflict")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Gone(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 410 - Gone.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Indicates that the resource requested is no longer available and will
 | 
				
			||||||
 | 
					    not be available again.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 410
 | 
				
			||||||
 | 
					    message = _("Gone")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LengthRequired(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 411 - Length Required.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The request did not specify the length of its content, which is
 | 
				
			||||||
 | 
					    required by the requested resource.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 411
 | 
				
			||||||
 | 
					    message = _("Length Required")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PreconditionFailed(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 412 - Precondition Failed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The server does not meet one of the preconditions that the requester
 | 
				
			||||||
 | 
					    put on the request.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 412
 | 
				
			||||||
 | 
					    message = _("Precondition Failed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RequestEntityTooLarge(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 413 - Request Entity Too Large.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The request is larger than the server is willing or able to process.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 413
 | 
				
			||||||
 | 
					    message = _("Request Entity Too Large")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.retry_after = int(kwargs.pop('retry_after'))
 | 
				
			||||||
 | 
					        except (KeyError, ValueError):
 | 
				
			||||||
 | 
					            self.retry_after = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RequestUriTooLong(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 414 - Request-URI Too Long.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The URI provided was too long for the server to process.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 414
 | 
				
			||||||
 | 
					    message = _("Request-URI Too Long")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UnsupportedMediaType(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 415 - Unsupported Media Type.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The request entity has a media type which the server or resource does
 | 
				
			||||||
 | 
					    not support.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 415
 | 
				
			||||||
 | 
					    message = _("Unsupported Media Type")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RequestedRangeNotSatisfiable(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 416 - Requested Range Not Satisfiable.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The client has asked for a portion of the file, but the server cannot
 | 
				
			||||||
 | 
					    supply that portion.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 416
 | 
				
			||||||
 | 
					    message = _("Requested Range Not Satisfiable")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ExpectationFailed(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 417 - Expectation Failed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The server cannot meet the requirements of the Expect request-header field.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 417
 | 
				
			||||||
 | 
					    message = _("Expectation Failed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UnprocessableEntity(HTTPClientError):
 | 
				
			||||||
 | 
					    """HTTP 422 - Unprocessable Entity.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The request was well-formed but was unable to be followed due to semantic
 | 
				
			||||||
 | 
					    errors.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 422
 | 
				
			||||||
 | 
					    message = _("Unprocessable Entity")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class InternalServerError(HttpServerError):
 | 
				
			||||||
 | 
					    """HTTP 500 - Internal Server Error.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    A generic error message, given when no more specific message is suitable.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 500
 | 
				
			||||||
 | 
					    message = _("Internal Server Error")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# NotImplemented is a python keyword.
 | 
				
			||||||
 | 
					class HttpNotImplemented(HttpServerError):
 | 
				
			||||||
 | 
					    """HTTP 501 - Not Implemented.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The server either does not recognize the request method, or it lacks
 | 
				
			||||||
 | 
					    the ability to fulfill the request.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 501
 | 
				
			||||||
 | 
					    message = _("Not Implemented")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BadGateway(HttpServerError):
 | 
				
			||||||
 | 
					    """HTTP 502 - Bad Gateway.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The server was acting as a gateway or proxy and received an invalid
 | 
				
			||||||
 | 
					    response from the upstream server.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 502
 | 
				
			||||||
 | 
					    message = _("Bad Gateway")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ServiceUnavailable(HttpServerError):
 | 
				
			||||||
 | 
					    """HTTP 503 - Service Unavailable.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The server is currently unavailable.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 503
 | 
				
			||||||
 | 
					    message = _("Service Unavailable")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GatewayTimeout(HttpServerError):
 | 
				
			||||||
 | 
					    """HTTP 504 - Gateway Timeout.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The server was acting as a gateway or proxy and did not receive a timely
 | 
				
			||||||
 | 
					    response from the upstream server.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 504
 | 
				
			||||||
 | 
					    message = _("Gateway Timeout")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HttpVersionNotSupported(HttpServerError):
 | 
				
			||||||
 | 
					    """HTTP 505 - HttpVersion Not Supported.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The server does not support the HTTP protocol version used in the request.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    http_status = 505
 | 
				
			||||||
 | 
					    message = _("HTTP Version Not Supported")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# _code_map contains all the classes that have http_status attribute.
 | 
				
			||||||
 | 
					_code_map = dict(
 | 
				
			||||||
 | 
					    (getattr(obj, 'http_status', None), obj)
 | 
				
			||||||
 | 
					    for name, obj in six.iteritems(vars(sys.modules[__name__]))
 | 
				
			||||||
 | 
					    if inspect.isclass(obj) and getattr(obj, 'http_status', False)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def from_response(response, method, url):
 | 
				
			||||||
 | 
					    """Returns an instance of :class:`HttpError` or subclass based on response.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param response: instance of `requests.Response` class
 | 
				
			||||||
 | 
					    :param method: HTTP method used for request
 | 
				
			||||||
 | 
					    :param url: URL used for request
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    req_id = response.headers.get("x-openstack-request-id")
 | 
				
			||||||
 | 
					    # NOTE(hdd) true for older versions of nova and cinder
 | 
				
			||||||
 | 
					    if not req_id:
 | 
				
			||||||
 | 
					        req_id = response.headers.get("x-compute-request-id")
 | 
				
			||||||
 | 
					    kwargs = {
 | 
				
			||||||
 | 
					        "http_status": response.status_code,
 | 
				
			||||||
 | 
					        "response": response,
 | 
				
			||||||
 | 
					        "method": method,
 | 
				
			||||||
 | 
					        "url": url,
 | 
				
			||||||
 | 
					        "request_id": req_id,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if "retry-after" in response.headers:
 | 
				
			||||||
 | 
					        kwargs["retry_after"] = response.headers["retry-after"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    content_type = response.headers.get("Content-Type", "")
 | 
				
			||||||
 | 
					    if content_type.startswith("application/json"):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            body = response.json()
 | 
				
			||||||
 | 
					        except ValueError:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            if isinstance(body, dict):
 | 
				
			||||||
 | 
					                error = list(body.values())[0]
 | 
				
			||||||
 | 
					                kwargs["message"] = error.get("message")
 | 
				
			||||||
 | 
					                kwargs["details"] = error.get("details")
 | 
				
			||||||
 | 
					    elif content_type.startswith("text/"):
 | 
				
			||||||
 | 
					        kwargs["details"] = response.text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        cls = _code_map[response.status_code]
 | 
				
			||||||
 | 
					    except KeyError:
 | 
				
			||||||
 | 
					        if 500 <= response.status_code < 600:
 | 
				
			||||||
 | 
					            cls = HttpServerError
 | 
				
			||||||
 | 
					        elif 400 <= response.status_code < 500:
 | 
				
			||||||
 | 
					            cls = HTTPClientError
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            cls = HttpError
 | 
				
			||||||
 | 
					    return cls(**kwargs)
 | 
				
			||||||
							
								
								
									
										173
									
								
								rallyclient/openstack/common/apiclient/fake_client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								rallyclient/openstack/common/apiclient/fake_client.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
				
			|||||||
 | 
					# Copyright 2013 OpenStack Foundation
 | 
				
			||||||
 | 
					# 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					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.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# W0102: Dangerous default value %s as argument
 | 
				
			||||||
 | 
					# pylint: disable=W0102
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					import six
 | 
				
			||||||
 | 
					from six.moves.urllib import parse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rallyclient.openstack.common.apiclient import client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def assert_has_keys(dct, required=[], optional=[]):
 | 
				
			||||||
 | 
					    for k in required:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            assert k in dct
 | 
				
			||||||
 | 
					        except AssertionError:
 | 
				
			||||||
 | 
					            extra_keys = set(dct.keys()).difference(set(required + optional))
 | 
				
			||||||
 | 
					            raise AssertionError("found unexpected keys: %s" %
 | 
				
			||||||
 | 
					                                 list(extra_keys))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestResponse(requests.Response):
 | 
				
			||||||
 | 
					    """Wrap requests.Response and provide a convenient initialization.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, data):
 | 
				
			||||||
 | 
					        super(TestResponse, self).__init__()
 | 
				
			||||||
 | 
					        self._content_consumed = True
 | 
				
			||||||
 | 
					        if isinstance(data, dict):
 | 
				
			||||||
 | 
					            self.status_code = data.get('status_code', 200)
 | 
				
			||||||
 | 
					            # Fake the text attribute to streamline Response creation
 | 
				
			||||||
 | 
					            text = data.get('text', "")
 | 
				
			||||||
 | 
					            if isinstance(text, (dict, list)):
 | 
				
			||||||
 | 
					                self._content = json.dumps(text)
 | 
				
			||||||
 | 
					                default_headers = {
 | 
				
			||||||
 | 
					                    "Content-Type": "application/json",
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                self._content = text
 | 
				
			||||||
 | 
					                default_headers = {}
 | 
				
			||||||
 | 
					            if six.PY3 and isinstance(self._content, six.string_types):
 | 
				
			||||||
 | 
					                self._content = self._content.encode('utf-8', 'strict')
 | 
				
			||||||
 | 
					            self.headers = data.get('headers') or default_headers
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self.status_code = data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __eq__(self, other):
 | 
				
			||||||
 | 
					        return (self.status_code == other.status_code and
 | 
				
			||||||
 | 
					                self.headers == other.headers and
 | 
				
			||||||
 | 
					                self._content == other._content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FakeHTTPClient(client.HTTPClient):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        self.callstack = []
 | 
				
			||||||
 | 
					        self.fixtures = kwargs.pop("fixtures", None) or {}
 | 
				
			||||||
 | 
					        if not args and "auth_plugin" not in kwargs:
 | 
				
			||||||
 | 
					            args = (None, )
 | 
				
			||||||
 | 
					        super(FakeHTTPClient, self).__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def assert_called(self, method, url, body=None, pos=-1):
 | 
				
			||||||
 | 
					        """Assert than an API method was just called.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        expected = (method, url)
 | 
				
			||||||
 | 
					        called = self.callstack[pos][0:2]
 | 
				
			||||||
 | 
					        assert self.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:
 | 
				
			||||||
 | 
					            if self.callstack[pos][3] != body:
 | 
				
			||||||
 | 
					                raise AssertionError('%r != %r' %
 | 
				
			||||||
 | 
					                                     (self.callstack[pos][3], 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.callstack, \
 | 
				
			||||||
 | 
					            "Expected %s %s but no calls were made." % expected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        found = False
 | 
				
			||||||
 | 
					        entry = None
 | 
				
			||||||
 | 
					        for entry in self.callstack:
 | 
				
			||||||
 | 
					            if expected == entry[0:2]:
 | 
				
			||||||
 | 
					                found = True
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert found, 'Expected %s %s; got %s' % \
 | 
				
			||||||
 | 
					            (method, url, self.callstack)
 | 
				
			||||||
 | 
					        if body is not None:
 | 
				
			||||||
 | 
					            assert entry[3] == body, "%s != %s" % (entry[3], body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.callstack = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clear_callstack(self):
 | 
				
			||||||
 | 
					        self.callstack = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def authenticate(self):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def client_request(self, client, method, url, **kwargs):
 | 
				
			||||||
 | 
					        # Check that certain things are called correctly
 | 
				
			||||||
 | 
					        if method in ["GET", "DELETE"]:
 | 
				
			||||||
 | 
					            assert "json" not in kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Note the call
 | 
				
			||||||
 | 
					        self.callstack.append(
 | 
				
			||||||
 | 
					            (method,
 | 
				
			||||||
 | 
					             url,
 | 
				
			||||||
 | 
					             kwargs.get("headers") or {},
 | 
				
			||||||
 | 
					             kwargs.get("json") or kwargs.get("data")))
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            fixture = self.fixtures[url][method]
 | 
				
			||||||
 | 
					        except KeyError:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return TestResponse({"headers": fixture[0],
 | 
				
			||||||
 | 
					                                 "text": fixture[1]})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Call the method
 | 
				
			||||||
 | 
					        args = parse.parse_qsl(parse.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))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        resp = getattr(self, callback)(**kwargs)
 | 
				
			||||||
 | 
					        if len(resp) == 3:
 | 
				
			||||||
 | 
					            status, headers, body = resp
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            status, body = resp
 | 
				
			||||||
 | 
					            headers = {}
 | 
				
			||||||
 | 
					        return TestResponse({
 | 
				
			||||||
 | 
					            "status_code": status,
 | 
				
			||||||
 | 
					            "text": body,
 | 
				
			||||||
 | 
					            "headers": headers,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
							
								
								
									
										317
									
								
								rallyclient/openstack/common/cliutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								rallyclient/openstack/common/cliutils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,317 @@
 | 
				
			|||||||
 | 
					# Copyright 2012 Red Hat, 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# W0603: Using the global statement
 | 
				
			||||||
 | 
					# W0621: Redefining name %s from outer scope
 | 
				
			||||||
 | 
					# pylint: disable=W0603,W0621
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from __future__ import print_function
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import getpass
 | 
				
			||||||
 | 
					import inspect
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import textwrap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import prettytable
 | 
				
			||||||
 | 
					import six
 | 
				
			||||||
 | 
					from six import moves
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rallyclient.openstack.common.apiclient import exceptions
 | 
				
			||||||
 | 
					from rallyclient.openstack.common.gettextutils import _
 | 
				
			||||||
 | 
					from rallyclient.openstack.common import strutils
 | 
				
			||||||
 | 
					from rallyclient.openstack.common import uuidutils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def validate_args(fn, *args, **kwargs):
 | 
				
			||||||
 | 
					    """Check that the supplied args are sufficient for calling a function.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    >>> validate_args(lambda a: None)
 | 
				
			||||||
 | 
					    Traceback (most recent call last):
 | 
				
			||||||
 | 
					        ...
 | 
				
			||||||
 | 
					    MissingArgs: Missing argument(s): a
 | 
				
			||||||
 | 
					    >>> validate_args(lambda a, b, c, d: None, 0, c=1)
 | 
				
			||||||
 | 
					    Traceback (most recent call last):
 | 
				
			||||||
 | 
					        ...
 | 
				
			||||||
 | 
					    MissingArgs: Missing argument(s): b, d
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param fn: the function to check
 | 
				
			||||||
 | 
					    :param arg: the positional arguments supplied
 | 
				
			||||||
 | 
					    :param kwargs: the keyword arguments supplied
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    argspec = inspect.getargspec(fn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    num_defaults = len(argspec.defaults or [])
 | 
				
			||||||
 | 
					    required_args = argspec.args[:len(argspec.args) - num_defaults]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def isbound(method):
 | 
				
			||||||
 | 
					        return getattr(method, '__self__', None) is not None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if isbound(fn):
 | 
				
			||||||
 | 
					        required_args.pop(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    missing = [arg for arg in required_args if arg not in kwargs]
 | 
				
			||||||
 | 
					    missing = missing[len(args):]
 | 
				
			||||||
 | 
					    if missing:
 | 
				
			||||||
 | 
					        raise exceptions.MissingArgs(missing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def arg(*args, **kwargs):
 | 
				
			||||||
 | 
					    """Decorator for CLI args.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    >>> @arg("name", help="Name of the new entity")
 | 
				
			||||||
 | 
					    ... def entity_create(args):
 | 
				
			||||||
 | 
					    ...     pass
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    def _decorator(func):
 | 
				
			||||||
 | 
					        add_arg(func, *args, **kwargs)
 | 
				
			||||||
 | 
					        return func
 | 
				
			||||||
 | 
					    return _decorator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def env(*args, **kwargs):
 | 
				
			||||||
 | 
					    """Returns the first environment variable set.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    If all are empty, defaults to '' or keyword arg `default`.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    for arg in args:
 | 
				
			||||||
 | 
					        value = os.environ.get(arg)
 | 
				
			||||||
 | 
					        if value:
 | 
				
			||||||
 | 
					            return value
 | 
				
			||||||
 | 
					    return kwargs.get('default', '')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def add_arg(func, *args, **kwargs):
 | 
				
			||||||
 | 
					    """Bind CLI arguments to a shell.py `do_foo` function."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not hasattr(func, 'arguments'):
 | 
				
			||||||
 | 
					        func.arguments = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # NOTE(sirp): avoid dups that can occur when the module is shared across
 | 
				
			||||||
 | 
					    # tests.
 | 
				
			||||||
 | 
					    if (args, kwargs) not in func.arguments:
 | 
				
			||||||
 | 
					        # Because of the semantics of decorator composition if we just append
 | 
				
			||||||
 | 
					        # to the options list positional options will appear to be backwards.
 | 
				
			||||||
 | 
					        func.arguments.insert(0, (args, kwargs))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def unauthenticated(func):
 | 
				
			||||||
 | 
					    """Adds 'unauthenticated' attribute to decorated function.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Usage:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    >>> @unauthenticated
 | 
				
			||||||
 | 
					    ... def mymethod(f):
 | 
				
			||||||
 | 
					    ...     pass
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    func.unauthenticated = True
 | 
				
			||||||
 | 
					    return func
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def isunauthenticated(func):
 | 
				
			||||||
 | 
					    """Checks if the function does not require authentication.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Mark such functions with the `@unauthenticated` decorator.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :returns: bool
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return getattr(func, 'unauthenticated', False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def print_list(objs, fields, formatters=None, sortby_index=0,
 | 
				
			||||||
 | 
					               mixed_case_fields=None):
 | 
				
			||||||
 | 
					    """Print a list or objects as a table, one row per object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param objs: iterable of :class:`Resource`
 | 
				
			||||||
 | 
					    :param fields: attributes that correspond to columns, in order
 | 
				
			||||||
 | 
					    :param formatters: `dict` of callables for field formatting
 | 
				
			||||||
 | 
					    :param sortby_index: index of the field for sorting table rows
 | 
				
			||||||
 | 
					    :param mixed_case_fields: fields corresponding to object attributes that
 | 
				
			||||||
 | 
					        have mixed case names (e.g., 'serverId')
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    formatters = formatters or {}
 | 
				
			||||||
 | 
					    mixed_case_fields = mixed_case_fields or []
 | 
				
			||||||
 | 
					    if sortby_index is None:
 | 
				
			||||||
 | 
					        kwargs = {}
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        kwargs = {'sortby': fields[sortby_index]}
 | 
				
			||||||
 | 
					    pt = prettytable.PrettyTable(fields, caching=False)
 | 
				
			||||||
 | 
					    pt.align = 'l'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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(strutils.safe_encode(pt.get_string(**kwargs)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def print_dict(dct, dict_property="Property", wrap=0):
 | 
				
			||||||
 | 
					    """Print a `dict` as a table of two columns.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param dct: `dict` to print
 | 
				
			||||||
 | 
					    :param dict_property: name of the first column
 | 
				
			||||||
 | 
					    :param wrap: wrapping for the second column
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False)
 | 
				
			||||||
 | 
					    pt.align = 'l'
 | 
				
			||||||
 | 
					    for k, v in six.iteritems(dct):
 | 
				
			||||||
 | 
					        # convert dict to str to check length
 | 
				
			||||||
 | 
					        if isinstance(v, dict):
 | 
				
			||||||
 | 
					            v = six.text_type(v)
 | 
				
			||||||
 | 
					        if wrap > 0:
 | 
				
			||||||
 | 
					            v = textwrap.fill(six.text_type(v), wrap)
 | 
				
			||||||
 | 
					        # if value has a newline, add in multiple rows
 | 
				
			||||||
 | 
					        # e.g. fault with stacktrace
 | 
				
			||||||
 | 
					        if v and isinstance(v, six.string_types) and r'\n' in v:
 | 
				
			||||||
 | 
					            lines = v.strip().split(r'\n')
 | 
				
			||||||
 | 
					            col1 = k
 | 
				
			||||||
 | 
					            for line in lines:
 | 
				
			||||||
 | 
					                pt.add_row([col1, line])
 | 
				
			||||||
 | 
					                col1 = ''
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            pt.add_row([k, v])
 | 
				
			||||||
 | 
					    print(strutils.safe_encode(pt.get_string()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_password(max_password_prompts=3):
 | 
				
			||||||
 | 
					    """Read password from TTY."""
 | 
				
			||||||
 | 
					    verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
 | 
				
			||||||
 | 
					    pw = None
 | 
				
			||||||
 | 
					    if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
 | 
				
			||||||
 | 
					        # Check for Ctrl-D
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            for __ in moves.range(max_password_prompts):
 | 
				
			||||||
 | 
					                pw1 = getpass.getpass("OS Password: ")
 | 
				
			||||||
 | 
					                if verify:
 | 
				
			||||||
 | 
					                    pw2 = getpass.getpass("Please verify: ")
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    pw2 = pw1
 | 
				
			||||||
 | 
					                if pw1 == pw2 and pw1:
 | 
				
			||||||
 | 
					                    pw = pw1
 | 
				
			||||||
 | 
					                    break
 | 
				
			||||||
 | 
					        except EOFError:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					    return pw
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def find_resource(manager, name_or_id, **find_args):
 | 
				
			||||||
 | 
					    """Look for resource in a given manager.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Used as a helper for the _find_* methods.
 | 
				
			||||||
 | 
					    Example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. code-block:: python
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def _find_hypervisor(cs, hypervisor):
 | 
				
			||||||
 | 
					            #Get a hypervisor by name or ID.
 | 
				
			||||||
 | 
					            return cliutils.find_resource(cs.hypervisors, hypervisor)
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    # first try to get entity as integer id
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return manager.get(int(name_or_id))
 | 
				
			||||||
 | 
					    except (TypeError, ValueError, exceptions.NotFound):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # now try to get entity as uuid
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        if six.PY2:
 | 
				
			||||||
 | 
					            tmp_id = strutils.safe_encode(name_or_id)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            tmp_id = strutils.safe_decode(name_or_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if uuidutils.is_uuid_like(tmp_id):
 | 
				
			||||||
 | 
					            return manager.get(tmp_id)
 | 
				
			||||||
 | 
					    except (TypeError, ValueError, exceptions.NotFound):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # for str id which is not uuid
 | 
				
			||||||
 | 
					    if getattr(manager, 'is_alphanum_id_allowed', False):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return manager.get(name_or_id)
 | 
				
			||||||
 | 
					        except exceptions.NotFound:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return manager.find(human_id=name_or_id, **find_args)
 | 
				
			||||||
 | 
					        except exceptions.NotFound:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # finally try to find entity by name
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            resource = getattr(manager, 'resource_class', None)
 | 
				
			||||||
 | 
					            name_attr = resource.NAME_ATTR if resource else 'name'
 | 
				
			||||||
 | 
					            kwargs = {name_attr: name_or_id}
 | 
				
			||||||
 | 
					            kwargs.update(find_args)
 | 
				
			||||||
 | 
					            return manager.find(**kwargs)
 | 
				
			||||||
 | 
					        except exceptions.NotFound:
 | 
				
			||||||
 | 
					            msg = _("No %(name)s with a name or "
 | 
				
			||||||
 | 
					                    "ID of '%(name_or_id)s' exists.") % \
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "name": manager.resource_class.__name__.lower(),
 | 
				
			||||||
 | 
					                    "name_or_id": name_or_id
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            raise exceptions.CommandError(msg)
 | 
				
			||||||
 | 
					    except exceptions.NoUniqueMatch:
 | 
				
			||||||
 | 
					        msg = _("Multiple %(name)s matches found for "
 | 
				
			||||||
 | 
					                "'%(name_or_id)s', use an ID to be more specific.") % \
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "name": manager.resource_class.__name__.lower(),
 | 
				
			||||||
 | 
					                "name_or_id": name_or_id
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        raise exceptions.CommandError(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def service_type(stype):
 | 
				
			||||||
 | 
					    """Adds 'service_type' attribute to decorated function.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Usage:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. code-block:: python
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       @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 exit(msg=''):
 | 
				
			||||||
 | 
					    if msg:
 | 
				
			||||||
 | 
					        print (msg, file=sys.stderr)
 | 
				
			||||||
 | 
					    sys.exit(1)
 | 
				
			||||||
							
								
								
									
										479
									
								
								rallyclient/openstack/common/gettextutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										479
									
								
								rallyclient/openstack/common/gettextutils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,479 @@
 | 
				
			|||||||
 | 
					# Copyright 2012 Red Hat, Inc.
 | 
				
			||||||
 | 
					# Copyright 2013 IBM Corp.
 | 
				
			||||||
 | 
					# 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					gettext for openstack-common modules.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Usual usage in an openstack.common module:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from rallyclient.openstack.common.gettextutils import _
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import copy
 | 
				
			||||||
 | 
					import gettext
 | 
				
			||||||
 | 
					import locale
 | 
				
			||||||
 | 
					from logging import handlers
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from babel import localedata
 | 
				
			||||||
 | 
					import six
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_AVAILABLE_LANGUAGES = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# FIXME(dhellmann): Remove this when moving to oslo.i18n.
 | 
				
			||||||
 | 
					USE_LAZY = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TranslatorFactory(object):
 | 
				
			||||||
 | 
					    """Create translator functions
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, domain, localedir=None):
 | 
				
			||||||
 | 
					        """Establish a set of translation functions for the domain.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param domain: Name of translation domain,
 | 
				
			||||||
 | 
					                       specifying a message catalog.
 | 
				
			||||||
 | 
					        :type domain: str
 | 
				
			||||||
 | 
					        :param lazy: Delays translation until a message is emitted.
 | 
				
			||||||
 | 
					                     Defaults to False.
 | 
				
			||||||
 | 
					        :type lazy: Boolean
 | 
				
			||||||
 | 
					        :param localedir: Directory with translation catalogs.
 | 
				
			||||||
 | 
					        :type localedir: str
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.domain = domain
 | 
				
			||||||
 | 
					        if localedir is None:
 | 
				
			||||||
 | 
					            localedir = os.environ.get(domain.upper() + '_LOCALEDIR')
 | 
				
			||||||
 | 
					        self.localedir = localedir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _make_translation_func(self, domain=None):
 | 
				
			||||||
 | 
					        """Return a new translation function ready for use.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Takes into account whether or not lazy translation is being
 | 
				
			||||||
 | 
					        done.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        The domain can be specified to override the default from the
 | 
				
			||||||
 | 
					        factory, but the localedir from the factory is always used
 | 
				
			||||||
 | 
					        because we assume the log-level translation catalogs are
 | 
				
			||||||
 | 
					        installed in the same directory as the main application
 | 
				
			||||||
 | 
					        catalog.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if domain is None:
 | 
				
			||||||
 | 
					            domain = self.domain
 | 
				
			||||||
 | 
					        t = gettext.translation(domain,
 | 
				
			||||||
 | 
					                                localedir=self.localedir,
 | 
				
			||||||
 | 
					                                fallback=True)
 | 
				
			||||||
 | 
					        # Use the appropriate method of the translation object based
 | 
				
			||||||
 | 
					        # on the python version.
 | 
				
			||||||
 | 
					        m = t.gettext if six.PY3 else t.ugettext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def f(msg):
 | 
				
			||||||
 | 
					            """oslo.i18n.gettextutils translation function."""
 | 
				
			||||||
 | 
					            if USE_LAZY:
 | 
				
			||||||
 | 
					                return Message(msg, domain=domain)
 | 
				
			||||||
 | 
					            return m(msg)
 | 
				
			||||||
 | 
					        return f
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def primary(self):
 | 
				
			||||||
 | 
					        "The default translation function."
 | 
				
			||||||
 | 
					        return self._make_translation_func()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _make_log_translation_func(self, level):
 | 
				
			||||||
 | 
					        return self._make_translation_func(self.domain + '-log-' + level)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def log_info(self):
 | 
				
			||||||
 | 
					        "Translate info-level log messages."
 | 
				
			||||||
 | 
					        return self._make_log_translation_func('info')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def log_warning(self):
 | 
				
			||||||
 | 
					        "Translate warning-level log messages."
 | 
				
			||||||
 | 
					        return self._make_log_translation_func('warning')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def log_error(self):
 | 
				
			||||||
 | 
					        "Translate error-level log messages."
 | 
				
			||||||
 | 
					        return self._make_log_translation_func('error')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def log_critical(self):
 | 
				
			||||||
 | 
					        "Translate critical-level log messages."
 | 
				
			||||||
 | 
					        return self._make_log_translation_func('critical')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# NOTE(dhellmann): When this module moves out of the incubator into
 | 
				
			||||||
 | 
					# oslo.i18n, these global variables can be moved to an integration
 | 
				
			||||||
 | 
					# module within each application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create the global translation functions.
 | 
				
			||||||
 | 
					_translators = TranslatorFactory('rallyclient')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# The primary translation function using the well-known name "_"
 | 
				
			||||||
 | 
					_ = _translators.primary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Translators for log levels.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# The abbreviated names are meant to reflect the usual use of a short
 | 
				
			||||||
 | 
					# name like '_'. The "L" is for "log" and the other letter comes from
 | 
				
			||||||
 | 
					# the level.
 | 
				
			||||||
 | 
					_LI = _translators.log_info
 | 
				
			||||||
 | 
					_LW = _translators.log_warning
 | 
				
			||||||
 | 
					_LE = _translators.log_error
 | 
				
			||||||
 | 
					_LC = _translators.log_critical
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# NOTE(dhellmann): End of globals that will move to the application's
 | 
				
			||||||
 | 
					# integration module.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def enable_lazy():
 | 
				
			||||||
 | 
					    """Convenience function for configuring _() to use lazy gettext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Call this at the start of execution to enable the gettextutils._
 | 
				
			||||||
 | 
					    function to use lazy gettext functionality. This is useful if
 | 
				
			||||||
 | 
					    your project is importing _ directly instead of using the
 | 
				
			||||||
 | 
					    gettextutils.install() way of importing the _ function.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    global USE_LAZY
 | 
				
			||||||
 | 
					    USE_LAZY = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def install(domain):
 | 
				
			||||||
 | 
					    """Install a _() function using the given translation domain.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Given a translation domain, install a _() function using gettext's
 | 
				
			||||||
 | 
					    install() function.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The main difference from gettext.install() is that we allow
 | 
				
			||||||
 | 
					    overriding the default localedir (e.g. /usr/share/locale) using
 | 
				
			||||||
 | 
					    a translation-domain-specific environment variable (e.g.
 | 
				
			||||||
 | 
					    NOVA_LOCALEDIR).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Note that to enable lazy translation, enable_lazy must be
 | 
				
			||||||
 | 
					    called.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param domain: the translation domain
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    from six import moves
 | 
				
			||||||
 | 
					    tf = TranslatorFactory(domain)
 | 
				
			||||||
 | 
					    moves.builtins.__dict__['_'] = tf.primary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Message(six.text_type):
 | 
				
			||||||
 | 
					    """A Message object is a unicode object that can be translated.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Translation of Message is done explicitly using the translate() method.
 | 
				
			||||||
 | 
					    For all non-translation intents and purposes, a Message is simply unicode,
 | 
				
			||||||
 | 
					    and can be treated as such.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __new__(cls, msgid, msgtext=None, params=None,
 | 
				
			||||||
 | 
					                domain='rallyclient', *args):
 | 
				
			||||||
 | 
					        """Create a new Message object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        In order for translation to work gettext requires a message ID, this
 | 
				
			||||||
 | 
					        msgid will be used as the base unicode text. It is also possible
 | 
				
			||||||
 | 
					        for the msgid and the base unicode text to be different by passing
 | 
				
			||||||
 | 
					        the msgtext parameter.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        # If the base msgtext is not given, we use the default translation
 | 
				
			||||||
 | 
					        # of the msgid (which is in English) just in case the system locale is
 | 
				
			||||||
 | 
					        # not English, so that the base text will be in that locale by default.
 | 
				
			||||||
 | 
					        if not msgtext:
 | 
				
			||||||
 | 
					            msgtext = Message._translate_msgid(msgid, domain)
 | 
				
			||||||
 | 
					        # We want to initialize the parent unicode with the actual object that
 | 
				
			||||||
 | 
					        # would have been plain unicode if 'Message' was not enabled.
 | 
				
			||||||
 | 
					        msg = super(Message, cls).__new__(cls, msgtext)
 | 
				
			||||||
 | 
					        msg.msgid = msgid
 | 
				
			||||||
 | 
					        msg.domain = domain
 | 
				
			||||||
 | 
					        msg.params = params
 | 
				
			||||||
 | 
					        return msg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def translate(self, desired_locale=None):
 | 
				
			||||||
 | 
					        """Translate this message to the desired locale.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param desired_locale: The desired locale to translate the message to,
 | 
				
			||||||
 | 
					                               if no locale is provided the message will be
 | 
				
			||||||
 | 
					                               translated to the system's default locale.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :returns: the translated message in unicode
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        translated_message = Message._translate_msgid(self.msgid,
 | 
				
			||||||
 | 
					                                                      self.domain,
 | 
				
			||||||
 | 
					                                                      desired_locale)
 | 
				
			||||||
 | 
					        if self.params is None:
 | 
				
			||||||
 | 
					            # No need for more translation
 | 
				
			||||||
 | 
					            return translated_message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # This Message object may have been formatted with one or more
 | 
				
			||||||
 | 
					        # Message objects as substitution arguments, given either as a single
 | 
				
			||||||
 | 
					        # argument, part of a tuple, or as one or more values in a dictionary.
 | 
				
			||||||
 | 
					        # When translating this Message we need to translate those Messages too
 | 
				
			||||||
 | 
					        translated_params = _translate_args(self.params, desired_locale)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        translated_message = translated_message % translated_params
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return translated_message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def _translate_msgid(msgid, domain, desired_locale=None):
 | 
				
			||||||
 | 
					        if not desired_locale:
 | 
				
			||||||
 | 
					            system_locale = locale.getdefaultlocale()
 | 
				
			||||||
 | 
					            # If the system locale is not available to the runtime use English
 | 
				
			||||||
 | 
					            if not system_locale[0]:
 | 
				
			||||||
 | 
					                desired_locale = 'en_US'
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                desired_locale = system_locale[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
 | 
				
			||||||
 | 
					        lang = gettext.translation(domain,
 | 
				
			||||||
 | 
					                                   localedir=locale_dir,
 | 
				
			||||||
 | 
					                                   languages=[desired_locale],
 | 
				
			||||||
 | 
					                                   fallback=True)
 | 
				
			||||||
 | 
					        if six.PY3:
 | 
				
			||||||
 | 
					            translator = lang.gettext
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            translator = lang.ugettext
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        translated_message = translator(msgid)
 | 
				
			||||||
 | 
					        return translated_message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __mod__(self, other):
 | 
				
			||||||
 | 
					        # When we mod a Message we want the actual operation to be performed
 | 
				
			||||||
 | 
					        # by the parent class (i.e. unicode()), the only thing  we do here is
 | 
				
			||||||
 | 
					        # save the original msgid and the parameters in case of a translation
 | 
				
			||||||
 | 
					        params = self._sanitize_mod_params(other)
 | 
				
			||||||
 | 
					        unicode_mod = super(Message, self).__mod__(params)
 | 
				
			||||||
 | 
					        modded = Message(self.msgid,
 | 
				
			||||||
 | 
					                         msgtext=unicode_mod,
 | 
				
			||||||
 | 
					                         params=params,
 | 
				
			||||||
 | 
					                         domain=self.domain)
 | 
				
			||||||
 | 
					        return modded
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _sanitize_mod_params(self, other):
 | 
				
			||||||
 | 
					        """Sanitize the object being modded with this Message.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        - Add support for modding 'None' so translation supports it
 | 
				
			||||||
 | 
					        - Trim the modded object, which can be a large dictionary, to only
 | 
				
			||||||
 | 
					        those keys that would actually be used in a translation
 | 
				
			||||||
 | 
					        - Snapshot the object being modded, in case the message is
 | 
				
			||||||
 | 
					        translated, it will be used as it was when the Message was created
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if other is None:
 | 
				
			||||||
 | 
					            params = (other,)
 | 
				
			||||||
 | 
					        elif isinstance(other, dict):
 | 
				
			||||||
 | 
					            # Merge the dictionaries
 | 
				
			||||||
 | 
					            # Copy each item in case one does not support deep copy.
 | 
				
			||||||
 | 
					            params = {}
 | 
				
			||||||
 | 
					            if isinstance(self.params, dict):
 | 
				
			||||||
 | 
					                for key, val in self.params.items():
 | 
				
			||||||
 | 
					                    params[key] = self._copy_param(val)
 | 
				
			||||||
 | 
					            for key, val in other.items():
 | 
				
			||||||
 | 
					                params[key] = self._copy_param(val)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            params = self._copy_param(other)
 | 
				
			||||||
 | 
					        return params
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _copy_param(self, param):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return copy.deepcopy(param)
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            # Fallback to casting to unicode this will handle the
 | 
				
			||||||
 | 
					            # python code-like objects that can't be deep-copied
 | 
				
			||||||
 | 
					            return six.text_type(param)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __add__(self, other):
 | 
				
			||||||
 | 
					        msg = _('Message objects do not support addition.')
 | 
				
			||||||
 | 
					        raise TypeError(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __radd__(self, other):
 | 
				
			||||||
 | 
					        return self.__add__(other)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if six.PY2:
 | 
				
			||||||
 | 
					        def __str__(self):
 | 
				
			||||||
 | 
					            # NOTE(luisg): Logging in python 2.6 tries to str() log records,
 | 
				
			||||||
 | 
					            # and it expects specifically a UnicodeError in order to proceed.
 | 
				
			||||||
 | 
					            msg = _('Message objects do not support str() because they may '
 | 
				
			||||||
 | 
					                    'contain non-ascii characters. '
 | 
				
			||||||
 | 
					                    'Please use unicode() or translate() instead.')
 | 
				
			||||||
 | 
					            raise UnicodeError(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_available_languages(domain):
 | 
				
			||||||
 | 
					    """Lists the available languages for the given translation domain.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param domain: the domain to get languages for
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if domain in _AVAILABLE_LANGUAGES:
 | 
				
			||||||
 | 
					        return copy.copy(_AVAILABLE_LANGUAGES[domain])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    localedir = '%s_LOCALEDIR' % domain.upper()
 | 
				
			||||||
 | 
					    find = lambda x: gettext.find(domain,
 | 
				
			||||||
 | 
					                                  localedir=os.environ.get(localedir),
 | 
				
			||||||
 | 
					                                  languages=[x])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # NOTE(mrodden): en_US should always be available (and first in case
 | 
				
			||||||
 | 
					    # order matters) since our in-line message strings are en_US
 | 
				
			||||||
 | 
					    language_list = ['en_US']
 | 
				
			||||||
 | 
					    # NOTE(luisg): Babel <1.0 used a function called list(), which was
 | 
				
			||||||
 | 
					    # renamed to locale_identifiers() in >=1.0, the requirements master list
 | 
				
			||||||
 | 
					    # requires >=0.9.6, uncapped, so defensively work with both. We can remove
 | 
				
			||||||
 | 
					    # this check when the master list updates to >=1.0, and update all projects
 | 
				
			||||||
 | 
					    list_identifiers = (getattr(localedata, 'list', None) or
 | 
				
			||||||
 | 
					                        getattr(localedata, 'locale_identifiers'))
 | 
				
			||||||
 | 
					    locale_identifiers = list_identifiers()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for i in locale_identifiers:
 | 
				
			||||||
 | 
					        if find(i) is not None:
 | 
				
			||||||
 | 
					            language_list.append(i)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported
 | 
				
			||||||
 | 
					    # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they
 | 
				
			||||||
 | 
					    # are perfectly legitimate locales:
 | 
				
			||||||
 | 
					    #     https://github.com/mitsuhiko/babel/issues/37
 | 
				
			||||||
 | 
					    # In Babel 1.3 they fixed the bug and they support these locales, but
 | 
				
			||||||
 | 
					    # they are still not explicitly "listed" by locale_identifiers().
 | 
				
			||||||
 | 
					    # That is  why we add the locales here explicitly if necessary so that
 | 
				
			||||||
 | 
					    # they are listed as supported.
 | 
				
			||||||
 | 
					    aliases = {'zh': 'zh_CN',
 | 
				
			||||||
 | 
					               'zh_Hant_HK': 'zh_HK',
 | 
				
			||||||
 | 
					               'zh_Hant': 'zh_TW',
 | 
				
			||||||
 | 
					               'fil': 'tl_PH'}
 | 
				
			||||||
 | 
					    for (locale_, alias) in six.iteritems(aliases):
 | 
				
			||||||
 | 
					        if locale_ in language_list and alias not in language_list:
 | 
				
			||||||
 | 
					            language_list.append(alias)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _AVAILABLE_LANGUAGES[domain] = language_list
 | 
				
			||||||
 | 
					    return copy.copy(language_list)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def translate(obj, desired_locale=None):
 | 
				
			||||||
 | 
					    """Gets the translated unicode representation of the given object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    If the object is not translatable it is returned as-is.
 | 
				
			||||||
 | 
					    If the locale is None the object is translated to the system locale.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param obj: the object to translate
 | 
				
			||||||
 | 
					    :param desired_locale: the locale to translate the message to, if None the
 | 
				
			||||||
 | 
					                           default system locale will be used
 | 
				
			||||||
 | 
					    :returns: the translated object in unicode, or the original object if
 | 
				
			||||||
 | 
					              it could not be translated
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    message = obj
 | 
				
			||||||
 | 
					    if not isinstance(message, Message):
 | 
				
			||||||
 | 
					        # If the object to translate is not already translatable,
 | 
				
			||||||
 | 
					        # let's first get its unicode representation
 | 
				
			||||||
 | 
					        message = six.text_type(obj)
 | 
				
			||||||
 | 
					    if isinstance(message, Message):
 | 
				
			||||||
 | 
					        # Even after unicoding() we still need to check if we are
 | 
				
			||||||
 | 
					        # running with translatable unicode before translating
 | 
				
			||||||
 | 
					        return message.translate(desired_locale)
 | 
				
			||||||
 | 
					    return obj
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _translate_args(args, desired_locale=None):
 | 
				
			||||||
 | 
					    """Translates all the translatable elements of the given arguments object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This method is used for translating the translatable values in method
 | 
				
			||||||
 | 
					    arguments which include values of tuples or dictionaries.
 | 
				
			||||||
 | 
					    If the object is not a tuple or a dictionary the object itself is
 | 
				
			||||||
 | 
					    translated if it is translatable.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    If the locale is None the object is translated to the system locale.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param args: the args to translate
 | 
				
			||||||
 | 
					    :param desired_locale: the locale to translate the args to, if None the
 | 
				
			||||||
 | 
					                           default system locale will be used
 | 
				
			||||||
 | 
					    :returns: a new args object with the translated contents of the original
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if isinstance(args, tuple):
 | 
				
			||||||
 | 
					        return tuple(translate(v, desired_locale) for v in args)
 | 
				
			||||||
 | 
					    if isinstance(args, dict):
 | 
				
			||||||
 | 
					        translated_dict = {}
 | 
				
			||||||
 | 
					        for (k, v) in six.iteritems(args):
 | 
				
			||||||
 | 
					            translated_v = translate(v, desired_locale)
 | 
				
			||||||
 | 
					            translated_dict[k] = translated_v
 | 
				
			||||||
 | 
					        return translated_dict
 | 
				
			||||||
 | 
					    return translate(args, desired_locale)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TranslationHandler(handlers.MemoryHandler):
 | 
				
			||||||
 | 
					    """Handler that translates records before logging them.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The TranslationHandler takes a locale and a target logging.Handler object
 | 
				
			||||||
 | 
					    to forward LogRecord objects to after translating them. This handler
 | 
				
			||||||
 | 
					    depends on Message objects being logged, instead of regular strings.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The handler can be configured declaratively in the logging.conf as follows:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [handlers]
 | 
				
			||||||
 | 
					        keys = translatedlog, translator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [handler_translatedlog]
 | 
				
			||||||
 | 
					        class = handlers.WatchedFileHandler
 | 
				
			||||||
 | 
					        args = ('/var/log/api-localized.log',)
 | 
				
			||||||
 | 
					        formatter = context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [handler_translator]
 | 
				
			||||||
 | 
					        class = openstack.common.log.TranslationHandler
 | 
				
			||||||
 | 
					        target = translatedlog
 | 
				
			||||||
 | 
					        args = ('zh_CN',)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    If the specified locale is not available in the system, the handler will
 | 
				
			||||||
 | 
					    log in the default locale.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, locale=None, target=None):
 | 
				
			||||||
 | 
					        """Initialize a TranslationHandler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param locale: locale to use for translating messages
 | 
				
			||||||
 | 
					        :param target: logging.Handler object to forward
 | 
				
			||||||
 | 
					                       LogRecord objects to after translation
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        # NOTE(luisg): In order to allow this handler to be a wrapper for
 | 
				
			||||||
 | 
					        # other handlers, such as a FileHandler, and still be able to
 | 
				
			||||||
 | 
					        # configure it using logging.conf, this handler has to extend
 | 
				
			||||||
 | 
					        # MemoryHandler because only the MemoryHandlers' logging.conf
 | 
				
			||||||
 | 
					        # parsing is implemented such that it accepts a target handler.
 | 
				
			||||||
 | 
					        handlers.MemoryHandler.__init__(self, capacity=0, target=target)
 | 
				
			||||||
 | 
					        self.locale = locale
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setFormatter(self, fmt):
 | 
				
			||||||
 | 
					        self.target.setFormatter(fmt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def emit(self, record):
 | 
				
			||||||
 | 
					        # We save the message from the original record to restore it
 | 
				
			||||||
 | 
					        # after translation, so other handlers are not affected by this
 | 
				
			||||||
 | 
					        original_msg = record.msg
 | 
				
			||||||
 | 
					        original_args = record.args
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self._translate_and_log_record(record)
 | 
				
			||||||
 | 
					        finally:
 | 
				
			||||||
 | 
					            record.msg = original_msg
 | 
				
			||||||
 | 
					            record.args = original_args
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _translate_and_log_record(self, record):
 | 
				
			||||||
 | 
					        record.msg = translate(record.msg, self.locale)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # In addition to translating the message, we also need to translate
 | 
				
			||||||
 | 
					        # arguments that were passed to the log method that were not part
 | 
				
			||||||
 | 
					        # of the main message e.g., log.info(_('Some message %s'), this_one))
 | 
				
			||||||
 | 
					        record.args = _translate_args(record.args, self.locale)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.target.emit(record)
 | 
				
			||||||
							
								
								
									
										73
									
								
								rallyclient/openstack/common/importutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								rallyclient/openstack/common/importutils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					# Copyright 2011 OpenStack Foundation.
 | 
				
			||||||
 | 
					# 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 related utilities and helper functions.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import traceback
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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)
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return getattr(sys.modules[mod_str], class_str)
 | 
				
			||||||
 | 
					    except AttributeError:
 | 
				
			||||||
 | 
					        raise ImportError('Class %s cannot be found (%s)' %
 | 
				
			||||||
 | 
					                          (class_str,
 | 
				
			||||||
 | 
					                           traceback.format_exception(*sys.exc_info())))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def import_object(import_str, *args, **kwargs):
 | 
				
			||||||
 | 
					    """Import a class and return an instance of it."""
 | 
				
			||||||
 | 
					    return import_class(import_str)(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def import_object_ns(name_space, import_str, *args, **kwargs):
 | 
				
			||||||
 | 
					    """Tries to import object from default namespace.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Imports a class and return an instance of it, first by trying
 | 
				
			||||||
 | 
					    to find the class in a default namespace, then failing back to
 | 
				
			||||||
 | 
					    a full path if not found in the default namespace.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    import_value = "%s.%s" % (name_space, import_str)
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return import_class(import_value)(*args, **kwargs)
 | 
				
			||||||
 | 
					    except ImportError:
 | 
				
			||||||
 | 
					        return import_class(import_str)(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def import_module(import_str):
 | 
				
			||||||
 | 
					    """Import a module."""
 | 
				
			||||||
 | 
					    __import__(import_str)
 | 
				
			||||||
 | 
					    return sys.modules[import_str]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def import_versioned_module(version, submodule=None):
 | 
				
			||||||
 | 
					    module = 'rallyclient.v%s' % version
 | 
				
			||||||
 | 
					    if submodule:
 | 
				
			||||||
 | 
					        module = '.'.join((module, submodule))
 | 
				
			||||||
 | 
					    return import_module(module)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def try_import(import_str, default=None):
 | 
				
			||||||
 | 
					    """Try to import a module and if it fails return default."""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return import_module(import_str)
 | 
				
			||||||
 | 
					    except ImportError:
 | 
				
			||||||
 | 
					        return default
 | 
				
			||||||
							
								
								
									
										295
									
								
								rallyclient/openstack/common/strutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								rallyclient/openstack/common/strutils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,295 @@
 | 
				
			|||||||
 | 
					# Copyright 2011 OpenStack Foundation.
 | 
				
			||||||
 | 
					# 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					System-level utilities and helper functions.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import math
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import unicodedata
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import six
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rallyclient.openstack.common.gettextutils import _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					UNIT_PREFIX_EXPONENT = {
 | 
				
			||||||
 | 
					    'k': 1,
 | 
				
			||||||
 | 
					    'K': 1,
 | 
				
			||||||
 | 
					    'Ki': 1,
 | 
				
			||||||
 | 
					    'M': 2,
 | 
				
			||||||
 | 
					    'Mi': 2,
 | 
				
			||||||
 | 
					    'G': 3,
 | 
				
			||||||
 | 
					    'Gi': 3,
 | 
				
			||||||
 | 
					    'T': 4,
 | 
				
			||||||
 | 
					    'Ti': 4,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					UNIT_SYSTEM_INFO = {
 | 
				
			||||||
 | 
					    'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')),
 | 
				
			||||||
 | 
					    'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
 | 
				
			||||||
 | 
					FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]")
 | 
				
			||||||
 | 
					SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# NOTE(flaper87): The following 3 globals are used by `mask_password`
 | 
				
			||||||
 | 
					_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# NOTE(ldbragst): Let's build a list of regex objects using the list of
 | 
				
			||||||
 | 
					# _SANITIZE_KEYS we already have. This way, we only have to add the new key
 | 
				
			||||||
 | 
					# to the list of _SANITIZE_KEYS and we can generate regular expressions
 | 
				
			||||||
 | 
					# for XML and JSON automatically.
 | 
				
			||||||
 | 
					_SANITIZE_PATTERNS = []
 | 
				
			||||||
 | 
					_FORMAT_PATTERNS = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])',
 | 
				
			||||||
 | 
					                    r'(<%(key)s>).*?(</%(key)s>)',
 | 
				
			||||||
 | 
					                    r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])',
 | 
				
			||||||
 | 
					                    r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])',
 | 
				
			||||||
 | 
					                    r'([\'"].*?%(key)s[\'"]\s*,\s*\'--?[A-z]+\'\s*,\s*u?[\'"])'
 | 
				
			||||||
 | 
					                    '.*?([\'"])',
 | 
				
			||||||
 | 
					                    r'(%(key)s\s*--?[A-z]+\s*)\S+(\s*)']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					for key in _SANITIZE_KEYS:
 | 
				
			||||||
 | 
					    for pattern in _FORMAT_PATTERNS:
 | 
				
			||||||
 | 
					        reg_ex = re.compile(pattern % {'key': key}, re.DOTALL)
 | 
				
			||||||
 | 
					        _SANITIZE_PATTERNS.append(reg_ex)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def int_from_bool_as_string(subject):
 | 
				
			||||||
 | 
					    """Interpret a string as a boolean and return either 1 or 0.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Any string value in:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ('True', 'true', 'On', 'on', '1')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    is interpreted as a boolean True.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Useful for JSON-decoded stuff and config file parsing
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return bool_from_string(subject) and 1 or 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def bool_from_string(subject, strict=False, default=False):
 | 
				
			||||||
 | 
					    """Interpret a string as a boolean.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    A case-insensitive match is performed such that strings matching 't',
 | 
				
			||||||
 | 
					    'true', 'on', 'y', 'yes', or '1' are considered True and, when
 | 
				
			||||||
 | 
					    `strict=False`, anything else returns the value specified by 'default'.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Useful for JSON-decoded stuff and config file parsing.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    If `strict=True`, unrecognized values, including None, will raise a
 | 
				
			||||||
 | 
					    ValueError which is useful when parsing values passed in from an API call.
 | 
				
			||||||
 | 
					    Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if not isinstance(subject, six.string_types):
 | 
				
			||||||
 | 
					        subject = six.text_type(subject)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lowered = subject.strip().lower()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if lowered in TRUE_STRINGS:
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					    elif lowered in FALSE_STRINGS:
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    elif strict:
 | 
				
			||||||
 | 
					        acceptable = ', '.join(
 | 
				
			||||||
 | 
					            "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS))
 | 
				
			||||||
 | 
					        msg = _("Unrecognized value '%(val)s', acceptable values are:"
 | 
				
			||||||
 | 
					                " %(acceptable)s") % {'val': subject,
 | 
				
			||||||
 | 
					                                      'acceptable': acceptable}
 | 
				
			||||||
 | 
					        raise ValueError(msg)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return default
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def safe_decode(text, incoming=None, errors='strict'):
 | 
				
			||||||
 | 
					    """Decodes incoming text/bytes string using `incoming` if they're not
 | 
				
			||||||
 | 
					       already unicode.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param incoming: Text's current encoding
 | 
				
			||||||
 | 
					    :param errors: Errors handling policy. See here for valid
 | 
				
			||||||
 | 
					        values http://docs.python.org/2/library/codecs.html
 | 
				
			||||||
 | 
					    :returns: text or a unicode `incoming` encoded
 | 
				
			||||||
 | 
					                representation of it.
 | 
				
			||||||
 | 
					    :raises TypeError: If text is not an instance of str
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if not isinstance(text, (six.string_types, six.binary_type)):
 | 
				
			||||||
 | 
					        raise TypeError("%s can't be decoded" % type(text))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if isinstance(text, six.text_type):
 | 
				
			||||||
 | 
					        return text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not incoming:
 | 
				
			||||||
 | 
					        incoming = (sys.stdin.encoding or
 | 
				
			||||||
 | 
					                    sys.getdefaultencoding())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return text.decode(incoming, errors)
 | 
				
			||||||
 | 
					    except UnicodeDecodeError:
 | 
				
			||||||
 | 
					        # Note(flaper87) If we get here, it means that
 | 
				
			||||||
 | 
					        # sys.stdin.encoding / sys.getdefaultencoding
 | 
				
			||||||
 | 
					        # didn't return a suitable encoding to decode
 | 
				
			||||||
 | 
					        # text. This happens mostly when global LANG
 | 
				
			||||||
 | 
					        # var is not set correctly and there's no
 | 
				
			||||||
 | 
					        # default encoding. In this case, most likely
 | 
				
			||||||
 | 
					        # python will use ASCII or ANSI encoders as
 | 
				
			||||||
 | 
					        # default encodings but they won't be capable
 | 
				
			||||||
 | 
					        # of decoding non-ASCII characters.
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # Also, UTF-8 is being used since it's an ASCII
 | 
				
			||||||
 | 
					        # extension.
 | 
				
			||||||
 | 
					        return text.decode('utf-8', errors)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def safe_encode(text, incoming=None,
 | 
				
			||||||
 | 
					                encoding='utf-8', errors='strict'):
 | 
				
			||||||
 | 
					    """Encodes incoming text/bytes string using `encoding`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    If incoming is not specified, text is expected to be encoded with
 | 
				
			||||||
 | 
					    current python's default encoding. (`sys.getdefaultencoding`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param incoming: Text's current encoding
 | 
				
			||||||
 | 
					    :param encoding: Expected encoding for text (Default UTF-8)
 | 
				
			||||||
 | 
					    :param errors: Errors handling policy. See here for valid
 | 
				
			||||||
 | 
					        values http://docs.python.org/2/library/codecs.html
 | 
				
			||||||
 | 
					    :returns: text or a bytestring `encoding` encoded
 | 
				
			||||||
 | 
					                representation of it.
 | 
				
			||||||
 | 
					    :raises TypeError: If text is not an instance of str
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if not isinstance(text, (six.string_types, six.binary_type)):
 | 
				
			||||||
 | 
					        raise TypeError("%s can't be encoded" % type(text))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not incoming:
 | 
				
			||||||
 | 
					        incoming = (sys.stdin.encoding or
 | 
				
			||||||
 | 
					                    sys.getdefaultencoding())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if isinstance(text, six.text_type):
 | 
				
			||||||
 | 
					        return text.encode(encoding, errors)
 | 
				
			||||||
 | 
					    elif text and encoding != incoming:
 | 
				
			||||||
 | 
					        # Decode text before encoding it with `encoding`
 | 
				
			||||||
 | 
					        text = safe_decode(text, incoming, errors)
 | 
				
			||||||
 | 
					        return text.encode(encoding, errors)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def string_to_bytes(text, unit_system='IEC', return_int=False):
 | 
				
			||||||
 | 
					    """Converts a string into an float representation of bytes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The units supported for IEC ::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it)
 | 
				
			||||||
 | 
					        KB, KiB, MB, MiB, GB, GiB, TB, TiB
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The units supported for SI ::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        kb(it), Mb(it), Gb(it), Tb(it)
 | 
				
			||||||
 | 
					        kB, MB, GB, TB
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Note that the SI unit system does not support capital letter 'K'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param text: String input for bytes size conversion.
 | 
				
			||||||
 | 
					    :param unit_system: Unit system for byte size conversion.
 | 
				
			||||||
 | 
					    :param return_int: If True, returns integer representation of text
 | 
				
			||||||
 | 
					                       in bytes. (default: decimal)
 | 
				
			||||||
 | 
					    :returns: Numerical representation of text in bytes.
 | 
				
			||||||
 | 
					    :raises ValueError: If text has an invalid value.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        base, reg_ex = UNIT_SYSTEM_INFO[unit_system]
 | 
				
			||||||
 | 
					    except KeyError:
 | 
				
			||||||
 | 
					        msg = _('Invalid unit system: "%s"') % unit_system
 | 
				
			||||||
 | 
					        raise ValueError(msg)
 | 
				
			||||||
 | 
					    match = reg_ex.match(text)
 | 
				
			||||||
 | 
					    if match:
 | 
				
			||||||
 | 
					        magnitude = float(match.group(1))
 | 
				
			||||||
 | 
					        unit_prefix = match.group(2)
 | 
				
			||||||
 | 
					        if match.group(3) in ['b', 'bit']:
 | 
				
			||||||
 | 
					            magnitude /= 8
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        msg = _('Invalid string format: %s') % text
 | 
				
			||||||
 | 
					        raise ValueError(msg)
 | 
				
			||||||
 | 
					    if not unit_prefix:
 | 
				
			||||||
 | 
					        res = magnitude
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix])
 | 
				
			||||||
 | 
					    if return_int:
 | 
				
			||||||
 | 
					        return int(math.ceil(res))
 | 
				
			||||||
 | 
					    return res
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def to_slug(value, incoming=None, errors="strict"):
 | 
				
			||||||
 | 
					    """Normalize string.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Convert to lowercase, remove non-word characters, and convert spaces
 | 
				
			||||||
 | 
					    to hyphens.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Inspired by Django's `slugify` filter.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param value: Text to slugify
 | 
				
			||||||
 | 
					    :param incoming: Text's current encoding
 | 
				
			||||||
 | 
					    :param errors: Errors handling policy. See here for valid
 | 
				
			||||||
 | 
					        values http://docs.python.org/2/library/codecs.html
 | 
				
			||||||
 | 
					    :returns: slugified unicode representation of `value`
 | 
				
			||||||
 | 
					    :raises TypeError: If text is not an instance of str
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    value = safe_decode(value, incoming, errors)
 | 
				
			||||||
 | 
					    # NOTE(aababilov): no need to use safe_(encode|decode) here:
 | 
				
			||||||
 | 
					    # encodings are always "ascii", error handling is always "ignore"
 | 
				
			||||||
 | 
					    # and types are always known (first: unicode; second: str)
 | 
				
			||||||
 | 
					    value = unicodedata.normalize("NFKD", value).encode(
 | 
				
			||||||
 | 
					        "ascii", "ignore").decode("ascii")
 | 
				
			||||||
 | 
					    value = SLUGIFY_STRIP_RE.sub("", value).strip().lower()
 | 
				
			||||||
 | 
					    return SLUGIFY_HYPHENATE_RE.sub("-", value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def mask_password(message, secret="***"):
 | 
				
			||||||
 | 
					    """Replace password with 'secret' in message.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param message: The string which includes security information.
 | 
				
			||||||
 | 
					    :param secret: value with which to replace passwords.
 | 
				
			||||||
 | 
					    :returns: The unicode value of message with the password fields masked.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    For example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    >>> mask_password("'adminPass' : 'aaaaa'")
 | 
				
			||||||
 | 
					    "'adminPass' : '***'"
 | 
				
			||||||
 | 
					    >>> mask_password("'admin_pass' : 'aaaaa'")
 | 
				
			||||||
 | 
					    "'admin_pass' : '***'"
 | 
				
			||||||
 | 
					    >>> mask_password('"password" : "aaaaa"')
 | 
				
			||||||
 | 
					    '"password" : "***"'
 | 
				
			||||||
 | 
					    >>> mask_password("'original_password' : 'aaaaa'")
 | 
				
			||||||
 | 
					    "'original_password' : '***'"
 | 
				
			||||||
 | 
					    >>> mask_password("u'original_password' :   u'aaaaa'")
 | 
				
			||||||
 | 
					    "u'original_password' :   u'***'"
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    message = six.text_type(message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # NOTE(ldbragst): Check to see if anything in message contains any key
 | 
				
			||||||
 | 
					    # specified in _SANITIZE_KEYS, if not then just return the message since
 | 
				
			||||||
 | 
					    # we don't have to mask any passwords.
 | 
				
			||||||
 | 
					    if not any(key in message for key in _SANITIZE_KEYS):
 | 
				
			||||||
 | 
					        return message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    secret = r'\g<1>' + secret + r'\g<2>'
 | 
				
			||||||
 | 
					    for pattern in _SANITIZE_PATTERNS:
 | 
				
			||||||
 | 
					        message = re.sub(pattern, secret, message)
 | 
				
			||||||
 | 
					    return message
 | 
				
			||||||
							
								
								
									
										37
									
								
								rallyclient/openstack/common/uuidutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								rallyclient/openstack/common/uuidutils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					# Copyright (c) 2012 Intel Corporation.
 | 
				
			||||||
 | 
					# 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					UUID related utilities and helper functions.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import uuid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def generate_uuid():
 | 
				
			||||||
 | 
					    return str(uuid.uuid4())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def is_uuid_like(val):
 | 
				
			||||||
 | 
					    """Returns validation of a value as a UUID.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    For our purposes, a UUID is a canonical form string:
 | 
				
			||||||
 | 
					    aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return str(uuid.UUID(val)) == val
 | 
				
			||||||
 | 
					    except (TypeError, ValueError, AttributeError):
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
							
								
								
									
										4
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					argparse
 | 
				
			||||||
 | 
					oslo.i18n>=0.1.0
 | 
				
			||||||
 | 
					pbr>=0.6,!=0.7,<1.0
 | 
				
			||||||
 | 
					six>=1.7.0
 | 
				
			||||||
							
								
								
									
										33
									
								
								setup.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								setup.cfg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					[metadata]
 | 
				
			||||||
 | 
					name = python-rallyclient
 | 
				
			||||||
 | 
					summary = Client library for Rally API
 | 
				
			||||||
 | 
					description-file = README.rst
 | 
				
			||||||
 | 
					author = OpenStack
 | 
				
			||||||
 | 
					author-email = openstack-dev@lists.openstack.org
 | 
				
			||||||
 | 
					home-page = http://www.openstack.org/
 | 
				
			||||||
 | 
					classifier =
 | 
				
			||||||
 | 
					    Environment :: OpenStack
 | 
				
			||||||
 | 
					    Intended Audience :: Information Technology
 | 
				
			||||||
 | 
					    Intended Audience :: System Administrators
 | 
				
			||||||
 | 
					    License :: OSI Approved :: Apache Software License
 | 
				
			||||||
 | 
					    Operating System :: POSIX :: Linux
 | 
				
			||||||
 | 
					    Programming Language :: Python
 | 
				
			||||||
 | 
					    Programming Language :: Python :: 2
 | 
				
			||||||
 | 
					    Programming Language :: Python :: 2.7
 | 
				
			||||||
 | 
					    Programming Language :: Python :: 2.6
 | 
				
			||||||
 | 
					    Programming Language :: Python :: 3
 | 
				
			||||||
 | 
					    Programming Language :: Python :: 3.3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[files]
 | 
				
			||||||
 | 
					packages = rallyclient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[pbr]
 | 
				
			||||||
 | 
					autodoc_index_modules = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[build_sphinx]
 | 
				
			||||||
 | 
					all_files = 1
 | 
				
			||||||
 | 
					build-dir = doc/build
 | 
				
			||||||
 | 
					source-dir = doc/source
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[wheel]
 | 
				
			||||||
 | 
					universal = 1
 | 
				
			||||||
							
								
								
									
										22
									
								
								setup.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										22
									
								
								setup.py
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env python
 | 
				
			||||||
 | 
					# Copyright (c) 2014 Mirantis 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
 | 
				
			||||||
 | 
					import setuptools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					setuptools.setup(
 | 
				
			||||||
 | 
					    setup_requires=['pbr'],
 | 
				
			||||||
 | 
					    pbr=True)
 | 
				
			||||||
							
								
								
									
										11
									
								
								test-requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								test-requirements.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					coverage>=3.6
 | 
				
			||||||
 | 
					discover
 | 
				
			||||||
 | 
					fixtures>=0.3.14
 | 
				
			||||||
 | 
					hacking>=0.9.2,<0.10
 | 
				
			||||||
 | 
					mock>=1.0
 | 
				
			||||||
 | 
					oslosphinx
 | 
				
			||||||
 | 
					oslotest
 | 
				
			||||||
 | 
					python-subunit>=0.0.18
 | 
				
			||||||
 | 
					sphinx>=1.1.2,!=1.2.0,<1.3
 | 
				
			||||||
 | 
					testrepository>=0.0.18
 | 
				
			||||||
 | 
					testtools>=0.9.34
 | 
				
			||||||
							
								
								
									
										0
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										28
									
								
								tests/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								tests/base.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					# Copyright 2014: Mirantis 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 fixtures
 | 
				
			||||||
 | 
					from oslotest import base
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestCase(base.BaseTestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    """Test case base class for all unit tests."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        """Run before each test method to initialize test environment."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super(TestCase, self).setUp()
 | 
				
			||||||
 | 
					        self.log_fixture = self.useFixture(fixtures.FakeLogger())
 | 
				
			||||||
							
								
								
									
										32
									
								
								tests/test_rallyclient.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								tests/test_rallyclient.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					# 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					test_rally_client
 | 
				
			||||||
 | 
					-----------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Tests for `rallyclient` module.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from tests import base
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOG = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestRallyclient(base.TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_can_use_logging(self):
 | 
				
			||||||
 | 
					        # Just showing that we can import and use logging
 | 
				
			||||||
 | 
					        LOG.info('Nothing to see here.')
 | 
				
			||||||
							
								
								
									
										0
									
								
								tools/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tools/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										212
									
								
								tools/install_venv_common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								tools/install_venv_common.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,212 @@
 | 
				
			|||||||
 | 
					# -*- coding: utf-8 -*-
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Copyright 2013 OpenStack Foundation
 | 
				
			||||||
 | 
					# Copyright 2013 IBM Corp.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#    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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""Provides methods needed by installation script for OpenStack development
 | 
				
			||||||
 | 
					virtual environments.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Since this script is used to bootstrap a virtualenv from the system's Python
 | 
				
			||||||
 | 
					environment, it should be kept strictly compatible with Python 2.6.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Synced in from openstack-common
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from __future__ import print_function
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import optparse
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class InstallVenv(object):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, root, venv, requirements,
 | 
				
			||||||
 | 
					                 test_requirements, py_version,
 | 
				
			||||||
 | 
					                 project):
 | 
				
			||||||
 | 
					        self.root = root
 | 
				
			||||||
 | 
					        self.venv = venv
 | 
				
			||||||
 | 
					        self.requirements = requirements
 | 
				
			||||||
 | 
					        self.test_requirements = test_requirements
 | 
				
			||||||
 | 
					        self.py_version = py_version
 | 
				
			||||||
 | 
					        self.project = project
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def die(self, message, *args):
 | 
				
			||||||
 | 
					        print(message % args, file=sys.stderr)
 | 
				
			||||||
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check_python_version(self):
 | 
				
			||||||
 | 
					        if sys.version_info < (2, 6):
 | 
				
			||||||
 | 
					            self.die("Need Python Version >= 2.6")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run_command_with_code(self, cmd, redirect_output=True,
 | 
				
			||||||
 | 
					                              check_exit_code=True):
 | 
				
			||||||
 | 
					        """Runs a command in an out-of-process shell.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns the output of that command. Working directory is self.root.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if redirect_output:
 | 
				
			||||||
 | 
					            stdout = subprocess.PIPE
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            stdout = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout)
 | 
				
			||||||
 | 
					        output = proc.communicate()[0]
 | 
				
			||||||
 | 
					        if check_exit_code and proc.returncode != 0:
 | 
				
			||||||
 | 
					            self.die('Command "%s" failed.\n%s', ' '.join(cmd), output)
 | 
				
			||||||
 | 
					        return (output, proc.returncode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run_command(self, cmd, redirect_output=True, check_exit_code=True):
 | 
				
			||||||
 | 
					        return self.run_command_with_code(cmd, redirect_output,
 | 
				
			||||||
 | 
					                                          check_exit_code)[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_distro(self):
 | 
				
			||||||
 | 
					        if (os.path.exists('/etc/fedora-release') or
 | 
				
			||||||
 | 
					                os.path.exists('/etc/redhat-release')):
 | 
				
			||||||
 | 
					            return Fedora(
 | 
				
			||||||
 | 
					                self.root, self.venv, self.requirements,
 | 
				
			||||||
 | 
					                self.test_requirements, self.py_version, self.project)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return Distro(
 | 
				
			||||||
 | 
					                self.root, self.venv, self.requirements,
 | 
				
			||||||
 | 
					                self.test_requirements, self.py_version, self.project)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check_dependencies(self):
 | 
				
			||||||
 | 
					        self.get_distro().install_virtualenv()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def create_virtualenv(self, no_site_packages=True):
 | 
				
			||||||
 | 
					        """Creates the virtual environment and installs PIP.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Creates the virtual environment and installs PIP only into the
 | 
				
			||||||
 | 
					        virtual environment.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not os.path.isdir(self.venv):
 | 
				
			||||||
 | 
					            print('Creating venv...', end=' ')
 | 
				
			||||||
 | 
					            if no_site_packages:
 | 
				
			||||||
 | 
					                self.run_command(['virtualenv', '-q', '--no-site-packages',
 | 
				
			||||||
 | 
					                                 self.venv])
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                self.run_command(['virtualenv', '-q', self.venv])
 | 
				
			||||||
 | 
					            print('done.')
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            print("venv already exists...")
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def pip_install(self, *args):
 | 
				
			||||||
 | 
					        self.run_command(['tools/with_venv.sh',
 | 
				
			||||||
 | 
					                         'pip', 'install', '--upgrade'] + list(args),
 | 
				
			||||||
 | 
					                         redirect_output=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def install_dependencies(self):
 | 
				
			||||||
 | 
					        print('Installing dependencies with pip (this can take a while)...')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # First things first, make sure our venv has the latest pip and
 | 
				
			||||||
 | 
					        # setuptools.
 | 
				
			||||||
 | 
					        self.pip_install('pip>=1.3')
 | 
				
			||||||
 | 
					        self.pip_install('setuptools')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.pip_install('-r', self.requirements)
 | 
				
			||||||
 | 
					        self.pip_install('-r', self.test_requirements)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post_process(self):
 | 
				
			||||||
 | 
					        self.get_distro().post_process()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def parse_args(self, argv):
 | 
				
			||||||
 | 
					        """Parses command-line arguments."""
 | 
				
			||||||
 | 
					        parser = optparse.OptionParser()
 | 
				
			||||||
 | 
					        parser.add_option('-n', '--no-site-packages',
 | 
				
			||||||
 | 
					                          action='store_true',
 | 
				
			||||||
 | 
					                          help="Do not inherit packages from global Python "
 | 
				
			||||||
 | 
					                               "install")
 | 
				
			||||||
 | 
					        return parser.parse_args(argv[1:])[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Distro(InstallVenv):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check_cmd(self, cmd):
 | 
				
			||||||
 | 
					        return bool(self.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...', end=' ')
 | 
				
			||||||
 | 
					            if self.run_command(['easy_install', 'virtualenv']):
 | 
				
			||||||
 | 
					                print('Succeeded')
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                print('Failed')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.die('ERROR: virtualenv not found.\n\n%s development'
 | 
				
			||||||
 | 
					                 ' requires virtualenv, please install it using your'
 | 
				
			||||||
 | 
					                 ' favorite package management tool' % self.project)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 Fedora(Distro):
 | 
				
			||||||
 | 
					    """This covers all Fedora-based distributions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Includes: Fedora, RHEL, CentOS, Scientific Linux
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check_pkg(self, pkg):
 | 
				
			||||||
 | 
					        return self.run_command_with_code(['rpm', '-q', pkg],
 | 
				
			||||||
 | 
					                                          check_exit_code=False)[1] == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def apply_patch(self, originalfile, patchfile):
 | 
				
			||||||
 | 
					        self.run_command(['patch', '-N', originalfile, patchfile],
 | 
				
			||||||
 | 
					                         check_exit_code=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def install_virtualenv(self):
 | 
				
			||||||
 | 
					        if self.check_cmd('virtualenv'):
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not self.check_pkg('python-virtualenv'):
 | 
				
			||||||
 | 
					            self.die("Please install 'python-virtualenv'.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super(Fedora, self).install_virtualenv()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post_process(self):
 | 
				
			||||||
 | 
					        """Workaround for a bug in eventlet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This currently affects RHEL6.1, but the fix can safely be
 | 
				
			||||||
 | 
					        applied to all RHEL and Fedora distributions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This can be removed when the fix is applied upstream.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Nova: https://bugs.launchpad.net/nova/+bug/884915
 | 
				
			||||||
 | 
					        Upstream: https://bitbucket.org/eventlet/eventlet/issue/89
 | 
				
			||||||
 | 
					        RHEL: https://bugzilla.redhat.com/958868
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Install "patch" program if it's not there
 | 
				
			||||||
 | 
					        if not self.check_pkg('patch'):
 | 
				
			||||||
 | 
					            self.die("Please install 'patch'.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Apply the eventlet patch
 | 
				
			||||||
 | 
					        self.apply_patch(os.path.join(self.venv, 'lib', self.py_version,
 | 
				
			||||||
 | 
					                                      'site-packages',
 | 
				
			||||||
 | 
					                                      'eventlet/green/subprocess.py'),
 | 
				
			||||||
 | 
					                         'contrib/redhat-eventlet.patch')
 | 
				
			||||||
							
								
								
									
										32
									
								
								tools/requirements_style_check.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										32
									
								
								tools/requirements_style_check.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Enforce the requirement that dependencies are listed in the input
 | 
				
			||||||
 | 
					# files in alphabetical order.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# FIXME(dhellmann): This doesn't deal with URL requirements very
 | 
				
			||||||
 | 
					# well. We should probably sort those on the egg-name, rather than the
 | 
				
			||||||
 | 
					# full line.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function check_file() {
 | 
				
			||||||
 | 
					    typeset f=$1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # We don't care about comment lines.
 | 
				
			||||||
 | 
					    grep -v '^#' $f > ${f}.unsorted
 | 
				
			||||||
 | 
					    sort -i -f ${f}.unsorted > ${f}.sorted
 | 
				
			||||||
 | 
					    diff -c ${f}.unsorted ${f}.sorted
 | 
				
			||||||
 | 
					    rc=$?
 | 
				
			||||||
 | 
					    rm -f ${f}.sorted ${f}.unsorted
 | 
				
			||||||
 | 
					    return $rc
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exit_code=0
 | 
				
			||||||
 | 
					for filename in $@
 | 
				
			||||||
 | 
					do
 | 
				
			||||||
 | 
					    check_file $filename
 | 
				
			||||||
 | 
					    if [ $? -ne 0 ]
 | 
				
			||||||
 | 
					    then
 | 
				
			||||||
 | 
					        echo "Please list requirements in $filename in alphabetical order" 1>&2
 | 
				
			||||||
 | 
					        exit_code=1
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					done
 | 
				
			||||||
 | 
					exit $exit_code
 | 
				
			||||||
							
								
								
									
										7
									
								
								tools/with_venv.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								tools/with_venv.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					tools_path=${tools_path:-$(dirname $0)}
 | 
				
			||||||
 | 
					venv_path=${venv_path:-${tools_path}}
 | 
				
			||||||
 | 
					venv_dir=${venv_name:-/../.venv}
 | 
				
			||||||
 | 
					TOOLS=${tools_path}
 | 
				
			||||||
 | 
					VENV=${venv:-${venv_path}/${venv_dir}}
 | 
				
			||||||
 | 
					source ${VENV}/bin/activate && "$@"
 | 
				
			||||||
							
								
								
									
										35
									
								
								tox.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								tox.ini
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					[tox]
 | 
				
			||||||
 | 
					minversion = 1.6
 | 
				
			||||||
 | 
					envlist = py26,py27,py33,pep8
 | 
				
			||||||
 | 
					skipsdist = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[testenv]
 | 
				
			||||||
 | 
					setenv = VIRTUAL_ENV={envdir}
 | 
				
			||||||
 | 
					install_command = pip install -U {opts} {packages}
 | 
				
			||||||
 | 
					deps =
 | 
				
			||||||
 | 
					    -r{toxinidir}/requirements.txt
 | 
				
			||||||
 | 
					    -r{toxinidir}/test-requirements.txt
 | 
				
			||||||
 | 
					commands =
 | 
				
			||||||
 | 
					    python setup.py testr --slowest --testr-args='{posargs}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tox:jenkins]
 | 
				
			||||||
 | 
					downloadcache = ~/cache/pip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[testenv:pep8]
 | 
				
			||||||
 | 
					commands =
 | 
				
			||||||
 | 
					    flake8 {posargs}
 | 
				
			||||||
 | 
					    {toxinidir}/tools/requirements_style_check.sh requirements.txt test-requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[testenv:cover]
 | 
				
			||||||
 | 
					commands = python setup.py testr --coverage --testr-args='{posargs}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[testenv:venv]
 | 
				
			||||||
 | 
					commands = {posargs}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[flake8]
 | 
				
			||||||
 | 
					ignore = E12
 | 
				
			||||||
 | 
					builtins = _
 | 
				
			||||||
 | 
					exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[hacking]
 | 
				
			||||||
 | 
					import_exceptions = testtools.matchers
 | 
				
			||||||
		Reference in New Issue
	
	Block a user