From 5c3a1641cc267fdeef9e43adb3bd3b5c735714bf Mon Sep 17 00:00:00 2001 From: Fabio Verboso Date: Mon, 19 Feb 2018 18:01:46 +0100 Subject: [PATCH] Cloud Service Cloud Services API implemented on the client Change-Id: Ia129cfb471f78fa02c66db77ebb18fbb8449bed4 --- iotronicclient/v1/client.py | 5 + iotronicclient/v1/exposed_service.py | 106 ++++++++++++ iotronicclient/v1/exposed_service_shell.py | 132 ++++++++++++++ iotronicclient/v1/resource_fields.py | 47 +++++ iotronicclient/v1/service.py | 97 +++++++++++ iotronicclient/v1/service_shell.py | 191 +++++++++++++++++++++ iotronicclient/v1/shell.py | 5 + tox.ini | 44 +++-- 8 files changed, 604 insertions(+), 23 deletions(-) create mode 100644 iotronicclient/v1/exposed_service.py create mode 100644 iotronicclient/v1/exposed_service_shell.py create mode 100644 iotronicclient/v1/service.py create mode 100644 iotronicclient/v1/service_shell.py diff --git a/iotronicclient/v1/client.py b/iotronicclient/v1/client.py index dc4b953..b5f7303 100644 --- a/iotronicclient/v1/client.py +++ b/iotronicclient/v1/client.py @@ -19,8 +19,10 @@ from iotronicclient.common.http import DEFAULT_VER from iotronicclient.common.i18n import _ from iotronicclient import exc from iotronicclient.v1 import board +from iotronicclient.v1 import exposed_service from iotronicclient.v1 import plugin from iotronicclient.v1 import plugin_injection +from iotronicclient.v1 import service class Client(object): @@ -61,3 +63,6 @@ class Client(object): self.plugin = plugin.PluginManager(self.http_client) self.plugin_injection = plugin_injection.InjectionPluginManager( self.http_client) + self.service = service.ServiceManager(self.http_client) + self.exposed_service = exposed_service.ExposedServiceManager( + self.http_client) diff --git a/iotronicclient/v1/exposed_service.py b/iotronicclient/v1/exposed_service.py new file mode 100644 index 0000000..7f41540 --- /dev/null +++ b/iotronicclient/v1/exposed_service.py @@ -0,0 +1,106 @@ +# 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 + +from iotronicclient.common import base +from iotronicclient.common.i18n import _ +from iotronicclient.common import utils +from iotronicclient import exc + + +LOG = logging.getLogger(__name__) +_DEFAULT_POLL_INTERVAL = 2 + + +class ExposedService(base.Resource): + def __repr__(self): + return "" % self._info + + +class ExposedServiceManager(base.Manager): + resource_class = ExposedService + _resource_name = 'boards' + + def services_on_board(self, board_ident, marker=None, limit=None, + detail=False, sort_key=None, sort_dir=None, + fields=None): + """Retrieve the list of services on the board. + + :param board_ident: the UUID or name of the board. + + :param marker: Optional, the UUID of a board, eg the last + board from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of boards to return. + 2) limit == 0, return the entire list of boards. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Iotronic API + (see Iotronic's api.max_limit option). + + :param detail: Optional, boolean whether to return detailed information + about boards. + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. Can not be used + when 'detail' is set. + + :returns: A list of services injected on a board. + + """ + if limit is not None: + limit = int(limit) + + if detail and fields: + raise exc.InvalidAttribute(_("Can't fetch a subset of fields " + "with 'detail' set")) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir, + fields) + + path = "%s/services" % board_ident + + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), "exposed") + else: + return self._list_pagination(self._path(path), "exposed", + limit=limit) + + def service_action(self, board_ident, service_ident, action): + if service_ident: + path = "%(board)s/services/%(service)s/action" % \ + {'board': board_ident, + 'service': service_ident} + else: + path = "%(board)s/services/restore" % { + 'board': board_ident} + + body = {"action": action + } + return self._update(path, body, method='POST') + + def restore_services(self, board_ident): + + path = "%(board)s/services/restore" % { + 'board': board_ident} + return self._list(self._path(path), "exposed") diff --git a/iotronicclient/v1/exposed_service_shell.py b/iotronicclient/v1/exposed_service_shell.py new file mode 100644 index 0000000..d665d0c --- /dev/null +++ b/iotronicclient/v1/exposed_service_shell.py @@ -0,0 +1,132 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from iotronicclient.common import cliutils +from iotronicclient.common.i18n import _ +from iotronicclient.v1 import resource_fields as res_fields + + +@cliutils.arg( + 'board', + metavar='', + help="Name or UUID of the board ") +@cliutils.arg( + '--limit', + metavar='', + type=int, + help='Maximum number of services to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Iotronic API Service.') +@cliutils.arg( + '--marker', + metavar='', + help='Service UUID (for example, of the last service in the list from ' + 'a previous request). Returns the list of services after this UUID.') +@cliutils.arg( + '--sort-key', + metavar='', + help='Service field that will be used for sorting.') +@cliutils.arg( + '--sort-dir', + metavar='', + choices=['asc', 'desc'], + help='Sort direction: "asc" (the default) or "desc".') +@cliutils.arg( + '--detail', + dest='detail', + action='store_true', + default=False, + help="Show detailed information about the services.") +@cliutils.arg( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + help="One or more service fields. Only these fields will be fetched from " + "the server. Can not be used when '--detail' is specified.") +def do_services_on_board(cc, args): + """Show information about a the exposed services on a board.""" + fields = res_fields.EXPOSED_SERVICE_RESOURCE_ON_BOARD.fields + field_labels = res_fields.EXPOSED_SERVICE_RESOURCE_ON_BOARD.labels + list = cc.exposed_service.services_on_board(args.board) + if list: + cliutils.print_list(list, fields=fields, + field_labels=field_labels, + sortby_index=None, + json_flag=args.json) + else: + print(_('%s') % 'no services could be found') + + +@cliutils.arg('board', + metavar='', + help="Name or UUID of the board.") +@cliutils.arg('service', + metavar='', + help="Name or UUID of the service.") +def do_enable_service(cc, args): + """Execute an action of the service.""" + + result = cc.exposed_service.service_action(args.board, + args.service, + "ServiceEnable") + print(_('%s') % result) + + +@cliutils.arg('board', + metavar='', + help="Name or UUID of the board.") +@cliutils.arg('service', + metavar='', + help="Name or UUID of the service.") +def do_disable_service(cc, args): + """Execute an action of the service.""" + + result = cc.exposed_service.service_action(args.board, + args.service, + "ServiceDisable") + print(_('%s') % result) + + +@cliutils.arg('board', + metavar='', + help="Name or UUID of the board.") +@cliutils.arg('service', + metavar='', + help="Name or UUID of the service.") +def do_restore_service(cc, args): + """Execute an action of the service.""" + + result = cc.exposed_service.service_action(args.board, + args.service, + "ServiceRestore") + print(_('%s') % result) + + +@cliutils.arg('board', + metavar='', + help="Name or UUID of the board.") +def do_restore_services(cc, args): + """Execute an action of the service.""" + + fields = res_fields.EXPOSED_SERVICE_RESOURCE_ON_BOARD.fields + field_labels = res_fields.EXPOSED_SERVICE_RESOURCE_ON_BOARD.labels + list = cc.exposed_service.restore_services(args.board) + if list: + cliutils.print_list(list, fields=fields, + field_labels=field_labels, + sortby_index=None, + json_flag=args.json) + else: + print(_('%s') % 'no services could be found') diff --git a/iotronicclient/v1/resource_fields.py b/iotronicclient/v1/resource_fields.py index 93fe409..5c543ff 100644 --- a/iotronicclient/v1/resource_fields.py +++ b/iotronicclient/v1/resource_fields.py @@ -50,8 +50,14 @@ class Resource(object): 'onboot': 'On Boot', 'board_uuid': 'Board uuid', 'plugin_uuid': 'Plugin uuid', + 'service_uuid': 'Service uuid', 'plugin': 'Plugin', 'parameters': 'Parameters', + 'service': 'Service', + 'port': 'Port', + 'public_port': 'Public Port', + 'pid': 'Pid', + 'protocol': 'Protocol', # # 'address': 'Address', @@ -175,6 +181,8 @@ PLUGIN_DETAILED_RESOURCE = Resource( 'code', 'public', 'callable', + 'created_at', + 'updated_at', 'extra' ], @@ -206,3 +214,42 @@ PLUGIN_INJECT_RESOURCE = Resource( 'created_at', 'updated_at', ]) + +# Service +SERVICE_DETAILED_RESOURCE = Resource( + ['uuid', + 'name', + 'port', + 'project', + 'protocol', + 'extra', + 'created_at', + 'updated_at', + ], + sort_excluded=[ + 'extra', + ]) + + +SERVICE_RESOURCE = Resource( + ['uuid', + 'name', + 'port', + 'protocol' + ]) + +EXPOSED_SERVICE_RESOURCE_ON_BOARD = Resource( + [ + 'service', + 'public_port', + 'created_at', + 'updated_at', + ]) + +EXPOSED_SERVICE_RESOURCE = Resource( + ['board_uuid', + 'service_uuid', + 'public_port', + 'created_at', + 'updated_at', + ]) diff --git a/iotronicclient/v1/service.py b/iotronicclient/v1/service.py new file mode 100644 index 0000000..bda3f83 --- /dev/null +++ b/iotronicclient/v1/service.py @@ -0,0 +1,97 @@ +# 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 + +from iotronicclient.common import base +from iotronicclient.common.i18n import _ +from iotronicclient.common import utils +from iotronicclient import exc + +LOG = logging.getLogger(__name__) +_DEFAULT_POLL_INTERVAL = 2 + + +class Service(base.Resource): + def __repr__(self): + return "" % self._info + + +class ServiceManager(base.CreateManager): + resource_class = Service + _creation_attributes = ['name', 'port', 'protocol', 'extra'] + + _resource_name = 'services' + + def list(self, marker=None, limit=None, + detail=False, sort_key=None, sort_dir=None, fields=None): + """Retrieve a list of services. + + :param marker: Optional, the UUID of a service, eg the last + service from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of services to return. + 2) limit == 0, return the entire list of services. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Iotronic API + (see Iotronic's api.max_limit option). + + :param detail: Optional, boolean whether to return detailed information + about services. + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. Can not be used + when 'detail' is set. + + :returns: A list of services. + + """ + if limit is not None: + limit = int(limit) + + if detail and fields: + raise exc.InvalidAttribute(_("Can't fetch a subset of fields " + "with 'detail' set")) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir, + fields) + path = '' + + if detail: + path += 'detail' + + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), "services") + else: + return self._list_pagination(self._path(path), "services", + limit=limit) + + def get(self, service_id, fields=None): + return self._get(resource_id=service_id, fields=fields) + + def delete(self, service_id): + return self._delete(resource_id=service_id) + + def update(self, service_id, patch, http_method='PATCH'): + return self._update(resource_id=service_id, patch=patch, + method=http_method) diff --git a/iotronicclient/v1/service_shell.py b/iotronicclient/v1/service_shell.py new file mode 100644 index 0000000..ece5393 --- /dev/null +++ b/iotronicclient/v1/service_shell.py @@ -0,0 +1,191 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from iotronicclient.common.apiclient import exceptions +from iotronicclient.common import cliutils +from iotronicclient.common.i18n import _ +from iotronicclient.common import utils +from iotronicclient.v1 import resource_fields as res_fields + + +def _print_service_show(service, fields=None, json=False): + if fields is None: + fields = res_fields.SERVICE_DETAILED_RESOURCE.fields + + data = dict( + [(f, getattr(service, f, '')) for f in fields]) + cliutils.print_dict(data, wrap=72, json_flag=json) + + +@cliutils.arg( + 'service', + metavar='', + help="Name or UUID of the service ") +@cliutils.arg( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + help="One or more service fields. Only these fields will be fetched from " + "the server.") +def do_service_show(cc, args): + """Show detailed information about a service.""" + fields = args.fields[0] if args.fields else None + utils.check_empty_arg(args.service, '') + utils.check_for_invalid_fields( + fields, res_fields.SERVICE_DETAILED_RESOURCE.fields) + service = cc.service.get(args.service, fields=fields) + _print_service_show(service, fields=fields, json=args.json) + + +@cliutils.arg( + '--limit', + metavar='', + type=int, + help='Maximum number of services to return per request, ' + '0 for no limit. Default is the maximum number used ' + 'by the Iotronic API Service.') +@cliutils.arg( + '--marker', + metavar='', + help='Service UUID (for example, of the last service in the list from ' + 'a previous request). Returns the list of services after this UUID.') +@cliutils.arg( + '--sort-key', + metavar='', + help='Service field that will be used for sorting.') +@cliutils.arg( + '--sort-dir', + metavar='', + choices=['asc', 'desc'], + help='Sort direction: "asc" (the default) or "desc".') +@cliutils.arg( + '--detail', + dest='detail', + action='store_true', + default=False, + help="Show detailed information about the services.") +@cliutils.arg( + '--fields', + nargs='+', + dest='fields', + metavar='', + action='append', + default=[], + help="One or more service fields. Only these fields will be fetched from " + "the server. Can not be used when '--detail' is specified.") +def do_service_list(cc, args): + """List the services which are registered with the Iotronic service.""" + params = {} + + if args.detail: + fields = res_fields.SERVICE_DETAILED_RESOURCE.fields + field_labels = res_fields.SERVICE_DETAILED_RESOURCE.labels + elif args.fields: + utils.check_for_invalid_fields( + args.fields[0], res_fields.SERVICE_DETAILED_RESOURCE.fields) + resource = res_fields.Resource(args.fields[0]) + fields = resource.fields + field_labels = resource.labels + else: + fields = res_fields.SERVICE_RESOURCE.fields + field_labels = res_fields.SERVICE_RESOURCE.labels + + sort_fields = res_fields.SERVICE_DETAILED_RESOURCE.sort_fields + sort_field_labels = res_fields.SERVICE_DETAILED_RESOURCE.sort_labels + + params.update(utils.common_params_for_list(args, + sort_fields, + sort_field_labels)) + + services = cc.service.list(**params) + cliutils.print_list(services, fields, + field_labels=field_labels, + sortby_index=None, + json_flag=args.json) + + +@cliutils.arg( + 'name', + metavar='', + help="Name or UUID of the service ") +@cliutils.arg( + 'port', + metavar='', + help="Port of the service") +@cliutils.arg( + 'protocol', + metavar='', + help="Protocol of the service TCP|UDP|ANY") +def do_service_create(cc, args): + """Register a new service with the Iotronic service.""" + + field_list = ['name', 'port', 'protocol', 'extra'] + + fields = dict((k, v) for (k, v) in vars(args).items() + if k in field_list and not (v is None)) + + fields = utils.args_array_to_dict(fields, 'extra') + + if fields['protocol'] not in ['TCP', 'UDP', 'ANY']: + print("protocol must be TCP | UDP | ANY") + return 1 + + service = cc.service.create(**fields) + + data = dict([(f, getattr(service, f, '')) for f in + res_fields.SERVICE_DETAILED_RESOURCE.fields]) + + cliutils.print_dict(data, wrap=72, json_flag=args.json) + + +@cliutils.arg('service', + metavar='', + nargs='+', + help="Name or UUID of the service.") +def do_service_delete(cc, args): + """Unregister service(s) from the Iotronic service. + + Returns errors for any services that could not be unregistered. + """ + + failures = [] + for n in args.service: + try: + cc.service.delete(n) + print(_('Deleted service %s') % n) + except exceptions.ClientException as e: + failures.append( + _("Failed to delete service %(service)s: %(error)s") + % {'service': n, 'error': e}) + if failures: + raise exceptions.ClientException("\n".join(failures)) + + +@cliutils.arg('service', metavar='', + help="Name or UUID of the service.") +@cliutils.arg( + 'attributes', + metavar='', + nargs='+', + action='append', + default=[], + help="Values to be changed.") +def do_service_update(cc, args): + """Update information about a registered service.""" + + patch = {k: v for k, v in (x.split('=') for x in args.attributes[0])} + + service = cc.service.update(args.service, patch) + _print_service_show(service, json=args.json) diff --git a/iotronicclient/v1/shell.py b/iotronicclient/v1/shell.py index 6589eef..446d804 100644 --- a/iotronicclient/v1/shell.py +++ b/iotronicclient/v1/shell.py @@ -13,13 +13,18 @@ from iotronicclient.common import utils from iotronicclient.v1 import board_shell +from iotronicclient.v1 import exposed_service_shell from iotronicclient.v1 import plugin_injection_shell from iotronicclient.v1 import plugin_shell +from iotronicclient.v1 import service_shell + COMMAND_MODULES = [ board_shell, plugin_shell, plugin_injection_shell, + service_shell, + exposed_service_shell, ] diff --git a/tox.ini b/tox.ini index db03f6c..82da489 100644 --- a/tox.ini +++ b/tox.ini @@ -1,41 +1,39 @@ [tox] -minversion = 2.0 -envlist = py35,py34,py27,pypy,pep8 +minversion = 2.3.1 +envlist = py27,pep8 skipsdist = True [testenv] +setenv = + VIRTUAL_ENV={envdir} + PYTHONWARNINGS=default::DeprecationWarning + LANGUAGE=en_US + LC_ALL=en_US.utf-8 +whitelist_externals = bash + find + rm usedevelop = True install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} -setenv = - VIRTUAL_ENV={envdir} - PYTHONWARNINGS=default::DeprecationWarning deps = -r{toxinidir}/test-requirements.txt commands = - find . -type f -name "*.pyc" -delete + find . -type f -name "*.pyc" -delete [testenv:pep8] +basepython = python2.7 commands = flake8 {posargs} -[testenv:venv] -commands = {posargs} +[testenv:py27] +basepython = python2.7 -[testenv:cover] -commands = python setup.py test --coverage --testr-args='{posargs}' - -[testenv:docs] -commands = python setup.py build_sphinx - -[testenv:releasenotes] -commands = - sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html - -[testenv:debug] -commands = oslo_debug_helper {posargs} [flake8] +# TODO(dmllr): Analyze or fix the warnings blacklisted below +# E711 comparison to None should be 'if cond is not None:' +# E712 comparison to True should be 'if cond is True:' or 'if cond:' +# H404 multi line docstring should start with a summary +# H405 multi line docstring summary not separated with an empty line # E123, E125 skipped as they are invalid PEP-8. - show-source = True -ignore = E123,E125 builtins = _ -exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build +ignore = E711,E712,H404,H405,E123,E125,E901,H301 +exclude = .venv,.git,.tox,dist,doc,etc,*lib/python*,*egg,build