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