Integrate client with osc-lib

osc-lib library is foundation on which a CLI client
for openstack can be built. It is meant to facilitate several
aspects, that were previously hard-coded in client:

* keystone communication handling
* supporting multiple authentication methods (not only password)
* common authentication parameters (i.e. environmental OS_*)
* communicating over http with service endpoint
* interactive CLI mode

Thanks to those items, it was possible not only to drop
nearly 3k lines of code and replace them with osc-lib but also
increase reliabity of the client in terms of new openstack releases.
Also it allowed to greatly simpify existing set of unit-tests.
They are now testing only actual logic instead of mocking
entire process of calling shell (i.e. MonascaShell.run(args)) or
mocking HTTP communication. Both items are handled by osc-lib thus
not they are not subject of monascaclient unit tests layers.

Note:
This change is partial integration with osc-lib and its main
purpose is to move the responsibility of:

* keystone communication
* rest-ful communication with service endpoint

to underlying library thus allowing client to implement only
necessary functionality and not supporting boilerplate code,
mentioned above.

Story: 2000995
Task: 4172

Change-Id: I1712a24739438e2d8331a495f18f357749a633c5
This commit is contained in:
Tomasz Trębski 2017-04-28 22:21:01 +02:00
parent c46b781405
commit 94c5223f02
32 changed files with 1047 additions and 2933 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
.coverage
.coverage*
.venv
cover
*.pyc

View File

@ -1,18 +1,70 @@
# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import pbr.version
"""
__version__ = pbr.version.VersionInfo('python-monascaclient').version_string()
Patches method that transforms error responses.
That is required to handle different format monasca follows.
"""
from keystoneauth1 import exceptions as exc
from keystoneauth1.exceptions import http
def mon_exc_from_response(response, method, url):
req_id = response.headers.get('x-openstack-request-id')
kwargs = {
'http_status': response.status_code,
'response': response,
'method': method,
'url': url,
'request_id': req_id,
}
if 'retry-after' in response.headers:
kwargs['retry_after'] = response.headers['retry-after']
content_type = response.headers.get('Content-Type', '')
if content_type.startswith('application/json'):
try:
body = response.json()
except ValueError:
pass
else:
if isinstance(body, dict):
if isinstance(body.get('error'), dict):
error = body['error']
kwargs['message'] = error.get('message')
kwargs['details'] = error.get('details')
elif {'description', 'title'} <= set(body):
# monasca-api error response structure
kwargs['message'] = body.get('title')
kwargs['details'] = body.get('description')
elif content_type.startswith('text/'):
kwargs['details'] = response.text
try:
cls = http._code_map[response.status_code]
except KeyError:
if 500 <= response.status_code < 600:
cls = exc.HttpServerError
elif 400 <= response.status_code < 500:
cls = exc.HTTPClientError
else:
cls = exc.HttpError
return cls(**kwargs)
exc.from_response = mon_exc_from_response

View File

@ -1,496 +0,0 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2012 Grid Dynamics
# Copyright 2013 OpenStack Foundation
# (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Base utilities to build API operation managers and objects on top of.
"""
# E1102: %s is not callable
# pylint: disable=E1102
import abc
import copy
from oslo_utils import strutils
import six
from six.moves.urllib import parse
from monascaclient.apiclient import exceptions
def getid(obj):
"""Return id if argument is a Resource.
Abstracts the common pattern of allowing both an object or an object's ID
(UUID) as a parameter when dealing with relationships.
"""
try:
if obj.uuid:
return obj.uuid
except AttributeError:
pass
try:
return obj.id
except AttributeError:
return obj
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
class HookableMixin(object):
"""Mixin so classes can register and run hooks."""
_hooks_map = {}
@classmethod
def add_hook(cls, hook_type, hook_func):
"""Add a new hook of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param hook_func: hook function
"""
if hook_type not in cls._hooks_map:
cls._hooks_map[hook_type] = []
cls._hooks_map[hook_type].append(hook_func)
@classmethod
def run_hooks(cls, hook_type, *args, **kwargs):
"""Run all hooks of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param **args: args to be passed to every hook function
:param **kwargs: kwargs to be passed to every hook function
"""
hook_funcs = cls._hooks_map.get(hook_type) or []
for hook_func in hook_funcs:
hook_func(*args, **kwargs)
class BaseManager(HookableMixin):
"""Basic manager type providing common operations.
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, client):
"""Initializes BaseManager with `client`.
:param client: instance of BaseClient descendant for HTTP requests
"""
super(BaseManager, self).__init__()
self.client = client
def _list(self, url, response_key, obj_class=None, json=None):
"""List the collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
"""
if json:
body = self.client.post(url, json=json).json()
else:
body = self.client.get(url).json()
if obj_class is None:
obj_class = self.resource_class
data = body[response_key]
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
try:
data = data['values']
except (KeyError, TypeError):
pass
return [obj_class(self, res, loaded=True) for res in data if res]
def _get(self, url, response_key):
"""Get an object from collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'
"""
body = self.client.get(url).json()
return self.resource_class(self, body[response_key], loaded=True)
def _head(self, url):
"""Retrieve request headers for an object.
:param url: a partial URL, e.g., '/servers'
"""
resp = self.client.head(url)
return resp.status_code == 204
def _post(self, url, json, response_key, return_raw=False):
"""Create an object.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
body = self.client.post(url, json=json).json()
if return_raw:
return body[response_key]
return self.resource_class(self, body[response_key])
def _put(self, url, json=None, response_key=None):
"""Update an object with PUT method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
"""
resp = self.client.put(url, json=json)
# PUT requests may not return a body
if resp.content:
body = resp.json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _patch(self, url, json=None, response_key=None):
"""Update an object with PATCH method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'
"""
body = self.client.patch(url, json=json).json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _delete(self, url):
"""Delete an object.
:param url: a partial URL, e.g., '/servers/my-server'
"""
return self.client.delete(url)
@six.add_metaclass(abc.ABCMeta)
class ManagerWithFind(BaseManager):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(msg)
elif num_matches > 1:
raise exceptions.NoUniqueMatch()
else:
return matches[0]
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class CrudManager(BaseManager):
"""Base manager class for manipulating entities.
Children of this class are expected to define a `collection_key` and `key`.
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
objects containing a list of member resources (e.g. `{'entities': [{},
{}, {}]}`).
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
refer to an individual member of the collection.
"""
collection_key = None
key = None
def build_url(self, base_url=None, **kwargs):
"""Builds a resource URL for the given kwargs.
Given an example collection where `collection_key = 'entities'` and
`key = 'entity'`, the following URL's could be generated.
By default, the URL will represent a collection of entities, e.g.::
/entities
If kwargs contains an `entity_id`, then the URL will represent a
specific member, e.g.::
/entities/{entity_id}
:param base_url: if provided, the generated URL will be appended to it
"""
url = base_url if base_url is not None else ''
url += '/%s' % self.collection_key
# do we have a specific entity?
entity_id = kwargs.get('%s_id' % self.key)
if entity_id is not None:
url += '/%s' % entity_id
return url
def _filter_kwargs(self, kwargs):
"""Drop null values and handle ids."""
for key, ref in six.iteritems(kwargs.copy()):
if ref is None:
kwargs.pop(key)
else:
if isinstance(ref, Resource):
kwargs.pop(key)
kwargs['%s_id' % key] = getid(ref)
return kwargs
def create(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs),
{self.key: kwargs},
self.key)
def get(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._get(
self.build_url(**kwargs),
self.key)
def head(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._head(self.build_url(**kwargs))
def list(self, base_url=None, **kwargs):
"""List the collection.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
def put(self, base_url=None, **kwargs):
"""Update an element.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._put(self.build_url(base_url=base_url, **kwargs))
def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
params.pop('%s_id' % self.key)
return self._patch(
self.build_url(**kwargs),
{self.key: params},
self.key)
def delete(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._delete(
self.build_url(**kwargs))
def find(self, base_url=None, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
rl = self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
num = len(rl)
if num == 0:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(404, msg)
elif num > 1:
raise exceptions.NoUniqueMatch
else:
return rl[0]
class Extension(HookableMixin):
"""Extension descriptor."""
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
manager_class = None
def __init__(self, name, module):
super(Extension, self).__init__()
self.name = name
self.module = module
self._parse_extension_module()
def _parse_extension_module(self):
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
if attr_name in self.SUPPORTED_HOOKS:
self.add_hook(attr_name, attr_value)
else:
try:
if issubclass(attr_value, BaseManager):
self.manager_class = attr_value
except TypeError:
pass
def __repr__(self):
return "<Extension '%s'>" % self.name
class Resource(object):
"""Base class for OpenStack resources (tenant, user, etc.).
This is pretty much just a bag for attributes.
"""
HUMAN_ID = False
NAME_ATTR = 'name'
def __init__(self, manager, info, loaded=False):
"""Populate and bind to a manager.
:param manager: BaseManager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
def __repr__(self):
reprkeys = sorted(k
for k in self.__dict__.keys()
if k[0] != '_' and k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
@property
def human_id(self):
"""Human-readable ID which can be used for bash completion."""
if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID:
return strutils.to_slug(getattr(self, self.NAME_ATTR))
return None
def _add_details(self, info):
for (k, v) in six.iteritems(info):
try:
setattr(self, k, v)
self._info[k] = v
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __getattr__(self, k):
if k not in self.__dict__:
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded:
self._get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def _get(self):
# set _loaded first ... so if we have to bail, we know we tried.
self._loaded = True
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, Resource):
return NotImplemented
# two resources of different types are not equal
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info
def __ne__(self, other):
return not self.__eq__(other)
@property
def is_loaded(self):
return self._loaded
def to_dict(self):
return copy.deepcopy(self._info)

View File

