Initial work for the CloudKitty client
Change-Id: Icfcd82c156c433911230fbc98865ba7b662024ee
This commit is contained in:
		
							
								
								
									
										7
									
								
								.coveragerc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.coveragerc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | [run] | ||||||
|  | branch = True | ||||||
|  | source = cloudkittyclient | ||||||
|  | omit = cloudkittyclient/tests/*,cloudkittyclient/openstack/*,cloudkittyclient/i18n.py | ||||||
|  |  | ||||||
|  | [report] | ||||||
|  | ignore-errors = True | ||||||
							
								
								
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | *.pyc | ||||||
|  | *.egg-info | ||||||
|  | *.log | ||||||
|  | build | ||||||
|  | .coverage | ||||||
|  | .coverage.* | ||||||
|  | .tox | ||||||
|  | cover | ||||||
|  | .testrepository | ||||||
|  | .venv | ||||||
|  | dist | ||||||
|  | *.egg | ||||||
|  | *.sw? | ||||||
							
								
								
									
										19
									
								
								.pylintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.pylintrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | [MASTER] | ||||||
|  | ignore=openstack,test | ||||||
|  |  | ||||||
|  | [MESSAGES CONTROL] | ||||||
|  | # C0111: Don't require docstrings on every method | ||||||
|  | # W0511: TODOs in code comments are fine. | ||||||
|  | # W0142: *args and **kwargs are fine. | ||||||
|  | # W0622: Redefining id is fine. | ||||||
|  | disable=C0111,W0511,W0142,W0622 | ||||||
|  |  | ||||||
|  | [BASIC] | ||||||
|  | # Don't require docstrings on tests. | ||||||
|  | no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$ | ||||||
|  |  | ||||||
|  | [Variables] | ||||||
|  | # List of additional names supposed to be defined in builtins. Remember that | ||||||
|  | # you should avoid to define new builtins when possible. | ||||||
|  | # _ is used by our localization | ||||||
|  | additional-builtins=_ | ||||||
							
								
								
									
										4
									
								
								.testr.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.testr.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | [DEFAULT] | ||||||
|  | test_command=${PYTHON:-python} -m subunit.run discover -t ./ ./cloudkittyclient/tests $LISTOPT $IDOPTION | ||||||
|  | test_id_option=--load-list $IDFILE | ||||||
|  | test_list_option=--list | ||||||
							
								
								
									
										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. | ||||||
							
								
								
									
										34
									
								
								README.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								README.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | Python bindings to the CloudKitty API | ||||||
|  | ===================================== | ||||||
|  |  | ||||||
|  | :version: 1.0 | ||||||
|  | :wiki: Wiki: `CloudKitty Wiki`_ | ||||||
|  | :IRC: #cloudkitty @ freenode | ||||||
|  |  | ||||||
|  |  | ||||||
|  | .. _CloudKitty Wiki: https://wiki.openstack.org/wiki/CloudKitty | ||||||
|  |  | ||||||
|  |  | ||||||
|  | python-cloudkittyclient | ||||||
|  | ======================= | ||||||
|  |  | ||||||
|  | This is a client library for CloudKitty built on the CloudKitty API. It | ||||||
|  | provides a Python API (the ``cloudkittyclient`` module) and a command-line | ||||||
|  | tool (``cloudkitty``). | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Status | ||||||
|  | ====== | ||||||
|  |  | ||||||
|  | This project is **highly** work in progress. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Roadmap | ||||||
|  | ======= | ||||||
|  |  | ||||||
|  | * Add some tests. | ||||||
|  | * Add some doc. | ||||||
|  | * Move from importutils to stevedore. | ||||||
|  | * Move from test to oslotest. | ||||||
|  | * Add a command-line tool. | ||||||
|  | * Global code improvement. | ||||||
							
								
								
									
										26
									
								
								cloudkittyclient/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								cloudkittyclient/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Copyright 2014 Objectif Libre | ||||||
|  | # | ||||||
|  | # 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. | ||||||
|  | # | ||||||
|  | # @author: François Magimel (linkid) | ||||||
|  |  | ||||||
|  | __all__ = ['__version__'] | ||||||
|  |  | ||||||
|  | import pbr.version | ||||||
|  |  | ||||||
|  | version_info = pbr.version.VersionInfo('python-cloudkittyclient') | ||||||
|  | try: | ||||||
|  |     __version__ = version_info.version_string() | ||||||
|  | except AttributeError: | ||||||
|  |     __version__ = None | ||||||
							
								
								
									
										66
									
								
								cloudkittyclient/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								cloudkittyclient/client.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Copyright 2014 Objectif Libre | ||||||
|  | # | ||||||
|  | # 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. | ||||||
|  | # | ||||||
|  | # @author: François Magimel (linkid) | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | OpenStack Client interface. Handles the REST calls and responses. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | from cloudkittyclient.common import auth as ks_auth | ||||||
|  | from cloudkittyclient.common import client | ||||||
|  | from cloudkittyclient.openstack.common import importutils | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_client(api_version, **kwargs): | ||||||
|  |     """Get an authenticated client. | ||||||
|  |  | ||||||
|  |     This is based on the credentials in the keyword args. | ||||||
|  |  | ||||||
|  |     :param api_version: the API version to use | ||||||
|  |     :param kwargs: keyword args containing credentials, either: | ||||||
|  |             * os_auth_token: pre-existing token to re-use | ||||||
|  |             * endpoint: CloudKitty API endpoint | ||||||
|  |             or: | ||||||
|  |             * os_username: name of user | ||||||
|  |             * os_password: user's password | ||||||
|  |             * os_auth_url: endpoint to authenticate against | ||||||
|  |             * os_tenant_name: name of tenant | ||||||
|  |     """ | ||||||
|  |     cli_kwargs = { | ||||||
|  |         'username': kwargs.get('os_username'), | ||||||
|  |         'password': kwargs.get('os_password'), | ||||||
|  |         'tenant_name': kwargs.get('os_tenant_name'), | ||||||
|  |         'token': kwargs.get('os_auth_token'), | ||||||
|  |         'auth_url': kwargs.get('os_auth_url'), | ||||||
|  |         'endpoint': kwargs.get('cloudkitty_url') | ||||||
|  |     } | ||||||
|  |     return Client(api_version, **cli_kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def Client(version, **kwargs): | ||||||
|  |     module = importutils.import_versioned_module(version, 'client') | ||||||
|  |     client_class = getattr(module, 'Client') | ||||||
|  |  | ||||||
|  |     keystone_auth = ks_auth.KeystoneAuthPlugin( | ||||||
|  |         username=kwargs.get('username'), | ||||||
|  |         password=kwargs.get('password'), | ||||||
|  |         tenant_name=kwargs.get('tenant_name'), | ||||||
|  |         token=kwargs.get('token'), | ||||||
|  |         auth_url=kwargs.get('auth_url'), | ||||||
|  |         endpoint=kwargs.get('endpoint')) | ||||||
|  |     http_client = client.HTTPClient(keystone_auth) | ||||||
|  |  | ||||||
|  |     return client_class(http_client) | ||||||
							
								
								
									
										0
									
								
								cloudkittyclient/common/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cloudkittyclient/common/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										79
									
								
								cloudkittyclient/common/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								cloudkittyclient/common/auth.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Copyright 2014 Objectif Libre | ||||||
|  | # | ||||||
|  | # 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. | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | Keystone auth plugin. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | from keystoneclient.v2_0 import client as ksclient | ||||||
|  |  | ||||||
|  | from cloudkittyclient.openstack.common.apiclient import auth | ||||||
|  | from cloudkittyclient.openstack.common.apiclient import exceptions | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class KeystoneAuthPlugin(auth.BaseAuthPlugin): | ||||||
|  |  | ||||||
|  |     opt_names = [ | ||||||
|  |         "username", | ||||||
|  |         "password", | ||||||
|  |         "tenant_name", | ||||||
|  |         "token", | ||||||
|  |         "auth_url", | ||||||
|  |         "endpoint" | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     def _do_authenticate(self, http_client): | ||||||
|  |         if self.opts.get('token') is None: | ||||||
|  |             ks_kwargs = { | ||||||
|  |                 'username': self.opts.get('username'), | ||||||
|  |                 'password': self.opts.get('password'), | ||||||
|  |                 'tenant_name': self.opts.get('tenant_name'), | ||||||
|  |                 'auth_url': self.opts.get('auth_url'), | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             self._ksclient = ksclient.Client(**ks_kwargs) | ||||||
|  |  | ||||||
|  |     def token_and_endpoint(self, endpoint_type, service_type): | ||||||
|  |         token = endpoint = None | ||||||
|  |  | ||||||
|  |         if self.opts.get('token') and self.opts.get('endpoint'): | ||||||
|  |             token = self.opts.get('token') | ||||||
|  |             endpoint = self.opts.get('endpoint') | ||||||
|  |  | ||||||
|  |         elif hasattr(self, '_ksclient'): | ||||||
|  |             token = self._ksclient.auth_token | ||||||
|  |             endpoint = (self.opts.get('endpoint') or | ||||||
|  |                         self._ksclient.service_catalog.url_for( | ||||||
|  |                             service_type=service_type, | ||||||
|  |                             endpoint_type=endpoint_type)) | ||||||
|  |  | ||||||
|  |         return (token, endpoint) | ||||||
|  |  | ||||||
|  |     def sufficient_options(self): | ||||||
|  |         """Check if all required options are present. | ||||||
|  |  | ||||||
|  |         :raises: AuthPluginOptionsMissing | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if self.opts.get('token'): | ||||||
|  |             lookup_table = ["token", "endpoint"] | ||||||
|  |         else: | ||||||
|  |             lookup_table = ["username", "password", "tenant_name", "auth_url"] | ||||||
|  |  | ||||||
|  |         missing = [opt | ||||||
|  |                    for opt in lookup_table | ||||||
|  |                    if not self.opts.get(opt)] | ||||||
|  |         if missing: | ||||||
|  |             raise exceptions.AuthPluginOptionsMissing(missing) | ||||||
							
								
								
									
										76
									
								
								cloudkittyclient/common/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								cloudkittyclient/common/client.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Copyright 2014 Objectif Libre | ||||||
|  | # | ||||||
|  | # 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. | ||||||
|  | # | ||||||
|  | # @author: François Magimel (linkid) | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | OpenStack Client interface. Handles the REST calls and responses. | ||||||
|  | Override the oslo-incubator one. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  | import time | ||||||
|  |  | ||||||
|  | from cloudkittyclient.common import exceptions | ||||||
|  | from cloudkittyclient.openstack.common.apiclient import client | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPClient(client.HTTPClient): | ||||||
|  |     """This client handles sending HTTP requests to OpenStack servers. | ||||||
|  |     [Overrider] | ||||||
|  |     """ | ||||||
|  |     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 | ||||||
							
								
								
									
										84
									
								
								cloudkittyclient/common/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								cloudkittyclient/common/exceptions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Copyright 2014 Objectif Libre | ||||||
|  | # | ||||||
|  | # 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. | ||||||
|  | # | ||||||
|  | # @author: François Magimel (linkid) | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | Exception definitions. | ||||||
|  | See cloudkittyclient.openstack.common.apiclient.exceptions. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | from cloudkittyclient.openstack.common.apiclient.exceptions import * | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # _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): | ||||||
|  |                 if isinstance(body.get("error"), dict): | ||||||
|  |                     error = body["error"] | ||||||
|  |                     kwargs["message"] = error.get("message") | ||||||
|  |                     kwargs["details"] = error.get("details") | ||||||
|  |                 elif "faultstring" in body and "faultcode" in body: | ||||||
|  |                     # WSME | ||||||
|  |                     kwargs["message"] = "%(faultcode)s: %(faultstring)s" % body | ||||||
|  |                     kwargs["details"] = body.get("debuginfo", "") | ||||||
|  |     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) | ||||||
							
								
								
									
										32
									
								
								cloudkittyclient/i18n.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								cloudkittyclient/i18n.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Copyright 2014 Objectif Libre | ||||||
|  | # | ||||||
|  | # Licensed under the Apache License, Version 2.0 (the "License"); you may | ||||||
|  | # not use this file except in compliance with the License. You may obtain | ||||||
|  | # a copy of the License at | ||||||
|  | # | ||||||
|  | #      http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  | # | ||||||
|  | # Unless required by applicable law or agreed to in writing, software | ||||||
|  | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||||
|  | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||||
|  | # License for the specific language governing permissions and limitations | ||||||
|  | # under the License. | ||||||
|  |  | ||||||
|  | from oslo import i18n | ||||||
|  |  | ||||||
|  | _translators = i18n.TranslatorFactory(domain='cloudkittyclient') | ||||||
|  | i18n.enable_lazy() | ||||||
|  |  | ||||||
|  | # 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 | ||||||
							
								
								
									
										0
									
								
								cloudkittyclient/openstack/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cloudkittyclient/openstack/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										17
									
								
								cloudkittyclient/openstack/common/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cloudkittyclient/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')) | ||||||
							
								
								
									
										221
									
								
								cloudkittyclient/openstack/common/apiclient/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								cloudkittyclient/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 cloudkittyclient.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 = "cloudkittyclient.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 | ||||||
|  |         """ | ||||||
							
								
								
									
										509
									
								
								cloudkittyclient/openstack/common/apiclient/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										509
									
								
								cloudkittyclient/openstack/common/apiclient/base.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,509 @@ | |||||||
|  | # 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 cloudkittyclient.openstack.common.apiclient import exceptions | ||||||
|  | from cloudkittyclient.openstack.common.gettextutils import _ | ||||||
|  | from cloudkittyclient.openstack.common import strutils | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  |  | ||||||
|  |     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) | ||||||
							
								
								
									
										363
									
								
								cloudkittyclient/openstack/common/apiclient/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										363
									
								
								cloudkittyclient/openstack/common/apiclient/client.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,363 @@ | |||||||
|  | # 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 cloudkittyclient.openstack.common.apiclient import exceptions | ||||||
|  | from cloudkittyclient.openstack.common.gettextutils import _ | ||||||
|  | from cloudkittyclient.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 = "cloudkittyclient.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
									
								
								cloudkittyclient/openstack/common/apiclient/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										466
									
								
								cloudkittyclient/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 cloudkittyclient.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) and isinstance(body.get("error"), dict): | ||||||
|  |                 error = body["error"] | ||||||
|  |                 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
									
								
								cloudkittyclient/openstack/common/apiclient/fake_client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								cloudkittyclient/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 cloudkittyclient.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, | ||||||
|  |         }) | ||||||
							
								
								
									
										479
									
								
								cloudkittyclient/openstack/common/gettextutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										479
									
								
								cloudkittyclient/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 cloudkittyclient.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('cloudkittyclient') | ||||||
|  |  | ||||||
|  | # 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='cloudkittyclient', *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
									
								
								cloudkittyclient/openstack/common/importutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								cloudkittyclient/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 = 'cloudkittyclient.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
									
								
								cloudkittyclient/openstack/common/strutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								cloudkittyclient/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 cloudkittyclient.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 | ||||||
							
								
								
									
										99
									
								
								cloudkittyclient/openstack/common/test.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								cloudkittyclient/openstack/common/test.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. | ||||||
|  | # 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. | ||||||
|  |  | ||||||
|  | ############################################################################## | ||||||
|  | ############################################################################## | ||||||
|  | # | ||||||
|  | # DO NOT MODIFY THIS FILE | ||||||
|  | # | ||||||
|  | # This file is being graduated to the oslotest library. Please make all | ||||||
|  | # changes there, and only backport critical fixes here. - dhellmann | ||||||
|  | # | ||||||
|  | ############################################################################## | ||||||
|  | ############################################################################## | ||||||
|  |  | ||||||
|  | """Common utilities used in testing""" | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  | import os | ||||||
|  | import tempfile | ||||||
|  |  | ||||||
|  | import fixtures | ||||||
|  | import testtools | ||||||
|  |  | ||||||
|  | _TRUE_VALUES = ('True', 'true', '1', 'yes') | ||||||
|  | _LOG_FORMAT = "%(levelname)8s [%(name)s] %(message)s" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BaseTestCase(testtools.TestCase): | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super(BaseTestCase, self).setUp() | ||||||
|  |         self._set_timeout() | ||||||
|  |         self._fake_output() | ||||||
|  |         self._fake_logs() | ||||||
|  |         self.useFixture(fixtures.NestedTempfile()) | ||||||
|  |         self.useFixture(fixtures.TempHomeDir()) | ||||||
|  |         self.tempdirs = [] | ||||||
|  |  | ||||||
|  |     def _set_timeout(self): | ||||||
|  |         test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) | ||||||
|  |         try: | ||||||
|  |             test_timeout = int(test_timeout) | ||||||
|  |         except ValueError: | ||||||
|  |             # If timeout value is invalid do not set a timeout. | ||||||
|  |             test_timeout = 0 | ||||||
|  |         if test_timeout > 0: | ||||||
|  |             self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) | ||||||
|  |  | ||||||
|  |     def _fake_output(self): | ||||||
|  |         if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: | ||||||
|  |             stdout = self.useFixture(fixtures.StringStream('stdout')).stream | ||||||
|  |             self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) | ||||||
|  |         if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES: | ||||||
|  |             stderr = self.useFixture(fixtures.StringStream('stderr')).stream | ||||||
|  |             self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) | ||||||
|  |  | ||||||
|  |     def _fake_logs(self): | ||||||
|  |         if os.environ.get('OS_DEBUG') in _TRUE_VALUES: | ||||||
|  |             level = logging.DEBUG | ||||||
|  |         else: | ||||||
|  |             level = logging.INFO | ||||||
|  |         capture_logs = os.environ.get('OS_LOG_CAPTURE') in _TRUE_VALUES | ||||||
|  |         if capture_logs: | ||||||
|  |             self.useFixture( | ||||||
|  |                 fixtures.FakeLogger( | ||||||
|  |                     format=_LOG_FORMAT, | ||||||
|  |                     level=level, | ||||||
|  |                     nuke_handlers=capture_logs, | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             logging.basicConfig(format=_LOG_FORMAT, level=level) | ||||||
|  |  | ||||||
|  |     def create_tempfiles(self, files, ext='.conf'): | ||||||
|  |         tempfiles = [] | ||||||
|  |         for (basename, contents) in files: | ||||||
|  |             if not os.path.isabs(basename): | ||||||
|  |                 (fd, path) = tempfile.mkstemp(prefix=basename, suffix=ext) | ||||||
|  |             else: | ||||||
|  |                 path = basename + ext | ||||||
|  |                 fd = os.open(path, os.O_CREAT | os.O_WRONLY) | ||||||
|  |             tempfiles.append(path) | ||||||
|  |             try: | ||||||
|  |                 os.write(fd, contents) | ||||||
|  |             finally: | ||||||
|  |                 os.close(fd) | ||||||
|  |         return tempfiles | ||||||
							
								
								
									
										0
									
								
								cloudkittyclient/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cloudkittyclient/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										6
									
								
								cloudkittyclient/tests/test_fake.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								cloudkittyclient/tests/test_fake.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | import testtools | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FakeTest(testtools.TestCase): | ||||||
|  |     def test_foo(self): | ||||||
|  |         pass | ||||||
							
								
								
									
										18
									
								
								cloudkittyclient/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								cloudkittyclient/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Copyright 2014 Objectif Libre | ||||||
|  | # | ||||||
|  | # 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. | ||||||
|  | # | ||||||
|  | # @author: François Magimel (linkid) | ||||||
|  |  | ||||||
|  | from cloudkittyclient.v1.client import Client  # noqa | ||||||
							
								
								
									
										30
									
								
								cloudkittyclient/v1/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								cloudkittyclient/v1/client.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Copyright 2014 Objectif Libre | ||||||
|  | # | ||||||
|  | # 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. | ||||||
|  | # | ||||||
|  | # @author: François Magimel (linkid) | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | OpenStack Client interface. Handles the REST calls and responses. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | from cloudkittyclient.openstack.common.apiclient import client | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Client(client.BaseClient): | ||||||
|  |     """Client for the Cloudkitty v1 API.""" | ||||||
|  |  | ||||||
|  |     def __init__(self, http_client, extensions=None): | ||||||
|  |         """Initialize a new client for the Cloudkitty v1 API.""" | ||||||
|  |         super(Client, self).__init__(http_client, extensions) | ||||||
							
								
								
									
										56
									
								
								cloudkittyclient/v1/report.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								cloudkittyclient/v1/report.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Copyright 2014 Objectif Libre | ||||||
|  | # | ||||||
|  | # 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. | ||||||
|  | # | ||||||
|  | # @author: François Magimel (linkid) | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | Report resource and manager. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | from cloudkittyclient.openstack.common.apiclient import base | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Report(base.Resource): | ||||||
|  |     """A resource represents a particular instance of an object (tenant, user, | ||||||
|  |     etc). This is pretty much just a bag for attributes. | ||||||
|  |  | ||||||
|  |     :param manager: Manager object | ||||||
|  |     :param info: dictionary representing resource attributes | ||||||
|  |     :param loaded: prevent lazy-loading if set to True | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def _add_details(self, info): | ||||||
|  |         try: | ||||||
|  |             setattr(self, 'total', info) | ||||||
|  |         except AttributeError: | ||||||
|  |             # In this case we already defined the attribute on the class | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ReportManager(base.CrudManager): | ||||||
|  |     """Managers interact with a particular type of API and provide CRUD | ||||||
|  |     operations for them. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     resource_class = Report | ||||||
|  |     collection_key = 'report/total' | ||||||
|  |     key = 'report/total' | ||||||
|  |  | ||||||
|  |     def _get(self, url, response_key=None): | ||||||
|  |         body = self.client.get(url).json() | ||||||
|  |         return self.resource_class(self, body, loaded=True) | ||||||
|  |  | ||||||
|  |     def get(self, **kwargs): | ||||||
|  |         return super(ReportManager, self).get(base_url='/v1', **kwargs) | ||||||
							
								
								
									
										177
									
								
								doc/makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								doc/makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | |||||||
|  | # Makefile for Sphinx documentation | ||||||
|  | # | ||||||
|  |  | ||||||
|  | # You can set these variables from the command line. | ||||||
|  | SPHINXOPTS    = | ||||||
|  | SPHINXBUILD   = sphinx-build | ||||||
|  | PAPER         = | ||||||
|  | BUILDDIR      = build | ||||||
|  |  | ||||||
|  | # User-friendly check for sphinx-build | ||||||
|  | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) | ||||||
|  | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) | ||||||
|  | endif | ||||||
|  |  | ||||||
|  | # Internal variables. | ||||||
|  | PAPEROPT_a4     = -D latex_paper_size=a4 | ||||||
|  | PAPEROPT_letter = -D latex_paper_size=letter | ||||||
|  | ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source | ||||||
|  | # the i18n builder cannot share the environment and doctrees with the others | ||||||
|  | I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source | ||||||
|  |  | ||||||
|  | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext | ||||||
|  |  | ||||||
|  | help: | ||||||
|  | 	@echo "Please use \`make <target>' where <target> is one of" | ||||||
|  | 	@echo "  html       to make standalone HTML files" | ||||||
|  | 	@echo "  dirhtml    to make HTML files named index.html in directories" | ||||||
|  | 	@echo "  singlehtml to make a single large HTML file" | ||||||
|  | 	@echo "  pickle     to make pickle files" | ||||||
|  | 	@echo "  json       to make JSON files" | ||||||
|  | 	@echo "  htmlhelp   to make HTML files and a HTML help project" | ||||||
|  | 	@echo "  qthelp     to make HTML files and a qthelp project" | ||||||
|  | 	@echo "  devhelp    to make HTML files and a Devhelp project" | ||||||
|  | 	@echo "  epub       to make an epub" | ||||||
|  | 	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter" | ||||||
|  | 	@echo "  latexpdf   to make LaTeX files and run them through pdflatex" | ||||||
|  | 	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx" | ||||||
|  | 	@echo "  text       to make text files" | ||||||
|  | 	@echo "  man        to make manual pages" | ||||||
|  | 	@echo "  texinfo    to make Texinfo files" | ||||||
|  | 	@echo "  info       to make Texinfo files and run them through makeinfo" | ||||||
|  | 	@echo "  gettext    to make PO message catalogs" | ||||||
|  | 	@echo "  changes    to make an overview of all changed/added/deprecated items" | ||||||
|  | 	@echo "  xml        to make Docutils-native XML files" | ||||||
|  | 	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes" | ||||||
|  | 	@echo "  linkcheck  to check all external links for integrity" | ||||||
|  | 	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)" | ||||||
|  |  | ||||||
|  | clean: | ||||||
|  | 	rm -rf $(BUILDDIR)/* | ||||||
|  |  | ||||||
|  | html: | ||||||
|  | 	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html." | ||||||
|  |  | ||||||
|  | dirhtml: | ||||||
|  | 	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." | ||||||
|  |  | ||||||
|  | singlehtml: | ||||||
|  | 	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." | ||||||
|  |  | ||||||
|  | pickle: | ||||||
|  | 	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished; now you can process the pickle files." | ||||||
|  |  | ||||||
|  | json: | ||||||
|  | 	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished; now you can process the JSON files." | ||||||
|  |  | ||||||
|  | htmlhelp: | ||||||
|  | 	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished; now you can run HTML Help Workshop with the" \ | ||||||
|  | 	      ".hhp project file in $(BUILDDIR)/htmlhelp." | ||||||
|  |  | ||||||
|  | qthelp: | ||||||
|  | 	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished; now you can run "qcollectiongenerator" with the" \ | ||||||
|  | 	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:" | ||||||
|  | 	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-cloudkittyclient.qhcp" | ||||||
|  | 	@echo "To view the help file:" | ||||||
|  | 	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-cloudkittyclient.qhc" | ||||||
|  |  | ||||||
|  | devhelp: | ||||||
|  | 	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished." | ||||||
|  | 	@echo "To view the help file:" | ||||||
|  | 	@echo "# mkdir -p $$HOME/.local/share/devhelp/python-cloudkittyclient" | ||||||
|  | 	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-cloudkittyclient" | ||||||
|  | 	@echo "# devhelp" | ||||||
|  |  | ||||||
|  | epub: | ||||||
|  | 	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished. The epub file is in $(BUILDDIR)/epub." | ||||||
|  |  | ||||||
|  | latex: | ||||||
|  | 	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." | ||||||
|  | 	@echo "Run \`make' in that directory to run these through (pdf)latex" \ | ||||||
|  | 	      "(use \`make latexpdf' here to do that automatically)." | ||||||
|  |  | ||||||
|  | latexpdf: | ||||||
|  | 	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | ||||||
|  | 	@echo "Running LaTeX files through pdflatex..." | ||||||
|  | 	$(MAKE) -C $(BUILDDIR)/latex all-pdf | ||||||
|  | 	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | ||||||
|  |  | ||||||
|  | latexpdfja: | ||||||
|  | 	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | ||||||
|  | 	@echo "Running LaTeX files through platex and dvipdfmx..." | ||||||
|  | 	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja | ||||||
|  | 	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | ||||||
|  |  | ||||||
|  | text: | ||||||
|  | 	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished. The text files are in $(BUILDDIR)/text." | ||||||
|  |  | ||||||
|  | man: | ||||||
|  | 	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished. The manual pages are in $(BUILDDIR)/man." | ||||||
|  |  | ||||||
|  | texinfo: | ||||||
|  | 	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." | ||||||
|  | 	@echo "Run \`make' in that directory to run these through makeinfo" \ | ||||||
|  | 	      "(use \`make info' here to do that automatically)." | ||||||
|  |  | ||||||
|  | info: | ||||||
|  | 	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | ||||||
|  | 	@echo "Running Texinfo files through makeinfo..." | ||||||
|  | 	make -C $(BUILDDIR)/texinfo info | ||||||
|  | 	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." | ||||||
|  |  | ||||||
|  | gettext: | ||||||
|  | 	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." | ||||||
|  |  | ||||||
|  | changes: | ||||||
|  | 	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes | ||||||
|  | 	@echo | ||||||
|  | 	@echo "The overview file is in $(BUILDDIR)/changes." | ||||||
|  |  | ||||||
|  | linkcheck: | ||||||
|  | 	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Link check complete; look for any errors in the above output " \ | ||||||
|  | 	      "or in $(BUILDDIR)/linkcheck/output.txt." | ||||||
|  |  | ||||||
|  | doctest: | ||||||
|  | 	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest | ||||||
|  | 	@echo "Testing of doctests in the sources finished, look at the " \ | ||||||
|  | 	      "results in $(BUILDDIR)/doctest/output.txt." | ||||||
|  |  | ||||||
|  | xml: | ||||||
|  | 	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished. The XML files are in $(BUILDDIR)/xml." | ||||||
|  |  | ||||||
|  | pseudoxml: | ||||||
|  | 	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml | ||||||
|  | 	@echo | ||||||
|  | 	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." | ||||||
							
								
								
									
										280
									
								
								doc/source/conf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								doc/source/conf.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,280 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | # you may not use this file except in compliance with the License. | ||||||
|  | # You may obtain a copy of the License at | ||||||
|  | # | ||||||
|  | # http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  | # | ||||||
|  | # Unless required by applicable law or agreed to in writing, software | ||||||
|  | # distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||||
|  | # implied. | ||||||
|  | # See the License for the specific language governing permissions and | ||||||
|  | # limitations under the License. | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # If extensions (or modules to document with autodoc) are in another directory, | ||||||
|  | # add these directories to sys.path here. If the directory is relative to the | ||||||
|  | # documentation root, use os.path.abspath to make it absolute, like shown here. | ||||||
|  | #sys.path.insert(0, os.path.abspath('.')) | ||||||
|  |  | ||||||
|  | # -- General configuration ------------------------------------------------ | ||||||
|  |  | ||||||
|  | # If your documentation needs a minimal Sphinx version, state it here. | ||||||
|  | #needs_sphinx = '1.0' | ||||||
|  |  | ||||||
|  | # 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', | ||||||
|  |     'oslosphinx', | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | # Add any paths that contain templates here, relative to this directory. | ||||||
|  | templates_path = ['_templates'] | ||||||
|  |  | ||||||
|  | # The suffix of source filenames. | ||||||
|  | source_suffix = '.rst' | ||||||
|  |  | ||||||
|  | # The encoding of source files. | ||||||
|  | #source_encoding = 'utf-8-sig' | ||||||
|  |  | ||||||
|  | # The master toctree document. | ||||||
|  | master_doc = 'index' | ||||||
|  |  | ||||||
|  | # General information about the project. | ||||||
|  | project = u'python-cloudkittyclient' | ||||||
|  | copyright = u'2014, Objectif Libre' | ||||||
|  |  | ||||||
|  | # The version info for the project you're documenting, acts as replacement for | ||||||
|  | # |version| and |release|, also used in various other places throughout the | ||||||
|  | # built documents. | ||||||
|  | # | ||||||
|  | # The short X.Y version. | ||||||
|  | version = '0.1' | ||||||
|  | # The full version, including alpha/beta/rc tags. | ||||||
|  | release = '0.1' | ||||||
|  |  | ||||||
|  | # The language for content autogenerated by Sphinx. Refer to documentation | ||||||
|  | # for a list of supported languages. | ||||||
|  | #language = None | ||||||
|  |  | ||||||
|  | # There are two options for replacing |today|: either, you set today to some | ||||||
|  | # non-false value, then it is used: | ||||||
|  | #today = '' | ||||||
|  | # Else, today_fmt is used as the format for a strftime call. | ||||||
|  | #today_fmt = '%B %d, %Y' | ||||||
|  |  | ||||||
|  | # List of patterns, relative to source directory, that match files and | ||||||
|  | # directories to ignore when looking for source files. | ||||||
|  | exclude_patterns = [] | ||||||
|  |  | ||||||
|  | # The reST default role (used for this markup: `text`) to use for all | ||||||
|  | # documents. | ||||||
|  | #default_role = None | ||||||
|  |  | ||||||
|  | # If true, '()' will be appended to :func: etc. cross-reference text. | ||||||
|  | add_function_parentheses = True | ||||||
|  |  | ||||||
|  | # If true, the current module name will be prepended to all description | ||||||
|  | # unit titles (such as .. function::). | ||||||
|  | add_module_names = True | ||||||
|  |  | ||||||
|  | # If true, sectionauthor and moduleauthor directives will be shown in the | ||||||
|  | # output. They are ignored by default. | ||||||
|  | #show_authors = False | ||||||
|  |  | ||||||
|  | # The name of the Pygments (syntax highlighting) style to use. | ||||||
|  | pygments_style = 'sphinx' | ||||||
|  |  | ||||||
|  | # A list of ignored prefixes for module index sorting. | ||||||
|  | #modindex_common_prefix = [] | ||||||
|  |  | ||||||
|  | # If true, keep warnings as "system message" paragraphs in the built documents. | ||||||
|  | #keep_warnings = False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # -- Options for HTML output ---------------------------------------------- | ||||||
|  |  | ||||||
|  | # The theme to use for HTML and HTML Help pages.  See the documentation for | ||||||
|  | # a list of builtin themes. | ||||||
|  | #html_theme = 'default' | ||||||
|  | html_theme = 'nature' | ||||||
|  |  | ||||||
|  | # Theme options are theme-specific and customize the look and feel of a theme | ||||||
|  | # further.  For a list of options available for each theme, see the | ||||||
|  | # documentation. | ||||||
|  | #html_theme_options = {} | ||||||
|  |  | ||||||
|  | # Add any paths that contain custom themes here, relative to this directory. | ||||||
|  | #html_theme_path = [] | ||||||
|  |  | ||||||
|  | # The name for this set of Sphinx documents.  If None, it defaults to | ||||||
|  | # "<project> v<release> documentation". | ||||||
|  | #html_title = None | ||||||
|  |  | ||||||
|  | # A shorter title for the navigation bar.  Default is the same as html_title. | ||||||
|  | #html_short_title = None | ||||||
|  |  | ||||||
|  | # The name of an image file (relative to this directory) to place at the top | ||||||
|  | # of the sidebar. | ||||||
|  | #html_logo = None | ||||||
|  |  | ||||||
|  | # The name of an image file (within the static path) to use as favicon of the | ||||||
|  | # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32 | ||||||
|  | # pixels large. | ||||||
|  | #html_favicon = None | ||||||
|  |  | ||||||
|  | # Add any paths that contain custom static files (such as style sheets) here, | ||||||
|  | # relative to this directory. They are copied after the builtin static files, | ||||||
|  | # so a file named "default.css" will overwrite the builtin "default.css". | ||||||
|  | html_static_path = ['_static'] | ||||||
|  |  | ||||||
|  | # Add any extra paths that contain custom files (such as robots.txt or | ||||||
|  | # .htaccess) here, relative to this directory. These files are copied | ||||||
|  | # directly to the root of the documentation. | ||||||
|  | #html_extra_path = [] | ||||||
|  |  | ||||||
|  | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, | ||||||
|  | # using the given strftime format. | ||||||
|  | #html_last_updated_fmt = '%b %d, %Y' | ||||||
|  |  | ||||||
|  | # If true, SmartyPants will be used to convert quotes and dashes to | ||||||
|  | # typographically correct entities. | ||||||
|  | #html_use_smartypants = True | ||||||
|  |  | ||||||
|  | # Custom sidebar templates, maps document names to template names. | ||||||
|  | #html_sidebars = {} | ||||||
|  |  | ||||||
|  | # Additional templates that should be rendered to pages, maps page names to | ||||||
|  | # template names. | ||||||
|  | #html_additional_pages = {} | ||||||
|  |  | ||||||
|  | # If false, no module index is generated. | ||||||
|  | #html_domain_indices = True | ||||||
|  |  | ||||||
|  | # If false, no index is generated. | ||||||
|  | #html_use_index = True | ||||||
|  |  | ||||||
|  | # If true, the index is split into individual pages for each letter. | ||||||
|  | #html_split_index = False | ||||||
|  |  | ||||||
|  | # If true, links to the reST sources are added to the pages. | ||||||
|  | #html_show_sourcelink = True | ||||||
|  |  | ||||||
|  | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. | ||||||
|  | #html_show_sphinx = True | ||||||
|  |  | ||||||
|  | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. | ||||||
|  | #html_show_copyright = True | ||||||
|  |  | ||||||
|  | # If true, an OpenSearch description file will be output, and all pages will | ||||||
|  | # contain a <link> tag referring to it.  The value of this option must be the | ||||||
|  | # base URL from which the finished HTML is served. | ||||||
|  | #html_use_opensearch = '' | ||||||
|  |  | ||||||
|  | # This is the file name suffix for HTML files (e.g. ".xhtml"). | ||||||
|  | #html_file_suffix = None | ||||||
|  |  | ||||||
|  | # Output file base name for HTML help builder. | ||||||
|  | htmlhelp_basename = '%sdoc' % project | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # -- Options for LaTeX output --------------------------------------------- | ||||||
|  |  | ||||||
|  | latex_elements = { | ||||||
|  | # The paper size ('letterpaper' or 'a4paper'). | ||||||
|  | #'papersize': 'letterpaper', | ||||||
|  |  | ||||||
|  | # The font size ('10pt', '11pt' or '12pt'). | ||||||
|  | #'pointsize': '10pt', | ||||||
|  |  | ||||||
|  | # Additional stuff for the LaTeX preamble. | ||||||
|  | #'preamble': '', | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # Grouping the document tree into LaTeX files. List of tuples | ||||||
|  | # (source start file, target name, title, | ||||||
|  | #  author, documentclass [howto, manual, or own class]). | ||||||
|  | latex_documents = [ | ||||||
|  |     ( | ||||||
|  |         'index', | ||||||
|  |         '%s.tex' % project, | ||||||
|  |         u'%s Documentation' % project, | ||||||
|  |         u'Objectif Libre', | ||||||
|  |         'manual' | ||||||
|  |     ), | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | # The name of an image file (relative to this directory) to place at the top of | ||||||
|  | # the title page. | ||||||
|  | #latex_logo = None | ||||||
|  |  | ||||||
|  | # For "manual" documents, if this is true, then toplevel headings are parts, | ||||||
|  | # not chapters. | ||||||
|  | #latex_use_parts = False | ||||||
|  |  | ||||||
|  | # If true, show page references after internal links. | ||||||
|  | #latex_show_pagerefs = False | ||||||
|  |  | ||||||
|  | # If true, show URL addresses after external links. | ||||||
|  | #latex_show_urls = False | ||||||
|  |  | ||||||
|  | # Documents to append as an appendix to all manuals. | ||||||
|  | #latex_appendices = [] | ||||||
|  |  | ||||||
|  | # If false, no module index is generated. | ||||||
|  | #latex_domain_indices = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # -- Options for manual page output --------------------------------------- | ||||||
|  |  | ||||||
|  | # One entry per manual page. List of tuples | ||||||
|  | # (source start file, name, description, authors, manual section). | ||||||
|  | man_pages = [ | ||||||
|  |     ( | ||||||
|  |         'index', | ||||||
|  |         project, | ||||||
|  |         u'%s Documentation' % project, | ||||||
|  |         [u'Objectif Libre'], | ||||||
|  |         1 | ||||||
|  |     ), | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | # If true, show URL addresses after external links. | ||||||
|  | #man_show_urls = False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # -- Options for Texinfo output ------------------------------------------- | ||||||
|  |  | ||||||
|  | # Grouping the document tree into Texinfo files. List of tuples | ||||||
|  | # (source start file, target name, title, author, | ||||||
|  | #  dir menu entry, description, category) | ||||||
|  | texinfo_documents = [ | ||||||
|  |     ( | ||||||
|  |         'index', | ||||||
|  |         project, | ||||||
|  |         u'%s Documentation' % project, | ||||||
|  |         u'Objectif Libre', | ||||||
|  |         project, | ||||||
|  |         'Python client library for CloudKitty API', | ||||||
|  |         'Miscellaneous' | ||||||
|  |     ), | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | # Documents to append as an appendix to all manuals. | ||||||
|  | #texinfo_appendices = [] | ||||||
|  |  | ||||||
|  | # If false, no module index is generated. | ||||||
|  | #texinfo_domain_indices = True | ||||||
|  |  | ||||||
|  | # How to display URL addresses: 'footnote', 'no', or 'inline'. | ||||||
|  | #texinfo_show_urls = 'footnote' | ||||||
|  |  | ||||||
|  | # If true, do not generate a @detailmenu in the "Top" node's menu. | ||||||
|  | #texinfo_no_detailmenu = False | ||||||
							
								
								
									
										23
									
								
								doc/source/index.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								doc/source/index.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | .. python-cloudkittyclient documentation master file, created by | ||||||
|  |    sphinx-quickstart on Thu Jul  3 17:15:04 2014. | ||||||
|  |    You can adapt this file completely to your liking, but it should at least | ||||||
|  |    contain the root `toctree` directive. | ||||||
|  |  | ||||||
|  | Welcome to CloudKitty Client's documentation! | ||||||
|  | ============================================= | ||||||
|  |  | ||||||
|  | Introduction | ||||||
|  | ============ | ||||||
|  |  | ||||||
|  | CloudKitty is a PricingAsAService project aimed at translating Ceilometer | ||||||
|  | metrics to prices. | ||||||
|  |  | ||||||
|  | python-cloudkitty is the Python client library for CloudKitty API. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Indices and tables | ||||||
|  | ================== | ||||||
|  |  | ||||||
|  | * :ref:`genindex` | ||||||
|  | * :ref:`modindex` | ||||||
|  | * :ref:`search` | ||||||
							
								
								
									
										9
									
								
								openstack-common.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								openstack-common.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | [DEFAULT] | ||||||
|  |  | ||||||
|  | # The list of modules to copy from openstack-common | ||||||
|  | module=apiclient | ||||||
|  | module=importutils | ||||||
|  | module=test | ||||||
|  |  | ||||||
|  | # The base module to hold the copy of openstack.common | ||||||
|  | base=cloudkittyclient | ||||||
							
								
								
									
										5
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | argparse | ||||||
|  | oslo.i18n | ||||||
|  | pbr>=0.6,!=0.7,<1.0 | ||||||
|  | python-keystoneclient | ||||||
|  | six>=1.7.0 | ||||||
							
								
								
									
										34
									
								
								setup.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								setup.cfg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | [metadata] | ||||||
|  | name = python-cloudkittyclient | ||||||
|  | summary = Python client library for CloudKitty API | ||||||
|  | description-file = | ||||||
|  |     README.rst | ||||||
|  | author = Objectif Libre | ||||||
|  | author-email = francois.magimel@objectif-libre.com | ||||||
|  | home-page = http://objectif-libre.com | ||||||
|  | classifier = | ||||||
|  |     Environment :: Console | ||||||
|  |     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.6 | ||||||
|  |     Programming Language :: Python :: 2.7 | ||||||
|  |     Programming Language :: Python :: 3 | ||||||
|  |     Programming Language :: Python :: 3.3 | ||||||
|  |  | ||||||
|  | [files] | ||||||
|  | packages = | ||||||
|  |     cloudkittyclient | ||||||
|  |  | ||||||
|  | [global] | ||||||
|  | setup-hooks = | ||||||
|  |     pbr.hooks.setup_hook | ||||||
|  |  | ||||||
|  | [build_sphinx] | ||||||
|  | all_files = 1 | ||||||
|  | build-dir = doc/build | ||||||
|  | source-dir = doc/source | ||||||
							
								
								
									
										29
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | #!/usr/bin/env python | ||||||
|  | # Copyright 2014 Objectif Libre | ||||||
|  | # | ||||||
|  | # 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 | ||||||
|  |  | ||||||
|  | # In python < 2.7.4, a lazy loading of package `pbr` will break | ||||||
|  | # setuptools if some other modules registered functions in `atexit`. | ||||||
|  | # solution from: http://bugs.python.org/issue15881#msg170215 | ||||||
|  | try: | ||||||
|  |     import multiprocessing  # noqa | ||||||
|  | except ImportError: | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  | setuptools.setup( | ||||||
|  |     setup_requires=['pbr'], | ||||||
|  |     pbr=True) | ||||||
							
								
								
									
										13
									
								
								test-requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								test-requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | # Hacking already pins down pep8, pyflakes and flake8 | ||||||
|  | hacking>=0.9.1,<0.10 | ||||||
|  |  | ||||||
|  | coverage>=3.6 | ||||||
|  | discover | ||||||
|  | doc8 | ||||||
|  | fixtures>=0.3.14 | ||||||
|  | mock>=1.0 | ||||||
|  | oslosphinx | ||||||
|  | oslotest | ||||||
|  | sphinx>=1.1.2,!=1.2.0,<1.3 | ||||||
|  | testrepository>=0.0.18 | ||||||
|  | testtools>=0.9.34 | ||||||
							
								
								
									
										172
									
								
								tools/install_venv_common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								tools/install_venv_common.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | |||||||
|  | # 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 and pbr | ||||||
|  |         self.pip_install('pip>=1.4') | ||||||
|  |         self.pip_install('setuptools') | ||||||
|  |         self.pip_install('pbr') | ||||||
|  |  | ||||||
|  |         self.pip_install('-r', self.requirements, '-r', self.test_requirements) | ||||||
|  |  | ||||||
|  |     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) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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 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() | ||||||
							
								
								
									
										38
									
								
								tox.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tox.ini
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | [tox] | ||||||
|  | envlist = py26,py27,py33,pep8 | ||||||
|  | minversion = 1.6 | ||||||
|  | skipsdist = True | ||||||
|  |  | ||||||
|  | [testenv] | ||||||
|  | usedevelop = True | ||||||
|  | install_command = pip install -U {opts} {packages} | ||||||
|  | setenv = VIRTUAL_ENV={envdir} | ||||||
|  | deps = -r{toxinidir}/requirements.txt | ||||||
|  |        -r{toxinidir}/test-requirements.txt | ||||||
|  | commands = | ||||||
|  |   python setup.py testr --testr-args='{posargs}' | ||||||
|  |  | ||||||
|  | [tox:jenkins] | ||||||
|  | downloadcache = ~/cache/pip | ||||||
|  |  | ||||||
|  | [testenv:pep8] | ||||||
|  | commands = flake8 {posargs} | ||||||
|  |  | ||||||
|  | [testenv:cover] | ||||||
|  | commands = python setup.py testr --coverage --testr-args='{posargs}' | ||||||
|  |  | ||||||
|  | [testenv:docs] | ||||||
|  | commands = | ||||||
|  |     doc8 -e .rst README.rst doc/source | ||||||
|  |     python setup.py build_sphinx | ||||||
|  |  | ||||||
|  | [testenv:venv] | ||||||
|  | commands = {posargs} | ||||||
|  |  | ||||||
|  | [flake8] | ||||||
|  | # H405 multi line docstring summary not separated with an empty line | ||||||
|  | # H904 Wrap long lines in parentheses instead of a backslash | ||||||
|  | # H102 Apache 2.0 license header not found | ||||||
|  | ignore = H405,H904,H102 | ||||||
|  | show-source = True | ||||||
|  | exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools,./cloudkittyclient/common/exceptions.py | ||||||
		Reference in New Issue
	
	Block a user
	 François Magimel
					François Magimel