From 9a52ce01e2188bc347d767481bb1a5b2ca3992df Mon Sep 17 00:00:00 2001 From: "janne.suominen" Date: Wed, 8 May 2019 15:02:46 +0300 Subject: [PATCH] Seed code for hostcli Seed code for hostcli Change-Id: I964d71697266410830d99122f64b6ff1511c0b40 Signed-off-by: janne.suominen --- LICENSE | 202 +++++++++++++++++++++++++++++ README.rst | 134 ++++++++++++++++++++ docs/hostcli.asciio | Bin 0 -> 27137 bytes docs/hostcli.png | Bin 0 -> 67020 bytes hostcli.spec | 58 +++++++++ src/hostcli/__init__.py | 15 +++ src/hostcli/helper.py | 307 +++++++++++++++++++++++++++++++++++++++++++++ src/hostcli/main.py | 121 ++++++++++++++++++ src/hostcli/resthandler.py | 132 +++++++++++++++++++ src/setup.py | 42 +++++++ 10 files changed, 1011 insertions(+) create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 docs/hostcli.asciio create mode 100644 docs/hostcli.png create mode 100644 hostcli.spec create mode 100644 src/hostcli/__init__.py create mode 100644 src/hostcli/helper.py create mode 100644 src/hostcli/main.py create mode 100644 src/hostcli/resthandler.py create mode 100644 src/setup.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + 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. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..93fa916 --- /dev/null +++ b/README.rst @@ -0,0 +1,134 @@ +:: + + Copyright 2019 Nokia + + 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. + + +============================== +Host CLI +============================== + +.. raw:: pdf + + PageBreak + +.. sectnum:: + +.. contents:: + +.. raw:: pdf + + PageBreak + +Introduction +============ + +Cloud infratructure is openstack based, openstack services already utilize +a common framework for openstack own commands. These commands are accessed via +the **openstack** command. The syntax used in this command is the following: + +:: + + openstack [] + + +Below is an example of some **openstack** commands. + +:: + + openstack server list + + openstack server show + + openstack network list + + ... + +The **openstack** command is implemented ontop of the python **cliff** framework. + +**cliff** framework supports the following: + +- structure the commands tokens. + +- map command tokens to command handlers. + +- parse command arguments. + +- format commands output. + +- bash auto-completion of commands. + +A well integrated platform is expected to provide a common way to access the +interfaces provided by the platform. As such cloud infrastructure utilizes +the same **cliff** framework to implement the infrastrcture own middleware CLIs. + +The infrastrcutre CLIs can be accessed via the **hostcli** command. + +The syntax used in this command is the following: + +:: + + hostcli [] + +Below is an example of some **hostcli** commands. + +:: + + hostcli has show nodes + + hostcli has show services --node + + hostcli motd show + + hostcli motd set --motd + + ... + +In addition to providing the common look and feel for the end user, the +*hostcli* takes care of authorizing with keystone and provide a token +for authentication usage + +The following diagram describes the highlevel design for **hostcli** CLI +frameowk. + +.. image:: docs/hostcli.png + + +When using yarf restful framework to implement the backend for middleware +the frame provides a RestRequest class to make the authentication transperant +to a module. + +Example of using this request: + +.. code:: python + + def take_action(self, parsed_args): + req = self.app.client_manager.resthandler + ret = req.get("has/v1/cluster", decode_json=True) + status = ret['data'] + columns = ('admin-state', + 'running-state', + 'role' + ) + data = (status['admin-state'], + status['running-state'], + status['role'] + ) + return (columns, data) + +The beef is ot get the resthandler from client_manager that is defined in the app. +This will return the object RestRequest for you that has the HTTP operations predefined. +The only thing needed is the path of the url. The actual address should not be defined, +since it's extracted from the keystone session from the endpoints. diff --git a/docs/hostcli.asciio b/docs/hostcli.asciio new file mode 100644 index 0000000000000000000000000000000000000000..53b68002b0e919683e2c8aed1a134ded76930b33 GIT binary patch literal 27137 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 Xnn6b9H~#RpojI371<|`i6!L!nL|cU# literal 0 HcmV?d00001 diff --git a/docs/hostcli.png b/docs/hostcli.png new file mode 100644 index 0000000000000000000000000000000000000000..6e089c8c3ef05d0f4fa96572bba9114f715259c5 GIT binary patch literal 67020 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 literal 0 HcmV?d00001 diff --git a/hostcli.spec b/hostcli.spec new file mode 100644 index 0000000..1f81310 --- /dev/null +++ b/hostcli.spec @@ -0,0 +1,58 @@ +# Copyright 2019 Nokia +# +# 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. +# + +Name: hostcli +Version: %{_version} +Release: 1%{?dist} +Summary: Contains code for the hostcli framework. +License: %{_platform_licence} +Source0: %{name}-%{version}.tar.gz +Vendor: %{_platform_vendor} +BuildArch: noarch + +Requires: python-cliff python-pip python2-osc-lib python-openstackclient +BuildRequires: python +BuildRequires: python-setuptools + + +%description +This RPM contains source code for the hostcli framework. + +%prep +%autosetup + +%install +cd src && python setup.py install --root %{buildroot} --no-compile --install-purelib %{_python_site_packages_path} --install-scripts %{_platform_bin_path} && cd - + + +%files +%{_python_site_packages_path}/hostcli* +#%{_python_site_packages_path}/hostcli.* +%{_platform_bin_path}/hostcli + +%pre + +%post + + +%preun + +%postun + +%clean +rm -rf %{buildroot} + +# TIPS: +# File /usr/lib/rpm/macros contains useful variables which can be used for example to define target directory for man page. diff --git a/src/hostcli/__init__.py b/src/hostcli/__init__.py new file mode 100644 index 0000000..f035b4a --- /dev/null +++ b/src/hostcli/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2019 Nokia +# +# 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. +# + diff --git a/src/hostcli/helper.py b/src/hostcli/helper.py new file mode 100644 index 0000000..9f569c4 --- /dev/null +++ b/src/hostcli/helper.py @@ -0,0 +1,307 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import sys +import re +from datetime import datetime +from dateutil import tz +from cliff.show import ShowOne +from cliff.lister import Lister +from cliff.command import Command + + +DEFAULT = 'default' +ALL = 'all' +DISPLAY = 'display' +HELP = 'help' +SORT = 'sort' +DATA = 'data' +CHOICES = 'choices' # allowed values for an argument (coming from argparse 'choices' facility) +VALUES = 'values' # allowed values for an argument, even if the argument supports adding multiple instances +FIELDS = 'fields' # filtered list of expected columns in the response (translates to argparse's 'columns' of) +COLUMNS = 'columns' # same as fields +DETAILED = 'detailed' # Makes the command to show all accessible details of the requsted objects. + # Should not be positional (so should not be the first in the arguments list) +TIME = 'time' +UTC = 'utc' + + +class HelperBase(object): + """Helper base class validating arguments and doing the business logic (send query and receive and process table in response)""" + def __init__(self): + self.operation = 'get' + self.endpoint = '' + self.arguments = [] + self.columns = [] + self.detailed = [] + self.fieldmap = {} + self.message = '' + self.no_positional = False + self.mandatory_positional = False + self.usebody = False + self.resource_prefix = '' + self.default_sort = None + self.positional_count = 1 # how many mandatory arguments are + + def get_parser_with_arguments(self, parser): + args = self.arguments[:] + if self.no_positional is False: + for i in range (0, self.positional_count): + first = args.pop(0) + parser.add_argument(first, + metavar=first.upper(), + nargs=None if self.mandatory_positional else '?', + default=self.fieldmap[first].get(DEFAULT, ALL), + help=self.fieldmap[first][HELP]) + for e in args: + # This is very similar to 'choices' facility of argparse, however it allows multiple choices combined... + multichoices = '' + default = self.fieldmap[e].get(DEFAULT, ALL) + if e in [DETAILED, UTC]: + parser.add_argument('--%s' % e, dest=e, action='store_true', help=self.fieldmap[e][HELP]) + continue + if e == SORT: + # ...and is needed here to list the allowed arguments in the help + multichoices = ' [%s]' % ','.join([self.fieldmap[i][DISPLAY] for i in self.columns]) + if self.default_sort: + default = '%s:%s' % (self.fieldmap[self.default_sort[0]][DISPLAY], self.default_sort[1]) + elif VALUES in self.fieldmap[e]: + multichoices = ' [%s]' % ','.join(self.fieldmap[e][VALUES]) + parser.add_argument('--%s' % e, + dest=e, + metavar=e.upper(), + required=False, + default=default, + type=str, + choices=self.fieldmap[e].get(CHOICES, None), + help=self.fieldmap[e][HELP] + multichoices) + return parser + + def send_receive(self, app, parsed_args): + parsed_args = self.validate_parameters(parsed_args) + if parsed_args.fields: + self.arguments.append(FIELDS) + arguments = {k: v for k, v in sorted(vars(parsed_args).items()) if k in self.arguments and + k != COLUMNS and + v != ALL and + v is not False} + if not arguments: + arguments = None + req = app.client_manager.resthandler + response = req._operation(self.operation, + '%s%s' %(self.resource_prefix, self.endpoint), + arguments if self.usebody else None, + None if self.usebody else arguments, + False) + if not response.ok: + raise Exception('Request response is not OK (%s)' % response.reason) + result = response.json() + if 0 != result['code']: + raise Exception(result['description']) + return result + + def validate_parameters(self, args): + if 'starttime' in self.arguments: + args.starttime = HelperBase.convert_timezone_to_utc(args.starttime) + if 'endtime' in self.arguments: + args.endtime = HelperBase.convert_timezone_to_utc(args.endtime) + args.fields = ALL + if hasattr(args, COLUMNS): + args.columns = list(set(j for i in args.columns for j in i.split(','))) + if args.columns: + args.fields = ','.join([self.get_key_by_value(k) for k in sorted(args.columns)]) + for a in self.arguments: + argval = getattr(args, a) + if isinstance(argval, str): + for p in argval.split(','): + if argval != ALL and VALUES in self.fieldmap[a] and p not in self.fieldmap[a][VALUES]: + raise Exception('%s is not supported by %s argument' % (p, a)) + return args + + @staticmethod + def validate_datetime(dt): + if dt == ALL: + return dt + formats = ['%Y-%m-%dT%H:%M:%S.%fZ', + '%Y-%m-%dT%H:%M:%SZ', + '%Y-%m-%dT%H:%MZ', + '%Y-%m-%dZ'] + for f in formats: + try: + retval = dt + if 'Z' != retval[-1]: + retval += 'Z' + retval = datetime.strptime(retval, f).__str__() + retval = '%s.000' % retval if len(retval) <= 19 else retval[:-3] + return retval.replace(' ', 'T') + 'Z' + except ValueError: + pass + raise Exception('Datetime format (%s) is not supported' % dt) + + @staticmethod + def convert_utc_to_timezone(timestr): + timestr = timestr.replace('Z', '') + # max resolution for strptime is microsec + if len(timestr) > 26: + timestr = timestr[:26] + from_zone = tz.tzutc() + to_zone = tz.tzlocal() + utc = datetime.strptime(HelperBase.validate_datetime(timestr + 'Z'), '%Y-%m-%dT%H:%M:%S.%fZ') + utc = utc.replace(tzinfo=from_zone) + ret = str(utc.astimezone(to_zone)) + return ret[:23] if '.' in ret else ret[:19] + '.000' + + @staticmethod + def convert_timezone_to_utc(timestr): + if timestr[-1] == 'Z' or timestr == ALL: + # we assume that UTC will always have a Z character at the end + return timestr + formats = ['%Y-%m-%dT%H:%M:%S.%f', + '%Y-%m-%dT%H:%M:%S', + '%Y-%m-%dT%H:%M', + '%Y-%m-%d'] + origstr = timestr + timestr = timestr.replace(' ', 'T') + if len(timestr) > 26: + timestr = timestr[:26] + from_zone = tz.tzlocal() + to_zone = tz.tzutc() + for f in formats: + try: + localtime = datetime.strptime(timestr, f) + localtime = localtime.replace(tzinfo=from_zone) + ret = str(localtime.astimezone(to_zone)) + retval = ret[:23] if '.' in ret else ret[:19] + '.000' + return retval.replace(' ', 'T') + 'Z' + except ValueError: + pass + raise Exception('Datetime format (%s) is not supported' % origstr) + + def filter_columns(self, args): + if getattr(args, DETAILED, False) is True: + self.columns.extend(self.detailed) + if ALL != args.fields: + for i in range(len(self.columns) - 1, -1, -1): + if self.columns[i] not in args.fields: + del self.columns[i] + return [self.fieldmap[f][DISPLAY] for f in self.columns] + + def get_key_by_value(self, val): + for k, v in self.fieldmap.items(): + if DISPLAY in v and val == v[DISPLAY]: + return k + raise Exception('No column named %s' % val) + + def get_sorted_keys(self, parsed_args, data): + keylist = data.keys() + if hasattr(parsed_args, SORT): + sortexp = parsed_args.sort + if sortexp != ALL: + # The next one generates two lists, one with the field names (to be sorted), + # and another with the directions. True if reversed, false otherwise + # also if no direction is added for a field, then it adds an ':asc' by default. + skeys, sdir = zip(*[(self.get_key_by_value(x[0]), False if 'asc' in x[1].lower() else True) + for x in (('%s:asc' % x).split(":") for x in reversed(sortexp.split(',')))]) + for k, d in zip(skeys, sdir): + keylist.sort(key=lambda x: data[x][k], reverse=d) + return keylist + + @staticmethod + def construct_message(text, result): + p = re.compile('\#\#(\w+)') + while True: + m = p.search(text) + if not m: + break + text = p.sub(result[DATA][m.group(1)], text, 1) + return '%s\n' % text + + +class ListerHelper(Lister, HelperBase): + """Helper class for Lister""" + def __init__(self, app, app_args, cmd_name=None): + Lister.__init__(self, app, app_args, cmd_name) + HelperBase.__init__(self) + + def get_parser(self, prog_name): + parser = super(ListerHelper, self).get_parser(prog_name) + return self.get_parser_with_arguments(parser) + + def take_action(self, parsed_args): + try: + result = self.send_receive(self.app, parsed_args) + header = self.filter_columns(parsed_args) + data = [] + for k in self.get_sorted_keys(parsed_args, result[DATA]): + row = [HelperBase.convert_utc_to_timezone(result[DATA][k][i]) + if not getattr(parsed_args, UTC, False) and i == TIME + else result[DATA][k][i] for i in self.columns] + data.append(row) + if self.message: + self.app.stdout.write(self.message + '\n') + return header, data + except Exception as exp: + self.app.stderr.write('Failed with error:\n%s\n' % str(exp)) + sys.exit(1) + + +class ShowOneHelper(ShowOne, HelperBase): + """Helper class for ShowOne""" + def __init__(self, app, app_args, cmd_name=None): + ShowOne.__init__(self, app, app_args, cmd_name) + HelperBase.__init__(self) + + def get_parser(self, prog_name): + parser = super(ShowOneHelper, self).get_parser(prog_name) + return self.get_parser_with_arguments(parser) + + def take_action(self, parsed_args): + try: + result = self.send_receive(self.app, parsed_args) + header = self.filter_columns(parsed_args) + sorted_keys = self.get_sorted_keys(parsed_args, result[DATA]) + if self.message: + self.app.stdout.write(self.message + '\n') + for k in sorted_keys: + data = [HelperBase.convert_utc_to_timezone(result[DATA][k][i]) + if not getattr(parsed_args, UTC, False) and i == TIME + else result[DATA][k][i] for i in self.columns] + if k != sorted_keys[-1]: + self.formatter.emit_one(header, data, self.app.stdout, parsed_args) + self.app.stdout.write('\n') + return header, data + except Exception as exp: + self.app.stderr.write('Failed with error:\n%s\n' % str(exp)) + sys.exit(1) + + +class CommandHelper(Command, HelperBase): + """Helper class for Command""" + def __init__(self, app, app_args, cmd_name=None): + Command.__init__(self, app, app_args, cmd_name) + HelperBase.__init__(self) + + def get_parser(self, prog_name): + parser = super(CommandHelper, self).get_parser(prog_name) + return self.get_parser_with_arguments(parser) + + def take_action(self, parsed_args): + try: + result = self.send_receive(self.app, parsed_args) + if self.message: + self.app.stdout.write(HelperBase.construct_message(self.message, result)) + except Exception as exp: + self.app.stderr.write('Failed with error:\n%s\n' % str(exp)) + sys.exit(1) diff --git a/src/hostcli/main.py b/src/hostcli/main.py new file mode 100644 index 0000000..ba5bbd8 --- /dev/null +++ b/src/hostcli/main.py @@ -0,0 +1,121 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import sys +import time + +from cliff.commandmanager import CommandManager + +from keystoneauth1.exceptions.http import BadGateway + +from osc_lib import shell +from osc_lib import utils +from osc_lib import clientmanager +from osc_lib.api import auth +from osc_lib.cli import client_config as cloud_config + +from openstackclient.i18n import _ + +from hostcli import resthandler + + +class HOSTCLI(shell.OpenStackShell): + LOG = logging.getLogger(__name__) + def __init__(self): + super(HOSTCLI, self).__init__( + description='HOSTCLI', + version='0.1', + command_manager=CommandManager('hostcli.commands') + ) + self.failure_count = 30 + + def build_option_parser(self, description, version): + parser = super(HOSTCLI, self).build_option_parser( + description, + version) + parser = auth.build_auth_plugins_option_parser(parser) + #HACK: Add the api version so that we wont use version 2 + #This part comes from openstack module so it cannot be imported + parser.add_argument('--os-identity-api-version', + metavar='', + default=utils.env('OS_IDENTITY_API_VERSION'), + help=_('Identity API version, default=%s ' + '(Env: OS_IDENTITY_API_VERSION)') % 3, + ) + + return parser + + def initialize_app(self, argv): + self.LOG.debug('initialize_app') + super(HOSTCLI, self).initialize_app(argv) + try: + self.cloud_config = cloud_config.OSC_Config( + override_defaults={ + 'interface': None, + 'auth_type': self._auth_type, + }, + pw_func=shell.prompt_for_password, + ) + except (IOError, OSError): + self.log.critical("Could not read clouds.yaml configuration file") + self.print_help_if_requested() + raise + if not self.options.debug: + self.options.debug = None + + setattr(clientmanager.ClientManager, + resthandler.API_NAME, + clientmanager.ClientCache(getattr(resthandler, 'make_instance'))) + self.client_manager = clientmanager.ClientManager( + cli_options=self.cloud, + api_version=self.api_version, + pw_func=shell.prompt_for_password, + ) + + def _final_defaults(self): + + super(HOSTCLI, self)._final_defaults() + # Set the default plugin to token_endpoint if url and token are given + if self.options.url and self.options.token: + # Use service token authentication + self._auth_type = 'token_endpoint' + else: + self._auth_type = 'password' + + + def prepare_to_run_command(self, cmd): + self.LOG.debug('prepare_to_run_command %s', cmd.__class__.__name__) + error = Exception() + for count in range(0, self.failure_count): + try: + return super(HOSTCLI, self).prepare_to_run_command(cmd) + except BadGateway as error: + self.LOG.debug('Got BadGateway %s, counter %d', str(error), count) + time.sleep(2) + raise error + + def clean_up(self, cmd, result, err): + self.LOG.debug('clean_up %s', cmd.__class__.__name__) + if err: + self.LOG.debug('got an error: %s', err) + +def main(argv=sys.argv[1:]): + hostcli = HOSTCLI() + return hostcli.run(argv) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/src/hostcli/resthandler.py b/src/hostcli/resthandler.py new file mode 100644 index 0000000..0f1d16d --- /dev/null +++ b/src/hostcli/resthandler.py @@ -0,0 +1,132 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import requests +import os + +API_NAME = 'resthandler' +LOG = logging.getLogger(__name__) + +def make_instance(instance): + return RestRequest(instance) + +class RestRequest(object): + """ RestRequest object + This module can be used in the context of hostcli rest implementations. + Example usage is: + def take_action(self, parsed_args): + req = self.app.client_manager.resthandler + ret = req.get("has/v1/cluster", decode_json=True) + status = ret['data'] + columns = ('admin-state', + 'running-state', + 'role' + ) + data = (status['admin-state'], + status['running-state'], + status['role'] + ) + return (columns, data) + Why: + This module will fill the needed information for authentication. + The authentication will be based on keystone. + Notes: + The object will fill the prefix to the request. So it's not mandatory + to write it. This information will be populated from the endpoint of rest frame. + """ + def __init__(self, app_instance): + self.instance = app_instance + if self.instance._auth_required: + self.token = self.instance.auth_ref.auth_token + self.auth_ref = self.instance.auth_ref + self.url = self.auth_ref.service_catalog.url_for(service_type="restfulapi", + service_name="restfulframework", + interface=self.instance.interface) + else: + if 'OS_REST_URL' in os.environ: + self.url = os.environ['OS_REST_URL'] + else: + raise Exception("OS_REST_URL environment variable missing") + + def get(self, url, data=None, params=None, decode_json=True): + return self._operation("get", url, data=data, params=params, decode_json=decode_json) + + def post(self, url, data=None, params=None, decode_json=True): + return self._operation("post", url, data=data, params=params, decode_json=decode_json) + + def put(self, url, data=None, params=None, decode_json=True): + return self._operation("put", url, data=data, params=params, decode_json=decode_json) + + def patch(self, url, data=None, params=None, decode_json=True): + return self._operation("patch", url, data=data, params=params, decode_json=decode_json) + + def delete(self, url, data=None, params=None, decode_json=True): + return self._operation("delete", url, data=data, params=params, decode_json=decode_json) + + def _operation(self, oper, url, data=None, params=None, decode_json=True): + + operation = getattr(requests, oper, None) + + if not operation: + raise NameError("Operation %s not found" % oper) + + if not url.startswith("http"): + url = self.url + '/' + url + + LOG.debug("Working with url %s" % url) + + # Disable request debug logs + logging.getLogger("requests").setLevel(logging.WARNING) + + # HACK:Check if the authentication will expire and if so then renew it + if self.instance._auth_required and self.auth_ref.will_expire_soon(): + LOG.debug("Session will expire soon... Renewing token") + self.instance._auth_setup_completed = False + self.instance._auth_ref = None + self.token = self.instance.auth_ref.auth_token + else: + LOG.debug("Session is solid. Using existing token.") + + # Add security headers + arguments = {} + headers = {'User-Agent': 'HostCli'} + + if self.instance._auth_required: + headers.update({'X-Auth-Token': self.token}) + + if data: + if isinstance(data, dict): + arguments["json"] = data + headers["Content-Type"] = "application/json" + else: + arguments["data"] = data + headers["Content-Type"] = "text/plain" + + arguments["headers"] = headers + + if params: + arguments["params"] = params + + ret = operation(url, **arguments) + + if decode_json: + ret.raise_for_status() + try: + return ret.json() + except ValueError: + return {} + else: + return ret diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..af972d1 --- /dev/null +++ b/src/setup.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# Copyright 2019 Nokia +# +# 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. +# + + +PROJECT = 'hostcli' + +VERSION = '0.1' + +from setuptools import setup, find_packages + +setup( + name=PROJECT, + version=VERSION, + description='HOST CLI', + author='Janne Suominen', + author_email='janne.suominen@nokia.com', + platforms=['Any'], + scripts=[], + provides=[], + install_requires=['cliff', 'requests', 'keystoneauth1', 'osc_lib'], + packages=find_packages(), + include_package_data=True, + entry_points={ + 'console_scripts': [ + 'hostcli = hostcli.main:main' + ], + }, + zip_safe=False, +) -- 2.16.6