@ -1,438 +0,0 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Nebula, Inc.
# Copyright 2013 Alessio Ababilov
# Copyright 2013 OpenStack Foundation
# (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Exception definitions.
"""
import inspect
import sys
import six
class ClientException(Exception):
"""The base exception class for all exceptions this library raises."""
pass
class MissingArgs(ClientException):
"""Supplied arguments are not sufficient for calling a function."""
def __init__(self, missing):
self.missing = missing
msg = "Missing argument(s): %s" % ", ".join(missing)
super(MissingArgs, self).__init__(msg)
class ValidationError(ClientException):
"""Error in validation on API client side."""
pass
class UnsupportedVersion(ClientException):
"""User is trying to use an unsupported version of the API."""
pass
class CommandError(ClientException):
"""Error in CLI tool."""
pass
class AuthorizationFailure(ClientException):
"""Cannot authorize API client."""
pass
class AuthPluginOptionsMissing(AuthorizationFailure):
"""Auth plugin misses some options."""
def __init__(self, opt_names):
super(AuthPluginOptionsMissing, self).__init__(
"Authentication failed. Missing options: %s" %
", ".join(opt_names))
self.opt_names = opt_names
class AuthSystemNotFound(AuthorizationFailure):
"""User has specified a AuthSystem that is not installed."""
def __init__(self, auth_system):
super(AuthSystemNotFound, self).__init__(
"AuthSystemNotFound: %s" % repr(auth_system))
self.auth_system = auth_system
class NoUniqueMatch(ClientException):
"""Multiple entities found instead of one."""
pass
class EndpointException(ClientException):
"""Something is rotten in Service Catalog."""
pass
class EndpointNotFound(EndpointException):
"""Could not find requested endpoint in Service Catalog."""
pass
class AmbiguousEndpoints(EndpointException):
"""Found more than one matching endpoint in Service Catalog."""
def __init__(self, endpoints=None):
super(AmbiguousEndpoints, self).__init__(
"AmbiguousEndpoints: %s" % repr(endpoints))
self.endpoints = endpoints
class HttpError(ClientException):
"""The base exception class for all HTTP exceptions."""
http_status = 0
message = "HTTP Error"
def __init__(self, message=None, details=None,
response=None, request_id=None,
url=None, method=None, http_status=None):
self.http_status = http_status or self.http_status
self.message = message or self.message
self.details = details
self.request_id = request_id
self.response = response
self.url = url
self.method = method
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
if request_id:
formatted_string += " (Request-ID: %s)" % request_id
super(HttpError, self).__init__(formatted_string)
class HTTPClientError(HttpError):
"""Client-side HTTP error.
Exception for cases in which the client seems to have erred.
"""
message = "HTTP Client Error"
class HttpServerError(HttpError):
"""Server-side HTTP error.
Exception for cases in which the server is aware that it has
erred or is incapable of performing the request.
"""
message = "HTTP Server Error"
class BadRequest(HTTPClientError):
"""HTTP 400 - Bad Request.
The request cannot be fulfilled due to bad syntax.
"""
http_status = 400
message = "Bad Request"
class Unauthorized(HTTPClientError):
"""HTTP 401 - Unauthorized.
Similar to 403 Forbidden, but specifically for use when authentication
is required and has failed or has not yet been provided.
"""
http_status = 401
message = "Unauthorized"
class PaymentRequired(HTTPClientError):
"""HTTP 402 - Payment Required.
Reserved for future use.
"""
http_status = 402
message = "Payment Required"
class Forbidden(HTTPClientError):
"""HTTP 403 - Forbidden.
The request was a valid request, but the server is refusing to respond
to it.
"""
http_status = 403
message = "Forbidden"
class NotFound(HTTPClientError):
"""HTTP 404 - Not Found.
The requested resource could not be found but may be available again
in the future.
"""
http_status = 404
message = "Not Found"
class MethodNotAllowed(HTTPClientError):
"""HTTP 405 - Method Not Allowed.
A request was made of a resource using a request method not supported
by that resource.
"""
http_status = 405
message = "Method Not Allowed"
class NotAcceptable(HTTPClientError):
"""HTTP 406 - Not Acceptable.
The requested resource is only capable of generating content not
acceptable according to the Accept headers sent in the request.
"""
http_status = 406
message = "Not Acceptable"
class ProxyAuthenticationRequired(HTTPClientError):
"""HTTP 407 - Proxy Authentication Required.
The client must first authenticate itself with the proxy.
"""
http_status = 407
message = "Proxy Authentication Required"
class RequestTimeout(HTTPClientError):
"""HTTP 408 - Request Timeout.
The server timed out waiting for the request.
"""
http_status = 408
message = "Request Timeout"
class Conflict(HTTPClientError):
"""HTTP 409 - Conflict.
Indicates that the request could not be processed because of conflict
in the request, such as an edit conflict.
"""
http_status = 409
message = "Conflict"
class Gone(HTTPClientError):
"""HTTP 410 - Gone.
Indicates that the resource requested is no longer available and will
not be available again.
"""
http_status = 410
message = "Gone"
class LengthRequired(HTTPClientError):
"""HTTP 411 - Length Required.
The request did not specify the length of its content, which is
required by the requested resource.
"""
http_status = 411
message = "Length Required"
class PreconditionFailed(HTTPClientError):
"""HTTP 412 - Precondition Failed.
The server does not meet one of the preconditions that the requester
put on the request.
"""
http_status = 412
message = "Precondition Failed"
class RequestEntityTooLarge(HTTPClientError):
"""HTTP 413 - Request Entity Too Large.
The request is larger than the server is willing or able to process.
"""
http_status = 413
message = "Request Entity Too Large"
def __init__(self, *args, **kwargs):
try:
self.retry_after = int(kwargs.pop('retry_after'))
except (KeyError, ValueError):
self.retry_after = 0
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
class RequestUriTooLong(HTTPClientError):
"""HTTP 414 - Request-URI Too Long.
The URI provided was too long for the server to process.
"""
http_status = 414
message = "Request-URI Too Long"
class UnsupportedMediaType(HTTPClientError):
"""HTTP 415 - Unsupported Media Type.
The request entity has a media type which the server or resource does
not support.
"""
http_status = 415
message = "Unsupported Media Type"
class RequestedRangeNotSatisfiable(HTTPClientError):
"""HTTP 416 - Requested Range Not Satisfiable.
The client has asked for a portion of the file, but the server cannot
supply that portion.
"""
http_status = 416
message = "Requested Range Not Satisfiable"
class ExpectationFailed(HTTPClientError):
"""HTTP 417 - Expectation Failed.
The server cannot meet the requirements of the Expect request-header field.
"""
http_status = 417
message = "Expectation Failed"
class UnprocessableEntity(HTTPClientError):
"""HTTP 422 - Unprocessable Entity.
The request was well-formed but was unable to be followed due to semantic
errors.
"""
http_status = 422
message = "Unprocessable Entity"
class InternalServerError(HttpServerError):
"""HTTP 500 - Internal Server Error.
A generic error message, given when no more specific message is suitable.
"""
http_status = 500
message = "Internal Server Error"
# NotImplemented is a python keyword.
class HttpNotImplemented(HttpServerError):
"""HTTP 501 - Not Implemented.
The server either does not recognize the request method, or it lacks
the ability to fulfill the request.
"""
http_status = 501
message = "Not Implemented"
class BadGateway(HttpServerError):
"""HTTP 502 - Bad Gateway.
The server was acting as a gateway or proxy and received an invalid
response from the upstream server.
"""
http_status = 502
message = "Bad Gateway"
class ServiceUnavailable(HttpServerError):
"""HTTP 503 - Service Unavailable.
The server is currently unavailable.
"""
http_status = 503
message = "Service Unavailable"
class GatewayTimeout(HttpServerError):
"""HTTP 504 - Gateway Timeout.
The server was acting as a gateway or proxy and did not receive a timely
response from the upstream server.
"""
http_status = 504
message = "Gateway Timeout"
class HttpVersionNotSupported(HttpServerError):
"""HTTP 505 - HttpVersion Not Supported.
The server does not support the HTTP protocol version used in the request.
"""
http_status = 505
message = "HTTP Version Not Supported"
# _code_map contains all the classes that have http_status attribute.
_code_map = dict(
(getattr(obj, 'http_status', None), obj)
for name, obj in six.iteritems(vars(sys.modules[__name__]))
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
)
def from_response(response, method, url):
"""Returns an instance of :class:`HttpError` or subclass based on response.
:param response: instance of `requests.Response` class
:param method: HTTP method used for request
:param url: URL used for request
"""
kwargs = {
"http_status": response.status_code,
"response": response,
"method": method,
"url": url,
"request_id": response.headers.get("x-compute-request-id"),
}
if "retry-after" in response.headers:
kwargs["retry_after"] = response.headers["retry-after"]
content_type = response.headers.get("Content-Type", "")
if content_type.startswith("application/json"):
try:
body = response.json()
except ValueError:
pass
else:
if hasattr(body, "keys"):
error = body[body.keys()[0]]
kwargs["message"] = error.get("message")
kwargs["details"] = error.get("details")
elif content_type.startswith("text/"):
kwargs["details"] = response.text
try:
cls = _code_map[response.status_code]
except KeyError:
if 500 <= response.status_code < 600:
cls = HttpServerError
elif 400 <= response.status_code < 500:
cls = HTTPClientError
else:
cls = HttpError
return cls(**kwargs)

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
# Copyright 2017 Fujitsu LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -13,12 +14,63 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from monascaclient.common import utils
from keystoneauth1 import identity
from keystoneauth1 import session
from monascaclient.osc import migration
from monascaclient import version
def Client(version, *args, **kwargs):
module = utils.import_versioned_module(version, 'client')
client_class = getattr(module, 'Client')
if 'use_environment_variables' in kwargs and kwargs['use_environment_variables']:
utils.set_env_variables(kwargs)
return client_class(*args, **kwargs)
def Client(api_version, **kwargs):
auth = _get_auth_handler(kwargs)
sess = _get_session(auth, kwargs)
client = migration.make_client(
api_version=api_version,
session=sess,
endpoint=kwargs.get('endpoint'),
service_type=kwargs.get('service_type', 'monitoring')
)
return client
def _get_session(auth, kwargs):
return session.Session(auth=auth,
app_name='monascaclient',
app_version=version.version_string,
cert=kwargs.get('cert', None),
timeout=kwargs.get('timeout', None),
verify=kwargs.get('verify',
not kwargs.get('insecure',
False)))
def _get_auth_handler(kwargs):
if 'token' in kwargs:
auth = identity.Token(
auth_url=kwargs.get('auth_url', None),
token=kwargs.get('token', None),
project_id=kwargs.get('project_id', None),
project_name=kwargs.get('project_name', None),
project_domain_id=kwargs.get('project_domain_id', None),
project_domain_name=kwargs.get('project_domain_name', None)
)
elif {'username', 'password'} <= set(kwargs):
auth = identity.Password(
auth_url=kwargs.get('auth_url', None),
username=kwargs.get('username', None),
password=kwargs.get('password', None),
project_id=kwargs.get('project_id', None),
project_name=kwargs.get('project_name', None),
project_domain_id=kwargs.get('project_domain_id', None),
project_domain_name=kwargs.get('project_domain_name', None),
user_domain_id=kwargs.get('user_domain_id', None),
user_domain_name=kwargs.get('user_domain_name', None)
)
else:
raise Exception('monascaclient can be configured with either '
'"token" or "username:password" but neither of '
'them was found in passed arguments.')
return auth

View File

@ -1,323 +0,0 @@
# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import logging
import os
import socket
import requests
import six
from six.moves.urllib import parse
from monascaclient import exc
from monascaclient import ksclient
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
LOG = logging.getLogger(__name__)
if not LOG.handlers:
LOG.addHandler(logging.StreamHandler())
USER_AGENT = 'python-monascaclient'
CHUNKSIZE = 1024 * 64 # 64kB
def get_system_ca_file():
"""Return path to system default CA file."""
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
# Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/ca-bundle.pem',
'/etc/ssl/cert.pem',
'/System/Library/OpenSSL/certs/cacert.pem',
requests.certs.where()]
for ca in ca_path:
LOG.debug("Looking for ca file %s", ca)
if os.path.exists(ca):
LOG.debug("Using ca file %s", ca)
return ca
LOG.warning("System ca file could not be found.")
class HTTPClient(object):
def __init__(self, endpoint, write_timeout=None, read_timeout=None, **kwargs):
if endpoint.endswith('/'):
endpoint = endpoint[:-1]
self.endpoint = endpoint
self.write_timeout = write_timeout
self.read_timeout = read_timeout
self.auth_url = kwargs.get('auth_url')
self.auth_token = kwargs.get('token')
self.username = kwargs.get('username')
self.password = kwargs.get('password')
self.user_domain_id = kwargs.get('user_domain_id')
self.user_domain_name = kwargs.get('user_domain_name')
self.region_name = kwargs.get('region_name')
self.endpoint_url = endpoint
# adding for re-authenticate
self.project_name = kwargs.get('project_name')
self.region_name = kwargs.get('region_name')
self.project_id = kwargs.get('project_id')
self.domain_id = kwargs.get('domain_id')
self.domain_name = kwargs.get('domain_name')
self.endpoint_type = kwargs.get('endpoint_type')
self.service_type = kwargs.get('service_type')
self.keystone_timeout = kwargs.get('keystone_timeout')
self.cert_file = kwargs.get('cert_file')
self.key_file = kwargs.get('key_file')
self.ssl_connection_params = {
'os_cacert': kwargs.get('os_cacert'),
'cert_file': kwargs.get('cert_file'),
'key_file': kwargs.get('key_file'),
'insecure': kwargs.get('insecure'),
}
self.verify_cert = None
if parse.urlparse(endpoint).scheme == "https":
if kwargs.get('insecure'):
self.verify_cert = False
else:
self.verify_cert = kwargs.get(
'os_cacert', get_system_ca_file())
def replace_token(self, token):
self.auth_token = token
def re_authenticate(self):
ks_args = {
'username': self.username,
'password': self.password,
'user_domain_id': self.user_domain_id,
'user_domain_name': self.user_domain_name,
'token': '',
'auth_url': self.auth_url,
'service_type': self.service_type,
'endpoint_type': self.endpoint_type,
'os_cacert': self.ssl_connection_params['os_cacert'],
'project_id': self.project_id,
'project_name': self.project_name,
'domain_id': self.domain_id,
'domain_name': self.domain_name,
'insecure': self.ssl_connection_params['insecure'],
'region_name': self.region_name,
'keystone_timeout': self.keystone_timeout
}
try:
_ksclient = ksclient.KSClient(**ks_args)
self.auth_token = _ksclient.token
except Exception as e:
raise exc.KeystoneException(e)
def log_curl_request(self, method, url, kwargs):
curl = ['curl -i -X %s' % method]
for (key, value) in kwargs['headers'].items():
if key in ('X-Auth-Token'):
value = '*****'
header = '-H \'%s: %s\'' % (encodeutils.safe_decode(key),
encodeutils.safe_decode(value))
curl.append(header)
conn_params_fmt = [
('key_file', '--key %s'),
('cert_file', '--cert %s'),
('os_cacert', '--cacert %s'),
]
for (key, fmt) in conn_params_fmt:
value = self.ssl_connection_params.get(key)
if value:
curl.append(fmt % value)
if self.ssl_connection_params.get('insecure'):
curl.append('-k')
if 'data' in kwargs:
curl.append('-d \'%s\'' % kwargs['data'])
curl.append('%s%s' % (self.endpoint, url))
LOG.debug(' '.join(curl))
@staticmethod
def log_http_response(resp):
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
dump = ['\nHTTP/%.1f %s %s' % status]
dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()])
dump.append('')
if resp.content:
content = resp.content
if isinstance(content, six.binary_type):
content = content.decode('utf-8', 'strict')
dump.extend([content, ''])
LOG.debug('\n'.join(dump))
def _http_request(self, url, method, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around requests.request to handle tasks such as
setting headers and error handling.
"""
# Copy the kwargs so we can reuse the original in case of redirects
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
if self.auth_token:
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
self.log_curl_request(method, url, kwargs)
if self.cert_file and self.key_file:
kwargs['cert'] = (self.cert_file, self.key_file)
if self.verify_cert is not None:
kwargs['verify'] = self.verify_cert
# Since requests does not follow the RFC when doing redirection to sent
# back the same method on a redirect we are simply bypassing it. For
# example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says
# that we should follow that URL with the same method as before,
# requests doesn't follow that and send a GET instead for the method.
# Hopefully this could be fixed as they say in a comment in a future
# point version i.e: 3.x
# See issue: https://github.com/kennethreitz/requests/issues/1704
allow_redirects = False
timeout = None
if method in ['POST', 'DELETE', 'PUT', 'PATCH']:
timeout = self.write_timeout
elif method is 'GET':
timeout = self.read_timeout
resp = self._make_request(method, url, allow_redirects, timeout,
**kwargs)
if self._unauthorized(resp):
try:
# re-authenticate and attempt one more request
self.re_authenticate()
kwargs['headers']['X-Auth-Token'] = self.auth_token
resp = self._make_request(method, url, allow_redirects,
timeout, **kwargs)
self._check_status_code(resp, method, **kwargs)
except exc.KeystoneException:
raise
else:
self._check_status_code(resp, method, **kwargs)
return resp
def _unauthorized(self, resp):
status401 = (resp.status_code == 401)
status500 = (resp.status_code == 500 and "(HTTP 401)" in resp.content)
return status401 or status500
def _check_status_code(self, resp, method, **kwargs):
if self._unauthorized(resp):
message = "Unauthorized error"
raise exc.HTTPUnauthorized(message=message)
elif 400 <= resp.status_code < 600:
raise exc.from_response(resp)
elif resp.status_code in (301, 302, 305):
# Redirected. Reissue the request to the new location.
location = resp.headers.get('location')
if location is None:
message = "Location not returned with 302"
raise exc.InvalidEndpoint(message=message)
elif location.startswith(self.endpoint):
# shave off the endpoint, it will be prepended when we recurse
location = location[len(self.endpoint):]
else:
message = "Prohibited endpoint redirect %s" % location
raise exc.InvalidEndpoint(message=message)
return self._http_request(location, method, **kwargs)
elif resp.status_code == 300:
raise exc.from_response(resp)
def _make_request(self, method, url, allow_redirects, timeout, **kwargs):
try:
resp = requests.request(
method,
self.endpoint_url + url,
allow_redirects=allow_redirects,
timeout=timeout,
**kwargs)
except socket.gaierror as e:
message = ("Error finding address for %(url)s: %(e)s" %
{'url': self.endpoint_url + url, 'e': e})
raise exc.InvalidEndpoint(message=message)
except (socket.error, socket.timeout) as e:
endpoint = self.endpoint
message = ("Error communicating with %(endpoint)s %(e)s" %
{'endpoint': endpoint, 'e': e})
raise exc.CommunicationError(message=message)
except requests.Timeout as e:
endpoint = self.endpoint
message = ("Error %(method)s timeout request to %(endpoint)s %(e)s" %
{'method': method, 'endpoint': endpoint, 'e': e})
raise exc.RequestTimeoutError(message=message)
except requests.ConnectionError as ex:
endpoint = self.endpoint
message = ("Failed to connect to %s, error was %s" % (endpoint, ex.message))
raise exc.CommunicationError(message=message)
self.log_http_response(resp)
return resp
def json_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type', 'application/json')
kwargs['headers'].setdefault('Accept', 'application/json')
if 'data' in kwargs:
kwargs['data'] = jsonutils.dumps(kwargs['data'])
resp = self._http_request(url, method, **kwargs)
body = resp.content
if 'application/json' in resp.headers.get('content-type', ''):
try:
body = resp.json()
except ValueError:
LOG.error('Could not decode response body as JSON')
else:
body = None
return resp, body
def raw_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type',
'application/octet-stream')
return self._http_request(url, method, **kwargs)
def client_request(self, method, url, **kwargs):
resp, body = self.json_request(method, url, **kwargs)
return resp
def head(self, url, **kwargs):
return self.client_request("HEAD", url, **kwargs)
def get(self, url, **kwargs):
return self.client_request("GET", url, **kwargs)
def post(self, url, **kwargs):
return self.client_request("POST", url, **kwargs)
def put(self, url, **kwargs):
return self.client_request("PUT", url, **kwargs)
def delete(self, url, **kwargs):
return self.raw_request("DELETE", url, **kwargs)
def patch(self, url, **kwargs):
return self.client_request("PATCH", url, **kwargs)

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014, 2015 Hewlett Packard Enterprise Development Company LP
# Copyright 2017 Fujitsu LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -13,16 +14,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from monascaclient.apiclient import base
from six.moves.urllib import parse
from six.moves.urllib_parse import unquote
class MonascaManager(base.BaseManager):
class MonascaManager(object):
base_url = None
def __init__(self, client, **kwargs):
super(MonascaManager, self).__init__(client)
def __init__(self, client):
self.client = client
def _parse_body(self, body):
if type(body) is dict:
@ -38,18 +38,22 @@ class MonascaManager(base.BaseManager):
"""Get a list of metrics."""
url_str = self.base_url + path
if dim_key and dim_key in kwargs:
dimstr = self.get_dimensions_url_string(kwargs[dim_key])
kwargs[dim_key] = dimstr
dim_str = self.get_dimensions_url_string(kwargs[dim_key])
kwargs[dim_key] = dim_str
if kwargs:
url_str += '?%s' % parse.urlencode(kwargs, True)
resp, body = self.client.json_request(
'GET', url_str)
body = self.client.list(
path=url_str
)
return self._parse_body(body)
def get_dimensions_url_string(self, dimdict):
@staticmethod
def get_dimensions_url_string(dimensions):
dim_list = list()
for k, v in dimdict.items():
for k, v in dimensions.items():
# In case user specifies a dimension multiple times
if isinstance(v, (list, tuple)):
v = v[-1]
@ -59,10 +63,3 @@ class MonascaManager(base.BaseManager):
dim_str = k
dim_list.append(dim_str)
return ','.join(dim_list)
def list_next(self):
if hasattr(self, 'next') and self.next:
self.next = unquote(self.next)
path = self.next.split(self.base_url, 1)[-1]
return self._list(path)
return None

