diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 68e77de..0000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -branch = True -source = cratonclient -omit = cratonclient/openstack/* - -[report] -ignore_errors = True diff --git a/.gitignore b/.gitignore deleted file mode 100644 index e52bb44..0000000 --- a/.gitignore +++ /dev/null @@ -1,55 +0,0 @@ -*.py[cod] - -# C extensions -*.so - -# Packages -*.egg* -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -cover/ -.coverage* -!.coveragerc -.tox -nosetests.xml -.testrepository -.venv - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# Complexity -output/*.html -output/*/index.html - -# Sphinx -doc/build - -# pbr generates these -AUTHORS -ChangeLog - -# Editors -*~ -.*.swp -.*sw? diff --git a/.gitreview b/.gitreview deleted file mode 100644 index 0d5b716..0000000 --- a/.gitreview +++ /dev/null @@ -1,4 +0,0 @@ -[gerrit] -host=review.openstack.org -port=29418 -project=openstack/python-cratonclient.git diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 516ae6f..0000000 --- a/.mailmap +++ /dev/null @@ -1,3 +0,0 @@ -# Format is: -# -# diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index 6d83b3c..0000000 --- a/.testr.conf +++ /dev/null @@ -1,7 +0,0 @@ -[DEFAULT] -test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ - OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ - OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ - ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 76a69f1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -sudo: false -language: python - -matrix: - include: - - python: 2.7 - env: TOXENV=py27 - - python: 3.5 - env: TOXENV=py35 - - env: TOXENV=pep8 - -install: pip install tox-travis - -script: tox diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index e124784..0000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,17 +0,0 @@ -If you would like to contribute to the development of OpenStack, you must -follow the steps in this page: - - http://docs.openstack.org/infra/manual/developers.html - -If you already have a good understanding of how the system works and your -OpenStack accounts are set up, you can skip to the development workflow -section of this documentation to learn how changes to OpenStack should be -submitted for review via the Gerrit tool: - - http://docs.openstack.org/infra/manual/developers.html#development-workflow - -Pull requests submitted through GitHub will be ignored. - -Bugs should be filed on Launchpad, not GitHub: - - https://bugs.launchpad.net/python-cratonclient diff --git a/HACKING.rst b/HACKING.rst deleted file mode 100644 index 742db14..0000000 --- a/HACKING.rst +++ /dev/null @@ -1,4 +0,0 @@ -python-cratonclient Style Commandments -=============================================== - -Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 68c771a..0000000 --- a/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ - - 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. - diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index c978a52..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include AUTHORS -include ChangeLog -exclude .gitignore -exclude .gitreview - -global-exclude *.pyc diff --git a/README.rst b/README.rst index 21bf133..d98af48 100644 --- a/README.rst +++ b/README.rst @@ -1,19 +1,10 @@ -=============================== -python-cratonclient -=============================== +This project is no longer maintained. -Craton API Client and Command-line Utility +The contents of this repository are still available in the Git +source code management system. To see the contents of this +repository before it reached its end of life, please check out the +previous commit with "git checkout HEAD^1". -Please fill here a long description which must be at least 3 lines wrapped on -80 cols, so that distribution package maintainers can use it in their packages. -Note that this is a hard requirement. - -* Free software: Apache license -* Documentation: https://python-cratonclient.readthedocs.io -* Source: http://git.openstack.org/cgit/openstack/python-cratonclient -* Bugs: http://bugs.launchpad.net/python-cratonclient - -Features --------- - -* TODO +For any further questions, please email +openstack-dev@lists.openstack.org or join #openstack-dev on +Freenode. diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index 15cd6cb..0000000 --- a/babel.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[python: **.py] - diff --git a/cratonclient/__init__.py b/cratonclient/__init__.py deleted file mode 100644 index 1c491e2..0000000 --- a/cratonclient/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- 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. -"""Craton API Client Library and Command-Line Application.""" - -import pbr.version - - -__version__ = pbr.version.VersionInfo( - 'cratonclient').version_string() diff --git a/cratonclient/auth.py b/cratonclient/auth.py deleted file mode 100644 index 1f7b5b6..0000000 --- a/cratonclient/auth.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright (c) 2016 Rackspace -# -# 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. -"""Module that simplifies and unifies authentication for Craton.""" -from keystoneauth1.identity.v3 import password as ksa_password -from keystoneauth1 import plugin -from keystoneauth1 import session as ksa_session - -from cratonclient import exceptions as exc - - -def craton_auth(username, token, project_id, verify=True): - """Configure a cratonclient Session to authenticate to Craton. - - This will create, configure, and return a Session object that will use - Craton's built-in authentication method. - - :param str username: - The username with which to authentiate against the API. - :param str token: - The token with which to authenticate against the API. - :param str project_id: - The project ID that the user belongs to. - :param bool verify: - (Optional) Whether or not to verify HTTPS certificates provided by the - server. Default: True - :returns: - Configured cratonclient session. - :rtype: - cratonclient.session.Session - - Example: - - .. code-block:: python - - from cratonclient import auth - from cratonclient.v1 import client - - craton = client.Client(session=auth.craton_auth( - username='demo', - token='demo', - project_id='b9f10eca66ac4c279c139d01e65f96b4', - )) - - """ - auth_plugin = CratonAuth( - username=username, - token=token, - project_id=project_id, - ) - return create_session_with(auth_plugin, verify) - - -def keystone_auth(auth_url, username, password, verify=True, - project_name=None, project_id=None, - project_domain_name=None, project_domain_id=None, - user_domain_name=None, user_domain_id=None, - **auth_parameters): - r"""Configure a cratonclient Session to authenticate with Keystone. - - This will create, configure, and return a Session using thet appropriate - Keystone authentication plugin to be able to communicate and authenticate - to Craton. - - .. note:: - - Presently, this function supports only V3 Password based - authentication to Keystone. We also do not validate that you specify - required attributes. For example, Keystone will require you provide - ``project_name`` or ``project_id`` but we will not enforce whether or - not you've specified one. - - :param str auth_url: - The URL of the Keystone instance to authenticate to. - :param str username: - The username with which we will authenticate to Keystone. - :param str password: - The password used to authenticate to Keystone. - :param str project_name: - (Optional) The name of the project the user belongs to. - :param str project_id: - (Optional) The ID of the project the user belongs to. - :param str project_domain_name: - (Optional) The name of the project's domain. - :param str project_domain_id: - (Optional) The ID of the project's domain. - :param str user_domain_name: - (Optional) The name of the user's domain. - :param str user_domain_id: - (Optional) The ID of the user's domain. - :param bool verify: - (Optional) Whether or not to verify HTTPS certificates provided by the - server. Default: True - :param \*\*auth_parameters: - Any extra authentication parameters used to authenticate to Keystone. - See the Keystone documentation for usage of: - - ``trust_id`` - - ``domain_id`` - - ``domain_name`` - - ``reauthenticate`` - :returns: - Configured cratonclient session. - :rtype: - cratonclient.session.Session - - Example: - - .. code-block:: python - - from cratonclient import auth - from cratonclient.v1 import client - - craton = client.Client(session=auth.keystone_auth( - auth_url='https://keystone.cloud.org/v3', - username='admin', - password='s3cr373p@55w0rd', - project_name='admin', - project_domain_name='Default', - user_domain_name='Default', - )) - """ - password_auth = ksa_password.Password( - auth_url=auth_url, - username=username, - password=password, - project_id=project_id, - project_name=project_name, - project_domain_id=project_domain_id, - project_domain_name=project_domain_name, - user_domain_id=user_domain_id, - user_domain_name=user_domain_name, - **auth_parameters - ) - return create_session_with(password_auth, verify) - - -def create_session_with(auth_plugin, verify): - """Create a cratonclient Session with the specified auth and verify values. - - :param auth_plugin: - The authentication plugin to use with the keystoneauth1 Session - object. - :type auth_plugin: - keystoneauth1.plugin.BaseAuthPlugin - :param bool verify: - Whether or not to verify HTTPS certificates provided by the server. - :returns: - Configured cratonclient session. - :rtype: - cratonclient.session.Session - """ - from cratonclient import session - return session.Session(session=ksa_session.Session( - auth=auth_plugin, - verify=verify, - )) - - -class CratonAuth(plugin.BaseAuthPlugin): - """Custom authentication plugin for keystoneauth1. - - This is specifically for the case where we're not using Keystone for - authentication. - """ - - def __init__(self, username, project_id, token): - """Initialize our craton authentication class.""" - self.username = username - self.project_id = project_id - self.token = token - - def get_token(self, session, **kwargs): - """Return our token.""" - return self.token - - def get_headers(self, session, **kwargs): - """Return the craton authentication headers.""" - headers = super(CratonAuth, self).get_headers(session, **kwargs) - if headers is None: - # NOTE(sigmavirus24): This means that the token must be None. We - # should not allow this to go further. We're using built-in Craton - # authentication (not authenticating against Keystone) so we will - # be unable to authenticate. - raise exc.UnableToAuthenticate() - - headers['X-Auth-User'] = self.username - headers['X-Auth-Project'] = '{}'.format(self.project_id) - return headers diff --git a/cratonclient/common/__init__.py b/cratonclient/common/__init__.py deleted file mode 100644 index bc91c3c..0000000 --- a/cratonclient/common/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Common Craton common classes and functions.""" diff --git a/cratonclient/common/cliutils.py b/cratonclient/common/cliutils.py deleted file mode 100644 index 5c17101..0000000 --- a/cratonclient/common/cliutils.py +++ /dev/null @@ -1,176 +0,0 @@ -# -*- 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. -"""Craton CLI helper classes and functions.""" -import functools -import json -import os -import sys - -from oslo_utils import encodeutils -from oslo_utils import strutils - -from cratonclient import exceptions as exc - - -def arg(*args, **kwargs): - """Decorator for CLI args. - - Example: - - >>> @arg("name", help="Name of the new entity.") - ... def entity_create(args): - ... pass - """ - def _decorator(func): - """Decorator definition.""" - add_arg(func, *args, **kwargs) - return func - - return _decorator - - -def add_arg(func, *args, **kwargs): - """Bind CLI arguments to a shell.py `do_foo` function.""" - if not hasattr(func, 'arguments'): - func.arguments = [] - - # NOTE(sirp): avoid dups that can occur when the module is shared across - # tests. - if (args, kwargs) not in func.arguments: - # Because of the semantics of decorator composition if we just append - # to the options list positional options will appear to be backwards. - func.arguments.insert(0, (args, kwargs)) - - -def field_labels_from(attributes): - """Generate a list of slightly more human readable field names. - - This takes the list of fields/attributes on the object and makes them - easier to read. - - :param list attributes: - The attribute names to convert. For example, ``["parent_id"]``. - :returns: - List of field names. For example ``["Parent Id"]`` - :rtype: - list - - Example: - - >>> field_labels_from(["id", "name", "cloud_id"]) - ["Id", "Name", "Cloud Id"] - """ - return [field.replace('_', ' ').title() for field in attributes] - - -def handle_shell_exception(function): - """Generic error handler for shell methods.""" - @functools.wraps(function) - def wrapper(cc, args): - prop_map = { - "vars": "variables" - } - try: - function(cc, args) - except exc.ClientException as client_exc: - # NOTE(thomasem): All shell methods follow a similar pattern, - # so we can parse this name to get intended parts for - # messaging what went wrong to the end-user. - # The pattern is "do__(_)", like - # do_project_show or do_project_vars_get, where is - # not guaranteed to be there, but will afford support for - # actions on some property of the resource. - parsed = function.__name__.split('_') - resource = parsed[1] - verb = parsed[-1] - prop = parsed[2] if len(parsed) > 3 else None - msg = 'Failed to {}'.format(verb) - if prop: - # NOTE(thomasem): Prop would be something like "vars" in - # "do_project_vars_get". - msg = '{} {}'.format(msg, prop_map.get(prop)) - # NOTE(thomasem): Append resource and ClientException details - # to error message. - msg = '{} for {} {} due to "{}: {}"'.format( - msg, resource, args.id, client_exc.__class__, - encodeutils.exception_to_unicode(client_exc) - ) - raise exc.CommandError(msg) - - return wrapper - - -def env(*args, **kwargs): - """Return the first environment variable set. - - If all are empty, defaults to '' or keyword arg `default`. - """ - for arg in args: - value = os.environ.get(arg) - if value: - return value - return kwargs.get('default', '') - - -def convert_arg_value(v): - """Convert different user inputs to normalized type.""" - # NOTE(thomasem): Handle case where one wants to escape this value - # conversion using the format key='"value"' - if v.startswith('"'): - return v.strip('"') - - lower_v = v.lower() - if strutils.is_int_like(v): - return int(v) - if strutils.is_valid_boolstr(lower_v): - return strutils.bool_from_string(lower_v) - if lower_v == 'null' or lower_v == 'none': - return None - try: - return float(v) - except ValueError: - pass - return v - - -def variable_updates(variables): - """Derive list of expected variables for a resource and set them.""" - update_variables = {} - delete_variables = set() - for variable in variables: - k, v = variable.split('=', 1) - if v: - update_variables[k] = convert_arg_value(v) - else: - delete_variables.add(k) - if not sys.stdin.isatty(): - if update_variables or delete_variables: - raise exc.CommandError("Cannot use variable settings from both " - "stdin and command line arguments. Please " - "choose one or the other.") - update_variables = json.load(sys.stdin) - return (update_variables, list(delete_variables)) - - -def variable_deletes(variables): - """Delete a list of variables (by key) from a resource.""" - if not sys.stdin.isatty(): - if variables: - raise exc.CommandError("Cannot use variable settings from both " - "stdin and command line arguments. Please " - "choose one or the other.") - delete_variables = json.load(sys.stdin) - else: - delete_variables = variables - return delete_variables diff --git a/cratonclient/crud.py b/cratonclient/crud.py deleted file mode 100644 index 4bcf1bd..0000000 --- a/cratonclient/crud.py +++ /dev/null @@ -1,263 +0,0 @@ -# -*- 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. -"""Client for CRUD operations.""" -import copy - -from oslo_utils import strutils - - -class CRUDClient(object): - """Class that handles the basic create, read, upload, delete workflow.""" - - key = "" - base_path = None - resource_class = None - - def __init__(self, session, url, **extra_request_kwargs): - """Initialize our Client with a session and base url.""" - self.session = session - self.url = url.rstrip('/') - self.extra_request_kwargs = extra_request_kwargs - - def build_url(self, path_arguments=None): - """Build a complete URL from the url, base_path, and arguments. - - A CRUDClient is constructed with the base URL, e.g. - - .. code-block:: python - - RegionManager(url='https://10.1.1.0:8080/v1', ...) - - The child class of the CRUDClient may set the ``base_path``, e.g., - - .. code-block:: python - - base_path = '/regions' - - And its ``key``, e.g., - - .. code-block:: python - - key = 'region' - - And based on the ``path_arguments`` parameter we will construct a - complete URL. For example, if someone calls: - - .. code-block:: python - - self.build_url(path_arguments={'region_id': 1}) - - with the hypothetical values above, we would return - - https://10.1.1.0:8080/v1/regions/1 - - Users can also override ``base_path`` in ``path_arguments``. - """ - if path_arguments is None: - path_arguments = {} - - base_path = path_arguments.pop('base_path', None) or self.base_path - item_id = path_arguments.pop('{0}_id'.format(self.key), None) - - url = self.url + base_path - - if item_id is not None: - url += '/{0}'.format(item_id) - - return url - - def merge_request_arguments(self, request_kwargs, skip_merge): - """Merge the extra request arguments into the per-request args.""" - if skip_merge: - return - - keys = set(self.extra_request_kwargs.keys()) - missing_keys = keys.difference(request_kwargs.keys()) - for key in missing_keys: - request_kwargs[key] = self.extra_request_kwargs[key] - - def create(self, skip_merge=False, **kwargs): - """Create a new item based on the keyword arguments provided.""" - self.merge_request_arguments(kwargs, skip_merge) - url = self.build_url(path_arguments=kwargs) - response = self.session.post(url, json=kwargs) - return self.resource_class(self, response.json(), loaded=True) - - def get(self, item_id=None, skip_merge=True, **kwargs): - """Retrieve the item based on the keyword arguments provided.""" - self.merge_request_arguments(kwargs, skip_merge) - kwargs.setdefault(self.key + '_id', item_id) - url = self.build_url(path_arguments=kwargs) - response = self.session.get(url) - return self.resource_class(self, response.json(), loaded=True) - - def list(self, skip_merge=False, **kwargs): - """Generate the items from this endpoint.""" - autopaginate = kwargs.pop('autopaginate', True) - nested = kwargs.pop('nested', False) - self.merge_request_arguments(kwargs, skip_merge) - url = self.build_url(path_arguments=kwargs) - - response_generator = self.session.paginate( - url, - autopaginate=autopaginate, - items_key=(self.key + 's'), - nested=nested, - params=kwargs, - ) - for response, items in response_generator: - for item in items: - yield self.resource_class(self, item, loaded=True) - - def update(self, item_id=None, skip_merge=True, **kwargs): - """Update the item based on the keyword arguments provided.""" - self.merge_request_arguments(kwargs, skip_merge) - kwargs.setdefault(self.key + '_id', item_id) - url = self.build_url(path_arguments=kwargs) - response = self.session.put(url, json=kwargs) - return self.resource_class(self, response.json(), loaded=True) - - def delete(self, item_id=None, skip_merge=True, json=None, **kwargs): - """Delete the item based on the keyword arguments provided.""" - self.merge_request_arguments(kwargs, skip_merge) - kwargs.setdefault(self.key + '_id', item_id) - url = self.build_url(path_arguments=kwargs) - response = self.session.delete(url, params=kwargs, json=json) - if 200 <= response.status_code < 300: - return True - return False - - def __repr__(self): - """Return string representation of a Variable.""" - return '%(class)s(%(session)s, %(url)s, %(extra_request_kwargs)s)' % \ - { - "class": self.__class__.__name__, - "session": self.session, - "url": self.url, - "extra_request_kwargs": self.extra_request_kwargs, - } - - -# NOTE(sigmavirus24): Credit for this Resource object goes to the -# keystoneclient developers and contributors. -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' - subresource_managers = {} - - 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 - - session = self.manager.session - subresource_base_url = self.manager.build_url( - {"{0}_id".format(self.manager.key): self.id} - ) - for attribute, cls in self.subresource_managers.items(): - setattr(self, attribute, - cls(session, subresource_base_url, - **self.manager.extra_request_kwargs)) - - def __repr__(self): - """Return string representation of resource attributes.""" - 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 info.items(): - try: - setattr(self, k, v) - self._info[k] = v - except AttributeError: # nosec(cjschaef): we already defined the - # attribute on the class - pass - - def __getattr__(self, k): - """Checking attrbiute existence.""" - 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) - self._add_details( - {'x_request_id': self.manager.client.last_request_id}) - - def __eq__(self, other): - """Define equality for resources.""" - if not isinstance(other, Resource): - return NotImplemented - # two resources of different types are not equal - if not isinstance(other, self.__class__): - return False - return self._info == other._info - - def is_loaded(self): - """Check if the resource has been loaded.""" - return self._loaded - - def set_loaded(self, val): - """Set whether the resource has been loaded or not.""" - self._loaded = val - - def to_dict(self): - """Return the resource as a dictionary.""" - return copy.deepcopy(self._info) - - def delete(self): - """Delete the resource from the service.""" - return self.manager.delete(self.id) diff --git a/cratonclient/exceptions.py b/cratonclient/exceptions.py deleted file mode 100644 index fde1bf6..0000000 --- a/cratonclient/exceptions.py +++ /dev/null @@ -1,292 +0,0 @@ -# -*- 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. -"""Exception classes and logic for cratonclient.""" - - -class ClientException(Exception): - """Base exception class for all exceptions in cratonclient.""" - - message = None - - def __init__(self, message=None): - """Initialize our exception instance with our class level message.""" - if message is None: - if self.message is None: - message = self.__class__.__name__ - else: - message = self.message - super(ClientException, self).__init__(message) - - -class UnableToAuthenticate(ClientException): - """There are insufficient parameters for authentication.""" - - message = "Some of the parameters required to authenticate were missing.""" - - -class Timeout(ClientException): - """Catch-all class for connect and read timeouts from requests.""" - - message = 'Request timed out' - - def __init__(self, message=None, **kwargs): - """Initialize our Timeout exception. - - This takes an optional keyword-only argument of - ``original_exception``. - """ - self.original_exception = kwargs.pop('exception', None) - super(Timeout, self).__init__(message) - - -class HTTPError(ClientException): - """Base exception class for all HTTP related exceptions in.""" - - message = "An error occurred while talking to the remote server." - status_code = None - - def __init__(self, message=None, **kwargs): - """Initialize our HTTPError instance. - - Optional keyword-only arguments include: - - - response: for the response generating the error - - original_exception: in the event that this is a requests exception - that we are re-raising. - """ - self.response = kwargs.pop('response', None) - self.original_exception = kwargs.pop('exception', None) - self.status_code = (self.status_code - or getattr(self.response, 'status_code', None)) - super(HTTPError, self).__init__(message) - - @property - def status_code(self): - """Shim to provide a similar API to other OpenStack clients.""" - return self.status_code - - @status_code.setter - def status_code(self, code): - self.status_code = code - - -class CommandError(ClientException): - """Client command was invalid or failed.""" - - message = "The command used was invalid or caused an error.""" - - -class ConnectionFailed(HTTPError): - """Connecting to the server failed.""" - - message = "An error occurred while connecting to the server.""" - - -class HTTPClientError(HTTPError): - """Base exception for client side errors (4xx status codes).""" - - message = "Something went wrong with the request." - - -class BadRequest(HTTPClientError): - """Client sent a malformed request.""" - - status_code = 400 - message = "The request sent to the server was invalid." - - -class Unauthorized(HTTPClientError): - """Client is unauthorized to access the resource in question.""" - - status_code = 401 - message = ("The user has either provided insufficient parameters for " - "authentication or is not authorized to access this resource.") - - -class Forbidden(HTTPClientError): - """Client is forbidden to access the resource.""" - - status_code = 403 - message = ("The user was unable to access the resource because they are " - "forbidden.") - - -class NotFound(HTTPClientError): - """Resource could not be found.""" - - status_code = 404 - message = "The requested resource was not found.""" - - -class MethodNotAllowed(HTTPClientError): - """The request method is not supported.""" - - status_code = 405 - message = "The method used in the request is not supported." - - -class NotAcceptable(HTTPClientError): - """The requested resource can not respond with acceptable content. - - Based on the Accept headers specified by the client, the resource can not - generate content that is an acceptable content-type. - """ - - status_code = 406 - message = "The resource can not return acceptable content." - - -class ProxyAuthenticationRequired(HTTPClientError): - """The client must first authenticate itself with the proxy.""" - - status_code = 407 - message = "The client must first authenticate itself with a proxy." - - -class Conflict(HTTPClientError): - """The request presents a conflict.""" - - status_code = 409 - message = "The request could not be processed due to a conflict." - - -class Gone(HTTPClientError): - """The requested resource is no longer available. - - The resource requested is no longer available and will not be available - again. - """ - - status_code = 410 - message = ("The resource requested is no longer available and will not be" - " available again.") - - -class LengthRequired(HTTPClientError): - """The request did not specify a Content-Length header.""" - - status_code = 411 - message = ("The request did not contain a Content-Length header but one" - " was required by the resource.") - - -class PreconditionFailed(HTTPClientError): - """The server failed to meet one of the preconditions in the request.""" - - status_code = 412 - message = ("The server failed to meet one of the preconditions in the " - "request.") - - -class RequestEntityTooLarge(HTTPClientError): - """The request is larger than the server is willing or able to process.""" - - status_code = 413 - message = ("The request is larger than the server is willing or able to " - "process.") - - -class RequestUriTooLong(HTTPClientError): - """The URI provided was too long for the server to process.""" - - status_code = 414 - message = "The URI provided was too long for the server to process." - - -class UnsupportedMediaType(HTTPClientError): - """The request entity has a media type which is unsupported.""" - - status_code = 415 - message = ("The request entity has a media type which is unsupported by " - "the server or resource.") - - -class RequestedRangeNotSatisfiable(HTTPClientError): - """The requestor wanted a range but the server was unable to provide it.""" - - status_code = 416 - message = ("The requestor wanted a range but the server was unable to " - "provide it.") - - -class UnprocessableEntity(HTTPClientError): - """There were semantic errors in the request.""" - - status_code = 422 - message = ("The request is of a valid content-type and structure but " - "semantically invalid.") - - -_4xx_classes = [ - BadRequest, - Unauthorized, - Forbidden, - NotFound, - MethodNotAllowed, - NotAcceptable, - ProxyAuthenticationRequired, - Conflict, - Gone, - LengthRequired, - PreconditionFailed, - RequestEntityTooLarge, - RequestUriTooLong, - UnsupportedMediaType, - RequestedRangeNotSatisfiable, - UnprocessableEntity, -] -_4xx_codes = {cls.status_code: cls for cls in _4xx_classes} - - -class HTTPServerError(HTTPError): - """The server encountered an error it could not recover from.""" - - message = "HTTP Server-side Error" - - -class InternalServerError(HTTPServerError): - """The server encountered an error it could not recover from.""" - - status_code = 500 - message = ("There was an internal server error that could not be recovered" - " from.") - - -_5xx_classes = [ - InternalServerError, - # NOTE(sigmavirus24): Allow for future expansion -] -_5xx_codes = {cls.status_code: cls for cls in _5xx_classes} - - -def _error_class_from(status_code): - if 400 <= status_code < 500: - cls = _4xx_codes.get(status_code, HTTPClientError) - elif 500 <= status_code < 600: - cls = _5xx_codes.get(status_code, HTTPServerError) - else: - cls = HTTPError - return cls - - -def error_from(response): - """Find an error code that matches a response status_code.""" - cls = _error_class_from(response.status_code) - return cls(response=response) - - -def raise_from(exception): - """Raise an exception from the keystoneauth1 exception.""" - cls = _error_class_from(exception.http_status) - return cls(response=exception.response, exception=exception) diff --git a/cratonclient/formatters/__init__.py b/cratonclient/formatters/__init__.py deleted file mode 100644 index 51a1b17..0000000 --- a/cratonclient/formatters/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# 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. -"""Module containing built-in formatters for cratonclient CLI.""" diff --git a/cratonclient/formatters/base.py b/cratonclient/formatters/base.py deleted file mode 100644 index acc702d..0000000 --- a/cratonclient/formatters/base.py +++ /dev/null @@ -1,107 +0,0 @@ -# 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 class implementation for formatting plugins.""" - - -class Formatter(object): - """Class that defines the formatter interface. - - Instead of having to override and call up to this class's ``__init__`` - method, we also provide an ``after_init`` method that can be implemented - to extend what happens on initialization. - - .. attribute:: args - - Parsed command-line arguments stored as an instance of - :class:`argparse.Namespace`. - - """ - - def __init__(self, parsed_args): - """Instantiate our formatter with the parsed CLI arguments. - - :param parsed_args: - The CLI arguments parsed by :mod:`argparse`. - :type parsed_args: - argparse.Namespace - """ - self.args = parsed_args - self.after_init() - - def after_init(self): - """Initialize the object further after ``__init__``.""" - pass - - def configure(self, *args, **kwargs): - """Optional configuration of the plugin after instantiation.""" - return self - - def handle(self, item_to_format): - """Handle a returned item from the cratonclient API. - - cratonclient's API produces both single Resource objects as well as - generators of those objects. This method should be capable of handling - both. - - Based on the type, this will either call ``handle_generator`` or - ``handle_instance``. Subclasses must implement both of those methods. - - :returns: - None - :rtype: - None - :raises ValueError: - If the item provided is not a subclass of - :class:`~cratonclient.crud.Resource` or an iterable class then - we will not know how to handle it. In that case, we will raise a - ValueError. - """ - to_dict = getattr(item_to_format, 'to_dict', None) - if to_dict is not None: - self.handle_instance(item_to_format) - return - - try: - self.handle_generator(item_to_format) - except TypeError as err: - raise ValueError( - "Expected an iterable object but instead received something " - "of type: %s. Received a TypeError: %s" % ( - type(item_to_format), - err - ) - ) - - def handle_instance(self, instance): - """Format and print the instance provided. - - :param instance: - The instance retrieved from the API that needs to be formatted. - :type instance: - cratonclient.crud.Resource - """ - raise NotImplementedError( - "A formatter plugin subclassed Formatter but did not implement" - " the handle_instance method." - ) - - def handle_generator(self, generator): - """Format and print the instance provided. - - :param generator: - The generator retrieved from the API whose items need to be - formatted. - """ - raise NotImplementedError( - "A formatter plugin subclassed Formatter but did not implement" - " the handle_generator method." - ) diff --git a/cratonclient/formatters/json_format.py b/cratonclient/formatters/json_format.py deleted file mode 100644 index f9bbdfe..0000000 --- a/cratonclient/formatters/json_format.py +++ /dev/null @@ -1,72 +0,0 @@ -# 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. -"""JSON formatter implementation for the craton CLI.""" -from __future__ import print_function - -import json - -from cratonclient.formatters import base - - -class Formatter(base.Formatter): - """JSON output formatter for the CLI.""" - - def after_init(self): - """Set-up our defaults. - - At some point in the future, we may allow people to configure this via - the CLI. - """ - self.indent = 4 - self.sort_keys = True - - def format(self, dictionary): - """Return the dictionary as a JSON string.""" - return json.dumps( - dictionary, - sort_keys=self.sort_keys, - indent=self.indent, - ) - - def handle_instance(self, instance): - """Print the JSON representation of a single instance.""" - print(self.format(instance.to_dict())) - - def handle_generator(self, generator): - """Print the JSON representation of a collection.""" - # NOTE(sigmavirus24): This is tricky logic that is caused by the JSON - # specification's intolerance for trailing commas. - try: - instance = next(generator) - except StopIteration: - # If there is nothing in the generator, we should just print an - # empty Array and then exit immediately. - print('[]') - return - - # Otherwise, let's print our opening bracket to start our Array - # formatting. - print('[', end='') - while True: - print(self.format(instance.to_dict()), end='') - # After printing our instance as a JSON object, we need to - # decide if we have another object to print. If we do have - # another object to print, we need to print a comma to separate - # our previous object and our next one. If we don't, we exit our - # loop to print our closing Array bracket. - try: - instance = next(generator) - except StopIteration: - break - else: - print(', ', end='') - print(']') diff --git a/cratonclient/formatters/table.py b/cratonclient/formatters/table.py deleted file mode 100644 index 6ae7b2d..0000000 --- a/cratonclient/formatters/table.py +++ /dev/null @@ -1,181 +0,0 @@ -# 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. -"""Pretty-table formatter implementation for the craton CLI.""" -from __future__ import print_function - -import textwrap - -from oslo_utils import encodeutils -import prettytable -import six - -from cratonclient.common import cliutils -from cratonclient.formatters import base - - -class Formatter(base.Formatter): - """Implementation of the default table-style formatter.""" - - def after_init(self): - """Set-up after initialization.""" - self.fields = [] - self.formatters = {} - self.sortby_index = None - self.mixed_case_fields = set([]) - self.field_labels = [] - self.dict_property = "Property" - self.wrap = 0 - self.dict_value = "Value" - - def configure(self, fields=None, formatters=None, sortby_index=False, - mixed_case_fields=None, field_labels=None, - dict_property=None, dict_value=None, wrap=None): - """Configure some of the settings used to print the tables. - - Parameters that configure list presentation: - - :param list fields: - List of field names as strings. - :param dict formatters: - Mapping of field names to formatter functions that accept the - resource. - :param int sortby_index: - The index of the field name in :param:`fields` to sort the table - rows by. If ``None``, PrettyTable will not sort the items at all. - :param list mixed_case_fields: - List of field names also in :param:`fields` that are mixed case - and need preprocessing prior to retrieving the attribute. - :param list field_labels: - List of field labels that need to match :param:`fields`. - - Parameters that configure the plain resource representation: - - :param str dict_property: - The name of the first column. - :param str dict_value: - The name of the second column. - :param int wrap: - Length at which to wrap the second column. - - All of these may be specified, but will be ignored based on how the - formatter is executed. - """ - if fields is not None: - self.fields = fields - if field_labels is None: - self.field_labels = cliutils.field_labels_from(self.fields) - elif len(field_labels) != len(self.fields): - raise ValueError( - "Field labels list %(labels)s has different number " - "of elements than fields list %(fields)s" % - {'labels': field_labels, 'fields': fields} - ) - else: - self.field_labels = field_labels - - if formatters is not None: - self.formatters = formatters - - if sortby_index is not False: - try: - sortby_index = int(sortby_index) - except TypeError: - if sortby_index is not None: - raise ValueError( - 'sortby_index must be None or an integer' - ) - except ValueError: - raise - else: - if self.field_labels and ( - sortby_index < 0 or - sortby_index > len(self.field_labels)): - raise ValueError( - 'sortby_index must be a non-negative number less ' - 'than {}'.format(len(self.field_labels)) - ) - self.sortby_index = sortby_index - - if mixed_case_fields is not None: - self.mixed_case_fields = set(mixed_case_fields) - - if dict_property is not None: - self.dict_property = dict_property - - if dict_value is not None: - self.dict_value = dict_value - - if wrap is not None: - self.wrap = wrap - - return self - - def sortby_kwargs(self): - """Generate the sortby keyword argument for PrettyTable.""" - if self.sortby_index is None: - return {} - return {'sortby': self.field_labels[self.sortby_index]} - - def build_table(self, field_labels, alignment='l'): - """Create a PrettyTable instance based off of the labels.""" - table = prettytable.PrettyTable(field_labels) - table.align = alignment - return table - - def handle_generator(self, generator): - """Handle a generator of resources.""" - sortby_kwargs = self.sortby_kwargs() - table = self.build_table(self.field_labels) - - for resource in generator: - row = [] - for field in self.fields: - formatter = self.formatters.get(field) - if formatter is not None: - data = formatter(resource) - else: - if field in self.mixed_case_fields: - field_name = field.replace(' ', '_') - else: - field_name = field.lower().replace(' ', '_') - data = getattr(resource, field_name, '') - row.append(data) - table.add_row(row) - - output = encodeutils.safe_encode(table.get_string(**sortby_kwargs)) - if six.PY3: - output = output.decode() - print(output) - - def handle_instance(self, instance): - """Handle a single resource.""" - table = self.build_table([self.dict_property, self.dict_value]) - - for key, value in sorted(instance.to_dict().items()): - if isinstance(value, dict): - value = six.text_type(value) - if self.wrap > 0: - value = textwrap.fill(six.text_type(value), self.wrap) - - if value and isinstance(value, six.string_types) and '\n' in value: - lines = value.strip().split('\n') - column1 = key - for line in lines: - table.add_row([column1, line]) - column1 = '' - else: - table.add_row([key, value]) - - output = encodeutils.safe_encode(table.get_string()) - if six.PY3: - output = output.decode() - print(output) diff --git a/cratonclient/session.py b/cratonclient/session.py deleted file mode 100644 index fa2dbe2..0000000 --- a/cratonclient/session.py +++ /dev/null @@ -1,288 +0,0 @@ -# -*- 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. -"""Craton-specific session details.""" -from itertools import chain -import logging - -from keystoneauth1 import exceptions as ksa_exc -from keystoneauth1 import session as ksa_session -from requests import exceptions as requests_exc - -import cratonclient -from cratonclient import auth -from cratonclient import exceptions as exc - -LOG = logging.getLogger(__name__) - - -class Session(object): - """Management class to allow different types of sessions to be used. - - If an instance of Craton is deployed with Keystone Middleware, this allows - for a keystoneauth session to be used so authentication will happen - immediately. - """ - - def __init__(self, session=None, username=None, token=None, - project_id=None): - """Initialize our Session. - - :param session: - The session instance to use as an underlying HTTP transport. If - not provided, we will create a keystoneauth1 Session object. - :param str username: - The username of the person authenticating against the API. - :param str token: - The authentication token of the user authenticating. - :param str project_id: - The user's project id in Craton. - """ - if session is None: - _auth = auth.CratonAuth( - username=username, - project_id=project_id, - token=token, - ) - session = ksa_session.Session(auth=_auth) - self._session = session - self._session.user_agent = 'python-cratonclient/{0}'.format( - cratonclient.__version__) - - def delete(self, url, **kwargs): - """Make a DELETE request with url and optional parameters. - - See the :meth:`Session.request` documentation for more details. - - .. code-block:: python - - >>> from cratonclient import session as craton - >>> session = craton.Session( - ... username='demo', - ... token='p@$$w0rd', - ... project_id='1', - ... ) - >>> response = session.delete('http://example.com') - """ - return self.request('DELETE', url, **kwargs) - - def get(self, url, **kwargs): - """Make a GET request with url and optional parameters. - - See the :meth:`Session.request` documentation for more details. - - .. code-block:: python - - >>> from cratonclient import session as craton - >>> session = craton.Session( - ... username='demo', - ... token='p@$$w0rd', - ... project_id='1', - ... ) - >>> response = session.get('http://example.com') - """ - return self.request('GET', url, **kwargs) - - def head(self, url, **kwargs): - """Make a HEAD request with url and optional parameters. - - See the :meth:`Session.request` documentation for more details. - - .. code-block:: python - - >>> from cratonclient import session as craton - >>> session = craton.Session( - ... username='demo', - ... token='p@$$w0rd', - ... project_id='1', - ... ) - >>> response = session.head('http://example.com') - """ - return self.request('HEAD', url, **kwargs) - - def options(self, url, **kwargs): - """Make an OPTIONS request with url and optional parameters. - - See the :meth:`Session.request` documentation for more details. - - .. code-block:: python - - >>> from cratonclient import session as craton - >>> session = craton.Session( - ... username='demo', - ... token='p@$$w0rd', - ... project_id='1', - ... ) - >>> response = session.options('http://example.com') - """ - return self.request('OPTIONS', url, **kwargs) - - def post(self, url, **kwargs): - """Make a POST request with url and optional parameters. - - See the :meth:`Session.request` documentation for more details. - - .. code-block:: python - - >>> from cratonclient import session as craton - >>> session = craton.Session( - ... username='demo', - ... token='p@$$w0rd', - ... project_id='1', - ... ) - >>> response = session.post( - ... 'http://example.com', - ... data=b'foo', - ... headers={'Content-Type': 'text/plain'}, - ... ) - """ - return self.request('POST', url, **kwargs) - - def put(self, url, **kwargs): - """Make a PUT request with url and optional parameters. - - See the :meth:`Session.request` documentation for more details. - - .. code-block:: python - - >>> from cratonclient import session as craton - >>> session = craton.Session( - ... username='demo', - ... token='p@$$w0rd', - ... project_id='1', - ... ) - >>> response = session.put( - ... 'http://example.com', - ... data=b'foo', - ... headers={'Content-Type': 'text/plain'}, - ... ) - """ - return self.request('PUT', url, **kwargs) - - def patch(self, url, **kwargs): - """Make a PATCH request with url and optional parameters. - - See the :meth:`Session.request` documentation for more details. - - .. code-block:: python - - >>> from cratonclient import session as craton - >>> session = craton.Session( - ... username='demo', - ... token='p@$$w0rd', - ... project_id='1', - ... ) - >>> response = session.put( - ... 'http://example.com', - ... data=b'foo', - ... headers={'Content-Type': 'text/plain'}, - ... ) - >>> response = session.patch( - ... 'http://example.com', - ... data=b'bar', - ... headers={'Content-Type': 'text/plain'}, - ... ) - """ - return self.request('PATCH', url, **kwargs) - - def request(self, method, url, **kwargs): - """Make a request with a method, url, and optional parameters. - - See also: python-requests.org for documentation of acceptable - parameters. - - .. code-block:: python - - >>> from cratonclient import session as craton - >>> session = craton.Session( - ... username='demo', - ... token='p@$$w0rd', - ... project_id='1', - ... ) - >>> response = session.request('GET', 'http://example.com') - """ - kwargs.setdefault('endpoint_filter', - {'service_type': 'fleet_management'}) - try: - response = self._session.request( - method=method, - url=url, - **kwargs - ) - except requests_exc.HTTPError as err: - raise exc.HTTPError(exception=err, response=err.response) - # NOTE(sigmavirus24): The ordering of Timeout before ConnectionError - # is important on requests 2.x. The ConnectTimeout exception inherits - # from both ConnectionError and Timeout. To catch both connect and - # read timeouts similarly, we need to catch this one first. - except requests_exc.Timeout as err: - raise exc.Timeout(exception=err) - except requests_exc.ConnectionError as err: - raise exc.ConnectionFailed(exception=err) - except ksa_exc.HttpError as err: - raise exc.raise_from(err) - - if response.status_code >= 400: - raise exc.error_from(response) - - return response - - def paginate(self, url, items_key, autopaginate=True, nested=False, - **kwargs): - """Make a GET request to a paginated resource. - - If :param:`autopaginate` is set to ``True``, this will automatically - handle finding and retrieving the next page of items. - - .. code-block:: python - - >>> from cratonclient import session as craton - >>> session = craton.Session( - ... username='demo', - ... token='p@##w0rd', - ... project_id='84363597-721c-4068-9731-8824692b51bb', - ... ) - >>> url = 'https://example.com/v1/hosts' - >>> for response in session.paginate(url, items_key='hosts'): - ... print("Received status {}".format(response.status_code)) - ... print("Received {} items".format(len(items))) - - :param bool autopaginate: - Determines whether or not this method continues requesting items - automatically after the first page. - """ - get_items = True - - while get_items: - response = self.get(url, **kwargs) - json_body = response.json() - if nested: - items = list(chain(*json_body[items_key].values())) - else: - items = json_body[items_key] - - yield response, items - - links = json_body['links'] - url = _find_next_link(links) - - kwargs = {} - get_items = url and autopaginate and len(items) > 0 - - -def _find_next_link(links): - for link in links: - if link['rel'] == 'next': - return link['href'] - - return None diff --git a/cratonclient/shell/__init__.py b/cratonclient/shell/__init__.py deleted file mode 100644 index 1dab1a0..0000000 --- a/cratonclient/shell/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Command-line application that interfaces with Craton API.""" diff --git a/cratonclient/shell/main.py b/cratonclient/shell/main.py deleted file mode 100644 index 981cf0e..0000000 --- a/cratonclient/shell/main.py +++ /dev/null @@ -1,207 +0,0 @@ -# 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. -"""Main shell for parsing arguments directed toward Craton.""" - -from __future__ import print_function - -import argparse -import sys - -from oslo_utils import encodeutils -from oslo_utils import importutils -from stevedore import extension - -from cratonclient import __version__ -from cratonclient import exceptions as exc -from cratonclient import session as craton - -from cratonclient.common import cliutils -from cratonclient.v1 import client - - -FORMATTERS_NAMESPACE = 'cratonclient.formatters' - - -class CratonShell(object): - """Class used to handle shell definition and parsing.""" - - def __init__(self): - """Initialize our shell object. - - This sets up our formatters extension manager. If we add further - managers, they will be initialized here. - """ - self.extension_mgr = extension.ExtensionManager( - namespace=FORMATTERS_NAMESPACE, - invoke_on_load=False, - ) - - def get_base_parser(self): - """Configure base craton arguments and parsing.""" - parser = argparse.ArgumentParser( - prog='craton', - description=__doc__.strip(), - epilog='See "craton help COMMAND" ' - 'for help on a specific command.', - add_help=False, - formatter_class=argparse.HelpFormatter - ) - - parser.add_argument('-h', '--help', - action='store_true', - help=argparse.SUPPRESS, - ) - parser.add_argument('--version', - action='version', - version=__version__, - ) - parser.add_argument('--format', - default='default', - choices=list(sorted(self.extension_mgr.names())), - help='The format to use to print the information ' - 'to the console. Defaults to pretty-printing ' - 'using ASCII tables.', - ) - parser.add_argument('--craton-url', - default=cliutils.env('CRATON_URL'), - help='The base URL of the running Craton service.' - ' Defaults to env[CRATON_URL].', - ) - parser.add_argument('--craton-version', - type=int, - default=cliutils.env('CRATON_VERSION', - default=1), - help='The version of the Craton API to use. ' - 'Defaults to env[CRATON_VERSION].' - ) - parser.add_argument('--os-project-id', - default=cliutils.env('OS_PROJECT_ID'), - help='The project ID used to authenticate to ' - 'Craton. Defaults to env[OS_PROJECT_ID].', - ) - parser.add_argument('--os-username', - default=cliutils.env('OS_USERNAME'), - help='The username used to authenticate to ' - 'Craton. Defaults to env[OS_USERNAME].', - ) - parser.add_argument('--os-password', - default=cliutils.env('OS_PASSWORD'), - help='The password used to authenticate to ' - 'Craton. Defaults to env[OS_PASSWORD].', - ) - return parser - - # NOTE(cmspence): Credit for this get_subcommand_parser function - # goes to the magnumclient developers and contributors. - def get_subcommand_parser(self, api_version): - """Get subcommands by parsing COMMAND_MODULES.""" - parser = self.get_base_parser() - - self.subcommands = {} - subparsers = parser.add_subparsers(metavar='', - dest='subparser_name') - shell = importutils.import_versioned_module( - 'cratonclient.shell', - api_version, - 'shell', - ) - command_modules = shell.COMMAND_MODULES - for command_module in command_modules: - self._find_subparsers(subparsers, command_module) - self._find_subparsers(subparsers, self) - return parser - - # NOTE(cmspence): Credit for this function goes to the - # magnumclient developers and contributors. - def _find_subparsers(self, subparsers, actions_module): - """Find subparsers by looking at *_shell files.""" - help_formatter = argparse.HelpFormatter - for attr in (a for a in dir(actions_module) if a.startswith('do_')): - command = attr[3:].replace('_', '-') - callback = getattr(actions_module, attr) - desc = callback.__doc__ or '' - action_help = desc.strip() - arguments = getattr(callback, 'arguments', []) - subparser = (subparsers.add_parser(command, - help=action_help, - description=desc, - add_help=False, - formatter_class=help_formatter) - ) - subparser.add_argument('-h', '--help', - action='help', - help=argparse.SUPPRESS) - self.subcommands[command] = subparser - for (args, kwargs) in arguments: - subparser.add_argument(*args, **kwargs) - subparser.set_defaults(func=callback) - - def main(self, argv): - """Main entry-point for cratonclient shell argument parsing.""" - parser = self.get_base_parser() - (options, args) = parser.parse_known_args(argv) - subcommand_parser = ( - self.get_subcommand_parser(options.craton_version) - ) - self.parser = subcommand_parser - - if options.help or not argv: - self.parser.print_help() - return 0 - - args = subcommand_parser.parse_args(argv) - - # Short-circuit and deal with help right away. - if args.func == self.do_help: - self.do_help(args) - return 0 - - session = craton.Session( - username=args.os_username, - token=args.os_password, - project_id=args.os_project_id, - ) - self.cc = client.Client(session, args.craton_url) - formatter_class = self.extension_mgr[args.format].plugin - args.formatter = formatter_class(args) - args.func(self.cc, args) - - @cliutils.arg( - 'command', - metavar='', - nargs='?', - help='Display help for .') - def do_help(self, args): - """Display help about this program or one of its subcommands.""" - if args.command: - if args.command in self.subcommands: - self.subcommands[args.command].print_help() - else: - raise exc.CommandError("'%s' is not a valid subcommand" % - args.command) - else: - self.parser.print_help() - - -def main(): - """Main entry-point for cratonclient's CLI.""" - try: - CratonShell().main([encodeutils.safe_decode(a) for a in sys.argv[1:]]) - except Exception as e: - print("ERROR: {}".format(encodeutils.exception_to_unicode(e)), - file=sys.stderr) - sys.exit(1) - return 0 - - -if __name__ == "__main__": - main() diff --git a/cratonclient/shell/v1/__init__.py b/cratonclient/shell/v1/__init__.py deleted file mode 100644 index 69318df..0000000 --- a/cratonclient/shell/v1/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Shell libraries for version 1 of Craton's API.""" diff --git a/cratonclient/shell/v1/cells_shell.py b/cratonclient/shell/v1/cells_shell.py deleted file mode 100644 index 3a45132..0000000 --- a/cratonclient/shell/v1/cells_shell.py +++ /dev/null @@ -1,283 +0,0 @@ -# -*- 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. -"""Cells resource and resource shell wrapper.""" -from __future__ import print_function - -import argparse -import sys - -from cratonclient.common import cliutils -from cratonclient import exceptions as exc - -DEFAULT_CELL_FIELDS = [ - 'id', - 'name', - 'cloud_id', - 'region_id', - 'created_at', -] - -CELL_FIELDS = DEFAULT_CELL_FIELDS + [ - 'updated_at', - 'note', - 'variables', - 'project_id', -] - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID of the cell.') -def do_cell_show(cc, args): - """Show detailed information about a cell.""" - cell = cc.cells.get(args.id) - args.formatter.configure(wrap=72).handle(cell) - - -@cliutils.arg('-r', '--region', - metavar='', - type=int, - help='ID of the region that the cell belongs to.') -@cliutils.arg('--cloud', - metavar='', - type=int, - help='ID of the cloud that the cell belongs to.') -@cliutils.arg('--detail', - action='store_true', - default=False, - help='Show detailed information about the cells.') -@cliutils.arg('--sort-key', - metavar='', - help='Cell field that will be used for sorting.') -@cliutils.arg('--sort-dir', - metavar='', - default='asc', - choices=('asc', 'desc'), - help='Sort direction: "asc" (default) or "desc".') -@cliutils.arg('--fields', - nargs='+', - metavar='', - default=DEFAULT_CELL_FIELDS, - help='Space-separated list of fields to display. ' - 'Only these fields will be fetched from the server. ' - 'Can not be used when "--detail" is specified') -@cliutils.arg('--all', - action='store_true', - default=False, - help='Retrieve and show all cells. This will override ' - 'the provided value for --limit and automatically ' - 'retrieve each page of results.') -@cliutils.arg('--limit', - metavar='', - type=int, - help='Maximum number of cells to return.') -@cliutils.arg('--marker', - metavar='', - default=None, - help='ID of the cell to use to resume listing cells.') -@cliutils.arg('--vars', - metavar='', - nargs='+', - action='append', - default=[], - help='Variables to use as filter in the form of ' - '--vars="key:value" --vars="key2:value2"') -def do_cell_list(cc, args): - """Print list of cells which are registered with the Craton service.""" - params = {} - if args.vars: - query_vars = ",".join([i[0] for i in args.vars]) - params['vars'] = query_vars - if args.cloud is not None: - params['cloud_id'] = args.cloud - if args.limit is not None: - if args.limit < 0: - raise exc.CommandError('Invalid limit specified. Expected ' - 'non-negative limit, got {0}' - .format(args.limit)) - params['limit'] = args.limit - if args.all is True: - params['limit'] = 100 - - if args.detail: - if args.fields and args.fields == DEFAULT_CELL_FIELDS: - args.fields = CELL_FIELDS - else: - raise exc.CommandError( - 'Cannot specify both --fields and --detail.' - ) - params['detail'] = args.detail - - fields = args.fields - for field in fields: - if field not in CELL_FIELDS: - raise exc.CommandError( - 'Invalid field "{}"'.format(field) - ) - sort_key = args.sort_key and args.sort_key.lower() - if sort_key is not None: - if sort_key not in CELL_FIELDS: - raise exc.CommandError( - ('"--sort-key" value was "{}" but should ' - 'be one of: {}').format( - args.sort_key, - ', '.join(CELL_FIELDS) - ) - ) - params['sort_key'] = sort_key - if args.region is not None: - params['region_id'] = args.region - - params['sort_dir'] = args.sort_dir - params['marker'] = args.marker - params['autopaginate'] = args.all - - listed_cells = cc.cells.list(**params) - args.formatter.configure(fields=fields).handle(listed_cells) - - -@cliutils.arg('-n', '--name', - metavar='', - required=True, - help='Name of the cell.') -@cliutils.arg('-r', '--region', - dest='region_id', - metavar='', - type=int, - required=True, - help='ID of the region that the cell belongs to.') -@cliutils.arg('--cloud', - dest='cloud_id', - metavar='', - type=int, - required=True, - help='ID of the cloud that the cell belongs to.') -@cliutils.arg('--note', - help='Note about the cell.') -def do_cell_create(cc, args): - """Register a new cell with the Craton service.""" - fields = {k: v for (k, v) in vars(args).items() - if k in CELL_FIELDS and not (v is None)} - cell = cc.cells.create(**fields) - args.formatter.configure(wrap=72).handle(cell) - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID of the cell.') -@cliutils.arg('-n', '--name', - metavar='', - help='Name of the cell.') -@cliutils.arg('-r', '--region', - dest='region_id', - metavar='', - type=int, - help='Desired ID of the region that the cell should change to.') -@cliutils.arg('--cloud', - dest='cloud_id', - metavar='', - type=int, - help='Desired ID of the cloud that the cell should change to.') -@cliutils.arg('--note', - help='Note about the cell.') -def do_cell_update(cc, args): - """Update a cell that is registered with the Craton service.""" - fields = {k: v for (k, v) in vars(args).items() - if k in CELL_FIELDS and not (v is None)} - cell_id = fields.pop('id') - if not fields: - raise exc.CommandError( - 'Nothing to update... Please specify one of --name, --region, ' - '--cloud, or --note' - ) - cell = cc.cells.update(cell_id, **fields) - args.formatter.configure(wrap=72).handle(cell) - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID of the cell.') -def do_cell_delete(cc, args): - """Delete a cell that is registered with the Craton service.""" - try: - response = cc.cells.delete(args.id) - except exc.ClientException as client_exc: - raise exc.CommandError( - 'Failed to delete cell {} due to "{}:{}"'.format( - args.id, client_exc.__class__, str(client_exc) - ) - ) - else: - print("Cell {0} was {1} deleted.". - format(args.id, 'successfully' if response else 'not')) - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID or name of the cell.') -@cliutils.handle_shell_exception -def do_cell_vars_get(cc, args): - """Get variables for a cell.""" - variables = cc.cells.get(args.id).variables.get() - formatter = args.formatter.configure(dict_property="Variable", wrap=72) - formatter.handle(variables) - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID of the cell.') -@cliutils.arg('variables', nargs=argparse.REMAINDER) -@cliutils.handle_shell_exception -def do_cell_vars_set(cc, args): - """Set variables for a cell.""" - cell_id = args.id - if not args.variables and sys.stdin.isatty(): - raise exc.CommandError( - 'Nothing to update... Please specify variables to set in the ' - 'following format: "key=value". You may also specify variables to ' - 'delete by key using the format: "key="' - ) - adds, deletes = cliutils.variable_updates(args.variables) - variables = cc.cells.get(cell_id).variables - if deletes: - variables.delete(*deletes) - variables.update(**adds) - formatter = args.formatter.configure(wrap=72, dict_property="Variable") - formatter.handle(variables.get()) - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID of the cell.') -@cliutils.arg('variables', nargs=argparse.REMAINDER) -@cliutils.handle_shell_exception -def do_cell_vars_delete(cc, args): - """Delete variables for a cell by key.""" - cell_id = args.id - if not args.variables and sys.stdin.isatty(): - raise exc.CommandError( - 'Nothing to delete... Please specify variables to delete by ' - 'listing the keys you wish to delete separated by spaces.' - ) - deletes = cliutils.variable_deletes(args.variables) - variables = cc.cells.get(cell_id).variables - response = variables.delete(*deletes) - print("Variables {0} deleted.". - format('successfully' if response else 'not')) diff --git a/cratonclient/shell/v1/clouds_shell.py b/cratonclient/shell/v1/clouds_shell.py deleted file mode 100644 index 86b0a51..0000000 --- a/cratonclient/shell/v1/clouds_shell.py +++ /dev/null @@ -1,215 +0,0 @@ -# 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. -"""Hosts resource and resource shell wrapper.""" -from __future__ import print_function - -import argparse -import sys - -from cratonclient.common import cliutils -from cratonclient import exceptions as exc - -DEFAULT_CLOUD_FIELDS = [ - 'id', - 'name', - 'created_at', -] - -CLOUD_FIELDS = DEFAULT_CLOUD_FIELDS + [ - 'updated_at', - 'note', - 'project_id', -] - - -@cliutils.arg('-n', '--name', - metavar='', - required=True, - help='Name of the host.') -@cliutils.arg('--note', - help='Note about the host.') -def do_cloud_create(cc, args): - """Register a new cloud with the Craton service.""" - fields = {k: v for (k, v) in vars(args).items() - if k in CLOUD_FIELDS and not (v is None)} - - cloud = cc.clouds.create(**fields) - args.formatter.configure(wrap=72).handle(cloud) - - -@cliutils.arg('--fields', - nargs='+', - metavar='', - default=DEFAULT_CLOUD_FIELDS, - help='Comma-separated list of fields to display. ' - 'Only these fields will be fetched from the server. ' - 'Can not be used when "--detail" is specified') -@cliutils.arg('--all', - action='store_true', - default=False, - help='Retrieve and show all clouds. This will override ' - 'the provided value for --limit and automatically ' - 'retrieve each page of results.') -@cliutils.arg('--detail', - action='store_true', - default=False, - help='Show detailed information about all clouds.') -@cliutils.arg('--limit', - metavar='', - type=int, - help='Maximum number of clouds to return.') -@cliutils.arg('--marker', - metavar='', - default=None, - help='ID of the cell to use to resume listing clouds.') -def do_cloud_list(cc, args): - """List all clouds.""" - params = {} - if args.limit is not None: - if args.limit < 0: - raise exc.CommandError('Invalid limit specified. Expected ' - 'non-negative limit, got {0}' - .format(args.limit)) - params['limit'] = args.limit - if args.all is True: - params['limit'] = 100 - - if args.detail: - if args.fields and args.fields == DEFAULT_CLOUD_FIELDS: - args.fields = CLOUD_FIELDS - else: - raise exc.CommandError( - 'Cannot specify both --fields and --detail.' - ) - params['detail'] = args.detail - - fields = args.fields - for field in args.fields: - if field not in CLOUD_FIELDS: - raise exc.CommandError( - 'Invalid field "{}"'.format(field) - ) - - params['marker'] = args.marker - params['autopaginate'] = args.all - - clouds_list = cc.clouds.list(**params) - args.formatter.configure(fields=list(fields)).handle(clouds_list) - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID of the cloud.') -def do_cloud_show(cc, args): - """Show detailed information about a cloud.""" - cloud = cc.clouds.get(args.id) - args.formatter.configure(wrap=72).handle(cloud) - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID of the cloud') -@cliutils.arg('-n', '--name', - metavar='', - help='Name of the cloud.') -@cliutils.arg('--note', - help='Note about the cloud.') -def do_cloud_update(cc, args): - """Update a cloud that is registered with the Craton service.""" - fields = {k: v for (k, v) in vars(args).items() - if k in CLOUD_FIELDS and not (v is None)} - item_id = fields.pop('id') - if not fields: - raise exc.CommandError( - 'Nothing to update... Please specify one or more of --name, or ' - '--note' - ) - cloud = cc.clouds.update(item_id, **fields) - args.formatter.configure(wrap=72).handle(cloud) - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID of the cloud.') -def do_cloud_delete(cc, args): - """Delete a cloud that is registered with the Craton service.""" - try: - response = cc.clouds.delete(args.id) - except exc.ClientException as client_exc: - raise exc.CommandError( - 'Failed to delete cloud {} due to "{}:{}"'.format( - args.id, client_exc.__class__, str(client_exc), - ) - ) - else: - print("Cloud {0} was {1} deleted.". - format(args.id, 'successfully' if response else 'not')) - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID or name of the cloud.') -@cliutils.handle_shell_exception -def do_cloud_vars_get(cc, args): - """Get variables for a cloud.""" - variables = cc.clouds.get(args.id).variables.get() - formatter = args.formatter.configure(dict_property="Variable", wrap=72) - formatter.handle(variables) - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID of the cloud.') -@cliutils.arg('variables', nargs=argparse.REMAINDER) -@cliutils.handle_shell_exception -def do_cloud_vars_set(cc, args): - """Set variables for a cloud.""" - cloud_id = args.id - if not args.variables and sys.stdin.isatty(): - raise exc.CommandError( - 'Nothing to update... Please specify variables to set in the ' - 'following format: "key=value". You may also specify variables to ' - 'delete by key using the format: "key="' - ) - adds, deletes = cliutils.variable_updates(args.variables) - variables = cc.clouds.get(cloud_id).variables - if deletes: - variables.delete(*deletes) - variables.update(**adds) - formatter = args.formatter.configure(wrap=72, dict_property="Variable") - formatter.handle(variables.get()) - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID of the cloud.') -@cliutils.arg('variables', nargs=argparse.REMAINDER) -@cliutils.handle_shell_exception -def do_cloud_vars_delete(cc, args): - """Delete variables for a cloud by key.""" - cloud_id = args.id - if not args.variables and sys.stdin.isatty(): - raise exc.CommandError( - 'Nothing to delete... Please specify variables to delete by ' - 'listing the keys you wish to delete separated by spaces.' - ) - deletes = cliutils.variable_deletes(args.variables) - variables = cc.clouds.get(cloud_id).variables - response = variables.delete(*deletes) - print("Variables {0} deleted.". - format('successfully' if response else 'not')) diff --git a/cratonclient/shell/v1/devices_shell.py b/cratonclient/shell/v1/devices_shell.py deleted file mode 100644 index afcce9e..0000000 --- a/cratonclient/shell/v1/devices_shell.py +++ /dev/null @@ -1,150 +0,0 @@ -# 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. -"""Hosts resource and resource shell wrapper.""" -from __future__ import print_function - -from cratonclient.common import cliutils -from cratonclient import exceptions as exc - -DEFAULT_DEVICE_FIELDS = [ - 'id', - 'name', - 'device_type', - 'ip_address', - 'cloud_id', - 'region_id', - 'cell_id', - 'parent_id', -] - - -DEVICE_FIELDS = DEFAULT_DEVICE_FIELDS + [ - 'note', - 'created_at', - 'updated_at', - 'project_id', -] - - -@cliutils.arg('--fields', - nargs='+', - metavar='', - default=DEFAULT_DEVICE_FIELDS, - help='Space-separated list of fields to display. ' - 'Only these fields will be fetched from the server. ' - 'This cannot be combined with --detail.') -@cliutils.arg('--detail', - action='store_true', - default=False, - help='Retrieve and show all detail about devices in listing.') -@cliutils.arg('--all', - action='store_true', - default=False, - help='Retrieve and show all devices. This will override ' - 'the provided value for --limit and automatically ' - 'retrieve each page of results.') -@cliutils.arg('--sort-key', - metavar='', - help='Device field that will be used for sorting.') -@cliutils.arg('--sort-dir', - metavar='', - default='asc', - choices=('asc', 'desc'), - help='Sort direction: "asc" (default) or "desc".') -@cliutils.arg('--limit', - metavar='', - type=int, - help='Maximum number of devices to return.') -@cliutils.arg('--marker', - metavar='', - default=None, - help='ID of the device to use to resume listing devices.') -@cliutils.arg('--cloud', - metavar='', - type=int, - help='ID of the cloud that the device belongs to.') -@cliutils.arg('-r', '--region', - metavar='', - type=int, - help='ID of the region that the device belongs to.') -@cliutils.arg('-c', '--cell', - metavar='', - type=int, - help='Integer ID of the cell that contains ' - 'the desired list of devices.') -@cliutils.arg('--parent', - metavar='', - type=int, - help='Parent ID of required devices.') -@cliutils.arg('--descendants', - default=False, - action='store_true', - help='When parent is also specified, include all descendants.') -@cliutils.arg('--active', - metavar='', - choices=("true", "false"), - help='Filter devices by their active state.') -def do_device_list(cc, args): - """List all devices.""" - params = {} - if args.limit is not None: - if args.limit < 0: - raise exc.CommandError('Invalid limit specified. Expected ' - 'non-negative limit, got {0}' - .format(args.limit)) - params['limit'] = args.limit - if args.all is True: - params['limit'] = 100 - - if args.detail: - if args.fields and args.fields == DEFAULT_DEVICE_FIELDS: - args.fields = DEVICE_FIELDS - else: - raise exc.CommandError( - 'Cannot specify both --fields and --detail.' - ) - params['detail'] = args.detail - - fields = args.fields - for field in fields: - if field not in DEVICE_FIELDS: - raise exc.CommandError( - 'Invalid field "{}"'.format(field) - ) - - sort_key = args.sort_key and args.sort_key.lower() - if sort_key is not None: - if sort_key not in DEVICE_FIELDS: - raise exc.CommandError( - '{0} is an invalid key for sorting, valid values for ' - '--sort-key are: {1}'.format( - args.sort_key, DEVICE_FIELDS - ) - ) - params['sort_keys'] = sort_key - params['sort_dir'] = args.sort_dir - params['marker'] = args.marker - params['autopaginate'] = args.all - if args.parent: - params['parent_id'] = args.parent - params['descendants'] = args.descendants - if args.cloud: - params['cloud_id'] = args.cloud - if args.region: - params['region_id'] = args.region - if args.cell: - params['cell_id'] = args.cell - if args.active: - params['active'] = args.active - - devices_list = cc.devices.list(**params) - args.formatter.configure(fields=fields).handle(devices_list) diff --git a/cratonclient/shell/v1/hosts_shell.py b/cratonclient/shell/v1/hosts_shell.py deleted file mode 100644 index cdbb185..0000000 --- a/cratonclient/shell/v1/hosts_shell.py +++ /dev/null @@ -1,341 +0,0 @@ -# -*- 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. -"""Hosts resource and resource shell wrapper.""" -from __future__ import print_function - -import argparse -import sys - -from cratonclient.common import cliutils -from cratonclient import exceptions as exc - - -DEFAULT_HOST_FIELDS = [ - 'id', - 'name', - 'active', - 'device_type', - 'ip_address', - 'cloud_id', - 'region_id', - 'cell_id', - 'created_at', -] - -HOST_FIELDS = DEFAULT_HOST_FIELDS + [ - 'updated_at', - 'note', - 'variables', - 'labels', - 'parent_id', - 'project_id', -] - - -@cliutils.arg('id', - metavar='', - type=int, - help='ID of the host.') -def do_host_show(cc, args): - """Show detailed information about a host.""" - host = cc.hosts.get(args.id) - args.formatter.configure(wrap=72).handle(host) - - -@cliutils.arg('-r', '--region', - metavar='', - type=int, - help='ID of the region that the host belongs to.') -@cliutils.arg('--cloud', - metavar='', - type=int, - help='ID of the cloud that the host belongs to.') -@cliutils.arg('-c', '--cell', - metavar='', - type=int, - help='Integer ID of the cell that contains ' - 'the desired list of hosts.') -@cliutils.arg('--detail', - action='store_true', - default=False, - help='Show detailed information about the hosts.') -@cliutils.arg('--sort-key', - metavar='', - help='Host field that will be used for sorting.') -@cliutils.arg('--sort-dir', - metavar='', - default='asc', - choices=('asc', 'desc'), - help='Sort direction: "asc" (default) or "desc".') -@cliutils.arg('--fields', - nargs='+', - metavar='', - default=DEFAULT_HOST_FIELDS, - help='Space-separated list of fields to display. ' - 'Only these fields will be fetched from the server. ' - 'Can not be used when "--detail" is specified') -@cliutils.arg('--all', - action='store_true', - default=False, - help='Retrieve and show all hosts. This will override ' - 'the provided value for --limit and automatically ' - 'retrieve each page of results.') -@cliutils.arg('--limit', - metavar='', - type=int, - help='Maximum number of hosts to return.') -@cliutils.arg('--marker', - metavar='', - default=None, - help='ID of the cell to use to resume listing hosts.') -@cliutils.arg('--device-type', - metavar='', - default=None, - help='Device type to use as filter.') -@cliutils.arg('--vars', - metavar='', - default=None, - help='Variables to use as filter in the form of key:value.') -@cliutils.arg('--label', - metavar='