diff --git a/.gitignore b/.gitignore index f83b264..7a467f7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build .tox cover .testrepository +.stestr .venv dist *.egg diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..9832aa3 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./cloudkittyclient/tests/unit +top_dir=./ diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index f92e428..0000000 --- a/.testr.conf +++ /dev/null @@ -1,4 +0,0 @@ -[DEFAULT] -test_command=${PYTHON:-python} -m subunit.run discover -t ./ ./cloudkittyclient/tests $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 0000000..45a6b79 --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,37 @@ +- job: + name: cloudkittyclient-devstack-functional + parent: devstack + description: | + Job for cloudkittyclient functional tests + pre-run: playbooks/cloudkittyclient-devstack-functional/pre.yaml + run: playbooks/cloudkittyclient-devstack-functional/run.yaml + post-run: playbooks/cloudkittyclient-devstack-functional/post.yaml + required-projects: + - name: openstack/cloudkitty + - name: openstack/python-cloudkittyclient + roles: + - zuul: openstack-infra/devstack + timeout: 5400 + irrelevant-files: + - ^.*\.rst$ + - ^doc/.*$ + - ^releasenotes/.*$ + vars: + devstack_plugins: + cloudkitty: https://git.openstack.org/openstack/cloudkitty + devstack_services: + ck-api: true + horizon: false + tox_install_siblings: false + zuul_work_dir: src/git.openstack.org/openstack/python-cloudkittyclient + tox_envlist: functional + +- project: + check: + jobs: + - cloudkittyclient-devstack-functional: + voting: true + gate: + jobs: + - cloudkittyclient-devstack-functional: + voting: true diff --git a/README.rst b/README.rst index e770270..670775f 100644 --- a/README.rst +++ b/README.rst @@ -2,16 +2,8 @@ CloudKittyClient ================ -.. image:: http://governance.openstack.org/badges/python-cloudkittyclient.svg - :target: http://governance.openstack.org/reference/tags/index.html - -:version: 1.1.0 -:Wiki: `CloudKitty Wiki`_ -:IRC: #cloudkitty @ freenode - - -.. _CloudKitty Wiki: https://wiki.openstack.org/wiki/CloudKitty - +.. image:: https://governance.openstack.org/badges/python-cloudkittyclient.svg + :target: https://governance.openstack.org/reference/tags/index.html This is a client for CloudKitty_. It provides a Python api (the ``cloudkittyclient`` module), a command-line script (``cloudkitty``), and an @@ -21,4 +13,4 @@ The client is available on PyPi_. .. _OpenStack Client: https://docs.openstack.org/python-openstackclient/latest/ .. _CloudKitty: https://github.com/openstack/cloudkitty -.. _PyPi: https://pypi.python.org/pypi/python-cloudkittyclient +.. _PyPi: https://pypi.org/project/python-cloudkittyclient/ diff --git a/cloudkittyclient/apiclient/auth.py b/cloudkittyclient/apiclient/auth.py deleted file mode 100644 index 41c852f..0000000 --- a/cloudkittyclient/apiclient/auth.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# Copyright 2013 Spanish National Research Council. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# E0202: An attribute inherited from %s hide this method -# pylint: disable=E0202 - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -import abc -import argparse -import os - -import six -from stevedore import extension - -from cloudkittyclient.apiclient import exceptions - - -_discovered_plugins = {} - - -def discover_auth_systems(): - """Discover the available auth-systems. - - This won't take into account the old style auth-systems. - """ - global _discovered_plugins - _discovered_plugins = {} - - def add_plugin(ext): - _discovered_plugins[ext.name] = ext.plugin - - ep_namespace = "cloudkittyclient.apiclient.auth" - mgr = extension.ExtensionManager(ep_namespace) - mgr.map(add_plugin) - - -def load_auth_system_opts(parser): - """Load options needed by the available auth-systems into a parser. - - This function will try to populate the parser with options from the - available plugins. - """ - group = parser.add_argument_group("Common auth options") - BaseAuthPlugin.add_common_opts(group) - for name, auth_plugin in six.iteritems(_discovered_plugins): - group = parser.add_argument_group( - "Auth-system '%s' options" % name, - conflict_handler="resolve") - auth_plugin.add_opts(group) - - -def load_plugin(auth_system): - try: - plugin_class = _discovered_plugins[auth_system] - except KeyError: - raise exceptions.AuthSystemNotFound(auth_system) - return plugin_class(auth_system=auth_system) - - -def load_plugin_from_args(args): - """Load required plugin and populate it with options. - - Try to guess auth system if it is not specified. Systems are tried in - alphabetical order. - - :type args: argparse.Namespace - :raises: AuthPluginOptionsMissing - """ - auth_system = args.os_auth_system - if auth_system: - plugin = load_plugin(auth_system) - plugin.parse_opts(args) - plugin.sufficient_options() - return plugin - - for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)): - plugin_class = _discovered_plugins[plugin_auth_system] - plugin = plugin_class() - plugin.parse_opts(args) - try: - plugin.sufficient_options() - except exceptions.AuthPluginOptionsMissing: - continue - return plugin - raise exceptions.AuthPluginOptionsMissing(["auth_system"]) - - -@six.add_metaclass(abc.ABCMeta) -class BaseAuthPlugin(object): - """Base class for authentication plugins. - - An authentication plugin needs to override at least the authenticate - method to be a valid plugin. - """ - - auth_system = None - opt_names = [] - common_opt_names = [ - "auth_system", - "username", - "password", - "tenant_name", - "token", - "auth_url", - ] - - def __init__(self, auth_system=None, **kwargs): - self.auth_system = auth_system or self.auth_system - self.opts = dict((name, kwargs.get(name)) - for name in self.opt_names) - - @staticmethod - def _parser_add_opt(parser, opt): - """Add an option to parser in two variants. - - :param opt: option name (with underscores) - """ - dashed_opt = opt.replace("_", "-") - env_var = "OS_%s" % opt.upper() - arg_default = os.environ.get(env_var, "") - arg_help = "Defaults to env[%s]." % env_var - parser.add_argument( - "--os-%s" % dashed_opt, - metavar="<%s>" % dashed_opt, - default=arg_default, - help=arg_help) - parser.add_argument( - "--os_%s" % opt, - metavar="<%s>" % dashed_opt, - help=argparse.SUPPRESS) - - @classmethod - def add_opts(cls, parser): - """Populate the parser with the options for this plugin.""" - for opt in cls.opt_names: - # use `BaseAuthPlugin.common_opt_names` since it is never - # changed in child classes - if opt not in BaseAuthPlugin.common_opt_names: - cls._parser_add_opt(parser, opt) - - @classmethod - def add_common_opts(cls, parser): - """Add options that are common for several plugins.""" - for opt in cls.common_opt_names: - cls._parser_add_opt(parser, opt) - - @staticmethod - def get_opt(opt_name, args): - """Return option name and value. - - :param opt_name: name of the option, e.g., "username" - :param args: parsed arguments - """ - return (opt_name, getattr(args, "os_%s" % opt_name, None)) - - def parse_opts(self, args): - """Parse the actual auth-system options if any. - - This method is expected to populate the attribute `self.opts` with a - dict containing the options and values needed to make authentication. - """ - self.opts.update(dict(self.get_opt(opt_name, args) - for opt_name in self.opt_names)) - - def authenticate(self, http_client): - """Authenticate using plugin defined method. - - The method usually analyses `self.opts` and performs - a request to authentication server. - - :param http_client: client object that needs authentication - :type http_client: HTTPClient - :raises: AuthorizationFailure - """ - self.sufficient_options() - self._do_authenticate(http_client) - - @abc.abstractmethod - def _do_authenticate(self, http_client): - """Protected method for authentication.""" - - def sufficient_options(self): - """Check if all required options are present. - - :raises: AuthPluginOptionsMissing - """ - missing = [opt - for opt in self.opt_names - if not self.opts.get(opt)] - if missing: - raise exceptions.AuthPluginOptionsMissing(missing) - - @abc.abstractmethod - def token_and_endpoint(self, endpoint_type, service_type): - """Return token and endpoint. - - :param service_type: Service type of the endpoint - :type service_type: string - :param endpoint_type: Type of endpoint. - Possible values: public or publicURL, - internal or internalURL, - admin or adminURL - :type endpoint_type: string - :returns: tuple of token and endpoint strings - :raises: EndpointException - """ diff --git a/cloudkittyclient/apiclient/base.py b/cloudkittyclient/apiclient/base.py deleted file mode 100644 index be4b99e..0000000 --- a/cloudkittyclient/apiclient/base.py +++ /dev/null @@ -1,535 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack Foundation -# Copyright 2012 Grid Dynamics -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Base utilities to build API operation managers and objects on top of. -""" - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - - -# 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 cloudkittyclient.apiclient import exceptions -from cloudkittyclient.i18n import _ - - -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=None, 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'. If response_key is None - all response body - will be used. - :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] if response_key is not None else body - # 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=None): - """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'. If response_key is None - all response body - will be used. - """ - body = self.client.get(url).json() - data = body[response_key] if response_key is not None else body - return self.resource_class(self, data, 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=None, 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., 'server'. If response_key is None - all response body - will be used. - :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() - data = body[response_key] if response_key is not None else body - if return_raw: - return data - return self.resource_class(self, data) - - 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'. If response_key is None - all response body - will be used. - """ - 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'. If response_key is None - all response body - will be used. - """ - body = self.client.patch(url, json=json).json() - if response_key is not None: - return self.resource_class(self, body[response_key]) - else: - return self.resource_class(self, body) - - def _delete(self, url): - """Delete an object. - - :param url: a partial URL, e.g., '/servers/my-server' - """ - return self.client.delete(url) - - -@six.add_metaclass(abc.ABCMeta) -class ManagerWithFind(BaseManager): - """Manager with additional `find()`/`findall()` methods.""" - - @abc.abstractmethod - def list(self): - pass - - def find(self, **kwargs): - """Find a single item with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - matches = self.findall(**kwargs) - num_matches = len(matches) - if num_matches == 0: - msg = _("No %(name)s matching %(args)s.") % { - 'name': self.resource_class.__name__, - 'args': kwargs - } - raise exceptions.NotFound(msg) - elif num_matches > 1: - raise exceptions.NoUniqueMatch() - else: - return matches[0] - - def findall(self, **kwargs): - """Find all items with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - found = [] - searches = kwargs.items() - - for obj in self.list(): - try: - if all(getattr(obj, attr) == value - for (attr, value) in searches): - found.append(obj) - except AttributeError: - continue - - return found - - -class CrudManager(BaseManager): - """Base manager class for manipulating entities. - - Children of this class are expected to define a `collection_key` and `key`. - - - `collection_key`: Usually a plural noun by convention (e.g. `entities`); - used to refer collections in both URL's (e.g. `/v3/entities`) and JSON - objects containing a list of member resources (e.g. `{'entities': [{}, - {}, {}]}`). - - `key`: Usually a singular noun by convention (e.g. `entity`); used to - refer to an individual member of the collection. - - """ - collection_key = None - key = None - - def build_url(self, base_url=None, **kwargs): - """Builds a resource URL for the given kwargs. - - Given an example collection where `collection_key = 'entities'` and - `key = 'entity'`, the following URL's could be generated. - - By default, the URL will represent a collection of entities, e.g.:: - - /entities - - If kwargs contains an `entity_id`, then the URL will represent a - specific member, e.g.:: - - /entities/{entity_id} - - :param base_url: if provided, the generated URL will be appended to it - """ - url = base_url if base_url is not None else '' - - url += '/%s' % self.collection_key - - # do we have a specific entity? - entity_id = kwargs.get('%s_id' % self.key) - if entity_id is not None: - url += '/%s' % entity_id - - return url - - def _filter_kwargs(self, kwargs): - """Drop null values and handle ids.""" - for key, ref in six.iteritems(kwargs.copy()): - if ref is None: - kwargs.pop(key) - else: - if isinstance(ref, Resource): - kwargs.pop(key) - kwargs['%s_id' % key] = getid(ref) - return kwargs - - def create(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._post( - self.build_url(**kwargs), - {self.key: kwargs}, - self.key) - - def get(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._get( - self.build_url(**kwargs), - self.key) - - def head(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._head(self.build_url(**kwargs)) - - def list(self, base_url=None, **kwargs): - """List the collection. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - return self._list( - '%(base_url)s%(query)s' % { - 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) - - def put(self, base_url=None, **kwargs): - """Update an element. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - return self._put(self.build_url(base_url=base_url, **kwargs)) - - def update(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - params = kwargs.copy() - params.pop('%s_id' % self.key) - - return self._patch( - self.build_url(**kwargs), - {self.key: params}, - self.key) - - def delete(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - - return self._delete( - self.build_url(**kwargs)) - - def find(self, base_url=None, **kwargs): - """Find a single item with attributes matching ``**kwargs``. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - rl = self._list( - '%(base_url)s%(query)s' % { - 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) - num = len(rl) - - if num == 0: - msg = _("No %(name)s matching %(args)s.") % { - 'name': self.resource_class.__name__, - 'args': kwargs - } - raise exceptions.NotFound(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.HUMAN_ID: - name = getattr(self, self.NAME_ATTR, None) - if name is not None: - return strutils.to_slug(name) - return None - - def _add_details(self, info): - for (k, v) in six.iteritems(info): - try: - setattr(self, k, v) - self._info[k] = v - except AttributeError: - # In this case we already defined the attribute on the class - pass - - def __getattr__(self, k): - if k not in self.__dict__: - # NOTE(bcwaldon): disallow lazy-loading if already loaded once - if not self.is_loaded(): - self.get() - return self.__getattr__(k) - - raise AttributeError(k) - else: - return self.__dict__[k] - - def get(self): - """Support for lazy loading details. - - Some clients, such as novaclient have the option to lazy load the - details, details which can be loaded with this function. - """ - # set_loaded() first ... so if we have to bail, we know we tried. - self.set_loaded(True) - if not hasattr(self.manager, 'get'): - return - - new = self.manager.get(self.id) - if new: - self._add_details(new._info) - if self.manager.client.last_request_id: - self._add_details( - {'x_request_id': self.manager.client.last_request_id}) - - 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) - - def is_loaded(self): - return self._loaded - - def set_loaded(self, val): - self._loaded = val - - def to_dict(self): - return copy.deepcopy(self._info) diff --git a/cloudkittyclient/apiclient/client.py b/cloudkittyclient/apiclient/client.py deleted file mode 100644 index 1fca998..0000000 --- a/cloudkittyclient/apiclient/client.py +++ /dev/null @@ -1,392 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack Foundation -# Copyright 2011 Piston Cloud Computing, Inc. -# Copyright 2013 Alessio Ababilov -# Copyright 2013 Grid Dynamics -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -OpenStack Client interface. Handles the REST calls and responses. -""" - -# E0202: An attribute inherited from %s hide this method -# pylint: disable=E0202 - -import hashlib -import logging -import time - -try: - import simplejson as json -except ImportError: - import json - -from oslo_utils import encodeutils -from oslo_utils import importutils -import requests - -from cloudkittyclient.apiclient import exceptions -from cloudkittyclient.i18n import _ - - -_logger = logging.getLogger(__name__) -SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',) - - -class HTTPClient(object): - """This client handles sending HTTP requests to OpenStack servers. - - Features: - - - share authentication information between several clients to different - services (e.g., for compute and image clients); - - reissue authentication request for expired tokens; - - encode/decode JSON bodies; - - raise exceptions on HTTP errors; - - pluggable authentication; - - store authentication information in a keyring; - - store time spent for requests; - - register clients for particular services, so one can use - `http_client.identity` or `http_client.compute`; - - log requests and responses in a format that is easy to copy-and-paste - into terminal and send the same request with curl. - """ - - user_agent = "cloudkittyclient.apiclient" - - def __init__(self, - auth_plugin, - region_name=None, - endpoint_type="publicURL", - original_ip=None, - verify=True, - cert=None, - timeout=None, - timings=False, - keyring_saver=None, - debug=False, - user_agent=None, - http=None): - self.auth_plugin = auth_plugin - - self.endpoint_type = endpoint_type - self.region_name = region_name - - self.original_ip = original_ip - self.timeout = timeout - self.verify = verify - self.cert = cert - - self.keyring_saver = keyring_saver - self.debug = debug - self.user_agent = user_agent or self.user_agent - - self.times = [] # [("item", starttime, endtime), ...] - self.timings = timings - - # requests within the same session can reuse TCP connections from pool - self.http = http or requests.Session() - - self.cached_token = None - self.last_request_id = None - - def _safe_header(self, name, value): - if name in SENSITIVE_HEADERS: - # because in python3 byte string handling is ... ug - v = value.encode('utf-8') - h = hashlib.sha1(v) - d = h.hexdigest() - return encodeutils.safe_decode(name), "{SHA1}%s" % d - else: - return (encodeutils.safe_decode(name), - encodeutils.safe_decode(value)) - - def _http_log_req(self, method, url, kwargs): - if not self.debug: - return - - string_parts = [ - "curl -g -i", - "-X '%s'" % method, - "'%s'" % url, - ] - - if not kwargs.get('verify', self.verify): - string_parts.insert(1, '--insecure') - - for element in kwargs['headers']: - header = ("-H '%s: %s'" % - self._safe_header(element, kwargs['headers'][element])) - string_parts.append(header) - - _logger.debug("REQ: %s" % " ".join(string_parts)) - if 'data' in kwargs: - _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) - - def _http_log_resp(self, resp): - if not self.debug: - return - _logger.debug( - "RESP: [%s] %s\n", - resp.status_code, - resp.headers) - if resp._content_consumed: - _logger.debug( - "RESP BODY: %s\n", - resp.text) - - def serialize(self, kwargs): - if kwargs.get('json') is not None: - kwargs['headers']['Content-Type'] = 'application/json' - kwargs['data'] = json.dumps(kwargs['json']) - try: - del kwargs['json'] - except KeyError: - pass - - def get_timings(self): - return self.times - - def reset_timings(self): - self.times = [] - - def request(self, method, url, **kwargs): - """Send an http request with the specified characteristics. - - Wrapper around `requests.Session.request` to handle tasks such as - setting headers, JSON encoding/decoding, and error handling. - - :param method: method of HTTP request - :param url: URL of HTTP request - :param kwargs: any other parameter that can be passed to - requests.Session.request (such as `headers`) or `json` - that will be encoded as JSON and used as `data` argument - """ - kwargs.setdefault("headers", {}) - kwargs["headers"]["User-Agent"] = self.user_agent - if self.original_ip: - kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( - self.original_ip, self.user_agent) - if self.timeout is not None: - kwargs.setdefault("timeout", self.timeout) - kwargs.setdefault("verify", self.verify) - if self.cert is not None: - kwargs.setdefault("cert", self.cert) - self.serialize(kwargs) - - self._http_log_req(method, url, kwargs) - if self.timings: - start_time = time.time() - resp = self.http.request(method, url, **kwargs) - if self.timings: - self.times.append(("%s %s" % (method, url), - start_time, time.time())) - self._http_log_resp(resp) - - self.last_request_id = resp.headers.get('x-openstack-request-id') - - if resp.status_code >= 400: - _logger.debug( - "Request returned failure status: %s", - resp.status_code) - raise exceptions.from_response(resp, method, url) - - return resp - - @staticmethod - def concat_url(endpoint, url): - """Concatenate endpoint and final URL. - - E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to - "http://keystone/v2.0/tokens". - - :param endpoint: the base URL - :param url: the final URL - """ - return "%s/%s" % (endpoint.rstrip("/"), url.strip("/")) - - def client_request(self, client, method, url, **kwargs): - """Send an http request using `client`'s endpoint and specified `url`. - - If request was rejected as unauthorized (possibly because the token is - expired), issue one authorization attempt and send the request once - again. - - :param client: instance of BaseClient descendant - :param method: method of HTTP request - :param url: URL of HTTP request - :param kwargs: any other parameter that can be passed to - `HTTPClient.request` - """ - - filter_args = { - "endpoint_type": client.endpoint_type or self.endpoint_type, - "service_type": client.service_type, - } - token, endpoint = (self.cached_token, client.cached_endpoint) - just_authenticated = False - if not (token and endpoint): - try: - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - except exceptions.EndpointException: - pass - if not (token and endpoint): - self.authenticate() - just_authenticated = True - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - if not (token and endpoint): - raise exceptions.AuthorizationFailure( - _("Cannot find endpoint or token for request")) - - old_token_endpoint = (token, endpoint) - kwargs.setdefault("headers", {})["X-Auth-Token"] = token - self.cached_token = token - client.cached_endpoint = endpoint - # Perform the request once. If we get Unauthorized, then it - # might be because the auth token expired, so try to - # re-authenticate and try again. If it still fails, bail. - try: - return self.request( - method, self.concat_url(endpoint, url), **kwargs) - except exceptions.Unauthorized as unauth_ex: - if just_authenticated: - raise - self.cached_token = None - client.cached_endpoint = None - if self.auth_plugin.opts.get('token'): - self.auth_plugin.opts['token'] = None - if self.auth_plugin.opts.get('endpoint'): - self.auth_plugin.opts['endpoint'] = None - self.authenticate() - try: - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - except exceptions.EndpointException: - raise unauth_ex - if (not (token and endpoint) or - old_token_endpoint == (token, endpoint)): - raise unauth_ex - self.cached_token = token - client.cached_endpoint = endpoint - kwargs["headers"]["X-Auth-Token"] = token - return self.request( - method, self.concat_url(endpoint, url), **kwargs) - - def add_client(self, base_client_instance): - """Add a new instance of :class:`BaseClient` descendant. - - `self` will store a reference to `base_client_instance`. - - Example: - - >>> def test_clients(): - ... from keystoneclient.auth import keystone - ... from openstack.common.apiclient import client - ... auth = keystone.KeystoneAuthPlugin( - ... username="user", password="pass", tenant_name="tenant", - ... auth_url="http://auth:5000/v2.0") - ... openstack_client = client.HTTPClient(auth) - ... # create nova client - ... from novaclient.v1_1 import client - ... client.Client(openstack_client) - ... # create keystone client - ... from keystoneclient.v2_0 import client - ... client.Client(openstack_client) - ... # use them - ... openstack_client.identity.tenants.list() - ... openstack_client.compute.servers.list() - """ - service_type = base_client_instance.service_type - if service_type and not hasattr(self, service_type): - setattr(self, service_type, base_client_instance) - - def authenticate(self): - self.auth_plugin.authenticate(self) - # Store the authentication results in the keyring for later requests - if self.keyring_saver: - self.keyring_saver.save(self) - - -class BaseClient(object): - """Top-level object to access the OpenStack API. - - This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient` - will handle a bunch of issues such as authentication. - """ - - service_type = None - endpoint_type = None # "publicURL" will be used - cached_endpoint = None - - def __init__(self, http_client, extensions=None): - self.http_client = http_client - http_client.add_client(self) - - # Add in any extensions... - if extensions: - for extension in extensions: - if extension.manager_class: - setattr(self, extension.name, - extension.manager_class(self)) - - def client_request(self, method, url, **kwargs): - return self.http_client.client_request( - self, method, url, **kwargs) - - @property - def last_request_id(self): - return self.http_client.last_request_id - - def head(self, url, **kwargs): - return self.client_request("HEAD", url, **kwargs) - - def get(self, url, **kwargs): - return self.client_request("GET", url, **kwargs) - - def post(self, url, **kwargs): - return self.client_request("POST", url, **kwargs) - - def put(self, url, **kwargs): - return self.client_request("PUT", url, **kwargs) - - def delete(self, url, **kwargs): - return self.client_request("DELETE", url, **kwargs) - - def patch(self, url, **kwargs): - return self.client_request("PATCH", url, **kwargs) - - @staticmethod - def get_class(api_name, version, version_map): - """Returns the client class for the requested API version - - :param api_name: the name of the API, e.g. 'compute', 'image', etc - :param version: the requested API version - :param version_map: a dict of client classes keyed by version - :rtype: a client class for the requested API version - """ - try: - client_path = version_map[str(version)] - except (KeyError, ValueError): - msg = _("Invalid %(api_name)s client version '%(version)s'. " - "Must be one of: %(version_map)s") % { - 'api_name': api_name, - 'version': version, - 'version_map': ', '.join(version_map.keys())} - raise exceptions.UnsupportedVersion(msg) - - return importutils.import_class(client_path) diff --git a/cloudkittyclient/apiclient/exceptions.py b/cloudkittyclient/apiclient/exceptions.py deleted file mode 100644 index 0d8e92a..0000000 --- a/cloudkittyclient/apiclient/exceptions.py +++ /dev/null @@ -1,477 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 Nebula, Inc. -# Copyright 2013 Alessio Ababilov -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Exception definitions. -""" - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -import inspect -import sys - -import six - -from cloudkittyclient.i18n import _ - - -class ClientException(Exception): - """The base exception class for all exceptions this library raises.""" - pass - - -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 ConnectionError(ClientException): - """Cannot connect to API service.""" - pass - - -class ConnectionRefused(ConnectionError): - """Connection refused while trying to connect to API service.""" - pass - - -class AuthPluginOptionsMissing(AuthorizationFailure): - """Auth plugin misses some options.""" - def __init__(self, opt_names): - super(AuthPluginOptionsMissing, self).__init__( - _("Authentication failed. Missing options: %s") % - ", ".join(opt_names)) - self.opt_names = opt_names - - -class AuthSystemNotFound(AuthorizationFailure): - """User has specified an AuthSystem that is not installed.""" - def __init__(self, auth_system): - super(AuthSystemNotFound, self).__init__( - _("AuthSystemNotFound: %r") % 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: %r") % endpoints) - self.endpoints = endpoints - - -class HttpError(ClientException): - """The base exception class for all HTTP exceptions.""" - http_status = 0 - message = _("HTTP Error") - - def __init__(self, message=None, details=None, - response=None, request_id=None, - url=None, method=None, http_status=None): - self.http_status = http_status or self.http_status - self.message = message or self.message - self.details = details - self.request_id = request_id - self.response = response - self.url = url - self.method = method - formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) - if request_id: - formatted_string += " (Request-ID: %s)" % request_id - super(HttpError, self).__init__(formatted_string) - - -class HTTPRedirection(HttpError): - """HTTP Redirection.""" - message = _("HTTP Redirection") - - -class HTTPClientError(HttpError): - """Client-side HTTP error. - - Exception for cases in which the client seems to have erred. - """ - message = _("HTTP Client Error") - - -class HttpServerError(HttpError): - """Server-side HTTP error. - - Exception for cases in which the server is aware that it has - erred or is incapable of performing the request. - """ - message = _("HTTP Server Error") - - -class MultipleChoices(HTTPRedirection): - """HTTP 300 - Multiple Choices. - - Indicates multiple options for the resource that the client may follow. - """ - - http_status = 300 - message = _("Multiple Choices") - - -class BadRequest(HTTPClientError): - """HTTP 400 - Bad Request. - - The request cannot be fulfilled due to bad syntax. - """ - http_status = 400 - message = _("Bad Request") - - -class Unauthorized(HTTPClientError): - """HTTP 401 - Unauthorized. - - Similar to 403 Forbidden, but specifically for use when authentication - is required and has failed or has not yet been provided. - """ - http_status = 401 - message = _("Unauthorized") - - -class PaymentRequired(HTTPClientError): - """HTTP 402 - Payment Required. - - Reserved for future use. - """ - http_status = 402 - message = _("Payment Required") - - -class Forbidden(HTTPClientError): - """HTTP 403 - Forbidden. - - The request was a valid request, but the server is refusing to respond - to it. - """ - http_status = 403 - message = _("Forbidden") - - -class NotFound(HTTPClientError): - """HTTP 404 - Not Found. - - The requested resource could not be found but may be available again - in the future. - """ - http_status = 404 - message = _("Not Found") - - -class MethodNotAllowed(HTTPClientError): - """HTTP 405 - Method Not Allowed. - - A request was made of a resource using a request method not supported - by that resource. - """ - http_status = 405 - message = _("Method Not Allowed") - - -class NotAcceptable(HTTPClientError): - """HTTP 406 - Not Acceptable. - - The requested resource is only capable of generating content not - acceptable according to the Accept headers sent in the request. - """ - http_status = 406 - message = _("Not Acceptable") - - -class ProxyAuthenticationRequired(HTTPClientError): - """HTTP 407 - Proxy Authentication Required. - - The client must first authenticate itself with the proxy. - """ - http_status = 407 - message = _("Proxy Authentication Required") - - -class RequestTimeout(HTTPClientError): - """HTTP 408 - Request Timeout. - - The server timed out waiting for the request. - """ - http_status = 408 - message = _("Request Timeout") - - -class Conflict(HTTPClientError): - """HTTP 409 - Conflict. - - Indicates that the request could not be processed because of conflict - in the request, such as an edit conflict. - """ - http_status = 409 - message = _("Conflict") - - -class Gone(HTTPClientError): - """HTTP 410 - Gone. - - Indicates that the resource requested is no longer available and will - not be available again. - """ - http_status = 410 - message = _("Gone") - - -class LengthRequired(HTTPClientError): - """HTTP 411 - Length Required. - - The request did not specify the length of its content, which is - required by the requested resource. - """ - http_status = 411 - message = _("Length Required") - - -class PreconditionFailed(HTTPClientError): - """HTTP 412 - Precondition Failed. - - The server does not meet one of the preconditions that the requester - put on the request. - """ - http_status = 412 - message = _("Precondition Failed") - - -class RequestEntityTooLarge(HTTPClientError): - """HTTP 413 - Request Entity Too Large. - - The request is larger than the server is willing or able to process. - """ - http_status = 413 - message = _("Request Entity Too Large") - - def __init__(self, *args, **kwargs): - try: - self.retry_after = int(kwargs.pop('retry_after')) - except (KeyError, ValueError): - self.retry_after = 0 - - super(RequestEntityTooLarge, self).__init__(*args, **kwargs) - - -class RequestUriTooLong(HTTPClientError): - """HTTP 414 - Request-URI Too Long. - - The URI provided was too long for the server to process. - """ - http_status = 414 - message = _("Request-URI Too Long") - - -class UnsupportedMediaType(HTTPClientError): - """HTTP 415 - Unsupported Media Type. - - The request entity has a media type which the server or resource does - not support. - """ - http_status = 415 - message = _("Unsupported Media Type") - - -class RequestedRangeNotSatisfiable(HTTPClientError): - """HTTP 416 - Requested Range Not Satisfiable. - - The client has asked for a portion of the file, but the server cannot - supply that portion. - """ - http_status = 416 - message = _("Requested Range Not Satisfiable") - - -class ExpectationFailed(HTTPClientError): - """HTTP 417 - Expectation Failed. - - The server cannot meet the requirements of the Expect request-header field. - """ - http_status = 417 - message = _("Expectation Failed") - - -class UnprocessableEntity(HTTPClientError): - """HTTP 422 - Unprocessable Entity. - - The request was well-formed but was unable to be followed due to semantic - errors. - """ - http_status = 422 - message = _("Unprocessable Entity") - - -class InternalServerError(HttpServerError): - """HTTP 500 - Internal Server Error. - - A generic error message, given when no more specific message is suitable. - """ - http_status = 500 - message = _("Internal Server Error") - - -# NotImplemented is a python keyword. -class HttpNotImplemented(HttpServerError): - """HTTP 501 - Not Implemented. - - The server either does not recognize the request method, or it lacks - the ability to fulfill the request. - """ - http_status = 501 - message = _("Not Implemented") - - -class BadGateway(HttpServerError): - """HTTP 502 - Bad Gateway. - - The server was acting as a gateway or proxy and received an invalid - response from the upstream server. - """ - http_status = 502 - message = _("Bad Gateway") - - -class ServiceUnavailable(HttpServerError): - """HTTP 503 - Service Unavailable. - - The server is currently unavailable. - """ - http_status = 503 - message = _("Service Unavailable") - - -class GatewayTimeout(HttpServerError): - """HTTP 504 - Gateway Timeout. - - The server was acting as a gateway or proxy and did not receive a timely - response from the upstream server. - """ - http_status = 504 - message = _("Gateway Timeout") - - -class HttpVersionNotSupported(HttpServerError): - """HTTP 505 - HttpVersion Not Supported. - - The server does not support the HTTP protocol version used in the request. - """ - http_status = 505 - message = _("HTTP Version Not Supported") - - -# _code_map contains all the classes that have http_status attribute. -_code_map = dict( - (getattr(obj, 'http_status', None), obj) - for name, obj in six.iteritems(vars(sys.modules[__name__])) - if inspect.isclass(obj) and getattr(obj, 'http_status', False) -) - - -def from_response(response, method, url): - """Returns an instance of :class:`HttpError` or subclass based on response. - - :param response: instance of `requests.Response` class - :param method: HTTP method used for request - :param url: URL used for request - """ - - req_id = response.headers.get("x-openstack-request-id") - # NOTE(hdd) true for older versions of nova and cinder - if not req_id: - req_id = response.headers.get("x-compute-request-id") - kwargs = { - "http_status": response.status_code, - "response": response, - "method": method, - "url": url, - "request_id": req_id, - } - if "retry-after" in response.headers: - kwargs["retry_after"] = response.headers["retry-after"] - - content_type = response.headers.get("Content-Type", "") - if content_type.startswith("application/json"): - try: - body = response.json() - except ValueError: - pass - else: - if isinstance(body, dict): - error = body.get(list(body)[0]) - if isinstance(error, dict): - kwargs["message"] = (error.get("message") or - error.get("faultstring")) - kwargs["details"] = (error.get("details") or - six.text_type(body)) - elif content_type.startswith("text/"): - kwargs["details"] = getattr(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/cloudkittyclient/apiclient/fake_client.py b/cloudkittyclient/apiclient/fake_client.py deleted file mode 100644 index c100e2b..0000000 --- a/cloudkittyclient/apiclient/fake_client.py +++ /dev/null @@ -1,189 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -A fake server that "responds" to API methods with pre-canned responses. - -All of these responses come from the spec, so if for some reason the spec's -wrong the tests might raise AssertionError. I've indicated in comments the -places where actual behavior differs from the spec. -""" - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -# W0102: Dangerous default value %s as argument -# pylint: disable=W0102 - -import json - -import requests -import six -from six.moves.urllib import parse - -from cloudkittyclient.apiclient import client - - -def assert_has_keys(dct, required=None, optional=None): - required = required or [] - optional = optional or [] - for k in required: - try: - assert k in dct - except AssertionError: - extra_keys = set(dct.keys()).difference(set(required + optional)) - raise AssertionError("found unexpected keys: %s" % - list(extra_keys)) - - -class TestResponse(requests.Response): - """Wrap requests.Response and provide a convenient initialization.""" - - def __init__(self, data): - super(TestResponse, self).__init__() - self._content_consumed = True - if isinstance(data, dict): - self.status_code = data.get('status_code', 200) - # Fake the text attribute to streamline Response creation - text = data.get('text', "") - if isinstance(text, (dict, list)): - self._content = json.dumps(text) - default_headers = { - "Content-Type": "application/json", - } - else: - self._content = text - default_headers = {} - if six.PY3 and isinstance(self._content, six.string_types): - self._content = self._content.encode('utf-8', 'strict') - self.headers = data.get('headers') or default_headers - else: - self.status_code = data - - def __eq__(self, other): - return (self.status_code == other.status_code and - self.headers == other.headers and - self._content == other._content) - - def __ne__(self, other): - return not self.__eq__(other) - - -class FakeHTTPClient(client.HTTPClient): - - def __init__(self, *args, **kwargs): - self.callstack = [] - self.fixtures = kwargs.pop("fixtures", None) or {} - if not args and "auth_plugin" not in kwargs: - args = (None, ) - super(FakeHTTPClient, self).__init__(*args, **kwargs) - - def assert_called(self, method, url, body=None, pos=-1): - """Assert than an API method was just called.""" - expected = (method, url) - called = self.callstack[pos][0:2] - msg = "Expected %s %s but no calls were made." % expected - assert self.callstack, msg - - msg = 'Expected %s %s; got %s %s' % (expected + called) - assert expected == called, msg - - if body is not None: - if self.callstack[pos][3] != body: - raise AssertionError('%r != %r' % - (self.callstack[pos][3], body)) - - def assert_called_anytime(self, method, url, body=None): - """Assert than an API method was called anytime in the test.""" - expected = (method, url) - - msg = "Expected %s %s but no calls were made." % expected - assert self.callstack, msg - - found = False - entry = None - for entry in self.callstack: - if expected == entry[0:2]: - found = True - break - - msg = 'Expected %s %s; got %s' % (method, url, self.callstack) - assert found, msg - if body is not None: - assert entry[3] == body, "%s != %s" % (entry[3], body) - - self.callstack = [] - - def clear_callstack(self): - self.callstack = [] - - def authenticate(self): - pass - - def client_request(self, client, method, url, **kwargs): - # Check that certain things are called correctly - if method in ["GET", "DELETE"]: - assert "json" not in kwargs - - # Note the call - self.callstack.append( - (method, - url, - kwargs.get("headers") or {}, - kwargs.get("json") or kwargs.get("data"))) - try: - fixture = self.fixtures[url][method] - except KeyError: - pass - else: - return TestResponse({"headers": fixture[0], - "text": fixture[1]}) - - # Call the method - args = parse.parse_qsl(parse.urlparse(url)[4]) - kwargs.update(args) - munged_url = url.rsplit('?', 1)[0] - munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') - munged_url = munged_url.replace('-', '_') - - callback = "%s_%s" % (method.lower(), munged_url) - - if not hasattr(self, callback): - raise AssertionError('Called unknown API method: %s %s, ' - 'expected fakes method name: %s' % - (method, url, callback)) - - resp = getattr(self, callback)(**kwargs) - if len(resp) == 3: - status, headers, body = resp - else: - status, body = resp - headers = {} - self.last_request_id = headers.get('x-openstack-request-id') - return TestResponse({ - "status_code": status, - "text": body, - "headers": headers, - }) diff --git a/cloudkittyclient/apiclient/utils.py b/cloudkittyclient/apiclient/utils.py deleted file mode 100644 index a1952d0..0000000 --- a/cloudkittyclient/apiclient/utils.py +++ /dev/null @@ -1,96 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-cloudkittyclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - -from oslo_utils import encodeutils -from oslo_utils import uuidutils -import six - -from cloudkittyclient.apiclient import exceptions -from cloudkittyclient.i18n import _ - - -def find_resource(manager, name_or_id, **find_args): - """Look for resource in a given manager. - - Used as a helper for the _find_* methods. - Example: - - .. code-block:: python - - def _find_hypervisor(cs, hypervisor): - #Get a hypervisor by name or ID. - return cliutils.find_resource(cs.hypervisors, hypervisor) - """ - # first try to get entity as integer id - try: - return manager.get(int(name_or_id)) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # now try to get entity as uuid - try: - if six.PY2: - tmp_id = encodeutils.safe_encode(name_or_id) - else: - tmp_id = encodeutils.safe_decode(name_or_id) - - if uuidutils.is_uuid_like(tmp_id): - return manager.get(tmp_id) - except (TypeError, ValueError, exceptions.NotFound): - pass - - # for str id which is not uuid - if getattr(manager, 'is_alphanum_id_allowed', False): - try: - return manager.get(name_or_id) - except exceptions.NotFound: - pass - - try: - try: - return manager.find(human_id=name_or_id, **find_args) - except exceptions.NotFound: - pass - - # finally try to find entity by name - try: - resource = getattr(manager, 'resource_class', None) - name_attr = resource.NAME_ATTR if resource else 'name' - kwargs = {name_attr: name_or_id} - kwargs.update(find_args) - return manager.find(**kwargs) - except exceptions.NotFound: - msg = _("No %(name)s with a name or " - "ID of '%(name_or_id)s' exists.") % { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id} - raise exceptions.CommandError(msg) - except exceptions.NoUniqueMatch: - msg = _("Multiple %(name)s matches found for " - "'%(name_or_id)s', use an ID to be more specific.") % { - "name": manager.resource_class.__name__.lower(), - "name_or_id": name_or_id} - raise exceptions.CommandError(msg) diff --git a/cloudkittyclient/auth.py b/cloudkittyclient/auth.py new file mode 100644 index 0000000..ac38737 --- /dev/null +++ b/cloudkittyclient/auth.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from keystoneauth1 import loading +from keystoneauth1 import plugin + + +class CloudKittyNoAuthPlugin(plugin.BaseAuthPlugin): + """No authentication plugin for CloudKitty + + """ + def __init__(self, endpoint='http://localhost:8889', *args, **kwargs): + super(CloudKittyNoAuthPlugin, self).__init__() + self._endpoint = endpoint + + def get_auth_ref(self, session, **kwargs): + return None + + def get_endpoint(self, session, **kwargs): + return self._endpoint + + def get_headers(self, session, **kwargs): + return {} + + +class CloudKittyNoAuthLoader(loading.BaseLoader): + plugin_class = CloudKittyNoAuthPlugin + + def get_options(self): + options = super(CloudKittyNoAuthLoader, self).get_options() + options.extend([ + loading.Opt('endpoint', help='CloudKitty Endpoint', + required=True, default='http://localhost:8889'), + ]) + return options diff --git a/cloudkittyclient/client.py b/cloudkittyclient/client.py index 6fb46f8..e807177 100644 --- a/cloudkittyclient/client.py +++ b/cloudkittyclient/client.py @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -9,426 +12,12 @@ # 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 contextlib -import time - -from keystoneclient import adapter -from keystoneclient.auth.identity import v2 as v2_auth -from keystoneclient.auth.identity import v3 as v3_auth -from keystoneclient import discover -from keystoneclient import exceptions as ks_exc -from keystoneclient import session -from oslo_utils import strutils -import six.moves.urllib.parse as urlparse - -from cloudkittyclient.apiclient import auth -from cloudkittyclient.apiclient import client -from cloudkittyclient.apiclient import exceptions -from cloudkittyclient.common import utils -from cloudkittyclient import exc - - -def _discover_auth_versions(session, auth_url): - # discover the API versions the server is supporting based on the - # given URL - v2_auth_url = None - v3_auth_url = None - try: - ks_discover = discover.Discover(session=session, auth_url=auth_url) - v2_auth_url = ks_discover.url_for('2.0') - v3_auth_url = ks_discover.url_for('3.0') - except ks_exc.DiscoveryFailure: - raise - except exceptions.ClientException: - # Identity service may not support discovery. In that case, - # try to determine version from auth_url - url_parts = urlparse.urlparse(auth_url) - (scheme, netloc, path, params, query, fragment) = url_parts - path = path.lower() - if path.startswith('/v3'): - v3_auth_url = auth_url - elif path.startswith('/v2'): - v2_auth_url = auth_url - else: - raise exc.CommandError('Unable to determine the Keystone ' - 'version to authenticate with ' - 'using the given auth_url.') - return v2_auth_url, v3_auth_url - - -def _get_keystone_session(**kwargs): - # TODO(fabgia): the heavy lifting here should be really done by Keystone. - # Unfortunately Keystone does not support a richer method to perform - # discovery and return a single viable URL. A bug against Keystone has - # been filed: https://bugs.launchpad.net/python-keystoneclient/+bug/1330677 - - # first create a Keystone session - cacert = kwargs.pop('cacert', None) - cert = kwargs.pop('cert', None) - key = kwargs.pop('key', None) - insecure = kwargs.pop('insecure', False) - auth_url = kwargs.pop('auth_url', None) - project_id = kwargs.pop('project_id', None) - project_name = kwargs.pop('project_name', None) - - if insecure: - verify = False - else: - verify = cacert or True - - if cert and key: - # passing cert and key together is deprecated in favour of the - # requests lib form of having the cert and key as a tuple - cert = (cert, key) - - # create the keystone client session - ks_session = session.Session(verify=verify, cert=cert) - v2_auth_url, v3_auth_url = _discover_auth_versions(ks_session, auth_url) - - username = kwargs.pop('username', None) - user_id = kwargs.pop('user_id', None) - user_domain_name = kwargs.pop('user_domain_name', None) - user_domain_id = kwargs.pop('user_domain_id', None) - project_domain_name = kwargs.pop('project_domain_name', None) - project_domain_id = kwargs.pop('project_domain_id', None) - auth = None - - use_domain = (user_domain_id or user_domain_name or - project_domain_id or project_domain_name) - use_v3 = v3_auth_url and (use_domain or (not v2_auth_url)) - use_v2 = v2_auth_url and not use_domain - - if use_v3: - # the auth_url as v3 specified - # e.g. http://no.where:5000/v3 - # Keystone will return only v3 as viable option - auth = v3_auth.Password( - v3_auth_url, - username=username, - password=kwargs.pop('password', None), - user_id=user_id, - user_domain_name=user_domain_name, - user_domain_id=user_domain_id, - project_name=project_name, - project_id=project_id, - project_domain_name=project_domain_name, - project_domain_id=project_domain_id) - elif use_v2: - # the auth_url as v2 specified - # e.g. http://no.where:5000/v2.0 - # Keystone will return only v2 as viable option - auth = v2_auth.Password( - v2_auth_url, - username, - kwargs.pop('password', None), - tenant_id=project_id, - tenant_name=project_name) - else: - raise exc.CommandError('Unable to determine the Keystone version ' - 'to authenticate with using the given ' - 'auth_url.') - - ks_session.auth = auth - return ks_session - - -def _get_endpoint(ks_session, **kwargs): - """Get an endpoint using the provided keystone session.""" - - # set service specific endpoint types - endpoint_type = kwargs.get('endpoint_type') or 'publicURL' - service_type = kwargs.get('service_type') or 'rating' - - endpoint = ks_session.get_endpoint(service_type=service_type, - interface=endpoint_type, - region_name=kwargs.get('region_name')) - - return endpoint - - -class AuthPlugin(auth.BaseAuthPlugin): - opt_names = ['tenant_id', 'region_name', 'auth_token', - 'service_type', 'endpoint_type', 'cacert', - 'auth_url', 'insecure', 'cert_file', 'key_file', - 'cert', 'key', 'tenant_name', 'project_name', - 'project_id', 'project_domain_id', 'project_domain_name', - 'user_id', 'user_domain_id', 'user_domain_name', - 'password', 'username', 'endpoint'] - - def __init__(self, auth_system=None, **kwargs): - self.opt_names.extend(self.common_opt_names) - super(AuthPlugin, self).__init__(auth_system, **kwargs) - - def _do_authenticate(self, http_client): - token = self.opts.get('token') or self.opts.get('auth_token') - endpoint = self.opts.get('endpoint') - if not (token and endpoint): - project_id = (self.opts.get('project_id') or - self.opts.get('tenant_id')) - project_name = (self.opts.get('project_name') or - self.opts.get('tenant_name')) - ks_kwargs = { - 'username': self.opts.get('username'), - 'password': self.opts.get('password'), - 'user_id': self.opts.get('user_id'), - 'user_domain_id': self.opts.get('user_domain_id'), - 'user_domain_name': self.opts.get('user_domain_name'), - 'project_id': project_id, - 'project_name': project_name, - 'project_domain_name': self.opts.get('project_domain_name'), - 'project_domain_id': self.opts.get('project_domain_id'), - 'auth_url': self.opts.get('auth_url'), - 'cacert': self.opts.get('cacert'), - 'cert': self.opts.get('cert'), - 'key': self.opts.get('key'), - 'insecure': strutils.bool_from_string( - self.opts.get('insecure')), - 'endpoint_type': self.opts.get('endpoint_type'), - } - - # retrieve session - ks_session = _get_keystone_session(**ks_kwargs) - token = lambda: ks_session.get_token() - endpoint = (self.opts.get('endpoint') or - _get_endpoint(ks_session, **ks_kwargs)) - self.opts['token'] = token - self.opts['endpoint'] = endpoint - - def token_and_endpoint(self, endpoint_type, service_type): - token = self.opts.get('token') - if callable(token): - token = token() - return token, self.opts.get('endpoint') - - def sufficient_options(self): - """Check if all required options are present. - - :raises: AuthPluginOptionsMissing - """ - has_token = self.opts.get('token') or self.opts.get('auth_token') - no_auth = has_token and self.opts.get('endpoint') - has_project = (self.opts.get('project_id') - or (self.opts.get('project_name') - and (self.opts.get('user_domain_name') - or self.opts.get('user_domain_id')))) - has_tenant = self.opts.get('tenant_id') or self.opts.get('tenant_name') - has_credential = (self.opts.get('username') - and (has_project or has_tenant) - and self.opts.get('password') - and self.opts.get('auth_url')) - missing = not (no_auth or has_credential) - if missing: - missing_opts = [] - opts = ['token', 'endpoint', 'username', 'password', 'auth_url', - 'tenant_id', 'tenant_name'] - for opt in opts: - if not self.opts.get(opt): - missing_opts.append(opt) - raise exceptions.AuthPluginOptionsMissing(missing_opts) +# +import sys def Client(version, *args, **kwargs): - module = utils.import_versioned_module(version, 'client') - client_class = getattr(module, 'Client') - kwargs['token'] = kwargs.get('token') or kwargs.get('auth_token') + module = 'cloudkittyclient.v%s.client' % version + __import__(module) + client_class = getattr(sys.modules[module], 'Client') return client_class(*args, **kwargs) - - -def _adjust_params(kwargs): - timeout = kwargs.get('timeout') - if timeout is not None: - timeout = int(timeout) - if timeout <= 0: - timeout = None - - insecure = strutils.bool_from_string(kwargs.get('insecure')) - verify = kwargs.get('verify') - if verify is None: - if insecure: - verify = False - else: - verify = kwargs.get('cacert') or True - - cert = kwargs.get('cert_file') - key = kwargs.get('key_file') - if cert and key: - cert = cert, key - return {'verify': verify, 'cert': cert, 'timeout': timeout} - - -def get_client(version, **kwargs): - """Get an authenticated client, based on the credentials in the kwargs. - - :param api_version: the API version to use ('1') - :param kwargs: keyword args containing credentials, either: - - * session: a keystoneauth/keystoneclient session object - * service_type: The default service_type for URL discovery - * service_name: The default service_name for URL discovery - * interface: The default interface for URL discovery - (Default: public) - * region_name: The default region_name for URL discovery - * endpoint_override: Always use this endpoint URL for requests - for this cloudkittyclient - * auth: An auth plugin to use instead of the session one - * user_agent: The User-Agent string to set - (Default is python-cloudkittyclient) - * connect_retries: the maximum number of retries that should be - attempted for connection errors - * logger: A logging object - - or (DEPRECATED): - - * os_token: pre-existing token to re-use - * os_endpoint: Cloudkitty API endpoint - - or (DEPRECATED): - - * os_username: name of user - * os_password: user's password - * os_user_id: user's id - * os_user_domain_id: the domain id of the user - * os_user_domain_name: the domain name of the user - * os_project_id: the user project id - * os_tenant_id: V2 alternative to os_project_id - * os_project_name: the user project name - * os_tenant_name: V2 alternative to os_project_name - * os_project_domain_name: domain name for the user project - * os_project_domain_id: domain id for the user project - * os_auth_url: endpoint to authenticate against - * os_cert|os_cacert: path of CA TLS certificate - * os_key: SSL private key - * insecure: allow insecure SSL (no cert verification) - """ - endpoint = kwargs.get('os_endpoint') - - cli_kwargs = { - 'username': kwargs.get('os_username'), - 'password': kwargs.get('os_password'), - 'tenant_id': (kwargs.get('os_tenant_id') - or kwargs.get('os_project_id')), - 'tenant_name': (kwargs.get('os_tenant_name') - or kwargs.get('os_project_name')), - 'auth_url': kwargs.get('os_auth_url'), - 'region_name': kwargs.get('os_region_name'), - 'service_type': kwargs.get('os_service_type'), - 'endpoint_type': kwargs.get('os_endpoint_type'), - 'cacert': kwargs.get('os_cacert'), - 'cert_file': kwargs.get('os_cert'), - 'key_file': kwargs.get('os_key'), - 'token': kwargs.get('os_token') or kwargs.get('os_auth_token'), - 'user_domain_name': kwargs.get('os_user_domain_name'), - 'user_domain_id': kwargs.get('os_user_domain_id'), - 'project_domain_name': kwargs.get('os_project_domain_name'), - 'project_domain_id': kwargs.get('os_project_domain_id'), - } - - cli_kwargs.update(kwargs) - cli_kwargs.update(_adjust_params(cli_kwargs)) - - return Client(version, endpoint, **cli_kwargs) - - -def get_auth_plugin(endpoint, **kwargs): - auth_plugin = AuthPlugin( - auth_url=kwargs.get('auth_url'), - service_type=kwargs.get('service_type'), - token=kwargs.get('token'), - endpoint_type=kwargs.get('endpoint_type'), - cacert=kwargs.get('cacert'), - tenant_id=kwargs.get('project_id') or kwargs.get('tenant_id'), - endpoint=endpoint, - username=kwargs.get('username'), - password=kwargs.get('password'), - tenant_name=kwargs.get('tenant_name') or kwargs.get('project_name'), - user_domain_name=kwargs.get('user_domain_name'), - user_domain_id=kwargs.get('user_domain_id'), - project_domain_name=kwargs.get('project_domain_name'), - project_domain_id=kwargs.get('project_domain_id') - ) - return auth_plugin - - -LEGACY_OPTS = ('auth_plugin', 'auth_url', 'token', 'insecure', 'cacert', - 'tenant_id', 'project_id', 'username', 'password', - 'project_name', 'tenant_name', - 'user_domain_name', 'user_domain_id', - 'project_domain_name', 'project_domain_id', - 'key_file', 'cert_file', 'verify', 'timeout', 'cert') - - -def construct_http_client(**kwargs): - kwargs = kwargs.copy() - if kwargs.get('session') is not None: - # Drop legacy options - for opt in LEGACY_OPTS: - kwargs.pop(opt, None) - - return SessionClient( - session=kwargs.pop('session'), - service_type=kwargs.pop('service_type', 'rating') or 'rating', - interface=kwargs.pop('interface', kwargs.pop('endpoint_type', - 'publicURL')), - region_name=kwargs.pop('region_name', None), - user_agent=kwargs.pop('user_agent', 'python-cloudkittyclient'), - auth=kwargs.get('auth', None), - timings=kwargs.pop('timings', None), - **kwargs) - else: - return client.BaseClient(client.HTTPClient( - auth_plugin=kwargs.get('auth_plugin'), - region_name=kwargs.get('region_name'), - endpoint_type=kwargs.get('endpoint_type'), - original_ip=kwargs.get('original_ip'), - verify=kwargs.get('verify'), - cert=kwargs.get('cert'), - timeout=kwargs.get('timeout'), - timings=kwargs.get('timings'), - keyring_saver=kwargs.get('keyring_saver'), - debug=kwargs.get('debug'), - user_agent=kwargs.get('user_agent'), - http=kwargs.get('http') - )) - - -@contextlib.contextmanager -def record_time(times, enabled, *args): - """Record the time of a specific action. - - :param times: A list of tuples holds time data. - :type times: list - :param enabled: Whether timing is enabled. - :type enabled: bool - :param args: Other data to be stored besides time data, these args - will be joined to a string. - """ - if not enabled: - yield - else: - start = time.time() - yield - end = time.time() - times.append((' '.join(args), start, end)) - - -class SessionClient(adapter.LegacyJsonAdapter): - def __init__(self, *args, **kwargs): - self.times = [] - self.timings = kwargs.pop('timings', False) - super(SessionClient, self).__init__(*args, **kwargs) - - def request(self, url, method, **kwargs): - kwargs.setdefault('headers', kwargs.get('headers', {})) - # NOTE(sileht): The standard call raises errors from - # keystoneauth, where we need to raise the cloudkittyclient errors. - raise_exc = kwargs.pop('raise_exc', True) - with record_time(self.times, self.timings, method, url): - resp, body = super(SessionClient, self).request(url, - method, - raise_exc=False, - **kwargs) - - if raise_exc and resp.status_code >= 400: - raise exc.from_response(resp, body) - return resp diff --git a/cloudkittyclient/common/base.py b/cloudkittyclient/common/base.py deleted file mode 100644 index 6212817..0000000 --- a/cloudkittyclient/common/base.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# Copyright 2015 Objectif Libre -# 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. -""" - -import copy - -from six.moves.urllib import parse - -from cloudkittyclient.apiclient import base -from cloudkittyclient import exc -from cloudkittyclient.i18n import _ - - -def getid(obj): - """Extracts object ID. - - Abstracts the common pattern of allowing both an object or an - object's ID (UUID) as a parameter when dealing with relationships. - """ - try: - return obj.id - except AttributeError: - return obj - - -class Manager(object): - """Managers interact with a particular type of API. - - It works with samples, meters, alarms, etc. and provide CRUD operations for - them. - """ - resource_class = None - - def __init__(self, api): - self.api = api - - @property - def client(self): - """Compatible with latest oslo-incubator.apiclient code.""" - return self.api - - def _create(self, url, body): - body = self.api.post(url, json=body).json() - if body: - return self.resource_class(self, body) - - def _list(self, url, response_key=None, obj_class=None, body=None, - expect_single=False): - resp = self.api.get(url) - if not resp.content: - raise exc.HTTPNotFound - body = resp.json() - - if obj_class is None: - obj_class = self.resource_class - - if response_key: - try: - data = body[response_key] - except KeyError: - return [] - else: - data = body - if expect_single: - data = [data] - return [obj_class(self, res, loaded=True) for res in data if res] - - def _update(self, url, item, response_key=None): - if not item.dirty_fields: - return item - item = self.api.put(url, json=item.dirty_fields).json() - # PUT requests may not return a item - if item: - return self.resource_class(self, item) - - def _delete(self, url): - self.api.delete(url) - - -class CrudManager(base.CrudManager): - """A CrudManager that automatically gets its base URL.""" - - base_url = None - - def build_url(self, base_url=None, **kwargs): - base_url = base_url or self.base_url - return super(CrudManager, self).build_url(base_url, **kwargs) - - def get(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._get( - self.build_url(**kwargs)) - - def create(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._post( - self.build_url(**kwargs), kwargs) - - def update(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - params = kwargs.copy() - - return self._put( - self.build_url(**kwargs), params) - - def findall(self, base_url=None, **kwargs): - """Find multiple items with attributes matching ``**kwargs``. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - rl = self._list( - '%(base_url)s%(query)s' % { - 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) - num = len(rl) - - if num == 0: - msg = _("No %(name)s matching %(args)s.") % { - 'name': self.resource_class.__name__, - 'args': kwargs - } - raise exc.HTTPNotFound(msg) - return rl - - -class Resource(base.Resource): - """A resource represents a particular instance of an object. - - Resource might be tenant, user, etc. - This is pretty much just a bag for attributes. - - :param manager: Manager object - :param info: dictionary representing resource attributes - :param loaded: prevent lazy-loading if set to True - """ - - key = None - - def to_dict(self): - return copy.deepcopy(self._info) - - @property - def dirty_fields(self): - out = self.to_dict() - for k, v in self._info.items(): - if self.__dict__[k] != v: - out[k] = self.__dict__[k] - return out - - def update(self): - try: - return self.manager.update(**self.dirty_fields) - except AttributeError: - raise exc.NotUpdatableError(self) diff --git a/cloudkittyclient/common/cliutils.py b/cloudkittyclient/common/cliutils.py deleted file mode 100644 index 47a2093..0000000 --- a/cloudkittyclient/common/cliutils.py +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright 2012 Red Hat, Inc. -# -# 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. - -# W0603: Using the global statement -# W0621: Redefining name %s from outer scope -# pylint: disable=W0603,W0621 - -from __future__ import print_function - -import getpass -import inspect -import os -import sys -import textwrap - -from oslo_utils import encodeutils -from oslo_utils import strutils -import prettytable -import six -from six import moves - -from cloudkittyclient.i18n import _ - - -class MissingArgs(Exception): - """Supplied arguments are not sufficient for calling a function.""" - def __init__(self, missing): - self.missing = missing - msg = _("Missing arguments: %s") % ", ".join(missing) - super(MissingArgs, self).__init__(msg) - - -def validate_args(fn, *args, **kwargs): - """Check that the supplied args are sufficient for calling a function. - - >>> validate_args(lambda a: None) - Traceback (most recent call last): - ... - MissingArgs: Missing argument(s): a - >>> validate_args(lambda a, b, c, d: None, 0, c=1) - Traceback (most recent call last): - ... - MissingArgs: Missing argument(s): b, d - - :param fn: the function to check - :param arg: the positional arguments supplied - :param kwargs: the keyword arguments supplied - """ - argspec = inspect.getargspec(fn) - - num_defaults = len(argspec.defaults or []) - required_args = argspec.args[:len(argspec.args) - num_defaults] - - def isbound(method): - return getattr(method, '__self__', None) is not None - - if isbound(fn): - required_args.pop(0) - - missing = [arg for arg in required_args if arg not in kwargs] - missing = missing[len(args):] - if missing: - raise MissingArgs(missing) - - -def arg(*args, **kwargs): - """Decorator for CLI args. - - Example: - - >>> @arg("name", help="Name of the new entity") - ... def entity_create(args): - ... pass - """ - def _decorator(func): - add_arg(func, *args, **kwargs) - return func - return _decorator - - -def env(*args, **kwargs): - """Returns the first environment variable set. - - If all are empty, defaults to '' or keyword arg `default`. - """ - for arg in args: - value = os.environ.get(arg) - if value: - return value - return kwargs.get('default', '') - - -def add_arg(func, *args, **kwargs): - """Bind CLI arguments to a shell.py `do_foo` function.""" - - if not hasattr(func, 'arguments'): - func.arguments = [] - - # NOTE(sirp): avoid dups that can occur when the module is shared across - # tests. - if (args, kwargs) not in func.arguments: - # Because of the semantics of decorator composition if we just append - # to the options list positional options will appear to be backwards. - func.arguments.insert(0, (args, kwargs)) - - -def unauthenticated(func): - """Adds 'unauthenticated' attribute to decorated function. - - Usage: - - >>> @unauthenticated - ... def mymethod(f): - ... pass - """ - func.unauthenticated = True - return func - - -def isunauthenticated(func): - """Checks if the function does not require authentication. - - Mark such functions with the `@unauthenticated` decorator. - - :returns: bool - """ - return getattr(func, 'unauthenticated', False) - - -def print_list(objs, fields, formatters=None, sortby_index=0, - mixed_case_fields=None, field_labels=None): - """Print a list or objects as a table, one row per object. - - :param objs: iterable of :class:`Resource` - :param fields: attributes that correspond to columns, in order - :param formatters: `dict` of callables for field formatting - :param sortby_index: index of the field for sorting table rows - :param mixed_case_fields: fields corresponding to object attributes that - have mixed case names (e.g., 'serverId') - :param field_labels: Labels to use in the heading of the table, default to - fields. - """ - formatters = formatters or {} - mixed_case_fields = mixed_case_fields or [] - field_labels = field_labels or fields - if len(field_labels) != len(fields): - raise ValueError(_("Field labels list %(labels)s has different number " - "of elements than fields list %(fields)s"), - {'labels': field_labels, 'fields': fields}) - - if sortby_index is None: - kwargs = {} - else: - kwargs = {'sortby': field_labels[sortby_index]} - pt = prettytable.PrettyTable(field_labels) - pt.align = 'l' - - for o in objs: - row = [] - for field in fields: - if field in formatters: - row.append(formatters[field](o)) - else: - if field in mixed_case_fields: - field_name = field.replace(' ', '_') - else: - field_name = field.lower().replace(' ', '_') - data = getattr(o, field_name, '') - row.append(data) - pt.add_row(row) - - if six.PY3: - print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode()) - else: - print(encodeutils.safe_encode(pt.get_string(**kwargs))) - - -def print_dict(dct, dict_property="Property", wrap=0): - """Print a `dict` as a table of two columns. - - :param dct: `dict` to print - :param dict_property: name of the first column - :param wrap: wrapping for the second column - """ - pt = prettytable.PrettyTable([dict_property, 'Value']) - pt.align = 'l' - for k, v in six.iteritems(dct): - # convert dict to str to check length - if isinstance(v, dict): - v = six.text_type(v) - if wrap > 0: - v = textwrap.fill(six.text_type(v), wrap) - # if value has a newline, add in multiple rows - # e.g. fault with stacktrace - if v and isinstance(v, six.string_types) and r'\n' in v: - lines = v.strip().split(r'\n') - col1 = k - for line in lines: - pt.add_row([col1, line]) - col1 = '' - else: - pt.add_row([k, v]) - - if six.PY3: - print(encodeutils.safe_encode(pt.get_string()).decode()) - else: - print(encodeutils.safe_encode(pt.get_string())) - - -def get_password(max_password_prompts=3): - """Read password from TTY.""" - verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD")) - pw = None - if hasattr(sys.stdin, "isatty") and sys.stdin.isatty(): - # Check for Ctrl-D - try: - for __ in moves.range(max_password_prompts): - pw1 = getpass.getpass("OS Password: ") - if verify: - pw2 = getpass.getpass("Please verify: ") - else: - pw2 = pw1 - if pw1 == pw2 and pw1: - pw = pw1 - break - except EOFError: - pass - return pw - - -def service_type(stype): - """Adds 'service_type' attribute to decorated function. - - Usage: - - .. code-block:: python - - @service_type('volume') - def mymethod(f): - ... - """ - def inner(f): - f.service_type = stype - return f - return inner - - -def get_service_type(f): - """Retrieves service type from function.""" - return getattr(f, 'service_type', None) - - -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) - - -def exit(msg=''): - if msg: - print(msg, file=sys.stderr) - sys.exit(1) diff --git a/cloudkittyclient/common/utils.py b/cloudkittyclient/common/utils.py deleted file mode 100644 index 4348397..0000000 --- a/cloudkittyclient/common/utils.py +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from __future__ import print_function - -import datetime -import sys -import textwrap -import uuid - -from oslo_serialization import jsonutils -from oslo_utils import encodeutils -from oslo_utils import importutils -from oslo_utils import timeutils -import prettytable -import six - -from cloudkittyclient.common import cliutils -from cloudkittyclient import exc -from cloudkittyclient.i18n import _ - - -def iso2dt(iso_date): - """iso8601 format to datetime.""" - iso_dt = timeutils.parse_isotime(iso_date) - trans_dt = timeutils.normalize_time(iso_dt) - return trans_dt - - -def import_versioned_module(version, submodule=None): - module = 'cloudkittyclient.v%s' % version - if submodule: - module = '.'.join((module, submodule)) - return importutils.import_module(module) - - -# Decorator for cli-args -def arg(*args, **kwargs): - def _decorator(func): - if 'help' in kwargs: - if 'default' in kwargs: - kwargs['help'] += " Defaults to %s." % kwargs['default'] - required = kwargs.get('required', False) - if required: - kwargs['help'] += " required." - elif 'default' not in kwargs: - kwargs['help'] += "." - - # Because of the sematics of decorator composition if we just append - # to the options list positional options will appear to be backwards. - func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) - return func - return _decorator - - -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) - - -def print_list(objs, fields, field_labels, formatters={}, sortby=0): - - def _make_default_formatter(field): - return lambda o: getattr(o, field, '') - - new_formatters = {} - for field, field_label in six.moves.zip(fields, field_labels): - if field in formatters: - new_formatters[field_label] = formatters[field] - else: - new_formatters[field_label] = _make_default_formatter(field) - - cliutils.print_list(objs, field_labels, - formatters=new_formatters, - sortby_index=sortby) - - -def nested_list_of_dict_formatter(field, column_names): - # (TMaddox) Because the formatting scheme actually drops the whole object - # into the formatter, rather than just the specified field, we have to - # extract it and then pass the value. - return lambda o: format_nested_list_of_dict(getattr(o, field), - column_names) - - -def format_nested_list_of_dict(l, column_names): - pt = prettytable.PrettyTable(caching=False, print_empty=False, - header=True, hrules=prettytable.FRAME, - field_names=column_names) - for d in l: - pt.add_row(list(map(lambda k: d[k], column_names))) - return pt.get_string() - - -def print_dict(d, dict_property="Property", wrap=0): - pt = prettytable.PrettyTable([dict_property, 'Value'], print_empty=False) - pt.align = 'l' - for k, v in sorted(six.iteritems(d)): - # convert dict to str to check length - if isinstance(v, dict): - v = jsonutils.dumps(v) - # if value has a newline, add in multiple rows - # e.g. fault with stacktrace - if v and isinstance(v, six.string_types) and r'\n' in v: - lines = v.strip().split(r'\n') - col1 = k - for line in lines: - if wrap > 0: - line = textwrap.fill(str(line), wrap) - pt.add_row([col1, line]) - col1 = '' - else: - if wrap > 0: - v = textwrap.fill(str(v), wrap) - pt.add_row([k, v]) - encoded = encodeutils.safe_encode(pt.get_string()) - # FIXME(gordc): https://bugs.launchpad.net/oslo-incubator/+bug/1370710 - if six.PY3: - encoded = encoded.decode() - print(encoded) - - -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.HTTPNotFound: - pass - - # now try to get entity as uuid - try: - uuid.UUID(str(name_or_id)) - return manager.get(name_or_id) - except (ValueError, exc.HTTPNotFound): - pass - - # finally try to find entity by name - try: - return manager.find(name=name_or_id) - except exc.HTTPNotFound: - msg = _("No %(name)s with a name or ID of '%(id)s' exists.") % { - "name": manager.resource_class.__name__.lower(), - "id": name_or_id - } - raise exc.CommandError(msg) - - -def args_array_to_dict(kwargs, key_to_convert): - values_to_convert = kwargs.get(key_to_convert) - if values_to_convert: - try: - kwargs[key_to_convert] = dict(v.split("=", 1) - for v in values_to_convert) - except ValueError: - msg = _("%(key)s must be a list of key=value " - "not '%(value)s'") % { - "key": key_to_convert, - "value": values_to_convert - } - raise exc.CommandError(msg) - return kwargs - - -def args_array_to_list_of_dicts(kwargs, key_to_convert): - """Converts ['a=1;b=2','c=3;d=4'] to [{a:1,b:2},{c:3,d:4}].""" - values_to_convert = kwargs.get(key_to_convert) - if values_to_convert: - try: - kwargs[key_to_convert] = [] - for lst in values_to_convert: - pairs = lst.split(";") - dct = dict() - for pair in pairs: - kv = pair.split("=", 1) - dct[kv[0]] = kv[1].strip(" \"'") # strip spaces and quotes - kwargs[key_to_convert].append(dct) - except Exception: - msg = _("%(key)s must be a list of " - "key1=value1;key2=value2;... not '%(value)s'") % { - "key": key_to_convert, - "value": values_to_convert - } - raise exc.CommandError(msg) - return kwargs - - -def key_with_slash_to_nested_dict(kwargs): - nested_kwargs = {} - for k in list(kwargs): - keys = k.split('/', 1) - if len(keys) == 2: - nested_kwargs.setdefault(keys[0], {})[keys[1]] = kwargs[k] - del kwargs[k] - kwargs.update(nested_kwargs) - return kwargs - - -def merge_nested_dict(dest, source, depth=0): - for (key, value) in six.iteritems(source): - if isinstance(value, dict) and depth: - merge_nested_dict(dest[key], value, - depth=(depth - 1)) - else: - dest[key] = value - - -def ts2dt(timestamp): - """timestamp to datetime format.""" - if not isinstance(timestamp, float): - timestamp = float(timestamp) - return datetime.datetime.utcfromtimestamp(timestamp) - - -def exit(msg=''): - if msg: - print(msg, file=sys.stderr) - sys.exit(1) diff --git a/cloudkittyclient/exc.py b/cloudkittyclient/exc.py index a4c1a52..fcf3c60 100644 --- a/cloudkittyclient/exc.py +++ b/cloudkittyclient/exc.py @@ -31,10 +31,18 @@ class InvalidEndpoint(BaseException): """The provided endpoint is invalid.""" +class ArgumentRequired(BaseException): + """A required argument was not provided.""" + + class CommunicationError(BaseException): """Unable to communicate with server.""" +class InvalidArgumentError(BaseException): + """Exception raised when a provided argument is invalid""" + + class NotUpdatableError(BaseException): """This Resource is not updatable.""" diff --git a/cloudkittyclient/format.py b/cloudkittyclient/format.py new file mode 100644 index 0000000..cd8f202 --- /dev/null +++ b/cloudkittyclient/format.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import csv + +from cliff.formatters import base +import jsonpath_rw_ext as jp +from oslo_log import log +import yaml + + +LOG = log.getLogger(__name__) + + +class DataframeToCsvFormatter(base.ListFormatter): + """Cliff formatter allowing to customize CSV report content.""" + + default_config = [ + ('Begin', '$.begin'), + ('End', '$.end'), + ('Metric Type', '$.service'), + ('Qty', '$.volume'), + ('Cost', '$.rating'), + ('Project ID', '$.desc.project_id'), + ('Resource ID', '$.desc.resource_id'), + ('User ID', '$.desc.user_id'), + ] + + def _load_config(self, filename): + config = self.default_config + if filename: + try: + with open(filename, 'r') as fd: + yml_config = yaml.safe_load(fd.read()) + if len(yml_config): + config = [(list(item.keys())[0], list(item.values())[0]) + for item in yml_config] + else: + LOG.warning('Invalid config file {file}. Using default ' + 'configuration'.format(file=filename)) + except (IOError, yaml.scanner.ScannerError) as err: + LOG.warning('Error: {err}. Using default ' + 'configuration'.format(err=err)) + self.parsers = {} + for col, path in config: + self.parsers[col] = jp.parse(path) + return config + + def add_argument_group(self, parser): + group = parser.add_argument_group('dataframe-to-csv formatter') + group.add_argument('--format-config-file', + type=str, dest='format_config', + help='Config file for the dict-to-csv formatter') + + def _get_csv_row(self, config, json_item): + row = {} + for col, parser in self.parsers.items(): + items = parser.find(json_item) + row[col] = items[0].value if items else '' + return row + + def emit_list(self, column_names, data, stdout, parsed_args): + config = self._load_config(vars(parsed_args).get('format_config')) + self.writer = csv.DictWriter(stdout, + fieldnames=[elem[0] for elem in config]) + self.writer.writeheader() + for dataframe in data: + rating_data = dataframe[3] + for item in rating_data: + item['begin'] = dataframe[0] + item['end'] = dataframe[1] + row = self._get_csv_row(config, item) + self.writer.writerow(row) diff --git a/cloudkittyclient/i18n.py b/cloudkittyclient/i18n.py deleted file mode 100644 index 7b8831f..0000000 --- a/cloudkittyclient/i18n.py +++ /dev/null @@ -1,24 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""oslo.i18n integration module. - -See http://docs.openstack.org/developer/oslo.i18n/usage.html - -""" - -import oslo_i18n as i18n - -_translators = i18n.TranslatorFactory(domain='cloudkittyclient') -i18n.enable_lazy() - -_ = _translators.primary diff --git a/cloudkittyclient/osc.py b/cloudkittyclient/osc.py index 4b2d680..8f52860 100644 --- a/cloudkittyclient/osc.py +++ b/cloudkittyclient/osc.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudkittyclient import client as ckclient +from osc_lib import utils DEFAULT_API_VERSION = '1' API_VERSION_OPTION = 'os_rating_api_version' @@ -25,9 +25,12 @@ API_VERSIONS = { def make_client(instance): """Returns a rating service client.""" version = instance._api_version[API_NAME] - version = int(version) - auth_config = instance.get_configuration()['auth'] - return ckclient.get_client(version, **auth_config) + ck_client = utils.get_client_class( + API_NAME, + version, + API_VERSIONS) + instance.setup_auth() + return ck_client(session=instance.session) def build_option_parser(parser): diff --git a/cloudkittyclient/shell.py b/cloudkittyclient/shell.py index 04738d6..f1dc940 100644 --- a/cloudkittyclient/shell.py +++ b/cloudkittyclient/shell.py @@ -1,5 +1,6 @@ -# Copyright 2015 Objectif Libre - +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -11,321 +12,135 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +# +import os +from sys import argv -""" -Command-line interface to the OpenStack Cloudkitty API. -""" +import cliff.app +from cliff.commandmanager import CommandManager +import os_client_config +from oslo_log import log -from __future__ import print_function - -import argparse -import logging -import sys - -from oslo_utils import encodeutils -import six -from stevedore import extension - -import cloudkittyclient -from cloudkittyclient import client as ckclient -from cloudkittyclient.common import cliutils -from cloudkittyclient.common import utils -from cloudkittyclient import exc -from cloudkittyclient.v1.collector import shell as collector_shell -from cloudkittyclient.v1.report import shell as report_shell -from cloudkittyclient.v1.storage import shell as storage_shell - -SUBMODULES_NAMESPACE = 'cloudkitty.client.modules' +from cloudkittyclient import client +from cloudkittyclient import utils -def _positive_non_zero_int(argument_value): - if argument_value is None: - return None - try: - value = int(argument_value) - except ValueError: - msg = "%s must be an integer" % argument_value - raise argparse.ArgumentTypeError(msg) - if value <= 0: - msg = "%s must be greater than 0" % argument_value - raise argparse.ArgumentTypeError(msg) - return value +LOG = log.getLogger(__name__) -class CloudkittyShell(object): +class CloudKittyShell(cliff.app.App): - def __init__(self): - self.auth_plugin = ckclient.AuthPlugin() + legacy_commands = [ + 'module-list', + 'module-enable', + 'module-list', + 'module-enable', + 'module-disable', + 'module-set-priority', + 'info-config-get', + 'info-service-get', + 'total-get', + 'summary-get', + 'report-tenant-list', + 'collector-mapping-list', + 'collector-mapping-get', + 'collector-mapping-create', + 'collector-mapping-delete', + 'collector-state-get', + 'collector-state-enable', + 'collector-state-disable', + 'storage-dataframe-list', + 'hashmap-service-create', + 'hashmap-service-list', + 'hashmap-service-delete', + 'hashmap-field-create', + 'hashmap-field-list', + 'hashmap-field-delete', + 'hashmap-mapping-create', + 'hashmap-mapping-update', + 'hashmap-mapping-list', + 'hashmap-mapping-delete', + 'hashmap-group-create', + 'hashmap-group-list', + 'hashmap-group-delete', + 'hashmap-threshold-create' + 'hashmap-threshold-update' + 'hashmap-threshold-list', + 'hashmap-threshold-delete', + 'hashmap-threshold-get', + 'hashmap-threshold-group', + 'pyscripts-script-create', + 'pyscripts-script-list', + 'pyscripts-script-get', + 'pyscripts-script-get-data', + 'pyscripts-script-delete', + 'pyscripts-script-update', + ] - def get_base_parser(self): - parser = argparse.ArgumentParser( - prog='cloudkitty', - description=__doc__.strip(), - epilog='See "cloudkitty help COMMAND" ' - 'for help on a specific command.', - add_help=False, - formatter_class=HelpFormatter, + def __init__(self, args): + self._args = args + self.cloud_config = os_client_config.OpenStackConfig() + super(CloudKittyShell, self).__init__( + description='CloudKitty CLI client', + version=utils.get_version(), + command_manager=CommandManager('cloudkittyclient'), + deferred_help=True, ) + self._client = None - # Global arguments - parser.add_argument('-h', '--help', - action='store_true', - help=argparse.SUPPRESS, - ) - - parser.add_argument('--version', - action='version', - version=cloudkittyclient.__version__) - - parser.add_argument('-d', '--debug', - default=bool(cliutils.env('CLOUDKITTYCLIENT_DEBUG') - ), - action='store_true', - help='Defaults to env[CLOUDKITTYCLIENT_DEBUG].') - - parser.add_argument('-v', '--verbose', - default=False, action="store_true", - help="Print more verbose output.") - - parser.add_argument('--timeout', - default=600, - type=_positive_non_zero_int, - help='Number of seconds to wait for a response.') - - parser.add_argument('--cloudkitty-url', metavar='', - dest='os_endpoint', - default=cliutils.env('CLOUDKITTY_URL'), - help=("DEPRECATED, use --os-endpoint instead. " - "Defaults to env[CLOUDKITTY_URL].")) - - parser.add_argument('--cloudkitty_url', - dest='os_endpoint', - help=argparse.SUPPRESS) - - parser.add_argument('--cloudkitty-api-version', - default=cliutils.env( - 'CLOUDKITTY_API_VERSION', default='1'), - help='Defaults to env[CLOUDKITTY_API_VERSION] ' - 'or 1.') - - parser.add_argument('--cloudkitty_api_version', - help=argparse.SUPPRESS) - - self.auth_plugin.add_opts(parser) - self.auth_plugin.add_common_opts(parser) - - return parser - - def get_subcommand_parser(self, version): - parser = self.get_base_parser() - - self.subcommands = {} - subparsers = parser.add_subparsers(metavar='') - submodule = utils.import_versioned_module(version, 'shell') - self._find_actions(subparsers, submodule) - self._find_actions(subparsers, collector_shell) - self._find_actions(subparsers, report_shell) - self._find_actions(subparsers, storage_shell) - extensions = extension.ExtensionManager( - SUBMODULES_NAMESPACE, - ) - for ext in extensions: - shell = ext.plugin.get_shell() - self._find_actions(subparsers, shell) - self._find_actions(subparsers, self) - self._add_bash_completion_subparser(subparsers) - return parser - - 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): - for attr in (a for a in dir(actions_module) if a.startswith('do_')): - # I prefer to be hypen-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', []) - - 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) - - @staticmethod - def _setup_logging(debug): - format = '%(levelname)s (%(module)s) %(message)s' - if debug: - logging.basicConfig(format=format, level=logging.DEBUG) - else: - logging.basicConfig(format=format, level=logging.WARN) - logging.getLogger('iso8601').setLevel(logging.WARNING) - logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING) - - def parse_args(self, argv): - # Parse args once to find version - parser = self.get_base_parser() - (options, args) = parser.parse_known_args(argv) - self.auth_plugin.parse_opts(options) - self._setup_logging(options.debug) - - # build available subcommands based on version - api_version = options.cloudkitty_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 options.help or not argv: - self.do_help(options) - return 0 - - # Return parsed args - return api_version, subcommand_parser.parse_args(argv) - - @staticmethod - def no_project_and_domain_set(args): - return not (((args.os_project_id or (args.os_project_name and - (args.os_project_domain_name or - args.os_project_domain_id))) - and (args.os_user_domain_name or args.os_user_domain_id)) - or (args.os_tenant_id or args.os_tenant_name)) - - def main(self, argv): - parsed = self.parse_args(argv) - if parsed == 0: - return 0 - api_version, args = parsed - - # 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 ((self.auth_plugin.opts.get('token') - or self.auth_plugin.opts.get('auth_token')) - and self.auth_plugin.opts['endpoint']): - if not self.auth_plugin.opts['username']: - raise exc.CommandError("You must provide a username via " - "either --os-username or via " - "env[OS_USERNAME]") - - if not self.auth_plugin.opts['password']: - raise exc.CommandError("You must provide a password via " - "either --os-password or via " - "env[OS_PASSWORD]") - - if self.no_project_and_domain_set(args): - # steer users towards Keystone V3 API - raise exc.CommandError("You must provide a project_id via " - "either --os-project-id or via " - "env[OS_PROJECT_ID] and " - "a domain_name via either " - "--os-user-domain-name or via " - "env[OS_USER_DOMAIN_NAME] or " - "a domain_id via either " - "--os-user-domain-id or via " - "env[OS_USER_DOMAIN_ID]\n\n" - "As an alternative to project_id, " - "you can provide a project_name via " - "either --os-project-name or via " - "env[OS_PROJECT_NAME] and " - "a project_domain_name via either " - "--os-project-domain-name or via " - "env[OS_PROJECT_DOMAIN_NAME] or " - "a project_domain_id via either " - "--os-project-domain-id or via " - "env[OS_PROJECT_DOMAIN_ID]") - - if not self.auth_plugin.opts['auth_url']: - raise exc.CommandError("You must provide an auth url via " - "either --os-auth-url or via " - "env[OS_AUTH_URL]") - - client_kwargs = {} - client_kwargs.update(self.auth_plugin.opts) - client_kwargs['auth_plugin'] = self.auth_plugin - client = ckclient.get_client(api_version, **client_kwargs) - # call whatever callback was selected + # NOTE(peschk_l): Used to warn users about command syntax change in Rocky. + # To be deleted in S. + def run_subcommand(self, argv): try: - args.func(client, args) - except exc.HTTPUnauthorized: - raise exc.CommandError("Invalid OpenStack Identity credentials.") + self.command_manager.find_command(argv) + except ValueError: + if argv[0] in self.legacy_commands: + LOG.warning('WARNING: This command is deprecated, please see' + ' the reference for the new commands\n') + exit(1) + return super(CloudKittyShell, self).run_subcommand(argv) - def do_bash_completion(self, args): - """Prints all of the commands and options to stdout. + def build_option_parser(self, description, version): + parser = super(CloudKittyShell, self).build_option_parser( + description, + version, + argparse_kwargs={'allow_abbrev': False}) + parser.add_argument( + '--ck-api-version', type=int, default=1, dest='ck_version', + help='Cloudkitty API version (defaults to 1)') + if 'OS_AUTH_TYPE' not in os.environ.keys() \ + and 'OS_PASSWORD' in os.environ.keys(): + os.environ['OS_AUTH_TYPE'] = 'password' + self.cloud_config.register_argparse_arguments( + parser, self._args, service_keys=['rating']) + return parser - The cloudkitty.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 __init__(self, prog, indent_increment=2, max_help_position=32, - width=None): - super(HelpFormatter, self).__init__(prog, indent_increment, - max_help_position, width) - - def start_section(self, heading): - # Title-case the headings - heading = '%s%s' % (heading[0].upper(), heading[1:]) - super(HelpFormatter, self).start_section(heading) + @property + def client(self): + if self._client is None: + self.cloud = self.cloud_config.get_one_cloud( + argparse=self.options) + session = self.cloud.get_session() + adapter_options = dict( + service_type=(self.options.os_rating_service_type or + self.options.os_service_type), + service_name=(self.options.os_rating_service_name or + self.options.os_service_name), + interface=(self.options.os_rating_interface or + self.options.os_interface), + region_name=self.options.os_region_name, + endpoint_override=( + self.options.os_rating_endpoint_override or + self.options.os_endpoint_override), + ) + self._client = client.Client(str(self.options.ck_version), + session=session, + adapter_options=adapter_options) + return self._client def main(args=None): - try: - if args is None: - args = sys.argv[1:] - - CloudkittyShell().main(args) - - except Exception as e: - if '--debug' in args or '-d' in args: - raise - else: - print(encodeutils.safe_encode(six.text_type(e)), file=sys.stderr) - sys.exit(1) - except KeyboardInterrupt: - print("Stopping Cloudkitty Client", file=sys.stderr) - sys.exit(130) - -if __name__ == "__main__": - main() + if args is None: + args = argv[1:] + client_app = CloudKittyShell(args) + return client_app.run(args) diff --git a/cloudkittyclient/tests/base.py b/cloudkittyclient/tests/base.py deleted file mode 100644 index 2159808..0000000 --- a/cloudkittyclient/tests/base.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2010-2011 OpenStack Foundation -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. -# -# 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 oslotest import base - - -class TestCase(base.BaseTestCase): - - """Test case base class for all unit tests.""" diff --git a/cloudkittyclient/tests/fakes.py b/cloudkittyclient/tests/fakes.py deleted file mode 100644 index 705efd6..0000000 --- a/cloudkittyclient/tests/fakes.py +++ /dev/null @@ -1,64 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from keystoneclient.v2_0 import client as ksclient - - -def script_keystone_client(): - ksclient.Client(auth_url='http://no.where', - insecure=False, - password='password', - tenant_id='', - tenant_name='tenant_name', - username='username').AndReturn(FakeKeystone('abcd1234')) - - -def fake_headers(): - return {'X-Auth-Token': 'abcd1234', - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'python-cloudkittyclient'} - - -class FakeServiceCatalog(object): - @staticmethod - def url_for(endpoint_type, service_type): - return 'http://192.168.1.5:8004/v1/f14b41234' - - -class FakeKeystone(object): - service_catalog = FakeServiceCatalog() - - def __init__(self, auth_token): - self.auth_token = auth_token - - -class FakeHTTPResponse(object): - - version = 1.1 - - def __init__(self, status, reason, headers, body): - self.headers = headers - self.body = body - self.status = status - self.reason = reason - - def getheader(self, name, default=None): - return self.headers.get(name, default) - - def getheaders(self): - return self.headers.items() - - def read(self, amt=None): - b = self.body - self.body = None - return b diff --git a/cloudkittyclient/apiclient/__init__.py b/cloudkittyclient/tests/functional/__init__.py similarity index 100% rename from cloudkittyclient/apiclient/__init__.py rename to cloudkittyclient/tests/functional/__init__.py diff --git a/cloudkittyclient/common/__init__.py b/cloudkittyclient/tests/functional/v1/__init__.py similarity index 100% rename from cloudkittyclient/common/__init__.py rename to cloudkittyclient/tests/functional/v1/__init__.py diff --git a/cloudkittyclient/tests/functional/v1/base.py b/cloudkittyclient/tests/functional/v1/base.py new file mode 100644 index 0000000..37890fb --- /dev/null +++ b/cloudkittyclient/tests/functional/v1/base.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import json +import os +import shlex +import subprocess + +from cloudkittyclient.tests import utils + + +class BaseFunctionalTest(utils.BaseTestCase): + + def _run(self, executable, action, + flags='', params='', fmt='-f json', has_output=True): + if not has_output: + fmt = '' + cmd = ' '.join([executable, flags, action, params, fmt]) + cmd = shlex.split(cmd) + p = subprocess.Popen(cmd, env=os.environ.copy(), shell=False, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + if p.returncode != 0: + raise RuntimeError('"{cmd}" returned {val}: {msg}'.format( + cmd=' '.join(cmd), val=p.returncode, msg=stderr)) + return json.loads(stdout) if has_output else None + + def openstack(self, action, + flags='', params='', fmt='-f json', has_output=True): + return self._run('openstack rating', action, + flags, params, fmt, has_output) + + def cloudkitty(self, action, + flags='', params='', fmt='-f json', has_output=True): + return self._run('cloudkitty', action, flags, params, fmt, has_output) diff --git a/cloudkittyclient/tests/functional/v1/test_collector.py b/cloudkittyclient/tests/functional/v1/test_collector.py new file mode 100644 index 0000000..a6c85db --- /dev/null +++ b/cloudkittyclient/tests/functional/v1/test_collector.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient.tests.functional.v1 import base + + +class CkCollectorTest(base.BaseFunctionalTest): + + def __init__(self, *args, **kwargs): + super(CkCollectorTest, self).__init__(*args, **kwargs) + self.runner = self.cloudkitty + + def test_create_get_delete_collector_mapping(self): + # Create Mapping + resp = self.runner( + 'collector-mapping create', params='compute gnocchi')[0] + self.assertEqual(resp['Collector'], 'gnocchi') + self.assertEqual(resp['Service'], 'compute') + + # Check that mapping is queryable + resp = self.runner('collector-mapping list') + self.assertEqual(len(resp), 1) + resp = resp[0] + self.assertEqual(resp['Collector'], 'gnocchi') + self.assertEqual(resp['Service'], 'compute') + + # Delete mapping + self.runner('collector-mapping delete', + params='compute', has_output=False) + + # Check that mapping was deleted + resp = self.runner('collector-mapping list') + self.assertEqual(len(resp), 0) + + def test_collector_enable_disable(self): + # Enable collector + resp = self.runner('collector enable gnocchi') + self.assertEqual(len(resp), 1) + resp = resp[0] + self.assertEqual(resp['Collector'], 'gnocchi') + self.assertEqual(resp['State'], True) + + # Disable collector + resp = self.runner('collector disable gnocchi') + self.assertEqual(len(resp), 1) + resp = resp[0] + self.assertEqual(resp['Collector'], 'gnocchi') + self.assertEqual(resp['State'], False) + + +class OSCCollectorTest(CkCollectorTest): + + def __init__(self, *args, **kwargs): + super(OSCCollectorTest, self).__init__(*args, **kwargs) + self.runner = self.openstack diff --git a/cloudkittyclient/tests/functional/v1/test_hashmap.py b/cloudkittyclient/tests/functional/v1/test_hashmap.py new file mode 100644 index 0000000..3d7957c --- /dev/null +++ b/cloudkittyclient/tests/functional/v1/test_hashmap.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient.tests.functional.v1 import base + + +class CkHashmapTest(base.BaseFunctionalTest): + + def __init__(self, *args, **kwargs): + super(CkHashmapTest, self).__init__(*args, **kwargs) + self.runner = self.cloudkitty + + def setUp(self): + super(CkHashmapTest, self).setUp() + self._fields = list() + self._services = list() + self._mappings = list() + self._groups = list() + self._thresholds = list() + + def tearDown(self): + super(CkHashmapTest, self).tearDown() + for field in self._fields: + try: + self.runner( + 'hashmap field delete', params=field, has_output=False) + except RuntimeError: + pass + for service in self._services: + try: + self.runner( + 'hashmap service delete', params=service, has_output=False) + except RuntimeError: + pass + for group in self._groups: + try: + self.runner( + 'hashmap group delete', params=group, has_output=False) + except RuntimeError: + pass + for mapping in self._mappings: + try: + self.runner( + 'hashmap mapping delete', params=mapping, has_output=False) + except RuntimeError: + pass + for threshold in self._thresholds: + try: + self.runner('hashmap threshold delete', + params=threshold, has_output=False) + except RuntimeError: + pass + + def test_list_mapping_types(self): + resp = self.runner('hashmap mapping-types list') + found_types = [elem['Mapping types'] for elem in resp] + self.assertIn('flat', found_types) + self.assertIn('rate', found_types) + + def test_create_get_delete_service(self): + # Create service + resp = self.runner('hashmap service create', params='testservice')[0] + self.assertEqual(resp['Name'], 'testservice') + service_id = resp['Service ID'] + self._services.append(service_id) + + # Check that resp is the same with service get and list + resp_with_sid = self.runner( + 'hashmap service get', params=service_id) + resp_without_sid = self.runner('hashmap service list') + self.assertEqual(resp_with_sid, resp_without_sid) + self.assertEqual(len(resp_with_sid), 1) + + # Check that deletion works + self.runner('hashmap service delete', + params=resp['Service ID'], + has_output=False) + resp = self.runner('hashmap service list') + self.assertEqual(len(resp), 0) + + def test_group_get_create_delete(self): + # Create group + resp = self.runner('hashmap group create', params='testgroup')[0] + self.assertEqual(resp['Name'], 'testgroup') + group_id = resp['Group ID'] + self._groups.append(group_id) + + resp = self.runner('hashmap group list') + self.assertEqual(len(resp), 1) + + # Check that deletion works + self.runner('hashmap group delete', + params=group_id, has_output=False) + resp = self.runner('hashmap group list') + self.assertEqual(len(resp), 0) + + def test_create_get_delete_field(self): + # Create service + resp = self.runner('hashmap service create', params='testservice')[0] + service_id = resp['Service ID'] + self._services.append(service_id) + + # Create field + resp = self.runner('hashmap field create', + params='{} testfield'.format(service_id))[0] + self.assertEqual(resp['Name'], 'testfield') + self.assertEqual(resp['Service ID'], service_id) + field_id = resp['Field ID'] + self._fields.append(field_id) + + # Check that resp is the same with field get and list + resp_with_fid = self.runner('hashmap field get', params=field_id) + resp_with_sid = self.runner('hashmap field list', params=service_id) + self.assertEqual(resp_with_fid, resp_with_sid) + self.assertEqual(len(resp_with_fid), 1) + + # Check that deletion works + self.runner( + 'hashmap field delete', params=field_id, has_output=False) + # resp = self.runner( + # 'hashmap field list', params='-s {}'.format(service_id)) + resp = self.runner( + 'hashmap field list', params=service_id) + self.assertEqual(len(resp), 0) + + def test_create_get_update_delete_mapping_service(self): + resp = self.runner('hashmap service create', params='testservice')[0] + service_id = resp['Service ID'] + self._services.append(service_id) + + # Create mapping + resp = self.runner('hashmap mapping create', + params='-s {} 12'.format(service_id))[0] + mapping_id = resp['Mapping ID'] + self._mappings.append(mapping_id) + self.assertEqual(resp['Service ID'], service_id) + self.assertEqual(float(resp['Cost']), float(12)) + + # Get mapping + resp_with_sid = self.runner( + 'hashmap mapping list', params='-s {}'.format(service_id))[0] + resp_with_mid = self.runner( + 'hashmap mapping get', params=mapping_id)[0] + self.assertEqual(resp_with_sid, resp_with_mid) + self.assertEqual(resp_with_sid['Mapping ID'], mapping_id) + self.assertEqual(resp_with_sid['Service ID'], service_id) + self.assertEqual(float(resp_with_sid['Cost']), float(12)) + + # Update mapping + resp = self.runner('hashmap mapping update', + params='--cost 10 {}'.format(mapping_id))[0] + self.assertEqual(float(resp['Cost']), float(10)) + + # Check that deletion works + self.runner( + 'hashmap mapping delete', params=mapping_id, has_output=False) + resp = self.runner( + 'hashmap mapping list', params='-s {}'.format(service_id)) + self.assertEqual(len(resp), 0) + self.runner( + 'hashmap service delete', params=service_id, has_output=False) + + def test_create_get_update_delete_mapping_field(self): + resp = self.runner('hashmap service create', params='testservice')[0] + service_id = resp['Service ID'] + self._services.append(service_id) + + resp = self.runner('hashmap field create', + params='{} testfield'.format(service_id))[0] + field_id = resp['Field ID'] + self._fields.append(field_id) + + # Create mapping + resp = self.runner( + 'hashmap mapping create', + params='--field-id {} 12 --value testvalue'.format(field_id))[0] + mapping_id = resp['Mapping ID'] + self._mappings.append(service_id) + self.assertEqual(resp['Field ID'], field_id) + self.assertEqual(float(resp['Cost']), float(12)) + self.assertEqual(resp['Value'], 'testvalue') + + # Get mapping + resp = self.runner( + 'hashmap mapping get', params=mapping_id)[0] + self.assertEqual(resp['Mapping ID'], mapping_id) + self.assertEqual(float(resp['Cost']), float(12)) + + # Update mapping + resp = self.runner('hashmap mapping update', + params='--cost 10 {}'.format(mapping_id))[0] + self.assertEqual(float(resp['Cost']), float(10)) + + def test_group_mappings_get(self): + # Service and group + resp = self.runner('hashmap service create', params='testservice')[0] + service_id = resp['Service ID'] + self._services.append(service_id) + resp = self.runner('hashmap group create', params='testgroup')[0] + group_id = resp['Group ID'] + self._groups.append(group_id) + + # Create service mapping bleonging to testgroup + resp = self.runner( + 'hashmap mapping create', + params='-s {} -g {} 12'.format(service_id, group_id))[0] + mapping_id = resp['Mapping ID'] + self._mappings.append(mapping_id) + + resp = self.runner('hashmap group mappings get', params=group_id)[0] + self.assertEqual(resp['Group ID'], group_id) + self.assertEqual(float(resp['Cost']), float(12)) + + def test_create_get_update_delete_threshold_service(self): + resp = self.runner('hashmap service create', params='testservice')[0] + service_id = resp['Service ID'] + self._services.append(service_id) + + # Create threshold + resp = self.runner('hashmap threshold create', + params='-s {} 12 0.9'.format(service_id))[0] + threshold_id = resp['Threshold ID'] + self._thresholds.append(threshold_id) + self.assertEqual(resp['Service ID'], service_id) + self.assertEqual(float(resp['Level']), float(12)) + self.assertEqual(float(resp['Cost']), float(0.9)) + + # Get threshold + resp_with_sid = self.runner( + 'hashmap threshold list', params='-s {}'.format(service_id))[0] + resp_with_tid = self.runner( + 'hashmap threshold get', params=threshold_id)[0] + self.assertEqual(resp_with_sid, resp_with_tid) + self.assertEqual(resp_with_sid['Threshold ID'], threshold_id) + self.assertEqual(resp_with_sid['Service ID'], service_id) + self.assertEqual(float(resp_with_sid['Level']), float(12)) + self.assertEqual(float(resp_with_sid['Cost']), float(0.9)) + + # Update threshold + resp = self.runner('hashmap threshold update', + params='--cost 10 {}'.format(threshold_id))[0] + self.assertEqual(float(resp['Cost']), float(10)) + + # Check that deletion works + self.runner( + 'hashmap threshold delete', params=threshold_id, has_output=False) + resp = self.runner( + 'hashmap threshold list', params='-s {}'.format(service_id)) + self.assertEqual(len(resp), 0) + + def test_create_get_update_delete_threshold_field(self): + resp = self.runner('hashmap service create', params='testservice')[0] + service_id = resp['Service ID'] + self._services.append(service_id) + + resp = self.runner('hashmap field create', + params='{} testfield'.format(service_id))[0] + field_id = resp['Field ID'] + self._fields.append(field_id) + + # Create threshold + resp = self.runner( + 'hashmap threshold create', + params='--field-id {} 12 0.9'.format(field_id))[0] + threshold_id = resp['Threshold ID'] + self._thresholds.append(service_id) + self.assertEqual(resp['Field ID'], field_id) + self.assertEqual(float(resp['Level']), float(12)) + self.assertEqual(float(resp['Cost']), float(0.9)) + + # Get threshold + resp = self.runner('hashmap threshold get', params=threshold_id)[0] + self.assertEqual(resp['Threshold ID'], threshold_id) + self.assertEqual(float(resp['Level']), float(12)) + self.assertEqual(float(resp['Cost']), float(0.9)) + + # Update threshold + resp = self.runner('hashmap threshold update', + params='--cost 10 {}'.format(threshold_id))[0] + self.assertEqual(float(resp['Cost']), float(10)) + + def test_group_thresholds_get(self): + # Service and group + resp = self.runner('hashmap service create', params='testservice')[0] + service_id = resp['Service ID'] + self._services.append(service_id) + resp = self.runner('hashmap group create', params='testgroup')[0] + group_id = resp['Group ID'] + self._groups.append(group_id) + + # Create service threshold bleonging to testgroup + resp = self.runner( + 'hashmap threshold create', + params='-s {} -g {} 12 0.9'.format(service_id, group_id))[0] + threshold_id = resp['Threshold ID'] + self._thresholds.append(threshold_id) + resp = self.runner('hashmap group thresholds get', params=group_id)[0] + self.assertEqual(resp['Group ID'], group_id) + self.assertEqual(float(resp['Level']), float(12)) + self.assertEqual(float(resp['Cost']), float(0.9)) + + +class OSCHashmapTest(CkHashmapTest): + + def __init__(self, *args, **kwargs): + super(OSCHashmapTest, self).__init__(*args, **kwargs) + self.runner = self.openstack diff --git a/cloudkittyclient/tests/functional/v1/test_info.py b/cloudkittyclient/tests/functional/v1/test_info.py new file mode 100644 index 0000000..9ed44c8 --- /dev/null +++ b/cloudkittyclient/tests/functional/v1/test_info.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import jsonpath_rw_ext as jp + +from cloudkittyclient.tests.functional.v1 import base + + +class CkInfoTest(base.BaseFunctionalTest): + + def __init__(self, *args, **kwargs): + super(CkInfoTest, self).__init__(*args, **kwargs) + self.runner = self.cloudkitty + + def test_info_config_get(self): + resp = self.runner('info config get') + for elem in resp: + if elem.get('Section') == 'name': + self.assertEqual(elem['Value'], 'OpenStack') + + def test_info_metric_list(self): + resp = self.runner('info metric list') + res = jp.match1('$.[*].Metric', resp) + self.assertIsNotNone(res) + + def test_info_service_get_image_size(self): + resp = self.runner('info metric get', params='image.size')[0] + self.assertEqual(resp['Metric'], 'image.size') + + +class OSCInfoTest(CkInfoTest): + + def __init__(self, *args, **kwargs): + super(OSCInfoTest, self).__init__(*args, **kwargs) + self.runner = self.openstack diff --git a/cloudkittyclient/tests/functional/v1/test_pyscripts.py b/cloudkittyclient/tests/functional/v1/test_pyscripts.py new file mode 100644 index 0000000..dbd7570 --- /dev/null +++ b/cloudkittyclient/tests/functional/v1/test_pyscripts.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient.tests.functional.v1 import base + + +class CkPyscriptTest(base.BaseFunctionalTest): + + def __init__(self, *args, **kwargs): + super(CkPyscriptTest, self).__init__(*args, **kwargs) + self.runner = self.cloudkitty + + def test_create_get_update_list_delete(self): + # Create + resp = self.runner( + 'pyscript create', params="testscript 'return 0'")[0] + script_id = resp['Script ID'] + self.assertEqual(resp['Name'], 'testscript') + + # Get + resp = self.runner('pyscript get', params=script_id)[0] + self.assertEqual(resp['Name'], 'testscript') + self.assertEqual(resp['Script ID'], script_id) + + # Update + resp = self.runner( + 'pyscript update', + params="-n newname -d 'return 1' {}".format(script_id))[0] + self.assertEqual(resp['Name'], 'newname') + self.assertEqual(resp['Script ID'], script_id) + self.assertEqual(resp['Data'], 'return 1') + + # List + resp = self.runner('pyscript list') + self.assertEqual(len(resp), 1) + resp = resp[0] + self.assertEqual(resp['Name'], 'newname') + self.assertEqual(resp['Script ID'], script_id) + self.assertEqual(resp['Data'], 'return 1') + + # Delete + self.runner('pyscript delete', params=script_id, has_output=False) + + +class OSCPyscriptTest(CkPyscriptTest): + + def __init__(self, *args, **kwargs): + super(CkPyscriptTest, self).__init__(*args, **kwargs) + self.runner = self.openstack diff --git a/cloudkittyclient/tests/functional/v1/test_rating.py b/cloudkittyclient/tests/functional/v1/test_rating.py new file mode 100644 index 0000000..38988cd --- /dev/null +++ b/cloudkittyclient/tests/functional/v1/test_rating.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient.tests.functional.v1 import base + + +class CkRatingTest(base.BaseFunctionalTest): + + def __init__(self, *args, **kwargs): + super(CkRatingTest, self).__init__(*args, **kwargs) + self.runner = self.cloudkitty + + def test_module_enable_get_disable(self): + # enable + resp = self.runner('module enable', params='hashmap')[0] + self.assertTrue(resp['Enabled']) + + # get + resp = self.runner('module get', params='hashmap')[0] + self.assertTrue(resp['Enabled']) + self.assertEqual(resp['Module'], 'hashmap') + + # disable + resp = self.runner('module disable', params='hashmap')[0] + self.assertFalse(resp['Enabled']) + + def test_module_set_priority(self): + resp = self.runner('module set priority', params='hashmap 100')[0] + self.assertEqual(resp['Priority'], 100) + + +class OSCRatingTest(CkRatingTest): + + def __init__(self, *args, **kwargs): + super(CkRatingTest, self).__init__(*args, **kwargs) + self.runner = self.openstack diff --git a/cloudkittyclient/tests/functional/v1/test_report.py b/cloudkittyclient/tests/functional/v1/test_report.py new file mode 100644 index 0000000..7ca8498 --- /dev/null +++ b/cloudkittyclient/tests/functional/v1/test_report.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient.tests.functional.v1 import base + + +class CkReportTest(base.BaseFunctionalTest): + def __init__(self, *args, **kwargs): + super(CkReportTest, self).__init__(*args, **kwargs) + self.runner = self.cloudkitty + + def test_get_summary(self): + resp = self.runner('summary get')[0] + self.assertEqual(resp['Resource Type'], 'ALL') + + def test_get_summary_with_groupby(self): + resp = self.runner('summary get', params='-g res_type tenant_id') + self.assertEqual(len(resp), 0) + + def test_get_total(self): + resp = self.runner('total get') + self.assertIn('Total', resp.keys()) + + def test_get_tenants(self): + self.runner('report tenant list') + + +class OSCReportTest(CkReportTest): + + def __init__(self, *args, **kwargs): + super(OSCReportTest, self).__init__(*args, **kwargs) + self.runner = self.openstack diff --git a/cloudkittyclient/v1/collector/mapping.py b/cloudkittyclient/tests/functional/v1/test_storage.py similarity index 51% rename from cloudkittyclient/v1/collector/mapping.py rename to cloudkittyclient/tests/functional/v1/test_storage.py index e7139cf..be170a6 100644 --- a/cloudkittyclient/v1/collector/mapping.py +++ b/cloudkittyclient/tests/functional/v1/test_storage.py @@ -1,4 +1,5 @@ -# Copyright 2015 Objectif Libre +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -11,20 +12,22 @@ # 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 cloudkittyclient.common import base +# +from cloudkittyclient.tests.functional.v1 import base -class Mapping(base.Resource): +class CkStorageTest(base.BaseFunctionalTest): - key = 'mapping' + def __init__(self, *args, **kwargs): + super(CkStorageTest, self).__init__(*args, **kwargs) + self.runner = self.cloudkitty - def __repr__(self): - return "" % self._info + def test_dataframes_get(self): + self.runner('dataframes get') -class MappingManager(base.CrudManager): - resource_class = Mapping - base_url = "/v1/collector" - key = "mapping" - collection_key = "mappings" +class OSCStorageTest(CkStorageTest): + + def __init__(self, *args, **kwargs): + super(CkStorageTest, self).__init__(*args, **kwargs) + self.runner = self.openstack diff --git a/cloudkittyclient/tests/test_client.py b/cloudkittyclient/tests/test_client.py deleted file mode 100644 index 7b601df..0000000 --- a/cloudkittyclient/tests/test_client.py +++ /dev/null @@ -1,159 +0,0 @@ -# Copyright 2015 Objectif Libre -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import types - -import mock - -from cloudkittyclient import client -from cloudkittyclient.tests import fakes -from cloudkittyclient.tests import utils -from cloudkittyclient.v1 import client as v1client - -FAKE_ENV = { - 'username': 'username', - 'password': 'password', - 'tenant_name': 'tenant_name', - 'auth_url': 'http://no.where', - 'os_endpoint': 'http://no.where', - 'auth_plugin': 'fake_auth', - 'token': '1234', - 'user_domain_name': 'default', - 'project_domain_name': 'default', -} - - -class ClientTest(utils.BaseTestCase): - - @staticmethod - def create_client(env, api_version=1, endpoint=None, exclude=[]): - env = dict((k, v) for k, v in env.items() - if k not in exclude) - - return client.get_client(api_version, **env) - - def setUp(self): - super(ClientTest, self).setUp() - - def test_client_v1_with_session(self): - resp = mock.Mock(status_code=200, text=b'') - resp.json.return_value = {"modules": []} - session = mock.Mock() - session.request.return_value = resp - c = client.get_client(1, session=session) - c.modules.list() - self.assertTrue(session.request.called) - self.assertTrue(resp.json.called) - - def test_client_version(self): - c1 = self.create_client(env=FAKE_ENV, api_version=1) - self.assertIsInstance(c1, v1client.Client) - - def test_client_auth_lambda(self): - env = FAKE_ENV.copy() - env['token'] = lambda: env['token'] - self.assertIsInstance(env['token'], - types.FunctionType) - c1 = self.create_client(env) - self.assertIsInstance(c1, v1client.Client) - - def test_client_auth_non_lambda(self): - env = FAKE_ENV.copy() - env['token'] = "1234" - self.assertIsInstance(env['token'], str) - c1 = self.create_client(env) - self.assertIsInstance(c1, v1client.Client) - - @mock.patch('keystoneclient.v2_0.client', fakes.FakeKeystone) - def test_client_without_auth_plugin(self): - env = FAKE_ENV.copy() - del env['auth_plugin'] - c = self.create_client(env, api_version=1, endpoint='fake_endpoint') - self.assertIsInstance(c.auth_plugin, client.AuthPlugin) - - def test_client_without_auth_plugin_keystone_v3(self): - env = FAKE_ENV.copy() - del env['auth_plugin'] - expected = { - 'username': 'username', - 'endpoint': 'http://no.where', - 'tenant_name': 'tenant_name', - 'service_type': None, - 'token': '1234', - 'endpoint_type': None, - 'auth_url': 'http://no.where', - 'tenant_id': None, - 'cacert': None, - 'password': 'password', - 'user_domain_name': 'default', - 'user_domain_id': None, - 'project_domain_name': 'default', - 'project_domain_id': None, - } - with mock.patch('cloudkittyclient.client.AuthPlugin') as auth_plugin: - self.create_client(env, api_version=1) - auth_plugin.assert_called_with(**expected) - - def test_client_with_auth_plugin(self): - c = self.create_client(FAKE_ENV, api_version=1) - self.assertIsInstance(c.auth_plugin, str) - - def test_v1_client_timeout_invalid_value(self): - env = FAKE_ENV.copy() - env['timeout'] = 'abc' - self.assertRaises(ValueError, self.create_client, env) - env['timeout'] = '1.5' - self.assertRaises(ValueError, self.create_client, env) - - def _test_v1_client_timeout_integer(self, timeout, expected_value): - env = FAKE_ENV.copy() - env['timeout'] = timeout - expected = { - 'auth_plugin': 'fake_auth', - 'timeout': expected_value, - 'original_ip': None, - 'http': None, - 'region_name': None, - 'verify': True, - 'timings': None, - 'keyring_saver': None, - 'cert': None, - 'endpoint_type': None, - 'user_agent': None, - 'debug': None, - } - cls = 'cloudkittyclient.apiclient.client.HTTPClient' - with mock.patch(cls) as mocked: - self.create_client(env) - mocked.assert_called_with(**expected) - - def test_v1_client_timeout_zero(self): - self._test_v1_client_timeout_integer(0, None) - - def test_v1_client_timeout_valid_value(self): - self._test_v1_client_timeout_integer(30, 30) - - def test_v1_client_cacert_in_verify(self): - env = FAKE_ENV.copy() - env['cacert'] = '/path/to/cacert' - client = self.create_client(env) - self.assertEqual('/path/to/cacert', - client.http_client.http_client.verify) - - def test_v1_client_certfile_and_keyfile(self): - env = FAKE_ENV.copy() - env['cert_file'] = '/path/to/cert' - env['key_file'] = '/path/to/keycert' - client = self.create_client(env) - self.assertEqual(('/path/to/cert', '/path/to/keycert'), - client.http_client.http_client.cert) diff --git a/cloudkittyclient/tests/test_cloudkittyclient.py b/cloudkittyclient/tests/test_cloudkittyclient.py deleted file mode 100644 index 1418f02..0000000 --- a/cloudkittyclient/tests/test_cloudkittyclient.py +++ /dev/null @@ -1,26 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -test_cloudkittyclient ----------------------------------- - -Tests for `cloudkittyclient` module. -""" - -from cloudkittyclient.tests import base - - -class TestCloudkittyclient(base.TestCase): - - def test_something(self): - pass diff --git a/cloudkittyclient/tests/v1/__init__.py b/cloudkittyclient/tests/unit/__init__.py similarity index 100% rename from cloudkittyclient/tests/v1/__init__.py rename to cloudkittyclient/tests/unit/__init__.py diff --git a/cloudkittyclient/tests/unit/v1/__init__.py b/cloudkittyclient/tests/unit/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloudkittyclient/tests/unit/v1/base.py b/cloudkittyclient/tests/unit/v1/base.py new file mode 100644 index 0000000..8e13b19 --- /dev/null +++ b/cloudkittyclient/tests/unit/v1/base.py @@ -0,0 +1,37 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cloudkittyclient.tests import utils +from cloudkittyclient.v1 import collector +from cloudkittyclient.v1 import info +from cloudkittyclient.v1 import rating +from cloudkittyclient.v1.rating import hashmap +from cloudkittyclient.v1.rating import pyscripts +from cloudkittyclient.v1 import report +from cloudkittyclient.v1 import storage + + +class BaseAPIEndpointTestCase(utils.BaseTestCase): + + def setUp(self): + super(BaseAPIEndpointTestCase, self).setUp() + self.api_client = utils.FakeHTTPClient() + self.storage = storage.StorageManager(self.api_client) + self.rating = rating.RatingManager(self.api_client) + self.collector = collector.CollectorManager(self.api_client) + self.info = info.InfoManager(self.api_client) + self.report = report.ReportManager(self.api_client) + self.pyscripts = pyscripts.PyscriptManager(self.api_client) + self.hashmap = hashmap.HashmapManager(self.api_client) diff --git a/cloudkittyclient/tests/unit/v1/test_collector.py b/cloudkittyclient/tests/unit/v1/test_collector.py new file mode 100644 index 0000000..a158baf --- /dev/null +++ b/cloudkittyclient/tests/unit/v1/test_collector.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient import exc +from cloudkittyclient.tests.unit.v1 import base + + +class TestCollector(base.BaseAPIEndpointTestCase): + + def test_get_mapping_no_args(self): + self.collector.get_mapping() + self.api_client.get.assert_called_once_with('/v1/collector/mappings/') + + def test_get_mapping_service_id(self): + self.collector.get_mapping(service='testservice') + self.api_client.get.assert_called_once_with( + '/v1/collector/mappings/testservice') + + def test_get_mapping_collector(self): + self.collector.get_mapping(collector='testcollector') + self.api_client.get.assert_called_once_with( + '/v1/collector/mappings/?collector=testcollector') + + def test_get_mapping_collector_service_id(self): + self.collector.get_mapping( + service='testservice', collector='testcollector') + self.api_client.get.assert_called_once_with( + '/v1/collector/mappings/testservice?collector=testcollector') + + def test_create_mapping(self): + kwargs = dict(service='testservice', collector='testcollector') + self.collector.create_mapping(**kwargs) + self.api_client.post.assert_called_once_with( + '/v1/collector/mappings/', json=kwargs) + + def test_create_mapping_no_name(self): + self.assertRaises(exc.ArgumentRequired, + self.collector.create_mapping, + collector='testcollector') + + def test_delete_mapping(self): + kwargs = dict(service='testservice') + self.collector.delete_mapping(**kwargs) + self.api_client.delete.assert_called_once_with( + '/v1/collector/mappings/', json=kwargs) + + def test_delete_mapping_no_service(self): + self.assertRaises(exc.ArgumentRequired, + self.collector.create_mapping) + + def test_get_state(self): + self.collector.get_state(name='testcollector') + self.api_client.get.assert_called_once_with( + '/v1/collector/states/?name=testcollector') + + def test_set_state(self): + kwargs = dict(name='testcollector', enabled=True) + self.collector.set_state(**kwargs) + self.api_client.put.assert_called_once_with( + '/v1/collector/states/', json=kwargs) diff --git a/cloudkittyclient/tests/unit/v1/test_hashmap.py b/cloudkittyclient/tests/unit/v1/test_hashmap.py new file mode 100644 index 0000000..eed33cb --- /dev/null +++ b/cloudkittyclient/tests/unit/v1/test_hashmap.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import mock + +from cloudkittyclient import exc +from cloudkittyclient.tests.unit.v1 import base +from cloudkittyclient.tests import utils + + +class TestHashmap(base.BaseAPIEndpointTestCase): + + def test_get_mapping_types(self): + self.hashmap.get_mapping_types() + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/types/') + + def test_get_service(self): + self.hashmap.get_service() + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/services/') + + def test_get_service_service_id(self): + self.hashmap.get_service(service_id='service_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/services/service_id') + + def test_create_service(self): + kwargs = dict(name='service') + self.hashmap.create_service(**kwargs) + self.api_client.post.assert_called_once_with( + '/v1/rating/module_config/hashmap/services/', json=kwargs) + + def test_create_service_no_name(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.create_service) + + def test_delete_service(self): + self.hashmap.delete_service(service_id='service_id') + self.api_client.delete.assert_called_once_with( + '/v1/rating/module_config/hashmap/services/', + json={'service_id': 'service_id'}) + + def test_delete_service_no_id(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.delete_service) + + def test_get_fields_of_service(self): + self.hashmap.get_field(service_id='service_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/fields/?service_id=service_id') + + def test_get_field(self): + self.hashmap.get_field(field_id='field_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/fields/field_id') + + def test_get_field_no_args(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.get_field) + + def test_get_field_with_service_id_and_field_id(self): + self.assertRaises(exc.InvalidArgumentError, self.hashmap.get_field, + service_id='service_id', field_id='field_id') + + def test_create_field(self): + kwargs = dict(name='name', service_id='service_id') + self.hashmap.create_field(**kwargs) + self.api_client.post.assert_called_once_with( + '/v1/rating/module_config/hashmap/fields/', json=kwargs) + + def test_create_field_no_name(self): + self.assertRaises(exc.ArgumentRequired, + self.hashmap.create_field, + service_id='service_id') + + def test_create_field_no_service_id(self): + self.assertRaises( + exc.ArgumentRequired, self.hashmap.create_field, name='name') + + def test_delete_field(self): + kwargs = dict(field_id='field_id') + self.hashmap.delete_field(**kwargs) + self.api_client.delete.assert_called_once_with( + '/v1/rating/module_config/hashmap/fields/', json=kwargs) + + def test_delete_field_no_arg(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.delete_field) + + def test_get_mapping_with_id(self): + self.hashmap.get_mapping(mapping_id='mapping_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/mappings/mapping_id') + + def test_get_mapping_service_id(self): + self.hashmap.get_mapping(service_id='service_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/mappings/?service_id=service_id') + + def test_get_mapping_no_args(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.get_mapping) + + def test_create_mapping(self): + kwargs = dict(cost=2, value='value', field_id='field_id') + body = dict( + cost=kwargs.get('cost'), + value=kwargs.get('value'), + service_id=kwargs.get('service_id'), + field_id=kwargs.get('field_id'), + group_id=kwargs.get('group_id'), + tenant_id=kwargs.get('tenant_id'), + type=kwargs.get('type') or 'flat', + ) + self.hashmap.create_mapping(**kwargs) + self.api_client.post.assert_called_once_with( + '/v1/rating/module_config/hashmap/mappings/', json=body) + + def test_create_mapping_no_cost(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.create_mapping, + value='value', field_id='field_id') + + def test_create_mapping_no_id(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.create_mapping, + value='value', cost=12) + + def test_create_mapping_field_and_service_id(self): + self.assertRaises( + exc.InvalidArgumentError, self.hashmap.create_mapping, cost=12, + field_id='field_id', service_id='service_id') + + def test_create_mapping_value_and_service_id(self): + self.assertRaises( + exc.InvalidArgumentError, self.hashmap.create_mapping, + value='value', service_id='service_id', cost=0.8) + + def test_update_mapping(self): + kwargs = dict( + cost=12, + value='value', + service_id='service_id', + field_id='field_id', + tenant_id='tenant_id', + type='type', + mapping_id='mapping_id', + ) + fake_get = mock.Mock(return_value=utils.FakeRequest( + cost='Bad value', + value='Bad value', + service_id='Bad value', + field_id='Bad value', + tenant_id='Bad value', + type='Bad value', + mapping_id='mapping_id', + )) + self.api_client.get = fake_get + self.hashmap.update_mapping(**kwargs) + self.api_client.get.assert_called_with( + '/v1/rating/module_config/hashmap/mappings/mapping_id') + self.api_client.put.assert_called_once_with( + '/v1/rating/module_config/hashmap/mappings/', json=kwargs) + + def test_update_mapping_no_arg(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.update_mapping) + + def test_get_mapping_group(self): + self.hashmap.get_mapping_group(mapping_id='mapping_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/' + 'hashmap/mappings/group?mapping_id=mapping_id') + + def test_delete_mapping(self): + kwargs = dict(mapping_id='mapping_id') + self.hashmap.delete_mapping(**kwargs) + self.api_client.delete.assert_called_once_with( + '/v1/rating/module_config/hashmap/mappings/', json=kwargs) + + def test_delete_mapping_no_arg(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.delete_mapping) + + def test_get_mapping_group_no_arg(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.get_mapping_group) + + def test_get_group_no_arg(self): + self.hashmap.get_group() + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/groups/') + + def test_get_group(self): + self.hashmap.get_group(group_id='group_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/groups/group_id') + + def test_create_group(self): + kwargs = dict(name='group') + self.hashmap.create_group(**kwargs) + self.api_client.post.assert_called_once_with( + '/v1/rating/module_config/hashmap/groups/', + json=kwargs) + + def test_create_group_no_name(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.create_group) + + def test_delete_group(self): + kwargs = dict(group_id='group_id') + self.hashmap.delete_group(**kwargs) + kwargs['recursive'] = False + self.api_client.delete.assert_called_once_with( + '/v1/rating/module_config/hashmap/groups/', + json=kwargs) + + def test_delete_group_recursive(self): + kwargs = dict(group_id='group_id', recursive=True) + self.hashmap.delete_group(**kwargs) + self.api_client.delete.assert_called_once_with( + '/v1/rating/module_config/hashmap/groups/', + json=kwargs) + + def test_delete_group_no_id(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.create_group) + + def test_get_group_mappings(self): + self.hashmap.get_group_mappings(group_id='group_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/groups/mappings' + '?group_id=group_id') + + def test_get_group_mappings_no_args(self): + self.assertRaises( + exc.ArgumentRequired, self.hashmap.get_group_mappings) + + def test_get_group_thresholds(self): + self.hashmap.get_group_thresholds(group_id='group_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/groups/thresholds' + '?group_id=group_id') + + def test_get_group_thresholds_no_args(self): + self.assertRaises( + exc.ArgumentRequired, self.hashmap.get_group_thresholds) + + def test_get_threshold_with_id(self): + self.hashmap.get_threshold(threshold_id='threshold_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/thresholds/threshold_id') + + def test_get_threshold_service_id(self): + self.hashmap.get_threshold(service_id='service_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/hashmap/thresholds/' + '?service_id=service_id') + + def test_get_threshold_no_args(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.get_threshold) + + def test_create_threshold(self): + kwargs = dict(cost=2, level=123, field_id='field_id') + body = dict( + cost=kwargs.get('cost'), + level=kwargs.get('level'), + service_id=kwargs.get('service_id'), + field_id=kwargs.get('field_id'), + group_id=kwargs.get('group_id'), + tenant_id=kwargs.get('tenant_id'), + type=kwargs.get('type') or 'flat', + ) + self.hashmap.create_threshold(**kwargs) + self.api_client.post.assert_called_once_with( + '/v1/rating/module_config/hashmap/thresholds/', json=body) + + def test_create_threshold_no_cost(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.create_threshold, + level=123, field_id='field_id') + + def test_create_threshold_no_id(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.create_threshold, + level=123, cost=12) + + def test_create_threshold_field_and_service_id(self): + self.assertRaises( + exc.ArgumentRequired, self.hashmap.create_threshold, cost=12, + field_id='field_id', service_id='service_id') + + def test_delete_threshold(self): + kwargs = dict(threshold_id='threshold_id') + self.hashmap.delete_threshold(**kwargs) + self.api_client.delete.assert_called_once_with( + '/v1/rating/module_config/hashmap/thresholds/', json=kwargs) + + def test_delete_threshold_no_arg(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.delete_threshold) + + def test_update_threshold(self): + kwargs = dict( + cost=12, + level=123, + service_id='service_id', + field_id='field_id', + tenant_id='tenant_id', + type='type', + threshold_id='threshold_id' + ) + fake_get = mock.Mock(return_value=utils.FakeRequest( + cost='Bad value', + level='Bad value', + service_id='Bad value', + field_id='Bad value', + tenant_id='Bad value', + type='Bad value', + threshold_id='threshold_id' + )) + self.api_client.get = fake_get + self.hashmap.update_threshold(**kwargs) + self.api_client.get.assert_called_with( + '/v1/rating/module_config/hashmap/thresholds/threshold_id') + self.api_client.put.assert_called_once_with( + '/v1/rating/module_config/hashmap/thresholds/', json=kwargs) + + def test_update_threshold_no_arg(self): + self.assertRaises(exc.ArgumentRequired, self.hashmap.update_threshold) + + def test_get_threshold_group(self): + self.hashmap.get_threshold_group(threshold_id='threshold_id') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/' + 'hashmap/thresholds/group?threshold_id=threshold_id') + + def test_get_threshold_group_no_arg(self): + self.assertRaises( + exc.ArgumentRequired, self.hashmap.get_threshold_group) diff --git a/cloudkittyclient/tests/unit/v1/test_info.py b/cloudkittyclient/tests/unit/v1/test_info.py new file mode 100644 index 0000000..f4b5a21 --- /dev/null +++ b/cloudkittyclient/tests/unit/v1/test_info.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient.tests.unit.v1 import base + + +class TestInfo(base.BaseAPIEndpointTestCase): + + def test_get_metric(self): + self.info.get_metric() + self.api_client.get.assert_called_once_with('/v1/info/metrics/') + + def test_get_metric_with_arg(self): + self.info.get_metric(metric_name='testmetric') + self.api_client.get.assert_called_once_with( + '/v1/info/metrics/testmetric') + + def test_get_config(self): + self.info.get_config() + self.api_client.get.assert_called_once_with('/v1/info/config/') diff --git a/cloudkittyclient/tests/unit/v1/test_pyscripts.py b/cloudkittyclient/tests/unit/v1/test_pyscripts.py new file mode 100644 index 0000000..be38488 --- /dev/null +++ b/cloudkittyclient/tests/unit/v1/test_pyscripts.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient import exc +from cloudkittyclient.tests.unit.v1 import base + + +class TestPyscripts(base.BaseAPIEndpointTestCase): + + def test_list_scripts(self): + self.pyscripts.list_scripts() + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/pyscripts/scripts/') + + def test_list_scripts_no_data(self): + self.pyscripts.list_scripts(no_data=True) + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/pyscripts/scripts/?no_data=True') + + def test_get_script(self): + self.pyscripts.get_script(script_id='testscript') + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/pyscripts/scripts/testscript') + + def test_get_script_no_arg(self): + self.assertRaises(exc.ArgumentRequired, self.pyscripts.get_script) + + def test_create_script(self): + kwargs = dict(name='name', data='data') + self.pyscripts.create_script(**kwargs) + self.api_client.post.assert_called_once_with( + '/v1/rating/module_config/pyscripts/scripts/', json=kwargs) + + def test_create_script_no_data(self): + self.assertRaises( + exc.ArgumentRequired, self.pyscripts.create_script, name='name') + + def test_create_script_no_name(self): + self.assertRaises( + exc.ArgumentRequired, self.pyscripts.create_script, data='data') + + def test_update_script(self): + args = dict(script_id='script_id', name='name', data='data') + self.pyscripts.update_script(**args) + self.api_client.get.assert_called_once_with( + '/v1/rating/module_config/pyscripts/scripts/script_id') + args.pop('script_id', None) + self.api_client.put.assert_called_once_with( + '/v1/rating/module_config/pyscripts/scripts/script_id', json=args) + + def test_update_script_no_script_id(self): + self.assertRaises( + exc.ArgumentRequired, self.pyscripts.update_script, name='name') + + def test_delete_script(self): + kwargs = dict(script_id='script_id') + self.pyscripts.delete_script(**kwargs) + self.api_client.delete.assert_called_once_with( + '/v1/rating/module_config/pyscripts/scripts/script_id') diff --git a/cloudkittyclient/tests/unit/v1/test_report.py b/cloudkittyclient/tests/unit/v1/test_report.py new file mode 100644 index 0000000..ab84892 --- /dev/null +++ b/cloudkittyclient/tests/unit/v1/test_report.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient.tests.unit.v1 import base + + +class TestReport(base.BaseAPIEndpointTestCase): + + def test_get_summary(self): + self.report.get_summary() + self.api_client.get.assert_called_once_with('/v1/report/summary') + + def test_get_summary_with_groupby(self): + self.report.get_summary(groupby=['res_type', 'tenant_id']) + self.api_client.get.assert_called_once_with( + '/v1/report/summary?groupby=res_type%2Ctenant_id') + + def test_get_summary_with_begin_end(self): + self.report.get_summary(begin='begin', end='end') + try: + self.api_client.get.assert_called_once_with( + '/v1/report/summary?begin=begin&end=end') + # Passing a dict to urlencode can change arg order + except AssertionError: + self.api_client.get.assert_called_once_with( + '/v1/report/summary?end=end&begin=begin') + + def test_get_total(self): + self.report.get_total() + self.api_client.get.assert_called_once_with('/v1/report/total') + + def test_get_total_with_begin_end(self): + self.report.get_total(begin='begin', end='end') + try: + self.api_client.get.assert_called_once_with( + '/v1/report/total?begin=begin&end=end') + # Passing a dict to urlencode can change arg order + except AssertionError: + self.api_client.get.assert_called_once_with( + '/v1/report/total?end=end&begin=begin') + + def test_get_tenants(self): + self.report.get_tenants() + self.api_client.get.assert_called_once_with('/v1/report/tenants') + + def test_get_tenants_with_begin_end(self): + self.report.get_tenants(begin='begin', end='end') + try: + self.api_client.get.assert_called_once_with( + '/v1/report/tenants?begin=begin&end=end') + # Passing a dict to urlencode can change arg order + except AssertionError: + self.api_client.get.assert_called_once_with( + '/v1/report/tenants?end=end&begin=begin') diff --git a/cloudkittyclient/tests/unit/v1/test_storage.py b/cloudkittyclient/tests/unit/v1/test_storage.py new file mode 100644 index 0000000..fd37d1e --- /dev/null +++ b/cloudkittyclient/tests/unit/v1/test_storage.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient.tests.unit.v1 import base + + +class TestStorage(base.BaseAPIEndpointTestCase): + + def test_get_dataframes(self): + self.storage.get_dataframes() + self.api_client.get.assert_called_once_with('/v1/storage/dataframes') + + def test_get_dataframes_with_begin_end(self): + self.storage.get_dataframes(begin='begin', end='end') + try: + self.api_client.get.assert_called_once_with( + '/v1/storage/dataframes?begin=begin&end=end') + # Passing a dict to urlencode can change arg order + except AssertionError: + self.api_client.get.assert_called_once_with( + '/v1/storage/dataframes?end=end&begin=begin') diff --git a/cloudkittyclient/tests/utils.py b/cloudkittyclient/tests/utils.py index 57bc276..90fe223 100644 --- a/cloudkittyclient/tests/utils.py +++ b/cloudkittyclient/tests/utils.py @@ -12,13 +12,32 @@ # 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 fixtures import testtools +from keystoneauth1 import adapter +from keystoneauth1 import session +import mock + class BaseTestCase(testtools.TestCase): def setUp(self): super(BaseTestCase, self).setUp() self.useFixture(fixtures.FakeLogger()) + + +class FakeRequest(dict): + """Fake requests.Request object.""" + + def json(self): + return self + + +class FakeHTTPClient(adapter.Adapter): + """Keystone HTTP adapter with request methods being mocks""" + + def __init__(self): + super(FakeHTTPClient, self).__init__(session=session.Session()) + for attr in ('get', 'put', 'post', 'delete'): + setattr(self, attr, mock.Mock(return_value=FakeRequest())) diff --git a/cloudkittyclient/tests/v1/test_core.py b/cloudkittyclient/tests/v1/test_core.py deleted file mode 100644 index a4fac5c..0000000 --- a/cloudkittyclient/tests/v1/test_core.py +++ /dev/null @@ -1,159 +0,0 @@ -# Copyright 2015 Objectif Libre -# 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. -from cloudkittyclient.apiclient import client -from cloudkittyclient.apiclient import fake_client -from cloudkittyclient.tests import utils -import cloudkittyclient.v1.core - - -fixtures = { - '/v1/rating/modules': { - 'GET': ( - {}, - {'modules': [ - { - 'module_id': 'hashmap', - 'enabled': True, - 'priority': 1, - }, - { - 'module_id': 'noop', - 'enabled': False, - 'priority': 1, - }, - ]}, - ), - }, - '/v1/rating/modules/hashmap': { - 'GET': ( - {}, - { - 'module_id': 'hashmap', - 'enabled': True, - 'priority': 1, - } - ), - 'PUT': ( - {}, - { - 'module_id': 'hashmap', - 'enabled': False, - 'priority': 1, - } - ), - }, - '/v1/rating/modules/noop': { - 'GET': ( - {}, - { - 'module_id': 'noop', - 'enabled': False, - 'priority': 1, - } - ), - 'PUT': ( - {}, - { - 'module_id': 'noop', - 'enabled': True, - 'priority': 1, - } - ), - }, - '/v1/collectors': { - 'GET': ( - {}, - {'collectors': [ - { - 'module_id': 'ceilo', - 'enabled': True, - }, - ]}, - ), - }, -} - - -class CloudkittyModuleManagerTest(utils.BaseTestCase): - - def setUp(self): - super(CloudkittyModuleManagerTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = cloudkittyclient.v1.core.CloudkittyModuleManager(self.api) - - def test_list_all(self): - resources = list(self.mgr.list()) - expect = [ - 'GET', '/v1/rating/modules' - ] - self.http_client.assert_called(*expect) - self.assertEqual(2, len(resources)) - self.assertEqual('hashmap', resources[0].module_id) - self.assertEqual('noop', resources[1].module_id) - - def test_get_module_status(self): - resource = self.mgr.get(module_id='hashmap') - expect = [ - 'GET', '/v1/rating/modules/hashmap' - ] - self.http_client.assert_called(*expect) - self.assertEqual('hashmap', resource.module_id) - self.assertTrue(resource.enabled) - - -class CloudkittyModuleTest(utils.BaseTestCase): - - def setUp(self): - super(CloudkittyModuleTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = cloudkittyclient.v1.core.CloudkittyModuleManager(self.api) - - def test_enable(self): - self.ck_module = self.mgr.get(module_id='noop') - self.ck_module.enable() - # PUT /v1/rating/modules/noop - # body : {'enabled': True} - expect = [ - 'PUT', '/v1/rating/modules/noop', {'module_id': 'noop', - 'enabled': True, - 'priority': 1}, - ] - self.http_client.assert_called(*expect) - - def test_disable(self): - self.ck_module = self.mgr.get(module_id='hashmap') - self.ck_module.disable() - # PUT /v1/rating/modules/hashmap - # body : {'enabled': False} - expect = [ - 'PUT', '/v1/rating/modules/hashmap', {'module_id': 'hashmap', - 'enabled': False, - 'priority': 1}, - ] - self.http_client.assert_called(*expect) - - def test_set_priority(self): - self.ck_module = self.mgr.get(module_id='hashmap') - self.ck_module.set_priority(100) - # PUT /v1/rating/modules/hashmap - # body : {'priority': 100} - expect = [ - 'PUT', '/v1/rating/modules/hashmap', {'module_id': 'hashmap', - 'enabled': True, - 'priority': 100}, - ] - self.http_client.assert_called(*expect) diff --git a/cloudkittyclient/tests/v1/test_hashmap.py b/cloudkittyclient/tests/v1/test_hashmap.py deleted file mode 100644 index 7889ff6..0000000 --- a/cloudkittyclient/tests/v1/test_hashmap.py +++ /dev/null @@ -1,920 +0,0 @@ -# Copyright 2015 Objectif Libre -# 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. -from cloudkittyclient.apiclient import client -from cloudkittyclient.apiclient import fake_client -from cloudkittyclient.tests import utils -from cloudkittyclient.v1.rating import hashmap - -GROUP1 = { - 'group_id': 'aaa1c2e0-2c6b-4e75-987f-93661eef0fd5', - 'name': 'object_consumption'} - -GROUP2 = { - 'group_id': '36171313-9813-4456-bf40-0195b2c98d1e', - 'name': 'compute_instance'} - -GROUP3 = { - 'group_id': '1dc7d980-e80a-4449-888f-26686392f4cc', - 'name': 'networking'} - -SERVICE1 = { - 'service_id': '2451c2e0-2c6b-4e75-987f-93661eef0fd5', - 'name': 'compute'} - -SERVICE2 = { - 'service_id': '338dd381-2c25-4347-b14d-239194c6068c', - 'name': 'volume'} - -SERVICE3 = { - 'service_id': '2f5bc5be-3753-450f-9492-37a6dba2fa8a', - 'name': 'network'} - -SERVICE_MAPPING1 = { - 'mapping_id': 'ae6145c3-6b00-4954-b698-cbc36a3d6c4b', - 'service_id': SERVICE3['service_id'], - 'field_id': None, - 'group_id': None, - 'value': None, - 'cost': 0.50, - 'type': 'flat'} - -SERVICE_MAPPING1_PUT = { - 'mapping_id': SERVICE_MAPPING1['mapping_id'], - 'service_id': SERVICE3['service_id'], - 'field_id': None, - 'group_id': None, - 'value': None, - 'cost': 0.20, - 'type': SERVICE_MAPPING1['type']} - -SERVICE_THRESHOLD1 = { - 'threshold_id': '22e3ae52-a863-47c6-8994-6acdec200346', - 'service_id': SERVICE3['service_id'], - 'field_id': None, - 'group_id': GROUP3['group_id'], - 'level': 30, - 'cost': 5.98, - 'map_type': 'flat'} - -SERVICE_THRESHOLD1_PUT = { - 'threshold_id': SERVICE_THRESHOLD1['threshold_id'], - 'service_id': SERVICE3['service_id'], - 'group_id': SERVICE_THRESHOLD1['group_id'], - 'level': SERVICE_THRESHOLD1['level'], - 'cost': 5.99, - 'map_type': SERVICE_THRESHOLD1['map_type']} - -FIELD1 = { - 'field_id': 'a53db546-bac0-472c-be4b-5bf9f6117581', - 'service_id': SERVICE1['service_id'], - 'name': 'flavor'} - -FIELD2 = { - 'field_id': 'f818a5a6-da88-474c-bd33-184ed769be63', - 'service_id': SERVICE1['service_id'], - 'name': 'image_id'} - -FIELD3 = { - 'field_id': 'b9861ba3-26d8-4c39-bb66-c607d48ccfce', - 'service_id': SERVICE1['service_id'], - 'name': 'vcpus'} - -FIELD_MAPPING1 = { - 'mapping_id': 'bff0d209-a8e4-46f8-8c1a-f231db375dcb', - 'service_id': None, - 'field_id': FIELD1['field_id'], - 'group_id': GROUP2['group_id'], - 'value': 'm1.small', - 'cost': 0.50, - 'type': 'flat'} - -FIELD_MAPPING1_PUT = { - 'mapping_id': FIELD_MAPPING1['mapping_id'], - 'field_id': FIELD_MAPPING1['field_id'], - 'group_id': FIELD_MAPPING1['group_id'], - 'value': FIELD_MAPPING1['value'], - 'cost': 0.20, - 'type': FIELD_MAPPING1['type']} - -FIELD_MAPPING2 = { - 'mapping_id': '1f1a05f2-1549-4623-b70a-9ab5c69fcd91', - 'service_id': None, - 'field_id': FIELD1['field_id'], - 'group_id': None, - 'value': 'm1.tiny', - 'cost': 1.10, - 'type': 'flat'} - -FIELD_MAPPING3 = { - 'mapping_id': 'deb4efe8-77c4-40ca-b8ca-27ec4892fa5f', - 'service_id': None, - 'field_id': FIELD1['field_id'], - 'group_id': None, - 'value': 'm1.big', - 'cost': 1.50, - 'type': 'flat'} - -FIELD_THRESHOLD1 = { - 'threshold_id': 'a33aca4b-3c12-41c5-a153-134c705fdbe2', - 'service_id': None, - 'field_id': FIELD3['field_id'], - 'group_id': None, - 'level': 2, - 'cost': 1.2, - 'map_type': 'flat'} - -FIELD_THRESHOLD1_PUT = { - 'threshold_id': FIELD_THRESHOLD1['threshold_id'], - 'service_id': None, - 'field_id': FIELD3['field_id'], - 'group_id': None, - 'level': FIELD_THRESHOLD1['level'], - 'cost': 1.5, - 'map_type': FIELD_THRESHOLD1['map_type']} - -fixtures = { - # services - '/v1/rating/module_config/hashmap/services': { - 'GET': ( - {}, - {'services': - [ - SERVICE1, - SERVICE2, - SERVICE3 - ], - } - ), - }, - # a service - ('/v1/rating/module_config/hashmap/services/' + - SERVICE1['service_id']): { - 'GET': ( - {}, - SERVICE1 - ), - 'DELETE': ( - {}, - {}, - ), - }, - # a service - ('/v1/rating/module_config/hashmap/services/' + - SERVICE3['service_id']): { - 'GET': ( - {}, - SERVICE3 - ), - 'DELETE': ( - {}, - {}, - ), - }, - # a service mapping - ('/v1/rating/module_config/hashmap/mappings/' + - SERVICE_MAPPING1['mapping_id']): { - 'GET': ( - {}, - SERVICE_MAPPING1 - ), - 'PUT': ( - {}, - SERVICE_MAPPING1_PUT - ), - }, - # some service mappings - ('/v1/rating/module_config/hashmap/mappings?service_id=' + - SERVICE3['service_id']): { - 'GET': ( - {}, - {'mappings': - [ - SERVICE_MAPPING1 - ], - } - ), - 'PUT': ( - {}, - {}, - ), - }, - # a service threshold - ('/v1/rating/module_config/hashmap/thresholds/' + - SERVICE_THRESHOLD1['threshold_id']): { - 'GET': ( - {}, - SERVICE_THRESHOLD1 - ), - 'PUT': ( - {}, - SERVICE_THRESHOLD1_PUT - ), - 'DELETE': ( - {}, - {}, - ), - }, - # service thresholds - ('/v1/rating/module_config/hashmap/thresholds?service_id=' + - SERVICE3['service_id']): { - 'GET': ( - {}, - {'thresholds': - [ - SERVICE_THRESHOLD1 - ] - }, - ), - }, - # service thresholds in a group - ('/v1/rating/module_config/hashmap/thresholds?group_id=' + - GROUP3['group_id']): { - 'GET': ( - {}, - {'thresholds': - [ - SERVICE_THRESHOLD1 - ] - }, - ), - }, - # a field - ('/v1/rating/module_config/hashmap/fields/' + - FIELD1['field_id']): { - 'GET': ( - {}, - FIELD1 - ), - 'PUT': ( - {}, - {}, - ), - 'DELETE': ( - {}, - {}, - ), - }, - # a field - ('/v1/rating/module_config/hashmap/fields/' + - FIELD3['field_id']): { - 'GET': ( - {}, - FIELD3 - ), - 'PUT': ( - {}, - {}, - ), - }, - # some fields - ('/v1/rating/module_config/hashmap/fields?service_id=' + - SERVICE1['service_id']): { - 'GET': ( - {}, - {'fields': [ - FIELD1, - FIELD2, - FIELD3 - ] - }, - ), - 'PUT': ( - {}, - {}, - ), - }, - # a field mapping - ('/v1/rating/module_config/hashmap/mappings/' + - FIELD_MAPPING1['mapping_id']): { - 'GET': ( - {}, - FIELD_MAPPING1 - ), - 'PUT': ( - {}, - FIELD_MAPPING1_PUT - ), - 'DELETE': ( - {}, - {}, - ), - }, - # some mappings - ('/v1/rating/module_config/hashmap/mappings?field_id=' + - FIELD1['field_id']): { - 'GET': ( - {}, - {'mappings': - [ - FIELD_MAPPING1, - FIELD_MAPPING2, - FIELD_MAPPING3 - ], - } - ), - 'PUT': ( - {}, - {}, - ), - }, - # some mappings in a group - ('/v1/rating/module_config/hashmap/mappings?group_id=' + - GROUP2['group_id']): { - 'GET': ( - {}, - {'mappings': - [ - FIELD_MAPPING1, - ], - } - ), - 'PUT': ( - {}, - {}, - ), - }, - # a field threshold - ('/v1/rating/module_config/hashmap/thresholds/' + - FIELD_THRESHOLD1['threshold_id']): { - 'GET': ( - {}, - FIELD_THRESHOLD1 - ), - 'PUT': ( - {}, - FIELD_THRESHOLD1_PUT - ), - 'DELETE': ( - {}, - {}, - ), - }, - # field thresholds - ('/v1/rating/module_config/hashmap/thresholds?field_id=' + - FIELD3['field_id']): { - 'GET': ( - {}, - {'thresholds': - [ - FIELD_THRESHOLD1 - ] - }, - ), - }, - # some groups - '/v1/rating/module_config/hashmap/groups': { - 'GET': ( - {}, - {'groups': - [ - GROUP1, - GROUP2, - GROUP3 - ], - } - ), - }, - # a group - ('/v1/rating/module_config/hashmap/groups/' + - GROUP2['group_id']): { - 'GET': ( - {}, - GROUP2 - ), - 'DELETE': ( - {}, - {}, - ), - }, - # another group - ('/v1/rating/module_config/hashmap/groups/' + - GROUP3['group_id']): { - 'GET': ( - {}, - GROUP3 - ), - 'DELETE': ( - {}, - {}, - ), - }, - # recursive delete group - ('/v1/rating/module_config/hashmap/groups/' + - GROUP2['group_id'] + - '?recursive=True'): { - 'DELETE': ( - {}, - {}, - ), - }, -} - - -class ServiceManagerTest(utils.BaseTestCase): - - def setUp(self): - super(ServiceManagerTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = hashmap.ServiceManager(self.api) - - def test_list_services(self): - resources = list(self.mgr.list()) - expect = [ - 'GET', '/v1/rating/module_config/hashmap/services'] - self.http_client.assert_called(*expect) - self.assertEqual(3, len(resources)) - self.assertEqual( - SERVICE1['service_id'], - resources[0].service_id) - self.assertEqual(SERVICE1['name'], resources[0].name) - self.assertEqual(SERVICE2['name'], resources[1].name) - self.assertEqual(SERVICE3['name'], resources[2].name) - - def test_get_a_service(self): - resource = self.mgr.get( - service_id=SERVICE1['service_id']) - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/services/' + - SERVICE1['service_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(SERVICE1['service_id'], - resource.service_id) - self.assertEqual(SERVICE1['name'], - resource.name) - - def test_delete_a_service(self): - self.mgr.delete(service_id=SERVICE1['service_id']) - expect = [ - 'DELETE', ('/v1/rating/module_config/hashmap/services/' + - SERVICE1['service_id'])] - self.http_client.assert_called(*expect) - - -class ServiceTest(utils.BaseTestCase): - - def setUp(self): - super(ServiceTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = hashmap.ServiceManager(self.api) - self.resource = self.mgr.get(service_id=SERVICE3['service_id']) - - def test_get_fields(self): - self.resource = self.mgr.get( - service_id=SERVICE1['service_id']) - fields = self.resource.fields[:] - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/fields?service_id=' + - SERVICE1['service_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(3, len(fields)) - field = fields[0] - self.assertEqual(SERVICE1['service_id'], - field.service_id) - self.assertEqual(FIELD1['field_id'], - field.field_id) - self.assertEqual(FIELD1['name'], - field.name) - - def test_get_mappings(self): - mappings = self.resource.mappings[:] - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/mappings?service_id=' + - SERVICE3['service_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(1, len(mappings)) - mapping = mappings[0] - self.assertEqual(SERVICE3['service_id'], - mapping.service_id) - self.assertEqual(SERVICE_MAPPING1['mapping_id'], - mapping.mapping_id) - self.assertEqual(SERVICE_MAPPING1['value'], mapping.value) - self.assertEqual(SERVICE_MAPPING1['cost'], mapping.cost) - self.assertEqual(SERVICE_MAPPING1['type'], mapping.type) - - def test_get_thresholds(self): - thresholds = self.resource.thresholds[:] - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/thresholds?service_id=' + - SERVICE3['service_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(1, len(thresholds)) - threshold = thresholds[0] - self.assertEqual(SERVICE_THRESHOLD1['service_id'], - threshold.service_id) - self.assertEqual(SERVICE_THRESHOLD1['threshold_id'], - threshold.threshold_id) - self.assertEqual(SERVICE_THRESHOLD1['level'], threshold.level) - self.assertEqual(SERVICE_THRESHOLD1['cost'], threshold.cost) - self.assertEqual(SERVICE_THRESHOLD1['map_type'], threshold.map_type) - - -class FieldManagerTest(utils.BaseTestCase): - - def setUp(self): - super(FieldManagerTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = hashmap.FieldManager(self.api) - - def test_list_fields(self): - resources = list(self.mgr.list(service_id=SERVICE1['service_id'])) - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/fields?service_id=' + - SERVICE1['service_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(3, len(resources)) - self.assertEqual(SERVICE1['service_id'], - resources[0].service_id) - self.assertEqual(FIELD1['name'], resources[0].name) - self.assertEqual(FIELD2['name'], resources[1].name) - self.assertEqual(FIELD3['name'], resources[2].name) - - def test_get_a_field(self): - resource = self.mgr.get( - field_id=FIELD1['field_id']) - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/fields/' + - FIELD1['field_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(FIELD1['field_id'], resource.field_id) - self.assertEqual(SERVICE1['service_id'], resource.service_id) - self.assertEqual(FIELD1['name'], resource.name) - - def test_delete_a_field(self): - self.mgr.delete(field_id=FIELD1['field_id']) - expect = [ - 'DELETE', ('/v1/rating/module_config/hashmap/fields/' + - FIELD1['field_id'])] - self.http_client.assert_called(*expect) - - -class FieldTest(utils.BaseTestCase): - - def setUp(self): - super(FieldTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = hashmap.FieldManager(self.api) - self.resource = self.mgr.get(field_id=FIELD1['field_id']) - - def test_get_service(self): - service = self.resource.service - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/services/' + - SERVICE1['service_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(SERVICE1['service_id'], service.service_id) - self.assertEqual(SERVICE1['name'], service.name) - - def test_get_mappings(self): - mappings = self.resource.mappings[:] - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/mappings?field_id=' + - FIELD1['field_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(3, len(mappings)) - mapping = mappings[0] - self.assertEqual(FIELD1['field_id'], mapping.field_id) - self.assertEqual(FIELD_MAPPING1['mapping_id'], mapping.mapping_id) - self.assertEqual(FIELD_MAPPING1['value'], mapping.value) - self.assertEqual(FIELD_MAPPING1['cost'], mapping.cost) - self.assertEqual(FIELD_MAPPING1['type'], mapping.type) - - def test_get_thresholds(self): - resource = self.mgr.get(field_id=FIELD3['field_id']) - thresholds = resource.thresholds[:] - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/thresholds?field_id=' + - FIELD3['field_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(1, len(thresholds)) - threshold = thresholds[0] - self.assertEqual(FIELD3['field_id'], threshold.field_id) - self.assertEqual(FIELD_THRESHOLD1['threshold_id'], - threshold.threshold_id) - self.assertEqual(FIELD_THRESHOLD1['level'], - threshold.level) - self.assertEqual(FIELD_THRESHOLD1['cost'], - threshold.cost) - self.assertEqual(FIELD_THRESHOLD1['map_type'], - threshold.map_type) - - -class MappingManagerTest(utils.BaseTestCase): - - def setUp(self): - super(MappingManagerTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = hashmap.MappingManager(self.api) - - def test_get_mappings_by_group(self): - mappings = self.mgr.findall(group_id=GROUP2['group_id']) - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/mappings?group_id=' + - GROUP2['group_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(1, len(mappings)) - mapping = mappings[0] - self.assertEqual(FIELD1['field_id'], mapping.field_id) - self.assertEqual(FIELD_MAPPING1['group_id'], mapping.group_id) - self.assertEqual(FIELD_MAPPING1['mapping_id'], mapping.mapping_id) - self.assertEqual(FIELD_MAPPING1['value'], mapping.value) - self.assertEqual(FIELD_MAPPING1['cost'], mapping.cost) - self.assertEqual(FIELD_MAPPING1['type'], mapping.type) - - def test_get_a_mapping(self): - resource = self.mgr.get(mapping_id=FIELD_MAPPING1['mapping_id']) - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/mappings/' + - FIELD_MAPPING1['mapping_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(FIELD_MAPPING1['mapping_id'], resource.mapping_id) - self.assertEqual(FIELD1['field_id'], resource.field_id) - self.assertEqual(FIELD_MAPPING1['value'], resource.value) - self.assertEqual(FIELD_MAPPING1['cost'], resource.cost) - self.assertEqual(FIELD_MAPPING1['type'], resource.type) - - def test_update_a_mapping(self): - resource = self.mgr.get(mapping_id=FIELD_MAPPING1['mapping_id']) - resource.cost = 0.2 - self.mgr.update(**resource.dirty_fields) - expect = [ - 'PUT', ('/v1/rating/module_config/hashmap/mappings/' + - FIELD_MAPPING1['mapping_id']), - FIELD_MAPPING1_PUT] - self.http_client.assert_called(*expect) - - def test_delete_a_mapping(self): - self.mgr.delete(mapping_id=FIELD_MAPPING1['mapping_id']) - expect = [ - 'DELETE', ('/v1/rating/module_config/hashmap/mappings/' + - FIELD_MAPPING1['mapping_id'])] - self.http_client.assert_called(*expect) - - -class MappingTest(utils.BaseTestCase): - - def setUp(self): - super(MappingTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = hashmap.MappingManager(self.api) - self.resource = self.mgr.get(mapping_id=FIELD_MAPPING1['mapping_id']) - - def test_get_service_mapping_parent(self): - resource = self.mgr.get(mapping_id=SERVICE_MAPPING1['mapping_id']) - service = resource.service - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/services/' + - SERVICE3['service_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(SERVICE3['service_id'], service.service_id) - field = resource.field - self.assertIsNone(field) - - def test_get_field_mapping_parent(self): - service = self.resource.service - self.assertIsNone(service) - field = self.resource.field - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/fields/' + - FIELD1['field_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(FIELD1['field_id'], field.field_id) - - def test_get_group(self): - group = self.resource.group - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/groups/' + - GROUP2['group_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(GROUP2['group_id'], group.group_id) - self.assertEqual(GROUP2['name'], group.name) - - -class ThresholdManagerTest(utils.BaseTestCase): - - def setUp(self): - super(ThresholdManagerTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = hashmap.ThresholdManager(self.api) - - def test_get_thresholds_by_group(self): - mappings = self.mgr.findall(group_id=GROUP3['group_id']) - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/thresholds?group_id=' + - GROUP3['group_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(1, len(mappings)) - mapping = mappings[0] - self.assertEqual(SERVICE_THRESHOLD1['threshold_id'], - mapping.threshold_id) - self.assertEqual(SERVICE_THRESHOLD1['service_id'], - mapping.service_id) - self.assertEqual(SERVICE_THRESHOLD1['group_id'], - mapping.group_id) - self.assertEqual(SERVICE_THRESHOLD1['level'], - mapping.level) - self.assertEqual(SERVICE_THRESHOLD1['cost'], - mapping.cost) - self.assertEqual(SERVICE_THRESHOLD1['map_type'], - mapping.map_type) - - def test_get_a_threshold(self): - resource = self.mgr.get( - threshold_id=SERVICE_THRESHOLD1['threshold_id']) - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/thresholds/' + - SERVICE_THRESHOLD1['threshold_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(SERVICE_THRESHOLD1['threshold_id'], - resource.threshold_id) - self.assertEqual(SERVICE_THRESHOLD1['service_id'], - resource.service_id) - self.assertEqual(SERVICE_THRESHOLD1['level'], - resource.level) - self.assertEqual(SERVICE_THRESHOLD1['cost'], - resource.cost) - self.assertEqual(SERVICE_THRESHOLD1['map_type'], - resource.map_type) - - def test_update_a_threshold(self): - resource = self.mgr.get( - threshold_id=SERVICE_THRESHOLD1['threshold_id']) - resource.cost = 5.99 - self.mgr.update(**resource.dirty_fields) - expect = [ - 'PUT', ('/v1/rating/module_config/hashmap/thresholds/' + - SERVICE_THRESHOLD1['threshold_id']), - SERVICE_THRESHOLD1_PUT] - self.http_client.assert_called(*expect) - - def test_delete_a_threshold(self): - self.mgr.delete(threshold_id=SERVICE_THRESHOLD1['threshold_id']) - expect = [ - 'DELETE', ('/v1/rating/module_config/hashmap/thresholds/' + - SERVICE_THRESHOLD1['threshold_id'])] - self.http_client.assert_called(*expect) - - -class ThresholdTest(utils.BaseTestCase): - - def setUp(self): - super(ThresholdTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = hashmap.ThresholdManager(self.api) - self.resource = self.mgr.get( - threshold_id=SERVICE_THRESHOLD1['threshold_id']) - - def test_get_service_threshold_parent(self): - service = self.resource.service - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/services/' + - SERVICE3['service_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(SERVICE3['service_id'], service.service_id) - field = self.resource.field - self.assertIsNone(field) - - def test_get_field_mapping_parent(self): - resource = self.mgr.get( - threshold_id=FIELD_THRESHOLD1['threshold_id']) - service = resource.service - self.assertIsNone(service) - field = resource.field - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/fields/' + - FIELD3['field_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(FIELD3['field_id'], field.field_id) - - def test_get_group(self): - group = self.resource.group - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/groups/' + - GROUP3['group_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(GROUP3['group_id'], group.group_id) - self.assertEqual(GROUP3['name'], group.name) - - -class GroupManagerTest(utils.BaseTestCase): - - def setUp(self): - super(GroupManagerTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = hashmap.GroupManager(self.api) - - def test_get_a_group(self): - resource = self.mgr.get(group_id=GROUP2['group_id']) - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/groups/' + - GROUP2['group_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(GROUP2['group_id'], resource.group_id) - self.assertEqual(GROUP2['name'], resource.name) - - def test_list_groups(self): - resources = list(self.mgr.list()) - expect = [ - 'GET', '/v1/rating/module_config/hashmap/groups'] - self.http_client.assert_called(*expect) - self.assertEqual(3, len(resources)) - self.assertEqual( - resources[0].group_id, - GROUP1['group_id']) - self.assertEqual(GROUP1['name'], resources[0].name) - self.assertEqual(GROUP2['name'], resources[1].name) - self.assertEqual(GROUP3['name'], resources[2].name) - - def test_delete_a_group(self): - self.mgr.delete(group_id=GROUP2['group_id']) - expect = [ - 'DELETE', ('/v1/rating/module_config/hashmap/groups/' + - GROUP2['group_id'])] - self.http_client.assert_called(*expect) - - def test_delete_a_group_recursively(self): - self.mgr.delete(group_id=GROUP2['group_id'], - recursive=True) - expect = [ - 'DELETE', ('/v1/rating/module_config/hashmap/groups/' + - GROUP2['group_id'] + - '?recursive=True')] - self.http_client.assert_called(*expect) - - -class GroupTest(utils.BaseTestCase): - - def setUp(self): - super(GroupTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = hashmap.GroupManager(self.api) - self.resource = self.mgr.get(group_id=GROUP2['group_id']) - - def test_delete(self): - self.resource.delete() - expect = [ - 'DELETE', ('/v1/rating/module_config/hashmap/groups/' + - GROUP2['group_id'])] - self.http_client.assert_called(*expect) - - def test_delete_recursive(self): - self.resource.delete(recursive=True) - expect = [ - 'DELETE', ('/v1/rating/module_config/hashmap/groups/' + - GROUP2['group_id'] + - '?recursive=True')] - self.http_client.assert_called(*expect) - - def test_get_mappings(self): - mappings = self.resource.mappings[:] - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/mappings?group_id=' + - GROUP2['group_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(1, len(mappings)) - mapping = mappings[0] - self.assertEqual(FIELD1['field_id'], mapping.field_id) - self.assertEqual(FIELD_MAPPING1['mapping_id'], mapping.mapping_id) - self.assertEqual(FIELD_MAPPING1['value'], mapping.value) - self.assertEqual(FIELD_MAPPING1['cost'], mapping.cost) - self.assertEqual(FIELD_MAPPING1['type'], mapping.type) - - def test_get_thresholds(self): - resource = self.mgr.get(group_id=GROUP3['group_id']) - thresholds = resource.thresholds[:] - expect = [ - 'GET', ('/v1/rating/module_config/hashmap/thresholds?group_id=' + - GROUP3['group_id'])] - self.http_client.assert_called(*expect) - self.assertEqual(1, len(thresholds)) - threshold = thresholds[0] - self.assertEqual(SERVICE3['service_id'], threshold.service_id) - self.assertEqual(SERVICE_THRESHOLD1['threshold_id'], - threshold.threshold_id) - self.assertEqual(SERVICE_THRESHOLD1['level'], - threshold.level) - self.assertEqual(SERVICE_THRESHOLD1['cost'], - threshold.cost) - self.assertEqual(SERVICE_THRESHOLD1['map_type'], - threshold.map_type) diff --git a/cloudkittyclient/tests/v1/test_report.py b/cloudkittyclient/tests/v1/test_report.py deleted file mode 100644 index a0eccee..0000000 --- a/cloudkittyclient/tests/v1/test_report.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright 2015 Objectif Libre -# 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. -from cloudkittyclient.apiclient import client -from cloudkittyclient.apiclient import fake_client -from cloudkittyclient.tests import utils -import cloudkittyclient.v1.report - - -fixtures = { - '/v1/report/summary': { - 'GET': ( - {}, - {'summary': [ - { - 'tenant_id': 'ALL', - 'res_type': 'ALL', - 'begin': '2017-01-01T00:00:00', - 'end': '2017-02-01T00:00:00', - 'rate': '2325.29992' - }, - ]}, - ), - }, - '/v1/report/summary?tenant_id=649de47ad78a44bd8562b0aa84389b2b': { - 'GET': ( - {}, - {'summary': [ - { - 'tenant_id': '649de47ad78a44bd8562b0aa84389b2b', - 'res_type': 'ALL', - 'begin': '2017-01-01T00:00:00', - 'end': '2017-02-01T00:00:00', - 'rate': '990.14996' - }, - ]}, - ), - }, - '/v1/report/summary?service=compute': { - 'GET': ( - {}, - {'summary': [ - { - 'tenant_id': 'ALL', - 'res_type': 'compute', - 'begin': '2017-01-01T00:00:00', - 'end': '2017-02-01T00:00:00', - 'rate': '690.0' - }, - ]}, - ), - }, - '/v1/report/summary?groupby=res_type%2Ctenant_id': { - 'GET': ( - {}, - {'summary': [ - { - 'tenant_id': '3747afc360b64702a53bdd64dc1b8976', - 'res_type': 'compute', - 'begin': '2017-01-01T00:00:00', - 'end': '2017-02-01T00:00:00', - 'rate': '517.5' - }, - { - 'tenant_id': '3747afc360b64702a53bdd64dc1b8976', - 'res_type': 'volume', - 'begin': '2017-01-01T00:00:00', - 'end': '2017-02-01T00:00:00', - 'rate': '817.64996' - }, - { - 'tenant_id': '649de47ad78a44bd8562b0aa84389b2b', - 'res_type': 'compute', - 'begin': '2017-01-01T00:00:00', - 'end': '2017-02-01T00:00:00', - 'rate': '172.5' - }, - { - 'tenant_id': '649de47ad78a44bd8562b0aa84389b2b', - 'res_type': 'volume', - 'begin': '2017-01-01T00:00:00', - 'end': '2017-02-01T00:00:00', - 'rate': '817.64996' - }, - ]}, - ), - }, -} - - -class ReportSummaryManagerTest(utils.BaseTestCase): - - def setUp(self): - super(ReportSummaryManagerTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) - self.api = client.BaseClient(self.http_client) - self.mgr = cloudkittyclient.v1.report.ReportSummaryManager(self.api) - - def test_get_summary(self): - self.mgr.get_summary() - expect = [ - 'GET', '/v1/report/summary' - ] - self.http_client.assert_called(*expect) - - def test_get_summary_with_tenant(self): - self.mgr.get_summary(tenant_id='649de47ad78a44bd8562b0aa84389b2b') - expect = [ - 'GET', - '/v1/report/summary?tenant_id=649de47ad78a44bd8562b0aa84389b2b' - ] - self.http_client.assert_called(*expect) - - def test_get_summary_with_service(self): - self.mgr.get_summary(service='compute') - expect = [ - 'GET', - '/v1/report/summary?service=compute' - ] - self.http_client.assert_called(*expect) - - def test_get_summary_with_groupby(self): - self.mgr.get_summary(groupby='res_type,tenant_id') - expect = [ - 'GET', - '/v1/report/summary?groupby=res_type%2Ctenant_id' - ] - self.http_client.assert_called(*expect) diff --git a/cloudkittyclient/utils.py b/cloudkittyclient/utils.py new file mode 100644 index 0000000..0f59280 --- /dev/null +++ b/cloudkittyclient/utils.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import pbr.version + +from oslo_utils import timeutils + + +def get_version(): + """Returns cloudkittyclient's version.""" + return pbr.version.VersionInfo('python-cloudkittyclient').version_string() + + +def iso2dt(iso_date): + """iso8601 format to datetime.""" + iso_dt = timeutils.parse_isotime(iso_date) + trans_dt = timeutils.normalize_time(iso_dt) + return trans_dt + + +def get_client_from_osc(obj): + if hasattr(obj.app, 'client_manager'): + return obj.app.client_manager.rating + return obj.app.client + + +def dict_to_cols(dict_obj, cols): + """Converts a dict to a cliff-compatible value list. + + For cliff lister.Lister objects, you should use list_to_cols() instead + of this function. + 'cols' shouls be a list of (key, Name) tuples. + """ + values = [] + for col in cols: + values.append(dict_obj.get(col[0])) + return values + + +def list_to_cols(list_obj, cols): + if not isinstance(list_obj, list): + list_obj = [list_obj] + values = [] + for item in list_obj: + values.append(dict_to_cols(item, cols)) + return values diff --git a/cloudkittyclient/v1/__init__.py b/cloudkittyclient/v1/__init__.py index e86e6fe..e69de29 100644 --- a/cloudkittyclient/v1/__init__.py +++ b/cloudkittyclient/v1/__init__.py @@ -1,16 +0,0 @@ -# Copyright 2015 Objectif Libre -# 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. - -from cloudkittyclient.v1.client import Client # noqa diff --git a/cloudkittyclient/v1/base.py b/cloudkittyclient/v1/base.py new file mode 100644 index 0000000..fc4a385 --- /dev/null +++ b/cloudkittyclient/v1/base.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from string import Formatter as StringFormatter + +from six.moves.urllib.parse import urlencode + + +class BaseManager(object): + """Base class for Endpoint Manager objects.""" + + url = '' + + def __init__(self, api_client): + self.api_client = api_client + self._formatter = StringFormatter() + + def _get_format_kwargs(self, **kwargs): + it = self._formatter.parse(self.url) + output = {i[1]: '' for i in it} + for key in output.keys(): + if kwargs.get(key): + output[key] = kwargs[key] + if 'endpoint' in output.keys(): + output.pop('endpoint') + return output + + def get_url(self, + endpoint, + kwargs, + authorized_args=[]): + """Returns the required url for a request against CloudKitty's API. + + :param endpoint: The endpoint on which the request should be done + :type endpoint: str + :param kwargs: kwargs that will be used to build the query (part after + '?' in the url) and to format the url. + :type kwargs: dict + :param authorized_args: The arguments that are authorized in url + parameters + :type authorized_args: list + """ + query_kwargs = { + key: kwargs[key] for key in authorized_args + if kwargs.get(key, None) + } + kwargs = self._get_format_kwargs(**kwargs) + url = self.url.format(endpoint=endpoint, **kwargs) + query = urlencode(query_kwargs) + if query: + url += '?' + query + return url diff --git a/cloudkittyclient/v1/client.py b/cloudkittyclient/v1/client.py index 886338d..d661069 100644 --- a/cloudkittyclient/v1/client.py +++ b/cloudkittyclient/v1/client.py @@ -1,5 +1,5 @@ -# Copyright 2015 Objectif Libre -# All Rights Reserved. +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -12,62 +12,30 @@ # 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 keystoneauth1 import adapter +from keystoneauth1 import session as ks_session -from stevedore import extension - -from cloudkittyclient import client as ckclient from cloudkittyclient.v1 import collector -from cloudkittyclient.v1 import core +from cloudkittyclient.v1 import info +from cloudkittyclient.v1 import rating from cloudkittyclient.v1 import report from cloudkittyclient.v1 import storage -SUBMODULES_NAMESPACE = 'cloudkitty.client.modules' - class Client(object): - """Client for the Cloudkitty v1 API. - :param session: a keystoneauth/keystoneclient session object - :type session: keystoneclient.session.Session - :param str service_type: The default service_type for URL discovery - :param str service_name: The default service_name for URL discovery - :param str interface: The default interface for URL discovery - (Default: public) - :param str region_name: The default region_name for URL discovery - :param str endpoint_override: Always use this endpoint URL for requests - for this cloudkittyclient - :param auth: An auth plugin to use instead of the session one - :type auth: keystoneclient.auth.base.BaseAuthPlugin - :param str user_agent: The User-Agent string to set - (Default is python-cloudkittyclient) - :param int connect_retries: the maximum number of retries that should be - attempted for connection errors - :param logger: A logging object - :type logger: logging.Logger - """ + def __init__(self, session=None, adapter_options={}, **kwargs): + adapter_options.setdefault('service_type', 'rating') - def __init__(self, *args, **kwargs): - """Initialize a new client for the Cloudkitty v1 API.""" + self.session = session + if self.session is None: + self.session = ks_session.Session(**kwargs) - if not kwargs.get('auth_plugin'): - kwargs['auth_plugin'] = ckclient.get_auth_plugin(*args, **kwargs) - self.auth_plugin = kwargs.get('auth_plugin') - - self.http_client = ckclient.construct_http_client(**kwargs) - self.modules = core.CloudkittyModuleManager(self.http_client) - self.collector = collector.CollectorManager(self.http_client) - self.reports = report.ReportManager(self.http_client) - self.reportsummary = report.ReportSummaryManager(self.http_client) - self.quotations = core.QuotationManager(self.http_client) - self.storage = storage.StorageManager(self.http_client) - self.config = core.ConfigInfoManager(self.http_client) - self.service_info = core.ServiceInfoManager(self.http_client) - self._expose_submodules() - - def _expose_submodules(self): - extensions = extension.ExtensionManager( - SUBMODULES_NAMESPACE, - ) - for ext in extensions: - client = ext.plugin.get_client(self.http_client) - setattr(self, ext.name, client) + self.api_client = adapter.Adapter( + session=self.session, **adapter_options) + self.info = info.InfoManager(self.api_client) + self.collector = collector.CollectorManager(self.api_client) + self.rating = rating.RatingManager(self.api_client) + self.report = report.ReportManager(self.api_client) + self.storage = storage.StorageManager(self.api_client) diff --git a/cloudkittyclient/v1/collector.py b/cloudkittyclient/v1/collector.py new file mode 100644 index 0000000..e0e3fa9 --- /dev/null +++ b/cloudkittyclient/v1/collector.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from oslo_log import log + +from cloudkittyclient import exc +from cloudkittyclient.v1 import base + + +LOG = log.getLogger(__name__) + + +class CollectorManager(base.BaseManager): + """Class used to handle /v1/collector/mappings endpoint""" + url = '/v1/collector/{endpoint}/{service_id}' + + def get_mapping(self, **kwargs): + """Returns a service to collector mapping. + + If the service is not specified, returns a list of mappings for the + given collector. + + :param service: Name of the service to filter on. + :type service: str + :param collector: Name of the collector to filter on. + :type collector: str + """ + LOG.warning('WARNING: Collector mappings are deprecated and will ' + 'be removed in a future release') + kwargs['service_id'] = kwargs.get('service') or '' + authorized_args = ['collector'] + url = self.get_url('mappings', kwargs, authorized_args) + return self.api_client.get(url).json() + + def create_mapping(self, **kwargs): + """Creates a service to collector mapping. + + :param service: Name of the service to filter on. + :type service: str + :param collector: Name of the collector to filter on. + :type collector: str + """ + LOG.warning('WARNING: Collector mappings are deprecated and will ' + 'be removed in a future release') + for arg in ('collector', 'service'): + if not kwargs.get(arg): + raise exc.ArgumentRequired( + "'{arg}' argument is required.".format(arg=arg)) + url = self.get_url('mappings', kwargs) + body = dict( + collector=kwargs['collector'], + service=kwargs['service']) + return self.api_client.post(url, json=body).json() + + def delete_mapping(self, **kwargs): + """Deletes a service to collector mapping. + + :param service: Name of the service of which the mapping + should be deleted. + :type service: str + """ + LOG.warning('WARNING: Collector mappings are deprecated and will ' + 'be removed in a future release') + if not kwargs.get('service'): + raise exc.ArgumentRequired("'service' argument is required.") + body = dict(service=kwargs['service']) + url = self.get_url('mappings', kwargs) + self.api_client.delete(url, json=body) + + def get_state(self, **kwargs): + """Returns the state of a collector. + + :param name: Name of the collector. + :type name: str + """ + LOG.warning('WARNING: Collector mappings are deprecated and will ' + 'be removed in a future release') + if not kwargs.get('name'): + raise exc.ArgumentRequired("'name' argument is required.") + authorized_args = ['name'] + url = self.get_url('states', kwargs, authorized_args) + return self.api_client.get(url).json() + + def set_state(self, **kwargs): + """Sets the state of the collector. + + :param name: Name of the collector + :type name: str + :param enabled: State of the collector + :type name: bool + """ + LOG.warning('WARNING: Collector mappings are deprecated and will ' + 'be removed in a future release') + if not kwargs.get('name'): + raise exc.ArgumentRequired("'name' argument is required.") + kwargs['enabled'] = kwargs.get('enabled') or False + url = self.get_url('states', kwargs) + body = dict( + name=kwargs['name'], + enabled=kwargs['enabled'], + ) + self.api_client.put(url, json=body) + return self.get_state(**kwargs) diff --git a/cloudkittyclient/v1/collector/__init__.py b/cloudkittyclient/v1/collector/__init__.py deleted file mode 100644 index 021ef74..0000000 --- a/cloudkittyclient/v1/collector/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2015 Objectif Libre -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from cloudkittyclient.v1.collector import mapping -from cloudkittyclient.v1.collector import state - - -class CollectorManager(object): - def __init__(self, http_client): - self.mappings = mapping.MappingManager(http_client) - self.states = state.StateManager(http_client) diff --git a/cloudkittyclient/v1/collector/shell.py b/cloudkittyclient/v1/collector/shell.py deleted file mode 100644 index 23cd7ae..0000000 --- a/cloudkittyclient/v1/collector/shell.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2015 Objectif Libre -# -# 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. - -from cloudkittyclient.common import utils - - -@utils.arg('-c', '--collector', - help='Collector name to filter on.', - required=False, - default=None) -def do_collector_mapping_list(cc, args): - """List collector mapping.""" - data = cc.collector.mappings.list(collector=args.collector) - fields = ['service', 'collector'] - fields_labels = ['Service', 'Collector'] - utils.print_list(data, fields, fields_labels, sortby=0) - - -@utils.arg('-s', '--service', - help='Which service to get the mapping for.', - required=True) -def do_collector_mapping_get(cc, args): - """Show collector mapping detail.""" - data = cc.collector.mappings.get(mapping_id=args.service) - utils.print_dict(data.to_dict()) - - -@utils.arg('-c', '--collector', - help='Map a service to this collector.', - required=True) -@utils.arg('-s', '--service', - help='Map a collector to this service.', - required=True) -def do_collector_mapping_create(cc, args): - """Create collector mapping.""" - out = cc.collector.mappings.create(service=args.service, - collector=args.collector) - utils.print_dict(out.to_dict()) - - -@utils.arg('-s', '--service', - help='Filter on this service.', - required=True) -def do_collector_mapping_delete(cc, args): - """Delete collector mapping.""" - # TODO(sheeprine): Use a less hacky way to do this - cc.collector.mappings.delete(mapping_id=args.service) - - -@utils.arg('-n', '--name', - help='Name of the collector.', - required=True) -def do_collector_state_get(cc, args): - """Show collector state.""" - data = cc.collector.states.get(state_id=args.name) - utils.print_dict(data.to_dict()) - - -@utils.arg('-n', '--name', - help='Name of the collector.', - required=True) -def do_collector_state_enable(cc, args): - """Enable collector state.""" - new_state = cc.collector.states.update(name=args.name, enabled=True) - utils.print_dict(new_state.to_dict()) - - -@utils.arg('-n', '--name', - help='Name of the collector.', - required=True) -def do_collector_state_disable(cc, args): - """Disable collector state.""" - new_state = cc.collector.states.update(name=args.name, enabled=False) - utils.print_dict(new_state.to_dict()) diff --git a/cloudkittyclient/v1/collector/shell_cli.py b/cloudkittyclient/v1/collector/shell_cli.py deleted file mode 100644 index bd03d72..0000000 --- a/cloudkittyclient/v1/collector/shell_cli.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright 2016 Objectif Libre -# -# 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. - -from osc_lib.command import command - -from cloudkittyclient.v1.collector import shell - - -class CliCollectorMappingList(command.Command): - """List collector mappings.""" - def get_parser(self, prog_name): - parser = super(CliCollectorMappingList, self).get_parser(prog_name) - parser.add_argument('-c', '--collector', - help='Collector name to filter on.', - required=False, - default=None) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_collector_mapping_list(ckclient, parsed_args) - - -class CliCollectorMappingGet(command.Command): - """Show collector mapping detail.""" - def get_parser(self, prog_name): - parser = super(CliCollectorMappingGet, self).get_parser(prog_name) - parser.add_argument('-s', '--service', - help='Which service to get the mapping for.', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_collector_mapping_get(ckclient, parsed_args) - - -class CliCollectorMappingCreate(command.Command): - """Create collector mappings.""" - def get_parser(self, prog_name): - parser = super(CliCollectorMappingCreate, self).get_parser(prog_name) - parser.add_argument('-c', '--collector', - help='Map a service to this collector.', - required=True) - parser.add_argument('-s', '--service', - help='Map a collector to this service.', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_collector_mapping_create(ckclient, parsed_args) - - -class CliCollectorMappingDelete(command.Command): - """Delete collector mappings.""" - def get_parser(self, prog_name): - parser = super(CliCollectorMappingDelete, self).get_parser(prog_name) - parser.add_argument('-s', '--service', - help='Filter on this service', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_collector_mapping_delete(ckclient, parsed_args) - - -class BaseCliCollectorState(command.Command): - def get_parser(self, prog_name): - parser = super(BaseCliCollectorState, self).get_parser(prog_name) - parser.add_argument('-n', '--name', - help='Name of the collector', - required=True) - return parser - - -class CliCollectorStateGet(BaseCliCollectorState): - """Show collector state.""" - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_collector_state_get(ckclient, parsed_args) - - -class CliCollectorStateEnable(BaseCliCollectorState): - """Enable collector state.""" - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_collector_state_enable(ckclient, parsed_args) - - -class CliCollectorStateDisable(BaseCliCollectorState): - """Disable collector state.""" - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_collector_state_disable(ckclient, parsed_args) diff --git a/cloudkittyclient/v1/collector/state.py b/cloudkittyclient/v1/collector/state.py deleted file mode 100644 index 573f0cb..0000000 --- a/cloudkittyclient/v1/collector/state.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2015 Objectif Libre -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from cloudkittyclient.common import base - - -class State(base.Resource): - - key = 'state' - - def __repr__(self): - return "" % self._info - - -class StateManager(base.CrudManager): - resource_class = State - base_url = "/v1/collector" - key = "state" - collection_key = "states" diff --git a/cloudkittyclient/v1/collector_cli.py b/cloudkittyclient/v1/collector_cli.py new file mode 100644 index 0000000..ae92511 --- /dev/null +++ b/cloudkittyclient/v1/collector_cli.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cliff import command +from cliff import lister + +from cloudkittyclient import utils + + +class CliCollectorMappingGet(lister.Lister): + """(DEPRECATED) Get a service to collector mapping.""" + + columns = [ + ('service', 'Service'), + ('collector', 'Collector'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).collector.get_mapping( + service=parsed_args.service, + ) + resp = [resp] if resp.get('mappings') is None else resp['mappings'] + values = utils.list_to_cols(resp, self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliCollectorMappingGet, self).get_parser(prog_name) + parser.add_argument('service', type=str, + help='Name of the service to filter on') + return parser + + +class CliCollectorMappingList(lister.Lister): + """(DEPRECATED) List service to collector mappings.""" + + columns = [ + ('service', 'Service'), + ('collector', 'Collector'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).collector.get_mapping( + collector=parsed_args.collector) + resp = [resp] if resp.get('mappings') is None else resp['mappings'] + values = utils.list_to_cols(resp, self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliCollectorMappingList, self).get_parser(prog_name) + parser.add_argument('--collector', type=str, + help='Name of the collector to filter on') + return parser + + +class CliCollectorMappingCreate(lister.Lister): + """(DEPRECATED) Create a service to collector mapping.""" + + columns = [ + ('service', 'Service'), + ('collector', 'Collector'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).collector.create_mapping( + **vars(parsed_args)) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliCollectorMappingCreate, self).get_parser(prog_name) + parser.add_argument('service', type=str, help='Name of the service') + parser.add_argument('collector', type=str, + help='Name of the collector') + return parser + + +class CliCollectorMappingDelete(command.Command): + """(DEPRECATED) Delete a service to collector mapping.""" + + def take_action(self, parsed_args): + utils.get_client_from_osc(self).collector.delete_mapping( + **vars(parsed_args)) + + def get_parser(self, prog_name): + parser = super(CliCollectorMappingDelete, self).get_parser(prog_name) + parser.add_argument('service', type=str, help='Name of the service') + return parser + + +class CliCollectorGetState(lister.Lister): + """(DEPRECATED) Get the state of a collector.""" + + columns = [ + ('name', 'Collector'), + ('enabled', 'State'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).collector.get_state( + **vars(parsed_args)) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliCollectorGetState, self).get_parser(prog_name) + parser.add_argument('name', type=str, help='Name of the collector') + return parser + + +class CliCollectorEnable(lister.Lister): + """(DEPRECATED) Enable a collector.""" + + columns = [ + ('name', 'Collector'), + ('enabled', 'State'), + ] + + def take_action(self, parsed_args): + parsed_args.enabled = True + resp = utils.get_client_from_osc(self).collector.set_state( + **vars(parsed_args)) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliCollectorEnable, self).get_parser(prog_name) + parser.add_argument('name', type=str, help='Name of the collector') + return parser + + +class CliCollectorDisable(CliCollectorEnable): + """(DEPRECATED) Disable a collector.""" + + def take_action(self, parsed_args): + parsed_args.disabled = True + resp = utils.get_client_from_osc(self).collector.set_state( + **vars(parsed_args)) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values diff --git a/cloudkittyclient/v1/core.py b/cloudkittyclient/v1/core.py deleted file mode 100644 index d3b3cc6..0000000 --- a/cloudkittyclient/v1/core.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2015 Objectif Libre -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from cloudkittyclient.common import base - - -class CloudkittyModule(base.Resource): - - key = 'module' - - def __repr__(self): - return "" % self._info - - def enable(self): - self.enabled = True - self.update() - - def disable(self): - self.enabled = False - self.update() - - def set_priority(self, value): - self.priority = value - self.update() - - -class CloudkittyModuleManager(base.CrudManager): - resource_class = CloudkittyModule - base_url = "/v1/rating" - key = 'module' - collection_key = "modules" - - -class Collector(base.Resource): - - key = 'collector' - - def __repr__(self): - return "" % self._info - - -class CollectorManager(base.Manager): - resource_class = Collector - base_url = "/v1/rating" - key = "collector" - collection_key = "collectors" - - -class QuotationManager(base.Manager): - base_url = "/v1/rating/quote" - - def quote(self, resources): - out = self.api.post(self.base_url, - json={'resources': resources}).json() - return out - - -class ServiceInfo(base.Resource): - - key = "service" - - def __repr__(self): - return "" % self._info - - -class ServiceInfoManager(base.CrudManager): - resource_class = ServiceInfo - base_url = "/v1/info" - key = "service" - collection_key = "services" - - -class ConfigInfoManager(base.Manager): - base_url = "/v1/info/config" - - def get_config(self): - out = self.api.get(self.base_url).json() - return out diff --git a/cloudkittyclient/v1/info.py b/cloudkittyclient/v1/info.py new file mode 100644 index 0000000..ad974a1 --- /dev/null +++ b/cloudkittyclient/v1/info.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient.v1 import base + + +class InfoManager(base.BaseManager): + """Class used to handle /v1/info endpoint""" + url = '/v1/info/{endpoint}/{metric_name}' + + def get_metric(self, **kwargs): + """Returns info for the given service. + + If metric_name is not specified, returns info for all services. + + :param metric_name: Name of the service on which you want information + :type metric_name: str + """ + url = self.get_url('metrics', kwargs) + return self.api_client.get(url).json() + + def get_config(self, **kwargs): + """Returns the current configuration.""" + url = self.get_url('config', kwargs) + return self.api_client.get(url).json() diff --git a/cloudkittyclient/v1/info_cli.py b/cloudkittyclient/v1/info_cli.py new file mode 100644 index 0000000..1e4b045 --- /dev/null +++ b/cloudkittyclient/v1/info_cli.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cliff import lister + +from cloudkittyclient import utils + + +class CliInfoMetricGet(lister.Lister): + """Get information about current metrics.""" + info_columns = [ + ('metric_id', 'Metric'), + ('unit', 'Unit'), + ('metadata', 'Metadata'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).info.get_metric( + metric_name=parsed_args.metric_name, + ) + values = utils.list_to_cols([resp], self.info_columns) + return [col[1] for col in self.info_columns], values + + def get_parser(self, prog_name): + parser = super(CliInfoMetricGet, self).get_parser(prog_name) + parser.add_argument('metric_name', + type=str, default='', help='Metric name') + return parser + + +class CliInfoMetricList(lister.Lister): + """Get information about a single metric.""" + info_columns = [ + ('metric_id', 'Metric'), + ('unit', 'Unit'), + ('metadata', 'Metadata'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).info.get_metric() + values = utils.list_to_cols(resp['metrics'], self.info_columns) + return [col[1] for col in self.info_columns], values + + +class CliInfoConfigGet(lister.Lister): + """Get information about the current configuration.""" + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).info.get_config() + values = [(key, value) for key, value in resp.items()] + return ('Section', 'Value'), values diff --git a/cloudkittyclient/v1/rating/__init__.py b/cloudkittyclient/v1/rating/__init__.py index e69de29..25ffdaf 100644 --- a/cloudkittyclient/v1/rating/__init__.py +++ b/cloudkittyclient/v1/rating/__init__.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cliff import lister + +from cloudkittyclient import exc +from cloudkittyclient import utils +from cloudkittyclient.v1 import base +from cloudkittyclient.v1.rating import hashmap +from cloudkittyclient.v1.rating import pyscripts + + +class RatingManager(base.BaseManager): + """Class used to handle /v1/rating endpoint""" + + url = '/v1/rating/{endpoint}/{module_id}' + + def __init__(self, api_client): + super(RatingManager, self).__init__(api_client) + self.hashmap = hashmap.HashmapManager(api_client) + self.pyscripts = pyscripts.PyscriptManager(api_client) + + def get_module(self, **kwargs): + """Returns the given module. + + If module_id is not specified, returns the list of loaded modules. + + :param module_id: ID of the module on which you want information. + :type module_id: str + """ + authorized_args = ['module_id'] + url = self.get_url('modules', kwargs, authorized_args) + return self.api_client.get(url).json() + + def update_module(self, **kwargs): + """Update the given module. + + :param module_id: Id of the module to update. + :type module_id: str + :param enabled: Set to True to enable the module, False to disable it. + :type enabled: bool + :param priority: New priority of the module. + :type priority: int + """ + if not kwargs.get('module_id', None): + raise exc.ArgumentRequired("'module_id' argument is required.") + url = self.get_url('modules', kwargs) + module = self.get_module(**kwargs) + for key in module.keys(): + value = kwargs.get(key, None) + if value is not None and module[key] != value: + module[key] = value + self.api_client.put(url, json=module) + return self.get_module(**kwargs) + + def reload_modules(self, **kwargs): + """Triggers a reload of all rating modules.""" + url = self.get_url('reload_modules', kwargs) + self.api_client.get(url) + + def get_quotation(self, **kwargs): + """Returns a quote base on multiple resource descriptions. + + :param res_data: A list of resource descriptions. + :type res_data: list + """ + if not kwargs.get('res_data', None): + raise exc.ArgumentRequired("'res_data' argument is required.") + url = self.get_url('quote') + return self.api_client.post(url, kwargs['res_data']) + + +class CliModuleGet(lister.Lister): + """Get a rating module or list loaded rating modules. + + If module_id is not specified, returns a list of all loaded + rating modules. + """ + columns = [ + ('module_id', 'Module'), + ('enabled', 'Enabled'), + ('priority', 'Priority'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.get_module( + module_id=parsed_args.module_id, + ) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliModuleGet, self).get_parser(prog_name) + parser.add_argument('module_id', type=str, help='Module name') + return parser + + +class CliModuleList(lister.Lister): + """List loaded rating modules.""" + + columns = [ + ('module_id', 'Module'), + ('enabled', 'Enabled'), + ('priority', 'Priority'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.get_module() + values = utils.list_to_cols(resp['modules'], self.columns) + return [col[1] for col in self.columns], values + + +class CliModuleSet(lister.Lister): + columns = [ + ('module_id', 'Module'), + ('enabled', 'Enabled'), + ('priority', 'Priority'), + ] + + def _take_action(self, **kwargs): + resp = utils.get_client_from_osc(self).rating.update_module(**kwargs) + values = [resp.get(col[0]) for col in self.columns] + return [col[1] for col in self.columns], [values] + + def get_parser(self, prog_name): + parser = super(CliModuleSet, self).get_parser(prog_name) + parser.add_argument('module_id', type=str, help='Module name') + return parser + + +class CliModuleEnable(CliModuleSet): + """Enable a rating module.""" + + def take_action(self, parsed_args): + kwargs = vars(parsed_args) + kwargs['enabled'] = True + return self._take_action(**kwargs) + + +class CliModuleDisable(CliModuleEnable): + """Disable a rating module.""" + + def take_action(self, parsed_args): + kwargs = vars(parsed_args) + kwargs['enabled'] = False + return self._take_action(**kwargs) + + +class CliModuleSetPriority(CliModuleSet): + """Set the priority of a rating module.""" + + def get_parser(self, prog_name): + parser = super(CliModuleSetPriority, self).get_parser(prog_name) + parser.add_argument('priority', type=int, help='Priority (int)') + return parser + + def take_action(self, parsed_args): + return self._take_action(**vars(parsed_args)) diff --git a/cloudkittyclient/v1/rating/hashmap.py b/cloudkittyclient/v1/rating/hashmap.py new file mode 100644 index 0000000..cc156e4 --- /dev/null +++ b/cloudkittyclient/v1/rating/hashmap.py @@ -0,0 +1,446 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient import exc +from cloudkittyclient.v1 import base + + +class HashmapManager(base.BaseManager): + """Class used to manage the Hashmap rating module""" + + url = '/v1/rating/module_config/hashmap/{endpoint}/{resource_id}' + + def get_mapping_types(self, **kwargs): + """Returns a list of all available mapping types.""" + url = self.get_url('types', kwargs) + return self.api_client.get(url).json() + + def get_service(self, **kwargs): + """Returns the service corresponding to the provided ID. + + If no ID is provided, returns a list of all hashmap services. + + :param service_id: ID of the service + :type service_id: str + """ + if kwargs.get('service_id'): + kwargs['resource_id'] = kwargs['service_id'] + url = self.get_url('services', kwargs) + return self.api_client.get(url).json() + + def create_service(self, **kwargs): + """Creates a hashmap service. + + :param name: Name of the service + :type name: str + """ + if not kwargs.get('name'): + raise exc.ArgumentRequired("Argument 'service_name' is mandatory.") + url = self.get_url('services', kwargs) + body = dict(name=kwargs['name']) + return self.api_client.post(url, json=body).json() + + def delete_service(self, **kwargs): + """Deletes a hashmap service + + :param service_id: ID of the service to delete + :type service_id: uuid + """ + if not kwargs.get('service_id'): + raise exc.ArgumentRequired("Argument 'service_id' is mandatory.") + url = self.get_url('services', kwargs) + body = dict(service_id=kwargs['service_id']) + self.api_client.delete(url, json=body) + + def get_field(self, **kwargs): + """Returns a hashmap field. + + Either service_id or field_id must be specified. If service_id is + provided, all fields of the given service are returned. If field_id + is specified, only this field is returned. + + :param service_id: ID of the service of which you want fields + :type service_id: str + :param field_id: ID of the field you want + :type field_id: str + """ + if not kwargs.get('service_id') and not kwargs.get('field_id'): + raise exc.ArgumentRequired("Either 'service_id' or 'field_id' " + "must be specified.") + elif kwargs.get('service_id') and kwargs.get('field_id'): + raise exc.InvalidArgumentError( + "You can't specify both 'service_id' and 'field_id'") + elif kwargs.get('field_id'): + kwargs['resource_id'] = kwargs['field_id'] + kwargs.pop('service_id', None) + else: + kwargs.pop('resource_id', None) + authorized_args = ['service_id'] + url = self.get_url('fields', kwargs, authorized_args) + return self.api_client.get(url).json() + + def create_field(self, **kwargs): + """Creates a hashmap field. + + :param name: Field name + :type name: str + :param service_id: ID of the service the field belongs to + :type service_id: uuid + """ + if not kwargs.get('name'): + raise exc.ArgumentRequired("'name' argument is required") + if not kwargs.get('service_id'): + raise exc.ArgumentRequired("'service_id' argument is required") + body = dict(name=kwargs['name'], service_id=kwargs['service_id']) + url = self.get_url('fields', kwargs) + return self.api_client.post(url, json=body).json() + + def delete_field(self, **kwargs): + """Deletes the given field. + + :param field_id: ID of the field to delete. + :type field_id: uuid + """ + if not kwargs.get('field_id'): + raise exc.ArgumentRequired("'field_id' argument is required") + url = self.get_url('fields', kwargs) + body = dict(field_id=kwargs['field_id']) + self.api_client.delete(url, json=body) + + def get_mapping(self, **kwargs): + """Get hashmap mappings. + + If mapping_id is not provided, you need to specify either service_id, + field_id or group_id. + + :param mapping_id: ID of the mapping + :type mapping_id: uuid + :param service_id: ID of the service to filter on + :type service_id: uuid + :param group_id: ID of the group to filter on + :type group_id: uuid + :param field_id: ID of the field to filter on + :type field_id: uuid + :param tenant_id: ID of the tenant to filter on + :type tenant_id: uuid + :param filter_tenant: Explicitly filter on given tenant (allows to + filter on tenant being None). Defaults to false. + :type filter_tenant: bool + :param no_group: Filter on orphaned mappings. + :type no_group: bool + """ + if not kwargs.get('mapping_id'): + if not kwargs.get('service_id') and not kwargs.get('field_id') \ + and not kwargs.get('group_id'): + raise exc.ArgumentRequired("You must provide either 'field_id'" + ", 'service_id' or 'group_id'.") + allowed_args = ['service_id', 'group_id', 'field_id', 'tenant_id', + 'filter_tenant', 'no_group'] + else: + allowed_args = [] + kwargs['resource_id'] = kwargs['mapping_id'] + url = self.get_url('mappings', kwargs, allowed_args) + return self.api_client.get(url).json() + + def create_mapping(self, **kwargs): + """Create a hashmap mapping. + + :param cost: Cost of the mapping + :type cost: decimal.Decimal + :param field_id: ID of the field the mapping belongs to + :type field_id: uuid + :param service_id: ID of the service the mapping belongs to + :type service_id: uuid + :param tenant_id: ID of the tenant the mapping belongs to + :type tenant_id: uuid + :param group_id: ID of the group the mapping belongs to + :type group_id: uuid + :param type: Type of the mapping (flat or rate) + :type type: str + :param value: Value of the mapping + :type value: str + """ + if not kwargs.get('cost'): + raise exc.ArgumentRequired("'cost' argument is required") + if not kwargs.get('value'): + if not kwargs.get('service_id'): + raise exc.ArgumentRequired( + "'service_id' must be specified if no value is provided") + if kwargs.get('value') and kwargs.get('service_id'): + raise exc.InvalidArgumentError( + "You can't specify a value when 'service_id' is specified.") + if not kwargs.get('service_id') and not kwargs.get('field_id'): + raise exc.ArgumentRequired("You must specify either 'service_id'" + " or 'field_id'") + elif kwargs.get('service_id') and kwargs.get('field_id'): + raise exc.InvalidArgumentError( + "You can't specify both 'service_id'and 'field_id'") + body = dict( + cost=kwargs.get('cost'), + value=kwargs.get('value'), + service_id=kwargs.get('service_id'), + group_id=kwargs.get('group_id'), + field_id=kwargs.get('field_id'), + tenant_id=kwargs.get('tenant_id'), + type=kwargs.get('type') or 'flat', + ) + url = self.get_url('mappings', kwargs) + return self.api_client.post(url, json=body).json() + + def delete_mapping(self, **kwargs): + """Delete a hashmap mapping. + + :param mapping_id: ID of the mapping to delete. + :type mapping_id: uuid + """ + if not kwargs.get('mapping_id'): + raise exc.ArgumentRequired("'mapping_id' argument is required") + url = self.get_url('mappings', kwargs) + body = dict(mapping_id=kwargs['mapping_id']) + self.api_client.delete(url, json=body) + + def update_mapping(self, **kwargs): + """Update a hashmap mapping. + + :param mapping_id: ID of the mapping to update + :type mapping_id: uuid + :param cost: Cost of the mapping + :type cost: decimal.Decimal + :param field_id: ID of the field the mapping belongs to + :type field_id: uuid + :param service_id: ID of the field the mapping belongs to + :type service_id: uuid + :param tenant_id: ID of the field the mapping belongs to + :type tenant_id: uuid + :param type: Type of the mapping (flat or rate) + :type type: str + :param value: Value of the mapping + :type value: str + """ + if not kwargs.get('mapping_id'): + raise exc.ArgumentRequired("'mapping_id' argument is required") + mapping = self.get_mapping(**kwargs) + for key in mapping.keys(): + value = kwargs.get(key, None) + if value is not None and mapping[key] != value: + mapping[key] = value + url = self.get_url('mappings', kwargs) + self.api_client.put(url, json=mapping) + return self.get_mapping(**kwargs) + + def get_mapping_group(self, **kwargs): + """Get the group attached to a mapping. + + :param mapping_id: ID of the mapping to update + :type mapping_id: uuid + """ + if not kwargs.get('mapping_id'): + raise exc.ArgumentRequired("'mapping_id' argument is required") + kwargs['resource_id'] = 'group' + allowed_args = ['mapping_id'] + url = self.get_url('mappings', kwargs, allowed_args) + return self.api_client.get(url).json() + + def get_group(self, **kwargs): + """Get the hashmap group corresponding to the given ID. + + If group_id is not specified, returns a list of all hashmap groups. + + :param group_id: Group ID + :type group_id: uuid + """ + kwargs['resource_id'] = kwargs.get('group_id') or '' + url = self.get_url('groups', kwargs) + return self.api_client.get(url).json() + + def create_group(self, **kwargs): + """Create a hashmap group. + + :param name: Name of the group + :type name: str + """ + if not kwargs.get('name'): + raise exc.ArgumentRequired("'name' argument is required") + body = dict(name=kwargs['name']) + url = self.get_url('groups', kwargs) + return self.api_client.post(url, json=body).json() + + def delete_group(self, **kwargs): + """Delete a hashmap group. + + :param group_id: ID of the group to delete + :type group_id: uuid + :param recursive: Delete mappings recursively + :type recursive: bool + """ + if not kwargs.get('group_id'): + raise exc.ArgumentRequired("'group_id' argument is required") + body = dict( + group_id=kwargs['group_id'], + recursive=kwargs.get('recursive', False)) + url = self.get_url('groups', kwargs) + self.api_client.delete(url, json=body) + + def get_group_mappings(self, **kwargs): + """Get the mappings attached to the given group. + + :param group_id: ID of the group + :type group_id: uuid + """ + if not kwargs.get('group_id'): + raise exc.ArgumentRequired("'group_id' argument is required") + authorized_args = ['group_id'] + kwargs['resource_id'] = 'mappings' + url = self.get_url('groups', kwargs, authorized_args) + return self.api_client.get(url).json() + + def get_group_thresholds(self, **kwargs): + """Get the thresholds attached to the given group. + + :param group_id: ID of the group + :type group_id: uuid + """ + if not kwargs.get('group_id'): + raise exc.ArgumentRequired("'group_id' argument is required") + authorized_args = ['group_id'] + kwargs['resource_id'] = 'thresholds' + url = self.get_url('groups', kwargs, authorized_args) + return self.api_client.get(url).json() + + def get_threshold(self, **kwargs): + """Get hashmap thresholds. + + If threshold_id is not provided, you need to specify either service_id, + field_id or group_id. + + :param threshold_id: ID of the threshold + :type threshold_id: uuid + :param service_id: ID of the service to filter on + :type service_id: uuid + :param group_id: ID of the group to filter on + :type group_id: uuid + :param field_id: ID of the field to filter on + :type field_id: uuid + :param tenant_id: ID of the tenant to filter on + :type tenant_id: uuid + :param filter_tenant: Explicitly filter on given tenant (allows to + filter on tenant being None). Defaults to false. + :type filter_tenant: bool + :param no_group: Filter on orphaned thresholds. + :type no_group: bool + """ + if not kwargs.get('threshold_id'): + if not kwargs.get('service_id') and not kwargs.get('field_id') \ + and not kwargs.get('group_id'): + raise exc.ArgumentRequired("You must provide either 'field_id'" + ", 'service_id' or 'group_id'.") + allowed_args = ['service_id', 'group_id', 'field_id', 'tenant_id', + 'filter_tenant', 'no_group'] + else: + allowed_args = [] + kwargs['resource_id'] = kwargs['threshold_id'] + url = self.get_url('thresholds', kwargs, allowed_args) + return self.api_client.get(url).json() + + def create_threshold(self, **kwargs): + """Create a hashmap threshold. + + :param cost: Cost of the threshold + :type cost: decimal.Decimal + :param field_id: ID of the field the threshold belongs to + :type field_id: uuid + :param service_id: ID of the service the threshold belongs to + :type service_id: uuid + :param tenant_id: ID of the tenant the threshold belongs to + :type tenant_id: uuid + :param group_id: ID of the group the threshold belongs to + :type group_id: uuid + :param type: Type of the threshold (flat or rate) + :type type: str + :param level: Level of the threshold + :type level: str + """ + for arg in ['cost', 'level']: + if not kwargs.get(arg): + raise exc.ArgumentRequired( + "'{}' argument is required".format(arg)) + if not kwargs.get('service_id') and not kwargs.get('field_id'): + raise exc.ArgumentRequired("You must specify either 'service_id'" + " or 'field_id'") + body = dict( + cost=kwargs.get('cost'), + level=kwargs.get('level'), + service_id=kwargs.get('service_id'), + field_id=kwargs.get('field_id'), + group_id=kwargs.get('group_id'), + tenant_id=kwargs.get('tenant_id'), + type=kwargs.get('type') or 'flat', + ) + url = self.get_url('thresholds', kwargs) + return self.api_client.post(url, json=body).json() + + def delete_threshold(self, **kwargs): + """Delete a hashmap threshold. + + :param threshold_id: ID of the threshold to delete. + :type threshold_id: uuid + """ + if not kwargs.get('threshold_id'): + raise exc.ArgumentRequired("'threshold_id' argument is required") + url = self.get_url('thresholds', kwargs) + body = dict(threshold_id=kwargs['threshold_id']) + self.api_client.delete(url, json=body) + + def update_threshold(self, **kwargs): + """Update a hashmap threshold. + + :param threshold_id: ID of the threshold to update + :type threshold_id: uuid + :param cost: Cost of the threshold + :type cost: decimal.Decimal + :param field_id: ID of the field the threshold belongs to + :type field_id: uuid + :param service_id: ID of the field the threshold belongs to + :type service_id: uuid + :param tenant_id: ID of the field the threshold belongs to + :type tenant_id: uuid + :param type: Type of the threshold (flat or rate) + :type type: str + :param level: Level of the threshold + :type level: str + """ + if not kwargs.get('threshold_id'): + raise exc.ArgumentRequired("'threshold_id' argument is required") + threshold = self.get_threshold(**kwargs) + for key in threshold.keys(): + value = kwargs.get(key, None) + if value is not None and threshold[key] != value: + threshold[key] = value + url = self.get_url('thresholds', kwargs) + self.api_client.put(url, json=threshold) + return self.get_threshold(**kwargs) + + def get_threshold_group(self, **kwargs): + """Get the group attached to a threshold. + + :param threshold_id: ID of the threshold to update + :type threshold_id: uuid + """ + if not kwargs.get('threshold_id'): + raise exc.ArgumentRequired("'threshold_id' argument is required") + kwargs['resource_id'] = 'group' + allowed_args = ['threshold_id'] + url = self.get_url('thresholds', kwargs, allowed_args) + return self.api_client.get(url).json() diff --git a/cloudkittyclient/v1/rating/hashmap/__init__.py b/cloudkittyclient/v1/rating/hashmap/__init__.py deleted file mode 100644 index 85f4d00..0000000 --- a/cloudkittyclient/v1/rating/hashmap/__init__.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright 2015 Objectif Libre -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from cloudkittyclient.common import base - - -class BaseAttributeMixin(object): - def _validate_attribute(self, attribute): - attr = getattr(self, attribute) - if attr: - kwargs = {attribute: attr} - return kwargs - - def _get_resource(self, mgr, attribute): - kwargs = self._validate_attribute(attribute) - if kwargs: - return mgr(client=self.manager.client).get(**kwargs) - - def _get_resources(self, mgr, attribute): - kwargs = self._validate_attribute(attribute) - if kwargs: - try: - return mgr(client=self.manager.client).findall(**kwargs) - except Exception: - pass - return [] - - -class ServiceMixin(BaseAttributeMixin): - @property - def service(self): - return self._get_resource(ServiceManager, 'service_id') - - -class FieldMixin(BaseAttributeMixin): - @property - def field(self): - return self._get_resource(FieldManager, 'field_id') - - -class GroupMixin(BaseAttributeMixin): - @property - def group(self): - return self._get_resource(GroupManager, 'group_id') - - -class FieldsMixin(BaseAttributeMixin): - attribute = '' - - @property - def fields(self): - return self._get_resources(FieldManager, self.attribute) - - -class MappingsMixin(BaseAttributeMixin): - attribute = '' - - @property - def mappings(self): - return self._get_resources(MappingManager, self.attribute) - - -class ThresholdsMixin(BaseAttributeMixin): - attribute = '' - - @property - def thresholds(self): - return self._get_resources(ThresholdManager, self.attribute) - - -class Service(base.Resource, FieldsMixin, MappingsMixin, ThresholdsMixin): - key = 'service' - attribute = 'service_id' - - def __repr__(self): - return "" % self._info - - -class ServiceManager(base.CrudManager): - resource_class = Service - base_url = '/v1/rating/module_config/hashmap' - key = 'service' - collection_key = 'services' - - -class Field(base.Resource, ServiceMixin, MappingsMixin, ThresholdsMixin): - key = 'field' - attribute = 'field_id' - - def __repr__(self): - return "" % self._info - - -class FieldManager(base.CrudManager): - resource_class = Field - base_url = '/v1/rating/module_config/hashmap' - key = 'field' - collection_key = 'fields' - - -class Mapping(base.Resource, ServiceMixin, FieldMixin, GroupMixin): - key = 'mapping' - - def __repr__(self): - return "" % self._info - - -class MappingManager(base.CrudManager): - resource_class = Mapping - base_url = '/v1/rating/module_config/hashmap' - key = 'mapping' - collection_key = 'mappings' - - -class Group(base.Resource, MappingsMixin, ThresholdsMixin): - key = 'group' - attribute = 'group_id' - - def __repr__(self): - return "" % self._info - - def delete(self, recursive=False): - return self.manager.delete(group_id=self.group_id, recursive=recursive) - - -class GroupManager(base.CrudManager): - resource_class = Group - base_url = '/v1/rating/module_config/hashmap' - key = 'group' - collection_key = 'groups' - - def delete(self, group_id, recursive=False): - url = self.build_url(group_id=group_id) - if recursive: - url += "?recursive=True" - return self._delete(url) - - -class Threshold(base.Resource, ServiceMixin, FieldMixin, GroupMixin): - key = 'threshold' - - def __repr__(self): - return "" % self._info - - -class ThresholdManager(base.CrudManager): - resource_class = Threshold - base_url = '/v1/rating/module_config/hashmap' - key = 'threshold' - collection_key = 'thresholds' diff --git a/cloudkittyclient/v1/rating/hashmap/client.py b/cloudkittyclient/v1/rating/hashmap/client.py deleted file mode 100644 index e236605..0000000 --- a/cloudkittyclient/v1/rating/hashmap/client.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2015 Objectif Libre -# 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. - -from cloudkittyclient.v1.rating import hashmap - - -class Client(object): - """Client for the Hashmap v1 API. - - :param http_client: A http client. - """ - - def __init__(self, http_client): - """Initialize a new client for the Hashmap v1 API.""" - self.http_client = http_client - self.services = hashmap.ServiceManager(self.http_client) - self.fields = hashmap.FieldManager(self.http_client) - self.mappings = hashmap.MappingManager(self.http_client) - self.groups = hashmap.GroupManager(self.http_client) - self.thresholds = hashmap.ThresholdManager(self.http_client) diff --git a/cloudkittyclient/v1/rating/hashmap/extension.py b/cloudkittyclient/v1/rating/hashmap/extension.py deleted file mode 100644 index 32bb0d0..0000000 --- a/cloudkittyclient/v1/rating/hashmap/extension.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2015 Objectif Libre -# 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. - -from cloudkittyclient.v1.rating.hashmap import client -from cloudkittyclient.v1.rating.hashmap import shell - - -class Extension(object): - """Hashmap extension. - - """ - - @staticmethod - def get_client(http_client): - return client.Client(http_client) - - @staticmethod - def get_shell(): - return shell diff --git a/cloudkittyclient/v1/rating/hashmap/shell.py b/cloudkittyclient/v1/rating/hashmap/shell.py deleted file mode 100644 index 8b6ec42..0000000 --- a/cloudkittyclient/v1/rating/hashmap/shell.py +++ /dev/null @@ -1,434 +0,0 @@ -# Copyright 2015 Objectif Libre -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import functools - -from oslo_utils import strutils - -from cloudkittyclient.apiclient import exceptions -from cloudkittyclient.common import utils -from cloudkittyclient import exc - -_bool_strict = functools.partial(strutils.bool_from_string, strict=True) - - -@utils.arg('-n', '--name', - help='Service name', - required=True) -def do_hashmap_service_create(cc, args={}): - """Create a service.""" - arg_to_field_mapping = { - 'name': 'name' - } - fields = {} - for k, v in vars(args).items(): - if k in arg_to_field_mapping: - if v is not None: - fields[arg_to_field_mapping.get(k, k)] = v - out = cc.hashmap.services.create(**fields) - utils.print_dict(out.to_dict()) - - -def do_hashmap_service_list(cc, args={}): - """List services.""" - try: - services = cc.hashmap.services.list() - except exceptions.NotFound: - raise exc.CommandError('Services not found.') - else: - field_labels = ['Name', 'Service id'] - fields = ['name', 'service_id'] - utils.print_list(services, fields, field_labels, - sortby=0) - - -@utils.arg('-s', '--service-id', - help='Service uuid', - required=True) -def do_hashmap_service_delete(cc, args={}): - """Delete a service.""" - try: - cc.hashmap.services.delete(service_id=args.service_id) - except exceptions.NotFound: - raise exc.CommandError('Service not found: %s' % args.service_id) - - -@utils.arg('-n', '--name', - help='Field name', - required=True) -@utils.arg('-s', '--service-id', - help='Service id', - required=True) -def do_hashmap_field_create(cc, args={}): - """Create a field.""" - arg_to_field_mapping = { - 'name': 'name', - 'service_id': 'service_id' - } - fields = {} - for k, v in vars(args).items(): - if k in arg_to_field_mapping: - if v is not None: - fields[arg_to_field_mapping.get(k, k)] = v - out = cc.hashmap.fields.create(**fields) - utils.print_dict(out.to_dict()) - - -@utils.arg('-s', '--service-id', - help='Service id', - required=True) -def do_hashmap_field_list(cc, args={}): - """List fields.""" - try: - created_field = cc.hashmap.fields.list(service_id=args.service_id) - except exceptions.NotFound: - raise exc.CommandError('Fields not found in service: %s' - % args.service_id) - else: - field_labels = ['Name', 'Field id'] - fields = ['name', 'field_id'] - utils.print_list(created_field, fields, field_labels, - sortby=0) - - -@utils.arg('-f', '--field-id', - help='Field uuid', - required=True) -def do_hashmap_field_delete(cc, args={}): - """Delete a field.""" - try: - cc.hashmap.fields.delete(field_id=args.field_id) - except exceptions.NotFound: - raise exc.CommandError('Field not found: %s' % args.field_id) - - -def common_hashmap_mapping_arguments(create=False): - def _wrapper(func): - @utils.arg('-c', '--cost', - help='Mapping cost', - required=create) - @utils.arg('-v', '--value', - help='Mapping value', - required=False) - @utils.arg('-t', '--type', - help='Mapping type (flat, rate)', - required=False) - @utils.arg('-g', '--group-id', - help='Group id', - required=False) - @utils.arg('-p', '--project-id', - help='Project/tenant id', - required=False) - @functools.wraps(func) - def _wrapped(*args, **kwargs): - return func(*args, **kwargs) - return _wrapped - return _wrapper - - -@utils.arg('-s', '--service-id', - help='Service id', - required=False) -@utils.arg('-f', '--field-id', - help='Field id', - required=False) -@common_hashmap_mapping_arguments(create=True) -def do_hashmap_mapping_create(cc, args={}): - """Create a mapping.""" - arg_to_field_mapping = { - 'cost': 'cost', - 'value': 'value', - 'type': 'type', - 'service_id': 'service_id', - 'field_id': 'field_id', - 'group_id': 'group_id', - 'project_id': 'tenant_id', - } - fields = {} - for k, v in vars(args).items(): - if k in arg_to_field_mapping: - if v is not None: - fields[arg_to_field_mapping.get(k, k)] = v - out = cc.hashmap.mappings.create(**fields) - utils.print_dict(out.to_dict()) - - -@utils.arg('-m', '--mapping-id', - help='Mapping id', - required=True) -@common_hashmap_mapping_arguments() -def do_hashmap_mapping_update(cc, args={}): - """Update a mapping.""" - arg_to_field_mapping = { - 'mapping_id': 'mapping_id', - 'cost': 'cost', - 'value': 'value', - 'type': 'type', - 'group_id': 'group_id', - 'project_id': 'tenant_id', - } - try: - mapping = cc.hashmap.mappings.get(mapping_id=args.mapping_id) - except exceptions.NotFound: - raise exc.CommandError('Mapping not found: %s' % args.mapping_id) - for k, v in vars(args).items(): - if k in arg_to_field_mapping: - if v is not None: - setattr(mapping, k, v) - cc.hashmap.mappings.update(**mapping.dirty_fields) - - -@utils.arg('-s', '--service-id', - help='Service id', - required=False) -@utils.arg('-f', '--field-id', - help='Field id', - required=False) -@utils.arg('-g', '--group-id', - help='Group id', - required=False) -@utils.arg('-p', '--project-id', - help='Project/tenant id', - required=False) -def do_hashmap_mapping_list(cc, args={}): - """List mappings.""" - if (args.group_id is None and - args.service_id is None and args.field_id is None): - raise exc.CommandError("Provide either group-id, service-id or " - "field-id") - try: - mappings = cc.hashmap.mappings.list(service_id=args.service_id, - field_id=args.field_id, - group_id=args.group_id) - except exceptions.NotFound: - raise exc.CommandError('Mappings not found for field: %s' - % args.field_id) - else: - field_labels = ['Mapping id', 'Value', 'Cost', - 'Type', 'Field id', - 'Service id', 'Group id', 'Tenant id'] - fields = ['mapping_id', 'value', 'cost', - 'type', 'field_id', - 'service_id', 'group_id', 'tenant_id'] - utils.print_list(mappings, fields, field_labels, - sortby=0) - - -@utils.arg('-m', '--mapping-id', - help='Mapping uuid', - required=True) -def do_hashmap_mapping_delete(cc, args={}): - """Delete a mapping.""" - try: - cc.hashmap.mappings.delete(mapping_id=args.mapping_id) - except exceptions.NotFound: - raise exc.CommandError('Mapping not found: %s' % args.mapping_id) - - -@utils.arg('-n', '--name', - help='Group name', - required=True) -def do_hashmap_group_create(cc, args={}): - """Create a group.""" - arg_to_field_mapping = { - 'name': 'name', - } - fields = {} - for k, v in vars(args).items(): - if k in arg_to_field_mapping: - if v is not None: - fields[arg_to_field_mapping.get(k, k)] = v - group = cc.hashmap.groups.create(**fields) - utils.print_dict(group.to_dict()) - - -def do_hashmap_group_list(cc, args={}): - """List groups.""" - try: - groups = cc.hashmap.groups.list() - except exceptions.NotFound: - raise exc.CommandError('Groups not found.') - else: - field_labels = ['Name', - 'Group id'] - fields = ['name', 'group_id'] - utils.print_list(groups, fields, field_labels, - sortby=0) - - -@utils.arg('-g', '--group-id', - help='Group uuid', - required=True) -@utils.arg('-r', '--recursive', - help="""Delete the group's mappings""", - required=False, - default=False) -def do_hashmap_group_delete(cc, args={}): - """Delete a group.""" - try: - cc.hashmap.groups.delete(group_id=args.group_id, - recursive=args.recursive) - except exceptions.NotFound: - raise exc.CommandError('Group not found: %s' % args.group_id) - - -def common_hashmap_threshold_arguments(create=False): - def _wrapper(func): - @utils.arg('-l', '--level', - help='Threshold level', - required=create) - @utils.arg('-c', '--cost', - help='Threshold cost', - required=create) - @utils.arg('-t', '--type', - help='Threshold type (flat, rate)', - required=False) - @utils.arg('-g', '--group-id', - help='Group id', - required=False) - @utils.arg('-p', '--project-id', - help='Project/tenant id', - required=False) - @functools.wraps(func) - def _wrapped(*args, **kwargs): - return func(*args, **kwargs) - return _wrapped - return _wrapper - - -@utils.arg('-s', '--service-id', - help='Service id', - required=False) -@utils.arg('-f', '--field-id', - help='Field id', - required=False) -@common_hashmap_threshold_arguments(create=True) -def do_hashmap_threshold_create(cc, args={}): - """Create a mapping.""" - arg_to_field_mapping = { - 'level': 'level', - 'cost': 'cost', - 'type': 'type', - 'service_id': 'service_id', - 'field_id': 'field_id', - 'group_id': 'group_id', - 'project_id': 'tenant_id', - } - fields = {} - for k, v in vars(args).items(): - if k in arg_to_field_mapping: - if v is not None: - fields[arg_to_field_mapping.get(k, k)] = v - out = cc.hashmap.thresholds.create(**fields) - utils.print_dict(out.to_dict()) - - -@utils.arg('-i', '--threshold-id', - help='Threshold id', - required=True) -@common_hashmap_threshold_arguments() -def do_hashmap_threshold_update(cc, args={}): - """Update a threshold.""" - arg_to_field_mapping = { - 'threshold_id': 'threshold_id', - 'cost': 'cost', - 'level': 'level', - 'type': 'type', - 'group_id': 'group_id', - 'project_id': 'tenant_id', - } - try: - threshold = cc.hashmap.thresholds.get(threshold_id=args.threshold_id) - except exceptions.NotFound: - raise exc.CommandError('Threshold not found: %s' % args.threshold_id) - for k, v in vars(args).items(): - if k in arg_to_field_mapping: - if v is not None: - setattr(threshold, k, v) - cc.hashmap.thresholds.update(**threshold.dirty_fields) - - -@utils.arg('-s', '--service-id', - help='Service id', - required=False) -@utils.arg('-f', '--field-id', - help='Field id', - required=False) -@utils.arg('-g', '--group-id', - help='Group id', - required=False) -@utils.arg('--no-group', - type=_bool_strict, metavar='{True,False}', - help='If True, list only orhpaned thresholds', - required=False) -@utils.arg('-p', '--project-id', - help='Project/tenant id', - required=False) -def do_hashmap_threshold_list(cc, args={}): - """List thresholds.""" - if (args.group_id is None and - args.service_id is None and args.field_id is None): - raise exc.CommandError("Provide either group-id, service-id or " - "field-id") - try: - thresholds = cc.hashmap.thresholds.list(service_id=args.service_id, - field_id=args.field_id, - group_id=args.group_id, - no_group=args.no_group) - except exceptions.NotFound: - raise exc.CommandError('Thresholds not found') - else: - field_labels = ['Threshold id', 'Level', 'Cost', - 'Type', 'Field id', - 'Service id', 'Group id', 'Tenant id'] - fields = ['threshold_id', 'level', 'cost', - 'type', 'field_id', - 'service_id', 'group_id', 'tenant_id'] - utils.print_list(thresholds, fields, field_labels, sortby=0) - - -@utils.arg('-i', '--threshold-id', - help='Threshold uuid', - required=True) -def do_hashmap_threshold_delete(cc, args={}): - """Delete a threshold.""" - try: - cc.hashmap.thresholds.delete(threshold_id=args.threshold_id) - except exceptions.NotFound: - raise exc.CommandError('Threshold not found: %s' % args.threshold_id) - - -@utils.arg('-i', '--threshold-id', - help='Threshold uuid', - required=True) -def do_hashmap_threshold_get(cc, args={}): - """Get a threshold.""" - try: - threshold = cc.hashmap.thresholds.get(threshold_id=args.threshold_id) - except exceptions.NotFound: - raise exc.CommandError('Threshold not found: %s' % args.threshold_id) - utils.print_dict(threshold.to_dict()) - - -@utils.arg('-i', '--threshold-id', - help='Threshold uuid', - required=True) -def do_hashmap_threshold_group(cc, args={}): - """Get a threshold group.""" - try: - threshold = cc.hashmap.thresholds.group(threshold_id=args.threshold_id) - except exceptions.NotFound: - raise exc.CommandError('Threshold not found: %s' % args.threshold_id) - utils.print_dict(threshold.to_dict()) diff --git a/cloudkittyclient/v1/rating/hashmap/shell_cli.py b/cloudkittyclient/v1/rating/hashmap/shell_cli.py deleted file mode 100644 index 37c438f..0000000 --- a/cloudkittyclient/v1/rating/hashmap/shell_cli.py +++ /dev/null @@ -1,355 +0,0 @@ -# Copyright 2016 Objectif Libre -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import functools - -from osc_lib.command import command -from oslo_utils import strutils - -from cloudkittyclient.v1.rating.hashmap import shell - - -_bool_strict = functools.partial(strutils.bool_from_string, strict=True) - - -class CliHashmapServiceCreate(command.Command): - """Create a service.""" - def get_parser(self, prog_name): - parser = super(CliHashmapServiceCreate, self).get_parser(prog_name) - parser.add_argument('-n', '--name', - help='Service name', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_service_create(ckclient, parsed_args) - - -class CliHashmapServiceList(command.Command): - """List services.""" - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_service_list(ckclient, parsed_args) - - -class CliHashmapServiceDelete(command.Command): - """Delete a service.""" - def get_parser(self, prog_name): - parser = super(CliHashmapServiceDelete, self).get_parser(prog_name) - parser.add_argument('-s', '--service-id', - help='Service id', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_service_delete(ckclient, parsed_args) - - -class CliHashmapFieldCreate(command.Command): - """Create a field.""" - def get_parser(self, prog_name): - parser = super(CliHashmapFieldCreate, self).get_parser(prog_name) - parser.add_argument('-s', '--service-id', - help='Service id', - required=True) - parser.add_argument('-n', '--name', - help='Field name', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_field_create(ckclient, parsed_args) - - -class CliHashmapFieldList(command.Command): - """List fields.""" - def get_parser(self, prog_name): - parser = super(CliHashmapFieldList, self).get_parser(prog_name) - parser.add_argument('-s', '--service-id', - help='Service id', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_field_list(ckclient, parsed_args) - - -class CliHashmapFieldDelete(command.Command): - """Delete a field.""" - def get_parser(self, prog_name): - parser = super(CliHashmapFieldDelete, self).get_parser(prog_name) - parser.add_argument('-f', '--field-id', - help='Field id', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_field_delete(ckclient, parsed_args) - - -class CliHashmapMappingCommon(command.Command): - def get_parser(self, prog_name, cost=False): - parser = super(CliHashmapMappingCommon, self).get_parser(prog_name) - parser.add_argument('-c', '--cost', - help='Mapping Cost', - required=cost) - parser.add_argument('-v', '--value', - help='Mapping Value', - required=False) - parser.add_argument('-t', '--type', - help='Mapping type (flat, rate)', - required=False) - parser.add_argument('-g', '--group-id', - help='Group id', - required=False) - parser.add_argument('-p', '--project-id', - help='Project/Tenant id', - required=False) - return parser - - -class CliHashmapMappingCreate(CliHashmapMappingCommon): - """Create a mapping.""" - def get_parser(self, prog_name): - parser = super(CliHashmapMappingCreate, self).get_parser(prog_name, - cost=True) - parser.add_argument('-s', '--service-id', - help='Service id', - required=False) - parser.add_argument('-f', '--field-id', - help='Service id', - required=False) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_mapping_create(ckclient, parsed_args) - - -class CliHashmapMappingUpdate(CliHashmapMappingCommon): - """Update a mapping.""" - def get_parser(self, prog_name): - parser = super(CliHashmapMappingUpdate, self).get_parser(prog_name) - parser.add_argument('-m', '--mapping-id', - help='Mapping id', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_mapping_update(ckclient, parsed_args) - - -class CliHashmapMappingList(command.Command): - """List mappings.""" - def get_parser(self, prog_name): - parser = super(CliHashmapMappingList, self).get_parser(prog_name) - parser.add_argument('-s', '--service-id', - help='Service id', - required=False) - parser.add_argument('-f', '--field-id', - help='Field id', - required=False) - parser.add_argument('-g', '--group-id', - help='Group id', - required=False) - parser.add_argument('-p', '--project-id', - help='Project id', - required=False) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_mapping_list(ckclient, parsed_args) - - -class CliHashmapMappingDelete(command.Command): - """Delete a mapping.""" - def get_parser(self, prog_name): - parser = super(CliHashmapMappingDelete, self).get_parser(prog_name) - parser.add_argument('-m', '--mapping-id', - help='Mapping id', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_mapping_delete(ckclient, parsed_args) - - -class CliHashmapGroupCreate(command.Command): - """Create a group.""" - def get_parser(self, prog_name): - parser = super(CliHashmapGroupCreate, self).get_parser(prog_name) - parser.add_argument('-n', '--name', - help='Group name.', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_group_create(ckclient, parsed_args) - - -class CliHashmapGroupList(command.Command): - """List groups.""" - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_group_list(ckclient, parsed_args) - - -class CliHashmapGroupDelete(command.Command): - """Delete a group.""" - def get_parser(self, prog_name): - parser = super(CliHashmapGroupDelete, self).get_parser(prog_name) - parser.add_argument('-g', '--group-id', - help='Group uuid', - required=True) - parser.add_argument('-r', '--recursive', - help="""Delete the group's mappings.""", - required=False, - default=False) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_group_delete(ckclient, parsed_args) - - -class CliHashmapThresholdCommon(command.Command): - def get_parser(self, prog_name, create=False): - parser = super(CliHashmapThresholdCommon, self).get_parser(prog_name) - parser.add_argument('-l', '--level', - help='Threshold level', - required=create) - parser.add_argument('-c', '--cost', - help='Threshold cost', - required=create) - parser.add_argument('-t', '--type', - help='Threshold type', - required=False) - parser.add_argument('-g', '--group-id', - help='Group id', - required=False) - parser.add_argument('-p', '--project-id', - help='Project/tenant id', - required=False) - return parser - - -class CliHashmapThresholdCreate(CliHashmapThresholdCommon): - """Create a threshold.""" - def get_parser(self, prog_name): - parser = super(CliHashmapThresholdCreate, self).get_parser(prog_name, - create=True) - parser.add_argument('-s', '--service-id', - help='Service id', - required=False) - parser.add_argument('-f', '--field-id', - help='Field id', - required=False) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_threshold_create(ckclient, parsed_args) - - -class CliHashmapThresholdUpdate(CliHashmapThresholdCommon): - """Update a threshold.""" - def get_parser(self, prog_name): - parser = super(CliHashmapThresholdUpdate, self).get_parser(prog_name) - parser.add_argument('-i', '--threshold-id', - help='Threshold id', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_threshold_update(ckclient, parsed_args) - - -class CliHashmapThresholdList(command.Command): - """List thresholds.""" - def get_parser(self, prog_name): - parser = super(CliHashmapThresholdList, self).get_parser(prog_name) - parser.add_argument('-s', '--service-id', - help='Service id', - required=False) - parser.add_argument('-f', '--field-id', - help='Field id', - required=False) - parser.add_argument('-g', '--group-id', - help='Group id', - required=False) - parser.add_argument('--no-group', - type=_bool_strict, metavar='{True,False}', - help='If True, list only orphaned thresholds', - required=False) - parser.add_argument('-p', '--project-id', - help='Project/tenant id', - required=False) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_threshold_list(ckclient, parsed_args) - - -class CliHashmapThresholdDelete(command.Command): - """Delete a threshold.""" - def get_parser(self, prog_name): - parser = super(CliHashmapThresholdDelete, self).get_parser(prog_name) - parser.add_argument('-i', '--threshold-id', - help='Threshold uuid', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_threshold_delete(ckclient, parsed_args) - - -class CliHashmapThresholdGet(command.Command): - """Get a threshold.""" - def get_parser(self, prog_name): - parser = super(CliHashmapThresholdGet, self).get_parser(prog_name) - parser.add_argument('-i', '--threshold-id', - help='Threshold uuid', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_threshold_get(ckclient, parsed_args) - - -class CliHashmapThresholdGroup(command.Command): - """Get a threshold group.""" - def get_parser(self, prog_name): - parser = super(CliHashmapThresholdGroup, self).get_parser(prog_name) - parser.add_argument('-i', '--threshold-id', - help='Threshold uuid', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_hashmap_threshold_group(ckclient, parsed_args) diff --git a/cloudkittyclient/v1/rating/hashmap_cli.py b/cloudkittyclient/v1/rating/hashmap_cli.py new file mode 100644 index 0000000..0cbfeac --- /dev/null +++ b/cloudkittyclient/v1/rating/hashmap_cli.py @@ -0,0 +1,567 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cliff import command +from cliff import lister + +from cloudkittyclient import utils + + +class CliGetMappingTypes(lister.Lister): + """Get hashmap mapping types/""" + def take_action(self, parsed_args): + client = utils.get_client_from_osc(self) + resp = client.rating.hashmap.get_mapping_types() + return ['Mapping types'], [[item] for item in resp] + + +class CliGetService(lister.Lister): + """Get a hashmap service""" + + columns = [ + ('name', 'Name'), + ('service_id', 'Service ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.get_service( + service_id=parsed_args.service_id, + ) + # NOTE(lukapeschke): This can't be done with 'or', because it would + # lead to resp being [[]] if resp['services'] is an empty list. Having + # a list in a list causes cliff to display a row of 'None' instead of + # nothing + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliGetService, self).get_parser(prog_name) + parser.add_argument('service_id', type=str, help='Service ID') + return parser + + +class CliListService(lister.Lister): + """List hashmap services.""" + + columns = [ + ('name', 'Name'), + ('service_id', 'Service ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.get_service() + # NOTE(lukapeschke): This can't be done with 'or', because it would + # lead to resp being [[]] if resp['services'] is an empty list. Having + # a list in a list causes cliff to display a row of 'None' instead of + # nothing + values = utils.list_to_cols(resp['services'], self.columns) + return [col[1] for col in self.columns], values + + +class CliCreateService(lister.Lister): + """Create a hashmap service.""" + + columns = [ + ('name', 'Name'), + ('service_id', 'Service ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.create_service( + **vars(parsed_args)) + values = utils.list_to_cols(resp, self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliCreateService, self).get_parser(prog_name) + parser.add_argument('name', type=str, help='Service Name') + return parser + + +class CliDeleteService(command.Command): + """Delete a hashmap service""" + + def take_action(self, parsed_args): + utils.get_client_from_osc(self).rating.hashmap.delete_service( + **vars(parsed_args)) + + def get_parser(self, prog_name): + parser = super(CliDeleteService, self).get_parser(prog_name) + parser.add_argument('service_id', type=str, help='Service ID') + return parser + + +class CliGetField(lister.Lister): + """Get a Hashmap field.""" + columns = [ + ('name', 'Name'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.get_field( + field_id=parsed_args.field_id, + ) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliGetField, self).get_parser(prog_name) + parser.add_argument('field_id', type=str, help='Field ID') + return parser + + +class CliListField(lister.Lister): + """List hashmap fields for the given service.""" + + columns = [ + ('name', 'Name'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.get_field( + service_id=parsed_args.service_id, + ) + values = utils.list_to_cols(resp['fields'], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliListField, self).get_parser(prog_name) + parser.add_argument('service_id', type=str, help='Service ID') + return parser + + +class CliCreateField(lister.Lister): + """Create a hashmap field.""" + columns = [ + ('name', 'Name'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.create_field( + **vars(parsed_args)) + resp = [resp] if resp.get('fields') is None else resp['fields'] + values = utils.list_to_cols(resp, self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliCreateField, self).get_parser(prog_name) + parser.add_argument('service_id', type=str, help='Service ID') + parser.add_argument('name', type=str, help='Field name') + return parser + + +class CliDeleteField(command.Command): + """Delete a hashmap field.""" + + def take_action(self, parsed_args): + utils.get_client_from_osc(self).rating.hashmap.delete_field( + **vars(parsed_args)) + + def get_parser(self, prog_name): + parser = super(CliDeleteField, self).get_parser(prog_name) + parser.add_argument('field_id', type=str, help='Field ID') + return parser + + +class CliGetMapping(lister.Lister): + """Get a hashmap mapping.""" + + columns = [ + ('mapping_id', 'Mapping ID'), + ('value', 'Value'), + ('cost', 'Cost'), + ('type', 'Type'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ('group_id', 'Group ID'), + ('tenant_id', 'Project ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.get_mapping( + mapping_id=parsed_args.mapping_id) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliGetMapping, self).get_parser(prog_name) + parser.add_argument('mapping_id', type=str, + help='Mapping ID to filter on') + return parser + + +class CliListMapping(lister.Lister): + """List hashmap mappings.""" + + columns = [ + ('mapping_id', 'Mapping ID'), + ('value', 'Value'), + ('cost', 'Cost'), + ('type', 'Type'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ('group_id', 'Group ID'), + ('tenant_id', 'Project ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.get_mapping( + **vars(parsed_args)) + values = utils.list_to_cols(resp['mappings'], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliListMapping, self).get_parser(prog_name) + parser.add_argument('-s', '--service-id', type=str, + help='Service ID to filter on') + parser.add_argument('-g', '--group-id', type=str, + help='Group ID to filter on') + parser.add_argument('--field-id', type=str, + help='Field ID to filter on') + parser.add_argument('-p', '--project-id', type=str, dest='tenant_id', + help='Project ID to filter on') + parser.add_argument('--filter-tenant', action='store_true', + help='Explicitly filter on given tenant (allows ' + 'to filter on tenant being None)') + parser.add_argument('--no-group', action='store_true', + help='Filter on orphaned mappings') + return parser + + +class CliCreateMapping(lister.Lister): + """Create a Hashmap mapping.""" + columns = [ + ('mapping_id', 'Mapping ID'), + ('value', 'Value'), + ('cost', 'Cost'), + ('type', 'Type'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ('group_id', 'Group ID'), + ('tenant_id', 'Project ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.create_mapping( + **vars(parsed_args)) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliCreateMapping, self).get_parser(prog_name) + parser.add_argument('-s', '--service-id', type=str, help='Service ID') + parser.add_argument('-g', '--group-id', type=str, help='Group ID') + parser.add_argument('--field-id', type=str, help='Field ID') + parser.add_argument('-p', '--project-id', type=str, dest='tenant_id', + help='Project ID') + parser.add_argument('-t', '--type', type=str, help='Mapping type') + parser.add_argument('--value', type=str, help='Value') + parser.add_argument('cost', type=float, help='Cost') + return parser + + +class CliDeleteMapping(command.Command): + """Delete a Hashmap mapping.""" + + def take_action(self, parsed_args): + utils.get_client_from_osc(self).rating.hashmap.delete_mapping( + **vars(parsed_args)) + + def get_parser(self, prog_name): + parser = super(CliDeleteMapping, self).get_parser(prog_name) + parser.add_argument('mapping_id', type=str, help='Mapping ID') + return parser + + +class CliUpdateMapping(lister.Lister): + """Update a Hashmap mapping.""" + + columns = [ + ('mapping_id', 'Mapping ID'), + ('value', 'Value'), + ('cost', 'Cost'), + ('type', 'Type'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ('group_id', 'Group ID'), + ('tenant_id', 'Project ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.update_mapping( + **vars(parsed_args)) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliUpdateMapping, self).get_parser(prog_name) + parser.add_argument('-s', '--service-id', type=str, help='Service ID') + parser.add_argument('-g', '--group-id', type=str, help='Group ID') + parser.add_argument('--field-id', type=str, help='Field ID') + parser.add_argument('-p', '--project-id', type=str, dest='tenant_id', + help='Project ID') + parser.add_argument('--value', type=str, help='Value') + parser.add_argument('--cost', type=str, help='Cost') + parser.add_argument('mapping_id', type=str, help='Mapping ID') + return parser + + +class CliListGroup(lister.Lister): + """List existing hashmap groups.""" + + columns = [ + ('name', 'Name'), + ('group_id', 'Group ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.get_group() + values = utils.list_to_cols(resp['groups'], self.columns) + return [col[1] for col in self.columns], values + + +class CliCreateGroup(lister.Lister): + """Create a Hashmap group.""" + columns = [ + ('name', 'Name'), + ('group_id', 'Group ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.create_group( + **vars(parsed_args)) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliCreateGroup, self).get_parser(prog_name) + parser.add_argument('name', type=str, help='Group Name') + return parser + + +class CliDeleteGroup(command.Command): + """Create a Hashmap group.""" + + def take_action(self, parsed_args): + utils.get_client_from_osc(self).rating.hashmap.delete_group( + **vars(parsed_args)) + + def get_parser(self, prog_name): + parser = super(CliDeleteGroup, self).get_parser(prog_name) + parser.add_argument('--recursive', action='store_true', + help='Delete mappings recursively') + parser.add_argument('group_id', type=str, help='Group ID') + return parser + + +class CliGetGroupMappings(lister.Lister): + """Get all Hashmap mappings for the given group.""" + + columns = [ + ('mapping_id', 'Mapping ID'), + ('value', 'Value'), + ('cost', 'Cost'), + ('type', 'Type'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ('group_id', 'Group ID'), + ('tenant_id', 'Project ID'), + ] + + def take_action(self, parsed_args): + client = utils.get_client_from_osc(self) + resp = client.rating.hashmap.get_group_mappings(**vars(parsed_args)) + return ([col[1] for col in self.columns], + utils.list_to_cols(resp.get('mappings', []), self.columns)) + + def get_parser(self, prog_name): + parser = super(CliGetGroupMappings, self).get_parser(prog_name) + parser.add_argument('group_id', type=str, help='Group ID') + return parser + + +class CliGetGroupThresholds(lister.Lister): + """Get all thresholds for the given group.""" + + columns = [ + ('threshold_id', 'Threshold ID'), + ('level', 'Level'), + ('cost', 'Cost'), + ('type', 'Type'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ('group_id', 'Group ID'), + ('tenant_id', 'Project ID'), + ] + + def take_action(self, parsed_args): + client = utils.get_client_from_osc(self) + resp = client.rating.hashmap.get_group_thresholds(**vars(parsed_args)) + return ([col[1] for col in self.columns], + utils.list_to_cols(resp.get('thresholds', []), self.columns)) + + def get_parser(self, prog_name): + parser = super(CliGetGroupThresholds, self).get_parser(prog_name) + parser.add_argument('group_id', type=str, help='Group ID') + return parser + + +class CliGetThreshold(lister.Lister): + """Get a Hashmap threshold.""" + + columns = [ + ('threshold_id', 'Threshold ID'), + ('level', 'Level'), + ('cost', 'Cost'), + ('type', 'Type'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ('group_id', 'Group ID'), + ('tenant_id', 'Project ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.get_threshold( + threshold_id=parsed_args.threshold_id, + ) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliGetThreshold, self).get_parser(prog_name) + parser.add_argument('threshold_id', type=str, + help='Threshold ID to filter on') + return parser + + +class CliListThreshold(lister.Lister): + """List Hashmap thresholds""" + columns = [ + ('threshold_id', 'Threshold ID'), + ('level', 'Level'), + ('cost', 'Cost'), + ('type', 'Type'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ('group_id', 'Group ID'), + ('tenant_id', 'Project ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.get_threshold( + **vars(parsed_args)) + values = utils.list_to_cols(resp['thresholds'], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliListThreshold, self).get_parser(prog_name) + parser.add_argument('-s', '--service-id', type=str, + help='Service ID to filter on') + parser.add_argument('-g', '--group-id', type=str, + help='Group ID to filter on') + parser.add_argument('--field-id', type=str, + help='Field ID to filter on') + parser.add_argument('-p', '--project-id', type=str, dest='tenant_id', + help='Project ID to filter on') + parser.add_argument('--filter-tenant', action='store_true', + help='Explicitly filter on given tenant (allows ' + 'to filter on tenant being None)') + parser.add_argument('--no-group', action='store_true', + help='Filter on orphaned thresholds') + return parser + + +class CliCreateThreshold(lister.Lister): + """Create a Hashmap threshold.""" + columns = [ + ('threshold_id', 'Threshold ID'), + ('level', 'Level'), + ('cost', 'Cost'), + ('type', 'Type'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ('group_id', 'Group ID'), + ('tenant_id', 'Project ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.create_threshold( + **vars(parsed_args)) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliCreateThreshold, self).get_parser(prog_name) + parser.add_argument('-s', '--service-id', type=str, help='Service ID') + parser.add_argument('-g', '--group-id', type=str, help='Group ID') + parser.add_argument('--field-id', type=str, help='Field ID') + parser.add_argument('-p', '--project-id', type=str, dest='tenant_id', + help='Project ID') + parser.add_argument('-t', '--type', type=str, help='Threshold type') + parser.add_argument('level', type=str, help='Threshold level') + parser.add_argument('cost', type=float, help='Cost') + return parser + + +class CliDeleteThreshold(command.Command): + """Delete a Hashmap threshold.""" + + def take_action(self, parsed_args): + utils.get_client_from_osc(self).rating.hashmap.delete_threshold( + **vars(parsed_args)) + + def get_parser(self, prog_name): + parser = super(CliDeleteThreshold, self).get_parser(prog_name) + parser.add_argument('threshold_id', type=str, help='Threshold ID') + return parser + + +class CliUpdateThreshold(lister.Lister): + """Update a Hashmap threshold.""" + + columns = [ + ('threshold_id', 'Threshold ID'), + ('level', 'Level'), + ('cost', 'Cost'), + ('type', 'Type'), + ('field_id', 'Field ID'), + ('service_id', 'Service ID'), + ('group_id', 'Group ID'), + ('tenant_id', 'Project ID'), + ] + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.hashmap.update_threshold( + **vars(parsed_args)) + values = utils.list_to_cols([resp], self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliUpdateThreshold, self).get_parser(prog_name) + parser.add_argument('-s', '--service-id', type=str, help='Service ID') + parser.add_argument('-g', '--group-id', type=str, help='Group ID') + parser.add_argument('--field-id', type=str, help='Field ID') + parser.add_argument('-p', '--project-id', type=str, dest='tenant_id', + help='Project ID') + parser.add_argument('-l', '--level', type=str, help='Threshold level') + parser.add_argument('--cost', type=str, help='Cost') + parser.add_argument('threshold_id', type=str, help='Threshold ID') + return parser diff --git a/cloudkittyclient/v1/rating/pyscripts.py b/cloudkittyclient/v1/rating/pyscripts.py new file mode 100644 index 0000000..f4eb045 --- /dev/null +++ b/cloudkittyclient/v1/rating/pyscripts.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cloudkittyclient import exc +from cloudkittyclient.v1 import base + + +class PyscriptManager(base.BaseManager): + """Class used to manage the Pyscript rating module""" + + url = '/v1/rating/module_config/pyscripts/{endpoint}/{script_id}' + + def list_scripts(self, **kwargs): + """Get a list of all pyscripts. + + :param no_data: Set to True to remove script data from output. + :type no_data: bool + """ + authorized_args = ['no_data'] + url = self.get_url('scripts', kwargs, authorized_args) + return self.api_client.get(url).json() + + def get_script(self, **kwargs): + """Get the script corresponding to the given ID. + + :param script_id: ID of the script. + :type script_id: str + """ + if not kwargs.get('script_id'): + raise exc.ArgumentRequired("Argument 'script_id' is required.") + url = self.get_url('scripts', kwargs) + return self.api_client.get(url).json() + + def create_script(self, **kwargs): + """Create a new script. + + :param name: Name of the script to create + :type name: str + :param data: Content of the script + :type data: str + """ + for arg in ('name', 'data'): + if not kwargs.get(arg): + raise exc.ArgumentRequired( + "'Argument {} is required.'".format(arg)) + url = self.get_url('scripts', kwargs) + body = dict(name=kwargs['name'], data=kwargs['data']) + return self.api_client.post(url, json=body).json() + + def update_script(self, **kwargs): + """Update an existing script. + + :param script_id: ID of the script to update + :type script_id: str + :param name: Name of the script to create + :type name: str + :param data: Content of the script + :type data: str + """ + if not kwargs.get('script_id'): + raise exc.ArgumentRequired("Argument 'script_id' is required.") + script = self.get_script(script_id=kwargs['script_id']) + for key in ('name', 'data'): + if kwargs.get(key): + script[key] = kwargs[key] + script.pop('checksum', None) + url = self.get_url('scripts', kwargs) + return self.api_client.put(url, json=script).json() + + def delete_script(self, **kwargs): + """Delete a script. + + :param script_id: ID of the script to update + :type script_id: str + """ + if not kwargs.get('script_id'): + raise exc.ArgumentRequired("Argument 'script_id' is required.") + url = self.get_url('scripts', kwargs) + self.api_client.delete(url) diff --git a/cloudkittyclient/v1/rating/pyscripts/__init__.py b/cloudkittyclient/v1/rating/pyscripts/__init__.py deleted file mode 100644 index 21f3efa..0000000 --- a/cloudkittyclient/v1/rating/pyscripts/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2015 Objectif Libre -# 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. - -from cloudkittyclient.common import base - - -class Script(base.Resource): - key = 'script' - - def __repr__(self): - return "" % self._info - - -class ScriptManager(base.CrudManager): - resource_class = Script - base_url = '/v1/rating/module_config/pyscripts' - key = 'script' - collection_key = 'scripts' diff --git a/cloudkittyclient/v1/rating/pyscripts/client.py b/cloudkittyclient/v1/rating/pyscripts/client.py deleted file mode 100644 index b6a61b7..0000000 --- a/cloudkittyclient/v1/rating/pyscripts/client.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2015 Objectif Libre -# 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. - -from cloudkittyclient.v1.rating import pyscripts - - -class Client(object): - """Client for the PyScripts v1 API. - - :param http_client: A http client. - """ - - def __init__(self, http_client): - """Initialize a new client for the PyScripts v1 API.""" - self.http_client = http_client - self.scripts = pyscripts.ScriptManager(self.http_client) diff --git a/cloudkittyclient/v1/rating/pyscripts/extension.py b/cloudkittyclient/v1/rating/pyscripts/extension.py deleted file mode 100644 index 3025c1c..0000000 --- a/cloudkittyclient/v1/rating/pyscripts/extension.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2015 Objectif Libre -# 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. - -from cloudkittyclient.v1.rating.pyscripts import client -from cloudkittyclient.v1.rating.pyscripts import shell - - -class Extension(object): - """PyScripts extension. - - """ - - @staticmethod - def get_client(http_client): - return client.Client(http_client) - - @staticmethod - def get_shell(): - return shell diff --git a/cloudkittyclient/v1/rating/pyscripts/shell.py b/cloudkittyclient/v1/rating/pyscripts/shell.py deleted file mode 100644 index 591113c..0000000 --- a/cloudkittyclient/v1/rating/pyscripts/shell.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright 2015 Objectif Libre -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import functools - -from oslo_utils import strutils -import six - -from cloudkittyclient.apiclient import exceptions -from cloudkittyclient.common import utils -from cloudkittyclient import exc - -_bool_strict = functools.partial(strutils.bool_from_string, strict=True) - - -@utils.arg('-n', '--name', - help='Script name', - required=True) -@utils.arg('-f', '--file', - help='Script file', - required=False) -def do_pyscripts_script_create(cc, args={}): - """Create a script.""" - script_args = {'name': args.name} - if args.file: - with open(args.file) as fp: - script_args['data'] = fp.read() - out = cc.pyscripts.scripts.create(**script_args) - utils.print_dict(out.to_dict()) - - -@utils.arg('-d', '--show-data', - help='Show data in the listing', - required=False, - default=False) -def do_pyscripts_script_list(cc, args={}): - """List scripts.""" - request_args = {} - if not args.show_data: - request_args['no_data'] = True - scripts = cc.pyscripts.scripts.list(**request_args) - field_labels = ['Name', 'Script id', 'Data', 'Checksum'] - fields = ['name', 'script_id', 'data', 'checksum'] - utils.print_list(scripts, - fields, - field_labels, - sortby=0) - - -@utils.arg('-s', '--script-id', - help='Script uuid', - required=True) -def do_pyscripts_script_get(cc, args={}): - """Get script.""" - try: - script = cc.pyscripts.scripts.get(script_id=args.script_id) - except exceptions.NotFound: - raise exc.CommandError('Script not found: %s' % args.script_id) - utils.print_dict(script.to_dict()) - - -@utils.arg('-s', '--script-id', - help='Script uuid', - required=True) -def do_pyscripts_script_get_data(cc, args={}): - """Get script data.""" - try: - script = cc.pyscripts.scripts.get(script_id=args.script_id) - except exceptions.NotFound: - raise exc.CommandError('Script not found: %s' % args.script_id) - six.print_(script.data) - - -@utils.arg('-s', '--script-id', - help='Script uuid', - required=True) -def do_pyscripts_script_delete(cc, args={}): - """Delete a script.""" - try: - cc.pyscripts.scripts.delete(script_id=args.script_id) - except exceptions.NotFound: - raise exc.CommandError('Script not found: %s' % args.script_id) - - -@utils.arg('-s', '--script-id', - help='Script uuid', - required=True) -@utils.arg('-f', '--file', - help='Script file', - required=True) -def do_pyscripts_script_update(cc, args={}): - """Update a mapping.""" - excluded_fields = [ - 'checksum', - ] - with open(args.file) as fp: - content = fp.read() - try: - script = cc.pyscripts.scripts.get(script_id=args.script_id) - except exceptions.NotFound: - raise exc.CommandError('Script not found: %s' % args.script_id) - script_dict = script.to_dict() - for field in excluded_fields: - del script_dict[field] - script_dict['data'] = content - cc.pyscripts.scripts.update(**script_dict) diff --git a/cloudkittyclient/v1/rating/pyscripts/shell_cli.py b/cloudkittyclient/v1/rating/pyscripts/shell_cli.py deleted file mode 100644 index bf5f652..0000000 --- a/cloudkittyclient/v1/rating/pyscripts/shell_cli.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright 2016 Objectif Libre -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import functools - -from osc_lib.command import command -from oslo_utils import strutils - -from cloudkittyclient.v1.rating.pyscripts import shell - - -_bool_strict = functools.partial(strutils.bool_from_string, strict=True) - - -class CliPyScriptCreate(command.Command): - """Create a script.""" - def get_parser(self, prog_name): - parser = super(CliPyScriptCreate, self).get_parser(prog_name) - parser.add_argument('-n', '--name', - help='Script name', - required=True) - parser.add_argument('-f', '--file', - help='Script file', - required=False) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_pyscripts_script_create(ckclient, parsed_args) - - -class CliPyScriptList(command.Command): - """List scripts.""" - def get_parser(self, prog_name): - parser = super(CliPyScriptList, self).get_parser(prog_name) - parser.add_argument('-d', '--show-data', - help='Show data in the listing', - required=False, - default=False) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_pyscripts_script_list(ckclient, parsed_args) - - -class CliPyScriptGet(command.Command): - """Get script.""" - def get_parser(self, prog_name): - parser = super(CliPyScriptGet, self).get_parser(prog_name) - parser.add_argument('-s', '--script-id', - help='Script uuid', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_pyscripts_script_get(ckclient, parsed_args) - - -class CliPyScriptGetData(command.Command): - """Get script data.""" - def get_parser(self, prog_name): - parser = super(CliPyScriptGetData, self).get_parser(prog_name) - parser.add_argument('-s', '--script-id', - help='Script uuid', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_pyscripts_script_get_data(ckclient, parsed_args) - - -class CliPyScriptDelete(command.Command): - """Get script data.""" - def get_parser(self, prog_name): - parser = super(CliPyScriptDelete, self).get_parser(prog_name) - parser.add_argument('-s', '--script-id', - help='Script uuid', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_pyscripts_script_delete(ckclient, parsed_args) - - -class CliPyScriptUpdate(command.Command): - """Update a script.""" - def get_parser(self, prog_name): - parser = super(CliPyScriptUpdate, self).get_parser(prog_name) - parser.add_argument('-s', '--script-id', - help='Script uuid', - required=True) - parser.add_argument('-f', '--file', - help='Script file', - required=True) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_pyscripts_script_update(ckclient, parsed_args) diff --git a/cloudkittyclient/v1/rating/pyscripts_cli.py b/cloudkittyclient/v1/rating/pyscripts_cli.py new file mode 100644 index 0000000..8ede8de --- /dev/null +++ b/cloudkittyclient/v1/rating/pyscripts_cli.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cliff import command +from cliff import lister + +from cloudkittyclient import utils + + +class BaseScriptCli(lister.Lister): + + columns = [ + ('name', 'Name'), + ('script_id', 'Script ID'), + ('checksum', 'Checksum'), + ('data', 'Data'), + ] + + +class CliGetScript(BaseScriptCli): + """Get a PyScript.""" + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.pyscripts.get_script( + **vars(parsed_args)) + resp = [resp] + values = utils.list_to_cols(resp, self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliGetScript, self).get_parser(prog_name) + parser.add_argument('script_id', type=str, help='Script ID') + return parser + + +class CliListScripts(BaseScriptCli): + """List existing PyScripts.""" + + def take_action(self, parsed_args): + resp = utils.get_client_from_osc(self).rating.pyscripts.list_scripts( + **vars(parsed_args)) + resp = resp.get('scripts') or [] + values = utils.list_to_cols(resp, self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliListScripts, self).get_parser(prog_name) + parser.add_argument( + '-n', '--no-data', action='store_true', + help='Set to true to remove script data from output') + return parser + + +class CliCreateScript(BaseScriptCli): + """Create a PyScript.""" + + def take_action(self, parsed_args): + try: + with open(parsed_args.data, 'r') as fd: + parsed_args.data = fd.read() + except IOError: + pass + resp = utils.get_client_from_osc(self).rating.pyscripts.create_script( + **vars(parsed_args)) + resp = [resp] + values = utils.list_to_cols(resp, self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliCreateScript, self).get_parser(prog_name) + parser.add_argument('name', type=str, help='Script Name') + parser.add_argument('data', type=str, help='Script Data or data file') + return parser + + +class CliUpdateScript(BaseScriptCli): + """Update a PyScript.""" + + def take_action(self, parsed_args): + if parsed_args.data: + try: + with open(parsed_args.data, 'r') as fd: + parsed_args.data = fd.read() + except IOError: + pass + resp = utils.get_client_from_osc(self).rating.pyscripts.update_script( + **vars(parsed_args)) + resp = [resp] + values = utils.list_to_cols(resp, self.columns) + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliUpdateScript, self).get_parser(prog_name) + parser.add_argument('script_id', type=str, help='Script ID') + parser.add_argument('-n', '--name', type=str, help='Script Name') + parser.add_argument('-d', '--data', type=str, + help='Script Data or data file') + return parser + + +class CliDeleteScript(command.Command): + """Delete a PyScript.""" + + def take_action(self, parsed_args): + utils.get_client_from_osc(self).rating.pyscripts.delete_script( + **vars(parsed_args)) + + def get_parser(self, prog_name): + parser = super(CliDeleteScript, self).get_parser(prog_name) + parser.add_argument('script_id', type=str, help='Script ID') + return parser diff --git a/cloudkittyclient/v1/report.py b/cloudkittyclient/v1/report.py new file mode 100644 index 0000000..30cf24a --- /dev/null +++ b/cloudkittyclient/v1/report.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from oslo_log import log + +from cloudkittyclient.v1 import base + + +LOG = log.getLogger(__name__) + + +class ReportManager(base.BaseManager): + """Class used to handle /v1/report endpoint.""" + url = '/v1/report/{endpoint}' + + def get_summary(self, **kwargs): + """Returns a list of summaries. + + :param begin: Begin timestamp + :type begin: datetime.datetime + :param end: End timestamp + :type end: datetime.datetime + :param tenant_id: Tenant ID + :type tenant_id: str + :param groupby: Fields to group by. + :type groupby: list + :param all_tenants: Get summary from all tenants (admin only). Defaults + to False. + :type all_tenants: bool + """ + authorized_args = [ + 'begin', 'end', 'tenant_id', 'service', 'groupby', 'all_tenants'] + if kwargs.get('groupby', None): + kwargs['groupby'] = ','.join(kwargs['groupby']) + url = self.get_url('summary', kwargs, authorized_args) + return self.api_client.get(url).json() + + def get_total(self, **kwargs): + """Returns the total for the given tenant. + + :param begin: Begin timestamp + :type begin: datetime.datetime + :param end: End timestamp + :type end: datetime.datetime + :param tenant_id: Tenant ID + :type tenant_id: str + :param all_tenants: Get total from all tenants (admin only). Defaults + to False. + :type all_tenants: bool + """ + LOG.warning('WARNING: /v1/report/total/ endpoint is deprecated, ' + 'please use /v1/report/summary instead.') + authorized_args = [ + 'begin', 'end', 'tenant_id', 'service', 'all_tenants'] + url = self.get_url('total', kwargs, authorized_args) + return self.api_client.get(url).json() + + def get_tenants(self, **kwargs): + """Returns a list of tenants. + + :param begin: Begin timestamp + :type begin: datetime.datetime + :param end: End timestamp + :type end: datetime.datetime + """ + url = self.get_url('tenants', kwargs, ['begin', 'end']) + return self.api_client.get(url).json() diff --git a/cloudkittyclient/v1/report/__init__.py b/cloudkittyclient/v1/report/__init__.py deleted file mode 100644 index 0dbba8f..0000000 --- a/cloudkittyclient/v1/report/__init__.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2015 Objectif Libre -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from cloudkittyclient.common import base - - -class ReportSummary(base.Resource): - - key = 'summary' - - def __init(self, tenant_id=None, res_type=None, begin=None, - end=None, rate=None): - self.tenant_id = tenant_id - self.res_type = res_type - self.begin = begin - self.end = end - self.rate = rate - - def __repr__(self): - return "" % self._info - - -class DataFrameManager(base.CrudManager): - resource_class = DataFrameResource - base_url = '/v1/storage' - key = 'dataframe' - collection_key = 'dataframes' diff --git a/cloudkittyclient/v1/storage/shell.py b/cloudkittyclient/v1/storage/shell.py deleted file mode 100644 index 7f05395..0000000 --- a/cloudkittyclient/v1/storage/shell.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2015 Objectif Libre -# -# 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. - -from cloudkittyclient.common import utils - - -@utils.arg('-b', '--begin', - help='Starting date/time (YYYY-MM-DDTHH:MM:SS)', - required=False) -@utils.arg('-e', '--end', - help='Ending date/time (YYYY-MM-DDTHH:MM:SS)', - required=False) -@utils.arg('-t', '--tenant', - help='Tenant ID', - required=False, - default=None) -@utils.arg('-r', '--resource-type', - help='Resource type (compute, image, ...)', - required=False, - default=None) -def do_storage_dataframe_list(cc, args): - """List dataframes.""" - data = cc.storage.dataframes.list(begin=args.begin, end=args.end, - tenant_id=args.tenant, - resource_type=args.resource_type) - fields = ['begin', 'end', 'tenant_id', 'resources'] - fields_labels = ['Begin', 'End', 'Tenant ID', 'Resources'] - utils.print_list(data, fields, fields_labels, sortby=0) diff --git a/cloudkittyclient/v1/storage/shell_cli.py b/cloudkittyclient/v1/storage/shell_cli.py deleted file mode 100644 index 55943bb..0000000 --- a/cloudkittyclient/v1/storage/shell_cli.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2016 Objectif Libre -# -# 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. - -from osc_lib.command import command - -from cloudkittyclient.v1.storage import shell - - -class CliStorageDataframeList(command.Command): - """List dataframes.""" - def get_parser(self, prog_name): - parser = super(CliStorageDataframeList, self).get_parser(prog_name) - parser.add_argument('-b', '--begin', - help='Starting date/time (YYYY-MM-ddThh:mm:ss)', - required=False) - parser.add_argument('-e', '--end', - help='Ending date/time (YYYY-MM-ddThh:mm:ss)', - required=False) - parser.add_argument('-t', '--tenant', - help='Tenant ID', - required=False, - default=None) - parser.add_argument('-r', '--resource-type', - help='Resource type (compute, image...)', - required=False, - default=None) - return parser - - def take_action(self, parsed_args): - ckclient = self.app.client_manager.rating - shell.do_storage_dataframe_list(ckclient, parsed_args) diff --git a/cloudkittyclient/v1/storage_cli.py b/cloudkittyclient/v1/storage_cli.py new file mode 100644 index 0000000..ca04b22 --- /dev/null +++ b/cloudkittyclient/v1/storage_cli.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Objectif Libre +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from cliff import lister + +from cloudkittyclient import exc +from cloudkittyclient import utils + + +class CliGetDataframes(lister.Lister): + """List stored dataframes or generate CSV reports. + + Dataframes can be filtered on resource_type and project_id. + CSV reports can be generated with the 'df-to-csv' formatter. + A config file may be provided to configure the output of that formatter. + See documentation for more details. + """ + columns = [ + ('begin', 'Begin'), + ('end', 'End'), + ('tenant_id', 'Project ID'), + ('resources', 'Resources'), + ] + + def take_action(self, parsed_args): + for arg in ['begin', 'end']: + value = getattr(parsed_args, arg) + if value is not None: + try: + setattr(parsed_args, arg, utils.iso2dt(value)) + except ValueError: + raise exc.InvalidArgumentError( + 'Invalid timestamp "{}"'.format(value)) + resp = utils.get_client_from_osc(self).storage.get_dataframes( + **vars(parsed_args)).get('dataframes', []) + values = utils.list_to_cols(resp, self.columns) + for value in values: + for resource in value[3]: + rating = float(resource['rating']) + volume = float(resource['volume']) + if volume > 0: + resource['rate_value'] = '{:.4f}'.format(rating / volume) + else: + resource['rate_value'] = '' + return [col[1] for col in self.columns], values + + def get_parser(self, prog_name): + parser = super(CliGetDataframes, self).get_parser(prog_name) + parser.add_argument('-b', '--begin', type=str, help='Begin timestamp') + parser.add_argument('-e', '--end', type=str, help='End timestamp') + parser.add_argument('-p', '--project-id', type=str, dest='tenant_id', + help='Id of the tenant to filter on') + parser.add_argument('-r', '--resource_type', type=str, + help='Resource type to filter on') + return parser diff --git a/doc/source/api_reference/collector.rst b/doc/source/api_reference/collector.rst new file mode 100644 index 0000000..077e597 --- /dev/null +++ b/doc/source/api_reference/collector.rst @@ -0,0 +1,6 @@ +========================= +collector (/v1/collector) +========================= + +.. automodule:: cloudkittyclient.v1.collector + :members: diff --git a/doc/source/api_reference/index.rst b/doc/source/api_reference/index.rst new file mode 100644 index 0000000..e9e9c17 --- /dev/null +++ b/doc/source/api_reference/index.rst @@ -0,0 +1,15 @@ +============= +Api Reference +============= + +A ``client.Client`` instance has the following submodules (each one +corresponding to an API endpoint): + +.. toctree:: + :maxdepth: 2 + + report + info + rating + collector + storage diff --git a/doc/source/api_reference/info.rst b/doc/source/api_reference/info.rst new file mode 100644 index 0000000..0c22fef --- /dev/null +++ b/doc/source/api_reference/info.rst @@ -0,0 +1,6 @@ +=============== +info (/v1/info) +=============== + +.. automodule:: cloudkittyclient.v1.info + :members: diff --git a/doc/source/api_reference/rating.rst b/doc/source/api_reference/rating.rst new file mode 100644 index 0000000..899f2be --- /dev/null +++ b/doc/source/api_reference/rating.rst @@ -0,0 +1,18 @@ +=================== +rating (/v1/rating) +=================== + +.. automodule:: cloudkittyclient.v1.rating + :members: + +rating.hashmap (/v1/rating/module_config/hashmap) +================================================= + +.. automodule:: cloudkittyclient.v1.rating.hashmap + :members: + +rating.pyscripts (/v1/rating/module_config/pyscripts) +===================================================== + +.. automodule:: cloudkittyclient.v1.rating.pyscripts + :members: diff --git a/doc/source/api_reference/report.rst b/doc/source/api_reference/report.rst new file mode 100644 index 0000000..e20cd7c --- /dev/null +++ b/doc/source/api_reference/report.rst @@ -0,0 +1,6 @@ +=================== +report (/v1/report) +=================== + +.. automodule:: cloudkittyclient.v1.report + :members: diff --git a/doc/source/api_reference/storage.rst b/doc/source/api_reference/storage.rst new file mode 100644 index 0000000..caffb6a --- /dev/null +++ b/doc/source/api_reference/storage.rst @@ -0,0 +1,6 @@ +===================== +storage (/v1/storage) +===================== + +.. automodule:: cloudkittyclient.v1.storage + :members: diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst deleted file mode 100644 index ce2efdf..0000000 --- a/doc/source/cli/index.rst +++ /dev/null @@ -1,1106 +0,0 @@ -.. ################################################### -.. ## WARNING ###################################### -.. ############## WARNING ########################## -.. ########################## WARNING ############## -.. ###################################### WARNING ## -.. ################################################### -.. ################################################### -.. ## -.. This file is tool-generated. Do not edit manually. -.. http://docs.openstack.org/contributor-guide/ -.. doc-tools/cli-reference.html -.. ## -.. ## WARNING ###################################### -.. ############## WARNING ########################## -.. ########################## WARNING ############## -.. ###################################### WARNING ## -.. ################################################### - -=============================================== -Rating service (cloudkitty) command-line client -=============================================== - -The cloudkitty client is the command-line interface (CLI) for -the Rating service (cloudkitty) API and its extensions. - -This chapter documents :command:`cloudkitty` version ``1.1.0``. - -For help on a specific :command:`cloudkitty` command, enter: - -.. code-block:: console - - $ cloudkitty help COMMAND - -.. _cloudkitty_command_usage: - -cloudkitty usage -~~~~~~~~~~~~~~~~ - -.. code-block:: console - - usage: cloudkitty [--version] [-d] [-v] [--timeout TIMEOUT] - [--cloudkitty-url ] - [--cloudkitty-api-version CLOUDKITTY_API_VERSION] - [--os-tenant-id ] - [--os-region-name ] - [--os-auth-token ] - [--os-service-type ] - [--os-endpoint-type ] [--os-cacert ] - [--os-insecure ] [--os-cert-file ] - [--os-key-file ] [--os-cert ] - [--os-key ] [--os-project-name ] - [--os-project-id ] - [--os-project-domain-id ] - [--os-project-domain-name ] - [--os-user-id ] - [--os-user-domain-id ] - [--os-user-domain-name ] - [--os-endpoint ] [--os-auth-system ] - [--os-username ] [--os-password ] - [--os-tenant-name ] [--os-token ] - [--os-auth-url ] - ... - -**Subcommands:** - -``info-config-get`` - Get cloudkitty configuration. - -``info-service-get`` - Get service info. - -``module-disable`` - Disable a module. - -``module-enable`` - Enable a module. - -``module-list`` - List the samples for this meters. - -``module-set-priority`` - Set module priority. - -``collector-mapping-create`` - Create collector mapping. - -``collector-mapping-delete`` - Delete collector mapping. - -``collector-mapping-get`` - Show collector mapping detail. - -``collector-mapping-list`` - List collector mapping. - -``collector-state-disable`` - Disable collector state. - -``collector-state-enable`` - Enable collector state. - -``collector-state-get`` - Show collector state. - -``report-tenant-list`` - List tenant report. - -``summary-get`` - Get summary report. - -``total-get`` - Get total reports. - -``storage-dataframe-list`` - List dataframes. - -``hashmap-field-create`` - Create a field. - -``hashmap-field-delete`` - Delete a field. - -``hashmap-field-list`` - List fields. - -``hashmap-group-create`` - Create a group. - -``hashmap-group-delete`` - Delete a group. - -``hashmap-group-list`` - List groups. - -``hashmap-mapping-create`` - Create a mapping. - -``hashmap-mapping-delete`` - Delete a mapping. - -``hashmap-mapping-list`` - List mappings. - -``hashmap-mapping-update`` - Update a mapping. - -``hashmap-service-create`` - Create a service. - -``hashmap-service-delete`` - Delete a service. - -``hashmap-service-list`` - List services. - -``hashmap-threshold-create`` - Create a mapping. - -``hashmap-threshold-delete`` - Delete a threshold. - -``hashmap-threshold-get`` - Get a threshold. - -``hashmap-threshold-group`` - Get a threshold group. - -``hashmap-threshold-list`` - List thresholds. - -``hashmap-threshold-update`` - Update a threshold. - -``pyscripts-script-create`` - Create a script. - -``pyscripts-script-delete`` - Delete a script. - -``pyscripts-script-get`` - Get script. - -``pyscripts-script-get-data`` - Get script data. - -``pyscripts-script-list`` - List scripts. - -``pyscripts-script-update`` - Update a mapping. - -``bash-completion`` - Prints all of the commands and options to - stdout. - -``help`` - Display help about this program or one of its - subcommands. - -.. _cloudkitty_command_options: - -cloudkitty optional arguments -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``--version`` - show program's version number and exit - -``-d, --debug`` - Defaults to ``env[CLOUDKITTYCLIENT_DEBUG]``. - -``-v, --verbose`` - Print more verbose output. - -``--timeout TIMEOUT`` - Number of seconds to wait for a response. - -``--cloudkitty-url `` - **DEPRECATED**, use --os-endpoint instead. - Defaults to ``env[CLOUDKITTY_URL]``. - -``--cloudkitty-api-version CLOUDKITTY_API_VERSION`` - Defaults to ``env[CLOUDKITTY_API_VERSION]`` or 1. - -``--os-tenant-id `` - Defaults to ``env[OS_TENANT_ID]``. - -``--os-region-name `` - Defaults to ``env[OS_REGION_NAME]``. - -``--os-auth-token `` - Defaults to ``env[OS_AUTH_TOKEN]``. - -``--os-service-type `` - Defaults to ``env[OS_SERVICE_TYPE]``. - -``--os-endpoint-type `` - Defaults to ``env[OS_ENDPOINT_TYPE]``. - -``--os-cacert `` - Defaults to ``env[OS_CACERT]``. - -``--os-insecure `` - Defaults to ``env[OS_INSECURE]``. - -``--os-cert-file `` - Defaults to ``env[OS_CERT_FILE]``. - -``--os-key-file `` - Defaults to ``env[OS_KEY_FILE]``. - -``--os-cert `` - Defaults to ``env[OS_CERT]``. - -``--os-key `` - Defaults to ``env[OS_KEY]``. - -``--os-project-name `` - Defaults to ``env[OS_PROJECT_NAME]``. - -``--os-project-id `` - Defaults to ``env[OS_PROJECT_ID]``. - -``--os-project-domain-id `` - Defaults to ``env[OS_PROJECT_DOMAIN_ID]``. - -``--os-project-domain-name `` - Defaults to ``env[OS_PROJECT_DOMAIN_NAME]``. - -``--os-user-id `` - Defaults to ``env[OS_USER_ID]``. - -``--os-user-domain-id `` - Defaults to ``env[OS_USER_DOMAIN_ID]``. - -``--os-user-domain-name `` - Defaults to ``env[OS_USER_DOMAIN_NAME]``. - -``--os-endpoint `` - Defaults to ``env[OS_ENDPOINT]``. - -``--os-auth-system `` - Defaults to ``env[OS_AUTH_SYSTEM]``. - -``--os-username `` - Defaults to ``env[OS_USERNAME]``. - -``--os-password `` - Defaults to ``env[OS_PASSWORD]``. - -``--os-tenant-name `` - Defaults to ``env[OS_TENANT_NAME]``. - -``--os-token `` - Defaults to ``env[OS_TOKEN]``. - -``--os-auth-url `` - Defaults to ``env[OS_AUTH_URL]``. - -.. _cloudkitty_collector-mapping-create: - -cloudkitty collector-mapping-create ------------------------------------ - -.. code-block:: console - - usage: cloudkitty collector-mapping-create -c COLLECTOR -s SERVICE - -Create collector mapping. - -**Optional arguments:** - -``-c COLLECTOR, --collector COLLECTOR`` - Map a service to this collector. required. - -``-s SERVICE, --service SERVICE`` - Map a collector to this service. required. - -.. _cloudkitty_collector-mapping-delete: - -cloudkitty collector-mapping-delete ------------------------------------ - -.. code-block:: console - - usage: cloudkitty collector-mapping-delete -s SERVICE - -Delete collector mapping. - -**Optional arguments:** - -``-s SERVICE, --service SERVICE`` - Filter on this service. required. - -.. _cloudkitty_collector-mapping-get: - -cloudkitty collector-mapping-get --------------------------------- - -.. code-block:: console - - usage: cloudkitty collector-mapping-get -s SERVICE - -Show collector mapping detail. - -**Optional arguments:** - -``-s SERVICE, --service SERVICE`` - Which service to get the mapping for. - required. - -.. _cloudkitty_collector-mapping-list: - -cloudkitty collector-mapping-list ---------------------------------- - -.. code-block:: console - - usage: cloudkitty collector-mapping-list [-c COLLECTOR] - -List collector mapping. - -**Optional arguments:** - -``-c COLLECTOR, --collector COLLECTOR`` - Collector name to filter on. Defaults to None. - -.. _cloudkitty_collector-state-disable: - -cloudkitty collector-state-disable ----------------------------------- - -.. code-block:: console - - usage: cloudkitty collector-state-disable -n NAME - -Disable collector state. - -**Optional arguments:** - -``-n NAME, --name NAME`` - Name of the collector. required. - -.. _cloudkitty_collector-state-enable: - -cloudkitty collector-state-enable ---------------------------------- - -.. code-block:: console - - usage: cloudkitty collector-state-enable -n NAME - -Enable collector state. - -**Optional arguments:** - -``-n NAME, --name NAME`` - Name of the collector. required. - -.. _cloudkitty_collector-state-get: - -cloudkitty collector-state-get ------------------------------- - -.. code-block:: console - - usage: cloudkitty collector-state-get -n NAME - -Show collector state. - -**Optional arguments:** - -``-n NAME, --name NAME`` - Name of the collector. required. - -.. _cloudkitty_hashmap-field-create: - -cloudkitty hashmap-field-create -------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-field-create -n NAME -s SERVICE_ID - -Create a field. - -**Optional arguments:** - -``-n NAME, --name NAME`` - Field name required. - -``-s SERVICE_ID, --service-id SERVICE_ID`` - Service id required. - -.. _cloudkitty_hashmap-field-delete: - -cloudkitty hashmap-field-delete -------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-field-delete -f FIELD_ID - -Delete a field. - -**Optional arguments:** - -``-f FIELD_ID, --field-id FIELD_ID`` - Field uuid required. - -.. _cloudkitty_hashmap-field-list: - -cloudkitty hashmap-field-list ------------------------------ - -.. code-block:: console - - usage: cloudkitty hashmap-field-list -s SERVICE_ID - -List fields. - -**Optional arguments:** - -``-s SERVICE_ID, --service-id SERVICE_ID`` - Service id required. - -.. _cloudkitty_hashmap-group-create: - -cloudkitty hashmap-group-create -------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-group-create -n NAME - -Create a group. - -**Optional arguments:** - -``-n NAME, --name NAME`` - Group name required. - -.. _cloudkitty_hashmap-group-delete: - -cloudkitty hashmap-group-delete -------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-group-delete -g GROUP_ID [-r RECURSIVE] - -Delete a group. - -**Optional arguments:** - -``-g GROUP_ID, --group-id GROUP_ID`` - Group uuid required. - -``-r RECURSIVE, --recursive RECURSIVE`` - Delete the group's mappings Defaults to False. - -.. _cloudkitty_hashmap-group-list: - -cloudkitty hashmap-group-list ------------------------------ - -.. code-block:: console - - usage: cloudkitty hashmap-group-list - -List groups. - -.. _cloudkitty_hashmap-mapping-create: - -cloudkitty hashmap-mapping-create ---------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-mapping-create [-s SERVICE_ID] [-f FIELD_ID] -c COST - [-v VALUE] [-t TYPE] [-g GROUP_ID] - [-p PROJECT_ID] - -Create a mapping. - -**Optional arguments:** - -``-s SERVICE_ID, --service-id SERVICE_ID`` - Service id. - -``-f FIELD_ID, --field-id FIELD_ID`` - Field id. - -``-c COST, --cost COST`` - Mapping cost required. - -``-v VALUE, --value VALUE`` - Mapping value. - -``-t TYPE, --type TYPE`` - Mapping type (flat, rate). - -``-g GROUP_ID, --group-id GROUP_ID`` - Group id. - -``-p PROJECT_ID, --project-id PROJECT_ID`` - Project/tenant id. - -.. _cloudkitty_hashmap-mapping-delete: - -cloudkitty hashmap-mapping-delete ---------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-mapping-delete -m MAPPING_ID - -Delete a mapping. - -**Optional arguments:** - -``-m MAPPING_ID, --mapping-id MAPPING_ID`` - Mapping uuid required. - -.. _cloudkitty_hashmap-mapping-list: - -cloudkitty hashmap-mapping-list -------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-mapping-list [-s SERVICE_ID] [-f FIELD_ID] - [-g GROUP_ID] [-p PROJECT_ID] - -List mappings. - -**Optional arguments:** - -``-s SERVICE_ID, --service-id SERVICE_ID`` - Service id. - -``-f FIELD_ID, --field-id FIELD_ID`` - Field id. - -``-g GROUP_ID, --group-id GROUP_ID`` - Group id. - -``-p PROJECT_ID, --project-id PROJECT_ID`` - Project/tenant id. - -.. _cloudkitty_hashmap-mapping-update: - -cloudkitty hashmap-mapping-update ---------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-mapping-update -m MAPPING_ID [-c COST] [-v VALUE] - [-t TYPE] [-g GROUP_ID] - [-p PROJECT_ID] - -Update a mapping. - -**Optional arguments:** - -``-m MAPPING_ID, --mapping-id MAPPING_ID`` - Mapping id required. - -``-c COST, --cost COST`` - Mapping cost. - -``-v VALUE, --value VALUE`` - Mapping value. - -``-t TYPE, --type TYPE`` - Mapping type (flat, rate). - -``-g GROUP_ID, --group-id GROUP_ID`` - Group id. - -``-p PROJECT_ID, --project-id PROJECT_ID`` - Project/tenant id. - -.. _cloudkitty_hashmap-service-create: - -cloudkitty hashmap-service-create ---------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-service-create -n NAME - -Create a service. - -**Optional arguments:** - -``-n NAME, --name NAME`` - Service name required. - -.. _cloudkitty_hashmap-service-delete: - -cloudkitty hashmap-service-delete ---------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-service-delete -s SERVICE_ID - -Delete a service. - -**Optional arguments:** - -``-s SERVICE_ID, --service-id SERVICE_ID`` - Service uuid required. - -.. _cloudkitty_hashmap-service-list: - -cloudkitty hashmap-service-list -------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-service-list - -List services. - -.. _cloudkitty_hashmap-threshold-create: - -cloudkitty hashmap-threshold-create ------------------------------------ - -.. code-block:: console - - usage: cloudkitty hashmap-threshold-create [-s SERVICE_ID] [-f FIELD_ID] -l - LEVEL -c COST [-t TYPE] - [-g GROUP_ID] [-p PROJECT_ID] - -Create a mapping. - -**Optional arguments:** - -``-s SERVICE_ID, --service-id SERVICE_ID`` - Service id. - -``-f FIELD_ID, --field-id FIELD_ID`` - Field id. - -``-l LEVEL, --level LEVEL`` - Threshold level required. - -``-c COST, --cost COST`` - Threshold cost required. - -``-t TYPE, --type TYPE`` - Threshold type (flat, rate). - -``-g GROUP_ID, --group-id GROUP_ID`` - Group id. - -``-p PROJECT_ID, --project-id PROJECT_ID`` - Project/tenant id. - -.. _cloudkitty_hashmap-threshold-delete: - -cloudkitty hashmap-threshold-delete ------------------------------------ - -.. code-block:: console - - usage: cloudkitty hashmap-threshold-delete -i THRESHOLD_ID - -Delete a threshold. - -**Optional arguments:** - -``-i THRESHOLD_ID, --threshold-id THRESHOLD_ID`` - Threshold uuid required. - -.. _cloudkitty_hashmap-threshold-get: - -cloudkitty hashmap-threshold-get --------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-threshold-get -i THRESHOLD_ID - -Get a threshold. - -**Optional arguments:** - -``-i THRESHOLD_ID, --threshold-id THRESHOLD_ID`` - Threshold uuid required. - -.. _cloudkitty_hashmap-threshold-group: - -cloudkitty hashmap-threshold-group ----------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-threshold-group -i THRESHOLD_ID - -Get a threshold group. - -**Optional arguments:** - -``-i THRESHOLD_ID, --threshold-id THRESHOLD_ID`` - Threshold uuid required. - -.. _cloudkitty_hashmap-threshold-list: - -cloudkitty hashmap-threshold-list ---------------------------------- - -.. code-block:: console - - usage: cloudkitty hashmap-threshold-list [-s SERVICE_ID] [-f FIELD_ID] - [-g GROUP_ID] - [--no-group {True,False}] - [-p PROJECT_ID] - -List thresholds. - -**Optional arguments:** - -``-s SERVICE_ID, --service-id SERVICE_ID`` - Service id. - -``-f FIELD_ID, --field-id FIELD_ID`` - Field id. - -``-g GROUP_ID, --group-id GROUP_ID`` - Group id. - -``--no-group {True,False}`` - If True, list only orhpaned thresholds. - -``-p PROJECT_ID, --project-id PROJECT_ID`` - Project/tenant id. - -.. _cloudkitty_hashmap-threshold-update: - -cloudkitty hashmap-threshold-update ------------------------------------ - -.. code-block:: console - - usage: cloudkitty hashmap-threshold-update -i THRESHOLD_ID [-l LEVEL] - [-c COST] [-t TYPE] [-g GROUP_ID] - [-p PROJECT_ID] - -Update a threshold. - -**Optional arguments:** - -``-i THRESHOLD_ID, --threshold-id THRESHOLD_ID`` - Threshold id required. - -``-l LEVEL, --level LEVEL`` - Threshold level. - -``-c COST, --cost COST`` - Threshold cost. - -``-t TYPE, --type TYPE`` - Threshold type (flat, rate). - -``-g GROUP_ID, --group-id GROUP_ID`` - Group id. - -``-p PROJECT_ID, --project-id PROJECT_ID`` - Project/tenant id. - -.. _cloudkitty_info-config-get: - -cloudkitty info-config-get --------------------------- - -.. code-block:: console - - usage: cloudkitty info-config-get - -Get cloudkitty configuration. - -.. _cloudkitty_info-service-get: - -cloudkitty info-service-get ---------------------------- - -.. code-block:: console - - usage: cloudkitty info-service-get [-n NAME] - -Get service info. - -**Optional arguments:** - -``-n NAME, --name NAME`` - Service name. - -.. _cloudkitty_module-disable: - -cloudkitty module-disable -------------------------- - -.. code-block:: console - - usage: cloudkitty module-disable -n NAME - -Disable a module. - -**Optional arguments:** - -``-n NAME, --name NAME`` - Module name required. - -.. _cloudkitty_module-enable: - -cloudkitty module-enable ------------------------- - -.. code-block:: console - - usage: cloudkitty module-enable -n NAME - -Enable a module. - -**Optional arguments:** - -``-n NAME, --name NAME`` - Module name required. - -.. _cloudkitty_module-list: - -cloudkitty module-list ----------------------- - -.. code-block:: console - - usage: cloudkitty module-list - -List the samples for this meters. - -.. _cloudkitty_module-set-priority: - -cloudkitty module-set-priority ------------------------------- - -.. code-block:: console - - usage: cloudkitty module-set-priority -n NAME -p PRIORITY - -Set module priority. - -**Optional arguments:** - -``-n NAME, --name NAME`` - Module name required. - -``-p PRIORITY, --priority PRIORITY`` - Module priority required. - -.. _cloudkitty_pyscripts-script-create: - -cloudkitty pyscripts-script-create ----------------------------------- - -.. code-block:: console - - usage: cloudkitty pyscripts-script-create -n NAME [-f FILE] - -Create a script. - -**Optional arguments:** - -``-n NAME, --name NAME`` - Script name required. - -``-f FILE, --file FILE`` - Script file. - -.. _cloudkitty_pyscripts-script-delete: - -cloudkitty pyscripts-script-delete ----------------------------------- - -.. code-block:: console - - usage: cloudkitty pyscripts-script-delete -s SCRIPT_ID - -Delete a script. - -**Optional arguments:** - -``-s SCRIPT_ID, --script-id SCRIPT_ID`` - Script uuid required. - -.. _cloudkitty_pyscripts-script-get: - -cloudkitty pyscripts-script-get -------------------------------- - -.. code-block:: console - - usage: cloudkitty pyscripts-script-get -s SCRIPT_ID - -Get script. - -**Optional arguments:** - -``-s SCRIPT_ID, --script-id SCRIPT_ID`` - Script uuid required. - -.. _cloudkitty_pyscripts-script-get-data: - -cloudkitty pyscripts-script-get-data ------------------------------------- - -.. code-block:: console - - usage: cloudkitty pyscripts-script-get-data -s SCRIPT_ID - -Get script data. - -**Optional arguments:** - -``-s SCRIPT_ID, --script-id SCRIPT_ID`` - Script uuid required. - -.. _cloudkitty_pyscripts-script-list: - -cloudkitty pyscripts-script-list --------------------------------- - -.. code-block:: console - - usage: cloudkitty pyscripts-script-list [-d SHOW_DATA] - -List scripts. - -**Optional arguments:** - -``-d SHOW_DATA, --show-data SHOW_DATA`` - Show data in the listing Defaults to False. - -.. _cloudkitty_pyscripts-script-update: - -cloudkitty pyscripts-script-update ----------------------------------- - -.. code-block:: console - - usage: cloudkitty pyscripts-script-update -s SCRIPT_ID -f FILE - -Update a mapping. - -**Optional arguments:** - -``-s SCRIPT_ID, --script-id SCRIPT_ID`` - Script uuid required. - -``-f FILE, --file FILE`` - Script file required. - -.. _cloudkitty_report-tenant-list: - -cloudkitty report-tenant-list ------------------------------ - -.. code-block:: console - - usage: cloudkitty report-tenant-list - -List tenant report. - -.. _cloudkitty_storage-dataframe-list: - -cloudkitty storage-dataframe-list ---------------------------------- - -.. code-block:: console - - usage: cloudkitty storage-dataframe-list [-b BEGIN] [-e END] [-t TENANT] - [-r RESOURCE_TYPE] - -List dataframes. - -**Optional arguments:** - -``-b BEGIN, --begin BEGIN`` - Starting date/time (YYYY-MM-DDTHH:MM:SS). - -``-e END, --end END`` - Ending date/time (YYYY-MM-DDTHH:MM:SS). - -``-t TENANT, --tenant TENANT`` - Tenant ID Defaults to None. - -``-r RESOURCE_TYPE, --resource-type RESOURCE_TYPE`` - Resource type (compute, image, ...) Defaults - to None. - -.. _cloudkitty_summary-get: - -cloudkitty summary-get ----------------------- - -.. code-block:: console - - usage: cloudkitty summary-get [-t SUMMARY_TENANT_ID] [-b BEGIN] [-e END] - [-s SERVICE] [-g GROUPBY] [-a] - -Get summary report. - -**Optional arguments:** - -``-t SUMMARY_TENANT_ID, --tenant-id SUMMARY_TENANT_ID`` - Tenant id. - -``-b BEGIN, --begin BEGIN`` - Begin timestamp. - -``-e END, --end END`` - End timestamp. - -``-s SERVICE, --service SERVICE`` - Service Type. - -``-g GROUPBY, --groupby GROUPBY`` - Fields to groupby, separated by commas if - multiple, now support res_type,tenant_id. - -``-a, --all-tenants`` - Allows to get summary from all tenants (admin - only). Defaults to False. - -.. _cloudkitty_total-get: - -cloudkitty total-get --------------------- - -.. code-block:: console - - usage: cloudkitty total-get [-t TOTAL_TENANT_ID] [-b BEGIN] [-e END] - [-s SERVICE] [-a] - -Get total reports. - -**Optional arguments:** - -``-t TOTAL_TENANT_ID, --tenant-id TOTAL_TENANT_ID`` - Tenant id. - -``-b BEGIN, --begin BEGIN`` - Starting date/time (YYYY-MM-DDTHH:MM:SS). - -``-e END, --end END`` - Ending date/time (YYYY-MM-DDTHH:MM:SS). - -``-s SERVICE, --service SERVICE`` - Service Type. - -``-a, --all-tenants`` - Allows to get total from all tenants (admin - only). Defaults to False. - diff --git a/doc/source/cli_reference.rst b/doc/source/cli_reference.rst new file mode 100644 index 0000000..856f9bf --- /dev/null +++ b/doc/source/cli_reference.rst @@ -0,0 +1,7 @@ +============= +CLI Reference +============= + +.. autoprogram-cliff:: cloudkittyclient + :application: cloudkitty + :ignored: --format, --column, --max-width, --fit-width, --print-empty, --format-config-file, --noindent, --quote, --sort-column diff --git a/doc/source/conf.py b/doc/source/conf.py index 342129b..fb4c3b2 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -20,9 +20,9 @@ sys.path.insert(0, os.path.abspath('../..')) # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ + 'cliff.sphinxext', 'sphinx.ext.autodoc', - #'sphinx.ext.intersphinx', - 'openstackdocstheme' + 'openstackdocstheme', ] # autodoc generation is a bit aggressive and a nuisance when doing heavy diff --git a/doc/source/contributor/index.rst b/doc/source/contributor.rst similarity index 50% rename from doc/source/contributor/index.rst rename to doc/source/contributor.rst index 3d4ceb4..1f5ca23 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor.rst @@ -2,4 +2,4 @@ Contributing ============ -.. include:: ../../../CONTRIBUTING.rst +.. include:: ../../CONTRIBUTING.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index c3ea016..962ec2c 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -10,10 +10,11 @@ Contents: .. toctree:: :maxdepth: 2 - install/index - contributor/index - cli/index - user/index + install + usage + contributor + cli_reference + api_reference/index Indices and tables ================== diff --git a/doc/source/install/index.rst b/doc/source/install.rst similarity index 100% rename from doc/source/install/index.rst rename to doc/source/install.rst diff --git a/doc/source/usage.rst b/doc/source/usage.rst new file mode 100644 index 0000000..1579d9b --- /dev/null +++ b/doc/source/usage.rst @@ -0,0 +1,97 @@ +===== +Usage +===== + +Python library +============== + +You can use cloudkittyclient with or without keystone authentication. In order +to use it without keystone authentication, cloudkittyclient provides the +``CloudKittyNoAuthPlugin`` keystoneauth plugin:: + + >>> from cloudkittyclient import client as ck_client + >>> from cloudkittyclient import auth as ck_auth + + >>> auth = ck_auth.CloudKittyNoAuthPlugin(endpoint='http://127.0.0.1:8889') + >>> client = ck_client.Client('1', auth=auth) + >>> client.report.get_summary() + {u'summary': [{u'begin': u'2018-03-01T00:00:00', + u'end': u'2018-04-01T00:00:00', + u'rate': u'1672.71269', + u'res_type': u'ALL', + u'tenant_id': u'bea6a24f77e946b0a92dca7c78b7870b'}]} + +Else, use it the same way as any other OpenStack client:: + + >>> import os + + >>> from keystoneauth1 import session + >>> from keystoneauth1.identity import v3 + + >>> from cloudkittyclient import client as ck_client + + >>> auth = v3.Password( + auth_url=os.environ.get('OS_AUTH_URL'), + project_domain_id=os.environ.get('OS_PROJECT_DOMAIN_ID'), + user_domain_id=os.environ.get('OS_USER_DOMAIN_ID'), + username=os.environ.get('OS_USERNAME'), + project_name=os.environ.get('OS_PROJECT_NAME'), + password=os.environ.get('OS_PASSWORD')) + + >>> ck_session = session.Session(auth=auth) + + >>> c = ck_client.Client('1', session=ck_session) + + >>> c.report.get_summary() + {u'summary': [{u'begin': u'2018-03-01T00:00:00', + u'end': u'2018-04-01T00:00:00', + u'rate': u'1672.71269', + u'res_type': u'ALL', + u'tenant_id': u'bea6a24f77e946b0a92dca7c78b7870b'}]} + +When using the ``cloudkitty`` CLI client with keystone authentication, the +auth plugin to use should automagically be detected. If not, you can specify +the auth plugin to use with ``--os-auth-type/--os-auth-plugin``:: + + $ cloudkitty --debug --os-auth-type cloudkitty-noauth summary get + +------------+---------------+------------+---------------------+---------------------+ + | Project ID | Resource Type | Rate | Begin Time | End Time | + +------------+---------------+------------+---------------------+---------------------+ + | ALL | ALL | 1676.95499 | 2018-03-01T00:00:00 | 2018-04-01T00:00:00 | + +------------+---------------+------------+---------------------+---------------------+ + + +CSV report generation +===================== + +An output formatter (``DataframeToCsvFormatter``) has been created in order to +allow CSV report generation through the client. It can be used with the +``-f df-to-csv`` option. + +.. code:: shell + + $ cloudkitty dataframes get -b 2018-03-22T12:00:00 -f df-to-csv + Begin,End,Metric Type,Qty,Cost,Project ID,Resource ID,User ID + 2018-03-01T12:00:00,2018-03-01T13:00:00,compute,1,2.0,53c3fe396a1a4ab0914b9aa997a5ff88,382d23c3-7b77-4e32-8d65-b3baf86ed7bb,38c1949c2e624f729b30e034ac787640 + [...] + + +.. warning:: The ``df-to-csv`` formatter should NEVER be used together with the + ``-c/--column`` option and should only be used for the ``dataframes get`` + command. + +The example above shows how to get a CSV report with the standard columns. If +you want other columns, it is possible to customize the formatter through a +configuration file: + +.. literalinclude:: ../../etc/cloudkitty/csv_config.yml + +Example with this config file:: + + $ cloudkitty dataframes get -f df-to-csv --format-config-file /etc/cloudkitty/csv_config.yml > report.csv + $ head -n 2 report.csv + Begin,End,User ID,Resource ID,Qty,Cost + 2018-03-01T12:00:00,2018-03-01T13:00:00,38c1949c2e624f729b30e034ac787640,382d23c3-7b77-4e32-8d65-b3baf86ed7bb,1,2.0 + +An other config file is provided: ``legacy_csv_config.yml``. This file is +compatible with the format of ``cloudkitty-writer``'s CSV reports. diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst deleted file mode 100644 index a8f2ca9..0000000 --- a/doc/source/user/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -======== -Usage -======== - -To use python-cloudkittyclient in a project:: - - import cloudkittyclient diff --git a/etc/cloudkitty/csv_config.yml b/etc/cloudkitty/csv_config.yml new file mode 100644 index 0000000..9de9400 --- /dev/null +++ b/etc/cloudkitty/csv_config.yml @@ -0,0 +1,9 @@ +# This exact file format must be respected (- column_name: json_path) +# The path is interpreted using jsonpath-rw-ext, see +# https://github.com/sileht/python-jsonpath-rw-ext for syntax reference +- 'Begin': '$.begin' +- 'End': '$.end' +- 'User ID': '$.desc.user_id' +- 'Resource ID': '$.desc.resource_id' +- 'Qty': '$.volume' +- 'Cost': '$.rating' diff --git a/etc/cloudkitty/legacy_csv_config.yml b/etc/cloudkitty/legacy_csv_config.yml new file mode 100644 index 0000000..f62b15b --- /dev/null +++ b/etc/cloudkitty/legacy_csv_config.yml @@ -0,0 +1,17 @@ +# This exact file format must be respected (- column_name: json_path) +# The path is interpreted using jsonpath-rw-ext, see +# https://github.com/sileht/python-jsonpath-rw-ext for syntax reference +- 'UsageStart': '$.begin' +- 'UsageEnd': '$.end' +- 'ResourceId': '$.desc.resource_id' +- 'Operation': '$.operation' +- 'UserId': '$.desc.user_id' +- 'ProjectId': '$.desc.project_id' +- 'ItemName': '$.desc.name' +- 'ItemFlavor': '$.desc.flavor_name' +- 'ItemFlavorId': '$.desc.flavor_id' +- 'AvailabilityZone': '$.desc.availability_zone' +- 'Service': '$.service' +- 'UsageQuantity': '$.volume' +- 'RateValue': '$.rate_value' +- 'Cost': '$.rating' diff --git a/playbooks/cloudkittyclient-devstack-functional/post.yaml b/playbooks/cloudkittyclient-devstack-functional/post.yaml new file mode 100644 index 0000000..7f0cb19 --- /dev/null +++ b/playbooks/cloudkittyclient-devstack-functional/post.yaml @@ -0,0 +1,4 @@ +- hosts: all + roles: + - fetch-tox-output + - fetch-subunit-output diff --git a/playbooks/cloudkittyclient-devstack-functional/pre.yaml b/playbooks/cloudkittyclient-devstack-functional/pre.yaml new file mode 100644 index 0000000..f5a5723 --- /dev/null +++ b/playbooks/cloudkittyclient-devstack-functional/pre.yaml @@ -0,0 +1,5 @@ +- hosts: all + roles: + - run-devstack + - test-setup + - ensure-tox diff --git a/playbooks/cloudkittyclient-devstack-functional/run.yaml b/playbooks/cloudkittyclient-devstack-functional/run.yaml new file mode 100644 index 0000000..d1101dd --- /dev/null +++ b/playbooks/cloudkittyclient-devstack-functional/run.yaml @@ -0,0 +1,5 @@ +- hosts: all + environment: + OS_CLOUD: devstack-admin + roles: + - tox diff --git a/releasenotes/notes/rewrite-client-5e99a6d3c7302630.yaml b/releasenotes/notes/rewrite-client-5e99a6d3c7302630.yaml new file mode 100644 index 0000000..4210adb --- /dev/null +++ b/releasenotes/notes/rewrite-client-5e99a6d3c7302630.yaml @@ -0,0 +1,34 @@ +--- +prelude: > + python-cloudkittyclient has been completely rewritten in order to be easier + to maintain. It is now built with cliff. +features: + - | + * Client-side CSV report generation: It is possible for users to generate + CSV reports with the new client. There is a default format, but reports + may also be configured through a yaml config file. (see documentation) + + * The documentation has been improved. (A few examples on how to use the + python library + complete API bindings and CLI reference). + + * It is now possible to use the client without Keystone authentication + (this requires that CK's API is configured to use the noauth auth + strategy). + + * Various features are brought by cliff: completion, command output + formatting (table, shell, yaml, json...). + + * The 'python-cloudkittyclient' module is now compatible with python 2.7 and + 3.5 . + + * Integration tests (for 'openstack rating' and 'cloudkitty') have been + added. These should allow to create gate jobs running against a + CK devstack. + + * Tests are now ran with stestr instead of testr, which allows a better + control over execution. + + * The dependency list has been reduced and upper constraints have been set. + + * Simplification of commands. Most commands are now more http-like: no more + 'list' and 'get' commands, but only 'get' with or without a resource ID. diff --git a/requirements.txt b/requirements.txt index 11095e0..22627a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,11 +3,11 @@ # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -Babel!=2.4.0,>=2.3.4 # BSD -python-keystoneclient>=3.8.0 # Apache-2.0 -python-openstackclient!=3.10.0,>=3.3.0 # Apache-2.0 -stevedore>=1.20.0 # Apache-2.0 -oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 -oslo.serialization>=1.10.0 # Apache-2.0 -oslo.utils>=3.20.0 # Apache-2.0 -PrettyTable<0.8,>=0.7.1 # BSD +cliff>=2.11.0,<3.0 # Apache-2.0 +keystoneauth1>=3.4.0,<4.0 # Apache-2.0 +oslo.utils>=3.35,<4.0 # Apache-2.0 +oslo.log>=3.36,<4.0 # Apache-2.0 +PyYAML>=3.12,<4.0 # MIT +jsonpath-rw-ext>=1.0 # Apache-2.0 +six>=1.11,<2.0 # MIT +os-client-config>=1.29.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index dea458c..4f28c25 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,62 +26,126 @@ packages = console_scripts = cloudkitty = cloudkittyclient.shell:main -cloudkitty.client.modules = - hashmap = cloudkittyclient.v1.rating.hashmap.extension:Extension - pyscripts = cloudkittyclient.v1.rating.pyscripts.extension:Extension - openstack.cli.extension = rating = cloudkittyclient.osc openstack.rating.v1 = - rating_module-list = cloudkittyclient.v1.shell_cli:CliModuleList - rating_module-enable = cloudkittyclient.v1.shell_cli:CliModuleEnable - rating_module-disable = cloudkittyclient.v1.shell_cli:CliModuleDisable - rating_module-set-priority = cloudkittyclient.v1.shell_cli:CliModuleSetPriority + rating_total_get = cloudkittyclient.v1.report_cli:CliTotalGet + rating_summary_get = cloudkittyclient.v1.report_cli:CliSummaryGet + rating_report_tenant_list = cloudkittyclient.v1.report_cli:CliTenantList - rating_info-config-get = cloudkittyclient.v1.shell_cli:CliInfoGetConfig - rating_info-service-get = cloudkittyclient.v1.shell_cli:CliInfoGetService + rating_module_get = cloudkittyclient.v1.rating:CliModuleGet + rating_module_list = cloudkittyclient.v1.rating:CliModuleList + rating_module_enable = cloudkittyclient.v1.rating:CliModuleEnable + rating_module_disable = cloudkittyclient.v1.rating:CliModuleDisable + rating_module_set_priority = cloudkittyclient.v1.rating:CliModuleSetPriority - rating_total-get = cloudkittyclient.v1.report.shell_cli:CliTotalGet - rating_summary-get = cloudkittyclient.v1.report.shell_cli:CliSummaryGet - rating_report-tenant-list = cloudkittyclient.v1.report.shell_cli:CliReportTenantList + rating_info_config_get = cloudkittyclient.v1.info_cli:CliInfoConfigGet + rating_info_metric_get = cloudkittyclient.v1.info_cli:CliInfoMetricGet + rating_info_metric_list = cloudkittyclient.v1.info_cli:CliInfoMetricList - rating_collector-mapping-list = cloudkittyclient.v1.collector.shell_cli:CliCollectorMappingList - rating_collector-mapping-get = cloudkittyclient.v1.collector.shell_cli:CliCollectorMappingGet - rating_collector-mapping-create = cloudkittyclient.v1.collector.shell_cli:CliCollectorMappingCreate - rating_collector-mapping-delete = cloudkittyclient.v1.collector.shell_cli:CliCollectorMappingDelete - rating_collector-state-get = cloudkittyclient.v1.collector.shell_cli:CliCollectorStateGet - rating_collector-state-enable = cloudkittyclient.v1.collector.shell_cli:CliCollectorStateEnable - rating_collector-state-disable = cloudkittyclient.v1.collector.shell_cli:CliCollectorStateDisable + rating_hashmap_mapping-types_list = cloudkittyclient.v1.rating.hashmap_cli:CliGetMappingTypes + rating_hashmap_service_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetService + rating_hashmap_service_list = cloudkittyclient.v1.rating.hashmap_cli:CliListService + rating_hashmap_service_create = cloudkittyclient.v1.rating.hashmap_cli:CliCreateService + rating_hashmap_service_delete = cloudkittyclient.v1.rating.hashmap_cli:CliDeleteService + rating_hashmap_field_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetField + rating_hashmap_field_list = cloudkittyclient.v1.rating.hashmap_cli:CliListField + rating_hashmap_field_create = cloudkittyclient.v1.rating.hashmap_cli:CliCreateField + rating_hashmap_field_delete = cloudkittyclient.v1.rating.hashmap_cli:CliDeleteField + rating_hashmap_mapping_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetMapping + rating_hashmap_mapping_list = cloudkittyclient.v1.rating.hashmap_cli:CliListMapping + rating_hashmap_mapping_create = cloudkittyclient.v1.rating.hashmap_cli:CliCreateMapping + rating_hashmap_mapping_delete = cloudkittyclient.v1.rating.hashmap_cli:CliDeleteMapping + rating_hashmap_mapping_update = cloudkittyclient.v1.rating.hashmap_cli:CliUpdateMapping + rating_hashmap_group_list = cloudkittyclient.v1.rating.hashmap_cli:CliListGroup + rating_hashmap_group_create = cloudkittyclient.v1.rating.hashmap_cli:CliCreateGroup + rating_hashmap_group_delete = cloudkittyclient.v1.rating.hashmap_cli:CliDeleteGroup + rating_hashmap_group_mappings_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetGroupMappings + rating_hashmap_group_thresholds_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetGroupThresholds + rating_hashmap_threshold_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetThreshold + rating_hashmap_threshold_list = cloudkittyclient.v1.rating.hashmap_cli:CliListThreshold + rating_hashmap_threshold_create = cloudkittyclient.v1.rating.hashmap_cli:CliCreateThreshold + rating_hashmap_threshold_delete = cloudkittyclient.v1.rating.hashmap_cli:CliDeleteThreshold + rating_hashmap_threshold_update = cloudkittyclient.v1.rating.hashmap_cli:CliUpdateThreshold - rating_storage-dataframe-list = cloudkittyclient.v1.storage.shell_cli:CliStorageDataframeList + rating_collector-mapping_get = cloudkittyclient.v1.collector_cli:CliCollectorMappingGet + rating_collector-mapping_list = cloudkittyclient.v1.collector_cli:CliCollectorMappingList + rating_collector-mapping_create = cloudkittyclient.v1.collector_cli:CliCollectorMappingCreate + rating_collector-mapping_delete = cloudkittyclient.v1.collector_cli:CliCollectorMappingDelete + rating_collector_state_get = cloudkittyclient.v1.collector_cli:CliCollectorGetState + rating_collector_enable = cloudkittyclient.v1.collector_cli:CliCollectorEnable + rating_collector_disable = cloudkittyclient.v1.collector_cli:CliCollectorDisable + rating_dataframes_get = cloudkittyclient.v1.storage_cli:CliGetDataframes - rating_hashmap-service-create = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapServiceCreate - rating_hashmap-service-list = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapServiceList - rating_hashmap-service-delete = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapServiceDelete - rating_hashmap-field-create = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapFieldCreate - rating_hashmap-field-list = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapFieldList - rating_hashmap-field-delete = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapFieldDelete - rating_hashmap-mapping-create = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapMappingCreate - rating_hashmap-mapping-update = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapMappingUpdate - rating_hashmap-mapping-list = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapMappingList - rating_hashmap-mapping-delete = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapMappingDelete - rating_hashmap-group-create = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapGroupCreate - rating_hashmap-group-list = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapGroupList - rating_hashmap-group-delete = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapGroupDelete - rating_hashmap-threshold-create = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapThresholdCreate - rating_hashmap-threshold-update = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapThresholdUpdate - rating_hashmap-threshold-list = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapThresholdList - rating_hashmap-threshold-delete = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapThresholdDelete - rating_hashmap-threshold-get = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapThresholdGet - rating_hashmap-threshold-group = cloudkittyclient.v1.rating.hashmap.shell_cli:CliHashmapThresholdGroup + rating_pyscript_create = cloudkittyclient.v1.rating.pyscripts_cli:CliCreateScript + rating_pyscript_list = cloudkittyclient.v1.rating.pyscripts_cli:CliListScripts + rating_pyscript_get = cloudkittyclient.v1.rating.pyscripts_cli:CliGetScript + rating_pyscript_update = cloudkittyclient.v1.rating.pyscripts_cli:CliUpdateScript + rating_pyscript_delete = cloudkittyclient.v1.rating.pyscripts_cli:CliDeleteScript - rating_pyscripts-script-create = cloudkittyclient.v1.rating.pyscripts.shell_cli:CliPyScriptCreate - rating_pyscripts-script-list = cloudkittyclient.v1.rating.pyscripts.shell_cli:CliPyScriptList - rating_pyscripts-script-get = cloudkittyclient.v1.rating.pyscripts.shell_cli:CliPyScriptGet - rating_pyscripts-script-get-data = cloudkittyclient.v1.rating.pyscripts.shell_cli:CliPyScriptGetData - rating_pyscripts-script-delete = cloudkittyclient.v1.rating.pyscripts.shell_cli:CliPyScriptDelete - rating_pyscripts-script-update = cloudkittyclient.v1.rating.pyscripts.shell_cli:CliPyScriptUpdate + +cloudkittyclient = + total_get = cloudkittyclient.v1.report_cli:CliTotalGet + summary_get = cloudkittyclient.v1.report_cli:CliSummaryGet + report_tenant_list = cloudkittyclient.v1.report_cli:CliTenantList + + module_get = cloudkittyclient.v1.rating:CliModuleGet + module_list = cloudkittyclient.v1.rating:CliModuleList + module_enable = cloudkittyclient.v1.rating:CliModuleEnable + module_disable = cloudkittyclient.v1.rating:CliModuleDisable + module_set_priority = cloudkittyclient.v1.rating:CliModuleSetPriority + + info_config_get = cloudkittyclient.v1.info_cli:CliInfoConfigGet + info_metric_get = cloudkittyclient.v1.info_cli:CliInfoMetricGet + info_metric_list = cloudkittyclient.v1.info_cli:CliInfoMetricList + + hashmap_mapping-types_list = cloudkittyclient.v1.rating.hashmap_cli:CliGetMappingTypes + hashmap_service_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetService + hashmap_service_list = cloudkittyclient.v1.rating.hashmap_cli:CliListService + hashmap_service_create = cloudkittyclient.v1.rating.hashmap_cli:CliCreateService + hashmap_service_delete = cloudkittyclient.v1.rating.hashmap_cli:CliDeleteService + hashmap_field_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetField + hashmap_field_list = cloudkittyclient.v1.rating.hashmap_cli:CliListField + hashmap_field_create = cloudkittyclient.v1.rating.hashmap_cli:CliCreateField + hashmap_field_delete = cloudkittyclient.v1.rating.hashmap_cli:CliDeleteField + hashmap_mapping_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetMapping + hashmap_mapping_list = cloudkittyclient.v1.rating.hashmap_cli:CliListMapping + hashmap_mapping_create = cloudkittyclient.v1.rating.hashmap_cli:CliCreateMapping + hashmap_mapping_delete = cloudkittyclient.v1.rating.hashmap_cli:CliDeleteMapping + hashmap_mapping_update = cloudkittyclient.v1.rating.hashmap_cli:CliUpdateMapping + hashmap_group_list = cloudkittyclient.v1.rating.hashmap_cli:CliListGroup + hashmap_group_create = cloudkittyclient.v1.rating.hashmap_cli:CliCreateGroup + hashmap_group_delete = cloudkittyclient.v1.rating.hashmap_cli:CliDeleteGroup + hashmap_group_mappings_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetGroupMappings + hashmap_group_thresholds_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetGroupThresholds + hashmap_threshold_get = cloudkittyclient.v1.rating.hashmap_cli:CliGetThreshold + hashmap_threshold_list = cloudkittyclient.v1.rating.hashmap_cli:CliListThreshold + hashmap_threshold_create = cloudkittyclient.v1.rating.hashmap_cli:CliCreateThreshold + hashmap_threshold_delete = cloudkittyclient.v1.rating.hashmap_cli:CliDeleteThreshold + hashmap_threshold_update = cloudkittyclient.v1.rating.hashmap_cli:CliUpdateThreshold + + collector-mapping_get = cloudkittyclient.v1.collector_cli:CliCollectorMappingGet + collector-mapping_list = cloudkittyclient.v1.collector_cli:CliCollectorMappingList + + collector-mapping_create = cloudkittyclient.v1.collector_cli:CliCollectorMappingCreate + collector-mapping_delete = cloudkittyclient.v1.collector_cli:CliCollectorMappingDelete + collector_state_get = cloudkittyclient.v1.collector_cli:CliCollectorGetState + collector_enable = cloudkittyclient.v1.collector_cli:CliCollectorEnable + collector_disable = cloudkittyclient.v1.collector_cli:CliCollectorDisable + dataframes_get = cloudkittyclient.v1.storage_cli:CliGetDataframes + + pyscript_create = cloudkittyclient.v1.rating.pyscripts_cli:CliCreateScript + pyscript_list = cloudkittyclient.v1.rating.pyscripts_cli:CliListScripts + pyscript_get = cloudkittyclient.v1.rating.pyscripts_cli:CliGetScript + pyscript_update = cloudkittyclient.v1.rating.pyscripts_cli:CliUpdateScript + pyscript_delete = cloudkittyclient.v1.rating.pyscripts_cli:CliDeleteScript + +keystoneauth1.plugin = + cloudkitty-noauth = cloudkittyclient.auth:CloudKittyNoAuthLoader + +cliff.formatter.list = + df-to-csv = cloudkittyclient.format:DataframeToCsvFormatter [build_sphinx] source-dir = doc/source diff --git a/test-requirements.txt b/test-requirements.txt index 2a0e629..cce781e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ python-subunit>=0.0.18 # Apache-2.0/BSD sphinx>=1.6.2 # BSD openstackdocstheme>=1.11.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 -testrepository>=0.0.18 # Apache-2.0/BSD -testscenarios>=0.4 # Apache-2.0/BSD -testtools>=1.4.0 # MIT reno>=1.8.0 # Apache2 +stestr>=2.0 # Apache-2.0 +mock>=2.0 # BSD +python-openstackclient>=3.14 # Apache-2.0 diff --git a/tox.ini b/tox.ini index 8144a53..3eee539 100644 --- a/tox.ini +++ b/tox.ini @@ -10,20 +10,21 @@ setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt -commands = python setup.py testr --slowest --testr-args='{posargs}' +commands = stestr run {posargs} [testenv:debug] commands = oslo_debug_helper {posargs} +[testenv:functional] +passenv = OS_CLOUD OS_PROJECT_DOMAIN_ID OS_USER_DOMAIN_ID OS_PROJECT_DOMAIN_NAME OS_USER_DOMAIN_NAME OS_PROJECT_NAME OS_IDENTITY_API_VERSION OS_PASSWORD OS_AUTH_TYPE OS_AUTH_URL OS_USERNAME +commands = stestr run --concurrency=1 --test-path ./cloudkittyclient/tests/functional + [testenv:pep8] commands = flake8 [testenv:venv] commands = {posargs} -[testenv:cover] -commands = python setup.py testr --coverage --testr-args='{posargs}' - [testenv:docs] commands = python setup.py build_sphinx