View File

@ -16,18 +16,13 @@
from __future__ import print_function
import numbers
import os
import sys
import textwrap
import uuid
import prettytable
import yaml
from monascaclient import exc
from osc_lib import exceptions as exc
from oslo_serialization import jsonutils
from oslo_utils import importutils
supported_formats = {
"json": lambda x: jsonutils.dumps(x, indent=2),
@ -45,23 +40,14 @@ def arg(*args, **kwargs):
return _decorator
def link_formatter(links):
return '\n'.join([l.get('href', '') for l in links or []])
def json_formatter(js):
return (jsonutils.dumps(js, indent=2, ensure_ascii=False)).encode('utf-8')
def text_wrap_formatter(d):
return '\n'.join(textwrap.wrap(d or '', 55))
def print_list(objs, fields, field_labels=None, formatters=None, sortby=None):
if formatters is None:
formatters = {}
def newline_list_formatter(r):
return '\n'.join(r or [])
def print_list(objs, fields, field_labels=None, formatters={}, sortby=None):
field_labels = field_labels or fields
pt = prettytable.PrettyTable([f for f in field_labels],
caching=False, print_empty=False)
@ -84,7 +70,9 @@ def print_list(objs, fields, field_labels=None, formatters={}, sortby=None):
print(pt.get_string(sortby=field_labels[sortby]).encode('utf-8'))
def print_dict(d, formatters={}):
def print_dict(d, formatters=None):
if formatters is None:
formatters = {}
pt = prettytable.PrettyTable(['Property', 'Value'],
caching=False, print_empty=False)
pt.align = 'l'
@ -97,57 +85,6 @@ def print_dict(d, formatters={}):
print(pt.get_string(sortby='Property').encode('utf-8'))
def find_resource(manager, name_or_id):
"""Helper for the _find_* methods."""
# first try to get entity as integer id
try:
if isinstance(name_or_id, int) or name_or_id.isdigit():
return manager.get(int(name_or_id))
except exc.NotFound:
pass
# now try to get entity as uuid
try:
uuid.UUID(str(name_or_id))
return manager.get(name_or_id)
except (ValueError, exc.NotFound):
pass
# finally try to find entity by name
try:
return manager.find(name=name_or_id)
except exc.NotFound:
msg = ("No %s with a name or ID of '%s' exists." %
(manager.resource_class.__name__.lower(), name_or_id))
raise exc.CommandError(msg)
def env(*vars, **kwargs):
"""Search for the first defined of possibly many env vars
Returns the first environment variable defined in vars, or
returns the default defined in kwargs.
"""
for v in vars:
value = os.environ.get(v)
if value:
return value
return kwargs.get('default', None)
def import_versioned_module(version, submodule=None):
module = 'monascaclient.v%s' % version
if submodule:
module = '.'.join((module, submodule))
return importutils.import_module(module)
def exit(msg=''):
if msg:
print(msg.encode('utf-8'), file=sys.stderr)
sys.exit(1)
def format_parameters(params):
'''Reformat parameters into dict of format expected by the API.'''
@ -165,7 +102,7 @@ def format_parameters(params):
parameters = {}
for p in params:
try:
(n, v) = p.split(('='), 1)
(n, v) = p.split('=', 1)
except ValueError:
msg = '%s(%s). %s.' % ('Malformed parameter', p,
'Use the key=value format')
@ -206,24 +143,14 @@ def format_dimensions_query(dims):
return dimensions
def format_output(output, format='yaml'):
"""Format the supplied dict as specified."""
output_format = format.lower()
try:
return supported_formats[output_format](output)
except KeyError:
raise exc.HTTPUnsupported("The format(%s) is unsupported."
% output_format)
def format_dimensions(dict):
return ('dimensions: {\n' + format_dict(dict) + '\n}')
return 'dimensions: {\n' + format_dict(dict) + '\n}'
def format_expression_data(dict):
def format_expression_data(data):
# takes an dictionary containing a dict
string_list = list()
for k, v in dict.items():
for k, v in data.items():
if k == 'dimensions':
dim_str = format_dimensions(v)
string_list.append(dim_str)
@ -271,25 +198,3 @@ def format_list(in_list):
key = k
string_list.append(key)
return '\n'.join(string_list)
def set_env_variables(kwargs):
environment_variables = {
'username': 'OS_USERNAME',
'password': 'OS_PASSWORD',
'token': 'OS_AUTH_TOKEN',
'auth_url': 'OS_AUTH_URL',
'service_type': 'OS_SERVICE_TYPE',
'endpoint_type': 'OS_ENDPOINT_TYPE',
'os_cacert': 'OS_CACERT',
'user_domain_id': 'OS_USER_DOMAIN_ID',
'user_domain_name': 'OS_USER_DOMAIN_NAME',
'project_id': 'OS_PROJECT_ID',
'project_name': 'OS_PROJECT_NAME',
'domain_id': 'OS_DOMAIN_ID',
'domain_name': 'OS_DOMAIN_NAME',
'region_name': 'OS_REGION_NAME'
}
for k, v in environment_variables.items():
if k not in kwargs:
kwargs[k] = env(v)

View File

@ -1,233 +0,0 @@
# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import sys
from oslo_serialization import jsonutils
verbose = 0
log = logging.getLogger(__name__)
class BaseException(Exception):
"""An error occurred."""
def __init__(self, message=None):
self.message = message
def __str__(self):
return self.message or self.__class__.__doc__
class CommandError(BaseException):
"""Invalid usage of CLI."""
class InvalidEndpoint(BaseException):
"""The provided endpoint is invalid."""
class CommunicationError(BaseException):
"""Unable to communicate with server."""
class RequestTimeoutError(BaseException):
"""Timeout making a POST, GET, PATCH, DELETE, or PUT request to the server."""
class KeystoneException(Exception):
"""Base exception for all Keystone-derived exceptions."""
# This is initialized with the exception raised by the Keystone client so
# deriving this class from Exception instead of BaseException allows that to
# be handled without any additional code
pass
class HTTPException(BaseException):
"""Base exception for all HTTP-derived exceptions."""
code = 'N/A'
def __init__(self, message=None):
super(HTTPException, self).__init__(message)
try:
log.error("exception: {}".format(message))
self.error = jsonutils.loads(message)
except Exception:
self.error = {'error':
{'message': self.message or self.__class__.__doc__}}
def __str__(self):
if 'description' in self.error:
# Python API:
# Expected message format:
# {
# "title": "Foo",
# "description": "Bar"
# }
message = self.error['description']
else:
# Java API:
# Expected message format:
# {
# "conflict":{"code":409,
# "message":"Bar",
# "details":"",
# "internal_code":"Baz"}
# }
for key in self.error:
message = self.error[key].get('message', 'Internal Error')
if verbose:
traceback = self.error['error'].get('traceback', '')
return '%s\n%s' % (message, traceback)
else:
return '%s' % message
class HTTPMultipleChoices(HTTPException):
code = 300
def __str__(self):
self.details = ("Requested version of Monasca API is not"
"available.")
return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code,
self.details)
class BadRequest(HTTPException):
code = 400
class HTTPBadRequest(BadRequest):
pass
class Unauthorized(HTTPException):
code = 401
class HTTPUnauthorized(Unauthorized):
pass
class Forbidden(HTTPException):
"""DEPRECATED."""
code = 403
class HTTPForbidden(Forbidden):
pass
class NotFound(HTTPException):
"""DEPRECATED."""
code = 404
class HTTPNotFound(NotFound):
pass
class HTTPMethodNotAllowed(HTTPException):
code = 405
class Conflict(HTTPException):
"""DEPRECATED."""
code = 409
class HTTPConflict(Conflict):
pass
class OverLimit(HTTPException):
"""DEPRECATED."""
code = 413
class HTTPOverLimit(OverLimit):
pass
class HTTPUnsupported(HTTPException):
code = 415
class HTTPUnProcessable(HTTPException):
code = 422
class HTTPInternalServerError(HTTPException):
code = 500
class HTTPNotImplemented(HTTPException):
code = 501
class HTTPBadGateway(HTTPException):
code = 502
class ServiceUnavailable(HTTPException):
"""DEPRECATED."""
code = 503
class HTTPServiceUnavailable(ServiceUnavailable):
pass
# NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception
# classes
_code_map = {}
for obj_name in dir(sys.modules[__name__]):
if obj_name.startswith('HTTP'):
obj = getattr(sys.modules[__name__], obj_name)
_code_map[obj.code] = obj
def from_response(response):
"""Return an instance of an HTTPException based on requests response."""
cls = _code_map.get(response.status_code, HTTPException)
return cls(response.content)
class NoTokenLookupException(Exception):
"""DEPRECATED."""
pass
class EndpointNotFound(Exception):
"""DEPRECATED."""
pass

View File

