From 94c5223f022eed38282541a7ce7a5a5da94593ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Tr=C4=99bski?= Date: Fri, 28 Apr 2017 22:21:01 +0200 Subject: [PATCH] Integrate client with osc-lib osc-lib library is foundation on which a CLI client for openstack can be built. It is meant to facilitate several aspects, that were previously hard-coded in client: * keystone communication handling * supporting multiple authentication methods (not only password) * common authentication parameters (i.e. environmental OS_*) * communicating over http with service endpoint * interactive CLI mode Thanks to those items, it was possible not only to drop nearly 3k lines of code and replace them with osc-lib but also increase reliabity of the client in terms of new openstack releases. Also it allowed to greatly simpify existing set of unit-tests. They are now testing only actual logic instead of mocking entire process of calling shell (i.e. MonascaShell.run(args)) or mocking HTTP communication. Both items are handled by osc-lib thus not they are not subject of monascaclient unit tests layers. Note: This change is partial integration with osc-lib and its main purpose is to move the responsibility of: * keystone communication * rest-ful communication with service endpoint to underlying library thus allowing client to implement only necessary functionality and not supporting boilerplate code, mentioned above. Story: 2000995 Task: 4172 Change-Id: I1712a24739438e2d8331a495f18f357749a633c5 --- .gitignore | 2 +- monascaclient/__init__.py | 76 ++- monascaclient/apiclient/base.py | 496 ----------------- monascaclient/apiclient/exceptions.py | 438 --------------- monascaclient/client.py | 66 ++- monascaclient/common/http.py | 323 ----------- monascaclient/common/monasca_manager.py | 33 +- monascaclient/common/utils.py | 117 +--- monascaclient/exc.py | 233 -------- monascaclient/ksclient.py | 105 ---- monascaclient/{apiclient => osc}/__init__.py | 0 monascaclient/osc/migration.py | 169 ++++++ monascaclient/shell.py | 510 ++---------------- monascaclient/tests/fakes.py | 46 -- monascaclient/tests/test_shell.py | 453 ++-------------- monascaclient/tests/v2_0/__init__.py | 0 monascaclient/tests/v2_0/shell/__init__.py | 0 .../v2_0/shell/test_alarm_definitions.py | 134 +++++ .../tests/v2_0/shell/test_metrics.py | 85 +++ .../v2_0/shell/test_notification_types.py | 52 ++ .../tests/v2_0/shell/test_notifications.py | 160 ++++++ monascaclient/v2_0/alarm_definitions.py | 43 +- monascaclient/v2_0/alarms.py | 50 +- monascaclient/v2_0/client.py | 44 +- monascaclient/v2_0/metrics.py | 29 +- monascaclient/v2_0/notifications.py | 56 +- monascaclient/v2_0/notificationtypes.py | 9 +- monascaclient/v2_0/shell.py | 202 +++---- monascaclient/version.py | 20 + requirements.txt | 7 +- test-requirements.txt | 4 +- tox.ini | 18 +- 32 files changed, 1047 insertions(+), 2933 deletions(-) delete mode 100644 monascaclient/apiclient/base.py delete mode 100644 monascaclient/apiclient/exceptions.py delete mode 100644 monascaclient/common/http.py delete mode 100644 monascaclient/exc.py delete mode 100644 monascaclient/ksclient.py rename monascaclient/{apiclient => osc}/__init__.py (100%) create mode 100644 monascaclient/osc/migration.py delete mode 100644 monascaclient/tests/fakes.py create mode 100644 monascaclient/tests/v2_0/__init__.py create mode 100644 monascaclient/tests/v2_0/shell/__init__.py create mode 100644 monascaclient/tests/v2_0/shell/test_alarm_definitions.py create mode 100644 monascaclient/tests/v2_0/shell/test_metrics.py create mode 100644 monascaclient/tests/v2_0/shell/test_notification_types.py create mode 100644 monascaclient/tests/v2_0/shell/test_notifications.py create mode 100644 monascaclient/version.py diff --git a/.gitignore b/.gitignore index d7950c5..2c258e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.coverage +.coverage* .venv cover *.pyc diff --git a/monascaclient/__init__.py b/monascaclient/__init__.py index ce1a14f..ba41a8e 100644 --- a/monascaclient/__init__.py +++ b/monascaclient/__init__.py @@ -1,18 +1,70 @@ -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +# Copyright 2017 FUJITSU LIMITED # -# 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 +# 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 +# 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. +# 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 pbr.version +""" -__version__ = pbr.version.VersionInfo('python-monascaclient').version_string() +Patches method that transforms error responses. +That is required to handle different format monasca follows. + +""" + +from keystoneauth1 import exceptions as exc +from keystoneauth1.exceptions import http + + +def mon_exc_from_response(response, method, url): + req_id = response.headers.get('x-openstack-request-id') + + kwargs = { + 'http_status': response.status_code, + 'response': response, + 'method': method, + 'url': url, + 'request_id': req_id, + } + if 'retry-after' in response.headers: + kwargs['retry_after'] = response.headers['retry-after'] + + content_type = response.headers.get('Content-Type', '') + if content_type.startswith('application/json'): + try: + body = response.json() + except ValueError: + pass + else: + if isinstance(body, dict): + if isinstance(body.get('error'), dict): + error = body['error'] + kwargs['message'] = error.get('message') + kwargs['details'] = error.get('details') + elif {'description', 'title'} <= set(body): + # monasca-api error response structure + kwargs['message'] = body.get('title') + kwargs['details'] = body.get('description') + elif content_type.startswith('text/'): + kwargs['details'] = response.text + + try: + cls = http._code_map[response.status_code] + except KeyError: + if 500 <= response.status_code < 600: + cls = exc.HttpServerError + elif 400 <= response.status_code < 500: + cls = exc.HTTPClientError + else: + cls = exc.HttpError + return cls(**kwargs) + + +exc.from_response = mon_exc_from_response diff --git a/monascaclient/apiclient/base.py b/monascaclient/apiclient/base.py deleted file mode 100644 index ca7de87..0000000 --- a/monascaclient/apiclient/base.py +++ /dev/null @@ -1,496 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack Foundation -# Copyright 2012 Grid Dynamics -# Copyright 2013 OpenStack Foundation -# (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Base utilities to build API operation managers and objects on top of. -""" - -# E1102: %s is not callable -# pylint: disable=E1102 - -import abc -import copy - -from oslo_utils import strutils -import six -from six.moves.urllib import parse - -from monascaclient.apiclient import exceptions - - -def getid(obj): - """Return id if argument is a Resource. - - Abstracts the common pattern of allowing both an object or an object's ID - (UUID) as a parameter when dealing with relationships. - """ - try: - if obj.uuid: - return obj.uuid - except AttributeError: - pass - try: - return obj.id - except AttributeError: - return obj - - -# TODO(aababilov): call run_hooks() in HookableMixin's child classes -class HookableMixin(object): - """Mixin so classes can register and run hooks.""" - _hooks_map = {} - - @classmethod - def add_hook(cls, hook_type, hook_func): - """Add a new hook of specified type. - - :param cls: class that registers hooks - :param hook_type: hook type, e.g., '__pre_parse_args__' - :param hook_func: hook function - """ - if hook_type not in cls._hooks_map: - cls._hooks_map[hook_type] = [] - - cls._hooks_map[hook_type].append(hook_func) - - @classmethod - def run_hooks(cls, hook_type, *args, **kwargs): - """Run all hooks of specified type. - - :param cls: class that registers hooks - :param hook_type: hook type, e.g., '__pre_parse_args__' - :param **args: args to be passed to every hook function - :param **kwargs: kwargs to be passed to every hook function - """ - hook_funcs = cls._hooks_map.get(hook_type) or [] - for hook_func in hook_funcs: - hook_func(*args, **kwargs) - - -class BaseManager(HookableMixin): - """Basic manager type providing common operations. - - Managers interact with a particular type of API (servers, flavors, images, - etc.) and provide CRUD operations for them. - """ - resource_class = None - - def __init__(self, client): - """Initializes BaseManager with `client`. - - :param client: instance of BaseClient descendant for HTTP requests - """ - super(BaseManager, self).__init__() - self.client = client - - def _list(self, url, response_key, obj_class=None, json=None): - """List the collection. - - :param url: a partial URL, e.g., '/servers' - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' - :param obj_class: class for constructing the returned objects - (self.resource_class will be used by default) - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - """ - if json: - body = self.client.post(url, json=json).json() - else: - body = self.client.get(url).json() - - if obj_class is None: - obj_class = self.resource_class - - data = body[response_key] - # NOTE(ja): keystone returns values as list as {'values': [ ... ]} - # unlike other services which just return the list... - try: - data = data['values'] - except (KeyError, TypeError): - pass - - return [obj_class(self, res, loaded=True) for res in data if res] - - def _get(self, url, response_key): - """Get an object from collection. - - :param url: a partial URL, e.g., '/servers' - :param response_key: the key to be looked up in response dictionary, - e.g., 'server' - """ - body = self.client.get(url).json() - return self.resource_class(self, body[response_key], loaded=True) - - def _head(self, url): - """Retrieve request headers for an object. - - :param url: a partial URL, e.g., '/servers' - """ - resp = self.client.head(url) - return resp.status_code == 204 - - def _post(self, url, json, response_key, return_raw=False): - """Create an object. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' - :param return_raw: flag to force returning raw JSON instead of - Python object of self.resource_class - """ - body = self.client.post(url, json=json).json() - if return_raw: - return body[response_key] - return self.resource_class(self, body[response_key]) - - def _put(self, url, json=None, response_key=None): - """Update an object with PUT method. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' - """ - resp = self.client.put(url, json=json) - # PUT requests may not return a body - if resp.content: - body = resp.json() - if response_key is not None: - return self.resource_class(self, body[response_key]) - else: - return self.resource_class(self, body) - - def _patch(self, url, json=None, response_key=None): - """Update an object with PATCH method. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' - """ - body = self.client.patch(url, json=json).json() - if response_key is not None: - return self.resource_class(self, body[response_key]) - else: - return self.resource_class(self, body) - - def _delete(self, url): - """Delete an object. - - :param url: a partial URL, e.g., '/servers/my-server' - """ - return self.client.delete(url) - - -@six.add_metaclass(abc.ABCMeta) -class ManagerWithFind(BaseManager): - """Manager with additional `find()`/`findall()` methods.""" - - @abc.abstractmethod - def list(self): - pass - - def find(self, **kwargs): - """Find a single item with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - matches = self.findall(**kwargs) - num_matches = len(matches) - if num_matches == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) - raise exceptions.NotFound(msg) - elif num_matches > 1: - raise exceptions.NoUniqueMatch() - else: - return matches[0] - - def findall(self, **kwargs): - """Find all items with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - found = [] - searches = kwargs.items() - - for obj in self.list(): - try: - if all(getattr(obj, attr) == value - for (attr, value) in searches): - found.append(obj) - except AttributeError: - continue - - return found - - -class CrudManager(BaseManager): - """Base manager class for manipulating entities. - - Children of this class are expected to define a `collection_key` and `key`. - - - `collection_key`: Usually a plural noun by convention (e.g. `entities`); - used to refer collections in both URL's (e.g. `/v3/entities`) and JSON - objects containing a list of member resources (e.g. `{'entities': [{}, - {}, {}]}`). - - `key`: Usually a singular noun by convention (e.g. `entity`); used to - refer to an individual member of the collection. - - """ - collection_key = None - key = None - - def build_url(self, base_url=None, **kwargs): - """Builds a resource URL for the given kwargs. - - Given an example collection where `collection_key = 'entities'` and - `key = 'entity'`, the following URL's could be generated. - - By default, the URL will represent a collection of entities, e.g.:: - - /entities - - If kwargs contains an `entity_id`, then the URL will represent a - specific member, e.g.:: - - /entities/{entity_id} - - :param base_url: if provided, the generated URL will be appended to it - """ - url = base_url if base_url is not None else '' - - url += '/%s' % self.collection_key - - # do we have a specific entity? - entity_id = kwargs.get('%s_id' % self.key) - if entity_id is not None: - url += '/%s' % entity_id - - return url - - def _filter_kwargs(self, kwargs): - """Drop null values and handle ids.""" - for key, ref in six.iteritems(kwargs.copy()): - if ref is None: - kwargs.pop(key) - else: - if isinstance(ref, Resource): - kwargs.pop(key) - kwargs['%s_id' % key] = getid(ref) - return kwargs - - def create(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._post( - self.build_url(**kwargs), - {self.key: kwargs}, - self.key) - - def get(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._get( - self.build_url(**kwargs), - self.key) - - def head(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._head(self.build_url(**kwargs)) - - def list(self, base_url=None, **kwargs): - """List the collection. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - return self._list( - '%(base_url)s%(query)s' % { - 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) - - def put(self, base_url=None, **kwargs): - """Update an element. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - return self._put(self.build_url(base_url=base_url, **kwargs)) - - def update(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - params = kwargs.copy() - params.pop('%s_id' % self.key) - - return self._patch( - self.build_url(**kwargs), - {self.key: params}, - self.key) - - def delete(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - - return self._delete( - self.build_url(**kwargs)) - - def find(self, base_url=None, **kwargs): - """Find a single item with attributes matching ``**kwargs``. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - rl = self._list( - '%(base_url)s%(query)s' % { - 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) - num = len(rl) - - if num == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) - raise exceptions.NotFound(404, msg) - elif num > 1: - raise exceptions.NoUniqueMatch - else: - return rl[0] - - -class Extension(HookableMixin): - """Extension descriptor.""" - - SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') - manager_class = None - - def __init__(self, name, module): - super(Extension, self).__init__() - self.name = name - self.module = module - self._parse_extension_module() - - def _parse_extension_module(self): - self.manager_class = None - for attr_name, attr_value in self.module.__dict__.items(): - if attr_name in self.SUPPORTED_HOOKS: - self.add_hook(attr_name, attr_value) - else: - try: - if issubclass(attr_value, BaseManager): - self.manager_class = attr_value - except TypeError: - pass - - def __repr__(self): - return "" % self.name - - -class Resource(object): - """Base class for OpenStack resources (tenant, user, etc.). - - This is pretty much just a bag for attributes. - """ - - HUMAN_ID = False - NAME_ATTR = 'name' - - def __init__(self, manager, info, loaded=False): - """Populate and bind to a manager. - - :param manager: BaseManager object - :param info: dictionary representing resource attributes - :param loaded: prevent lazy-loading if set to True - """ - self.manager = manager - self._info = info - self._add_details(info) - self._loaded = loaded - - def __repr__(self): - reprkeys = sorted(k - for k in self.__dict__.keys() - if k[0] != '_' and k != 'manager') - info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) - return "<%s %s>" % (self.__class__.__name__, info) - - @property - def human_id(self): - """Human-readable ID which can be used for bash completion.""" - if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: - return strutils.to_slug(getattr(self, self.NAME_ATTR)) - return None - - def _add_details(self, info): - for (k, v) in six.iteritems(info): - try: - setattr(self, k, v) - self._info[k] = v - except AttributeError: - # In this case we already defined the attribute on the class - pass - - def __getattr__(self, k): - if k not in self.__dict__: - # NOTE(bcwaldon): disallow lazy-loading if already loaded once - if not self.is_loaded: - self._get() - return self.__getattr__(k) - - raise AttributeError(k) - else: - return self.__dict__[k] - - def _get(self): - # set _loaded first ... so if we have to bail, we know we tried. - self._loaded = True - if not hasattr(self.manager, 'get'): - return - - new = self.manager.get(self.id) - if new: - self._add_details(new._info) - - def __eq__(self, other): - if not isinstance(other, Resource): - return NotImplemented - # two resources of different types are not equal - if not isinstance(other, self.__class__): - return False - if hasattr(self, 'id') and hasattr(other, 'id'): - return self.id == other.id - return self._info == other._info - - def __ne__(self, other): - return not self.__eq__(other) - - @property - def is_loaded(self): - return self._loaded - - def to_dict(self): - return copy.deepcopy(self._info) diff --git a/monascaclient/apiclient/exceptions.py b/monascaclient/apiclient/exceptions.py deleted file mode 100644 index ce172a9..0000000 --- a/monascaclient/apiclient/exceptions.py +++ /dev/null @@ -1,438 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 Nebula, Inc. -# Copyright 2013 Alessio Ababilov -# Copyright 2013 OpenStack Foundation -# (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Exception definitions. -""" - -import inspect -import sys - -import six - - -class ClientException(Exception): - """The base exception class for all exceptions this library raises.""" - pass - - -class MissingArgs(ClientException): - """Supplied arguments are not sufficient for calling a function.""" - def __init__(self, missing): - self.missing = missing - msg = "Missing argument(s): %s" % ", ".join(missing) - super(MissingArgs, self).__init__(msg) - - -class ValidationError(ClientException): - """Error in validation on API client side.""" - pass - - -class UnsupportedVersion(ClientException): - """User is trying to use an unsupported version of the API.""" - pass - - -class CommandError(ClientException): - """Error in CLI tool.""" - pass - - -class AuthorizationFailure(ClientException): - """Cannot authorize API client.""" - pass - - -class AuthPluginOptionsMissing(AuthorizationFailure): - """Auth plugin misses some options.""" - def __init__(self, opt_names): - super(AuthPluginOptionsMissing, self).__init__( - "Authentication failed. Missing options: %s" % - ", ".join(opt_names)) - self.opt_names = opt_names - - -class AuthSystemNotFound(AuthorizationFailure): - """User has specified a AuthSystem that is not installed.""" - def __init__(self, auth_system): - super(AuthSystemNotFound, self).__init__( - "AuthSystemNotFound: %s" % repr(auth_system)) - self.auth_system = auth_system - - -class NoUniqueMatch(ClientException): - """Multiple entities found instead of one.""" - pass - - -class EndpointException(ClientException): - """Something is rotten in Service Catalog.""" - pass - - -class EndpointNotFound(EndpointException): - """Could not find requested endpoint in Service Catalog.""" - pass - - -class AmbiguousEndpoints(EndpointException): - """Found more than one matching endpoint in Service Catalog.""" - def __init__(self, endpoints=None): - super(AmbiguousEndpoints, self).__init__( - "AmbiguousEndpoints: %s" % repr(endpoints)) - self.endpoints = endpoints - - -class HttpError(ClientException): - """The base exception class for all HTTP exceptions.""" - http_status = 0 - message = "HTTP Error" - - def __init__(self, message=None, details=None, - response=None, request_id=None, - url=None, method=None, http_status=None): - self.http_status = http_status or self.http_status - self.message = message or self.message - self.details = details - self.request_id = request_id - self.response = response - self.url = url - self.method = method - formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) - if request_id: - formatted_string += " (Request-ID: %s)" % request_id - super(HttpError, self).__init__(formatted_string) - - -class HTTPClientError(HttpError): - """Client-side HTTP error. - - Exception for cases in which the client seems to have erred. - """ - message = "HTTP Client Error" - - -class HttpServerError(HttpError): - """Server-side HTTP error. - - Exception for cases in which the server is aware that it has - erred or is incapable of performing the request. - """ - message = "HTTP Server Error" - - -class BadRequest(HTTPClientError): - """HTTP 400 - Bad Request. - - The request cannot be fulfilled due to bad syntax. - """ - http_status = 400 - message = "Bad Request" - - -class Unauthorized(HTTPClientError): - """HTTP 401 - Unauthorized. - - Similar to 403 Forbidden, but specifically for use when authentication - is required and has failed or has not yet been provided. - """ - http_status = 401 - message = "Unauthorized" - - -class PaymentRequired(HTTPClientError): - """HTTP 402 - Payment Required. - - Reserved for future use. - """ - http_status = 402 - message = "Payment Required" - - -class Forbidden(HTTPClientError): - """HTTP 403 - Forbidden. - - The request was a valid request, but the server is refusing to respond - to it. - """ - http_status = 403 - message = "Forbidden" - - -class NotFound(HTTPClientError): - """HTTP 404 - Not Found. - - The requested resource could not be found but may be available again - in the future. - """ - http_status = 404 - message = "Not Found" - - -class MethodNotAllowed(HTTPClientError): - """HTTP 405 - Method Not Allowed. - - A request was made of a resource using a request method not supported - by that resource. - """ - http_status = 405 - message = "Method Not Allowed" - - -class NotAcceptable(HTTPClientError): - """HTTP 406 - Not Acceptable. - - The requested resource is only capable of generating content not - acceptable according to the Accept headers sent in the request. - """ - http_status = 406 - message = "Not Acceptable" - - -class ProxyAuthenticationRequired(HTTPClientError): - """HTTP 407 - Proxy Authentication Required. - - The client must first authenticate itself with the proxy. - """ - http_status = 407 - message = "Proxy Authentication Required" - - -class RequestTimeout(HTTPClientError): - """HTTP 408 - Request Timeout. - - The server timed out waiting for the request. - """ - http_status = 408 - message = "Request Timeout" - - -class Conflict(HTTPClientError): - """HTTP 409 - Conflict. - - Indicates that the request could not be processed because of conflict - in the request, such as an edit conflict. - """ - http_status = 409 - message = "Conflict" - - -class Gone(HTTPClientError): - """HTTP 410 - Gone. - - Indicates that the resource requested is no longer available and will - not be available again. - """ - http_status = 410 - message = "Gone" - - -class LengthRequired(HTTPClientError): - """HTTP 411 - Length Required. - - The request did not specify the length of its content, which is - required by the requested resource. - """ - http_status = 411 - message = "Length Required" - - -class PreconditionFailed(HTTPClientError): - """HTTP 412 - Precondition Failed. - - The server does not meet one of the preconditions that the requester - put on the request. - """ - http_status = 412 - message = "Precondition Failed" - - -class RequestEntityTooLarge(HTTPClientError): - """HTTP 413 - Request Entity Too Large. - - The request is larger than the server is willing or able to process. - """ - http_status = 413 - message = "Request Entity Too Large" - - def __init__(self, *args, **kwargs): - try: - self.retry_after = int(kwargs.pop('retry_after')) - except (KeyError, ValueError): - self.retry_after = 0 - - super(RequestEntityTooLarge, self).__init__(*args, **kwargs) - - -class RequestUriTooLong(HTTPClientError): - """HTTP 414 - Request-URI Too Long. - - The URI provided was too long for the server to process. - """ - http_status = 414 - message = "Request-URI Too Long" - - -class UnsupportedMediaType(HTTPClientError): - """HTTP 415 - Unsupported Media Type. - - The request entity has a media type which the server or resource does - not support. - """ - http_status = 415 - message = "Unsupported Media Type" - - -class RequestedRangeNotSatisfiable(HTTPClientError): - """HTTP 416 - Requested Range Not Satisfiable. - - The client has asked for a portion of the file, but the server cannot - supply that portion. - """ - http_status = 416 - message = "Requested Range Not Satisfiable" - - -class ExpectationFailed(HTTPClientError): - """HTTP 417 - Expectation Failed. - - The server cannot meet the requirements of the Expect request-header field. - """ - http_status = 417 - message = "Expectation Failed" - - -class UnprocessableEntity(HTTPClientError): - """HTTP 422 - Unprocessable Entity. - - The request was well-formed but was unable to be followed due to semantic - errors. - """ - http_status = 422 - message = "Unprocessable Entity" - - -class InternalServerError(HttpServerError): - """HTTP 500 - Internal Server Error. - - A generic error message, given when no more specific message is suitable. - """ - http_status = 500 - message = "Internal Server Error" - - -# NotImplemented is a python keyword. -class HttpNotImplemented(HttpServerError): - """HTTP 501 - Not Implemented. - - The server either does not recognize the request method, or it lacks - the ability to fulfill the request. - """ - http_status = 501 - message = "Not Implemented" - - -class BadGateway(HttpServerError): - """HTTP 502 - Bad Gateway. - - The server was acting as a gateway or proxy and received an invalid - response from the upstream server. - """ - http_status = 502 - message = "Bad Gateway" - - -class ServiceUnavailable(HttpServerError): - """HTTP 503 - Service Unavailable. - - The server is currently unavailable. - """ - http_status = 503 - message = "Service Unavailable" - - -class GatewayTimeout(HttpServerError): - """HTTP 504 - Gateway Timeout. - - The server was acting as a gateway or proxy and did not receive a timely - response from the upstream server. - """ - http_status = 504 - message = "Gateway Timeout" - - -class HttpVersionNotSupported(HttpServerError): - """HTTP 505 - HttpVersion Not Supported. - - The server does not support the HTTP protocol version used in the request. - """ - http_status = 505 - message = "HTTP Version Not Supported" - - -# _code_map contains all the classes that have http_status attribute. -_code_map = dict( - (getattr(obj, 'http_status', None), obj) - for name, obj in six.iteritems(vars(sys.modules[__name__])) - if inspect.isclass(obj) and getattr(obj, 'http_status', False) -) - - -def from_response(response, method, url): - """Returns an instance of :class:`HttpError` or subclass based on response. - - :param response: instance of `requests.Response` class - :param method: HTTP method used for request - :param url: URL used for request - """ - kwargs = { - "http_status": response.status_code, - "response": response, - "method": method, - "url": url, - "request_id": response.headers.get("x-compute-request-id"), - } - if "retry-after" in response.headers: - kwargs["retry_after"] = response.headers["retry-after"] - - content_type = response.headers.get("Content-Type", "") - if content_type.startswith("application/json"): - try: - body = response.json() - except ValueError: - pass - else: - if hasattr(body, "keys"): - error = body[body.keys()[0]] - kwargs["message"] = error.get("message") - kwargs["details"] = error.get("details") - elif content_type.startswith("text/"): - kwargs["details"] = response.text - - try: - cls = _code_map[response.status_code] - except KeyError: - if 500 <= response.status_code < 600: - cls = HttpServerError - elif 400 <= response.status_code < 500: - cls = HTTPClientError - else: - cls = HttpError - return cls(**kwargs) diff --git a/monascaclient/client.py b/monascaclient/client.py index a86ea60..f6c981b 100644 --- a/monascaclient/client.py +++ b/monascaclient/client.py @@ -1,4 +1,5 @@ # (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP +# Copyright 2017 Fujitsu LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,12 +14,63 @@ # See the License for the specific language governing permissions and # limitations under the License. -from monascaclient.common import utils +from keystoneauth1 import identity +from keystoneauth1 import session + +from monascaclient.osc import migration +from monascaclient import version -def Client(version, *args, **kwargs): - module = utils.import_versioned_module(version, 'client') - client_class = getattr(module, 'Client') - if 'use_environment_variables' in kwargs and kwargs['use_environment_variables']: - utils.set_env_variables(kwargs) - return client_class(*args, **kwargs) +def Client(api_version, **kwargs): + + auth = _get_auth_handler(kwargs) + sess = _get_session(auth, kwargs) + + client = migration.make_client( + api_version=api_version, + session=sess, + endpoint=kwargs.get('endpoint'), + service_type=kwargs.get('service_type', 'monitoring') + ) + + return client + + +def _get_session(auth, kwargs): + return session.Session(auth=auth, + app_name='monascaclient', + app_version=version.version_string, + cert=kwargs.get('cert', None), + timeout=kwargs.get('timeout', None), + verify=kwargs.get('verify', + not kwargs.get('insecure', + False))) + + +def _get_auth_handler(kwargs): + if 'token' in kwargs: + auth = identity.Token( + auth_url=kwargs.get('auth_url', None), + token=kwargs.get('token', None), + project_id=kwargs.get('project_id', None), + project_name=kwargs.get('project_name', None), + project_domain_id=kwargs.get('project_domain_id', None), + project_domain_name=kwargs.get('project_domain_name', None) + ) + elif {'username', 'password'} <= set(kwargs): + auth = identity.Password( + auth_url=kwargs.get('auth_url', None), + username=kwargs.get('username', None), + password=kwargs.get('password', None), + project_id=kwargs.get('project_id', None), + project_name=kwargs.get('project_name', None), + project_domain_id=kwargs.get('project_domain_id', None), + project_domain_name=kwargs.get('project_domain_name', None), + user_domain_id=kwargs.get('user_domain_id', None), + user_domain_name=kwargs.get('user_domain_name', None) + ) + else: + raise Exception('monascaclient can be configured with either ' + '"token" or "username:password" but neither of ' + 'them was found in passed arguments.') + return auth diff --git a/monascaclient/common/http.py b/monascaclient/common/http.py deleted file mode 100644 index c7c4644..0000000 --- a/monascaclient/common/http.py +++ /dev/null @@ -1,323 +0,0 @@ -# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company LP -# -# 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 copy -import logging -import os -import socket - -import requests -import six -from six.moves.urllib import parse - -from monascaclient import exc -from monascaclient import ksclient - -from oslo_serialization import jsonutils -from oslo_utils import encodeutils - -LOG = logging.getLogger(__name__) -if not LOG.handlers: - LOG.addHandler(logging.StreamHandler()) -USER_AGENT = 'python-monascaclient' -CHUNKSIZE = 1024 * 64 # 64kB - - -def get_system_ca_file(): - """Return path to system default CA file.""" - # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, - # Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca - ca_path = ['/etc/ssl/certs/ca-certificates.crt', - '/etc/pki/tls/certs/ca-bundle.crt', - '/etc/ssl/ca-bundle.pem', - '/etc/ssl/cert.pem', - '/System/Library/OpenSSL/certs/cacert.pem', - requests.certs.where()] - for ca in ca_path: - LOG.debug("Looking for ca file %s", ca) - if os.path.exists(ca): - LOG.debug("Using ca file %s", ca) - return ca - LOG.warning("System ca file could not be found.") - - -class HTTPClient(object): - - def __init__(self, endpoint, write_timeout=None, read_timeout=None, **kwargs): - if endpoint.endswith('/'): - endpoint = endpoint[:-1] - self.endpoint = endpoint - self.write_timeout = write_timeout - self.read_timeout = read_timeout - self.auth_url = kwargs.get('auth_url') - self.auth_token = kwargs.get('token') - self.username = kwargs.get('username') - self.password = kwargs.get('password') - self.user_domain_id = kwargs.get('user_domain_id') - self.user_domain_name = kwargs.get('user_domain_name') - self.region_name = kwargs.get('region_name') - self.endpoint_url = endpoint - # adding for re-authenticate - self.project_name = kwargs.get('project_name') - self.region_name = kwargs.get('region_name') - self.project_id = kwargs.get('project_id') - self.domain_id = kwargs.get('domain_id') - self.domain_name = kwargs.get('domain_name') - self.endpoint_type = kwargs.get('endpoint_type') - self.service_type = kwargs.get('service_type') - self.keystone_timeout = kwargs.get('keystone_timeout') - - self.cert_file = kwargs.get('cert_file') - self.key_file = kwargs.get('key_file') - - self.ssl_connection_params = { - 'os_cacert': kwargs.get('os_cacert'), - 'cert_file': kwargs.get('cert_file'), - 'key_file': kwargs.get('key_file'), - 'insecure': kwargs.get('insecure'), - } - - self.verify_cert = None - if parse.urlparse(endpoint).scheme == "https": - if kwargs.get('insecure'): - self.verify_cert = False - else: - self.verify_cert = kwargs.get( - 'os_cacert', get_system_ca_file()) - - def replace_token(self, token): - self.auth_token = token - - def re_authenticate(self): - ks_args = { - 'username': self.username, - 'password': self.password, - 'user_domain_id': self.user_domain_id, - 'user_domain_name': self.user_domain_name, - 'token': '', - 'auth_url': self.auth_url, - 'service_type': self.service_type, - 'endpoint_type': self.endpoint_type, - 'os_cacert': self.ssl_connection_params['os_cacert'], - 'project_id': self.project_id, - 'project_name': self.project_name, - 'domain_id': self.domain_id, - 'domain_name': self.domain_name, - 'insecure': self.ssl_connection_params['insecure'], - 'region_name': self.region_name, - 'keystone_timeout': self.keystone_timeout - } - try: - _ksclient = ksclient.KSClient(**ks_args) - self.auth_token = _ksclient.token - except Exception as e: - raise exc.KeystoneException(e) - - def log_curl_request(self, method, url, kwargs): - curl = ['curl -i -X %s' % method] - - for (key, value) in kwargs['headers'].items(): - if key in ('X-Auth-Token'): - value = '*****' - header = '-H \'%s: %s\'' % (encodeutils.safe_decode(key), - encodeutils.safe_decode(value)) - curl.append(header) - - conn_params_fmt = [ - ('key_file', '--key %s'), - ('cert_file', '--cert %s'), - ('os_cacert', '--cacert %s'), - ] - for (key, fmt) in conn_params_fmt: - value = self.ssl_connection_params.get(key) - if value: - curl.append(fmt % value) - - if self.ssl_connection_params.get('insecure'): - curl.append('-k') - - if 'data' in kwargs: - curl.append('-d \'%s\'' % kwargs['data']) - - curl.append('%s%s' % (self.endpoint, url)) - LOG.debug(' '.join(curl)) - - @staticmethod - def log_http_response(resp): - status = (resp.raw.version / 10.0, resp.status_code, resp.reason) - dump = ['\nHTTP/%.1f %s %s' % status] - dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()]) - dump.append('') - if resp.content: - content = resp.content - if isinstance(content, six.binary_type): - content = content.decode('utf-8', 'strict') - dump.extend([content, '']) - LOG.debug('\n'.join(dump)) - - def _http_request(self, url, method, **kwargs): - """Send an http request with the specified characteristics. - - Wrapper around requests.request to handle tasks such as - setting headers and error handling. - """ - # Copy the kwargs so we can reuse the original in case of redirects - kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) - kwargs['headers'].setdefault('User-Agent', USER_AGENT) - if self.auth_token: - kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) - - self.log_curl_request(method, url, kwargs) - - if self.cert_file and self.key_file: - kwargs['cert'] = (self.cert_file, self.key_file) - - if self.verify_cert is not None: - kwargs['verify'] = self.verify_cert - - # Since requests does not follow the RFC when doing redirection to sent - # back the same method on a redirect we are simply bypassing it. For - # example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says - # that we should follow that URL with the same method as before, - # requests doesn't follow that and send a GET instead for the method. - # Hopefully this could be fixed as they say in a comment in a future - # point version i.e: 3.x - # See issue: https://github.com/kennethreitz/requests/issues/1704 - allow_redirects = False - timeout = None - if method in ['POST', 'DELETE', 'PUT', 'PATCH']: - timeout = self.write_timeout - elif method is 'GET': - timeout = self.read_timeout - - resp = self._make_request(method, url, allow_redirects, timeout, - **kwargs) - if self._unauthorized(resp): - try: - # re-authenticate and attempt one more request - self.re_authenticate() - kwargs['headers']['X-Auth-Token'] = self.auth_token - resp = self._make_request(method, url, allow_redirects, - timeout, **kwargs) - self._check_status_code(resp, method, **kwargs) - except exc.KeystoneException: - raise - else: - self._check_status_code(resp, method, **kwargs) - return resp - - def _unauthorized(self, resp): - status401 = (resp.status_code == 401) - status500 = (resp.status_code == 500 and "(HTTP 401)" in resp.content) - return status401 or status500 - - def _check_status_code(self, resp, method, **kwargs): - if self._unauthorized(resp): - message = "Unauthorized error" - raise exc.HTTPUnauthorized(message=message) - elif 400 <= resp.status_code < 600: - raise exc.from_response(resp) - elif resp.status_code in (301, 302, 305): - # Redirected. Reissue the request to the new location. - location = resp.headers.get('location') - if location is None: - message = "Location not returned with 302" - raise exc.InvalidEndpoint(message=message) - elif location.startswith(self.endpoint): - # shave off the endpoint, it will be prepended when we recurse - location = location[len(self.endpoint):] - else: - message = "Prohibited endpoint redirect %s" % location - raise exc.InvalidEndpoint(message=message) - return self._http_request(location, method, **kwargs) - elif resp.status_code == 300: - raise exc.from_response(resp) - - def _make_request(self, method, url, allow_redirects, timeout, **kwargs): - try: - resp = requests.request( - method, - self.endpoint_url + url, - allow_redirects=allow_redirects, - timeout=timeout, - **kwargs) - except socket.gaierror as e: - message = ("Error finding address for %(url)s: %(e)s" % - {'url': self.endpoint_url + url, 'e': e}) - raise exc.InvalidEndpoint(message=message) - except (socket.error, socket.timeout) as e: - endpoint = self.endpoint - message = ("Error communicating with %(endpoint)s %(e)s" % - {'endpoint': endpoint, 'e': e}) - raise exc.CommunicationError(message=message) - except requests.Timeout as e: - endpoint = self.endpoint - message = ("Error %(method)s timeout request to %(endpoint)s %(e)s" % - {'method': method, 'endpoint': endpoint, 'e': e}) - raise exc.RequestTimeoutError(message=message) - except requests.ConnectionError as ex: - endpoint = self.endpoint - message = ("Failed to connect to %s, error was %s" % (endpoint, ex.message)) - raise exc.CommunicationError(message=message) - self.log_http_response(resp) - return resp - - def json_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', 'application/json') - kwargs['headers'].setdefault('Accept', 'application/json') - - if 'data' in kwargs: - kwargs['data'] = jsonutils.dumps(kwargs['data']) - - resp = self._http_request(url, method, **kwargs) - body = resp.content - if 'application/json' in resp.headers.get('content-type', ''): - try: - body = resp.json() - except ValueError: - LOG.error('Could not decode response body as JSON') - else: - body = None - - return resp, body - - def raw_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', - 'application/octet-stream') - return self._http_request(url, method, **kwargs) - - def client_request(self, method, url, **kwargs): - resp, body = self.json_request(method, url, **kwargs) - return resp - - def head(self, url, **kwargs): - return self.client_request("HEAD", url, **kwargs) - - def get(self, url, **kwargs): - return self.client_request("GET", url, **kwargs) - - def post(self, url, **kwargs): - return self.client_request("POST", url, **kwargs) - - def put(self, url, **kwargs): - return self.client_request("PUT", url, **kwargs) - - def delete(self, url, **kwargs): - return self.raw_request("DELETE", url, **kwargs) - - def patch(self, url, **kwargs): - return self.client_request("PATCH", url, **kwargs) diff --git a/monascaclient/common/monasca_manager.py b/monascaclient/common/monasca_manager.py index 6ad328c..f973abd 100644 --- a/monascaclient/common/monasca_manager.py +++ b/monascaclient/common/monasca_manager.py @@ -1,4 +1,5 @@ # (C) Copyright 2014, 2015 Hewlett Packard Enterprise Development Company LP +# Copyright 2017 Fujitsu LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,16 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from monascaclient.apiclient import base from six.moves.urllib import parse -from six.moves.urllib_parse import unquote -class MonascaManager(base.BaseManager): +class MonascaManager(object): + base_url = None - def __init__(self, client, **kwargs): - super(MonascaManager, self).__init__(client) + def __init__(self, client): + self.client = client def _parse_body(self, body): if type(body) is dict: @@ -38,18 +38,22 @@ class MonascaManager(base.BaseManager): """Get a list of metrics.""" url_str = self.base_url + path if dim_key and dim_key in kwargs: - dimstr = self.get_dimensions_url_string(kwargs[dim_key]) - kwargs[dim_key] = dimstr + dim_str = self.get_dimensions_url_string(kwargs[dim_key]) + kwargs[dim_key] = dim_str if kwargs: url_str += '?%s' % parse.urlencode(kwargs, True) - resp, body = self.client.json_request( - 'GET', url_str) + + body = self.client.list( + path=url_str + ) + return self._parse_body(body) - def get_dimensions_url_string(self, dimdict): + @staticmethod + def get_dimensions_url_string(dimensions): dim_list = list() - for k, v in dimdict.items(): + for k, v in dimensions.items(): # In case user specifies a dimension multiple times if isinstance(v, (list, tuple)): v = v[-1] @@ -59,10 +63,3 @@ class MonascaManager(base.BaseManager): dim_str = k dim_list.append(dim_str) return ','.join(dim_list) - - def list_next(self): - if hasattr(self, 'next') and self.next: - self.next = unquote(self.next) - path = self.next.split(self.base_url, 1)[-1] - return self._list(path) - return None diff --git a/monascaclient/common/utils.py b/monascaclient/common/utils.py index e233994..57fa75b 100644 --- a/monascaclient/common/utils.py +++ b/monascaclient/common/utils.py @@ -16,18 +16,13 @@ from __future__ import print_function import numbers -import os -import sys -import textwrap -import uuid import prettytable import yaml -from monascaclient import exc +from osc_lib import exceptions as exc from oslo_serialization import jsonutils -from oslo_utils import importutils supported_formats = { "json": lambda x: jsonutils.dumps(x, indent=2), @@ -45,23 +40,14 @@ def arg(*args, **kwargs): return _decorator -def link_formatter(links): - return '\n'.join([l.get('href', '') for l in links or []]) - - def json_formatter(js): return (jsonutils.dumps(js, indent=2, ensure_ascii=False)).encode('utf-8') -def text_wrap_formatter(d): - return '\n'.join(textwrap.wrap(d or '', 55)) +def print_list(objs, fields, field_labels=None, formatters=None, sortby=None): + if formatters is None: + formatters = {} - -def newline_list_formatter(r): - return '\n'.join(r or []) - - -def print_list(objs, fields, field_labels=None, formatters={}, sortby=None): field_labels = field_labels or fields pt = prettytable.PrettyTable([f for f in field_labels], caching=False, print_empty=False) @@ -84,7 +70,9 @@ def print_list(objs, fields, field_labels=None, formatters={}, sortby=None): print(pt.get_string(sortby=field_labels[sortby]).encode('utf-8')) -def print_dict(d, formatters={}): +def print_dict(d, formatters=None): + if formatters is None: + formatters = {} pt = prettytable.PrettyTable(['Property', 'Value'], caching=False, print_empty=False) pt.align = 'l' @@ -97,57 +85,6 @@ def print_dict(d, formatters={}): print(pt.get_string(sortby='Property').encode('utf-8')) -def find_resource(manager, name_or_id): - """Helper for the _find_* methods.""" - # first try to get entity as integer id - try: - if isinstance(name_or_id, int) or name_or_id.isdigit(): - return manager.get(int(name_or_id)) - except exc.NotFound: - pass - - # now try to get entity as uuid - try: - uuid.UUID(str(name_or_id)) - return manager.get(name_or_id) - except (ValueError, exc.NotFound): - pass - - # finally try to find entity by name - try: - return manager.find(name=name_or_id) - except exc.NotFound: - msg = ("No %s with a name or ID of '%s' exists." % - (manager.resource_class.__name__.lower(), name_or_id)) - raise exc.CommandError(msg) - - -def env(*vars, **kwargs): - """Search for the first defined of possibly many env vars - - Returns the first environment variable defined in vars, or - returns the default defined in kwargs. - """ - for v in vars: - value = os.environ.get(v) - if value: - return value - return kwargs.get('default', None) - - -def import_versioned_module(version, submodule=None): - module = 'monascaclient.v%s' % version - if submodule: - module = '.'.join((module, submodule)) - return importutils.import_module(module) - - -def exit(msg=''): - if msg: - print(msg.encode('utf-8'), file=sys.stderr) - sys.exit(1) - - def format_parameters(params): '''Reformat parameters into dict of format expected by the API.''' @@ -165,7 +102,7 @@ def format_parameters(params): parameters = {} for p in params: try: - (n, v) = p.split(('='), 1) + (n, v) = p.split('=', 1) except ValueError: msg = '%s(%s). %s.' % ('Malformed parameter', p, 'Use the key=value format') @@ -206,24 +143,14 @@ def format_dimensions_query(dims): return dimensions -def format_output(output, format='yaml'): - """Format the supplied dict as specified.""" - output_format = format.lower() - try: - return supported_formats[output_format](output) - except KeyError: - raise exc.HTTPUnsupported("The format(%s) is unsupported." - % output_format) - - def format_dimensions(dict): - return ('dimensions: {\n' + format_dict(dict) + '\n}') + return 'dimensions: {\n' + format_dict(dict) + '\n}' -def format_expression_data(dict): +def format_expression_data(data): # takes an dictionary containing a dict string_list = list() - for k, v in dict.items(): + for k, v in data.items(): if k == 'dimensions': dim_str = format_dimensions(v) string_list.append(dim_str) @@ -271,25 +198,3 @@ def format_list(in_list): key = k string_list.append(key) return '\n'.join(string_list) - - -def set_env_variables(kwargs): - environment_variables = { - 'username': 'OS_USERNAME', - 'password': 'OS_PASSWORD', - 'token': 'OS_AUTH_TOKEN', - 'auth_url': 'OS_AUTH_URL', - 'service_type': 'OS_SERVICE_TYPE', - 'endpoint_type': 'OS_ENDPOINT_TYPE', - 'os_cacert': 'OS_CACERT', - 'user_domain_id': 'OS_USER_DOMAIN_ID', - 'user_domain_name': 'OS_USER_DOMAIN_NAME', - 'project_id': 'OS_PROJECT_ID', - 'project_name': 'OS_PROJECT_NAME', - 'domain_id': 'OS_DOMAIN_ID', - 'domain_name': 'OS_DOMAIN_NAME', - 'region_name': 'OS_REGION_NAME' - } - for k, v in environment_variables.items(): - if k not in kwargs: - kwargs[k] = env(v) diff --git a/monascaclient/exc.py b/monascaclient/exc.py deleted file mode 100644 index cf4f87c..0000000 --- a/monascaclient/exc.py +++ /dev/null @@ -1,233 +0,0 @@ -# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP -# -# 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 - -from oslo_serialization import jsonutils - -verbose = 0 - -log = logging.getLogger(__name__) - - -class BaseException(Exception): - - """An error occurred.""" - - def __init__(self, message=None): - self.message = message - - def __str__(self): - return self.message or self.__class__.__doc__ - - -class CommandError(BaseException): - - """Invalid usage of CLI.""" - - -class InvalidEndpoint(BaseException): - - """The provided endpoint is invalid.""" - - -class CommunicationError(BaseException): - - """Unable to communicate with server.""" - - -class RequestTimeoutError(BaseException): - - """Timeout making a POST, GET, PATCH, DELETE, or PUT request to the server.""" - - -class KeystoneException(Exception): - - """Base exception for all Keystone-derived exceptions.""" - # This is initialized with the exception raised by the Keystone client so - # deriving this class from Exception instead of BaseException allows that to - # be handled without any additional code - pass - - -class HTTPException(BaseException): - - """Base exception for all HTTP-derived exceptions.""" - code = 'N/A' - - def __init__(self, message=None): - super(HTTPException, self).__init__(message) - try: - log.error("exception: {}".format(message)) - self.error = jsonutils.loads(message) - except Exception: - self.error = {'error': - {'message': self.message or self.__class__.__doc__}} - - def __str__(self): - - if 'description' in self.error: - # Python API: - # Expected message format: - # { - # "title": "Foo", - # "description": "Bar" - # } - message = self.error['description'] - else: - # Java API: - # Expected message format: - # { - # "conflict":{"code":409, - # "message":"Bar", - # "details":"", - # "internal_code":"Baz"} - # } - for key in self.error: - message = self.error[key].get('message', 'Internal Error') - - if verbose: - traceback = self.error['error'].get('traceback', '') - return '%s\n%s' % (message, traceback) - else: - return '%s' % message - - -class HTTPMultipleChoices(HTTPException): - code = 300 - - def __str__(self): - self.details = ("Requested version of Monasca API is not" - "available.") - return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code, - self.details) - - -class BadRequest(HTTPException): - code = 400 - - -class HTTPBadRequest(BadRequest): - pass - - -class Unauthorized(HTTPException): - code = 401 - - -class HTTPUnauthorized(Unauthorized): - pass - - -class Forbidden(HTTPException): - - """DEPRECATED.""" - code = 403 - - -class HTTPForbidden(Forbidden): - pass - - -class NotFound(HTTPException): - - """DEPRECATED.""" - code = 404 - - -class HTTPNotFound(NotFound): - pass - - -class HTTPMethodNotAllowed(HTTPException): - code = 405 - - -class Conflict(HTTPException): - - """DEPRECATED.""" - code = 409 - - -class HTTPConflict(Conflict): - pass - - -class OverLimit(HTTPException): - - """DEPRECATED.""" - code = 413 - - -class HTTPOverLimit(OverLimit): - pass - - -class HTTPUnsupported(HTTPException): - code = 415 - - -class HTTPUnProcessable(HTTPException): - code = 422 - - -class HTTPInternalServerError(HTTPException): - code = 500 - - -class HTTPNotImplemented(HTTPException): - code = 501 - - -class HTTPBadGateway(HTTPException): - code = 502 - - -class ServiceUnavailable(HTTPException): - - """DEPRECATED.""" - code = 503 - - -class HTTPServiceUnavailable(ServiceUnavailable): - pass - - -# NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception -# classes -_code_map = {} -for obj_name in dir(sys.modules[__name__]): - if obj_name.startswith('HTTP'): - obj = getattr(sys.modules[__name__], obj_name) - _code_map[obj.code] = obj - - -def from_response(response): - """Return an instance of an HTTPException based on requests response.""" - cls = _code_map.get(response.status_code, HTTPException) - return cls(response.content) - - -class NoTokenLookupException(Exception): - - """DEPRECATED.""" - pass - - -class EndpointNotFound(Exception): - - """DEPRECATED.""" - pass diff --git a/monascaclient/ksclient.py b/monascaclient/ksclient.py deleted file mode 100644 index 652bbc4..0000000 --- a/monascaclient/ksclient.py +++ /dev/null @@ -1,105 +0,0 @@ -# (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP -# -# 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. - -""" -Wrapper around python keystone client to assist in getting a properly scoped token and the registered service -endpoint for Monasca. -""" - -from keystoneclient.v3 import client - -from monascaclient import exc - - -class KSClient(object): - - def __init__(self, **kwargs): - """Get an endpoint and auth token from Keystone. - - :param username: name of user - :param password: user's password - :param user_domain_id: unique identifier of domain username resides in (optional) - :param user_domain_name: name of domain for username (optional), if user_domain_id not specified - :param project_id: unique identifier of project - :param project_name: name of project - :param project_domain_name: name of domain project is in - :param project_domain_id: id of domain project is in - :param auth_url: endpoint to authenticate against - :param token: token to use instead of username/password - """ - kc_args = {'auth_url': kwargs.get('auth_url'), - 'insecure': kwargs.get('insecure'), - 'timeout': kwargs.get('keystone_timeout')} - - if kwargs.get('os_cacert'): - kc_args['cacert'] = kwargs.get('os_cacert') - if kwargs.get('project_id'): - kc_args['project_id'] = kwargs.get('project_id') - elif kwargs.get('project_name'): - kc_args['project_name'] = kwargs.get('project_name') - if kwargs.get('project_domain_name'): - kc_args['project_domain_name'] = kwargs.get('project_domain_name') - elif kwargs.get('domain_name'): - kc_args['project_domain_name'] = kwargs.get('domain_name') # backwards compat to 1.0.30 API - if kwargs.get('project_domain_id'): - kc_args['project_domain_id'] = kwargs.get('project_domain_id') - elif kwargs.get('domain_id'): - kc_args['project_domain_id'] = kwargs.get('domain_id') # backwards compat to 1.0.30 API - - if kwargs.get('token'): - kc_args['token'] = kwargs.get('token') - else: - kc_args['username'] = kwargs.get('username') - kc_args['password'] = kwargs.get('password') - # when username not in the default domain (id='default'), supply user domain (as namespace) - if kwargs.get('user_domain_name'): - kc_args['user_domain_name'] = kwargs.get('user_domain_name') - if kwargs.get('user_domain_id'): - kc_args['user_domain_id'] = kwargs.get('user_domain_id') - - self._kwargs = kwargs - self._keystone = client.Client(**kc_args) - self._token = None - self._monasca_url = None - - @property - def token(self): - """Token property - - Validate token is project scoped and return it if it is - project_id and auth_token were fetched when keystone client was created - """ - if self._token is None: - if self._keystone.project_id: - self._token = self._keystone.auth_token - else: - raise exc.CommandError("No project id or project name.") - return self._token - - @property - def monasca_url(self): - """Return the monasca publicURL registered in keystone.""" - if self._monasca_url is None: - if self._kwargs.get('region_name'): - self._monasca_url = self._keystone.service_catalog.url_for( - service_type=self._kwargs.get('service_type') or 'monitoring', - attr='region', - filter_value=self._kwargs.get('region_name'), - endpoint_type=self._kwargs.get('endpoint_type') or 'publicURL') - else: - self._monasca_url = self._keystone.service_catalog.url_for( - service_type=self._kwargs.get('service_type') or 'monitoring', - endpoint_type=self._kwargs.get('endpoint_type') or 'publicURL') - return self._monasca_url diff --git a/monascaclient/apiclient/__init__.py b/monascaclient/osc/__init__.py similarity index 100% rename from monascaclient/apiclient/__init__.py rename to monascaclient/osc/__init__.py diff --git a/monascaclient/osc/migration.py b/monascaclient/osc/migration.py new file mode 100644 index 0000000..9c73da9 --- /dev/null +++ b/monascaclient/osc/migration.py @@ -0,0 +1,169 @@ +# Copyright 2017 FUJITSU LIMITED +# +# 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 six + +from osc_lib.command import command +from osc_lib import utils + +from monascaclient import version + +LOG = logging.getLogger(__name__) + +# NOTE(trebskit) this will be moved to another module +# once initial migration is up +# the point is to show how many code can we spare +# in order to get the client working with minimum effort needed +VERSION_MAP = { + '2_0': 'monascaclient.v2_0.client.Client' +} + + +def make_client(api_version, session=None, + endpoint=None, service_type='monitoring'): + """Returns an monitoring API client.""" + + client_cls = utils.get_client_class('monitoring', api_version, VERSION_MAP) + c = client_cls( + session=session, + service_type=service_type, + endpoint=endpoint, + app_name='monascaclient', + app_version=version.version_string, + ) + + return c + + +def create_command_class(name, func_module): + """Dynamically creates subclass of MigratingCommand. + + Method takes name of the function, module it is part of + and builds the subclass of :py:class:`MigratingCommand`. + Having a subclass of :py:class:`cliff.command.Command` is mandatory + for the osc-lib integration. + + :param name: name of the function + :type name: basestring + :param func_module: the module function is part of + :type func_module: module + :return: command name, subclass of :py:class:`MigratingCommand` + :rtype: tuple(basestring, class) + + """ + + cmd_name = name[3:].replace('_', '-') + callback = getattr(func_module, name) + desc = callback.__doc__ or '' + help = desc.strip().split('\n')[0] + + arguments = getattr(callback, 'arguments', []) + + body = { + '_args': arguments, + '_callback': staticmethod(callback), + '_description': desc, + '_epilog': desc, + '_help': help + } + + claz = type('%sCommand' % cmd_name.title().replace('-', ''), + (MigratingCommand,), body) + + return cmd_name, claz + + +class MigratingCommandMeta(command.CommandMeta): + """Overwrite module name based on osc_lib.CommandMeta requirements.""" + + def __new__(mcs, name, bases, cls_dict): + # NOTE(trebskit) little dirty, but should suffice for migration period + cls_dict['__module__'] = 'monascaclient.v2_0.shell' + return super(MigratingCommandMeta, mcs).__new__(mcs, name, + bases, cls_dict) + + +@six.add_metaclass(MigratingCommandMeta) +class MigratingCommand(command.Command): + """MigratingCommand is temporary command. + + MigratingCommand allows to map function defined + shell commands from :py:module:`monascaclient.v2_0.shell` + into :py:class:`command.Command` instances. + + Note: + This class is temporary solution during migrating + to osc_lib and will be removed when all + shell commands are migrated to cliff commands. + + """ + + _help = None + _args = None + _callback = None + + def __init__(self, app, app_args, cmd_name=None): + super(MigratingCommand, self).__init__(app, app_args, cmd_name) + self._client = None + self._endpoint = None + + def take_action(self, parsed_args): + return self._callback(self.mon_client, parsed_args) + + def get_parser(self, prog_name): + parser = super(MigratingCommand, self).get_parser(prog_name) + for (args, kwargs) in self._args: + parser.add_argument(*args, **kwargs) + parser.add_argument('-j', '--json', + action='store_true', + help='output raw json response') + return parser + + @property + def mon_client(self): + if not self._client: + self.log.debug('Initializing mon-client') + self._client = make_client(api_version=self.mon_version, + endpoint=self.mon_url, + session=self.app.client_manager.session) + return self._client + + @property + def mon_version(self): + return self.app_args.monasca_api_version + + @property + def mon_url(self): + if self._endpoint: + return self._endpoint + + app_args = self.app_args + cm = self.app.client_manager + + endpoint = app_args.monasca_api_url + + if not endpoint: + req_data = { + 'service_type': 'monitoring', + 'region_name': cm.region_name, + 'interface': cm.interface, + } + LOG.debug('Discovering monasca endpoint using %s' % req_data) + endpoint = cm.get_endpoint_for_service_type(**req_data) + else: + LOG.debug('Using supplied endpoint=%s' % endpoint) + + self._endpoint = endpoint + return self._endpoint diff --git a/monascaclient/shell.py b/monascaclient/shell.py index 0506833..354d426 100644 --- a/monascaclient/shell.py +++ b/monascaclient/shell.py @@ -1,4 +1,5 @@ # (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP +# Copyright 2017 Fujitsu LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,499 +18,100 @@ Command-line interface to the monasca-client API. """ -from __future__ import print_function - import argparse -import logging -import string +import locale import sys -import warnings -from six.moves.urllib.parse import urljoin +from osc_lib.api import auth +from osc_lib.cli import client_config as cloud_config +from osc_lib import shell +from osc_lib import utils as utils +from oslo_utils import importutils +import six -import monascaclient -from monascaclient import client as monasca_client -from monascaclient.common import utils -from monascaclient import exc -from monascaclient import ksclient +from monascaclient.osc import migration +from monascaclient import version as mc_version -logger = logging.getLogger(__name__) - - -class DeprecatedStore(argparse._StoreAction): - def __init__(self, *args, **kwargs): - super(DeprecatedStore, self).__init__(*args, **kwargs) - - def __call__(self, parser, namespace, values, option_string=None): - warnings.filterwarnings(action='default', category=DeprecationWarning, module='.*monascaclient.*') - warnings.warn("{} is deprecated".format(",".join(self.option_strings)), - DeprecationWarning) - setattr(namespace, self.dest, values) - - -class MonascaShell(object): - - def get_base_parser(self): - parser = argparse.ArgumentParser( - prog='monasca', +class MonascaShell(shell.OpenStackShell): + def __init__(self): + super(MonascaShell, self).__init__( description=__doc__.strip(), - epilog='See "monasca help COMMAND" ' - 'for help on a specific command.', - add_help=False, - # formatter_class=HelpFormatter, - formatter_class=lambda prog: argparse.HelpFormatter( - prog, - max_help_position=29) + version=mc_version.version_string + ) + self.cloud_config = None + + def initialize_app(self, argv): + super(MonascaShell, self).initialize_app(argv) + self.cloud_config = cloud_config.OSC_Config( + override_defaults={ + 'interface': None, + 'auth_type': self._auth_type, + }, ) - # Global arguments - parser.add_argument('-h', '--help', - action='store_true', - help=argparse.SUPPRESS) - - parser.add_argument('-j', '--json', - action='store_true', - help='output raw json response') - - parser.add_argument('--version', - action='version', - version=monascaclient.__version__, - help="Shows the client version and exits.") - - parser.add_argument('-d', '--debug', - default=bool(utils.env('MONASCA_DEBUG')), - action='store_true', - help='Defaults to env[MONASCA_DEBUG].') - - parser.add_argument('-v', '--verbose', - default=False, action="store_true", - help="Print more verbose output.") - - parser.add_argument('-k', '--insecure', - default=False, - action='store_true', - help="Explicitly allow the client to perform " - "\"insecure\" SSL (https) requests. The server's " - "certificate will not be verified against any " - "certificate authorities. " - "This option should be used with caution.") - - parser.add_argument('--cert-file', - help='Path of certificate file to use in SSL ' - 'connection. This file can optionally be ' - 'prepended with the private key.') - - parser.add_argument( - '--key-file', - help='Path of client key to use in SSL connection. ' - 'This option is not necessary if your key is' - ' prepended to your cert file.') - - parser.add_argument('--os-cacert', - default=utils.env('OS_CACERT'), - help='Specify a CA bundle file to use in verifying' - ' a TLS (https) server certificate. Defaults to' - ' env[OS_CACERT]. Without either of these, the' - ' client looks for the default system CA' - ' certificates.') - - parser.add_argument('--keystone_timeout', - default=20, - help='Number of seconds to wait for a response from keystone.') - - parser.add_argument('--os-username', - default=utils.env('OS_USERNAME'), - help='Defaults to env[OS_USERNAME].') - - parser.add_argument('--os_username', - help=argparse.SUPPRESS) - - parser.add_argument('--os-password', - default=utils.env('OS_PASSWORD'), - help='Defaults to env[OS_PASSWORD].') - - parser.add_argument('--os_password', - help=argparse.SUPPRESS) - - parser.add_argument('--os-user-domain-id', - default=utils.env('OS_USER_DOMAIN_ID'), - help='Defaults to env[OS_USER_DOMAIN_ID].') - - parser.add_argument('--os-user-domain-name', - default=utils.env('OS_USER_DOMAIN_NAME'), - help='Defaults to env[OS_USER_DOMAIN_NAME].') - - parser.add_argument('--os-project-id', - default=utils.env('OS_PROJECT_ID'), - help='Defaults to env[OS_PROJECT_ID].') - - parser.add_argument('--os_project_id', - help=argparse.SUPPRESS) - - parser.add_argument('--os-project-name', - default=utils.env('OS_PROJECT_NAME'), - help='Defaults to env[OS_PROJECT_NAME].') - - parser.add_argument('--os_project_name', - help=argparse.SUPPRESS) - - parser.add_argument('--os-tenant-name', - dest='os_project_name', - action=DeprecatedStore, - default=utils.env('OS_TENANT_NAME'), - help='(Deprecated, use --os-project_name) ' - 'Defaults to env[OS_TENANT_NAME].') - - parser.add_argument('--os_tenant_name', - dest='os_project_name', - action=DeprecatedStore, - help=argparse.SUPPRESS) - - parser.add_argument('--os-tenant-id', - dest='os_project_id', - action=DeprecatedStore, - default=utils.env('OS_TENANT_ID'), - help='(Deprecated, use --os-project_id) ' - 'Defaults to env[OS_TENANT_ID].') - - parser.add_argument('--os_tenant_id', - dest='os_project_id', - action=DeprecatedStore, - help=argparse.SUPPRESS) - - parser.add_argument('--os-project-domain-id', - default=utils.env('OS_PROJECT_DOMAIN_ID'), - help='Defaults to env[OS_PROJECT_DOMAIN_ID].') - - parser.add_argument('--os-project-domain-name', - default=utils.env('OS_PROJECT_DOMAIN_NAME'), - help='Defaults to env[OS_PROJECT_DOMAIN_NAME].') - - parser.add_argument('--os-auth-url', - default=utils.env('OS_AUTH_URL'), - help='Defaults to env[OS_AUTH_URL].') - - parser.add_argument('--os_auth_url', - help=argparse.SUPPRESS) - - parser.add_argument('--os-auth-version', - default=utils.env('OS_AUTH_VERSION'), - help='Defaults to env[OS_AUTH_VERSION].') - - parser.add_argument('--os_auth_version', - help=argparse.SUPPRESS) - - parser.add_argument('--os-region-name', - default=utils.env('OS_REGION_NAME'), - help='Defaults to env[OS_REGION_NAME].') - - parser.add_argument('--os_region_name', - help=argparse.SUPPRESS) - - parser.add_argument('--os-auth-token', - default=utils.env('OS_AUTH_TOKEN'), - help='Defaults to env[OS_AUTH_TOKEN].') - - parser.add_argument('--os_auth_token', - help=argparse.SUPPRESS) - - parser.add_argument('--os-no-client-auth', - default=utils.env('OS_NO_CLIENT_AUTH'), - action='store_true', - help="Do not contact keystone for a token. " - "Defaults to env[OS_NO_CLIENT_AUTH].") + def build_option_parser(self, description, version): + parser = super(MonascaShell, self).build_option_parser( + description, + version + ) + parser = auth.build_auth_plugins_option_parser(parser) + parser = self._append_monasca_args(parser) + return parser + @staticmethod + def _append_monasca_args(parser): parser.add_argument('--monasca-api-url', default=utils.env('MONASCA_API_URL'), help='Defaults to env[MONASCA_API_URL].') - parser.add_argument('--monasca_api_url', help=argparse.SUPPRESS) - parser.add_argument('--monasca-api-version', default=utils.env( 'MONASCA_API_VERSION', default='2_0'), help='Defaults to env[MONASCA_API_VERSION] or 2_0') - parser.add_argument('--monasca_api_version', help=argparse.SUPPRESS) - - parser.add_argument('--os-service-type', - default=utils.env('OS_SERVICE_TYPE'), - help='Defaults to env[OS_SERVICE_TYPE].') - - parser.add_argument('--os_service_type', - help=argparse.SUPPRESS) - - parser.add_argument('--os-endpoint-type', - default=utils.env('OS_ENDPOINT_TYPE'), - help='Defaults to env[OS_ENDPOINT_TYPE].') - - parser.add_argument('--os_endpoint_type', - help=argparse.SUPPRESS) - - # In OpenStack, the parameters below are intended for domain-scoping, i.e. authorize - # the user against a domain instead of a project. Previous versions of the agent used these - # to qualify the project name, leading to confusion and preventing reuse of typical RC files. - # Since domain scoping is not supported by Monasca, we can still support the old variable - # names for the time being. If the project-name is not scoped using the correct project - # domain name parameter, the code falls back to the domain scoping parameters. - parser.add_argument('--os-domain-id', - default=utils.env('OS_DOMAIN_ID'), - help=argparse.SUPPRESS) - - parser.add_argument('--os-domain-name', - default=utils.env('OS_DOMAIN_NAME'), - help=argparse.SUPPRESS) - return parser - def get_subcommand_parser(self, version): - parser = self.get_base_parser() + def _load_commands(self): + version = self.options.monasca_api_version - self.subcommands = {} - subparsers = parser.add_subparsers(metavar='') - submodule = utils.import_versioned_module(version, 'shell') - self._find_actions(subparsers, submodule) - self._find_actions(subparsers, self) - self._add_bash_completion_subparser(subparsers) + submodule = importutils.import_versioned_module('monascaclient', + version, + 'shell') - return parser + self._find_actions(submodule) - def _add_bash_completion_subparser(self, subparsers): - subparser = subparsers.add_parser( - 'bash_completion', - add_help=False, - formatter_class=HelpFormatter - ) - self.subcommands['bash_completion'] = subparser - subparser.set_defaults(func=self.do_bash_completion) - - def _find_actions(self, subparsers, actions_module): + def _find_actions(self, actions_module): for attr in (a for a in dir(actions_module) if a.startswith('do_')): - # I prefer to be hyphen-separated instead of underscores. - command = attr[3:].replace('_', '-') - callback = getattr(actions_module, attr) - desc = callback.__doc__ or '' - help = desc.strip().split('\n')[0] - arguments = getattr(callback, 'arguments', []) + name, clazz = migration.create_command_class(attr, actions_module) - subparser = subparsers.add_parser(command, - help=help, - description=desc, - add_help=False, - formatter_class=HelpFormatter) - 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) + if 'help' == name: + # help requires no auth + clazz.auth_required = False - def _setup_logging(self, debug): - log_lvl = logging.DEBUG if debug else logging.ERROR - logging.basicConfig( - format="%(levelname)s (%(module)s:%(lineno)d) %(message)s", - level=log_lvl) - - def _setup_verbose(self, verbose): - if verbose: - exc.verbose = 1 - - def main(self, argv): - # Parse args once to find version - parser = self.get_base_parser() - (options, args) = parser.parse_known_args(argv) - self._setup_logging(options.debug) - self._setup_verbose(options.verbose) - - # build available subcommands based on version - api_version = options.monasca_api_version - subcommand_parser = self.get_subcommand_parser(api_version) - self.parser = subcommand_parser - - # Handle top-level --help/-h before attempting to parse - # a command off the command line - if not args and options.help or not argv: - self.do_help(options) - return 0 - - # Parse args again and call whatever callback was selected - args = subcommand_parser.parse_args(argv) - - # Short-circuit and deal with help command right away. - if args.func == self.do_help: - self.do_help(args) - return 0 - elif args.func == self.do_bash_completion: - self.do_bash_completion(args) - return 0 - - if not args.os_username and not args.os_auth_token: - raise exc.CommandError("You must provide a username via" - " either --os-username or env[OS_USERNAME]" - " or a token via --os-auth-token or" - " env[OS_AUTH_TOKEN]") - - if not args.os_password and not args.os_auth_token: - raise exc.CommandError("You must provide a password via" - " either --os-password or env[OS_PASSWORD]" - " or a token via --os-auth-token or" - " env[OS_AUTH_TOKEN]") - - if args.os_no_client_auth: - if not args.monasca_api_url: - raise exc.CommandError("If you specify --os-no-client-auth" - " you must specify a Monasca API URL" - " via either --monasca-api-url or" - " env[MONASCA_API_URL]") - else: - if not args.os_auth_url: - raise exc.CommandError("You must provide an auth url via" - " either --os-auth-url or via" - " env[OS_AUTH_URL]") - - auth_vars_present = args.os_auth_url and args.os_auth_version - versioned = 'v2.0' in args.os_auth_url or 'v3' in args.os_auth_url - if auth_vars_present and not versioned: - args.os_auth_url = urljoin(args.os_auth_url, args.os_auth_version) - - if args.os_auth_url and 'v2.0' in args.os_auth_url: - args.os_auth_url = string.replace(args.os_auth_url, 'v2.0', 'v3') - - kwargs = { - 'username': args.os_username, - 'password': args.os_password, - 'token': args.os_auth_token, - 'auth_url': args.os_auth_url, - 'service_type': args.os_service_type, - 'endpoint_type': args.os_endpoint_type, - 'os_cacert': args.os_cacert, - 'user_domain_id': args.os_user_domain_id, - 'user_domain_name': args.os_user_domain_name, - 'project_id': args.os_project_id, - 'project_name': args.os_project_name, - # if project name is not scoped, fall back to previous behaviour (see above) - 'project_domain_id': args.os_project_domain_id if args.os_project_domain_id else args.os_domain_id, - 'project_domain_name': args.os_project_domain_name if args.os_project_domain_name else args.os_domain_name, - 'insecure': args.insecure, - 'region_name': args.os_region_name, - 'keystone_timeout': args.keystone_timeout - } - - endpoint = args.monasca_api_url - - if not args.os_no_client_auth: - _ksclient = ksclient.KSClient(**kwargs) - if args.os_auth_token: - token = args.os_auth_token - else: - try: - token = _ksclient.token - except exc.CommandError: - raise exc.CommandError( - "User does not have a default project. " - "You must provide a project id using " - "--os-project-id or via env[OS_PROJECT_ID], " - "or you must provide a project name using " - "--os-project-name or via env[OS_PROJECT_NAME] " - "and a project domain using --os-project-domain-name, via " - "env[OS_PROJECT_DOMAIN_NAME], using --os-project-domain-id or " - "via env[OS_PROJECT_DOMAIN_ID]") - - kwargs = { - 'token': token, - 'insecure': args.insecure, - 'os_cacert': args.os_cacert, - 'cert_file': args.cert_file, - 'key_file': args.key_file, - 'username': args.os_username, - 'password': args.os_password, - 'service_type': args.os_service_type, - 'endpoint_type': args.os_endpoint_type, - 'auth_url': args.os_auth_url, - 'keystone_timeout': args.keystone_timeout - } - - if args.os_user_domain_name: - kwargs['user_domain_name'] = args.os_user_domain_name - if args.os_user_domain_id: - kwargs['user_domain_id'] = args.os_user_domain_id - if args.os_region_name: - kwargs['region_name'] = args.os_region_name - if args.os_project_name: - kwargs['project_name'] = args.os_project_name - if args.os_project_id: - kwargs['project_id'] = args.os_project_id - # Monasca API uses domain_id/name for project_domain_id/name - # We cannot change this and therefore still use the misleading parameter names - if args.os_domain_name: - kwargs['domain_name'] = args.os_project_domain_name if args.os_project_domain_name \ - else args.os_domain_name - if args.os_domain_id: - kwargs['domain_id'] = args.os_project_domain_id if args.os_project_domain_id \ - else args.os_domain_id - - if not endpoint: - endpoint = _ksclient.monasca_url - - client = monasca_client.Client(api_version, endpoint, **kwargs) - - args.func(client, args) - - def do_bash_completion(self, args): - """Prints all of the commands and options to stdout. - - The monasca.bash_completion script doesn't have to hard code them. - """ - commands = set() - options = set() - for sc_str, sc in self.subcommands.items(): - commands.add(sc_str) - for option in list(sc._optionals._option_string_actions): - options.add(option) - - commands.remove('bash-completion') - commands.remove('bash_completion') - print(' '.join(commands | options)) - - @utils.arg('command', metavar='', nargs='?', - help='Display help for .') - def do_help(self, args): - """Display help about this program or one of its subcommands.""" - if getattr(args, 'command', None): - 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() - - -class HelpFormatter(argparse.HelpFormatter): - - def start_section(self, heading): - # Title-case the headings - heading = '%s%s' % (heading[0].upper(), heading[1:]) - super(HelpFormatter, self).start_section(heading) + self.command_manager.add_command(name, clazz) def main(args=None): try: if args is None: args = sys.argv[1:] - - MonascaShell().main(args) + if six.PY2: + # Emulate Py3, decode argv into Unicode based on locale so that + # commands always see arguments as text instead of binary data + encoding = locale.getpreferredencoding() + if encoding: + args = map(lambda arg: arg.decode(encoding), args) + MonascaShell().run(args) except Exception as e: if '--debug' in args or '-d' in args: raise else: - print(e, file=sys.stderr) - sys.exit(1) + print(e) + sys.exit(1) if __name__ == "__main__": - main() + sys.exit(main(sys.argv[1:])) diff --git a/monascaclient/tests/fakes.py b/monascaclient/tests/fakes.py deleted file mode 100644 index 21ab666..0000000 --- a/monascaclient/tests/fakes.py +++ /dev/null @@ -1,46 +0,0 @@ -# (C) Copyright 2014,2016 Hewlett Packard Enterprise Development LP -# -# 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 keystoneclient.v3 import client as ksclient - - -def script_keystone_client(token=None): - if token: - ksclient.Client(auth_url='http://no.where', - insecure=False, - tenant_id='tenant_id', - token=token).AndReturn(FakeKeystone(token, None)) - else: - ksclient.Client(auth_url='http://no.where', - insecure=False, - password='password', - project_name='project_name', - timeout=20, - username='username').AndReturn(FakeKeystone( - 'abcd1234', 'test')) - - -class FakeServiceCatalog(object): - - def url_for(self, endpoint_type, service_type): - return 'http://192.168.1.5:8004/v1/f14b41234' - - -class FakeKeystone(object): - service_catalog = FakeServiceCatalog() - - def __init__(self, auth_token, project_id): - self.auth_token = auth_token - self.project_id = project_id diff --git a/monascaclient/tests/test_shell.py b/monascaclient/tests/test_shell.py index 2a3d702..bed27f1 100644 --- a/monascaclient/tests/test_shell.py +++ b/monascaclient/tests/test_shell.py @@ -1,4 +1,5 @@ # (C) Copyright 2014-2017 Hewlett Packard Enterprise Development LP +# Copyright 2017 FUJITSU LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,440 +14,48 @@ # See the License for the specific language governing permissions and # limitations under the License. -import re -import sys -import warnings +import mock -import fixtures -from keystoneclient.v3 import client as ksclient -from mox3 import mox from oslotest import base -from requests_mock.contrib import fixture as requests_mock_fixture -import six -from monascaclient import exc -import monascaclient.shell -from monascaclient.tests import fakes +from monascaclient import shell -class TestCase(base.BaseTestCase): +class TestMonascaShell(base.BaseTestCase): - def setUp(self): - super(TestCase, self).setUp() + @mock.patch('monascaclient.shell.auth') + def test_should_use_auth_plugin_option_parser(self, auth): + auth.build_auth_plugins_option_parser = apop = mock.Mock() + shell.MonascaShell().run([]) + apop.assert_called_once() - def tearDown(self): - super(TestCase, self).tearDown() - - def set_fake_env(self, fake_env): - client_env = ('OS_USERNAME', 'OS_PASSWORD', 'OS_USER_DOMAIN_ID', - 'OS_USER_DOMAIN_NAME', 'OS_PROJECT_ID', - 'OS_PROJECT_NAME', 'OS_AUTH_URL', 'OS_REGION_NAME', - 'OS_AUTH_TOKEN', 'OS_NO_CLIENT_AUTH', 'OS_SERVICE_TYPE', - 'OS_DOMAIN_NAME', 'OS_DOMAIN_ID', - 'OS_ENDPOINT_TYPE', 'MONASCA_API_URL') - - for key in client_env: - self.useFixture( - fixtures.EnvironmentVariable(key, fake_env.get(key))) - - # required for testing with Python 2.6 - def assertRegexpMatches(self, text, expected_regexp, msg=None): - """Fail the test unless the text matches the regular expression.""" - if isinstance(expected_regexp, six.string_types): - expected_regexp = re.compile(expected_regexp) - if not expected_regexp.search(text): - msg = msg or "Regexp didn't match" - msg = '%s: %r not found in %r' % ( - msg, expected_regexp.pattern, text) - raise self.failureException(msg) - - def shell_error(self, argstr, error_match): - orig = sys.stderr - sys.stderr = six.StringIO() - _shell = monascaclient.shell.MonascaShell() - e = self.assertRaises(Exception, _shell.main, argstr.split()) # noqa - self.assertRegexpMatches(e.__str__(), error_match) - err = sys.stderr.getvalue() - sys.stderr.close() - sys.stderr = orig - return err - - -class ShellBase(TestCase): - - def setUp(self): - super(ShellBase, self).setUp() - self.requests_mock = self.useFixture(requests_mock_fixture.Fixture()) - - self.m = mox.Mox() - self.m.StubOutWithMock(ksclient, 'Client') - self.addCleanup(self.m.VerifyAll) - self.addCleanup(self.m.UnsetStubs) - - # Some tests set exc.verbose = 1, so reset on cleanup - def unset_exc_verbose(): - exc.verbose = 0 - - self.addCleanup(unset_exc_verbose) - - def shell(self, argstr): - orig = sys.stdout - try: - sys.stdout = six.StringIO() - _shell = monascaclient.shell.MonascaShell() - _shell.main(argstr.split()) - self.subcommands = _shell.subcommands.keys() - except SystemExit: - exc_type, exc_value, exc_traceback = sys.exc_info() - self.assertEqual(0, exc_value.code) - finally: - out = sys.stdout.getvalue() - sys.stdout.close() - sys.stdout = orig - - return out - - -class ShellTestCommon(ShellBase): - - def setUp(self): - super(ShellTestCommon, self).setUp() - - def test_help_unknown_command(self): - self.assertRaises(exc.CommandError, self.shell, 'help foofoo') - - def test_help(self): - required = [ - '^usage: monasca', - '(?m)^See "monasca help COMMAND" for help on a specific command', + def test_should_specify_monasca_args(self): + expected_args = [ + '--monasca-api-url', + '--monasca-api-version', + '--monasca_api_url', + '--monasca_api_version', ] - for argstr in ['--help', 'help']: - help_text = self.shell(argstr) - for r in required: - self.assertRegexpMatches(help_text, r) - def test_command_help(self): - output = self.shell('help help') - self.assertIn('usage: monasca help []', output) - subcommands = list(self.subcommands) - for command in subcommands: - if command.replace('_', '-') == 'bash-completion': - continue - output1 = self.shell('help %s' % command) - output2 = self.shell('%s --help' % command) - self.assertEqual(output1, output2) - self.assertRegexpMatches(output1, '^usage: monasca %s' % command) + parser = mock.Mock() + parser.add_argument = aa = mock.Mock() + shell.MonascaShell._append_monasca_args(parser) - def test_help_on_subcommand(self): - required = [ - '^usage: monasca metric-create', - "(?m)^Create metric", - ] - argstrings = [ - 'help metric-create', - ] - for argstr in argstrings: - help_text = self.shell(argstr) - for r in required: - self.assertRegexpMatches(help_text, r) + aa.assert_called() + for mc in aa.mock_calls: + name = mc[1][0] + self.assertIn(name, expected_args) - def test_deprecated_warning(self): - argrequired = [('--help --os-tenant-name=this', '--os-tenant-name is deprecated'), - ('--help --os-tenant-id=this', '--os-tenant-id is deprecated')] - for argstr, required in argrequired: - with warnings.catch_warnings(record=True) as w: - self.shell(argstr) - self.assertEqual(str(w[0].message), required) - self.assertEqual(w[0].category, DeprecationWarning) + @mock.patch('monascaclient.shell.importutils') + def test_should_load_commands_based_on_api_version(self, iu): + iu.import_versioned_module = ivm = mock.Mock() + instance = shell.MonascaShell() + instance.options = mock.Mock() + instance.options.monasca_api_version = version = mock.Mock() -class ShellTestMonascaCommands(ShellBase): + instance._find_actions = mock.Mock() - def setUp(self): - super(ShellTestMonascaCommands, self).setUp() - self._set_fake_env() + instance._load_commands() - def assertHeaders(self, req=None, **kwargs): - if not req: - req = self.requests_mock.last_request - - self.assertEqual('abcd1234', req.headers['X-Auth-Token']) - self.assertEqual('python-monascaclient', req.headers['User-Agent']) - - for k, v in kwargs.items(): - self.assertEqual(v, req.headers[k]) - - def _set_fake_env(self): - fake_env = { - 'OS_USERNAME': 'username', - 'OS_PASSWORD': 'password', - 'OS_PROJECT_NAME': 'project_name', - 'OS_AUTH_URL': 'http://no.where', - } - self.set_fake_env(fake_env) - - def _script_keystone_client(self): - fakes.script_keystone_client() - - def test_bad_metrics_create_subcommand(self): - argstrings = [ - 'metric-create metric1', - 'metric-create 123', - 'metric-create', - ] - _shell = monascaclient.shell.MonascaShell() - for argstr in argstrings: - self.assertRaises(SystemExit, _shell.main, argstr.split()) - - def test_good_metrics_create_subcommand(self): - self._script_keystone_client() - self.m.ReplayAll() - - headers = {'location': 'http://no.where/v2.0/metrics'} - self.requests_mock.post('http://192.168.1.5:8004/v1/f14b41234/metrics', - status_code=204, - headers=headers) - - argstrings = [ - 'metric-create metric1 123 --time 1395691090', - ] - for argstr in argstrings: - retvalue = self.shell(argstr) - self.assertRegexpMatches(retvalue, "^Success") - - data = {'timestamp': 1395691090, - 'name': 'metric1', - 'value': 123.0} - - self.assertHeaders() - self.assertEqual(data, self.requests_mock.last_request.json()) - - def test_good_metrics_create_subcommand_with_tenant_id(self): - self._script_keystone_client() - self.m.ReplayAll() - - headers = {'location': 'http://no.where/v2.0/metrics'} - self.requests_mock.post('http://192.168.1.5:8004/v1/f14b41234/metrics', - status_code=204, - headers=headers) - - proj = 'd48e63e76a5c4e05ba26a1185f31d4aa' - argstrings = [ - 'metric-create metric1 123 --time 1395691090 --project-id ' + proj, - ] - for argstr in argstrings: - retvalue = self.shell(argstr) - self.assertRegexpMatches(retvalue, "^Success") - - data = {'timestamp': 1395691090, - 'name': 'metric1', - 'value': 123.0} - - self.assertHeaders() - self.assertEqual(data, self.requests_mock.last_request.json()) - - request_url = self.requests_mock.last_request.url - query_arg = request_url[request_url.index('?') + 1:] - self.assertEqual('tenant_id=' + proj, query_arg) - - def test_bad_notifications_create_missing_args_subcommand(self): - argstrings = [ - 'notification-create email1 metric1@hp.com', - ] - _shell = monascaclient.shell.MonascaShell() - for argstr in argstrings: - self.assertRaises(SystemExit, _shell.main, argstr.split()) - - def test_good_notifications_create_subcommand(self): - self._script_keystone_client() - self.m.ReplayAll() - - url = 'http://192.168.1.5:8004/v1/f14b41234/notification-methods' - headers = {'location': 'http://no.where/v2.0/notification-methods', - 'Content-Type': 'application/json'} - self.requests_mock.post(url, - status_code=201, - headers=headers, - json='id') - - argstrings = [ - 'notification-create email1 EMAIL john.doe@hp.com', - ] - for argstr in argstrings: - retvalue = self.shell(argstr) - self.assertRegexpMatches(retvalue, "id") - - data = {'name': 'email1', - 'type': 'EMAIL', - 'address': 'john.doe@hp.com'} - - self.assertHeaders() - self.assertEqual(data, self.requests_mock.last_request.json()) - - def test_good_notifications_create_subcommand_webhook(self): - self._script_keystone_client() - self.m.ReplayAll() - - url = 'http://192.168.1.5:8004/v1/f14b41234/notification-methods' - headers = {'location': 'http://no.where/v2.0/notification-methods', - 'Content-Type': 'application/json'} - self.requests_mock.post(url, - status_code=201, - headers=headers, - json='id') - - argstrings = [ - 'notification-create mypost WEBHOOK http://localhost:8080', - ] - for argstr in argstrings: - retvalue = self.shell(argstr) - self.assertRegexpMatches(retvalue, "id") - - data = {'name': 'mypost', - 'type': 'WEBHOOK', - 'address': 'http://localhost:8080'} - - self.assertHeaders() - self.assertEqual(data, self.requests_mock.last_request.json()) - - def test_good_notifications_patch(self): - args = '--type EMAIL --address john.doe@hpe.com --period 0' - data = {'type': 'EMAIL', - 'address': 'john.doe@hpe.com', - 'period': 0} - self.run_notification_patch_test(args, data) - - def test_good_notifications_patch_just_name(self): - name = 'fred' - args = '--name ' + name - data = {'name': name} - self.run_notification_patch_test(args, data) - - def test_good_notifications_patch_just_address(self): - address = 'fred@fl.com' - args = '--address ' + address - data = {'address': address} - self.run_notification_patch_test(args, data) - - def test_good_notifications_patch_just_period(self): - period = 0 - args = '--period ' + str(period) - data = {'period': period} - self.run_notification_patch_test(args, data) - - def run_notification_patch_test(self, args, data): - self._script_keystone_client() - self.m.ReplayAll() - - id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96490' - url = 'http://192.168.1.5:8004/v1/f14b41234/notification-methods/' - headers = {'location': 'http://no.where/v2.0/notification-methods', - 'Content-Type': 'application/json'} - - self.requests_mock.patch(url + id_str, - status_code=201, - headers=headers, - json='id') - - argstring = 'notification-patch {0} {1}'.format(id_str, args) - retvalue = self.shell(argstring) - self.assertRegexpMatches(retvalue, "id") - - self.assertHeaders() - self.assertEqual(data, self.requests_mock.last_request.json()) - - def test_bad_notifications_patch(self): - self._script_keystone_client() - self.m.ReplayAll() - - id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96490' - argstring = 'notification-patch {0} --type EMAIL --address' \ - ' john.doe@hpe.com --period 60'.format(id_str) - - retvalue = self.shell(argstring) - self.assertRegexpMatches(retvalue, "^Invalid") - - def test_good_notifications_update(self): - self._script_keystone_client() - self.m.ReplayAll() - - id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96491' - url = 'http://192.168.1.5:8004/v1/f14b41234/notification-methods/' - headers = {'location': 'http://no.where/v2.0/notification-methods', - 'Content-Type': 'application/json'} - - self.requests_mock.put(url + id_str, - status_code=201, - headers=headers, - json='id') - - argstring = 'notification-update {0} notification_updated_name ' \ - 'EMAIL john.doe@hpe.com 0'.format(id_str) - retvalue = self.shell(argstring) - self.assertRegexpMatches(retvalue, "id") - - data = {'name': 'notification_updated_name', - 'type': 'EMAIL', - 'address': 'john.doe@hpe.com', - 'period': 0} - - self.assertHeaders() - self.assertEqual(data, self.requests_mock.last_request.json()) - - def test_good_alarm_definition_update(self): - self._script_keystone_client() - self.m.ReplayAll() - - id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96490' - url = 'http://192.168.1.5:8004/v1/f14b41234/alarm-definitions/' - headers = {'location': 'http://no.where/v2.0/notification-methods', - 'Content-Type': 'application/json'} - - self.requests_mock.put(url + id_str, - status_code=201, - headers=headers, - json='id') - - cmd = 'alarm-definition-update' - name = 'alarm_name' - description = 'test_alarm_definition' - expression = 'avg(Test_Metric_1)>=10' - notif_id = '16012650-0b62-4692-9103-2d04fe81cc93' - enabled = 'True' - match_by = 'hostname' - severity = 'CRITICAL' - - args = [cmd, id_str, name, description, expression, notif_id, - notif_id, notif_id, enabled, match_by, severity] - argstring = " ".join(args) - retvalue = self.shell(argstring) - self.assertRegexpMatches(retvalue, "id") - - data = {'name': name, - 'description': description, - 'expression': expression, - 'alarm_actions': [notif_id], - 'undetermined_actions': [notif_id], - 'ok_actions': [notif_id], - 'match_by': [match_by], - 'actions_enabled': bool(enabled), - 'severity': severity} - - self.assertHeaders() - self.assertEqual(data, self.requests_mock.last_request.json()) - - def test_notifications_types_list(self): - self._script_keystone_client() - self.m.ReplayAll() - - url = 'http://192.168.1.5:8004/v1/f14b41234/notification-methods/' - headers = {'Content-Type': 'application/json'} - body = [{"type": "WEBHOOK"}, {"type": "EMAIL"}, {"type": "PAGERDUTY"}] - self.requests_mock.get(url + 'types', headers=headers, json=body) - - argstrings = ["notification-type-list"] - - retvalue = self.shell("".join(argstrings)) - self.assertRegexpMatches(retvalue, "types") - - self.assertHeaders() + ivm.assert_called_once_with('monascaclient', version, 'shell') diff --git a/monascaclient/tests/v2_0/__init__.py b/monascaclient/tests/v2_0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/monascaclient/tests/v2_0/shell/__init__.py b/monascaclient/tests/v2_0/shell/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/monascaclient/tests/v2_0/shell/test_alarm_definitions.py b/monascaclient/tests/v2_0/shell/test_alarm_definitions.py new file mode 100644 index 0000000..a01fe10 --- /dev/null +++ b/monascaclient/tests/v2_0/shell/test_alarm_definitions.py @@ -0,0 +1,134 @@ +# Copyright 2017 FUJITSU LIMITED +# +# 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 mock + +from oslotest import base + +from monascaclient.osc import migration as migr +from monascaclient.v2_0 import alarm_definitions as ad +from monascaclient.v2_0 import shell + + +class FakeV2Client(object): + def __init__(self): + super(FakeV2Client, self).__init__() + self.alarm_definitions = mock.Mock( + spec=ad.AlarmDefinitionsManager) + + +class TestAlarmDefinitionShellV2(base.BaseTestCase): + + @mock.patch('monascaclient.osc.migration.make_client') + def test_should_update(self, mc): + mc.return_value = c = FakeV2Client() + + ad_id = '0495340b-58fd-4e1c-932b-5e6f9cc96490' + ad_name = 'alarm_name' + ad_desc = 'test_alarm_definition' + ad_expr = 'avg(Test_Metric_1)>=10' + ad_action_id = '16012650-0b62-4692-9103-2d04fe81cc93' + ad_action_enabled = 'True' + ad_match_by = 'hostname' + ad_severity = 'CRITICAL' + + raw_args = [ + ad_id, ad_name, ad_desc, ad_expr, + ad_action_id, ad_action_id, ad_action_id, ad_action_enabled, + ad_match_by, ad_severity + ] + name, cmd_clazz = migr.create_command_class( + 'do_alarm_definition_update', + shell + ) + cmd = cmd_clazz(mock.Mock(), mock.Mock()) + + parser = cmd.get_parser(name) + parsed_args = parser.parse_args(raw_args) + cmd.run(parsed_args) + + c.alarm_definitions.update.assert_called_once_with( + actions_enabled=True, + alarm_actions=[ad_action_id], + alarm_id=ad_id, + description=ad_desc, + expression=ad_expr, + match_by=[ad_match_by], + name=ad_name, + ok_actions=[ad_action_id], + severity=ad_severity, + undetermined_actions=[ad_action_id] + ) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_should_patch_name(self, mc): + ad_id = '0495340b-58fd-4e1c-932b-5e6f9cc96490' + ad_name = 'patch_name' + + raw_args = '{0} --name {1}'.format(ad_id, ad_name).split(' ') + self._patch_test(mc, raw_args, alarm_id=ad_id, name=ad_name) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_should_patch_actions(self, mc): + ad_id = '0495340b-58fd-4e1c-932b-5e6f9cc96490' + ad_action_id = '16012650-0b62-4692-9103-2d04fe81cc93' + + actions = ['alarm-actions', 'ok-actions', + 'undetermined-actions'] + for action in actions: + raw_args = ('{0} --{1} {2}'.format(ad_id, action, ad_action_id) + .split(' ')) + self._patch_test(mc, raw_args, **{ + 'alarm_id': ad_id, + action.replace('-', '_'): [ad_action_id] + }) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_should_patch_severity(self, mc): + ad_id = '0495340b-58fd-4e1c-932b-5e6f9cc96490' + + severity_types = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] + for st in severity_types: + raw_args = ('{0} --severity {1}'.format(ad_id, st) + .split(' ')) + self._patch_test(mc, raw_args, alarm_id=ad_id, severity=st) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_should_not_patch_unknown_severity(self, mc): + ad_id = '0495340b-58fd-4e1c-932b-5e6f9cc96490' + + st = 'foo' + raw_args = ('{0} --severity {1}'.format(ad_id, st) + .split(' ')) + self._patch_test(mc, raw_args, called=False) + + @staticmethod + def _patch_test(mc, args, called=True, **kwargs): + mc.return_value = c = FakeV2Client() + + name, cmd_clazz = migr.create_command_class( + 'do_alarm_definition_patch', + shell + ) + + cmd = cmd_clazz(mock.Mock(), mock.Mock()) + + parser = cmd.get_parser(name) + parsed_args = parser.parse_args(args) + cmd.run(parsed_args) + + if called: + c.alarm_definitions.patch.assert_called_once_with(**kwargs) + else: + c.alarm_definitions.patch.assert_not_called() diff --git a/monascaclient/tests/v2_0/shell/test_metrics.py b/monascaclient/tests/v2_0/shell/test_metrics.py new file mode 100644 index 0000000..fd46530 --- /dev/null +++ b/monascaclient/tests/v2_0/shell/test_metrics.py @@ -0,0 +1,85 @@ +# Copyright 2017 FUJITSU LIMITED +# +# 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 mock + +from oslotest import base + +from monascaclient.osc import migration as migr +from monascaclient.v2_0 import metrics +from monascaclient.v2_0 import shell + + +class FakeV2Client(object): + def __init__(self): + super(FakeV2Client, self).__init__() + self.metrics = mock.Mock(spec=metrics.MetricsManager) + + +class TestMetricsShellV2(base.BaseTestCase): + + def test_bad_metrics(self): + raw_args_list = [ + ['metric1'], + ['123'], + [''] + ] + name, cmd_clazz = migr.create_command_class('do_metric_create', + shell) + + for raw_args in raw_args_list: + cmd = cmd_clazz(mock.Mock(), mock.Mock()) + parser = cmd.get_parser(name) + self.assertRaises(SystemExit, parser.parse_args, raw_args) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_metric_create(self, mc): + mc.return_value = c = FakeV2Client() + + raw_args = 'metric1 123 --time 1395691090'.split(' ') + name, cmd_clazz = migr.create_command_class('do_metric_create', + shell) + cmd = cmd_clazz(mock.Mock(), mock.Mock()) + + parser = cmd.get_parser(name) + parsed_args = parser.parse_args(raw_args) + cmd.run(parsed_args) + + data = {'timestamp': 1395691090, + 'name': 'metric1', + 'value': 123.0} + + c.metrics.create.assert_called_once_with(**data) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_metric_create_with_project_id(self, mc): + mc.return_value = c = FakeV2Client() + + project_id = 'd48e63e76a5c4e05ba26a1185f31d4aa' + raw_args = ('metric1 123 --time 1395691090 --project-id %s' + % project_id).split(' ') + name, cmd_clazz = migr.create_command_class('do_metric_create', + shell) + cmd = cmd_clazz(mock.Mock(), mock.Mock()) + + parser = cmd.get_parser(name) + parsed_args = parser.parse_args(raw_args) + cmd.run(parsed_args) + + data = {'timestamp': 1395691090, + 'name': 'metric1', + 'tenant_id': project_id, + 'value': 123.0} + + c.metrics.create.assert_called_once_with(**data) diff --git a/monascaclient/tests/v2_0/shell/test_notification_types.py b/monascaclient/tests/v2_0/shell/test_notification_types.py new file mode 100644 index 0000000..80919dc --- /dev/null +++ b/monascaclient/tests/v2_0/shell/test_notification_types.py @@ -0,0 +1,52 @@ +# Copyright 2017 FUJITSU LIMITED +# +# 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 mock + +from oslotest import base + +from monascaclient.osc import migration as migr +from monascaclient.v2_0 import notificationtypes +from monascaclient.v2_0 import shell + + +class FakeV2Client(object): + + def __init__(self): + super(FakeV2Client, self).__init__() + self.notificationtypes = mock.Mock( + spec=notificationtypes.NotificationTypesManager) + + +class TestNotificationsTypesShellV2(base.BaseTestCase): + + @mock.patch('monascaclient.osc.migration.make_client') + def test_notification_types_list(self, mc): + mc.return_value = c = FakeV2Client() + c.notificationtypes.list.return_value = [ + {"type": "WEBHOOK"}, + {"type": "EMAIL"}, + {"type": "PAGERDUTY"} + ] + + raw_args = [] + name, cmd_clazz = migr.create_command_class('do_notification_type_list', + shell) + cmd = cmd_clazz(mock.Mock(), mock.Mock()) + + parser = cmd.get_parser(name) + parsed_args = parser.parse_args(raw_args) + cmd.run(parsed_args) + + c.notificationtypes.list.assert_called_once() diff --git a/monascaclient/tests/v2_0/shell/test_notifications.py b/monascaclient/tests/v2_0/shell/test_notifications.py new file mode 100644 index 0000000..1c7015c --- /dev/null +++ b/monascaclient/tests/v2_0/shell/test_notifications.py @@ -0,0 +1,160 @@ +# Copyright 2017 FUJITSU LIMITED +# +# 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 mock + +from oslotest import base + +from monascaclient.osc import migration as migr +from monascaclient.v2_0 import notifications +from monascaclient.v2_0 import shell + + +class FakeV2Client(object): + + def __init__(self): + super(FakeV2Client, self).__init__() + self.notifications = mock.Mock(spec=notifications.NotificationsManager) + + +class TestNotificationsShellV2(base.BaseTestCase): + + @mock.patch('monascaclient.osc.migration.make_client') + def test_notification_create_email(self, mc): + mc.return_value = c = FakeV2Client() + + raw_args = ['email1', 'EMAIL', 'john.doe@hp.com'] + name, cmd_clazz = migr.create_command_class('do_notification_create', + shell) + cmd = cmd_clazz(mock.Mock(), mock.Mock()) + + parser = cmd.get_parser(name) + parsed_args = parser.parse_args(raw_args) + cmd.run(parsed_args) + + data = {'name': 'email1', + 'type': 'EMAIL', + 'address': 'john.doe@hp.com'} + + c.notifications.create.assert_called_once_with(**data) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_notification_create_webhook(self, mc): + mc.return_value = c = FakeV2Client() + + raw_args = ['mypost', 'WEBHOOK', 'http://localhost:8080'] + name, cmd_clazz = migr.create_command_class('do_notification_create', + shell) + cmd = cmd_clazz(mock.Mock(), mock.Mock()) + + parser = cmd.get_parser(name) + parsed_args = parser.parse_args(raw_args) + cmd.run(parsed_args) + + data = {'name': 'mypost', + 'type': 'WEBHOOK', + 'address': 'http://localhost:8080'} + + c.notifications.create.assert_called_once_with(**data) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_good_notifications_patch(self, mc): + args = '--type EMAIL --address john.doe@hpe.com --period 0' + data = {'type': 'EMAIL', + 'address': 'john.doe@hpe.com', + 'period': 0} + self._patch_test(mc, args, data) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_good_notifications_patch_just_name(self, mc): + name = 'fred' + args = '--name ' + name + data = {'name': name} + self._patch_test(mc, args, data) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_good_notifications_patch_just_address(self, mc): + address = 'fred@fl.com' + args = '--address ' + address + data = {'address': address} + self._patch_test(mc, args, data) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_good_notifications_patch_just_period(self, mc): + period = 0 + args = '--period ' + str(period) + data = {'period': period} + self._patch_test(mc, args, data) + + @mock.patch('monascaclient.osc.migration.make_client') + def test_bad_notifications_patch(self, mc): + mc.return_value = c = FakeV2Client() + + id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96490' + raw_args = ('{0} --type EMAIL --address john.doe@hpe.com ' + '--period 60').format(id_str).split(' ') + name, cmd_clazz = migr.create_command_class('do_notification_patch', + shell) + cmd = cmd_clazz(mock.Mock(), mock.Mock()) + + parser = cmd.get_parser(name) + parsed_args = parser.parse_args(raw_args) + cmd.run(parsed_args) + + c.notifications.patch.assert_not_called() + + @mock.patch('monascaclient.osc.migration.make_client') + def test_good_notifications_update(self, mc): + mc.return_value = c = FakeV2Client() + + id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96491' + raw_args = ('{0} notification_updated_name ' + 'EMAIL john.doe@hpe.com 0').format(id_str).split(' ') + name, cmd_clazz = migr.create_command_class('do_notification_update', + shell) + cmd = cmd_clazz(mock.Mock(), mock.Mock()) + + parser = cmd.get_parser(name) + parsed_args = parser.parse_args(raw_args) + + cmd.run(parsed_args) + + data = { + 'name': 'notification_updated_name', + 'type': 'EMAIL', + 'address': 'john.doe@hpe.com', + 'period': 0, + 'notification_id': id_str + } + + c.notifications.update.assert_called_once_with(**data) + + @staticmethod + def _patch_test(mc, args, data): + mc.return_value = c = FakeV2Client() + + id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96490' + raw_args = '{0} {1}'.format(id_str, args).split(' ') + name, cmd_clazz = migr.create_command_class('do_notification_patch', + shell) + cmd = cmd_clazz(mock.Mock(), mock.Mock()) + + parser = cmd.get_parser(name) + parsed_args = parser.parse_args(raw_args) + cmd.run(parsed_args) + + # add notification_id to data + data['notification_id'] = id_str + + c.notifications.patch.assert_called_once_with(**data) diff --git a/monascaclient/v2_0/alarm_definitions.py b/monascaclient/v2_0/alarm_definitions.py index bca0ce4..d604679 100644 --- a/monascaclient/v2_0/alarm_definitions.py +++ b/monascaclient/v2_0/alarm_definitions.py @@ -1,4 +1,5 @@ # (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP +# Copyright 2017 FUJITSU LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,31 +14,27 @@ # See the License for the specific language governing permissions and # limitations under the License. -from monascaclient.apiclient import base from monascaclient.common import monasca_manager -class AlarmDefinitions(base.Resource): - - def __repr__(self): - return "" % self._info - - class AlarmDefinitionsManager(monasca_manager.MonascaManager): - resource_class = AlarmDefinitions base_url = '/alarm-definitions' def create(self, **kwargs): """Create an alarm definition.""" - resp, body = self.client.json_request('POST', self.base_url, - data=kwargs) - return body + resp = self.client.create(url=self.base_url, + json=kwargs) + return resp def get(self, **kwargs): """Get the details for a specific alarm definition.""" - url_str = self.base_url + '/%s' % kwargs['alarm_id'] - resp, body = self.client.json_request('GET', url_str) - return body + + # NOTE(trebskit) should actually be find_one, but + # monasca does not support expected response format + + url = '%s/%s' % (self.base_url, kwargs['alarm_id']) + resp = self.client.list(path=url) + return resp def list(self, **kwargs): """Get a list of alarm definitions.""" @@ -46,19 +43,27 @@ class AlarmDefinitionsManager(monasca_manager.MonascaManager): def delete(self, **kwargs): """Delete a specific alarm definition.""" url_str = self.base_url + '/%s' % kwargs['alarm_id'] - resp, body = self.client.json_request('DELETE', url_str) + resp = self.client.delete(url_str) return resp def update(self, **kwargs): """Update a specific alarm definition.""" url_str = self.base_url + '/%s' % kwargs['alarm_id'] del kwargs['alarm_id'] - resp, body = self.client.json_request('PUT', url_str, data=kwargs) - return body + + resp = self.client.create(url=url_str, + method='PUT', + json=kwargs) + + return resp def patch(self, **kwargs): """Patch a specific alarm definition.""" url_str = self.base_url + '/%s' % kwargs['alarm_id'] del kwargs['alarm_id'] - resp, body = self.client.json_request('PATCH', url_str, data=kwargs) - return body + + resp = self.client.create(url=url_str, + method='PATCH', + json=kwargs) + + return resp diff --git a/monascaclient/v2_0/alarms.py b/monascaclient/v2_0/alarms.py index 0ded894..fc51644 100644 --- a/monascaclient/v2_0/alarms.py +++ b/monascaclient/v2_0/alarms.py @@ -1,4 +1,5 @@ # (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company LP +# Copyright 2017 FUJITSU LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,25 +16,21 @@ from six.moves.urllib import parse -from monascaclient.apiclient import base from monascaclient.common import monasca_manager -class Alarms(base.Resource): - - def __repr__(self): - return "" % self._info - - class AlarmsManager(monasca_manager.MonascaManager): - resource_class = Alarms base_url = '/alarms' def get(self, **kwargs): """Get the details for a specific alarm.""" - url_str = self.base_url + '/%s' % kwargs['alarm_id'] - resp, body = self.client.json_request('GET', url_str) - return body + + # NOTE(trebskit) should actually be find_one, but + # monasca does not support expected response format + + url = '%s/%s' % (self.base_url, kwargs['alarm_id']) + resp = self.client.list(path=url) + return resp def list(self, **kwargs): """Get a list of alarms.""" @@ -42,34 +39,41 @@ class AlarmsManager(monasca_manager.MonascaManager): def delete(self, **kwargs): """Delete a specific alarm.""" url_str = self.base_url + '/%s' % kwargs['alarm_id'] - resp, body = self.client.json_request('DELETE', url_str) + resp = self.client.delete(url_str) return resp def update(self, **kwargs): """Update a specific alarm.""" url_str = self.base_url + '/%s' % kwargs['alarm_id'] del kwargs['alarm_id'] - resp, body = self.client.json_request('PUT', url_str, - data=kwargs) + + body = self.client.create(url=url_str, + method='PUT', + json=kwargs) + return body def patch(self, **kwargs): """Patch a specific alarm.""" url_str = self.base_url + '/%s' % kwargs['alarm_id'] del kwargs['alarm_id'] - resp, body = self.client.json_request('PATCH', url_str, - data=kwargs) - return body + + resp = self.client.create(url=url_str, + method='PATCH', + json=kwargs) + + return resp def count(self, **kwargs): url_str = self.base_url + '/count' if 'metric_dimensions' in kwargs: - dimstr = self.get_dimensions_url_string(kwargs['metric_dimensions']) + dimstr = self.get_dimensions_url_string( + kwargs['metric_dimensions']) kwargs['metric_dimensions'] = dimstr if kwargs: url_str = url_str + '?%s' % parse.urlencode(kwargs, True) - resp, body = self.client.json_request('GET', url_str) + body = self.client.list(url_str) return body def history(self, **kwargs): @@ -78,8 +82,8 @@ class AlarmsManager(monasca_manager.MonascaManager): del kwargs['alarm_id'] if kwargs: url_str = url_str + '?%s' % parse.urlencode(kwargs, True) - resp, body = self.client.json_request('GET', url_str) - return body['elements'] if type(body) is dict else body + resp = self.client.list(url_str) + return resp['elements'] if type(resp) is dict else resp def history_list(self, **kwargs): """History list of alarm state.""" @@ -89,5 +93,5 @@ class AlarmsManager(monasca_manager.MonascaManager): kwargs['dimensions'] = dimstr if kwargs: url_str = url_str + '?%s' % parse.urlencode(kwargs, True) - resp, body = self.client.json_request('GET', url_str) - return body['elements'] if type(body) is dict else body + resp = self.client.list(url_str) + return resp['elements'] if type(resp) is dict else resp diff --git a/monascaclient/v2_0/client.py b/monascaclient/v2_0/client.py index 3031073..33abfbf 100644 --- a/monascaclient/v2_0/client.py +++ b/monascaclient/v2_0/client.py @@ -1,4 +1,5 @@ # (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP +# Copyright 2017 FUJITSU LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,41 +13,28 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -import string -from monascaclient.common import http -from monascaclient.v2_0 import alarm_definitions +from osc_lib.api import api + +from monascaclient.v2_0 import alarm_definitions as ad from monascaclient.v2_0 import alarms from monascaclient.v2_0 import metrics from monascaclient.v2_0 import notifications -from monascaclient.v2_0 import notificationtypes +from monascaclient.v2_0 import notificationtypes as nt class Client(object): - - """Client for the Monasca v2_0 API. - - :param string endpoint: A user-supplied endpoint URL for the monasca api - service. - :param string token: Token for authentication. - :param integer timeout: Allows customization of the timeout for client - http requests. (optional) - """ - def __init__(self, *args, **kwargs): """Initialize a new http client for the monasca API.""" - if 'auth_url' in kwargs and 'v2.0' in kwargs['auth_url']: - kwargs['auth_url'] = string.replace( - kwargs['auth_url'], 'v2.0', 'v3') - self.http_client = http.HTTPClient(*args, **kwargs) - self.metrics = metrics.MetricsManager(self.http_client) - self.notifications = notifications.NotificationsManager( - self.http_client) - self.alarms = alarms.AlarmsManager(self.http_client) - self.alarm_definitions = alarm_definitions.AlarmDefinitionsManager( - self.http_client) - self.notificationtypes = notificationtypes.NotificationTypesManager( - self.http_client) - def replace_token(self, token): - self.http_client.replace_token(token) + client = MonascaApi(*args, **kwargs) + + self.metrics = metrics.MetricsManager(client) + self.notifications = notifications.NotificationsManager(client) + self.alarms = alarms.AlarmsManager(client) + self.alarm_definitions = ad.AlarmDefinitionsManager(client) + self.notificationtypes = nt.NotificationTypesManager(client) + + +class MonascaApi(api.BaseAPI): + SERVICE_TYPE = "monitoring" diff --git a/monascaclient/v2_0/metrics.py b/monascaclient/v2_0/metrics.py index c299f17..b2ec114 100644 --- a/monascaclient/v2_0/metrics.py +++ b/monascaclient/v2_0/metrics.py @@ -1,4 +1,5 @@ # (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP +# Copyright 2017 FUJITSU LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,36 +14,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -from copy import deepcopy - -from monascaclient.apiclient import base from monascaclient.common import monasca_manager -class Metrics(base.Resource): - - def __repr__(self): - return "" % self._info - - class MetricsManager(monasca_manager.MonascaManager): - resource_class = Metrics base_url = '/metrics' def create(self, **kwargs): - local_kwargs = deepcopy(kwargs) """Create a metric.""" url_str = self.base_url - if 'tenant_id' in local_kwargs: - url_str = url_str + '?tenant_id=%s' % local_kwargs['tenant_id'] - del local_kwargs['tenant_id'] - if 'jsonbody' in local_kwargs: - resp, body = self.client.json_request('POST', url_str, - data=local_kwargs['jsonbody']) - else: - resp, body = self.client.json_request('POST', url_str, - data=local_kwargs) - return resp + if 'tenant_id' in kwargs: + url_str = url_str + '?tenant_id=%s' % kwargs['tenant_id'] + del kwargs['tenant_id'] + + data = kwargs['jsonbody'] if 'jsonbody' in kwargs else kwargs + body = self.client.create(url=url_str, json=data) + return body def list(self, **kwargs): """Get a list of metrics.""" diff --git a/monascaclient/v2_0/notifications.py b/monascaclient/v2_0/notifications.py index 93c9be6..a4c5c7f 100644 --- a/monascaclient/v2_0/notifications.py +++ b/monascaclient/v2_0/notifications.py @@ -1,4 +1,5 @@ # (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP +# Copyright 2017 FUJITSU LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,33 +14,27 @@ # See the License for the specific language governing permissions and # limitations under the License. -from copy import deepcopy - -from monascaclient.apiclient import base from monascaclient.common import monasca_manager -class Notifications(base.Resource): - - def __repr__(self): - return "" % self._info - - class NotificationsManager(monasca_manager.MonascaManager): - resource_class = Notifications base_url = '/notification-methods' def create(self, **kwargs): """Create a notification.""" - resp, body = self.client.json_request('POST', self.base_url, - data=kwargs) + body = self.client.create(url=self.base_url, + json=kwargs) return body def get(self, **kwargs): """Get the details for a specific notification.""" - url_str = self.base_url + '/%s' % kwargs['notification_id'] - resp, body = self.client.json_request('GET', url_str) - return body + + # NOTE(trebskit) should actually be find_one, but + # monasca does not support expected response format + + url = '%s/%s' % (self.base_url, kwargs['notification_id']) + resp = self.client.list(path=url) + return resp def list(self, **kwargs): """Get a list of notifications.""" @@ -47,24 +42,27 @@ class NotificationsManager(monasca_manager.MonascaManager): def delete(self, **kwargs): """Delete a notification.""" - url_str = self.base_url + '/%s' % kwargs['notification_id'] - resp, body = self.client.json_request('DELETE', url_str) + url = self.base_url + '/%s' % kwargs['notification_id'] + resp = self.client.delete(url=url) return resp def update(self, **kwargs): - local_kwargs = deepcopy(kwargs) """Update a notification.""" - url_str = self.base_url + '/%s' % local_kwargs['notification_id'] - del local_kwargs['notification_id'] - resp, body = self.client.json_request('PUT', url_str, - data=local_kwargs) - return body + url_str = self.base_url + '/%s' % kwargs['notification_id'] + del kwargs['notification_id'] + + resp = self.client.create(url=url_str, + method='PUT', + json=kwargs) + return resp def patch(self, **kwargs): - local_kwargs = deepcopy(kwargs) """Patch a notification.""" - url_str = self.base_url + '/%s' % local_kwargs['notification_id'] - del local_kwargs['notification_id'] - resp, body = self.client.json_request('PATCH', url_str, - data=local_kwargs) - return body + url_str = self.base_url + '/%s' % kwargs['notification_id'] + del kwargs['notification_id'] + + resp = self.client.create(url=url_str, + method='PATCH', + json=kwargs) + + return resp diff --git a/monascaclient/v2_0/notificationtypes.py b/monascaclient/v2_0/notificationtypes.py index cb3dba1..926736d 100644 --- a/monascaclient/v2_0/notificationtypes.py +++ b/monascaclient/v2_0/notificationtypes.py @@ -1,4 +1,5 @@ # (C) Copyright 2016 Hewlett Packard Enterprise Development LP +# Copyright 2017 FUJITSU LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,18 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from monascaclient.apiclient import base from monascaclient.common import monasca_manager -class NotificationTypes(base.Resource): - - def __repr__(self): - return "" % self._info - - class NotificationTypesManager(monasca_manager.MonascaManager): - resource_class = NotificationTypes base_url = '/notification-methods/types' def list(self, **kwargs): diff --git a/monascaclient/v2_0/shell.py b/monascaclient/v2_0/shell.py index 508e9cb..3eed483 100644 --- a/monascaclient/v2_0/shell.py +++ b/monascaclient/v2_0/shell.py @@ -1,4 +1,5 @@ # (C) Copyright 2014-2017 Hewlett Packard Enterprise Development LP +# Copyright 2017 FUJITSU LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,12 +19,12 @@ import json import numbers import time +from keystoneauth1 import exceptions as k_exc +from osc_lib import exceptions as osc_exc from monascaclient.common import utils -import monascaclient.exc as exc from oslo_serialization import jsonutils -from six.moves import xrange # Alarm valid types severity_types = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] @@ -83,10 +84,8 @@ def do_metric_create(mc, args): fields['tenant_id'] = args.project_id try: mc.metrics.create(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print('Successfully created metric') @@ -96,14 +95,10 @@ def do_metric_create(mc, args): help='The raw JSON body in single quotes. See api doc.') def do_metric_create_raw(mc, args): '''Create metric from raw json body.''' - fields = {} - fields['jsonbody'] = args.jsonbody try: - mc.metrics.create(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + mc.metrics.create(**args.jsonbody) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print('Successfully created metric') @@ -136,17 +131,14 @@ def do_metric_name_list(mc, args): try: metric_names = mc.metrics.list_names(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) - - if args.json: - print(utils.json_formatter(metric_names)) - return - - if isinstance(metric_names, list): - utils.print_list(metric_names, ['Name'], formatters={'Name': lambda x: x['name']}) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) + else: + if args.json: + print(utils.json_formatter(metric_names)) + return + if isinstance(metric_names, list): + utils.print_list(metric_names, ['Name'], formatters={'Name': lambda x: x['name']}) @utils.arg('--name', metavar='', @@ -190,10 +182,8 @@ def do_metric_list(mc, args): try: metric = mc.metrics.list(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: if args.json: print(utils.json_formatter(metric)) @@ -241,10 +231,8 @@ def do_dimension_name_list(mc, args): try: dimension_names = mc.metrics.list_dimension_names(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) if args.json: print(utils.json_formatter(dimension_names)) @@ -283,10 +271,8 @@ def do_dimension_value_list(mc, args): try: dimension_values = mc.metrics.list_dimension_values(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) if args.json: print(utils.json_formatter(dimension_values)) @@ -429,10 +415,8 @@ def do_measurement_list(mc, args): try: metric = mc.metrics.list_measurements(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: if args.json: print(utils.json_formatter(metric)) @@ -496,8 +480,8 @@ def do_metric_statistics(mc, args): if stat.upper() not in statistic_types: errmsg = ('Invalid type, not one of [' + ', '.join(statistic_types) + ']') - print(errmsg) - return + raise osc_exc.CommandError(errmsg) + fields = {} fields['name'] = args.name if args.dimensions: @@ -522,10 +506,8 @@ def do_metric_statistics(mc, args): try: metric = mc.metrics.list_statistics(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: if args.json: print(utils.json_formatter(metric)) @@ -598,10 +580,8 @@ def do_notification_create(mc, args): fields['period'] = args.period try: notification = mc.notifications.create(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print(jsonutils.dumps(notification, indent=2)) @@ -614,10 +594,8 @@ def do_notification_show(mc, args): fields['notification_id'] = args.id try: notification = mc.notifications.get(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: if args.json: print(utils.json_formatter(notification)) @@ -665,9 +643,9 @@ def do_notification_list(mc, args): try: notification = mc.notifications.list(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % + except osc_exc.ClientException as he: + raise osc_exc.CommandError( + 'ClientException code=%s message=%s' % (he.code, he.message)) else: if args.json: @@ -701,10 +679,8 @@ def do_notification_delete(mc, args): fields['notification_id'] = args.id try: mc.notifications.delete(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print('Successfully deleted notification') @@ -732,10 +708,8 @@ def do_notification_update(mc, args): fields['period'] = args.period try: notification = mc.notifications.update(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print(jsonutils.dumps(notification, indent=2)) @@ -768,10 +742,8 @@ def do_notification_patch(mc, args): fields['period'] = args.period try: notification = mc.notifications.patch(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print(jsonutils.dumps(notification, indent=2)) @@ -832,10 +804,8 @@ def do_alarm_definition_create(mc, args): fields['match_by'] = args.match_by.split(',') try: alarm = mc.alarm_definitions.create(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print(jsonutils.dumps(alarm, indent=2)) @@ -848,10 +818,8 @@ def do_alarm_definition_show(mc, args): fields['alarm_id'] = args.id try: alarm = mc.alarm_definitions.get(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: if args.json: print(utils.json_formatter(alarm)) @@ -923,10 +891,8 @@ def do_alarm_definition_list(mc, args): fields['offset'] = args.offset try: alarm = mc.alarm_definitions.list(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: if args.json: print(utils.json_formatter(alarm)) @@ -957,10 +923,8 @@ def do_alarm_definition_delete(mc, args): fields['alarm_id'] = args.id try: mc.alarm_definitions.delete(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print('Successfully deleted alarm definition') @@ -1014,10 +978,8 @@ def do_alarm_definition_update(mc, args): fields['severity'] = args.severity try: alarm = mc.alarm_definitions.update(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print(jsonutils.dumps(alarm, indent=2)) @@ -1075,10 +1037,8 @@ def do_alarm_definition_patch(mc, args): fields['severity'] = args.severity try: alarm = mc.alarm_definitions.patch(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print(jsonutils.dumps(alarm, indent=2)) @@ -1159,10 +1119,8 @@ def do_alarm_list(mc, args): fields['sort_by'] = args.sort_by try: alarm = mc.alarms.list(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: if args.json: print(utils.json_formatter(alarm)) @@ -1202,10 +1160,8 @@ def do_alarm_show(mc, args): fields['alarm_id'] = args.id try: alarm = mc.alarms.get(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: if args.json: print(utils.json_formatter(alarm)) @@ -1243,10 +1199,8 @@ def do_alarm_update(mc, args): fields['link'] = args.link try: alarm = mc.alarms.update(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print(jsonutils.dumps(alarm, indent=2)) @@ -1276,10 +1230,8 @@ def do_alarm_patch(mc, args): fields['link'] = args.link try: alarm = mc.alarms.patch(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print(jsonutils.dumps(alarm, indent=2)) @@ -1292,10 +1244,8 @@ def do_alarm_delete(mc, args): fields['alarm_id'] = args.id try: mc.alarms.delete(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: print('Successfully deleted alarm') @@ -1397,17 +1347,15 @@ def do_alarm_count(mc, args): fields['offset'] = args.offset try: counts = mc.alarms.count(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: if args.json: print(utils.json_formatter(counts)) return cols = counts['columns'] - utils.print_list(counts['counts'], [i for i in xrange(len(cols))], + utils.print_list(counts['counts'], [i for i in range(len(cols))], field_labels=cols) @@ -1427,10 +1375,8 @@ def do_alarm_history(mc, args): fields['offset'] = args.offset try: alarm = mc.alarms.history(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: output_alarm_history(args, alarm) @@ -1466,10 +1412,8 @@ def do_alarm_history_list(mc, args): fields['offset'] = args.offset try: alarm = mc.alarms.history_list(**fields) - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: output_alarm_history(args, alarm) @@ -1479,10 +1423,8 @@ def do_notification_type_list(mc, args): try: notification_types = mc.notificationtypes.list() - except exc.HTTPException as he: - raise exc.CommandError( - 'HTTPException code=%s message=%s' % - (he.code, he.message)) + except (osc_exc.ClientException, k_exc.HttpError) as he: + raise osc_exc.CommandError('%s\n%s' % (he.message, he.details)) else: if args.json: print(utils.json_formatter(notification_types)) diff --git a/monascaclient/version.py b/monascaclient/version.py new file mode 100644 index 0000000..72da508 --- /dev/null +++ b/monascaclient/version.py @@ -0,0 +1,20 @@ +# Copyright 2017 FUJITSU LIMITED +# +# 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 pbr import version + +__all__ = ['version_info', 'version_string'] + +version_info = version.VersionInfo('python-monascaclient') +version_string = version_info.version_string() diff --git a/requirements.txt b/requirements.txt index ae65fcd..b2896ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,16 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. + +osc-lib>=1.5.1 # Apache-2.0 + oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 -python-keystoneclient>=3.8.0 # Apache-2.0 - Babel!=2.4.0,>=2.3.4 # BSD iso8601>=0.1.11 # MIT pbr!=2.1.0,>=2.0.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD PyYAML>=3.10.0 # MIT -requests>=2.14.2 # Apache-2.0 + six>=1.9.0 # MIT diff --git a/test-requirements.txt b/test-requirements.txt index d5d3ebb..6fad002 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,14 +1,12 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. + hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 bandit>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 -fixtures>=3.0.0 # Apache-2.0/BSD -mox3!=0.19.0,>=0.7.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 -requests-mock>=1.1 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT diff --git a/tox.ini b/tox.ini index 05e5a3c..8791863 100644 --- a/tox.ini +++ b/tox.ini @@ -9,22 +9,18 @@ setenv = BRANCH_NAME=master CLIENT_NAME=python-monascaclient OS_TEST_PATH=monascaclient/tests -passenv = http_proxy - HTTP_PROXY - https_proxy - HTTPS_PROXY - no_proxy - NO_PROXY +passenv = *_proxy + *_PROXY usedevelop = True install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt +deps = -r{toxinidir}/test-requirements.txt whitelist_externals = bash find rm commands = find . -type f -name "*.pyc" -delete + rm -Rf .testrepository/times.dbm [testenv:py27] basepython = python2.7 @@ -58,15 +54,21 @@ commands = oslo_debug_helper -t {env:OS_TEST_PATH} {posargs} [testenv:pep8] +skip_install = True +usedevelop = False commands = {[testenv:flake8]commands} {[testenv:bandit]commands} [testenv:flake8] +skip_install = True +usedevelop = False commands = flake8 monascaclient [testenv:bandit] +skip_install = True +usedevelop = False commands = bandit -r monascaclient -n5 -x {env:OS_TEST_PATH}