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:
		 Ilya Kharin
					Ilya Kharin
				
			
				
					committed by
					
						 Andrey Kurilin
						Andrey Kurilin
					
				
			
			
				
	
			
			
			 Andrey Kurilin
						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