@ -1,105 +0,0 @@
# (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Wrapper around python keystone client to assist in getting a properly scoped token and the registered service
endpoint for Monasca.
"""
from keystoneclient.v3 import client
from monascaclient import exc
class KSClient(object):
def __init__(self, **kwargs):
"""Get an endpoint and auth token from Keystone.
:param username: name of user
:param password: user's password
:param user_domain_id: unique identifier of domain username resides in (optional)
:param user_domain_name: name of domain for username (optional), if user_domain_id not specified
:param project_id: unique identifier of project
:param project_name: name of project
:param project_domain_name: name of domain project is in
:param project_domain_id: id of domain project is in
:param auth_url: endpoint to authenticate against
:param token: token to use instead of username/password
"""
kc_args = {'auth_url': kwargs.get('auth_url'),
'insecure': kwargs.get('insecure'),
'timeout': kwargs.get('keystone_timeout')}
if kwargs.get('os_cacert'):
kc_args['cacert'] = kwargs.get('os_cacert')
if kwargs.get('project_id'):
kc_args['project_id'] = kwargs.get('project_id')
elif kwargs.get('project_name'):
kc_args['project_name'] = kwargs.get('project_name')
if kwargs.get('project_domain_name'):
kc_args['project_domain_name'] = kwargs.get('project_domain_name')
elif kwargs.get('domain_name'):
kc_args['project_domain_name'] = kwargs.get('domain_name') # backwards compat to 1.0.30 API
if kwargs.get('project_domain_id'):
kc_args['project_domain_id'] = kwargs.get('project_domain_id')
elif kwargs.get('domain_id'):
kc_args['project_domain_id'] = kwargs.get('domain_id') # backwards compat to 1.0.30 API
if kwargs.get('token'):
kc_args['token'] = kwargs.get('token')
else:
kc_args['username'] = kwargs.get('username')
kc_args['password'] = kwargs.get('password')
# when username not in the default domain (id='default'), supply user domain (as namespace)
if kwargs.get('user_domain_name'):
kc_args['user_domain_name'] = kwargs.get('user_domain_name')
if kwargs.get('user_domain_id'):
kc_args['user_domain_id'] = kwargs.get('user_domain_id')
self._kwargs = kwargs
self._keystone = client.Client(**kc_args)
self._token = None
self._monasca_url = None
@property
def token(self):
"""Token property
Validate token is project scoped and return it if it is
project_id and auth_token were fetched when keystone client was created
"""
if self._token is None:
if self._keystone.project_id:
self._token = self._keystone.auth_token
else:
raise exc.CommandError("No project id or project name.")
return self._token
@property
def monasca_url(self):
"""Return the monasca publicURL registered in keystone."""
if self._monasca_url is None:
if self._kwargs.get('region_name'):
self._monasca_url = self._keystone.service_catalog.url_for(
service_type=self._kwargs.get('service_type') or 'monitoring',
attr='region',
filter_value=self._kwargs.get('region_name'),
endpoint_type=self._kwargs.get('endpoint_type') or 'publicURL')
else:
self._monasca_url = self._keystone.service_catalog.url_for(
service_type=self._kwargs.get('service_type') or 'monitoring',
endpoint_type=self._kwargs.get('endpoint_type') or 'publicURL')
return self._monasca_url

View File

@ -0,0 +1,169 @@
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import six
from osc_lib.command import command
from osc_lib import utils
from monascaclient import version
LOG = logging.getLogger(__name__)
# NOTE(trebskit) this will be moved to another module
# once initial migration is up
# the point is to show how many code can we spare
# in order to get the client working with minimum effort needed
VERSION_MAP = {
'2_0': 'monascaclient.v2_0.client.Client'
}
def make_client(api_version, session=None,
endpoint=None, service_type='monitoring'):
"""Returns an monitoring API client."""
client_cls = utils.get_client_class('monitoring', api_version, VERSION_MAP)
c = client_cls(
session=session,
service_type=service_type,
endpoint=endpoint,
app_name='monascaclient',
app_version=version.version_string,
)
return c
def create_command_class(name, func_module):
"""Dynamically creates subclass of MigratingCommand.
Method takes name of the function, module it is part of
and builds the subclass of :py:class:`MigratingCommand`.
Having a subclass of :py:class:`cliff.command.Command` is mandatory
for the osc-lib integration.
:param name: name of the function
:type name: basestring
:param func_module: the module function is part of
:type func_module: module
:return: command name, subclass of :py:class:`MigratingCommand`
:rtype: tuple(basestring, class)
"""
cmd_name = name[3:].replace('_', '-')
callback = getattr(func_module, name)
desc = callback.__doc__ or ''
help = desc.strip().split('\n')[0]
arguments = getattr(callback, 'arguments', [])
body = {
'_args': arguments,
'_callback': staticmethod(callback),
'_description': desc,
'_epilog': desc,
'_help': help
}
claz = type('%sCommand' % cmd_name.title().replace('-', ''),
(MigratingCommand,), body)
return cmd_name, claz
class MigratingCommandMeta(command.CommandMeta):
"""Overwrite module name based on osc_lib.CommandMeta requirements."""
def __new__(mcs, name, bases, cls_dict):
# NOTE(trebskit) little dirty, but should suffice for migration period
cls_dict['__module__'] = 'monascaclient.v2_0.shell'
return super(MigratingCommandMeta, mcs).__new__(mcs, name,
bases, cls_dict)
@six.add_metaclass(MigratingCommandMeta)
class MigratingCommand(command.Command):
"""MigratingCommand is temporary command.
MigratingCommand allows to map function defined
shell commands from :py:module:`monascaclient.v2_0.shell`
into :py:class:`command.Command` instances.
Note:
This class is temporary solution during migrating
to osc_lib and will be removed when all
shell commands are migrated to cliff commands.
"""
_help = None
_args = None
_callback = None
def __init__(self, app, app_args, cmd_name=None):
super(MigratingCommand, self).__init__(app, app_args, cmd_name)
self._client = None
self._endpoint = None
def take_action(self, parsed_args):
return self._callback(self.mon_client, parsed_args)
def get_parser(self, prog_name):
parser = super(MigratingCommand, self).get_parser(prog_name)
for (args, kwargs) in self._args:
parser.add_argument(*args, **kwargs)
parser.add_argument('-j', '--json',
action='store_true',
help='output raw json response')
return parser
@property
def mon_client(self):
if not self._client:
self.log.debug('Initializing mon-client')
self._client = make_client(api_version=self.mon_version,
endpoint=self.mon_url,
session=self.app.client_manager.session)
return self._client
@property
def mon_version(self):
return self.app_args.monasca_api_version
@property
def mon_url(self):
if self._endpoint:
return self._endpoint
app_args = self.app_args
cm = self.app.client_manager
endpoint = app_args.monasca_api_url
if not endpoint:
req_data = {
'service_type': 'monitoring',
'region_name': cm.region_name,
'interface': cm.interface,
}
LOG.debug('Discovering monasca endpoint using %s' % req_data)
endpoint = cm.get_endpoint_for_service_type(**req_data)
else:
LOG.debug('Using supplied endpoint=%s' % endpoint)
self._endpoint = endpoint
return self._endpoint

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP
# Copyright 2017 Fujitsu LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -17,499 +18,100 @@
Command-line interface to the monasca-client API.
"""
from __future__ import print_function
import argparse
import logging
import string
import locale
import sys
import warnings
from six.moves.urllib.parse import urljoin
from osc_lib.api import auth
from osc_lib.cli import client_config as cloud_config
from osc_lib import shell
from osc_lib import utils as utils
from oslo_utils import importutils
import six
import monascaclient
from monascaclient import client as monasca_client
from monascaclient.common import utils
from monascaclient import exc
from monascaclient import ksclient
from monascaclient.osc import migration
from monascaclient import version as mc_version
logger = logging.getLogger(__name__)
class DeprecatedStore(argparse._StoreAction):
def __init__(self, *args, **kwargs):
super(DeprecatedStore, self).__init__(*args, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
warnings.filterwarnings(action='default', category=DeprecationWarning, module='.*monascaclient.*')
warnings.warn("{} is deprecated".format(",".join(self.option_strings)),
DeprecationWarning)
setattr(namespace, self.dest, values)
class MonascaShell(object):
def get_base_parser(self):
parser = argparse.ArgumentParser(
prog='monasca',
class MonascaShell(shell.OpenStackShell):
def __init__(self):
super(MonascaShell, self).__init__(
description=__doc__.strip(),
epilog='See "monasca help COMMAND" '
'for help on a specific command.',
add_help=False,
# formatter_class=HelpFormatter,
formatter_class=lambda prog: argparse.HelpFormatter(
prog,
max_help_position=29)
version=mc_version.version_string
)
self.cloud_config = None
def initialize_app(self, argv):
super(MonascaShell, self).initialize_app(argv)
self.cloud_config = cloud_config.OSC_Config(
override_defaults={
'interface': None,
'auth_type': self._auth_type,
},
)
# Global arguments
parser.add_argument('-h', '--help',
action='store_true',
help=argparse.SUPPRESS)
parser.add_argument('-j', '--json',
action='store_true',
help='output raw json response')
parser.add_argument('--version',
action='version',
version=monascaclient.__version__,
help="Shows the client version and exits.")
parser.add_argument('-d', '--debug',
default=bool(utils.env('MONASCA_DEBUG')),
action='store_true',
help='Defaults to env[MONASCA_DEBUG].')
parser.add_argument('-v', '--verbose',
default=False, action="store_true",
help="Print more verbose output.")
parser.add_argument('-k', '--insecure',
default=False,
action='store_true',
help="Explicitly allow the client to perform "
"\"insecure\" SSL (https) requests. The server's "
"certificate will not be verified against any "
"certificate authorities. "
"This option should be used with caution.")
parser.add_argument('--cert-file',
help='Path of certificate file to use in SSL '
'connection. This file can optionally be '
'prepended with the private key.')
parser.add_argument(
'--key-file',
help='Path of client key to use in SSL connection. '
'This option is not necessary if your key is'
' prepended to your cert file.')
parser.add_argument('--os-cacert',
default=utils.env('OS_CACERT'),
help='Specify a CA bundle file to use in verifying'
' a TLS (https) server certificate. Defaults to'
' env[OS_CACERT]. Without either of these, the'
' client looks for the default system CA'
' certificates.')
parser.add_argument('--keystone_timeout',
default=20,
help='Number of seconds to wait for a response from keystone.')
parser.add_argument('--os-username',
default=utils.env('OS_USERNAME'),
help='Defaults to env[OS_USERNAME].')
parser.add_argument('--os_username',
help=argparse.SUPPRESS)
parser.add_argument('--os-password',
default=utils.env('OS_PASSWORD'),
help='Defaults to env[OS_PASSWORD].')
parser.add_argument('--os_password',
help=argparse.SUPPRESS)
parser.add_argument('--os-user-domain-id',
default=utils.env('OS_USER_DOMAIN_ID'),
help='Defaults to env[OS_USER_DOMAIN_ID].')
parser.add_argument('--os-user-domain-name',
default=utils.env('OS_USER_DOMAIN_NAME'),
help='Defaults to env[OS_USER_DOMAIN_NAME].')
parser.add_argument('--os-project-id',
default=utils.env('OS_PROJECT_ID'),
help='Defaults to env[OS_PROJECT_ID].')
parser.add_argument('--os_project_id',
help=argparse.SUPPRESS)
parser.add_argument('--os-project-name',
default=utils.env('OS_PROJECT_NAME'),
help='Defaults to env[OS_PROJECT_NAME].')
parser.add_argument('--os_project_name',
help=argparse.SUPPRESS)
parser.add_argument('--os-tenant-name',
dest='os_project_name',
action=DeprecatedStore,
default=utils.env('OS_TENANT_NAME'),
help='(Deprecated, use --os-project_name) '
'Defaults to env[OS_TENANT_NAME].')
parser.add_argument('--os_tenant_name',
dest='os_project_name',
action=DeprecatedStore,
help=argparse.SUPPRESS)
parser.add_argument('--os-tenant-id',
dest='os_project_id',
action=DeprecatedStore,
default=utils.env('OS_TENANT_ID'),
help='(Deprecated, use --os-project_id) '
'Defaults to env[OS_TENANT_ID].')
parser.add_argument('--os_tenant_id',
dest='os_project_id',
action=DeprecatedStore,
help=argparse.SUPPRESS)
parser.add_argument('--os-project-domain-id',
default=utils.env('OS_PROJECT_DOMAIN_ID'),
help='Defaults to env[OS_PROJECT_DOMAIN_ID].')
parser.add_argument('--os-project-domain-name',
default=utils.env('OS_PROJECT_DOMAIN_NAME'),
help='Defaults to env[OS_PROJECT_DOMAIN_NAME].')
parser.add_argument('--os-auth-url',
default=utils.env('OS_AUTH_URL'),
help='Defaults to env[OS_AUTH_URL].')
parser.add_argument('--os_auth_url',
help=argparse.SUPPRESS)
parser.add_argument('--os-auth-version',
default=utils.env('OS_AUTH_VERSION'),
help='Defaults to env[OS_AUTH_VERSION].')
parser.add_argument('--os_auth_version',
help=argparse.SUPPRESS)
parser.add_argument('--os-region-name',
default=utils.env('OS_REGION_NAME'),
help='Defaults to env[OS_REGION_NAME].')
parser.add_argument('--os_region_name',
help=argparse.SUPPRESS)
parser.add_argument('--os-auth-token',
default=utils.env('OS_AUTH_TOKEN'),
help='Defaults to env[OS_AUTH_TOKEN].')
parser.add_argument('--os_auth_token',
help=argparse.SUPPRESS)
parser.add_argument('--os-no-client-auth',
default=utils.env('OS_NO_CLIENT_AUTH'),
action='store_true',
help="Do not contact keystone for a token. "
"Defaults to env[OS_NO_CLIENT_AUTH].")
def build_option_parser(self, description, version):
parser = super(MonascaShell, self).build_option_parser(
description,
version
)
parser = auth.build_auth_plugins_option_parser(parser)
parser = self._append_monasca_args(parser)
return parser
@staticmethod
def _append_monasca_args(parser):
parser.add_argument('--monasca-api-url',
default=utils.env('MONASCA_API_URL'),
help='Defaults to env[MONASCA_API_URL].')
parser.add_argument('--monasca_api_url',
help=argparse.SUPPRESS)
parser.add_argument('--monasca-api-version',
default=utils.env(
'MONASCA_API_VERSION',
default='2_0'),
help='Defaults to env[MONASCA_API_VERSION] or 2_0')
parser.add_argument('--monasca_api_version',
help=argparse.SUPPRESS)
parser.add_argument('--os-service-type',
default=utils.env('OS_SERVICE_TYPE'),
help='Defaults to env[OS_SERVICE_TYPE].')
parser.add_argument('--os_service_type',
help=argparse.SUPPRESS)
parser.add_argument('--os-endpoint-type',
default=utils.env('OS_ENDPOINT_TYPE'),
help='Defaults to env[OS_ENDPOINT_TYPE].')
parser.add_argument('--os_endpoint_type',
help=argparse.SUPPRESS)
# In OpenStack, the parameters below are intended for domain-scoping, i.e. authorize
# the user against a domain instead of a project. Previous versions of the agent used these
# to qualify the project name, leading to confusion and preventing reuse of typical RC files.
# Since domain scoping is not supported by Monasca, we can still support the old variable
# names for the time being. If the project-name is not scoped using the correct project
# domain name parameter, the code falls back to the domain scoping parameters.
parser.add_argument('--os-domain-id',
default=utils.env('OS_DOMAIN_ID'),
help=argparse.SUPPRESS)
parser.add_argument('--os-domain-name',
default=utils.env('OS_DOMAIN_NAME'),
help=argparse.SUPPRESS)
return parser
def get_subcommand_parser(self, version):
parser = self.get_base_parser()
def _load_commands(self):
version = self.options.monasca_api_version
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
submodule = utils.import_versioned_module(version, 'shell')
self._find_actions(subparsers, submodule)
self._find_actions(subparsers, self)
self._add_bash_completion_subparser(subparsers)
submodule = importutils.import_versioned_module('monascaclient',
version,
'shell')
return parser
self._find_actions(submodule)
def _add_bash_completion_subparser(self, subparsers):
subparser = subparsers.add_parser(
'bash_completion',
add_help=False,
formatter_class=HelpFormatter
)
self.subcommands['bash_completion'] = subparser
subparser.set_defaults(func=self.do_bash_completion)
def _find_actions(self, subparsers, actions_module):
def _find_actions(self, actions_module):
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
# I prefer to be hyphen-separated instead of underscores.
command = attr[3:].replace('_', '-')
callback = getattr(actions_module, attr)
desc = callback.__doc__ or ''
help = desc.strip().split('\n')[0]
arguments = getattr(callback, 'arguments', [])
name, clazz = migration.create_command_class(attr, actions_module)
subparser = subparsers.add_parser(command,
help=help,
description=desc,
add_help=False,
formatter_class=HelpFormatter)
subparser.add_argument('-h', '--help',
action='help',
help=argparse.SUPPRESS)
self.subcommands[command] = subparser
for (args, kwargs) in arguments:
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=callback)
if 'help' == name:
# help requires no auth
clazz.auth_required = False
def _setup_logging(self, debug):
log_lvl = logging.DEBUG if debug else logging.ERROR
logging.basicConfig(
format="%(levelname)s (%(module)s:%(lineno)d) %(message)s",
level=log_lvl)
def _setup_verbose(self, verbose):
if verbose:
exc.verbose = 1
def main(self, argv):
# Parse args once to find version
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
self._setup_logging(options.debug)
self._setup_verbose(options.verbose)
# build available subcommands based on version
api_version = options.monasca_api_version
subcommand_parser = self.get_subcommand_parser(api_version)
self.parser = subcommand_parser
# Handle top-level --help/-h before attempting to parse
# a command off the command line
if not args and options.help or not argv:
self.do_help(options)
return 0
# Parse args again and call whatever callback was selected
args = subcommand_parser.parse_args(argv)
# Short-circuit and deal with help command right away.
if args.func == self.do_help:
self.do_help(args)
return 0
elif args.func == self.do_bash_completion:
self.do_bash_completion(args)
return 0
if not args.os_username and not args.os_auth_token:
raise exc.CommandError("You must provide a username via"
" either --os-username or env[OS_USERNAME]"
" or a token via --os-auth-token or"
" env[OS_AUTH_TOKEN]")
if not args.os_password and not args.os_auth_token:
raise exc.CommandError("You must provide a password via"
" either --os-password or env[OS_PASSWORD]"
" or a token via --os-auth-token or"
" env[OS_AUTH_TOKEN]")
if args.os_no_client_auth:
if not args.monasca_api_url:
raise exc.CommandError("If you specify --os-no-client-auth"
" you must specify a Monasca API URL"
" via either --monasca-api-url or"
" env[MONASCA_API_URL]")
else:
if not args.os_auth_url:
raise exc.CommandError("You must provide an auth url via"
" either --os-auth-url or via"
" env[OS_AUTH_URL]")
auth_vars_present = args.os_auth_url and args.os_auth_version
versioned = 'v2.0' in args.os_auth_url or 'v3' in args.os_auth_url
if auth_vars_present and not versioned:
args.os_auth_url = urljoin(args.os_auth_url, args.os_auth_version)
if args.os_auth_url and 'v2.0' in args.os_auth_url:
args.os_auth_url = string.replace(args.os_auth_url, 'v2.0', 'v3')
kwargs = {
'username': args.os_username,
'password': args.os_password,
'token': args.os_auth_token,
'auth_url': args.os_auth_url,
'service_type': args.os_service_type,
'endpoint_type': args.os_endpoint_type,
'os_cacert': args.os_cacert,
'user_domain_id': args.os_user_domain_id,
'user_domain_name': args.os_user_domain_name,
'project_id': args.os_project_id,
'project_name': args.os_project_name,
# if project name is not scoped, fall back to previous behaviour (see above)
'project_domain_id': args.os_project_domain_id if args.os_project_domain_id else args.os_domain_id,
'project_domain_name': args.os_project_domain_name if args.os_project_domain_name else args.os_domain_name,
'insecure': args.insecure,
'region_name': args.os_region_name,
'keystone_timeout': args.keystone_timeout
}
endpoint = args.monasca_api_url
if not args.os_no_client_auth:
_ksclient = ksclient.KSClient(**kwargs)
if args.os_auth_token:
token = args.os_auth_token
else:
try:
token = _ksclient.token
except exc.CommandError:
raise exc.CommandError(
"User does not have a default project. "
"You must provide a project id using "
"--os-project-id or via env[OS_PROJECT_ID], "
"or you must provide a project name using "
"--os-project-name or via env[OS_PROJECT_NAME] "
"and a project domain using --os-project-domain-name, via "
"env[OS_PROJECT_DOMAIN_NAME], using --os-project-domain-id or "
"via env[OS_PROJECT_DOMAIN_ID]")
kwargs = {
'token': token,
'insecure': args.insecure,
'os_cacert': args.os_cacert,
'cert_file': args.cert_file,
'key_file': args.key_file,
'username': args.os_username,
'password': args.os_password,
'service_type': args.os_service_type,
'endpoint_type': args.os_endpoint_type,
'auth_url': args.os_auth_url,
'keystone_timeout': args.keystone_timeout
}
if args.os_user_domain_name:
kwargs['user_domain_name'] = args.os_user_domain_name
if args.os_user_domain_id:
kwargs['user_domain_id'] = args.os_user_domain_id
if args.os_region_name:
kwargs['region_name'] = args.os_region_name
if args.os_project_name:
kwargs['project_name'] = args.os_project_name
if args.os_project_id:
kwargs['project_id'] = args.os_project_id
# Monasca API uses domain_id/name for project_domain_id/name
# We cannot change this and therefore still use the misleading parameter names
if args.os_domain_name:
kwargs['domain_name'] = args.os_project_domain_name if args.os_project_domain_name \
else args.os_domain_name
if args.os_domain_id:
kwargs['domain_id'] = args.os_project_domain_id if args.os_project_domain_id \
else args.os_domain_id
if not endpoint:
endpoint = _ksclient.monasca_url
client = monasca_client.Client(api_version, endpoint, **kwargs)
args.func(client, args)
def do_bash_completion(self, args):
"""Prints all of the commands and options to stdout.
The monasca.bash_completion script doesn't have to hard code them.
"""
commands = set()
options = set()
for sc_str, sc in self.subcommands.items():
commands.add(sc_str)
for option in list(sc._optionals._option_string_actions):
options.add(option)
commands.remove('bash-completion')
commands.remove('bash_completion')
print(' '.join(commands | options))
@utils.arg('command', metavar='<subcommand>', nargs='?',
help='Display help for <subcommand>.')
def do_help(self, args):
"""Display help about this program or one of its subcommands."""
if getattr(args, 'command', None):
if args.command in self.subcommands:
self.subcommands[args.command].print_help()
else:
raise exc.CommandError("'%s' is not a valid subcommand" %
args.command)
else:
self.parser.print_help()
class HelpFormatter(argparse.HelpFormatter):
def start_section(self, heading):
# Title-case the headings
heading = '%s%s' % (heading[0].upper(), heading[1:])
super(HelpFormatter, self).start_section(heading)
self.command_manager.add_command(name, clazz)
def main(args=None):
try:
if args is None:
args = sys.argv[1:]
MonascaShell().main(args)
if six.PY2:
# Emulate Py3, decode argv into Unicode based on locale so that
# commands always see arguments as text instead of binary data
encoding = locale.getpreferredencoding()
if encoding:
args = map(lambda arg: arg.decode(encoding), args)
MonascaShell().run(args)
except Exception as e:
if '--debug' in args or '-d' in args:
raise
else:
print(e, file=sys.stderr)
sys.exit(1)
print(e)
sys.exit(1)
if __name__ == "__main__":
main()
sys.exit(main(sys.argv[1:]))

View File

@ -1,46 +0,0 @@
# (C) Copyright 2014,2016 Hewlett Packard Enterprise Development LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from keystoneclient.v3 import client as ksclient
def script_keystone_client(token=None):
if token:
ksclient.Client(auth_url='http://no.where',
insecure=False,
tenant_id='tenant_id',
token=token).AndReturn(FakeKeystone(token, None))
else:
ksclient.Client(auth_url='http://no.where',
insecure=False,
password='password',
project_name='project_name',
timeout=20,
username='username').AndReturn(FakeKeystone(
'abcd1234', 'test'))
class FakeServiceCatalog(object):
def url_for(self, endpoint_type, service_type):
return 'http://192.168.1.5:8004/v1/f14b41234'
class FakeKeystone(object):
service_catalog = FakeServiceCatalog()
def __init__(self, auth_token, project_id):
self.auth_token = auth_token
self.project_id = project_id

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2017 Hewlett Packard Enterprise Development LP
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -13,440 +14,48 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import re
import sys
import warnings
import mock
import fixtures
from keystoneclient.v3 import client as ksclient
from mox3 import mox
from oslotest import base
from requests_mock.contrib import fixture as requests_mock_fixture
import six
from monascaclient import exc
import monascaclient.shell
from monascaclient.tests import fakes
from monascaclient import shell
class TestCase(base.BaseTestCase):
class TestMonascaShell(base.BaseTestCase):
def setUp(self):
super(TestCase, self).setUp()
@mock.patch('monascaclient.shell.auth')
def test_should_use_auth_plugin_option_parser(self, auth):
auth.build_auth_plugins_option_parser = apop = mock.Mock()
shell.MonascaShell().run([])
apop.assert_called_once()
def tearDown(self):
super(TestCase, self).tearDown()
def set_fake_env(self, fake_env):
client_env = ('OS_USERNAME', 'OS_PASSWORD', 'OS_USER_DOMAIN_ID',
'OS_USER_DOMAIN_NAME', 'OS_PROJECT_ID',
'OS_PROJECT_NAME', 'OS_AUTH_URL', 'OS_REGION_NAME',
'OS_AUTH_TOKEN', 'OS_NO_CLIENT_AUTH', 'OS_SERVICE_TYPE',
'OS_DOMAIN_NAME', 'OS_DOMAIN_ID',
'OS_ENDPOINT_TYPE', 'MONASCA_API_URL')
for key in client_env:
self.useFixture(
fixtures.EnvironmentVariable(key, fake_env.get(key)))
# required for testing with Python 2.6
def assertRegexpMatches(self, text, expected_regexp, msg=None):
"""Fail the test unless the text matches the regular expression."""
if isinstance(expected_regexp, six.string_types):
expected_regexp = re.compile(expected_regexp)
if not expected_regexp.search(text):
msg = msg or "Regexp didn't match"
msg = '%s: %r not found in %r' % (
msg, expected_regexp.pattern, text)
raise self.failureException(msg)
def shell_error(self, argstr, error_match):
orig = sys.stderr
sys.stderr = six.StringIO()
_shell = monascaclient.shell.MonascaShell()
e = self.assertRaises(Exception, _shell.main, argstr.split()) # noqa
self.assertRegexpMatches(e.__str__(), error_match)
err = sys.stderr.getvalue()
sys.stderr.close()
sys.stderr = orig
return err
class ShellBase(TestCase):
def setUp(self):
super(ShellBase, self).setUp()
self.requests_mock = self.useFixture(requests_mock_fixture.Fixture())
self.m = mox.Mox()
self.m.StubOutWithMock(ksclient, 'Client')
self.addCleanup(self.m.VerifyAll)
self.addCleanup(self.m.UnsetStubs)
# Some tests set exc.verbose = 1, so reset on cleanup
def unset_exc_verbose():
exc.verbose = 0
self.addCleanup(unset_exc_verbose)
def shell(self, argstr):
orig = sys.stdout
try:
sys.stdout = six.StringIO()
_shell = monascaclient.shell.MonascaShell()
_shell.main(argstr.split())
self.subcommands = _shell.subcommands.keys()
except SystemExit:
exc_type, exc_value, exc_traceback = sys.exc_info()
self.assertEqual(0, exc_value.code)
finally:
out = sys.stdout.getvalue()
sys.stdout.close()
sys.stdout = orig
return out
class ShellTestCommon(ShellBase):
def setUp(self):
super(ShellTestCommon, self).setUp()
def test_help_unknown_command(self):
self.assertRaises(exc.CommandError, self.shell, 'help foofoo')
def test_help(self):
required = [
'^usage: monasca',
'(?m)^See "monasca help COMMAND" for help on a specific command',
def test_should_specify_monasca_args(self):
expected_args = [
'--monasca-api-url',
'--monasca-api-version',
'--monasca_api_url',
'--monasca_api_version',
]
for argstr in ['--help', 'help']:
help_text = self.shell(argstr)
for r in required:
self.assertRegexpMatches(help_text, r)
def test_command_help(self):
output = self.shell('help help')
self.assertIn('usage: monasca help [<subcommand>]', output)
subcommands = list(self.subcommands)
for command in subcommands:
if command.replace('_', '-') == 'bash-completion':
continue
output1 = self.shell('help %s' % command)
output2 = self.shell('%s --help' % command)
self.assertEqual(output1, output2)
self.assertRegexpMatches(output1, '^usage: monasca %s' % command)
parser = mock.Mock()
parser.add_argument = aa = mock.Mock()
shell.MonascaShell._append_monasca_args(parser)
def test_help_on_subcommand(self):
required = [
'^usage: monasca metric-create',
"(?m)^Create metric",
]
argstrings = [
'help metric-create',
]
for argstr in argstrings:
help_text = self.shell(argstr)
for r in required:
self.assertRegexpMatches(help_text, r)
aa.assert_called()
for mc in aa.mock_calls:
name = mc[1][0]
self.assertIn(name, expected_args)
def test_deprecated_warning(self):
argrequired = [('--help --os-tenant-name=this', '--os-tenant-name is deprecated'),
('--help --os-tenant-id=this', '--os-tenant-id is deprecated')]
for argstr, required in argrequired:
with warnings.catch_warnings(record=True) as w:
self.shell(argstr)
self.assertEqual(str(w[0].message), required)
self.assertEqual(w[0].category, DeprecationWarning)
@mock.patch('monascaclient.shell.importutils')
def test_should_load_commands_based_on_api_version(self, iu):
iu.import_versioned_module = ivm = mock.Mock()
instance = shell.MonascaShell()
instance.options = mock.Mock()
instance.options.monasca_api_version = version = mock.Mock()
class ShellTestMonascaCommands(ShellBase):
instance._find_actions = mock.Mock()
def setUp(self):
super(ShellTestMonascaCommands, self).setUp()
self._set_fake_env()
instance._load_commands()
def assertHeaders(self, req=None, **kwargs):
if not req:
req = self.requests_mock.last_request
self.assertEqual('abcd1234', req.headers['X-Auth-Token'])
self.assertEqual('python-monascaclient', req.headers['User-Agent'])
for k, v in kwargs.items():
self.assertEqual(v, req.headers[k])
def _set_fake_env(self):
fake_env = {
'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_PROJECT_NAME': 'project_name',
'OS_AUTH_URL': 'http://no.where',
}
self.set_fake_env(fake_env)
def _script_keystone_client(self):
fakes.script_keystone_client()
def test_bad_metrics_create_subcommand(self):
argstrings = [
'metric-create metric1',
'metric-create 123',
'metric-create',
]
_shell = monascaclient.shell.MonascaShell()
for argstr in argstrings:
self.assertRaises(SystemExit, _shell.main, argstr.split())
def test_good_metrics_create_subcommand(self):
self._script_keystone_client()
self.m.ReplayAll()
headers = {'location': 'http://no.where/v2.0/metrics'}
self.requests_mock.post('http://192.168.1.5:8004/v1/f14b41234/metrics',
status_code=204,
headers=headers)
argstrings = [
'metric-create metric1 123 --time 1395691090',
]
for argstr in argstrings:
retvalue = self.shell(argstr)
self.assertRegexpMatches(retvalue, "^Success")
data = {'timestamp': 1395691090,
'name': 'metric1',
'value': 123.0}
self.assertHeaders()
self.assertEqual(data, self.requests_mock.last_request.json())
def test_good_metrics_create_subcommand_with_tenant_id(self):
self._script_keystone_client()
self.m.ReplayAll()
headers = {'location': 'http://no.where/v2.0/metrics'}
self.requests_mock.post('http://192.168.1.5:8004/v1/f14b41234/metrics',
status_code=204,
headers=headers)
proj = 'd48e63e76a5c4e05ba26a1185f31d4aa'
argstrings = [
'metric-create metric1 123 --time 1395691090 --project-id ' + proj,
]
for argstr in argstrings:
retvalue = self.shell(argstr)
self.assertRegexpMatches(retvalue, "^Success")
data = {'timestamp': 1395691090,
'name': 'metric1',
'value': 123.0}
self.assertHeaders()
self.assertEqual(data, self.requests_mock.last_request.json())
request_url = self.requests_mock.last_request.url
query_arg = request_url[request_url.index('?') + 1:]
self.assertEqual('tenant_id=' + proj, query_arg)
def test_bad_notifications_create_missing_args_subcommand(self):
argstrings = [
'notification-create email1 metric1@hp.com',
]
_shell = monascaclient.shell.MonascaShell()
for argstr in argstrings:
self.assertRaises(SystemExit, _shell.main, argstr.split())
def test_good_notifications_create_subcommand(self):
self._script_keystone_client()
self.m.ReplayAll()
url = 'http://192.168.1.5:8004/v1/f14b41234/notification-methods'
headers = {'location': 'http://no.where/v2.0/notification-methods',
'Content-Type': 'application/json'}
self.requests_mock.post(url,
status_code=201,
headers=headers,
json='id')
argstrings = [
'notification-create email1 EMAIL john.doe@hp.com',
]
for argstr in argstrings:
retvalue = self.shell(argstr)
self.assertRegexpMatches(retvalue, "id")
data = {'name': 'email1',
'type': 'EMAIL',
'address': 'john.doe@hp.com'}
self.assertHeaders()
self.assertEqual(data, self.requests_mock.last_request.json())
def test_good_notifications_create_subcommand_webhook(self):
self._script_keystone_client()
self.m.ReplayAll()
url = 'http://192.168.1.5:8004/v1/f14b41234/notification-methods'
headers = {'location': 'http://no.where/v2.0/notification-methods',
'Content-Type': 'application/json'}
self.requests_mock.post(url,
status_code=201,
headers=headers,
json='id')
argstrings = [
'notification-create mypost WEBHOOK http://localhost:8080',
]
for argstr in argstrings:
retvalue = self.shell(argstr)
self.assertRegexpMatches(retvalue, "id")
data = {'name': 'mypost',
'type': 'WEBHOOK',
'address': 'http://localhost:8080'}
self.assertHeaders()
self.assertEqual(data, self.requests_mock.last_request.json())
def test_good_notifications_patch(self):
args = '--type EMAIL --address john.doe@hpe.com --period 0'
data = {'type': 'EMAIL',
'address': 'john.doe@hpe.com',
'period': 0}
self.run_notification_patch_test(args, data)
def test_good_notifications_patch_just_name(self):
name = 'fred'
args = '--name ' + name
data = {'name': name}
self.run_notification_patch_test(args, data)
def test_good_notifications_patch_just_address(self):
address = 'fred@fl.com'
args = '--address ' + address
data = {'address': address}
self.run_notification_patch_test(args, data)
def test_good_notifications_patch_just_period(self):
period = 0
args = '--period ' + str(period)
data = {'period': period}
self.run_notification_patch_test(args, data)
def run_notification_patch_test(self, args, data):
self._script_keystone_client()
self.m.ReplayAll()
id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96490'
url = 'http://192.168.1.5:8004/v1/f14b41234/notification-methods/'
headers = {'location': 'http://no.where/v2.0/notification-methods',
'Content-Type': 'application/json'}
self.requests_mock.patch(url + id_str,
status_code=201,
headers=headers,
json='id')
argstring = 'notification-patch {0} {1}'.format(id_str, args)
retvalue = self.shell(argstring)
self.assertRegexpMatches(retvalue, "id")
self.assertHeaders()
self.assertEqual(data, self.requests_mock.last_request.json())
def test_bad_notifications_patch(self):
self._script_keystone_client()
self.m.ReplayAll()
id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96490'
argstring = 'notification-patch {0} --type EMAIL --address' \
' john.doe@hpe.com --period 60'.format(id_str)
retvalue = self.shell(argstring)
self.assertRegexpMatches(retvalue, "^Invalid")
def test_good_notifications_update(self):
self._script_keystone_client()
self.m.ReplayAll()
id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96491'
url = 'http://192.168.1.5:8004/v1/f14b41234/notification-methods/'
headers = {'location': 'http://no.where/v2.0/notification-methods',
'Content-Type': 'application/json'}
self.requests_mock.put(url + id_str,
status_code=201,
headers=headers,
json='id')
argstring = 'notification-update {0} notification_updated_name ' \
'EMAIL john.doe@hpe.com 0'.format(id_str)
retvalue = self.shell(argstring)
self.assertRegexpMatches(retvalue, "id")
data = {'name': 'notification_updated_name',
'type': 'EMAIL',
'address': 'john.doe@hpe.com',
'period': 0}
self.assertHeaders()
self.assertEqual(data, self.requests_mock.last_request.json())
def test_good_alarm_definition_update(self):
self._script_keystone_client()
self.m.ReplayAll()
id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96490'
url = 'http://192.168.1.5:8004/v1/f14b41234/alarm-definitions/'
headers = {'location': 'http://no.where/v2.0/notification-methods',
'Content-Type': 'application/json'}
self.requests_mock.put(url + id_str,
status_code=201,
headers=headers,
json='id')
cmd = 'alarm-definition-update'
name = 'alarm_name'
description = 'test_alarm_definition'
expression = 'avg(Test_Metric_1)>=10'
notif_id = '16012650-0b62-4692-9103-2d04fe81cc93'
enabled = 'True'
match_by = 'hostname'
severity = 'CRITICAL'
args = [cmd, id_str, name, description, expression, notif_id,
notif_id, notif_id, enabled, match_by, severity]
argstring = " ".join(args)
retvalue = self.shell(argstring)
self.assertRegexpMatches(retvalue, "id")
data = {'name': name,
'description': description,
'expression': expression,
'alarm_actions': [notif_id],
'undetermined_actions': [notif_id],
'ok_actions': [notif_id],
'match_by': [match_by],
'actions_enabled': bool(enabled),
'severity': severity}
self.assertHeaders()
self.assertEqual(data, self.requests_mock.last_request.json())
def test_notifications_types_list(self):
self._script_keystone_client()
self.m.ReplayAll()
url = 'http://192.168.1.5:8004/v1/f14b41234/notification-methods/'
headers = {'Content-Type': 'application/json'}
body = [{"type": "WEBHOOK"}, {"type": "EMAIL"}, {"type": "PAGERDUTY"}]
self.requests_mock.get(url + 'types', headers=headers, json=body)
argstrings = ["notification-type-list"]
retvalue = self.shell("".join(argstrings))
self.assertRegexpMatches(retvalue, "types")
self.assertHeaders()
ivm.assert_called_once_with('monascaclient', version, 'shell')

View File

View File

@ -0,0 +1,134 @@
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from oslotest import base
from monascaclient.osc import migration as migr
from monascaclient.v2_0 import alarm_definitions as ad
from monascaclient.v2_0 import shell
class FakeV2Client(object):
def __init__(self):
super(FakeV2Client, self).__init__()
self.alarm_definitions = mock.Mock(
spec=ad.AlarmDefinitionsManager)
class TestAlarmDefinitionShellV2(base.BaseTestCase):
@mock.patch('monascaclient.osc.migration.make_client')
def test_should_update(self, mc):
mc.return_value = c = FakeV2Client()
ad_id = '0495340b-58fd-4e1c-932b-5e6f9cc96490'
ad_name = 'alarm_name'
ad_desc = 'test_alarm_definition'
ad_expr = 'avg(Test_Metric_1)>=10'
ad_action_id = '16012650-0b62-4692-9103-2d04fe81cc93'
ad_action_enabled = 'True'
ad_match_by = 'hostname'
ad_severity = 'CRITICAL'
raw_args = [
ad_id, ad_name, ad_desc, ad_expr,
ad_action_id, ad_action_id, ad_action_id, ad_action_enabled,
ad_match_by, ad_severity
]
name, cmd_clazz = migr.create_command_class(
'do_alarm_definition_update',
shell
)
cmd = cmd_clazz(mock.Mock(), mock.Mock())
parser = cmd.get_parser(name)
parsed_args = parser.parse_args(raw_args)
cmd.run(parsed_args)
c.alarm_definitions.update.assert_called_once_with(
actions_enabled=True,
alarm_actions=[ad_action_id],
alarm_id=ad_id,
description=ad_desc,
expression=ad_expr,
match_by=[ad_match_by],
name=ad_name,
ok_actions=[ad_action_id],
severity=ad_severity,
undetermined_actions=[ad_action_id]
)
@mock.patch('monascaclient.osc.migration.make_client')
def test_should_patch_name(self, mc):
ad_id = '0495340b-58fd-4e1c-932b-5e6f9cc96490'
ad_name = 'patch_name'
raw_args = '{0} --name {1}'.format(ad_id, ad_name).split(' ')
self._patch_test(mc, raw_args, alarm_id=ad_id, name=ad_name)
@mock.patch('monascaclient.osc.migration.make_client')
def test_should_patch_actions(self, mc):
ad_id = '0495340b-58fd-4e1c-932b-5e6f9cc96490'
ad_action_id = '16012650-0b62-4692-9103-2d04fe81cc93'
actions = ['alarm-actions', 'ok-actions',
'undetermined-actions']
for action in actions:
raw_args = ('{0} --{1} {2}'.format(ad_id, action, ad_action_id)
.split(' '))
self._patch_test(mc, raw_args, **{
'alarm_id': ad_id,
action.replace('-', '_'): [ad_action_id]
})
@mock.patch('monascaclient.osc.migration.make_client')
def test_should_patch_severity(self, mc):
ad_id = '0495340b-58fd-4e1c-932b-5e6f9cc96490'
severity_types = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
for st in severity_types:
raw_args = ('{0} --severity {1}'.format(ad_id, st)
.split(' '))
self._patch_test(mc, raw_args, alarm_id=ad_id, severity=st)
@mock.patch('monascaclient.osc.migration.make_client')
def test_should_not_patch_unknown_severity(self, mc):
ad_id = '0495340b-58fd-4e1c-932b-5e6f9cc96490'
st = 'foo'
raw_args = ('{0} --severity {1}'.format(ad_id, st)
.split(' '))
self._patch_test(mc, raw_args, called=False)
@staticmethod
def _patch_test(mc, args, called=True, **kwargs):
mc.return_value = c = FakeV2Client()
name, cmd_clazz = migr.create_command_class(
'do_alarm_definition_patch',
shell
)
cmd = cmd_clazz(mock.Mock(), mock.Mock())
parser = cmd.get_parser(name)
parsed_args = parser.parse_args(args)
cmd.run(parsed_args)
if called:
c.alarm_definitions.patch.assert_called_once_with(**kwargs)
else:
c.alarm_definitions.patch.assert_not_called()

View File

@ -0,0 +1,85 @@
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from oslotest import base
from monascaclient.osc import migration as migr
from monascaclient.v2_0 import metrics
from monascaclient.v2_0 import shell
class FakeV2Client(object):
def __init__(self):
super(FakeV2Client, self).__init__()
self.metrics = mock.Mock(spec=metrics.MetricsManager)
class TestMetricsShellV2(base.BaseTestCase):
def test_bad_metrics(self):
raw_args_list = [
['metric1'],
['123'],
['']
]
name, cmd_clazz = migr.create_command_class('do_metric_create',
shell)
for raw_args in raw_args_list:
cmd = cmd_clazz(mock.Mock(), mock.Mock())
parser = cmd.get_parser(name)
self.assertRaises(SystemExit, parser.parse_args, raw_args)
@mock.patch('monascaclient.osc.migration.make_client')
def test_metric_create(self, mc):
mc.return_value = c = FakeV2Client()
raw_args = 'metric1 123 --time 1395691090'.split(' ')
name, cmd_clazz = migr.create_command_class('do_metric_create',
shell)
cmd = cmd_clazz(mock.Mock(), mock.Mock())
parser = cmd.get_parser(name)
parsed_args = parser.parse_args(raw_args)
cmd.run(parsed_args)
data = {'timestamp': 1395691090,
'name': 'metric1',
'value': 123.0}
c.metrics.create.assert_called_once_with(**data)
@mock.patch('monascaclient.osc.migration.make_client')
def test_metric_create_with_project_id(self, mc):
mc.return_value = c = FakeV2Client()
project_id = 'd48e63e76a5c4e05ba26a1185f31d4aa'
raw_args = ('metric1 123 --time 1395691090 --project-id %s'
% project_id).split(' ')
name, cmd_clazz = migr.create_command_class('do_metric_create',
shell)
cmd = cmd_clazz(mock.Mock(), mock.Mock())
parser = cmd.get_parser(name)
parsed_args = parser.parse_args(raw_args)
cmd.run(parsed_args)
data = {'timestamp': 1395691090,
'name': 'metric1',
'tenant_id': project_id,
'value': 123.0}
c.metrics.create.assert_called_once_with(**data)

View File

@ -0,0 +1,52 @@
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from oslotest import base
from monascaclient.osc import migration as migr
from monascaclient.v2_0 import notificationtypes
from monascaclient.v2_0 import shell
class FakeV2Client(object):
def __init__(self):
super(FakeV2Client, self).__init__()
self.notificationtypes = mock.Mock(
spec=notificationtypes.NotificationTypesManager)
class TestNotificationsTypesShellV2(base.BaseTestCase):
@mock.patch('monascaclient.osc.migration.make_client')
def test_notification_types_list(self, mc):
mc.return_value = c = FakeV2Client()
c.notificationtypes.list.return_value = [
{"type": "WEBHOOK"},
{"type": "EMAIL"},
{"type": "PAGERDUTY"}
]
raw_args = []
name, cmd_clazz = migr.create_command_class('do_notification_type_list',
shell)
cmd = cmd_clazz(mock.Mock(), mock.Mock())
parser = cmd.get_parser(name)
parsed_args = parser.parse_args(raw_args)
cmd.run(parsed_args)
c.notificationtypes.list.assert_called_once()

View File

@ -0,0 +1,160 @@
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from oslotest import base
from monascaclient.osc import migration as migr
from monascaclient.v2_0 import notifications
from monascaclient.v2_0 import shell
class FakeV2Client(object):
def __init__(self):
super(FakeV2Client, self).__init__()
self.notifications = mock.Mock(spec=notifications.NotificationsManager)
class TestNotificationsShellV2(base.BaseTestCase):
@mock.patch('monascaclient.osc.migration.make_client')
def test_notification_create_email(self, mc):
mc.return_value = c = FakeV2Client()
raw_args = ['email1', 'EMAIL', 'john.doe@hp.com']
name, cmd_clazz = migr.create_command_class('do_notification_create',
shell)
cmd = cmd_clazz(mock.Mock(), mock.Mock())
parser = cmd.get_parser(name)
parsed_args = parser.parse_args(raw_args)
cmd.run(parsed_args)
data = {'name': 'email1',
'type': 'EMAIL',
'address': 'john.doe@hp.com'}
c.notifications.create.assert_called_once_with(**data)
@mock.patch('monascaclient.osc.migration.make_client')
def test_notification_create_webhook(self, mc):
mc.return_value = c = FakeV2Client()
raw_args = ['mypost', 'WEBHOOK', 'http://localhost:8080']
name, cmd_clazz = migr.create_command_class('do_notification_create',
shell)
cmd = cmd_clazz(mock.Mock(), mock.Mock())
parser = cmd.get_parser(name)
parsed_args = parser.parse_args(raw_args)
cmd.run(parsed_args)
data = {'name': 'mypost',
'type': 'WEBHOOK',
'address': 'http://localhost:8080'}
c.notifications.create.assert_called_once_with(**data)
@mock.patch('monascaclient.osc.migration.make_client')
def test_good_notifications_patch(self, mc):
args = '--type EMAIL --address john.doe@hpe.com --period 0'
data = {'type': 'EMAIL',
'address': 'john.doe@hpe.com',
'period': 0}
self._patch_test(mc, args, data)
@mock.patch('monascaclient.osc.migration.make_client')
def test_good_notifications_patch_just_name(self, mc):
name = 'fred'
args = '--name ' + name
data = {'name': name}
self._patch_test(mc, args, data)
@mock.patch('monascaclient.osc.migration.make_client')
def test_good_notifications_patch_just_address(self, mc):
address = 'fred@fl.com'
args = '--address ' + address
data = {'address': address}
self._patch_test(mc, args, data)
@mock.patch('monascaclient.osc.migration.make_client')
def test_good_notifications_patch_just_period(self, mc):
period = 0
args = '--period ' + str(period)
data = {'period': period}
self._patch_test(mc, args, data)
@mock.patch('monascaclient.osc.migration.make_client')
def test_bad_notifications_patch(self, mc):
mc.return_value = c = FakeV2Client()
id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96490'
raw_args = ('{0} --type EMAIL --address john.doe@hpe.com '
'--period 60').format(id_str).split(' ')
name, cmd_clazz = migr.create_command_class('do_notification_patch',
shell)
cmd = cmd_clazz(mock.Mock(), mock.Mock())
parser = cmd.get_parser(name)
parsed_args = parser.parse_args(raw_args)
cmd.run(parsed_args)
c.notifications.patch.assert_not_called()
@mock.patch('monascaclient.osc.migration.make_client')
def test_good_notifications_update(self, mc):
mc.return_value = c = FakeV2Client()
id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96491'
raw_args = ('{0} notification_updated_name '
'EMAIL john.doe@hpe.com 0').format(id_str).split(' ')
name, cmd_clazz = migr.create_command_class('do_notification_update',
shell)
cmd = cmd_clazz(mock.Mock(), mock.Mock())
parser = cmd.get_parser(name)
parsed_args = parser.parse_args(raw_args)
cmd.run(parsed_args)
data = {
'name': 'notification_updated_name',
'type': 'EMAIL',
'address': 'john.doe@hpe.com',
'period': 0,
'notification_id': id_str
}
c.notifications.update.assert_called_once_with(**data)
@staticmethod
def _patch_test(mc, args, data):
mc.return_value = c = FakeV2Client()
id_str = '0495340b-58fd-4e1c-932b-5e6f9cc96490'
raw_args = '{0} {1}'.format(id_str, args).split(' ')
name, cmd_clazz = migr.create_command_class('do_notification_patch',
shell)
cmd = cmd_clazz(mock.Mock(), mock.Mock())
parser = cmd.get_parser(name)
parsed_args = parser.parse_args(raw_args)
cmd.run(parsed_args)
# add notification_id to data
data['notification_id'] = id_str
c.notifications.patch.assert_called_once_with(**data)

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -13,31 +14,27 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from monascaclient.apiclient import base
from monascaclient.common import monasca_manager
class AlarmDefinitions(base.Resource):
def __repr__(self):
return "<AlarmDefinitions %s>" % self._info
class AlarmDefinitionsManager(monasca_manager.MonascaManager):
resource_class = AlarmDefinitions
base_url = '/alarm-definitions'
def create(self, **kwargs):
"""Create an alarm definition."""
resp, body = self.client.json_request('POST', self.base_url,
data=kwargs)
return body
resp = self.client.create(url=self.base_url,
json=kwargs)
return resp
def get(self, **kwargs):
"""Get the details for a specific alarm definition."""
url_str = self.base_url + '/%s' % kwargs['alarm_id']
resp, body = self.client.json_request('GET', url_str)
return body
# NOTE(trebskit) should actually be find_one, but
# monasca does not support expected response format
url = '%s/%s' % (self.base_url, kwargs['alarm_id'])
resp = self.client.list(path=url)
return resp
def list(self, **kwargs):
"""Get a list of alarm definitions."""
@ -46,19 +43,27 @@ class AlarmDefinitionsManager(monasca_manager.MonascaManager):
def delete(self, **kwargs):
"""Delete a specific alarm definition."""
url_str = self.base_url + '/%s' % kwargs['alarm_id']
resp, body = self.client.json_request('DELETE', url_str)
resp = self.client.delete(url_str)
return resp
def update(self, **kwargs):
"""Update a specific alarm definition."""
url_str = self.base_url + '/%s' % kwargs['alarm_id']
del kwargs['alarm_id']
resp, body = self.client.json_request('PUT', url_str, data=kwargs)
return body
resp = self.client.create(url=url_str,
method='PUT',
json=kwargs)
return resp
def patch(self, **kwargs):
"""Patch a specific alarm definition."""
url_str = self.base_url + '/%s' % kwargs['alarm_id']
del kwargs['alarm_id']
resp, body = self.client.json_request('PATCH', url_str, data=kwargs)
return body
resp = self.client.create(url=url_str,
method='PATCH',
json=kwargs)
return resp

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company LP
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -15,25 +16,21 @@
from six.moves.urllib import parse
from monascaclient.apiclient import base
from monascaclient.common import monasca_manager
class Alarms(base.Resource):
def __repr__(self):
return "<Alarms %s>" % self._info
class AlarmsManager(monasca_manager.MonascaManager):
resource_class = Alarms
base_url = '/alarms'
def get(self, **kwargs):
"""Get the details for a specific alarm."""
url_str = self.base_url + '/%s' % kwargs['alarm_id']
resp, body = self.client.json_request('GET', url_str)
return body
# NOTE(trebskit) should actually be find_one, but
# monasca does not support expected response format
url = '%s/%s' % (self.base_url, kwargs['alarm_id'])
resp = self.client.list(path=url)
return resp
def list(self, **kwargs):
"""Get a list of alarms."""
@ -42,34 +39,41 @@ class AlarmsManager(monasca_manager.MonascaManager):
def delete(self, **kwargs):
"""Delete a specific alarm."""
url_str = self.base_url + '/%s' % kwargs['alarm_id']
resp, body = self.client.json_request('DELETE', url_str)
resp = self.client.delete(url_str)
return resp
def update(self, **kwargs):
"""Update a specific alarm."""
url_str = self.base_url + '/%s' % kwargs['alarm_id']
del kwargs['alarm_id']
resp, body = self.client.json_request('PUT', url_str,
data=kwargs)
body = self.client.create(url=url_str,
method='PUT',
json=kwargs)
return body
def patch(self, **kwargs):
"""Patch a specific alarm."""
url_str = self.base_url + '/%s' % kwargs['alarm_id']
del kwargs['alarm_id']
resp, body = self.client.json_request('PATCH', url_str,
data=kwargs)
return body
resp = self.client.create(url=url_str,
method='PATCH',
json=kwargs)
return resp
def count(self, **kwargs):
url_str = self.base_url + '/count'
if 'metric_dimensions' in kwargs:
dimstr = self.get_dimensions_url_string(kwargs['metric_dimensions'])
dimstr = self.get_dimensions_url_string(
kwargs['metric_dimensions'])
kwargs['metric_dimensions'] = dimstr
if kwargs:
url_str = url_str + '?%s' % parse.urlencode(kwargs, True)
resp, body = self.client.json_request('GET', url_str)
body = self.client.list(url_str)
return body
def history(self, **kwargs):
@ -78,8 +82,8 @@ class AlarmsManager(monasca_manager.MonascaManager):
del kwargs['alarm_id']
if kwargs:
url_str = url_str + '?%s' % parse.urlencode(kwargs, True)
resp, body = self.client.json_request('GET', url_str)
return body['elements'] if type(body) is dict else body
resp = self.client.list(url_str)
return resp['elements'] if type(resp) is dict else resp
def history_list(self, **kwargs):
"""History list of alarm state."""
@ -89,5 +93,5 @@ class AlarmsManager(monasca_manager.MonascaManager):
kwargs['dimensions'] = dimstr
if kwargs:
url_str = url_str + '?%s' % parse.urlencode(kwargs, True)
resp, body = self.client.json_request('GET', url_str)
return body['elements'] if type(body) is dict else body
resp = self.client.list(url_str)
return resp['elements'] if type(resp) is dict else resp

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -12,41 +13,28 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import string
from monascaclient.common import http
from monascaclient.v2_0 import alarm_definitions
from osc_lib.api import api
from monascaclient.v2_0 import alarm_definitions as ad
from monascaclient.v2_0 import alarms
from monascaclient.v2_0 import metrics
from monascaclient.v2_0 import notifications
from monascaclient.v2_0 import notificationtypes
from monascaclient.v2_0 import notificationtypes as nt
class Client(object):
"""Client for the Monasca v2_0 API.
:param string endpoint: A user-supplied endpoint URL for the monasca api
service.
:param string token: Token for authentication.
:param integer timeout: Allows customization of the timeout for client
http requests. (optional)
"""
def __init__(self, *args, **kwargs):
"""Initialize a new http client for the monasca API."""
if 'auth_url' in kwargs and 'v2.0' in kwargs['auth_url']:
kwargs['auth_url'] = string.replace(
kwargs['auth_url'], 'v2.0', 'v3')
self.http_client = http.HTTPClient(*args, **kwargs)
self.metrics = metrics.MetricsManager(self.http_client)
self.notifications = notifications.NotificationsManager(
self.http_client)
self.alarms = alarms.AlarmsManager(self.http_client)
self.alarm_definitions = alarm_definitions.AlarmDefinitionsManager(
self.http_client)
self.notificationtypes = notificationtypes.NotificationTypesManager(
self.http_client)
def replace_token(self, token):
self.http_client.replace_token(token)
client = MonascaApi(*args, **kwargs)
self.metrics = metrics.MetricsManager(client)
self.notifications = notifications.NotificationsManager(client)
self.alarms = alarms.AlarmsManager(client)
self.alarm_definitions = ad.AlarmDefinitionsManager(client)
self.notificationtypes = nt.NotificationTypesManager(client)
class MonascaApi(api.BaseAPI):
SERVICE_TYPE = "monitoring"

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -13,36 +14,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from copy import deepcopy
from monascaclient.apiclient import base
from monascaclient.common import monasca_manager
class Metrics(base.Resource):
def __repr__(self):
return "<Metrics %s>" % self._info
class MetricsManager(monasca_manager.MonascaManager):
resource_class = Metrics
base_url = '/metrics'
def create(self, **kwargs):
local_kwargs = deepcopy(kwargs)
"""Create a metric."""
url_str = self.base_url
if 'tenant_id' in local_kwargs:
url_str = url_str + '?tenant_id=%s' % local_kwargs['tenant_id']
del local_kwargs['tenant_id']
if 'jsonbody' in local_kwargs:
resp, body = self.client.json_request('POST', url_str,
data=local_kwargs['jsonbody'])
else:
resp, body = self.client.json_request('POST', url_str,
data=local_kwargs)
return resp
if 'tenant_id' in kwargs:
url_str = url_str + '?tenant_id=%s' % kwargs['tenant_id']
del kwargs['tenant_id']
data = kwargs['jsonbody'] if 'jsonbody' in kwargs else kwargs
body = self.client.create(url=url_str, json=data)
return body
def list(self, **kwargs):
"""Get a list of metrics."""

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -13,33 +14,27 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from copy import deepcopy
from monascaclient.apiclient import base
from monascaclient.common import monasca_manager
class Notifications(base.Resource):
def __repr__(self):
return "<Notifications %s>" % self._info
class NotificationsManager(monasca_manager.MonascaManager):
resource_class = Notifications
base_url = '/notification-methods'
def create(self, **kwargs):
"""Create a notification."""
resp, body = self.client.json_request('POST', self.base_url,
data=kwargs)
body = self.client.create(url=self.base_url,
json=kwargs)
return body
def get(self, **kwargs):
"""Get the details for a specific notification."""
url_str = self.base_url + '/%s' % kwargs['notification_id']
resp, body = self.client.json_request('GET', url_str)
return body
# NOTE(trebskit) should actually be find_one, but
# monasca does not support expected response format
url = '%s/%s' % (self.base_url, kwargs['notification_id'])
resp = self.client.list(path=url)
return resp
def list(self, **kwargs):
"""Get a list of notifications."""
@ -47,24 +42,27 @@ class NotificationsManager(monasca_manager.MonascaManager):
def delete(self, **kwargs):
"""Delete a notification."""
url_str = self.base_url + '/%s' % kwargs['notification_id']
resp, body = self.client.json_request('DELETE', url_str)
url = self.base_url + '/%s' % kwargs['notification_id']
resp = self.client.delete(url=url)
return resp
def update(self, **kwargs):
local_kwargs = deepcopy(kwargs)
"""Update a notification."""
url_str = self.base_url + '/%s' % local_kwargs['notification_id']
del local_kwargs['notification_id']
resp, body = self.client.json_request('PUT', url_str,
data=local_kwargs)
return body
url_str = self.base_url + '/%s' % kwargs['notification_id']
del kwargs['notification_id']
resp = self.client.create(url=url_str,
method='PUT',
json=kwargs)
return resp
def patch(self, **kwargs):
local_kwargs = deepcopy(kwargs)
"""Patch a notification."""
url_str = self.base_url + '/%s' % local_kwargs['notification_id']
del local_kwargs['notification_id']
resp, body = self.client.json_request('PATCH', url_str,
data=local_kwargs)
return body
url_str = self.base_url + '/%s' % kwargs['notification_id']
del kwargs['notification_id']
resp = self.client.create(url=url_str,
method='PATCH',
json=kwargs)
return resp

View File

@ -1,4 +1,5 @@
# (C) Copyright 2016 Hewlett Packard Enterprise Development LP
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -13,18 +14,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from monascaclient.apiclient import base
from monascaclient.common import monasca_manager
class NotificationTypes(base.Resource):
def __repr__(self):
return "<NotificationTypes %s>" % self._info
class NotificationTypesManager(monasca_manager.MonascaManager):
resource_class = NotificationTypes
base_url = '/notification-methods/types'
def list(self, **kwargs):

View File

@ -1,4 +1,5 @@
# (C) Copyright 2014-2017 Hewlett Packard Enterprise Development LP
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -18,12 +19,12 @@ import json
import numbers
import time
from keystoneauth1 import exceptions as k_exc
from osc_lib import exceptions as osc_exc
from monascaclient.common import utils
import monascaclient.exc as exc
from oslo_serialization import jsonutils
from six.moves import xrange
# Alarm valid types
severity_types = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
@ -83,10 +84,8 @@ def do_metric_create(mc, args):
fields['tenant_id'] = args.project_id
try:
mc.metrics.create(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print('Successfully created metric')
@ -96,14 +95,10 @@ def do_metric_create(mc, args):
help='The raw JSON body in single quotes. See api doc.')
def do_metric_create_raw(mc, args):
'''Create metric from raw json body.'''
fields = {}
fields['jsonbody'] = args.jsonbody
try:
mc.metrics.create(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
mc.metrics.create(**args.jsonbody)
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print('Successfully created metric')
@ -136,17 +131,14 @@ def do_metric_name_list(mc, args):
try:
metric_names = mc.metrics.list_names(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
if args.json:
print(utils.json_formatter(metric_names))
return
if isinstance(metric_names, list):
utils.print_list(metric_names, ['Name'], formatters={'Name': lambda x: x['name']})
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
if args.json:
print(utils.json_formatter(metric_names))
return
if isinstance(metric_names, list):
utils.print_list(metric_names, ['Name'], formatters={'Name': lambda x: x['name']})
@utils.arg('--name', metavar='<METRIC_NAME>',
@ -190,10 +182,8 @@ def do_metric_list(mc, args):
try:
metric = mc.metrics.list(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
if args.json:
print(utils.json_formatter(metric))
@ -241,10 +231,8 @@ def do_dimension_name_list(mc, args):
try:
dimension_names = mc.metrics.list_dimension_names(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
if args.json:
print(utils.json_formatter(dimension_names))
@ -283,10 +271,8 @@ def do_dimension_value_list(mc, args):
try:
dimension_values = mc.metrics.list_dimension_values(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
if args.json:
print(utils.json_formatter(dimension_values))
@ -429,10 +415,8 @@ def do_measurement_list(mc, args):
try:
metric = mc.metrics.list_measurements(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
if args.json:
print(utils.json_formatter(metric))
@ -496,8 +480,8 @@ def do_metric_statistics(mc, args):
if stat.upper() not in statistic_types:
errmsg = ('Invalid type, not one of [' +
', '.join(statistic_types) + ']')
print(errmsg)
return
raise osc_exc.CommandError(errmsg)
fields = {}
fields['name'] = args.name
if args.dimensions:
@ -522,10 +506,8 @@ def do_metric_statistics(mc, args):
try:
metric = mc.metrics.list_statistics(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
if args.json:
print(utils.json_formatter(metric))
@ -598,10 +580,8 @@ def do_notification_create(mc, args):
fields['period'] = args.period
try:
notification = mc.notifications.create(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print(jsonutils.dumps(notification, indent=2))
@ -614,10 +594,8 @@ def do_notification_show(mc, args):
fields['notification_id'] = args.id
try:
notification = mc.notifications.get(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
if args.json:
print(utils.json_formatter(notification))
@ -665,9 +643,9 @@ def do_notification_list(mc, args):
try:
notification = mc.notifications.list(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
except osc_exc.ClientException as he:
raise osc_exc.CommandError(
'ClientException code=%s message=%s' %
(he.code, he.message))
else:
if args.json:
@ -701,10 +679,8 @@ def do_notification_delete(mc, args):
fields['notification_id'] = args.id
try:
mc.notifications.delete(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print('Successfully deleted notification')
@ -732,10 +708,8 @@ def do_notification_update(mc, args):
fields['period'] = args.period
try:
notification = mc.notifications.update(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print(jsonutils.dumps(notification, indent=2))
@ -768,10 +742,8 @@ def do_notification_patch(mc, args):
fields['period'] = args.period
try:
notification = mc.notifications.patch(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print(jsonutils.dumps(notification, indent=2))
@ -832,10 +804,8 @@ def do_alarm_definition_create(mc, args):
fields['match_by'] = args.match_by.split(',')
try:
alarm = mc.alarm_definitions.create(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print(jsonutils.dumps(alarm, indent=2))
@ -848,10 +818,8 @@ def do_alarm_definition_show(mc, args):
fields['alarm_id'] = args.id
try:
alarm = mc.alarm_definitions.get(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
if args.json:
print(utils.json_formatter(alarm))
@ -923,10 +891,8 @@ def do_alarm_definition_list(mc, args):
fields['offset'] = args.offset
try:
alarm = mc.alarm_definitions.list(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
if args.json:
print(utils.json_formatter(alarm))
@ -957,10 +923,8 @@ def do_alarm_definition_delete(mc, args):
fields['alarm_id'] = args.id
try:
mc.alarm_definitions.delete(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print('Successfully deleted alarm definition')
@ -1014,10 +978,8 @@ def do_alarm_definition_update(mc, args):
fields['severity'] = args.severity
try:
alarm = mc.alarm_definitions.update(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print(jsonutils.dumps(alarm, indent=2))
@ -1075,10 +1037,8 @@ def do_alarm_definition_patch(mc, args):
fields['severity'] = args.severity
try:
alarm = mc.alarm_definitions.patch(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print(jsonutils.dumps(alarm, indent=2))
@ -1159,10 +1119,8 @@ def do_alarm_list(mc, args):
fields['sort_by'] = args.sort_by
try:
alarm = mc.alarms.list(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
if args.json:
print(utils.json_formatter(alarm))
@ -1202,10 +1160,8 @@ def do_alarm_show(mc, args):
fields['alarm_id'] = args.id
try:
alarm = mc.alarms.get(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
if args.json:
print(utils.json_formatter(alarm))
@ -1243,10 +1199,8 @@ def do_alarm_update(mc, args):
fields['link'] = args.link
try:
alarm = mc.alarms.update(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print(jsonutils.dumps(alarm, indent=2))
@ -1276,10 +1230,8 @@ def do_alarm_patch(mc, args):
fields['link'] = args.link
try:
alarm = mc.alarms.patch(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print(jsonutils.dumps(alarm, indent=2))
@ -1292,10 +1244,8 @@ def do_alarm_delete(mc, args):
fields['alarm_id'] = args.id
try:
mc.alarms.delete(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
print('Successfully deleted alarm')
@ -1397,17 +1347,15 @@ def do_alarm_count(mc, args):
fields['offset'] = args.offset
try:
counts = mc.alarms.count(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
if args.json:
print(utils.json_formatter(counts))
return
cols = counts['columns']
utils.print_list(counts['counts'], [i for i in xrange(len(cols))],
utils.print_list(counts['counts'], [i for i in range(len(cols))],
field_labels=cols)
@ -1427,10 +1375,8 @@ def do_alarm_history(mc, args):
fields['offset'] = args.offset
try:
alarm = mc.alarms.history(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
output_alarm_history(args, alarm)
@ -1466,10 +1412,8 @@ def do_alarm_history_list(mc, args):
fields['offset'] = args.offset
try:
alarm = mc.alarms.history_list(**fields)
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
output_alarm_history(args, alarm)
@ -1479,10 +1423,8 @@ def do_notification_type_list(mc, args):
try:
notification_types = mc.notificationtypes.list()
except exc.HTTPException as he:
raise exc.CommandError(
'HTTPException code=%s message=%s' %
(he.code, he.message))
except (osc_exc.ClientException, k_exc.HttpError) as he:
raise osc_exc.CommandError('%s\n%s' % (he.message, he.details))
else:
if args.json:
print(utils.json_formatter(notification_types))

20
monascaclient/version.py Normal file
View File

@ -0,0 +1,20 @@
# Copyright 2017 FUJITSU LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pbr import version
__all__ = ['version_info', 'version_string']
version_info = version.VersionInfo('python-monascaclient')
version_string = version_info.version_string()

View File

@ -1,15 +1,16 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
osc-lib>=1.5.1 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
oslo.utils>=3.20.0 # Apache-2.0
python-keystoneclient>=3.8.0 # Apache-2.0
Babel!=2.4.0,>=2.3.4 # BSD
iso8601>=0.1.11 # MIT
pbr!=2.1.0,>=2.0.0 # Apache-2.0
PrettyTable<0.8,>=0.7.1 # BSD
PyYAML>=3.10.0 # MIT
requests>=2.14.2 # Apache-2.0
six>=1.9.0 # MIT

View File

@ -1,14 +1,12 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
bandit>=1.1.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
fixtures>=3.0.0 # Apache-2.0/BSD
mox3!=0.19.0,>=0.7.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0
os-testr>=0.8.0 # Apache-2.0
requests-mock>=1.1 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT

18
tox.ini
View File

@ -9,22 +9,18 @@ setenv =
BRANCH_NAME=master
CLIENT_NAME=python-monascaclient
OS_TEST_PATH=monascaclient/tests
passenv = http_proxy
HTTP_PROXY
https_proxy
HTTPS_PROXY
no_proxy
NO_PROXY
passenv = *_proxy
*_PROXY
usedevelop = True
install_command =
{toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
deps = -r{toxinidir}/test-requirements.txt
whitelist_externals = bash
find
rm
commands =
find . -type f -name "*.pyc" -delete
rm -Rf .testrepository/times.dbm
[testenv:py27]
basepython = python2.7
@ -58,15 +54,21 @@ commands =
oslo_debug_helper -t {env:OS_TEST_PATH} {posargs}
[testenv:pep8]
skip_install = True
usedevelop = False
commands =
{[testenv:flake8]commands}
{[testenv:bandit]commands}
[testenv:flake8]
skip_install = True
usedevelop = False
commands =
flake8 monascaclient
[testenv:bandit]
skip_install = True
usedevelop = False
commands =
bandit -r monascaclient -n5 -x {env:OS_TEST_PATH}