push initial version

Change-Id: Ifecc2c7dd6bd859ba6ef327fddd891982382df3b
This commit is contained in:
David TARDIVEL 2015-06-04 14:59:34 +02:00
parent d8ea04cf39
commit 3957331e52
77 changed files with 9844 additions and 0 deletions

56
.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
.testrepository
.venv
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Complexity
output/*.html
output/*/index.html
# Sphinx
doc/build
# pbr generates these
AUTHORS
ChangeLog
# Editors
*~
.*.swp
.*sw?
sftp-config.json
/.idea/

7
.testr.conf Normal file
View File

@ -0,0 +1,7 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-10} \
${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

16
CONTRIBUTING.rst Normal file
View File

@ -0,0 +1,16 @@
If you would like to contribute to the development of OpenStack,
you must follow the steps in this page:
http://docs.openstack.org/infra/manual/developers.html
Once those steps have been completed, changes to OpenStack
should be submitted for review via the Gerrit tool, following
the workflow documented at:
http://docs.openstack.org/infra/manual/developers.html#development-workflow
Pull requests submitted through GitHub will be ignored.
Bugs should be filed on Launchpad, not GitHub:
https://bugs.launchpad.net/python-watcherclient

4
HACKING.rst Normal file
View File

@ -0,0 +1,4 @@
python-watcherclient Style Commandments
===============================================
Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/

6
MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
include AUTHORS
include ChangeLog
exclude .gitignore
exclude .gitreview
global-exclude *.pyc

13
README.rst Normal file
View File

@ -0,0 +1,13 @@
===============================
python-watcherclient
===============================
Python client library for Watcher API
* Free software: Apache license
Features
--------
* TODO

2
babel.cfg Normal file
View File

@ -0,0 +1,2 @@
[python: **.py]

94
doc/source/api_v1.rst Normal file
View File

@ -0,0 +1,94 @@
.. _api_v1:
========================
watcherclient Python API
========================
The watcherclient python API lets you access watcher, the OpenStack
TODEFINE Service.
For example, to manipulate audits, you interact with an `watcherclient.v1.audit`_ object.
You obtain access to audits via attributes of the `watcherclient.v1.client.Client`_ object.
Usage
=====
Get a Client object
-------------------
First, create an `watcherclient.v1.client.Client`_ instance by passing your
credentials to `watcherclient.client.get_client()`_. By default, the
Watcher system is configured so that only administrators (users with
'admin' role) have access.
There are two different sets of credentials that can be used::
* watcher endpoint and auth token
* Identity Service (keystone) credentials
Using watcher endpoint and auth token
.....................................
An auth token and the watcher endpoint can be used to authenticate::
* os_auth_token: authentication token (from Identity Service)
* watcher_url: watcher API endpoint, eg http://watcher.example.org:9322/v1
To create the client, you can use the API like so::
>>> from watcherclient import client
>>>
>>> kwargs = {'os_auth_token': '3bcc3d3a03f44e3d8377f9247b0ad155'
>>> 'watcher_url': 'http://watcher.example.org:9322/'}
>>> watcher = client.get_client(1, **kwargs)
Using Identity Service (keystone) credentials
.............................................
These Identity Service credentials can be used to authenticate::
* os_username: name of user
* os_password: user's password
* os_auth_url: Identity Service endpoint for authorization
* os_tenant_{name|id}: name or ID of tenant
To create a client, you can use the API like so::
>>> from watcherclient import client
>>>
>>> kwargs = {'os_username': 'name',
>>> 'os_password': 'password',
>>> 'os_auth_url': 'http://keystone.example.org:5000/',
>>> 'os_tenant_name': 'tenant'}
>>> watcher = client.get_client(1, **kwargs)
Perform watcher operations
--------------------------
Once you have an watcher `Client`_, you can perform various tasks::
>>> watcher.action.list() # list of actions
>>> watcher.action_plan.list() # list of action_plan
>>> watcher.audit.get(audit_uuid) # information about a particular audit
When the `Client`_ needs to propagate an exception, it will usually
raise an instance subclassed from
`watcherclient.exc.BaseException`_ or `watcherclient.exc.ClientException`_.
Refer to the modules themselves, for more details.
=====================
watcherclient Modules
=====================
.. toctree::
:maxdepth: 1
modules <api/autoindex>
.. _watcherclient.v1.audit: api/watcherclient.v1.audit.html#watcherclient.v1.audit.Audit
.. _watcherclient.v1.client.Client: api/watcherclient.v1.client.html#watcherclient.v1.client.Client
.. _Client: api/watcherclient.v1.client.html#watcherclient.v1.client.Client
.. _watcherclient.client.get_client(): api/watcherclient.client.html#watcherclient.client.get_client
.. _watcherclient.exc.BaseException: api/watcherclient.exc.html#watcherclient.exc.BaseException
.. _watcherclient.exc.ClientException: api/watcherclient.exc.html#watcherclient.exc.ClientException

89
doc/source/cli.rst Normal file
View File

@ -0,0 +1,89 @@
==============================================
:program:`watcher` Command-Line Interface (CLI)
==============================================
.. program:: watcher
.. highlight:: bash
SYNOPSIS
========
:program:`watcher` [options] <command> [command-options]
:program:`watcher help`
:program:`watcher help` <command>
DESCRIPTION
===========
The :program:`watcher` command-line interface (CLI) interacts with the
OpenStack TODEFINE Service (Watcher).
In order to use the CLI, you must provide your OpenStack username, password,
project (historically called tenant), and auth endpoint. You can use
configuration options :option:`--os-username`, :option:`--os-password`,
:option:`--os-tenant-id` (or :option:`--os-tenant-name`),
and :option:`--os-auth-url`, or set the corresponding
environment variables::
export OS_USERNAME=user
export OS_PASSWORD=password
export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b # or OS_TENANT_NAME
export OS_TENANT_NAME=project # or OS_TENANT_ID
export OS_AUTH_URL=http://auth.example.com:5000/v2.0
The command-line tool will attempt to reauthenticate using the provided
credentials for every request. You can override this behavior by manually
supplying an auth token using :option:`--watcher-url` and
:option:`--os-auth-token`, or by setting the corresponding environment variables::
export WATCHER_URL=http://watcher.example.org:9322/
export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155
Since Keystone can return multiple regions in the Service Catalog, you can
specify the one you want with :option:`--os-region-name` or set the following
environment variable. (It defaults to the first in the list returned.)
::
export OS_REGION_NAME=region
Watcher CLI supports bash completion. The command-line tool can automatically
fill partially typed commands. To use this feature, source the below file
(available at
https://git.openstack.org/cgit/openstack/python-watcherclient/tree/tools/watcher.bash_completion)
to your terminal and then bash completion should work::
source watcher.bash_completion
To avoid doing this every time, add this to your ``.bashrc`` or copy the
watcher.bash_completion file to the default bash completion scripts directory
on your linux distribution.
OPTIONS
=======
To get a list of available (sub)commands and options, run::
watcher help
To get usage and options of a command, run::
watcher help <command>
EXAMPLES
========
Get information about the audit-create command::
watcher help audit-create
Get a list of available goal::
watcher goal-list
Get a list of audits::
watcher audit-list

89
doc/source/conf.py Normal file
View File

@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
# 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 watcherclient import version as watcherclient_version
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'oslosphinx',
]
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
# execute "export SPHINX_DEBUG=1" in your terminal to disable
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'python-watcherclient'
copyright = u'OpenStack Foundation'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
# The full version, including alpha/beta/rc tags.
release = watcherclient_version.version_info.release_string()
# The short X.Y version.
version = watcherclient_version.version_info.version_string()
# A list of ignored prefixes for module index sorting.
modindex_common_prefix = ['watcherclient.']
# If true, '()' will be appended to :func: etc. cross-reference text.
add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
add_module_names = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
# html_theme_path = ["."]
# html_theme = '_theme'
# html_static_path = ['_static']
html_theme_options = {'incubating': True}
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
(
'index',
'%s.tex' % project,
u'%s Documentation' % project,
u'OpenStack Foundation', 'manual'
),
]

View File

@ -0,0 +1,54 @@
.. _contributing:
===================================
Contributing to python-watcherclient
===================================
If you're interested in contributing to the python-watcherclient project,
the following will help get you started.
Contributor License Agreement
-----------------------------
.. index::
single: license; agreement
In order to contribute to the python-watcherclient project, you need to have
signed OpenStack's contributor's agreement.
.. seealso::
* http://docs.openstack.org/infra/manual/developers.html
* http://wiki.openstack.org/CLA
LaunchPad Project
-----------------
Most of the tools used for OpenStack depend on a launchpad.net ID for
authentication. After signing up for a launchpad account, join the
"openstack" team to have access to the mailing list and receive
notifications of important events.
.. seealso::
* http://launchpad.net
* http://launchpad.net/python-watcherclient
* http://launchpad.net/~openstack
Project Hosting Details
-------------------------
Bug tracker
http://launchpad.net/python-watcherclient
Mailing list (prefix subjects with ``[watcher]`` for faster responses)
http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev
Code Hosting
https://git.openstack.org/cgit/openstack/python-watcherclient
Code Review
https://review.openstack.org/#/q/status:open+project:openstack/python-watcherclient,n,z

50
doc/source/index.rst Normal file
View File

@ -0,0 +1,50 @@
Python bindings to the OpenStack Watcher API
============================================
This is a client for OpenStack Watcher API. There's :doc:`a Python API
<api_v1>` (the :mod:`watcherclient` modules), and a :doc:`command-line script
<cli>` (installed as :program:`watcher`). Each implements the entire
OpenStack Watcher API.
You'll need credentials for an OpenStack cloud in order to use the watcher client.
Contents:
.. toctree::
:maxdepth: 1
readme
installation
api_v1
cli
contributing
Contributing
============
Code is hosted at `git.openstack.org`_. Submit bugs to the Watcher project on
`Launchpad`_. Submit code to the openstack/python-watcherclient project using
`Gerrit`_.
.. _git.openstack.org: https://git.openstack.org/cgit/openstack/python-watcherclient
.. _Launchpad: https://launchpad.net/watcher
.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow
Testing
-------
The preferred way to run the unit tests is using ``tox``.
See `Consistent Testing Interface`_ for more details.
.. _Consistent Testing Interface: http://git.openstack.org/cgit/openstack/governance/tree/reference/project-testing-interface.rst
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. _Watcher: https://wiki.openstack.org/wiki/Watcher

View File

@ -0,0 +1,10 @@
============
Installation
============
Or, if you have `virtualenvwrapper <https://virtualenvwrapper.readthedocs.org/en/latest/install.html>`_ installed::
$ mkvirtualenv python-watcherclient
$ git clone https://git.openstack.org/openstack/stackforge/python-watcherclient
$ cd python-watcherclient && python setup.py install
$ pip install -r ./requirements.txt

1
doc/source/readme.rst Normal file
View File

@ -0,0 +1 @@
.. include:: ../../README.rst

10
openstack-common.conf Normal file
View File

@ -0,0 +1,10 @@
[DEFAULT]
# The list of modules to copy from oslo-incubator.git
module=apiclient
module=cliutils
module=_i18n
# The base module to hold the copy of openstack.common
base=watcherclient

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
# 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.
argparse
pbr>=0.6,!=0.7,<1.0
Babel>=1.3
oslo.i18n
python-keystoneclient>=0.11.1
six>=1.7.0

37
setup.cfg Normal file
View File

@ -0,0 +1,37 @@
[metadata]
name = python-watcherclient
summary = Python client library for Watcher API
description-file =
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://www.openstack.org/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 2.6
Programming Language :: Python :: 3
Programming Language :: Python :: 3.3
Programming Language :: Python :: 3.4
[files]
packages =
watcherclient
[entry_points]
console_scripts =
watcher = watcherclient.shell:main
[pbr]
autodoc_index_modules = True
[build_sphinx]
source-dir = doc/source
build-dir = doc/build
all_files = 1

30
setup.py Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env python
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr'],
pbr=True)

18
test-requirements.txt Normal file
View File

@ -0,0 +1,18 @@
# 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.11,>=0.10.0
coverage>=3.6
discover
python-subunit>=0.0.18
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
oslosphinx>=2.2.0 # Apache-2.0
oslotest>=1.2.0 # Apache-2.0
testrepository>=0.0.18
testscenarios>=0.4
testtools>=0.9.36,!=1.2.0
mock>=1.0
# httpretty>=0.8.4,!=0.8.7
httpretty>=0.8.8

View File

@ -0,0 +1,27 @@
_watcher_opts="" # lazy init
_watcher_flags="" # lazy init
_watcher_opts_exp="" # lazy init
_watcher()
{
local cur prev nbc cflags
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
if [ "x$_watcher_opts" == "x" ] ; then
nbc="`watcher bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`"
_watcher_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`"
_watcher_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`"
_watcher_opts_exp="`echo "$_watcher_opts" | tr ' ' '|'`"
fi
if [[ " ${COMP_WORDS[@]} " =~ " "($_watcher_opts_exp)" " && "$prev" != "help" ]] ; then
COMPLETION_CACHE=$HOME/.cache/python-watcherclient/*/*-cache
cflags="$_watcher_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ')
COMPREPLY=($(compgen -W "${cflags}" -- ${cur}))
else
COMPREPLY=($(compgen -W "${_watcher_opts}" -- ${cur}))
fi
return 0
}
complete -F _watcher watcher

35
tox.ini Normal file
View File

@ -0,0 +1,35 @@
[tox]
minversion = 1.6
envlist = py33,py34,py26,py27,pep8
skipsdist = True
[testenv]
usedevelop = True
install_command = pip install -U {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = python setup.py testr --slowest --testr-args='{posargs}'
[testenv:pep8]
commands = flake8
[testenv:venv]
commands = {posargs}
[testenv:cover]
commands = python setup.py testr --coverage --testr-args='{posargs}'
[testenv:docs]
commands = python setup.py build_sphinx
[testenv:debug]
commands = oslo_debug_helper {posargs}
[flake8]
# E123, E125 skipped as they are invalid PEP-8.
show-source = True
ignore = E123,E125
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build

26
watcherclient/__init__.py Normal file
View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import pbr.version
from watcherclient import client
from watcherclient import exceptions
__version__ = pbr.version.VersionInfo(
'python-watcherclient').version_string()
__all__ = ['client', 'exceptions', ]

123
watcherclient/client.py Normal file
View File

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from keystoneclient.v2_0 import client as ksclient
from watcherclient.common import utils
from watcherclient import exceptions as exc
from watcherclient.openstack.common._i18n import _
from watcherclient.openstack.common import gettextutils
gettextutils.install('watcherclient')
def _get_ksclient(**kwargs):
"""Get an endpoint and auth token from Keystone.
:param kwargs: keyword args containing credentials:
* username: name of user
* password: user's password
* auth_url: endpoint to authenticate against
* insecure: allow insecure SSL (no cert verification)
* tenant_{name|id}: name or ID of tenant
"""
return ksclient.Client(username=kwargs.get('username'),
password=kwargs.get('password'),
tenant_id=kwargs.get('tenant_id'),
tenant_name=kwargs.get('tenant_name'),
auth_url=kwargs.get('auth_url'),
insecure=kwargs.get('insecure'))
def _get_endpoint(client, **kwargs):
"""Get an endpoint using the provided keystone client."""
attr = None
filter_value = None
if kwargs.get('region_name'):
attr = 'region'
filter_value = kwargs.get('region_name')
return client.service_catalog.url_for(
service_type=kwargs.get('service_type') or 'infra-optim',
attr=attr,
filter_value=filter_value,
endpoint_type=kwargs.get('endpoint_type') or 'publicURL')
def get_client(api_version, **kwargs):
"""Get an authenticated client, based on the credentials in args.
:param api_version: the API version to use. Valid value: '1'.
:param kwargs: keyword args containing credentials, either:
* os_auth_token: pre-existing token to re-use
* watcher_url: watcher API endpoint
or:
* os_username: name of user
* os_password: user's password
* os_auth_url: endpoint to authenticate against
* insecure: allow insecure SSL (no cert verification)
* os_tenant_{name|id}: name or ID of tenant
"""
if kwargs.get('os_auth_token') and kwargs.get('watcher_url'):
token = kwargs.get('os_auth_token')
endpoint = kwargs.get('watcher_url')
auth_ref = None
elif (kwargs.get('os_username') and
kwargs.get('os_password') and
kwargs.get('os_auth_url') and
(kwargs.get('os_tenant_id') or kwargs.get('os_tenant_name'))):
ks_kwargs = {
'username': kwargs.get('os_username'),
'password': kwargs.get('os_password'),
'tenant_id': kwargs.get('os_tenant_id'),
'tenant_name': kwargs.get('os_tenant_name'),
'auth_url': kwargs.get('os_auth_url'),
'service_type': kwargs.get('os_service_type'),
'endpoint_type': kwargs.get('os_endpoint_type'),
'insecure': kwargs.get('insecure'),
}
_ksclient = _get_ksclient(**ks_kwargs)
token = (kwargs.get('os_auth_token')
if kwargs.get('os_auth_token')
else _ksclient.auth_token)
ks_kwargs['region_name'] = kwargs.get('os_region_name')
endpoint = (kwargs.get('watcher_url') or
_get_endpoint(_ksclient, **ks_kwargs))
auth_ref = _ksclient.auth_ref
else:
e = (_('Must provide Keystone credentials or user-defined endpoint '
'and token'))
raise exc.AmbiguousAuthSystem(e)
cli_kwargs = {
'token': token,
'insecure': kwargs.get('insecure'),
'timeout': kwargs.get('timeout'),
'ca_file': kwargs.get('ca_file'),
'cert_file': kwargs.get('cert_file'),
'key_file': kwargs.get('key_file'),
'auth_ref': auth_ref,
}
return Client(api_version, endpoint, **cli_kwargs)
def Client(version, *args, **kwargs):
module = utils.import_versioned_module(version, 'client')
client_class = getattr(module, 'Client')
return client_class(*args, **kwargs)

View File

View File

@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
#
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Base utilities to build API operation managers and objects on top of.
"""
import copy
import six.moves.urllib.parse as urlparse
from watcherclient.openstack.common.apiclient import base
def getid(obj):
"""Wrapper to get object's ID.
Abstracts the common pattern of allowing both an object or an
object's ID (UUID) as a parameter when dealing with relationships.
"""
try:
return obj.id
except AttributeError:
return obj
class Manager(object):
"""Provides CRUD operations with a particular API."""
resource_class = None
def __init__(self, api):
self.api = api
def _create(self, url, body):
resp, body = self.api.json_request('POST', url, body=body)
if body:
return self.resource_class(self, body)
def _format_body_data(self, body, response_key):
if response_key:
try:
data = body[response_key]
except KeyError:
return []
else:
data = body
if not isinstance(data, list):
data = [data]
return data
def _list_pagination(self, url, response_key=None, obj_class=None,
limit=None):
"""Retrieve a list of items.
The Watcher API is configured to return a maximum number of
items per request, (see Watcher's api.max_limit option). This
iterates over the 'next' link (pagination) in the responses,
to get the number of items specified by 'limit'. If 'limit'
is None this function will continue pagination until there are
no more values to be returned.
:param url: a partial URL, e.g. '/nodes'
:param response_key: the key to be looked up in response
dictionary, e.g. 'nodes'
:param obj_class: class for constructing the returned objects.
:param limit: maximum number of items to return. If None returns
everything.
"""
if obj_class is None:
obj_class = self.resource_class
if limit is not None:
limit = int(limit)
object_list = []
object_count = 0
limit_reached = False
while url:
resp, body = self.api.json_request('GET', url)
data = self._format_body_data(body, response_key)
for obj in data:
object_list.append(obj_class(self, obj, loaded=True))
object_count += 1
if limit and object_count >= limit:
# break the for loop
limit_reached = True
break
# break the while loop and return
if limit_reached:
break
url = body.get('next')
if url:
# NOTE(lucasagomes): We need to edit the URL to remove
# the scheme and netloc
url_parts = list(urlparse.urlparse(url))
url_parts[0] = url_parts[1] = ''
url = urlparse.urlunparse(url_parts)
return object_list
def _list(self, url, response_key=None, obj_class=None, body=None):
resp, body = self.api.json_request('GET', url)
if obj_class is None:
obj_class = self.resource_class
data = self._format_body_data(body, response_key)
return [obj_class(self, res, loaded=True) for res in data if res]
def _update(self, url, body, method='PATCH', response_key=None):
resp, body = self.api.json_request(method, url, body=body)
# PATCH/PUT requests may not return a body
if body:
return self.resource_class(self, body)
def _delete(self, url):
self.api.raw_request('DELETE', url)
class Resource(base.Resource):
"""Represents a particular instance of an object (tenant, user, etc).
This is pretty much just a bag for attributes.
"""
def to_dict(self):
return copy.deepcopy(self._info)

View File

@ -0,0 +1,384 @@
# -*- coding: utf-8 -*-
#
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import json
import logging
import os
import socket
import ssl
from keystoneclient import adapter
import six
import six.moves.urllib.parse as urlparse
from watcherclient import exceptions as exc
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-watcherclient'
CHUNKSIZE = 1024 * 64 # 64kB
API_VERSION = '/v1'
def _trim_endpoint_api_version(url):
"""Trim API version and trailing slash from endpoint."""
return url.rstrip('/').rstrip(API_VERSION)
def _extract_error_json(body):
"""Return error_message from the HTTP response body."""
error_json = {}
try:
body_json = json.loads(body)
if 'error_message' in body_json:
raw_msg = body_json['error_message']
error_json = json.loads(raw_msg)
except ValueError:
pass
return error_json
class HTTPClient(object):
def __init__(self, endpoint, **kwargs):
self.endpoint = endpoint
self.endpoint_trimmed = _trim_endpoint_api_version(endpoint)
self.auth_token = kwargs.get('token')
self.auth_ref = kwargs.get('auth_ref')
self.connection_params = self.get_connection_params(endpoint, **kwargs)
@staticmethod
def get_connection_params(endpoint, **kwargs):
parts = urlparse.urlparse(endpoint)
path = _trim_endpoint_api_version(parts.path)
_args = (parts.hostname, parts.port, path)
_kwargs = {'timeout': (float(kwargs.get('timeout'))
if kwargs.get('timeout') else 600)}
if parts.scheme == 'https':
_class = VerifiedHTTPSConnection
_kwargs['ca_file'] = kwargs.get('ca_file', None)
_kwargs['cert_file'] = kwargs.get('cert_file', None)
_kwargs['key_file'] = kwargs.get('key_file', None)
_kwargs['insecure'] = kwargs.get('insecure', False)
elif parts.scheme == 'http':
_class = six.moves.http_client.HTTPConnection
else:
msg = 'Unsupported scheme: %s' % parts.scheme
raise exc.EndpointException(msg)
return (_class, _args, _kwargs)
def get_connection(self):
_class = self.connection_params[0]
try:
return _class(*self.connection_params[1][0:2],
**self.connection_params[2])
except six.moves.http_client.InvalidURL:
raise exc.EndpointException()
def log_curl_request(self, method, url, kwargs):
curl = ['curl -i -X %s' % method]
for (key, value) in kwargs['headers'].items():
header = '-H \'%s: %s\'' % (key, value)
curl.append(header)
conn_params_fmt = [
('key_file', '--key %s'),
('cert_file', '--cert %s'),
('ca_file', '--cacert %s'),
]
for (key, fmt) in conn_params_fmt:
value = self.connection_params[2].get(key)
if value:
curl.append(fmt % value)
if self.connection_params[2].get('insecure'):
curl.append('-k')
if 'body' in kwargs:
curl.append('-d \'%s\'' % kwargs['body'])
curl.append(urlparse.urljoin(self.endpoint_trimmed, url))
LOG.debug(' '.join(curl))
@staticmethod
def log_http_response(resp, body=None):
status = (resp.version / 10.0, resp.status, resp.reason)
dump = ['\nHTTP/%.1f %s %s' % status]
dump.extend(['%s: %s' % (k, v) for k, v in resp.getheaders()])
dump.append('')
if body:
dump.extend([body, ''])
LOG.debug('\n'.join(dump))
def _make_connection_url(self, url):
(_class, _args, _kwargs) = self.connection_params
base_url = _args[2]
return '%s/%s' % (base_url, url.lstrip('/'))
def _http_request(self, url, method, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around httplib.HTTP(S)Connection.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)
conn = self.get_connection()
try:
conn_url = self._make_connection_url(url)
conn.request(method, conn_url, **kwargs)
resp = conn.getresponse()
except socket.gaierror as e:
message = ("Error finding address for %(url)s: %(e)s"
% dict(url=url, e=e))
raise exc.EndpointNotFound(message)
except (socket.error, socket.timeout) as e:
endpoint = self.endpoint
message = ("Error communicating with %(endpoint)s %(e)s"
% dict(endpoint=endpoint, e=e))
raise exc.ConnectionRefused(message)
body_iter = ResponseBodyIterator(resp)
# Read body into string if it isn't obviously image data
body_str = None
if resp.getheader('content-type', None) != 'application/octet-stream':
body_str = ''.join([chunk for chunk in body_iter])
self.log_http_response(resp, body_str)
body_iter = six.StringIO(body_str)
else:
self.log_http_response(resp)
if 400 <= resp.status < 600:
LOG.warn("Request returned failure status.")
error_json = _extract_error_json(body_str)
raise exc.from_response(
resp, error_json.get('faultstring'),
error_json.get('debuginfo'), method, url)
elif resp.status in (301, 302, 305):
# Redirected. Reissue the request to the new location.
return self._http_request(resp['location'], method, **kwargs)
elif resp.status == 300:
raise exc.from_response(resp, method=method, url=url)
return resp, body_iter
def json_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type', 'application/json')
kwargs['headers'].setdefault('Accept', 'application/json')
if 'body' in kwargs:
kwargs['body'] = json.dumps(kwargs['body'])
resp, body_iter = self._http_request(url, method, **kwargs)
content_type = resp.getheader('content-type', None)
if resp.status == 204 or resp.status == 205 or content_type is None:
return resp, list()
if 'application/json' in content_type:
body = ''.join([chunk for chunk in body_iter])
try:
body = json.loads(body)
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)
class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection):
"""httplib-compatibile connection using client-side SSL authentication
:see http://code.activestate.com/recipes/
577548-https-httplib-client-connection-with-certificate-v/
"""
def __init__(self, host, port, key_file=None, cert_file=None,
ca_file=None, timeout=None, insecure=False):
six.moves.http_client.HTTPSConnection.__init__(
self, host, port,
key_file=key_file,
cert_file=cert_file)
self.key_file = key_file
self.cert_file = cert_file
if ca_file is not None:
self.ca_file = ca_file
else:
self.ca_file = self.get_system_ca_file()
self.timeout = timeout
self.insecure = insecure
def connect(self):
"""Connect to a host on a given (SSL) port.
If ca_file is pointing somewhere, use it to check Server Certificate.
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
ssl.wrap_socket(), which forces SSL to check server certificate against
our client certificate.
"""
sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host:
self.sock = sock
self._tunnel()
if self.insecure is True:
kwargs = {'cert_reqs': ssl.CERT_NONE}
else:
kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file}
if self.cert_file:
kwargs['certfile'] = self.cert_file
if self.key_file:
kwargs['keyfile'] = self.key_file
self.sock = ssl.wrap_socket(sock, **kwargs)
@staticmethod
def get_system_ca_file():
"""Return path to system default CA file."""
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
# Suse, FreeBSD/OpenBSD
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/ca-bundle.pem',
'/etc/ssl/cert.pem']
for ca in ca_path:
if os.path.exists(ca):
return ca
return None
class SessionClient(adapter.LegacyJsonAdapter):
"""HTTP client based on Keystone client session."""
def _http_request(self, url, method, **kwargs):
kwargs.setdefault('user_agent', USER_AGENT)
kwargs.setdefault('auth', self.auth)
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
endpoint_filter.setdefault('interface', self.interface)
endpoint_filter.setdefault('service_type', self.service_type)
endpoint_filter.setdefault('region_name', self.region_name)
resp = self.session.request(url, method,
raise_exc=False, **kwargs)
if 400 <= resp.status_code < 600:
error_json = _extract_error_json(resp.content)
raise exc.from_response(resp, error_json.get(
'faultstring'),
error_json.get('debuginfo'), method, url)
elif resp.status_code in (301, 302, 305):
# Redirected. Reissue the request to the new location.
location = resp.headers.get('location')
resp = self._http_request(location, method, **kwargs)
elif resp.status_code == 300:
raise exc.from_response(resp, method=method, url=url)
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 'body' in kwargs:
kwargs['data'] = json.dumps(kwargs.pop('body'))
resp = self._http_request(url, method, **kwargs)
body = resp.content
content_type = resp.headers.get('content-type', None)
status = resp.status_code
if status == 204 or status == 205 or content_type is None:
return resp, list()
if 'application/json' in 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)
class ResponseBodyIterator(object):
"""A class that acts as an iterator over an HTTP response."""
def __init__(self, resp):
self.resp = resp
def __iter__(self):
while True:
yield self.next()
def next(self):
chunk = self.resp.read(CHUNKSIZE)
if chunk:
return chunk
else:
raise StopIteration()
def _construct_http_client(*args, **kwargs):
session = kwargs.pop('session', None)
auth = kwargs.pop('auth', None)
if session:
service_type = kwargs.pop('service_type', 'infra-optim')
interface = kwargs.pop('endpoint_type', None)
region_name = kwargs.pop('region_name', None)
return SessionClient(session=session,
auth=auth,
interface=interface,
service_type=service_type,
region_name=region_name,
service_name=None,
user_agent='python-watcherclient')
else:
return HTTPClient(*args, **kwargs)

View File

@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
#
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import argparse
import json
from watcherclient import exceptions as exc
from watcherclient.openstack.common._i18n import _
from watcherclient.openstack.common import importutils
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)
def define_command(subparsers, command, callback, cmd_mapper):
'''Define a command in the subparsers collection.
:param subparsers: subparsers collection where the command will go
:param command: command name
:param callback: function that will be used to process the command
'''
desc = callback.__doc__ or ''
help = desc.strip().split('\n')[0]
arguments = getattr(callback, 'arguments', [])
subparser = subparsers.add_parser(command, help=help,
description=desc,
add_help=False,
formatter_class=HelpFormatter)
subparser.add_argument('-h', '--help', action='help',
help=argparse.SUPPRESS)
cmd_mapper[command] = subparser
for (args, kwargs) in arguments:
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=callback)
def define_commands_from_module(subparsers, command_module, cmd_mapper):
"""Add *do_* methods in a module and add as commands into a subparsers."""
for method_name in (a for a in dir(command_module) if a.startswith('do_')):
# Commands should be hypen-separated instead of underscores.
command = method_name[3:].replace('_', '-')
callback = getattr(command_module, method_name)
define_command(subparsers, command, callback, cmd_mapper)
def import_versioned_module(version, submodule=None):
module = 'watcherclient.v%s' % version
if submodule:
module = '.'.join((module, submodule))
return importutils.import_module(module)
def split_and_deserialize(string):
"""Split and try to JSON deserialize a string.
Gets a string with the KEY=VALUE format, split it (using '=' as the
separator) and try to JSON deserialize the VALUE.
:returns: A tuple of (key, value).
"""
try:
key, value = string.split("=", 1)
except ValueError:
raise exc.CommandError(_('Attributes must be a list of '
'PATH=VALUE not "%s"') % string)
try:
value = json.loads(value)
except ValueError:
pass
return (key, value)
def args_array_to_dict(kwargs, key_to_convert):
values_to_convert = kwargs.get(key_to_convert)
if values_to_convert:
kwargs[key_to_convert] = dict(split_and_deserialize(v)
for v in values_to_convert)
return kwargs
def args_array_to_patch(op, attributes):
patch = []
for attr in attributes:
# Sanitize
if not attr.startswith('/'):
attr = '/' + attr
if op in ['add', 'replace']:
path, value = split_and_deserialize(attr)
patch.append({'op': op, 'path': path, 'value': value})
elif op == "remove":
# For remove only the key is needed
patch.append({'op': op, 'path': attr})
else:
raise exc.CommandError(_('Unknown PATCH operation: %s') % op)
return patch
def common_params_for_list(args, fields, field_labels):
"""Generate 'params' dict that is common for every 'list' command.
:param args: arguments from command line.
:param fields: possible fields for sorting.
:param field_labels: possible field labels for sorting.
:returns: a dict with params to pass to the client method.
"""
params = {}
if args.limit is not None:
if args.limit < 0:
raise exc.CommandError(
_('Expected non-negative --limit, got %s') % args.limit)
params['limit'] = args.limit
if args.sort_key is not None:
# Support using both heading and field name for sort_key
fields_map = dict(zip(field_labels, fields))
fields_map.update(zip(fields, fields))
try:
sort_key = fields_map[args.sort_key]
except KeyError:
raise exc.CommandError(
_("%(sort_key)s is an invalid field for sorting, "
"valid values for --sort-key are: %(valid)s") %
{'sort_key': args.sort_key,
'valid': list(fields_map)})
params['sort_key'] = sort_key
if args.sort_dir is not None:
if args.sort_dir not in ('asc', 'desc'):
raise exc.CommandError(
_("%s is an invalid value for sort direction, "
"valid values for --sort-dir are: 'asc', 'desc'") %
args.sort_dir)
params['sort_dir'] = args.sort_dir
params['detail'] = args.detail
return params
def common_filters(limit=None, sort_key=None, sort_dir=None):
"""Generate common filters for any list request.
:param limit: maximum number of entities to return.
:param sort_key: field to use for sorting.
:param sort_dir: direction of sorting: 'asc' or 'desc'.
:returns: list of string filters.
"""
filters = []
if isinstance(limit, int) and limit > 0:
filters.append('limit=%s' % limit)
if sort_key is not None:
filters.append('sort_key=%s' % sort_key)
if sort_dir is not None:
filters.append('sort_dir=%s' % sort_dir)
return filters

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#
# 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 watcherclient.openstack.common.apiclient import exceptions
from watcherclient.openstack.common.apiclient.exceptions import * # noqa
# NOTE(akurilin): This alias is left here since v.0.1.3 to support backwards
# compatibility.
InvalidEndpoint = EndpointException
CommunicationError = ConnectionRefused
HTTPBadRequest = BadRequest
HTTPInternalServerError = InternalServerError
HTTPNotFound = NotFound
HTTPServiceUnavailable = ServiceUnavailable
class AmbiguousAuthSystem(ClientException):
"""Could not obtain token and endpoint using provided credentials."""
pass
# Alias for backwards compatibility
AmbigiousAuthSystem = AmbiguousAuthSystem
class InvalidAttribute(ClientException):
pass
def from_response(response, message=None, traceback=None, method=None,
url=None):
"""Return an HttpError instance based on response from httplib/requests."""
error_body = {}
if message:
error_body['message'] = message
if traceback:
error_body['details'] = traceback
if hasattr(response, 'status') and not hasattr(response, 'status_code'):
# NOTE(akurilin): These modifications around response object give
# ability to get all necessary information in method `from_response`
# from common code, which expecting response object from `requests`
# library instead of object from `httplib/httplib2` library.
response.status_code = response.status
response.headers = {
'Content-Type': response.getheader('content-type', "")}
if hasattr(response, 'status_code'):
# NOTE(jiangfei): These modifications allow SessionClient
# to handle faultstring.
response.json = lambda: {'error': error_body}
if (response.headers['Content-Type'].startswith('text/') and
not hasattr(response, 'text')):
# NOTE(clif_h): There seems to be a case in the
# openstack.common.apiclient.exceptions module where if the
# content-type of the response is text/* then it expects
# the response to have a 'text' attribute, but that
# doesn't always seem to necessarily be the case.
# This is to work around that problem.
response.text = ''
return exceptions.from_response(response, message, url)

View File

View File

@ -0,0 +1,45 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""oslo.i18n integration module.
See http://docs.openstack.org/developer/oslo.i18n/usage.html
"""
try:
import oslo_i18n
# NOTE(dhellmann): This reference to o-s-l-o will be replaced by the
# application name when this module is synced into the separate
# repository. It is OK to have more than one translation function
# using the same domain, since there will still only be one message
# catalog.
_translators = oslo_i18n.TranslatorFactory(domain='watcherclient')
# The primary translation function using the well-known name "_"
_ = _translators.primary
# Translators for log levels.
#
# The abbreviated names are meant to reflect the usual use of a short
# name like '_'. The "L" is for "log" and the other letter comes from
# the level.
_LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error
_LC = _translators.log_critical
except ImportError:
# NOTE(dims): Support for cases where a project wants to use
# code from oslo-incubator, but is not ready to be internationalized
# (like tempest)
_ = _LI = _LW = _LE = _LC = lambda x: x

View File

@ -0,0 +1,234 @@
# Copyright 2013 OpenStack Foundation
# Copyright 2013 Spanish National Research Council.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-watcherclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
import abc
import argparse
import os
import six
from stevedore import extension
from watcherclient.openstack.common.apiclient import exceptions
_discovered_plugins = {}
def discover_auth_systems():
"""Discover the available auth-systems.
This won't take into account the old style auth-systems.
"""
global _discovered_plugins
_discovered_plugins = {}
def add_plugin(ext):
_discovered_plugins[ext.name] = ext.plugin
ep_namespace = "watcherclient.openstack.common.apiclient.auth"
mgr = extension.ExtensionManager(ep_namespace)
mgr.map(add_plugin)
def load_auth_system_opts(parser):
"""Load options needed by the available auth-systems into a parser.
This function will try to populate the parser with options from the
available plugins.
"""
group = parser.add_argument_group("Common auth options")
BaseAuthPlugin.add_common_opts(group)
for name, auth_plugin in six.iteritems(_discovered_plugins):
group = parser.add_argument_group(
"Auth-system '%s' options" % name,
conflict_handler="resolve")
auth_plugin.add_opts(group)
def load_plugin(auth_system):
try:
plugin_class = _discovered_plugins[auth_system]
except KeyError:
raise exceptions.AuthSystemNotFound(auth_system)
return plugin_class(auth_system=auth_system)
def load_plugin_from_args(args):
"""Load required plugin and populate it with options.
Try to guess auth system if it is not specified. Systems are tried in
alphabetical order.
:type args: argparse.Namespace
:raises: AuthPluginOptionsMissing
"""
auth_system = args.os_auth_system
if auth_system:
plugin = load_plugin(auth_system)
plugin.parse_opts(args)
plugin.sufficient_options()
return plugin
for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
plugin_class = _discovered_plugins[plugin_auth_system]
plugin = plugin_class()
plugin.parse_opts(args)
try:
plugin.sufficient_options()
except exceptions.AuthPluginOptionsMissing:
continue
return plugin
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
@six.add_metaclass(abc.ABCMeta)
class BaseAuthPlugin(object):
"""Base class for authentication plugins.
An authentication plugin needs to override at least the authenticate
method to be a valid plugin.
"""
auth_system = None
opt_names = []
common_opt_names = [
"auth_system",
"username",
"password",
"tenant_name",
"token",
"auth_url",
]
def __init__(self, auth_system=None, **kwargs):
self.auth_system = auth_system or self.auth_system
self.opts = dict((name, kwargs.get(name))
for name in self.opt_names)
@staticmethod
def _parser_add_opt(parser, opt):
"""Add an option to parser in two variants.
:param opt: option name (with underscores)
"""
dashed_opt = opt.replace("_", "-")
env_var = "OS_%s" % opt.upper()
arg_default = os.environ.get(env_var, "")
arg_help = "Defaults to env[%s]." % env_var
parser.add_argument(
"--os-%s" % dashed_opt,
metavar="<%s>" % dashed_opt,
default=arg_default,
help=arg_help)
parser.add_argument(
"--os_%s" % opt,
metavar="<%s>" % dashed_opt,
help=argparse.SUPPRESS)
@classmethod
def add_opts(cls, parser):
"""Populate the parser with the options for this plugin.
"""
for opt in cls.opt_names:
# use `BaseAuthPlugin.common_opt_names` since it is never
# changed in child classes
if opt not in BaseAuthPlugin.common_opt_names:
cls._parser_add_opt(parser, opt)
@classmethod
def add_common_opts(cls, parser):
"""Add options that are common for several plugins.
"""
for opt in cls.common_opt_names:
cls._parser_add_opt(parser, opt)
@staticmethod
def get_opt(opt_name, args):
"""Return option name and value.
:param opt_name: name of the option, e.g., "username"
:param args: parsed arguments
"""
return (opt_name, getattr(args, "os_%s" % opt_name, None))
def parse_opts(self, args):
"""Parse the actual auth-system options if any.
This method is expected to populate the attribute `self.opts` with a
dict containing the options and values needed to make authentication.
"""
self.opts.update(dict(self.get_opt(opt_name, args)
for opt_name in self.opt_names))
def authenticate(self, http_client):
"""Authenticate using plugin defined method.
The method usually analyses `self.opts` and performs
a request to authentication server.
:param http_client: client object that needs authentication
:type http_client: HTTPClient
:raises: AuthorizationFailure
"""
self.sufficient_options()
self._do_authenticate(http_client)
@abc.abstractmethod
def _do_authenticate(self, http_client):
"""Protected method for authentication.
"""
def sufficient_options(self):
"""Check if all required options are present.
:raises: AuthPluginOptionsMissing
"""
missing = [opt
for opt in self.opt_names
if not self.opts.get(opt)]
if missing:
raise exceptions.AuthPluginOptionsMissing(missing)
@abc.abstractmethod
def token_and_endpoint(self, endpoint_type, service_type):
"""Return token and endpoint.
:param service_type: Service type of the endpoint
:type service_type: string
:param endpoint_type: Type of endpoint.
Possible values: public or publicURL,
internal or internalURL,
admin or adminURL
:type endpoint_type: string
:returns: tuple of token and endpoint strings
:raises: EndpointException
"""

View File

@ -0,0 +1,532 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2012 Grid Dynamics
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Base utilities to build API operation managers and objects on top of.
"""
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-watcherclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
# E1102: %s is not callable
# pylint: disable=E1102
import abc
import copy
from oslo_utils import strutils
import six
from six.moves.urllib import parse
from watcherclient.openstack.common._i18n import _
from watcherclient.openstack.common.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=None, obj_class=None, json=None):
"""List the collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'. If response_key is None - all response body
will be used.
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
"""
if json:
body = self.client.post(url, json=json).json()
else:
body = self.client.get(url).json()
if obj_class is None:
obj_class = self.resource_class
data = body[response_key] if response_key is not None else body
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
try:
data = data['values']
except (KeyError, TypeError):
pass
return [obj_class(self, res, loaded=True) for res in data if res]
def _get(self, url, response_key=None):
"""Get an object from collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'. If response_key is None - all response body
will be used.
"""
body = self.client.get(url).json()
data = body[response_key] if response_key is not None else body
return self.resource_class(self, data, loaded=True)
def _head(self, url):
"""Retrieve request headers for an object.
:param url: a partial URL, e.g., '/servers'
"""
resp = self.client.head(url)
return resp.status_code == 204
def _post(self, url, json, response_key=None, return_raw=False):
"""Create an object.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'. If response_key is None - all response body
will be used.
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
body = self.client.post(url, json=json).json()
data = body[response_key] if response_key is not None else body
if return_raw:
return data
return self.resource_class(self, data)
def _put(self, url, json=None, response_key=None):
"""Update an object with PUT method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'. If response_key is None - all response body
will be used.
"""
resp = self.client.put(url, json=json)
# PUT requests may not return a body
if resp.content:
body = resp.json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _patch(self, url, json=None, response_key=None):
"""Update an object with PATCH method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'. If response_key is None - all response body
will be used.
"""
body = self.client.patch(url, json=json).json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _delete(self, url):
"""Delete an object.
:param url: a partial URL, e.g., '/servers/my-server'
"""
return self.client.delete(url)
@six.add_metaclass(abc.ABCMeta)
class ManagerWithFind(BaseManager):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(msg)
elif num_matches > 1:
raise exceptions.NoUniqueMatch()
else:
return matches[0]
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class CrudManager(BaseManager):
"""Base manager class for manipulating entities.
Children of this class are expected to define a `collection_key` and `key`.
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
objects containing a list of member resources (e.g. `{'entities': [{},
{}, {}]}`).
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
refer to an individual member of the collection.
"""
collection_key = None
key = None
def build_url(self, base_url=None, **kwargs):
"""Builds a resource URL for the given kwargs.
Given an example collection where `collection_key = 'entities'` and
`key = 'entity'`, the following URL's could be generated.
By default, the URL will represent a collection of entities, e.g.::
/entities
If kwargs contains an `entity_id`, then the URL will represent a
specific member, e.g.::
/entities/{entity_id}
:param base_url: if provided, the generated URL will be appended to it
"""
url = base_url if base_url is not None else ''
url += '/%s' % self.collection_key
# do we have a specific entity?
entity_id = kwargs.get('%s_id' % self.key)
if entity_id is not None:
url += '/%s' % entity_id
return url
def _filter_kwargs(self, kwargs):
"""Drop null values and handle ids."""
for key, ref in six.iteritems(kwargs.copy()):
if ref is None:
kwargs.pop(key)
else:
if isinstance(ref, Resource):
kwargs.pop(key)
kwargs['%s_id' % key] = getid(ref)
return kwargs
def create(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs),
{self.key: kwargs},
self.key)
def get(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._get(
self.build_url(**kwargs),
self.key)
def head(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._head(self.build_url(**kwargs))
def list(self, base_url=None, **kwargs):
"""List the collection.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
def put(self, base_url=None, **kwargs):
"""Update an element.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._put(self.build_url(base_url=base_url, **kwargs))
def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
params.pop('%s_id' % self.key)
return self._patch(
self.build_url(**kwargs),
{self.key: params},
self.key)
def delete(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._delete(
self.build_url(**kwargs))
def find(self, base_url=None, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
rl = self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
num = len(rl)
if num == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(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.HUMAN_ID:
name = getattr(self, self.NAME_ATTR, None)
if name is not None:
return strutils.to_slug(name)
return None
def _add_details(self, info):
for (k, v) in six.iteritems(info):
try:
setattr(self, k, v)
self._info[k] = v
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __getattr__(self, k):
if k not in self.__dict__:
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def get(self):
"""Support for lazy loading details.
Some clients, such as novaclient have the option to lazy load the
details, details which can be loaded with this function.
"""
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
self._add_details(
{'x_request_id': self.manager.client.last_request_id})
def __eq__(self, other):
if not isinstance(other, Resource):
return NotImplemented
# two resources of different types are not equal
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val
def to_dict(self):
return copy.deepcopy(self._info)

View File

@ -0,0 +1,388 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2011 Piston Cloud Computing, Inc.
# Copyright 2013 Alessio Ababilov
# Copyright 2013 Grid Dynamics
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
OpenStack Client interface. Handles the REST calls and responses.
"""
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202
import hashlib
import logging
import time
try:
import simplejson as json
except ImportError:
import json
from oslo_utils import encodeutils
from oslo_utils import importutils
import requests
from watcherclient.openstack.common._i18n import _
from watcherclient.openstack.common.apiclient import exceptions
_logger = logging.getLogger(__name__)
SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',)
class HTTPClient(object):
"""This client handles sending HTTP requests to OpenStack servers.
Features:
- share authentication information between several clients to different
services (e.g., for compute and image clients);
- reissue authentication request for expired tokens;
- encode/decode JSON bodies;
- raise exceptions on HTTP errors;
- pluggable authentication;
- store authentication information in a keyring;
- store time spent for requests;
- register clients for particular services, so one can use
`http_client.identity` or `http_client.compute`;
- log requests and responses in a format that is easy to copy-and-paste
into terminal and send the same request with curl.
"""
user_agent = "watcherclient.openstack.common.apiclient"
def __init__(self,
auth_plugin,
region_name=None,
endpoint_type="publicURL",
original_ip=None,
verify=True,
cert=None,
timeout=None,
timings=False,
keyring_saver=None,
debug=False,
user_agent=None,
http=None):
self.auth_plugin = auth_plugin
self.endpoint_type = endpoint_type
self.region_name = region_name
self.original_ip = original_ip
self.timeout = timeout
self.verify = verify
self.cert = cert
self.keyring_saver = keyring_saver
self.debug = debug
self.user_agent = user_agent or self.user_agent
self.times = [] # [("item", starttime, endtime), ...]
self.timings = timings
# requests within the same session can reuse TCP connections from pool
self.http = http or requests.Session()
self.cached_token = None
self.last_request_id = None
def _safe_header(self, name, value):
if name in SENSITIVE_HEADERS:
# because in python3 byte string handling is ... ug
v = value.encode('utf-8')
h = hashlib.sha1(v)
d = h.hexdigest()
return encodeutils.safe_decode(name), "{SHA1}%s" % d
else:
return (encodeutils.safe_decode(name),
encodeutils.safe_decode(value))
def _http_log_req(self, method, url, kwargs):
if not self.debug:
return
string_parts = [
"curl -g -i",
"-X '%s'" % method,
"'%s'" % url,
]
for element in kwargs['headers']:
header = ("-H '%s: %s'" %
self._safe_header(element, kwargs['headers'][element]))
string_parts.append(header)
_logger.debug("REQ: %s" % " ".join(string_parts))
if 'data' in kwargs:
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
def _http_log_resp(self, resp):
if not self.debug:
return
_logger.debug(
"RESP: [%s] %s\n",
resp.status_code,
resp.headers)
if resp._content_consumed:
_logger.debug(
"RESP BODY: %s\n",
resp.text)
def serialize(self, kwargs):
if kwargs.get('json') is not None:
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['data'] = json.dumps(kwargs['json'])
try:
del kwargs['json']
except KeyError:
pass
def get_timings(self):
return self.times
def reset_timings(self):
self.times = []
def request(self, method, url, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around `requests.Session.request` to handle tasks such as
setting headers, JSON encoding/decoding, and error handling.
:param method: method of HTTP request
:param url: URL of HTTP request
:param kwargs: any other parameter that can be passed to
requests.Session.request (such as `headers`) or `json`
that will be encoded as JSON and used as `data` argument
"""
kwargs.setdefault("headers", {})
kwargs["headers"]["User-Agent"] = self.user_agent
if self.original_ip:
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
self.original_ip, self.user_agent)
if self.timeout is not None:
kwargs.setdefault("timeout", self.timeout)
kwargs.setdefault("verify", self.verify)
if self.cert is not None:
kwargs.setdefault("cert", self.cert)
self.serialize(kwargs)
self._http_log_req(method, url, kwargs)
if self.timings:
start_time = time.time()
resp = self.http.request(method, url, **kwargs)
if self.timings:
self.times.append(("%s %s" % (method, url),
start_time, time.time()))
self._http_log_resp(resp)
self.last_request_id = resp.headers.get('x-openstack-request-id')
if resp.status_code >= 400:
_logger.debug(
"Request returned failure status: %s",
resp.status_code)
raise exceptions.from_response(resp, method, url)
return resp
@staticmethod
def concat_url(endpoint, url):
"""Concatenate endpoint and final URL.
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
"http://keystone/v2.0/tokens".
:param endpoint: the base URL
:param url: the final URL
"""
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
def client_request(self, client, method, url, **kwargs):
"""Send an http request using `client`'s endpoint and specified `url`.
If request was rejected as unauthorized (possibly because the token is
expired), issue one authorization attempt and send the request once
again.
:param client: instance of BaseClient descendant
:param method: method of HTTP request
:param url: URL of HTTP request
:param kwargs: any other parameter that can be passed to
`HTTPClient.request`
"""
filter_args = {
"endpoint_type": client.endpoint_type or self.endpoint_type,
"service_type": client.service_type,
}
token, endpoint = (self.cached_token, client.cached_endpoint)
just_authenticated = False
if not (token and endpoint):
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
except exceptions.EndpointException:
pass
if not (token and endpoint):
self.authenticate()
just_authenticated = True
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
if not (token and endpoint):
raise exceptions.AuthorizationFailure(
_("Cannot find endpoint or token for request"))
old_token_endpoint = (token, endpoint)
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
self.cached_token = token
client.cached_endpoint = endpoint
# Perform the request once. If we get Unauthorized, then it
# might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail.
try:
return self.request(
method, self.concat_url(endpoint, url), **kwargs)
except exceptions.Unauthorized as unauth_ex:
if just_authenticated:
raise
self.cached_token = None
client.cached_endpoint = None
if self.auth_plugin.opts.get('token'):
self.auth_plugin.opts['token'] = None
if self.auth_plugin.opts.get('endpoint'):
self.auth_plugin.opts['endpoint'] = None
self.authenticate()
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
except exceptions.EndpointException:
raise unauth_ex
if (not (token and endpoint) or
old_token_endpoint == (token, endpoint)):
raise unauth_ex
self.cached_token = token
client.cached_endpoint = endpoint
kwargs["headers"]["X-Auth-Token"] = token
return self.request(
method, self.concat_url(endpoint, url), **kwargs)
def add_client(self, base_client_instance):
"""Add a new instance of :class:`BaseClient` descendant.
`self` will store a reference to `base_client_instance`.
Example:
>>> def test_clients():
... from keystoneclient.auth import keystone
... from openstack.common.apiclient import client
... auth = keystone.KeystoneAuthPlugin(
... username="user", password="pass", tenant_name="tenant",
... auth_url="http://auth:5000/v2.0")
... openstack_client = client.HTTPClient(auth)
... # create nova client
... from novaclient.v1_1 import client
... client.Client(openstack_client)
... # create keystone client
... from keystoneclient.v2_0 import client
... client.Client(openstack_client)
... # use them
... openstack_client.identity.tenants.list()
... openstack_client.compute.servers.list()
"""
service_type = base_client_instance.service_type
if service_type and not hasattr(self, service_type):
setattr(self, service_type, base_client_instance)
def authenticate(self):
self.auth_plugin.authenticate(self)
# Store the authentication results in the keyring for later requests
if self.keyring_saver:
self.keyring_saver.save(self)
class BaseClient(object):
"""Top-level object to access the OpenStack API.
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
will handle a bunch of issues such as authentication.
"""
service_type = None
endpoint_type = None # "publicURL" will be used
cached_endpoint = None
def __init__(self, http_client, extensions=None):
self.http_client = http_client
http_client.add_client(self)
# Add in any extensions...
if extensions:
for extension in extensions:
if extension.manager_class:
setattr(self, extension.name,
extension.manager_class(self))
def client_request(self, method, url, **kwargs):
return self.http_client.client_request(
self, method, url, **kwargs)
@property
def last_request_id(self):
return self.http_client.last_request_id
def head(self, url, **kwargs):
return self.client_request("HEAD", url, **kwargs)
def get(self, url, **kwargs):
return self.client_request("GET", url, **kwargs)
def post(self, url, **kwargs):
return self.client_request("POST", url, **kwargs)
def put(self, url, **kwargs):
return self.client_request("PUT", url, **kwargs)
def delete(self, url, **kwargs):
return self.client_request("DELETE", url, **kwargs)
def patch(self, url, **kwargs):
return self.client_request("PATCH", url, **kwargs)
@staticmethod
def get_class(api_name, version, version_map):
"""Returns the client class for the requested API version
:param api_name: the name of the API, e.g. 'compute', 'image', etc
:param version: the requested API version
:param version_map: a dict of client classes keyed by version
:rtype: a client class for the requested API version
"""
try:
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = _("Invalid %(api_name)s client version '%(version)s'. "
"Must be one of: %(version_map)s") % {
'api_name': api_name,
'version': version,
'version_map': ', '.join(version_map.keys())}
raise exceptions.UnsupportedVersion(msg)
return importutils.import_class(client_path)

View File

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

@ -0,0 +1,190 @@
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
A fake server that "responds" to API methods with pre-canned responses.
All of these responses come from the spec, so if for some reason the spec's
wrong the tests might raise AssertionError. I've indicated in comments the
places where actual behavior differs from the spec.
"""
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-watcherclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
# W0102: Dangerous default value %s as argument
# pylint: disable=W0102
import json
import requests
import six
from six.moves.urllib import parse
from watcherclient.openstack.common.apiclient import client
def assert_has_keys(dct, required=None, optional=None):
required = required or []
optional = optional or []
for k in required:
try:
assert k in dct
except AssertionError:
extra_keys = set(dct.keys()).difference(set(required + optional))
raise AssertionError("found unexpected keys: %s" %
list(extra_keys))
class TestResponse(requests.Response):
"""Wrap requests.Response and provide a convenient initialization.
"""
def __init__(self, data):
super(TestResponse, self).__init__()
self._content_consumed = True
if isinstance(data, dict):
self.status_code = data.get('status_code', 200)
# Fake the text attribute to streamline Response creation
text = data.get('text', "")
if isinstance(text, (dict, list)):
self._content = json.dumps(text)
default_headers = {
"Content-Type": "application/json",
}
else:
self._content = text
default_headers = {}
if six.PY3 and isinstance(self._content, six.string_types):
self._content = self._content.encode('utf-8', 'strict')
self.headers = data.get('headers') or default_headers
else:
self.status_code = data
def __eq__(self, other):
return (self.status_code == other.status_code and
self.headers == other.headers and
self._content == other._content)
class FakeHTTPClient(client.HTTPClient):
def __init__(self, *args, **kwargs):
self.callstack = []
self.fixtures = kwargs.pop("fixtures", None) or {}
if not args and "auth_plugin" not in kwargs:
args = (None, )
super(FakeHTTPClient, self).__init__(*args, **kwargs)
def assert_called(self, method, url, body=None, pos=-1):
"""Assert than an API method was just called.
"""
expected = (method, url)
called = self.callstack[pos][0:2]
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
assert expected == called, 'Expected %s %s; got %s %s' % \
(expected + called)
if body is not None:
if self.callstack[pos][3] != body:
raise AssertionError('%r != %r' %
(self.callstack[pos][3], body))
def assert_called_anytime(self, method, url, body=None):
"""Assert than an API method was called anytime in the test.
"""
expected = (method, url)
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
found = False
entry = None
for entry in self.callstack:
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s %s; got %s' % \
(method, url, self.callstack)
if body is not None:
assert entry[3] == body, "%s != %s" % (entry[3], body)
self.callstack = []
def clear_callstack(self):
self.callstack = []
def authenticate(self):
pass
def client_request(self, client, method, url, **kwargs):
# Check that certain things are called correctly
if method in ["GET", "DELETE"]:
assert "json" not in kwargs
# Note the call
self.callstack.append(
(method,
url,
kwargs.get("headers") or {},
kwargs.get("json") or kwargs.get("data")))
try:
fixture = self.fixtures[url][method]
except KeyError:
pass
else:
return TestResponse({"headers": fixture[0],
"text": fixture[1]})
# Call the method
args = parse.parse_qsl(parse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
raise AssertionError('Called unknown API method: %s %s, '
'expected fakes method name: %s' %
(method, url, callback))
resp = getattr(self, callback)(**kwargs)
if len(resp) == 3:
status, headers, body = resp
else:
status, body = resp
headers = {}
self.last_request_id = headers.get('x-openstack-request-id',
'req-test')
return TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})

View File

@ -0,0 +1,100 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-watcherclient-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
from oslo_utils import encodeutils
from oslo_utils import uuidutils
import six
from watcherclient.openstack.common._i18n import _
from watcherclient.openstack.common.apiclient import exceptions
def find_resource(manager, name_or_id, **find_args):
"""Look for resource in a given manager.
Used as a helper for the _find_* methods.
Example:
.. code-block:: python
def _find_hypervisor(cs, hypervisor):
#Get a hypervisor by name or ID.
return cliutils.find_resource(cs.hypervisors, hypervisor)
"""
# first try to get entity as integer id
try:
return manager.get(int(name_or_id))
except (TypeError, ValueError, exceptions.NotFound):
pass
# now try to get entity as uuid
try:
if six.PY2:
tmp_id = encodeutils.safe_encode(name_or_id)
else:
tmp_id = encodeutils.safe_decode(name_or_id)
if uuidutils.is_uuid_like(tmp_id):
return manager.get(tmp_id)
except (TypeError, ValueError, exceptions.NotFound):
pass
# for str id which is not uuid
if getattr(manager, 'is_alphanum_id_allowed', False):
try:
return manager.get(name_or_id)
except exceptions.NotFound:
pass
try:
try:
return manager.find(human_id=name_or_id, **find_args)
except exceptions.NotFound:
pass
# finally try to find entity by name
try:
resource = getattr(manager, 'resource_class', None)
name_attr = resource.NAME_ATTR if resource else 'name'
kwargs = {name_attr: name_or_id}
kwargs.update(find_args)
return manager.find(**kwargs)
except exceptions.NotFound:
msg = _("No %(name)s with a name or "
"ID of '%(name_or_id)s' exists.") % \
{
"name": manager.resource_class.__name__.lower(),
"name_or_id": name_or_id
}
raise exceptions.CommandError(msg)
except exceptions.NoUniqueMatch:
msg = _("Multiple %(name)s matches found for "
"'%(name_or_id)s', use an ID to be more specific.") % \
{
"name": manager.resource_class.__name__.lower(),
"name_or_id": name_or_id
}
raise exceptions.CommandError(msg)

View File

@ -0,0 +1,271 @@
# Copyright 2012 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# W0603: Using the global statement
# W0621: Redefining name %s from outer scope
# pylint: disable=W0603,W0621
from __future__ import print_function
import getpass
import inspect
import os
import sys
import textwrap
from oslo_utils import encodeutils
from oslo_utils import strutils
import prettytable
import six
from six import moves
from watcherclient.openstack.common._i18n import _
class MissingArgs(Exception):
"""Supplied arguments are not sufficient for calling a function."""
def __init__(self, missing):
self.missing = missing
msg = _("Missing arguments: %s") % ", ".join(missing)
super(MissingArgs, self).__init__(msg)
def validate_args(fn, *args, **kwargs):
"""Check that the supplied args are sufficient for calling a function.
>>> validate_args(lambda a: None)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): a
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): b, d
:param fn: the function to check
:param arg: the positional arguments supplied
:param kwargs: the keyword arguments supplied
"""
argspec = inspect.getargspec(fn)
num_defaults = len(argspec.defaults or [])
required_args = argspec.args[:len(argspec.args) - num_defaults]
def isbound(method):
return getattr(method, '__self__', None) is not None
if isbound(fn):
required_args.pop(0)
missing = [arg for arg in required_args if arg not in kwargs]
missing = missing[len(args):]
if missing:
raise MissingArgs(missing)
def arg(*args, **kwargs):
"""Decorator for CLI args.
Example:
>>> @arg("name", help="Name of the new entity")
... def entity_create(args):
... pass
"""
def _decorator(func):
add_arg(func, *args, **kwargs)
return func
return _decorator
def env(*args, **kwargs):
"""Returns the first environment variable set.
If all are empty, defaults to '' or keyword arg `default`.
"""
for arg in args:
value = os.environ.get(arg)
if value:
return value
return kwargs.get('default', '')
def add_arg(func, *args, **kwargs):
"""Bind CLI arguments to a shell.py `do_foo` function."""
if not hasattr(func, 'arguments'):
func.arguments = []
# NOTE(sirp): avoid dups that can occur when the module is shared across
# tests.
if (args, kwargs) not in func.arguments:
# Because of the semantics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.arguments.insert(0, (args, kwargs))
def unauthenticated(func):
"""Adds 'unauthenticated' attribute to decorated function.
Usage:
>>> @unauthenticated
... def mymethod(f):
... pass
"""
func.unauthenticated = True
return func
def isunauthenticated(func):
"""Checks if the function does not require authentication.
Mark such functions with the `@unauthenticated` decorator.
:returns: bool
"""
return getattr(func, 'unauthenticated', False)
def print_list(objs, fields, formatters=None, sortby_index=0,
mixed_case_fields=None, field_labels=None):
"""Print a list or objects as a table, one row per object.
:param objs: iterable of :class:`Resource`
:param fields: attributes that correspond to columns, in order
:param formatters: `dict` of callables for field formatting
:param sortby_index: index of the field for sorting table rows
:param mixed_case_fields: fields corresponding to object attributes that
have mixed case names (e.g., 'serverId')
:param field_labels: Labels to use in the heading of the table, default to
fields.
"""
formatters = formatters or {}
mixed_case_fields = mixed_case_fields or []
field_labels = field_labels or fields
if len(field_labels) != len(fields):
raise ValueError(_("Field labels list %(labels)s has different number "
"of elements than fields list %(fields)s"),
{'labels': field_labels, 'fields': fields})
if sortby_index is None:
kwargs = {}
else:
kwargs = {'sortby': field_labels[sortby_index]}
pt = prettytable.PrettyTable(field_labels)
pt.align = 'l'
for o in objs:
row = []
for field in fields:
if field in formatters:
row.append(formatters[field](o))
else:
if field in mixed_case_fields:
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_')
data = getattr(o, field_name, '')
row.append(data)
pt.add_row(row)
if six.PY3:
print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
else:
print(encodeutils.safe_encode(pt.get_string(**kwargs)))
def print_dict(dct, dict_property="Property", wrap=0):
"""Print a `dict` as a table of two columns.
:param dct: `dict` to print
:param dict_property: name of the first column
:param wrap: wrapping for the second column
"""
pt = prettytable.PrettyTable([dict_property, 'Value'])
pt.align = 'l'
for k, v in six.iteritems(dct):
# convert dict to str to check length
if isinstance(v, dict):
v = six.text_type(v)
if wrap > 0:
v = textwrap.fill(six.text_type(v), wrap)
# if value has a newline, add in multiple rows
# e.g. fault with stacktrace
if v and isinstance(v, six.string_types) and r'\n' in v:
lines = v.strip().split(r'\n')
col1 = k
for line in lines:
pt.add_row([col1, line])
col1 = ''
else:
pt.add_row([k, v])
if six.PY3:
print(encodeutils.safe_encode(pt.get_string()).decode())
else:
print(encodeutils.safe_encode(pt.get_string()))
def get_password(max_password_prompts=3):
"""Read password from TTY."""
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
pw = None
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
# Check for Ctrl-D
try:
for __ in moves.range(max_password_prompts):
pw1 = getpass.getpass("OS Password: ")
if verify:
pw2 = getpass.getpass("Please verify: ")
else:
pw2 = pw1
if pw1 == pw2 and pw1:
pw = pw1
break
except EOFError:
pass
return pw
def service_type(stype):
"""Adds 'service_type' attribute to decorated function.
Usage:
.. code-block:: python
@service_type('volume')
def mymethod(f):
...
"""
def inner(f):
f.service_type = stype
return f
return inner
def get_service_type(f):
"""Retrieves service type from function."""
return getattr(f, 'service_type', None)
def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
def exit(msg=''):
if msg:
print (msg, file=sys.stderr)
sys.exit(1)

View File

@ -0,0 +1,479 @@
# Copyright 2012 Red Hat, Inc.
# Copyright 2013 IBM Corp.
# 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.
"""
gettext for openstack-common modules.
Usual usage in an openstack.common module:
from watcherclient.openstack.common.gettextutils import _
"""
import copy
import gettext
import locale
from logging import handlers
import os
from babel import localedata
import six
_AVAILABLE_LANGUAGES = {}
# FIXME(dhellmann): Remove this when moving to oslo.i18n.
USE_LAZY = False
class TranslatorFactory(object):
"""Create translator functions
"""
def __init__(self, domain, localedir=None):
"""Establish a set of translation functions for the domain.
:param domain: Name of translation domain,
specifying a message catalog.
:type domain: str
:param lazy: Delays translation until a message is emitted.
Defaults to False.
:type lazy: Boolean
:param localedir: Directory with translation catalogs.
:type localedir: str
"""
self.domain = domain
if localedir is None:
localedir = os.environ.get(domain.upper() + '_LOCALEDIR')
self.localedir = localedir
def _make_translation_func(self, domain=None):
"""Return a new translation function ready for use.
Takes into account whether or not lazy translation is being
done.
The domain can be specified to override the default from the
factory, but the localedir from the factory is always used
because we assume the log-level translation catalogs are
installed in the same directory as the main application
catalog.
"""
if domain is None:
domain = self.domain
t = gettext.translation(domain,
localedir=self.localedir,
fallback=True)
# Use the appropriate method of the translation object based
# on the python version.
m = t.gettext if six.PY3 else t.ugettext
def f(msg):
"""oslo.i18n.gettextutils translation function."""
if USE_LAZY:
return Message(msg, domain=domain)
return m(msg)
return f
@property
def primary(self):
"The default translation function."
return self._make_translation_func()
def _make_log_translation_func(self, level):
return self._make_translation_func(self.domain + '-log-' + level)
@property
def log_info(self):
"Translate info-level log messages."
return self._make_log_translation_func('info')
@property
def log_warning(self):
"Translate warning-level log messages."
return self._make_log_translation_func('warning')
@property
def log_error(self):
"Translate error-level log messages."
return self._make_log_translation_func('error')
@property
def log_critical(self):
"Translate critical-level log messages."
return self._make_log_translation_func('critical')
# NOTE(dhellmann): When this module moves out of the incubator into
# oslo.i18n, these global variables can be moved to an integration
# module within each application.
# Create the global translation functions.
_translators = TranslatorFactory('watcherclient')
# The primary translation function using the well-known name "_"
_ = _translators.primary
# Translators for log levels.
#
# The abbreviated names are meant to reflect the usual use of a short
# name like '_'. The "L" is for "log" and the other letter comes from
# the level.
_LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error
_LC = _translators.log_critical
# NOTE(dhellmann): End of globals that will move to the application's
# integration module.
def enable_lazy():
"""Convenience function for configuring _() to use lazy gettext
Call this at the start of execution to enable the gettextutils._
function to use lazy gettext functionality. This is useful if
your project is importing _ directly instead of using the
gettextutils.install() way of importing the _ function.
"""
global USE_LAZY
USE_LAZY = True
def install(domain):
"""Install a _() function using the given translation domain.
Given a translation domain, install a _() function using gettext's
install() function.
The main difference from gettext.install() is that we allow
overriding the default localedir (e.g. /usr/share/locale) using
a translation-domain-specific environment variable (e.g.
NOVA_LOCALEDIR).
Note that to enable lazy translation, enable_lazy must be
called.
:param domain: the translation domain
"""
from six import moves
tf = TranslatorFactory(domain)
moves.builtins.__dict__['_'] = tf.primary
class Message(six.text_type):
"""A Message object is a unicode object that can be translated.
Translation of Message is done explicitly using the translate() method.
For all non-translation intents and purposes, a Message is simply unicode,
and can be treated as such.
"""
def __new__(cls, msgid, msgtext=None, params=None,
domain='watcherclient', *args):
"""Create a new Message object.
In order for translation to work gettext requires a message ID, this
msgid will be used as the base unicode text. It is also possible
for the msgid and the base unicode text to be different by passing
the msgtext parameter.
"""
# If the base msgtext is not given, we use the default translation
# of the msgid (which is in English) just in case the system locale is
# not English, so that the base text will be in that locale by default.
if not msgtext:
msgtext = Message._translate_msgid(msgid, domain)
# We want to initialize the parent unicode with the actual object that
# would have been plain unicode if 'Message' was not enabled.
msg = super(Message, cls).__new__(cls, msgtext)
msg.msgid = msgid
msg.domain = domain
msg.params = params
return msg
def translate(self, desired_locale=None):
"""Translate this message to the desired locale.
:param desired_locale: The desired locale to translate the message to,
if no locale is provided the message will be
translated to the system's default locale.
:returns: the translated message in unicode
"""
translated_message = Message._translate_msgid(self.msgid,
self.domain,
desired_locale)
if self.params is None:
# No need for more translation
return translated_message
# This Message object may have been formatted with one or more
# Message objects as substitution arguments, given either as a single
# argument, part of a tuple, or as one or more values in a dictionary.
# When translating this Message we need to translate those Messages too
translated_params = _translate_args(self.params, desired_locale)
translated_message = translated_message % translated_params
return translated_message
@staticmethod
def _translate_msgid(msgid, domain, desired_locale=None):
if not desired_locale:
system_locale = locale.getdefaultlocale()
# If the system locale is not available to the runtime use English
if not system_locale[0]:
desired_locale = 'en_US'
else:
desired_locale = system_locale[0]
locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
lang = gettext.translation(domain,
localedir=locale_dir,
languages=[desired_locale],
fallback=True)
if six.PY3:
translator = lang.gettext
else:
translator = lang.ugettext
translated_message = translator(msgid)
return translated_message
def __mod__(self, other):
# When we mod a Message we want the actual operation to be performed
# by the parent class (i.e. unicode()), the only thing we do here is
# save the original msgid and the parameters in case of a translation
params = self._sanitize_mod_params(other)
unicode_mod = super(Message, self).__mod__(params)
modded = Message(self.msgid,
msgtext=unicode_mod,
params=params,
domain=self.domain)
return modded
def _sanitize_mod_params(self, other):
"""Sanitize the object being modded with this Message.
- Add support for modding 'None' so translation supports it
- Trim the modded object, which can be a large dictionary, to only
those keys that would actually be used in a translation
- Snapshot the object being modded, in case the message is
translated, it will be used as it was when the Message was created
"""
if other is None:
params = (other,)
elif isinstance(other, dict):
# Merge the dictionaries
# Copy each item in case one does not support deep copy.
params = {}
if isinstance(self.params, dict):
for key, val in self.params.items():
params[key] = self._copy_param(val)
for key, val in other.items():
params[key] = self._copy_param(val)
else:
params = self._copy_param(other)
return params
def _copy_param(self, param):
try:
return copy.deepcopy(param)
except Exception:
# Fallback to casting to unicode this will handle the
# python code-like objects that can't be deep-copied
return six.text_type(param)
def __add__(self, other):
msg = _('Message objects do not support addition.')
raise TypeError(msg)
def __radd__(self, other):
return self.__add__(other)
if six.PY2:
def __str__(self):
# NOTE(luisg): Logging in python 2.6 tries to str() log records,
# and it expects specifically a UnicodeError in order to proceed.
msg = _('Message objects do not support str() because they may '
'contain non-ascii characters. '
'Please use unicode() or translate() instead.')
raise UnicodeError(msg)
def get_available_languages(domain):
"""Lists the available languages for the given translation domain.
:param domain: the domain to get languages for
"""
if domain in _AVAILABLE_LANGUAGES:
return copy.copy(_AVAILABLE_LANGUAGES[domain])
localedir = '%s_LOCALEDIR' % domain.upper()
find = lambda x: gettext.find(domain,
localedir=os.environ.get(localedir),
languages=[x])
# NOTE(mrodden): en_US should always be available (and first in case
# order matters) since our in-line message strings are en_US
language_list = ['en_US']
# NOTE(luisg): Babel <1.0 used a function called list(), which was
# renamed to locale_identifiers() in >=1.0, the requirements master list
# requires >=0.9.6, uncapped, so defensively work with both. We can remove
# this check when the master list updates to >=1.0, and update all projects
list_identifiers = (getattr(localedata, 'list', None) or
getattr(localedata, 'locale_identifiers'))
locale_identifiers = list_identifiers()
for i in locale_identifiers:
if find(i) is not None:
language_list.append(i)
# NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported
# locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they
# are perfectly legitimate locales:
# https://github.com/mitsuhiko/babel/issues/37
# In Babel 1.3 they fixed the bug and they support these locales, but
# they are still not explicitly "listed" by locale_identifiers().
# That is why we add the locales here explicitly if necessary so that
# they are listed as supported.
aliases = {'zh': 'zh_CN',
'zh_Hant_HK': 'zh_HK',
'zh_Hant': 'zh_TW',
'fil': 'tl_PH'}
for (locale_, alias) in six.iteritems(aliases):
if locale_ in language_list and alias not in language_list:
language_list.append(alias)
_AVAILABLE_LANGUAGES[domain] = language_list
return copy.copy(language_list)
def translate(obj, desired_locale=None):
"""Gets the translated unicode representation of the given object.
If the object is not translatable it is returned as-is.
If the locale is None the object is translated to the system locale.
:param obj: the object to translate
:param desired_locale: the locale to translate the message to, if None the
default system locale will be used
:returns: the translated object in unicode, or the original object if
it could not be translated
"""
message = obj
if not isinstance(message, Message):
# If the object to translate is not already translatable,
# let's first get its unicode representation
message = six.text_type(obj)
if isinstance(message, Message):
# Even after unicoding() we still need to check if we are
# running with translatable unicode before translating
return message.translate(desired_locale)
return obj
def _translate_args(args, desired_locale=None):
"""Translates all the translatable elements of the given arguments object.
This method is used for translating the translatable values in method
arguments which include values of tuples or dictionaries.
If the object is not a tuple or a dictionary the object itself is
translated if it is translatable.
If the locale is None the object is translated to the system locale.
:param args: the args to translate
:param desired_locale: the locale to translate the args to, if None the
default system locale will be used
:returns: a new args object with the translated contents of the original
"""
if isinstance(args, tuple):
return tuple(translate(v, desired_locale) for v in args)
if isinstance(args, dict):
translated_dict = {}
for (k, v) in six.iteritems(args):
translated_v = translate(v, desired_locale)
translated_dict[k] = translated_v
return translated_dict
return translate(args, desired_locale)
class TranslationHandler(handlers.MemoryHandler):
"""Handler that translates records before logging them.
The TranslationHandler takes a locale and a target logging.Handler object
to forward LogRecord objects to after translating them. This handler
depends on Message objects being logged, instead of regular strings.
The handler can be configured declaratively in the logging.conf as follows:
[handlers]
keys = translatedlog, translator
[handler_translatedlog]
class = handlers.WatchedFileHandler
args = ('/var/log/api-localized.log',)
formatter = context
[handler_translator]
class = openstack.common.log.TranslationHandler
target = translatedlog
args = ('zh_CN',)
If the specified locale is not available in the system, the handler will
log in the default locale.
"""
def __init__(self, locale=None, target=None):
"""Initialize a TranslationHandler
:param locale: locale to use for translating messages
:param target: logging.Handler object to forward
LogRecord objects to after translation
"""
# NOTE(luisg): In order to allow this handler to be a wrapper for
# other handlers, such as a FileHandler, and still be able to
# configure it using logging.conf, this handler has to extend
# MemoryHandler because only the MemoryHandlers' logging.conf
# parsing is implemented such that it accepts a target handler.
handlers.MemoryHandler.__init__(self, capacity=0, target=target)
self.locale = locale
def setFormatter(self, fmt):
self.target.setFormatter(fmt)
def emit(self, record):
# We save the message from the original record to restore it
# after translation, so other handlers are not affected by this
original_msg = record.msg
original_args = record.args
try:
self._translate_and_log_record(record)
finally:
record.msg = original_msg
record.args = original_args
def _translate_and_log_record(self, record):
record.msg = translate(record.msg, self.locale)
# In addition to translating the message, we also need to translate
# arguments that were passed to the log method that were not part
# of the main message e.g., log.info(_('Some message %s'), this_one))
record.args = _translate_args(record.args, self.locale)
self.target.emit(record)

View File

@ -0,0 +1,73 @@
# Copyright 2011 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Import related utilities and helper functions.
"""
import sys
import traceback
def import_class(import_str):
"""Returns a class from a string including module and class."""
mod_str, _sep, class_str = import_str.rpartition('.')
__import__(mod_str)
try:
return getattr(sys.modules[mod_str], class_str)
except AttributeError:
raise ImportError('Class %s cannot be found (%s)' %
(class_str,
traceback.format_exception(*sys.exc_info())))
def import_object(import_str, *args, **kwargs):
"""Import a class and return an instance of it."""
return import_class(import_str)(*args, **kwargs)
def import_object_ns(name_space, import_str, *args, **kwargs):
"""Tries to import object from default namespace.
Imports a class and return an instance of it, first by trying
to find the class in a default namespace, then failing back to
a full path if not found in the default namespace.
"""
import_value = "%s.%s" % (name_space, import_str)
try:
return import_class(import_value)(*args, **kwargs)
except ImportError:
return import_class(import_str)(*args, **kwargs)
def import_module(import_str):
"""Import a module."""
__import__(import_str)
return sys.modules[import_str]
def import_versioned_module(version, submodule=None):
module = 'watcherclient.v%s' % version
if submodule:
module = '.'.join((module, submodule))
return import_module(module)
def try_import(import_str, default=None):
"""Try to import a module and if it fails return default."""
try:
return import_module(import_str)
except ImportError:
return default

View File

@ -0,0 +1,316 @@
# Copyright 2011 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
System-level utilities and helper functions.
"""
import math
import re
import sys
import unicodedata
import six
from watcherclient.openstack.common.gettextutils import _
UNIT_PREFIX_EXPONENT = {
'k': 1,
'K': 1,
'Ki': 1,
'M': 2,
'Mi': 2,
'G': 3,
'Gi': 3,
'T': 4,
'Ti': 4,
}
UNIT_SYSTEM_INFO = {
'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')),
'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')),
}
TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]")
SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+")
# NOTE(flaper87): The following globals are used by `mask_password`
_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password']
# NOTE(ldbragst): Let's build a list of regex objects using the list of
# _SANITIZE_KEYS we already have. This way, we only have to add the new key
# to the list of _SANITIZE_KEYS and we can generate regular expressions
# for XML and JSON automatically.
_SANITIZE_PATTERNS_2 = []
_SANITIZE_PATTERNS_1 = []
# NOTE(amrith): Some regular expressions have only one parameter, some
# have two parameters. Use different lists of patterns here.
_FORMAT_PATTERNS_1 = [r'(%(key)s\s*[=]\s*)[^\s^\'^\"]+']
_FORMAT_PATTERNS_2 = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])',
r'(%(key)s\s+[\"\']).*?([\"\'])',
r'([-]{2}%(key)s\s+)[^\'^\"^=^\s]+([\s]*)',
r'(<%(key)s>).*?(</%(key)s>)',
r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])',
r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])',
r'([\'"].*?%(key)s[\'"]\s*,\s*\'--?[A-z]+\'\s*,\s*u?'
'[\'"]).*?([\'"])',
r'(%(key)s\s*--?[A-z]+\s*)\S+(\s*)']
for key in _SANITIZE_KEYS:
for pattern in _FORMAT_PATTERNS_2:
reg_ex = re.compile(pattern % {'key': key}, re.DOTALL)
_SANITIZE_PATTERNS_2.append(reg_ex)
for pattern in _FORMAT_PATTERNS_1:
reg_ex = re.compile(pattern % {'key': key}, re.DOTALL)
_SANITIZE_PATTERNS_1.append(reg_ex)
def int_from_bool_as_string(subject):
"""Interpret a string as a boolean and return either 1 or 0.
Any string value in:
('True', 'true', 'On', 'on', '1')
is interpreted as a boolean True.
Useful for JSON-decoded stuff and config file parsing
"""
return bool_from_string(subject) and 1 or 0
def bool_from_string(subject, strict=False, default=False):
"""Interpret a string as a boolean.
A case-insensitive match is performed such that strings matching 't',
'true', 'on', 'y', 'yes', or '1' are considered True and, when
`strict=False`, anything else returns the value specified by 'default'.
Useful for JSON-decoded stuff and config file parsing.
If `strict=True`, unrecognized values, including None, will raise a
ValueError which is useful when parsing values passed in from an API call.
Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
"""
if not isinstance(subject, six.string_types):
subject = six.text_type(subject)
lowered = subject.strip().lower()
if lowered in TRUE_STRINGS:
return True
elif lowered in FALSE_STRINGS:
return False
elif strict:
acceptable = ', '.join(
"'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS))
msg = _("Unrecognized value '%(val)s', acceptable values are:"
" %(acceptable)s") % {'val': subject,
'acceptable': acceptable}
raise ValueError(msg)
else:
return default
def safe_decode(text, incoming=None, errors='strict'):
"""Decodes incoming text/bytes string using `incoming` if they're not
already unicode.
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a unicode `incoming` encoded
representation of it.
:raises TypeError: If text is not an instance of str
"""
if not isinstance(text, (six.string_types, six.binary_type)):
raise TypeError("%s can't be decoded" % type(text))
if isinstance(text, six.text_type):
return text
if not incoming:
incoming = (sys.stdin.encoding or
sys.getdefaultencoding())
try:
return text.decode(incoming, errors)
except UnicodeDecodeError:
# Note(flaper87) If we get here, it means that
# sys.stdin.encoding / sys.getdefaultencoding
# didn't return a suitable encoding to decode
# text. This happens mostly when global LANG
# var is not set correctly and there's no
# default encoding. In this case, most likely
# python will use ASCII or ANSI encoders as
# default encodings but they won't be capable
# of decoding non-ASCII characters.
#
# Also, UTF-8 is being used since it's an ASCII
# extension.
return text.decode('utf-8', errors)
def safe_encode(text, incoming=None,
encoding='utf-8', errors='strict'):
"""Encodes incoming text/bytes string using `encoding`.
If incoming is not specified, text is expected to be encoded with
current python's default encoding. (`sys.getdefaultencoding`)
:param incoming: Text's current encoding
:param encoding: Expected encoding for text (Default UTF-8)
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a bytestring `encoding` encoded
representation of it.
:raises TypeError: If text is not an instance of str
"""
if not isinstance(text, (six.string_types, six.binary_type)):
raise TypeError("%s can't be encoded" % type(text))
if not incoming:
incoming = (sys.stdin.encoding or
sys.getdefaultencoding())
if isinstance(text, six.text_type):
return text.encode(encoding, errors)
elif text and encoding != incoming:
# Decode text before encoding it with `encoding`
text = safe_decode(text, incoming, errors)
return text.encode(encoding, errors)
else:
return text
def string_to_bytes(text, unit_system='IEC', return_int=False):
"""Converts a string into an float representation of bytes.
The units supported for IEC ::
Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it)
KB, KiB, MB, MiB, GB, GiB, TB, TiB
The units supported for SI ::
kb(it), Mb(it), Gb(it), Tb(it)
kB, MB, GB, TB
Note that the SI unit system does not support capital letter 'K'
:param text: String input for bytes size conversion.
:param unit_system: Unit system for byte size conversion.
:param return_int: If True, returns integer representation of text
in bytes. (default: decimal)
:returns: Numerical representation of text in bytes.
:raises ValueError: If text has an invalid value.
"""
try:
base, reg_ex = UNIT_SYSTEM_INFO[unit_system]
except KeyError:
msg = _('Invalid unit system: "%s"') % unit_system
raise ValueError(msg)
match = reg_ex.match(text)
if match:
magnitude = float(match.group(1))
unit_prefix = match.group(2)
if match.group(3) in ['b', 'bit']:
magnitude /= 8
else:
msg = _('Invalid string format: %s') % text
raise ValueError(msg)
if not unit_prefix:
res = magnitude
else:
res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix])
if return_int:
return int(math.ceil(res))
return res
def to_slug(value, incoming=None, errors="strict"):
"""Normalize string.
Convert to lowercase, remove non-word characters, and convert spaces
to hyphens.
Inspired by Django's `slugify` filter.
:param value: Text to slugify
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: slugified unicode representation of `value`
:raises TypeError: If text is not an instance of str
"""
value = safe_decode(value, incoming, errors)
# NOTE(aababilov): no need to use safe_(encode|decode) here:
# encodings are always "ascii", error handling is always "ignore"
# and types are always known (first: unicode; second: str)
value = unicodedata.normalize("NFKD", value).encode(
"ascii", "ignore").decode("ascii")
value = SLUGIFY_STRIP_RE.sub("", value).strip().lower()
return SLUGIFY_HYPHENATE_RE.sub("-", value)
def mask_password(message, secret="***"):
"""Replace password with 'secret' in message.
:param message: The string which includes security information.
:param secret: value with which to replace passwords.
:returns: The unicode value of message with the password fields masked.
For example:
>>> mask_password("'adminPass' : 'aaaaa'")
"'adminPass' : '***'"
>>> mask_password("'admin_pass' : 'aaaaa'")
"'admin_pass' : '***'"
>>> mask_password('"password" : "aaaaa"')
'"password" : "***"'
>>> mask_password("'original_password' : 'aaaaa'")
"'original_password' : '***'"
>>> mask_password("u'original_password' : u'aaaaa'")
"u'original_password' : u'***'"
"""
try:
message = six.text_type(message)
except UnicodeDecodeError:
# NOTE(jecarey): Temporary fix to handle cases where message is a
# byte string. A better solution will be provided in Kilo.
pass
# NOTE(ldbragst): Check to see if anything in message contains any key
# specified in _SANITIZE_KEYS, if not then just return the message since
# we don't have to mask any passwords.
if not any(key in message for key in _SANITIZE_KEYS):
return message
substitute = r'\g<1>' + secret + r'\g<2>'
for pattern in _SANITIZE_PATTERNS_2:
message = re.sub(pattern, substitute, message)
substitute = r'\g<1>' + secret
for pattern in _SANITIZE_PATTERNS_1:
message = re.sub(pattern, substitute, message)
return message

510
watcherclient/shell.py Normal file
View File

@ -0,0 +1,510 @@
# -*- coding: utf-8 -*-
#
# 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.
"""
Command-line interface to the Watcher API.
"""
from __future__ import print_function
import argparse
import getpass
import logging
import sys
from keystoneclient.auth.identity import v2 as v2_auth
from keystoneclient.auth.identity import v3 as v3_auth
from keystoneclient import discover
from keystoneclient.openstack.common.apiclient import exceptions as ks_exc
from keystoneclient import session as kssession
import six.moves.urllib.parse as urlparse
import watcherclient
from watcherclient import client as watcher_client
from watcherclient.common import utils
from watcherclient import exceptions as exc
from watcherclient.openstack.common._i18n import _
from watcherclient.openstack.common import cliutils
from watcherclient.openstack.common import gettextutils
gettextutils.install('watcherclient')
class WatcherShell(object):
def _append_global_identity_args(self, parser):
# FIXME(dhu): these are global identity (Keystone) arguments which
# should be consistent and shared by all service clients. Therefore,
# they should be provided by python-keystoneclient. We will need to
# refactor this code once this functionality is avaible in
# python-keystoneclient.
# Register arguments needed for a Session
kssession.Session.register_cli_options(parser)
parser.add_argument('--os-user-domain-id',
default=cliutils.env('OS_USER_DOMAIN_ID'),
help='Defaults to env[OS_USER_DOMAIN_ID].')
parser.add_argument('--os-user-domain-name',
default=cliutils.env('OS_USER_DOMAIN_NAME'),
help='Defaults to env[OS_USER_DOMAIN_NAME].')
parser.add_argument('--os-project-id',
default=cliutils.env('OS_PROJECT_ID'),
help='Another way to specify tenant ID. '
'This option is mutually exclusive with '
' --os-tenant-id. '
'Defaults to env[OS_PROJECT_ID].')
parser.add_argument('--os-project-name',
default=cliutils.env('OS_PROJECT_NAME'),
help='Another way to specify tenant name. '
'This option is mutually exclusive with '
' --os-tenant-name. '
'Defaults to env[OS_PROJECT_NAME].')
parser.add_argument('--os-project-domain-id',
default=cliutils.env('OS_PROJECT_DOMAIN_ID'),
help='Defaults to env[OS_PROJECT_DOMAIN_ID].')
parser.add_argument('--os-project-domain-name',
default=cliutils.env('OS_PROJECT_DOMAIN_NAME'),
help='Defaults to env[OS_PROJECT_DOMAIN_NAME].')
def get_base_parser(self):
parser = argparse.ArgumentParser(
prog='watcher',
description=__doc__.strip(),
epilog='See "watcher help COMMAND" '
'for help on a specific command.',
add_help=False,
formatter_class=HelpFormatter,
)
# Global arguments
parser.add_argument('-h', '--help',
action='store_true',
help=argparse.SUPPRESS,
)
parser.add_argument('--version',
action='version',
version=watcherclient.__version__)
parser.add_argument('--debug',
default=bool(cliutils.env('WATCHERCLIENT_DEBUG')),
action='store_true',
help='Defaults to env[WATCHERCLIENT_DEBUG]')
parser.add_argument('-v', '--verbose',
default=False, action="store_true",
help="Print more verbose output")
# for backward compatibility only
parser.add_argument('--cert-file',
dest='os_cert',
help='DEPRECATED! Use --os-cert.')
# for backward compatibility only
parser.add_argument('--key-file',
dest='os_key',
help='DEPRECATED! Use --os-key.')
# for backward compatibility only
parser.add_argument('--ca-file',
dest='os_cacert',
help='DEPRECATED! Use --os-cacert.')
parser.add_argument('--os-username',
default=cliutils.env('OS_USERNAME'),
help='Defaults to env[OS_USERNAME]')
parser.add_argument('--os_username',
help=argparse.SUPPRESS)
parser.add_argument('--os-password',
default=cliutils.env('OS_PASSWORD'),
help='Defaults to env[OS_PASSWORD]')
parser.add_argument('--os_password',
help=argparse.SUPPRESS)
parser.add_argument('--os-tenant-id',
default=cliutils.env('OS_TENANT_ID'),
help='Defaults to env[OS_TENANT_ID]')
parser.add_argument('--os_tenant_id',
help=argparse.SUPPRESS)
parser.add_argument('--os-tenant-name',
default=cliutils.env('OS_TENANT_NAME'),
help='Defaults to env[OS_TENANT_NAME]')
parser.add_argument('--os_tenant_name',
help=argparse.SUPPRESS)
parser.add_argument('--os-auth-url',
default=cliutils.env('OS_AUTH_URL'),
help='Defaults to env[OS_AUTH_URL]')
parser.add_argument('--os_auth_url',
help=argparse.SUPPRESS)
parser.add_argument('--os-region-name',
default=cliutils.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=cliutils.env('OS_AUTH_TOKEN'),
help='Defaults to env[OS_AUTH_TOKEN]')
parser.add_argument('--os_auth_token',
help=argparse.SUPPRESS)
parser.add_argument('--watcher-url',
default=cliutils.env('WATCHER_URL'),
help='Defaults to env[WATCHER_URL]')
parser.add_argument('--watcher_url',
help=argparse.SUPPRESS)
parser.add_argument('--watcher-api-version',
default=cliutils.env(
'WATCHER_API_VERSION', default='1'),
help='Defaults to env[WATCHER_API_VERSION] '
'or 1')
parser.add_argument('--watcher_api_version',
help=argparse.SUPPRESS)
parser.add_argument('--os-service-type',
default=cliutils.env('OS_SERVICE_TYPE'),
help='Defaults to env[OS_SERVICE_TYPE] or '
'"watcher"')
parser.add_argument('--os_service_type',
help=argparse.SUPPRESS)
parser.add_argument('--os-endpoint',
default=cliutils.env('OS_SERVICE_ENDPOINT'),
help='Specify an endpoint to use instead of '
'retrieving one from the service catalog '
'(via authentication). '
'Defaults to env[OS_SERVICE_ENDPOINT].')
parser.add_argument('--os_endpoint',
help=argparse.SUPPRESS)
parser.add_argument('--os-endpoint-type',
default=cliutils.env('OS_ENDPOINT_TYPE'),
help='Defaults to env[OS_ENDPOINT_TYPE] or '
'"publicURL"')
parser.add_argument('--os_endpoint_type',
help=argparse.SUPPRESS)
# FIXME(gyee): this method should come from python-keystoneclient.
# Will refactor this code once it is available.
# https://bugs.launchpad.net/python-keystoneclient/+bug/1332337
self._append_global_identity_args(parser)
return parser
def get_subcommand_parser(self, version):
parser = self.get_base_parser()
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
submodule = utils.import_versioned_module(version, 'shell')
submodule.enhance_parser(parser, subparsers, self.subcommands)
utils.define_commands_from_module(subparsers, self, self.subcommands)
return parser
def _setup_debugging(self, debug):
if debug:
logging.basicConfig(
format="%(levelname)s (%(module)s:%(lineno)d) %(message)s",
level=logging.DEBUG)
else:
logging.basicConfig(
format="%(levelname)s %(message)s",
level=logging.CRITICAL)
def do_bash_completion(self):
"""Prints all of the commands and options for bash-completion."""
commands = set()
options = set()
for sc_str, sc in self.subcommands.items():
commands.add(sc_str)
for option in sc._optionals._option_string_actions.keys():
options.add(option)
commands.remove('bash-completion')
print(' '.join(commands | options))
def _discover_auth_versions(self, session, auth_url):
# discover the API versions the server is supporting base on the
# given URL
v2_auth_url = None
v3_auth_url = None
try:
ks_discover = discover.Discover(session=session, auth_url=auth_url)
v2_auth_url = ks_discover.url_for('2.0')
v3_auth_url = ks_discover.url_for('3.0')
except ks_exc.ClientException:
# Identity service may not support discover API version.
# Let's try to figure out the API version from the original URL.
url_parts = urlparse.urlparse(auth_url)
(scheme, netloc, path, params, query, fragment) = url_parts
path = path.lower()
if path.startswith('/v3'):
v3_auth_url = auth_url
elif path.startswith('/v2'):
v2_auth_url = auth_url
else:
# not enough information to determine the auth version
msg = _('Unable to determine the Keystone version '
'to authenticate with using the given '
'auth_url. Identity service may not support API '
'version discovery. Please provide a versioned '
'auth_url instead. %s') % auth_url
raise exc.CommandError(msg)
return (v2_auth_url, v3_auth_url)
def _get_keystone_v3_auth(self, v3_auth_url, **kwargs):
auth_token = kwargs.pop('auth_token', None)
if auth_token:
return v3_auth.Token(v3_auth_url, auth_token)
else:
return v3_auth.Password(v3_auth_url, **kwargs)
def _get_keystone_v2_auth(self, v2_auth_url, **kwargs):
auth_token = kwargs.pop('auth_token', None)
if auth_token:
return v2_auth.Token(
v2_auth_url,
auth_token,
tenant_id=kwargs.pop('project_id', None),
tenant_name=kwargs.pop('project_name', None))
else:
return v2_auth.Password(
v2_auth_url,
username=kwargs.pop('username', None),
password=kwargs.pop('password', None),
tenant_id=kwargs.pop('project_id', None),
tenant_name=kwargs.pop('project_name', None))
def _get_keystone_auth(self, session, auth_url, **kwargs):
# FIXME(dhu): this code should come from keystoneclient
# discover the supported keystone versions using the given url
(v2_auth_url, v3_auth_url) = self._discover_auth_versions(
session=session,
auth_url=auth_url)
# Determine which authentication plugin to use. First inspect the
# auth_url to see the supported version. If both v3 and v2 are
# supported, then use the highest version if possible.
auth = None
if v3_auth_url and v2_auth_url:
user_domain_name = kwargs.get('user_domain_name', None)
user_domain_id = kwargs.get('user_domain_id', None)
project_domain_name = kwargs.get('project_domain_name', None)
project_domain_id = kwargs.get('project_domain_id', None)
# support both v2 and v3 auth. Use v3 if domain information is
# provided.
if (user_domain_name or user_domain_id or project_domain_name or
project_domain_id):
auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs)
else:
auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs)
elif v3_auth_url:
# support only v3
auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs)
elif v2_auth_url:
# support only v2
auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs)
else:
raise exc.CommandError('Unable to determine the Keystone version '
'to authenticate with using the given '
'auth_url.')
return auth
def main(self, argv):
# Parse args once to find version
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
self._setup_debugging(options.debug)
# build available subcommands based on version
api_version = options.watcher_api_version
subcommand_parser = self.get_subcommand_parser(api_version)
self.parser = subcommand_parser
# Handle top-level --help/-h before attempting to parse
# a command off the command line
if options.help or not argv:
self.do_help(options)
return 0
# Parse args again and call whatever callback was selected
args = subcommand_parser.parse_args(argv)
# Short-circuit and deal with these commands 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()
return 0
if not (args.os_auth_token and (args.watcher_url or args.os_endpoint)):
if not args.os_username:
raise exc.CommandError(_("You must provide a username via "
"either --os-username or via "
"env[OS_USERNAME]"))
if not args.os_password:
# No password, If we've got a tty, try prompting for it
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
# Check for Ctl-D
try:
args.os_password = getpass.getpass(
'OpenStack Password: ')
except EOFError:
pass
# No password because we didn't have a tty or the
# user Ctl-D when prompted.
if not args.os_password:
raise exc.CommandError(_("You must provide a password via "
"either --os-password, "
"env[OS_PASSWORD], "
"or prompted response"))
if not (args.os_tenant_id or args.os_tenant_name or
args.os_project_id or args.os_project_name):
raise exc.CommandError(_(
"You must provide a project name or"
" project id via --os-project-name, --os-project-id,"
" env[OS_PROJECT_ID] or env[OS_PROJECT_NAME]. You may"
" use os-project and os-tenant interchangeably."))
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]"))
endpoint = args.watcher_url or args.os_endpoint
service_type = args.os_service_type or 'infra-optim'
project_id = args.os_project_id or args.os_tenant_id
project_name = args.os_project_name or args.os_tenant_name
if (args.os_auth_token and (args.watcher_url or args.os_endpoint)):
kwargs = {
'token': args.os_auth_token,
'insecure': args.insecure,
'timeout': args.timeout,
'ca_file': args.os_cacert,
'cert_file': args.os_cert,
'key_file': args.os_key,
'auth_ref': None,
}
elif (args.os_username and
args.os_password and
args.os_auth_url and
(project_id or project_name)):
keystone_session = kssession.Session.load_from_cli_options(args)
kwargs = {
'username': args.os_username,
'user_domain_id': args.os_user_domain_id,
'user_domain_name': args.os_user_domain_name,
'password': args.os_password,
'auth_token': args.os_auth_token,
'project_id': project_id,
'project_name': project_name,
'project_domain_id': args.os_project_domain_id,
'project_domain_name': args.os_project_domain_name,
}
keystone_auth = self._get_keystone_auth(keystone_session,
args.os_auth_url,
**kwargs)
if not endpoint:
svc_type = args.os_service_type
region_name = args.os_region_name
endpoint = keystone_auth.get_endpoint(keystone_session,
service_type=svc_type,
region_name=region_name)
endpoint_type = args.os_endpoint_type or 'publicURL'
kwargs = {
'auth_url': args.os_auth_url,
'session': keystone_session,
'auth': keystone_auth,
'service_type': service_type,
'endpoint_type': endpoint_type,
'region_name': args.os_region_name,
'username': args.os_username,
'password': args.os_password,
}
client = watcher_client.Client(api_version, endpoint, **kwargs)
try:
args.func(client, args)
except exc.Unauthorized:
raise exc.CommandError(_("Invalid OpenStack Identity credentials"))
@cliutils.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)
def main():
try:
WatcherShell().main(sys.argv[1:])
except KeyboardInterrupt:
print("... terminating watcher client", file=sys.stderr)
sys.exit(130)
except Exception as e:
print(str(e), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

View File

@ -0,0 +1,81 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import uuid
from keystoneclient.fixture import v2 as ks_v2_fixture
from keystoneclient.fixture import v3 as ks_v3_fixture
# these are copied from python-keystoneclient tests
BASE_HOST = 'http://keystone.example.com'
BASE_URL = "%s:5000/" % BASE_HOST
UPDATED = '2013-03-06T00:00:00Z'
V2_URL = "%sv2.0" % BASE_URL
V2_DESCRIBED_BY_HTML = {'href': 'http://docs.openstack.org/api/'
'openstack-identity-service/2.0/content/',
'rel': 'describedby',
'type': 'text/html'}
V2_DESCRIBED_BY_PDF = {'href': 'http://docs.openstack.org/api/openstack-ident'
'ity-service/2.0/identity-dev-guide-2.0.pdf',
'rel': 'describedby',
'type': 'application/pdf'}
V2_VERSION = {'id': 'v2.0',
'links': [{'href': V2_URL, 'rel': 'self'},
V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF],
'status': 'stable',
'updated': UPDATED}
V3_URL = "%sv3" % BASE_URL
V3_MEDIA_TYPES = [{'base': 'application/json',
'type': 'application/vnd.openstack.identity-v3+json'},
{'base': 'application/xml',
'type': 'application/vnd.openstack.identity-v3+xml'}]
V3_VERSION = {'id': 'v3.0',
'links': [{'href': V3_URL, 'rel': 'self'}],
'media-types': V3_MEDIA_TYPES,
'status': 'stable',
'updated': UPDATED}
TOKENID = uuid.uuid4().hex
def _create_version_list(versions):
return json.dumps({'versions': {'values': versions}})
def _create_single_version(version):
return json.dumps({'version': version})
V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION])
V2_VERSION_LIST = _create_version_list([V2_VERSION])
V3_VERSION_ENTRY = _create_single_version(V3_VERSION)
V2_VERSION_ENTRY = _create_single_version(V2_VERSION)
def keystone_request_callback(request, uri, headers):
response_headers = {"content-type": "application/json"}
token_id = TOKENID
if uri == BASE_URL:
return (200, headers, V3_VERSION_LIST)
elif uri == BASE_URL + "/v2.0":
v2_token = ks_v2_fixture.Token(token_id)
return (200, response_headers, json.dumps(v2_token))
elif uri == BASE_URL + "/v3":
v3_token = ks_v3_fixture.Token()
response_headers["X-Subject-Token"] = token_id
return (201, response_headers, json.dumps(v3_token))

View File

@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
#
# 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 fixtures
from watcherclient.client import get_client
from watcherclient import exceptions as exc
from watcherclient.tests import utils
def fake_get_ksclient(**kwargs):
return utils.FakeKeystone('KSCLIENT_AUTH_TOKEN')
class ClientTest(utils.BaseTestCase):
def test_get_client_with_auth_token_watcher_url(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
kwargs = {
'watcher_url': 'http://watcher.example.org:6385/',
'os_auth_token': 'USER_AUTH_TOKEN',
}
client = get_client('1', **kwargs)
self.assertEqual('USER_AUTH_TOKEN', client.http_client.auth_token)
self.assertEqual('http://watcher.example.org:6385/',
client.http_client.endpoint)
def test_get_client_no_auth_token(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_auth_url': 'http://localhost:35357/v2.0',
'os_auth_token': '',
}
client = get_client('1', **kwargs)
self.assertEqual('KSCLIENT_AUTH_TOKEN', client.http_client.auth_token)
self.assertEqual('http://localhost:6385/v1/f14b41234',
client.http_client.endpoint)
self.assertEqual(fake_get_ksclient().auth_ref,
client.http_client.auth_ref)
def test_get_client_with_region_no_auth_token(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_region_name': 'REGIONONE',
'os_auth_url': 'http://localhost:35357/v2.0',
'os_auth_token': '',
}
client = get_client('1', **kwargs)
self.assertEqual('KSCLIENT_AUTH_TOKEN', client.http_client.auth_token)
self.assertEqual('http://regionhost:6385/v1/f14b41234',
client.http_client.endpoint)
self.assertEqual(fake_get_ksclient().auth_ref,
client.http_client.auth_ref)
def test_get_client_with_auth_token(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_auth_url': 'http://localhost:35357/v2.0',
'os_auth_token': 'USER_AUTH_TOKEN',
}
client = get_client('1', **kwargs)
self.assertEqual('USER_AUTH_TOKEN', client.http_client.auth_token)
self.assertEqual('http://localhost:6385/v1/f14b41234',
client.http_client.endpoint)
self.assertEqual(fake_get_ksclient().auth_ref,
client.http_client.auth_ref)
def test_get_client_with_region_name_auth_token(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_auth_url': 'http://localhost:35357/v2.0',
'os_region_name': 'REGIONONE',
'os_auth_token': 'USER_AUTH_TOKEN',
}
client = get_client('1', **kwargs)
self.assertEqual('USER_AUTH_TOKEN', client.http_client.auth_token)
self.assertEqual('http://regionhost:6385/v1/f14b41234',
client.http_client.endpoint)
self.assertEqual(fake_get_ksclient().auth_ref,
client.http_client.auth_ref)
def test_get_client_no_url_and_no_token(self):
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', fake_get_ksclient))
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_auth_url': '',
'os_auth_token': '',
}
self.assertRaises(exc.AmbiguousAuthSystem, get_client, '1', **kwargs)
# test the alias as well to ensure backwards compatibility
self.assertRaises(exc.AmbigiousAuthSystem, get_client, '1', **kwargs)
def test_ensure_auth_ref_propagated(self):
ksclient = fake_get_ksclient
self.useFixture(fixtures.MonkeyPatch(
'watcherclient.client._get_ksclient', ksclient))
kwargs = {
'os_tenant_name': 'TENANT_NAME',
'os_username': 'USERNAME',
'os_password': 'PASSWORD',
'os_auth_url': 'http://localhost:35357/v2.0',
'os_auth_token': '',
}
client = get_client('1', **kwargs)
self.assertEqual(ksclient().auth_ref, client.http_client.auth_ref)

View File

@ -0,0 +1,283 @@
# -*- coding: utf-8 -*-
#
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import six
from watcherclient.common import http
from watcherclient import exceptions as exc
from watcherclient.tests import utils
HTTP_CLASS = six.moves.http_client.HTTPConnection
HTTPS_CLASS = http.VerifiedHTTPSConnection
DEFAULT_TIMEOUT = 600
def _get_error_body(faultstring=None, debuginfo=None):
error_body = {
'faultstring': faultstring,
'debuginfo': debuginfo
}
raw_error_body = json.dumps(error_body)
body = {'error_message': raw_error_body}
raw_body = json.dumps(body)
return raw_body
class HttpClientTest(utils.BaseTestCase):
def test_url_generation_trailing_slash_in_base(self):
client = http.HTTPClient('http://localhost/')
url = client._make_connection_url('/v1/resources')
self.assertEqual('/v1/resources', url)
def test_url_generation_without_trailing_slash_in_base(self):
client = http.HTTPClient('http://localhost')
url = client._make_connection_url('/v1/resources')
self.assertEqual('/v1/resources', url)
def test_url_generation_prefix_slash_in_path(self):
client = http.HTTPClient('http://localhost/')
url = client._make_connection_url('/v1/resources')
self.assertEqual('/v1/resources', url)
def test_url_generation_without_prefix_slash_in_path(self):
client = http.HTTPClient('http://localhost')
url = client._make_connection_url('v1/resources')
self.assertEqual('/v1/resources', url)
def test_server_exception_empty_body(self):
error_body = _get_error_body()
fake_resp = utils.FakeResponse({'content-type': 'application/json'},
six.StringIO(error_body),
version=1,
status=500)
client = http.HTTPClient('http://localhost/')
client.get_connection = (
lambda *a, **kw: utils.FakeConnection(fake_resp))
error = self.assertRaises(exc.InternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual('Internal Server Error (HTTP 500)', str(error))
def test_server_exception_msg_only(self):
error_msg = 'test error msg'
error_body = _get_error_body(error_msg)
fake_resp = utils.FakeResponse({'content-type': 'application/json'},
six.StringIO(error_body),
version=1,
status=500)
client = http.HTTPClient('http://localhost/')
client.get_connection = (
lambda *a, **kw: utils.FakeConnection(fake_resp))
error = self.assertRaises(exc.InternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual(error_msg + ' (HTTP 500)', str(error))
def test_server_exception_msg_and_traceback(self):
error_msg = 'another test error'
error_trace = ("\"Traceback (most recent call last):\\n\\n "
"File \\\"/usr/local/lib/python2.7/...")
error_body = _get_error_body(error_msg, error_trace)
fake_resp = utils.FakeResponse({'content-type': 'application/json'},
six.StringIO(error_body),
version=1,
status=500)
client = http.HTTPClient('http://localhost/')
client.get_connection = (
lambda *a, **kw: utils.FakeConnection(fake_resp))
error = self.assertRaises(exc.InternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual(
'%(error)s (HTTP 500)\n%(trace)s' % {'error': error_msg,
'trace': error_trace},
"%(error)s\n%(details)s" % {'error': str(error),
'details': str(error.details)})
def test_get_connection_params(self):
endpoint = 'http://watcher-host:6385'
expected = (HTTP_CLASS,
('watcher-host', 6385, ''),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_trailing_slash(self):
endpoint = 'http://watcher-host:6385/'
expected = (HTTP_CLASS,
('watcher-host', 6385, ''),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_ssl(self):
endpoint = 'https://watcher-host:6385'
expected = (HTTPS_CLASS,
('watcher-host', 6385, ''),
{
'timeout': DEFAULT_TIMEOUT,
'ca_file': None,
'cert_file': None,
'key_file': None,
'insecure': False,
})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_ssl_params(self):
endpoint = 'https://watcher-host:6385'
ssl_args = {
'ca_file': '/path/to/ca_file',
'cert_file': '/path/to/cert_file',
'key_file': '/path/to/key_file',
'insecure': True,
}
expected_kwargs = {'timeout': DEFAULT_TIMEOUT}
expected_kwargs.update(ssl_args)
expected = (HTTPS_CLASS,
('watcher-host', 6385, ''),
expected_kwargs)
params = http.HTTPClient.get_connection_params(endpoint, **ssl_args)
self.assertEqual(expected, params)
def test_get_connection_params_with_timeout(self):
endpoint = 'http://watcher-host:6385'
expected = (HTTP_CLASS,
('watcher-host', 6385, ''),
{'timeout': 300.0})
params = http.HTTPClient.get_connection_params(endpoint, timeout=300)
self.assertEqual(expected, params)
def test_get_connection_params_with_version(self):
endpoint = 'http://watcher-host:6385/v1'
expected = (HTTP_CLASS,
('watcher-host', 6385, ''),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_version_trailing_slash(self):
endpoint = 'http://watcher-host:6385/v1/'
expected = (HTTP_CLASS,
('watcher-host', 6385, ''),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_subpath(self):
endpoint = 'http://watcher-host:6385/watcher'
expected = (HTTP_CLASS,
('watcher-host', 6385, '/watcher'),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_subpath_trailing_slash(self):
endpoint = 'http://watcher-host:6385/watcher/'
expected = (HTTP_CLASS,
('watcher-host', 6385, '/watcher'),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_subpath_version(self):
endpoint = 'http://watcher-host:6385/watcher/v1'
expected = (HTTP_CLASS,
('watcher-host', 6385, '/watcher'),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_get_connection_params_with_subpath_version_trailing_slash(self):
endpoint = 'http://watcher-host:6385/watcher/v1/'
expected = (HTTP_CLASS,
('watcher-host', 6385, '/watcher'),
{'timeout': DEFAULT_TIMEOUT})
params = http.HTTPClient.get_connection_params(endpoint)
self.assertEqual(expected, params)
def test_401_unauthorized_exception(self):
error_body = _get_error_body()
fake_resp = utils.FakeResponse({'content-type': 'text/plain'},
six.StringIO(error_body),
version=1,
status=401)
client = http.HTTPClient('http://localhost/')
client.get_connection = (
lambda *a, **kw: utils.FakeConnection(fake_resp))
self.assertRaises(exc.Unauthorized, client.json_request,
'GET', '/v1/resources')
class SessionClientTest(utils.BaseTestCase):
def test_server_exception_msg_and_traceback(self):
error_msg = 'another test error'
error_trace = ("\"Traceback (most recent call last):\\n\\n "
"File \\\"/usr/local/lib/python2.7/...")
error_body = _get_error_body(error_msg, error_trace)
fake_session = utils.FakeSession({'Content-Type': 'application/json'},
error_body,
500)
client = http.SessionClient(session=fake_session,
auth=None,
interface=None,
service_type='publicURL',
region_name='',
service_name=None)
error = self.assertRaises(exc.InternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual(
'%(error)s (HTTP 500)\n%(trace)s' % {'error': error_msg,
'trace': error_trace},
"%(error)s\n%(details)s" % {'error': str(error),
'details': str(error.details)})
def test_server_exception_empty_body(self):
error_body = _get_error_body()
fake_session = utils.FakeSession({'Content-Type': 'application/json'},
error_body,
500)
client = http.SessionClient(session=fake_session,
auth=None,
interface=None,
service_type='publicURL',
region_name='',
service_name=None)
error = self.assertRaises(exc.InternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual('Internal Server Error (HTTP 500)', str(error))

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
#
# 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 watcherclient.tests import utils
module_str = 'watcherclient'
class ImportTest(utils.BaseTestCase):
def check_exported_symbols(self, exported_symbols):
self.assertIn('client', exported_symbols)
self.assertIn('exceptions', exported_symbols)
def test_import_objects(self):
module = __import__(module_str)
exported_symbols = dir(module)
self.check_exported_symbols(exported_symbols)
def test_default_import(self):
default_imports = __import__(module_str, globals(), locals(), ['*'])
exported_symbols = dir(default_imports)
self.check_exported_symbols(exported_symbols)
def test_import__all__(self):
module = __import__(module_str)
self.check_exported_symbols(module.__all__)

View File

@ -0,0 +1,306 @@
# -*- coding: utf-8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import os
import re
import sys
import fixtures
import httpretty
from keystoneclient import exceptions as keystone_exc
from keystoneclient.fixture import v2 as ks_v2_fixture
from keystoneclient.fixture import v3 as ks_v3_fixture
import mock
import six
import testtools
from testtools import matchers
from watcherclient import exceptions as exc
from watcherclient import shell as watcher_shell
from watcherclient.tests import keystone_client_fixtures
from watcherclient.tests import utils
FAKE_ENV = {'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': 'http://no.where/v2.0/'}
FAKE_ENV_KEYSTONE_V2 = {
'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': keystone_client_fixtures.BASE_URL,
}
FAKE_ENV_KEYSTONE_V3 = {
'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': keystone_client_fixtures.BASE_URL,
'OS_USER_DOMAIN_ID': 'default',
'OS_PROJECT_DOMAIN_ID': 'default',
}
class ShellTest(utils.BaseTestCase):
re_options = re.DOTALL | re.MULTILINE
# Patch os.environ to avoid required auth info.
def make_env(self, exclude=None):
env = dict((k, v) for k, v in FAKE_ENV.items() if k != exclude)
self.useFixture(fixtures.MonkeyPatch('os.environ', env))
def setUp(self):
super(ShellTest, self).setUp()
def shell(self, argstr):
orig = sys.stdout
try:
sys.stdout = six.StringIO()
_shell = watcher_shell.WatcherShell()
_shell.main(argstr.split())
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
def test_help_unknown_command(self):
self.assertRaises(exc.CommandError, self.shell, 'help foofoo')
def test_help(self):
required = [
'.*?^usage: watcher',
'.*?^ +bash-completion',
'.*?^See "watcher help COMMAND" '
'for help on a specific command',
]
for argstr in ['--help', 'help']:
help_text = self.shell(argstr)
for r in required:
self.assertThat(help_text,
matchers.MatchesRegex(r,
self.re_options))
def test_help_on_subcommand(self):
required = [
'.*?^usage: watcher action-show',
".*?^Show detailed information about an action",
]
argstrings = [
'help action-show',
]
for argstr in argstrings:
help_text = self.shell(argstr)
for r in required:
self.assertThat(help_text,
matchers.MatchesRegex(r, self.re_options))
def test_auth_param(self):
self.make_env(exclude='OS_USERNAME')
self.test_help()
@mock.patch('sys.stdin', side_effect=mock.MagicMock)
@mock.patch('getpass.getpass', return_value='password')
def test_password_prompted(self, mock_getpass, mock_stdin):
self.make_env(exclude='OS_PASSWORD')
# We will get a Connection Refused because there is no keystone.
self.assertRaises(keystone_exc.ConnectionRefused,
self.shell, 'action-list')
# Make sure we are actually prompted.
mock_getpass.assert_called_with('OpenStack Password: ')
@mock.patch('sys.stdin', side_effect=mock.MagicMock)
@mock.patch('getpass.getpass', side_effect=EOFError)
def test_password_prompted_ctrlD(self, mock_getpass, mock_stdin):
self.make_env(exclude='OS_PASSWORD')
# We should get Command Error because we mock Ctl-D.
self.assertRaises(exc.CommandError,
self.shell, 'action-list')
# Make sure we are actually prompted.
mock_getpass.assert_called_with('OpenStack Password: ')
@mock.patch('sys.stdin')
def test_no_password_no_tty(self, mock_stdin):
# delete the isatty attribute so that we do not get
# prompted when manually running the tests
del mock_stdin.isatty
required = ('You must provide a password'
' via either --os-password, env[OS_PASSWORD],'
' or prompted response',)
self.make_env(exclude='OS_PASSWORD')
try:
self.shell('action-list')
except exc.CommandError as message:
self.assertEqual(required, message.args)
else:
self.fail('CommandError not raised')
def test_bash_completion(self):
stdout = self.shell('bash-completion')
# just check we have some output
required = [
'.*help',
'.*audit-list',
'.*audit-show',
'.*audit-delete',
'.*audit-update',
'.*audit-template-create',
'.*audit-template-update',
'.*audit-template-list',
'.*audit-template-show',
'.*audit-template-delete',
'.*action-list',
'.*action-show',
'.*action-update',
'.*action-plan-list',
'.*action-plan-show',
'.*action-plan-update',
]
for r in required:
self.assertThat(stdout,
matchers.MatchesRegex(r, self.re_options))
class TestCase(testtools.TestCase):
tokenid = keystone_client_fixtures.TOKENID
def set_fake_env(self, fake_env):
client_env = ('OS_USERNAME', 'OS_PASSWORD', 'OS_TENANT_ID',
'OS_TENANT_NAME', 'OS_AUTH_URL', 'OS_REGION_NAME',
'OS_AUTH_TOKEN', 'OS_NO_CLIENT_AUTH', 'OS_SERVICE_TYPE',
'OS_ENDPOINT_TYPE')
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 register_keystone_v2_token_fixture(self):
v2_token = ks_v2_fixture.Token(token_id=self.tokenid)
service = v2_token.add_service('baremetal')
service.add_endpoint('http://watcher.example.com', region='RegionOne')
httpretty.register_uri(
httpretty.POST,
'%s/tokens' % (keystone_client_fixtures.V2_URL),
body=json.dumps(v2_token))
def register_keystone_v3_token_fixture(self):
v3_token = ks_v3_fixture.Token()
service = v3_token.add_service('baremetal')
service.add_standard_endpoints(public='http://watcher.example.com')
httpretty.register_uri(
httpretty.POST,
'%s/auth/tokens' % (keystone_client_fixtures.V3_URL),
body=json.dumps(v3_token),
adding_headers={'X-Subject-Token': self.tokenid})
def register_keystone_auth_fixture(self):
self.register_keystone_v2_token_fixture()
self.register_keystone_v3_token_fixture()
httpretty.register_uri(
httpretty.GET,
keystone_client_fixtures.BASE_URL,
body=keystone_client_fixtures.keystone_request_callback)
class ShellTestNoMox(TestCase):
def setUp(self):
super(ShellTestNoMox, self).setUp()
# httpretty doesn't work as expected if http proxy environment
# variable is set.
os.environ = dict((k, v) for (k, v) in os.environ.items()
if k.lower() not in ('http_proxy', 'https_proxy'))
self.set_fake_env(FAKE_ENV_KEYSTONE_V2)
def shell(self, argstr):
orig = sys.stdout
try:
sys.stdout = six.StringIO()
_shell = watcher_shell.WatcherShell()
_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
# @httpretty.activate
# def test_action_list(self):
# self.register_keystone_auth_fixture()
# resp_dict = {"dummies": [
# {"instance_uuid": "null",
# "uuid": "351a82d6-9f04-4c36-b79a-a38b9e98ff71",
# "links": [{"href": "http://watcher.example.com:6385/"
# "v1/dummies/foo",
# "rel": "self"},
# {"href": "http://watcher.example.com:6385/"
# "dummies/foo",
# "rel": "bookmark"}],
# "maintenance": "false",
# "provision_state": "null",
# "power_state": "power off"},
# {"instance_uuid": "null",
# "uuid": "66fbba13-29e8-4b8a-9e80-c655096a40d3",
# "links": [{"href": "http://watcher.example.com:6385/"
# "v1/dummies/foo2",
# "rel": "self"},
# {"href": "http://watcher.example.com:6385/"
# "dummies/foo2",
# "rel": "bookmark"}],
# "maintenance": "false",
# "provision_state": "null",
# "power_state": "power off"}]}
# httpretty.register_uri(
# httpretty.GET,
# 'http://watcher.example.com/v1/dummies',
# status=200,
# content_type='application/json; charset=UTF-8',
# body=json.dumps(resp_dict))
# event_list_text = self.shell('action-list')
# required = [
# '351a82d6-9f04-4c36-b79a-a38b9e98ff71',
# '66fbba13-29e8-4b8a-9e80-c655096a40d3',
# ]
# for r in required:
# self.assertRegexpMatches(event_list_text, r)
class ShellTestNoMoxV3(ShellTestNoMox):
def _set_fake_env(self):
self.set_fake_env(FAKE_ENV_KEYSTONE_V3)

View File

@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from watcherclient.common import utils
from watcherclient import exceptions as exc
from watcherclient.tests import utils as test_utils
class UtilsTest(test_utils.BaseTestCase):
def test_args_array_to_dict(self):
my_args = {
'matching_metadata': ['str=foo', 'int=1', 'bool=true',
'list=[1, 2, 3]', 'dict={"foo": "bar"}'],
'other': 'value'
}
cleaned_dict = utils.args_array_to_dict(my_args,
"matching_metadata")
self.assertEqual({
'matching_metadata': {'str': 'foo', 'int': 1, 'bool': True,
'list': [1, 2, 3], 'dict': {'foo': 'bar'}},
'other': 'value'
}, cleaned_dict)
def test_args_array_to_patch(self):
my_args = {
'attributes': ['str=foo', 'int=1', 'bool=true',
'list=[1, 2, 3]', 'dict={"foo": "bar"}'],
'op': 'add',
}
patch = utils.args_array_to_patch(my_args['op'],
my_args['attributes'])
self.assertEqual([{'op': 'add', 'value': 'foo', 'path': '/str'},
{'op': 'add', 'value': 1, 'path': '/int'},
{'op': 'add', 'value': True, 'path': '/bool'},
{'op': 'add', 'value': [1, 2, 3], 'path': '/list'},
{'op': 'add', 'value': {"foo": "bar"},
'path': '/dict'}], patch)
def test_args_array_to_patch_format_error(self):
my_args = {
'attributes': ['foobar'],
'op': 'add',
}
self.assertRaises(exc.CommandError, utils.args_array_to_patch,
my_args['op'], my_args['attributes'])
def test_args_array_to_patch_remove(self):
my_args = {
'attributes': ['/foo', 'extra/bar'],
'op': 'remove',
}
patch = utils.args_array_to_patch(my_args['op'],
my_args['attributes'])
self.assertEqual([{'op': 'remove', 'path': '/foo'},
{'op': 'remove', 'path': '/extra/bar'}], patch)
def test_split_and_deserialize(self):
ret = utils.split_and_deserialize('str=foo')
self.assertEqual(('str', 'foo'), ret)
ret = utils.split_and_deserialize('int=1')
self.assertEqual(('int', 1), ret)
ret = utils.split_and_deserialize('bool=false')
self.assertEqual(('bool', False), ret)
ret = utils.split_and_deserialize('list=[1, "foo", 2]')
self.assertEqual(('list', [1, "foo", 2]), ret)
ret = utils.split_and_deserialize('dict={"foo": 1}')
self.assertEqual(('dict', {"foo": 1}), ret)
ret = utils.split_and_deserialize('str_int="1"')
self.assertEqual(('str_int', "1"), ret)
def test_split_and_deserialize_fail(self):
self.assertRaises(exc.CommandError,
utils.split_and_deserialize, 'foo:bar')
class CommonParamsForListTest(test_utils.BaseTestCase):
def setUp(self):
super(CommonParamsForListTest, self).setUp()
self.args = mock.Mock(limit=None, sort_key=None, sort_dir=None)
self.args.detail = False
self.expected_params = {'detail': False}
def test_nothing_set(self):
self.assertEqual(self.expected_params,
utils.common_params_for_list(self.args, [], []))
def test_limit(self):
self.args.limit = 42
self.expected_params.update({'limit': 42})
self.assertEqual(self.expected_params,
utils.common_params_for_list(self.args, [], []))
def test_invalid_limit(self):
self.args.limit = -42
self.assertRaises(exc.CommandError,
utils.common_params_for_list,
self.args, [], [])
def test_sort_key_and_sort_dir(self):
self.args.sort_key = 'field'
self.args.sort_dir = 'desc'
self.expected_params.update({'sort_key': 'field', 'sort_dir': 'desc'})
self.assertEqual(self.expected_params,
utils.common_params_for_list(self.args,
['field'],
[]))
def test_sort_key_allows_label(self):
self.args.sort_key = 'Label'
self.expected_params.update({'sort_key': 'field'})
self.assertEqual(self.expected_params,
utils.common_params_for_list(self.args,
['field', 'field2'],
['Label', 'Label2']))
def test_sort_key_invalid(self):
self.args.sort_key = 'something'
self.assertRaises(exc.CommandError,
utils.common_params_for_list,
self.args,
['field', 'field2'],
[])
def test_sort_dir_invalid(self):
self.args.sort_dir = 'something'
self.assertRaises(exc.CommandError,
utils.common_params_for_list,
self.args,
[],
[])
def test_detail(self):
self.args.detail = True
self.expected_params['detail'] = True
self.assertEqual(self.expected_params,
utils.common_params_for_list(self.args, [], []))
class CommonFiltersTest(test_utils.BaseTestCase):
def test_limit(self):
result = utils.common_filters(limit=42)
self.assertEqual(['limit=42'], result)
def test_limit_0(self):
result = utils.common_filters(limit=0)
self.assertEqual([], result)
def test_other(self):
for key in ('sort_key', 'sort_dir'):
result = utils.common_filters(**{key: 'test'})
self.assertEqual(['%s=test' % key], result)

View File

@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
#
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import datetime
import os
import fixtures
from oslo_utils import strutils
import six
import testtools
from watcherclient.common import http
class BaseTestCase(testtools.TestCase):
def setUp(self):
super(BaseTestCase, self).setUp()
self.useFixture(fixtures.FakeLogger())
# If enabled, stdout and/or stderr is captured and will appear in
# test results if that test fails.
if strutils.bool_from_string(os.environ.get('OS_STDOUT_CAPTURE')):
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
if strutils.bool_from_string(os.environ.get('OS_STDERR_CAPTURE')):
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
class FakeAPI(object):
def __init__(self, responses):
self.responses = responses
self.calls = []
def _request(self, method, url, headers=None, body=None):
call = (method, url, headers or {}, body)
self.calls.append(call)
return self.responses[url][method]
def raw_request(self, *args, **kwargs):
response = self._request(*args, **kwargs)
body_iter = http.ResponseBodyIterator(six.StringIO(response[1]))
return FakeResponse(response[0]), body_iter
def json_request(self, *args, **kwargs):
response = self._request(*args, **kwargs)
return FakeResponse(response[0]), response[1]
class FakeConnection(object):
def __init__(self, response=None):
self._response = response
self._last_request = None
def request(self, method, conn_url, **kwargs):
self._last_request = (method, conn_url, kwargs)
def setresponse(self, response):
self._response = response
def getresponse(self):
return self._response
class FakeResponse(object):
def __init__(self, headers, body=None, version=None, status=None,
reason=None):
"""Fake object to help testing.
:param headers: dict representing HTTP response headers
:param body: file-like object
"""
self.headers = headers
self.body = body
self.version = version
self.status = status
self.reason = reason
def getheaders(self):
return copy.deepcopy(self.headers).items()
def getheader(self, key, default):
return self.headers.get(key, default)
def read(self, amt):
return self.body.read(amt)
class FakeServiceCatalog(object):
def url_for(self, endpoint_type, service_type, attr=None,
filter_value=None):
if attr == 'region' and filter_value:
return 'http://regionhost:6385/v1/f14b41234'
else:
return 'http://localhost:6385/v1/f14b41234'
class FakeKeystone(object):
service_catalog = FakeServiceCatalog()
timestamp = datetime.datetime.utcnow() + datetime.timedelta(days=5)
def __init__(self, auth_token):
self.auth_token = auth_token
self.auth_ref = {
'token': {'expires': FakeKeystone.timestamp.strftime(
'%Y-%m-%dT%H:%M:%S.%f'),
'id': 'd1a541311782870742235'}
}
class FakeSessionResponse(object):
def __init__(self, headers, content=None, status_code=None):
self.headers = headers
self.content = content
self.status_code = status_code
class FakeSession(object):
def __init__(self, headers, content=None, status_code=None):
self.headers = headers
self.content = content
self.status_code = status_code
def request(self, url, method, **kwargs):
return FakeSessionResponse(self.headers, self.content,
self.status_code)

View File

View File

@ -0,0 +1,277 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import testtools
from testtools.matchers import HasLength
from watcherclient.tests import utils
import watcherclient.v1.action
ACTION1 = {
'id': 1,
'uuid': '770ef053-ecb3-48b0-85b5-d55a2dbc6588',
'action_plan': 'f8e47706-efcf-49a4-a5c4-af604eb492f2',
'description': 'Action_1 description',
'next': '239f02a5-9649-4e14-9d33-ac2bf67cb755',
'state': 'PENDING',
'alarm': None
}
ACTION2 = {
'id': 2,
'uuid': '239f02a5-9649-4e14-9d33-ac2bf67cb755',
'action_plan': 'f8e47706-efcf-49a4-a5c4-af604eb492f2',
'description': 'Action_2 description',
'next': '67653274-eb24-c7ba-70f6-a84e73d80843',
'state': 'PENDING',
'alarm': None
}
ACTION3 = {
'id': 3,
'uuid': '67653274-eb24-c7ba-70f6-a84e73d80843',
'action_plan': 'a5199d0e-0702-4613-9234-5ae2af8dafea',
'description': 'Action_3 description',
'next': None,
'state': 'PENDING',
'alarm': None
}
ACTION_PLAN1 = {
'id': 1,
'uuid': 'a5199d0e-0702-4613-9234-5ae2af8dafea',
'audit': '770ef053-ecb3-48b0-85b5-d55a2dbc6588',
'state': 'RECOMMENDED'
}
UPDATED_ACTION1 = copy.deepcopy(ACTION1)
NEW_EXTRA = 'key1=val1'
UPDATED_ACTION1['extra'] = NEW_EXTRA
fake_responses = {
'/v1/actions':
{
'GET': (
{},
{"actions": [ACTION1, ACTION2, ACTION3]},
),
},
'/v1/actions/?action_plan_uuid=%s' % ACTION1['action_plan']:
{
'GET': (
{},
{"actions": [ACTION1, ACTION2]},
),
},
'/v1/actions/?audit_uuid=%s' % ACTION_PLAN1['audit']:
{
'GET': (
{},
{"actions": [ACTION3]},
),
},
'/v1/actions/detail':
{
'GET': (
{},
{"actions": [ACTION1, ACTION2, ACTION3]},
),
},
'/v1/actions/%s' % ACTION1['uuid']:
{
'GET': (
{},
ACTION1,
),
'DELETE': (
{},
None,
),
'PATCH': (
{},
UPDATED_ACTION1,
),
},
'/v1/actions/detail?action_plan_uuid=%s' % ACTION1['action_plan']:
{
'GET': (
{},
{"actions": [ACTION1, ACTION2]},
),
},
'/v1/actions/detail?audit_uuid=%s' % ACTION_PLAN1['audit']:
{
'GET': (
{},
{"actions": [ACTION3]},
),
}
}
fake_responses_pagination = {
'/v1/actions':
{
'GET': (
{},
{"actions": [ACTION1],
"next": "http://127.0.0.1:6385/v1/actions/?limit=1"}
),
},
'/v1/actions/?limit=1':
{
'GET': (
{},
{"actions": [ACTION2]}
),
},
}
fake_responses_sorting = {
'/v1/actions/?sort_key=updated_at':
{
'GET': (
{},
{"actions": [ACTION3, ACTION2, ACTION1]}
),
},
'/v1/actions/?sort_dir=desc':
{
'GET': (
{},
{"actions": [ACTION3, ACTION2, ACTION1]}
),
},
}
class ActionManagerTest(testtools.TestCase):
def setUp(self):
super(ActionManagerTest, self).setUp()
self.api = utils.FakeAPI(fake_responses)
self.mgr = watcherclient.v1.action.ActionManager(self.api)
def test_actions_list(self):
actions = self.mgr.list()
expect = [
('GET', '/v1/actions', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(3, len(actions))
def test_actions_list_by_action_plan(self):
actions = self.mgr.list(action_plan=ACTION1['action_plan'])
expect = [
('GET',
'/v1/actions/?action_plan_uuid=%s' % ACTION1['action_plan'],
{}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(actions))
def test_actions_list_detail(self):
actions = self.mgr.list(detail=True)
expect = [
('GET', '/v1/actions/detail', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(3, len(actions))
def test_actions_list_by_action_plan_detail(self):
actions = self.mgr.list(action_plan=ACTION1['action_plan'],
detail=True)
expect = [
('GET',
'/v1/actions/detail?action_plan_uuid=%s' % ACTION1['action_plan'],
{},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(actions))
def test_actions_list_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.action.ActionManager(self.api)
actions = self.mgr.list(limit=1)
expect = [
('GET', '/v1/actions/?limit=1', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(actions, HasLength(1))
def test_actions_list_pagination_no_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.action.ActionManager(self.api)
actions = self.mgr.list(limit=0)
expect = [
('GET', '/v1/actions', {}, None),
('GET', '/v1/actions/?limit=1', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertThat(actions, HasLength(2))
def test_actions_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.action.ActionManager(self.api)
actions = self.mgr.list(sort_key='updated_at')
expect = [
('GET', '/v1/actions/?sort_key=updated_at', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(3, len(actions))
def test_actions_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.action.ActionManager(self.api)
actions = self.mgr.list(sort_dir='desc')
expect = [
('GET', '/v1/actions/?sort_dir=desc', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(3, len(actions))
def test_actions_show(self):
action = self.mgr.get(ACTION1['uuid'])
expect = [
('GET', '/v1/actions/%s' % ACTION1['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(ACTION1['uuid'], action.uuid)
self.assertEqual(ACTION1['action_plan'], action.action_plan)
self.assertEqual(ACTION1['alarm'], action.alarm)
self.assertEqual(ACTION1['next'], action.next)
def test_delete(self):
action = self.mgr.delete(action_id=ACTION1['uuid'])
expect = [
('DELETE', '/v1/actions/%s' % ACTION1['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertIsNone(action)
def test_update(self):
patch = {'op': 'replace',
'value': NEW_EXTRA,
'path': '/extra'}
action = self.mgr.update(action_id=ACTION1['uuid'], patch=patch)
expect = [
('PATCH', '/v1/actions/%s' % ACTION1['uuid'], {}, patch),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(NEW_EXTRA, action.extra)

View File

@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import testtools
from testtools.matchers import HasLength
from watcherclient.tests import utils
import watcherclient.v1.action_plan
ACTION_PLAN1 = {
'id': 1,
'uuid': 'f8e47706-efcf-49a4-a5c4-af604eb492f2',
'audit': '770ef053-ecb3-48b0-85b5-d55a2dbc6588',
'state': 'RECOMMENDED'
}
ACTION_PLAN2 = {
'id': 2,
'uuid': 'a5199d0e-0702-4613-9234-5ae2af8dafea',
'audit': '239f02a5-9649-4e14-9d33-ac2bf67cb755',
'state': 'RECOMMENDED'
}
UPDATED_ACTION_PLAN = copy.deepcopy(ACTION_PLAN1)
NEW_STATE = 'STARTING'
UPDATED_ACTION_PLAN['state'] = NEW_STATE
fake_responses = {
'/v1/action_plans':
{
'GET': (
{},
{"action_plans": [ACTION_PLAN1, ACTION_PLAN2]},
),
},
'/v1/action_plans/detail':
{
'GET': (
{},
{"action_plans": [ACTION_PLAN1, ACTION_PLAN2]},
),
},
'/v1/action_plans/%s' % ACTION_PLAN1['uuid']:
{
'GET': (
{},
ACTION_PLAN1,
),
'DELETE': (
{},
None,
),
'PATCH': (
{},
UPDATED_ACTION_PLAN,
),
},
'/v1/action_plans/detail?uuid=%s' % ACTION_PLAN1['uuid']:
{
'GET': (
{},
{"action_plans": [ACTION_PLAN1]},
),
},
}
fake_responses_pagination = {
'/v1/action_plans':
{
'GET': (
{},
{"action_plans": [ACTION_PLAN1],
"next": "http://127.0.0.1:6385/v1/action_plans/?limit=1"}
),
},
'/v1/action_plans/?limit=1':
{
'GET': (
{},
{"action_plans": [ACTION_PLAN2]}
),
},
}
fake_responses_sorting = {
'/v1/action_plans/?sort_key=updated_at':
{
'GET': (
{},
{"action_plans": [ACTION_PLAN2, ACTION_PLAN1]}
),
},
'/v1/action_plans/?sort_dir=desc':
{
'GET': (
{},
{"action_plans": [ACTION_PLAN2, ACTION_PLAN1]}
),
},
}
class ActionPlanManagerTest(testtools.TestCase):
def setUp(self):
super(ActionPlanManagerTest, self).setUp()
self.api = utils.FakeAPI(fake_responses)
self.mgr = watcherclient.v1.action_plan.ActionPlanManager(self.api)
def test_action_plans_list(self):
action_plans = self.mgr.list()
expect = [
('GET', '/v1/action_plans', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(action_plans))
def test_action_plans_list_detail(self):
action_plans = self.mgr.list(detail=True)
expect = [
('GET', '/v1/action_plans/detail', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(action_plans))
def test_action_plans_list_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.action_plan.ActionPlanManager(self.api)
action_plans = self.mgr.list(limit=1)
expect = [
('GET', '/v1/action_plans/?limit=1', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(action_plans, HasLength(1))
def test_action_plans_list_pagination_no_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.action_plan.ActionPlanManager(self.api)
action_plans = self.mgr.list(limit=0)
expect = [
('GET', '/v1/action_plans', {}, None),
('GET', '/v1/action_plans/?limit=1', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertThat(action_plans, HasLength(2))
def test_action_plans_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.action_plan.ActionPlanManager(self.api)
action_plans = self.mgr.list(sort_key='updated_at')
expect = [
('GET', '/v1/action_plans/?sort_key=updated_at', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(action_plans))
def test_action_plans_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.action_plan.ActionPlanManager(self.api)
action_plans = self.mgr.list(sort_dir='desc')
expect = [
('GET', '/v1/action_plans/?sort_dir=desc', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(action_plans))
def test_action_plans_show(self):
action_plan = self.mgr.get(ACTION_PLAN1['uuid'])
expect = [
('GET', '/v1/action_plans/%s' % ACTION_PLAN1['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(ACTION_PLAN1['uuid'], action_plan.uuid)
def test_action_plan_update(self):
patch = {'op': 'replace',
'value': NEW_STATE,
'path': '/state'}
action_plan = self.mgr.update(action_plan_id=ACTION_PLAN1['uuid'],
patch=patch)
expect = [
('PATCH',
'/v1/action_plans/%s' % ACTION_PLAN1['uuid'],
{},
patch),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(NEW_STATE, action_plan.state)

View File

@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 IBM Corp
#
# 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 watcherclient.common import utils as commonutils
from watcherclient.openstack.common.apiclient.exceptions import ValidationError
from watcherclient.openstack.common import cliutils
from watcherclient.tests import utils
import watcherclient.v1.action_plan_shell as ap_shell
class ActionPlanShellTest(utils.BaseTestCase):
def test_do_action_plan_show(self):
actual = {}
fake_print_dict = lambda data, *args, **kwargs: actual.update(data)
with mock.patch.object(cliutils, 'print_dict', fake_print_dict):
action_plan = object()
ap_shell._print_action_plan_show(action_plan)
exp = ['uuid', 'created_at', 'updated_at', 'deleted_at',
'state', 'audit_uuid']
act = actual.keys()
self.assertEqual(sorted(exp), sorted(act))
def test_do_action_plan_show_by_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'action-plan', 'a5199d0e-0702-4613-9234-5ae2af8dafea')
ap_shell.do_action_plan_show(client_mock, args)
client_mock.action_plan.get.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea'
)
# assert get_by_name() wasn't called
self.assertFalse(client_mock.action_plan.get_by_name.called)
def test_do_action_plan_show_by_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'action-plan', 'not_uuid')
self.assertRaises(ValidationError, ap_shell.do_action_plan_show,
client_mock, args)
def test_do_action_plan_delete(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
delete = ['a5199d0e-0702-4613-9234-5ae2af8dafea']
setattr(args, 'action-plan', delete)
ap_shell.do_action_plan_delete(client_mock, args)
client_mock.action_plan.delete.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea')
def test_do_action_plan_delete_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'action-plan', ['not_uuid'])
self.assertRaises(ValidationError, ap_shell.do_action_plan_delete,
client_mock, args)
def test_do_action_plan_delete_multiple(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'action-plan',
["a5199d0e-0702-4613-9234-5ae2af8dafea",
"a5199d0e-0702-4613-9234-5ae2af8dafeb"])
ap_shell.do_action_plan_delete(client_mock, args)
client_mock.action_plan.delete.assert_has_calls(
[mock.call('a5199d0e-0702-4613-9234-5ae2af8dafea'),
mock.call('a5199d0e-0702-4613-9234-5ae2af8dafeb')])
def test_do_action_plan_delete_multiple_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'action-plan',
["a5199d0e-0702-4613-9234-5ae2af8dafea",
"not_uuid",
"a5199d0e-0702-4613-9234-5ae2af8dafeb"])
self.assertRaises(ValidationError, ap_shell.do_action_plan_delete,
client_mock, args)
client_mock.action_plan.delete.assert_has_calls(
[mock.call('a5199d0e-0702-4613-9234-5ae2af8dafea')])
def test_do_action_plan_update(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'action-plan', "a5199d0e-0702-4613-9234-5ae2af8dafea")
args.op = 'add'
args.attributes = [['arg1=val1', 'arg2=val2']]
ap_shell.do_action_plan_update(client_mock, args)
patch = commonutils.args_array_to_patch(
args.op,
args.attributes[0])
client_mock.action_plan.update.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea', patch)
def test_do_action_plan_update_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'action-plan', "not_uuid")
args.op = 'add'
args.attributes = [['arg1=val1', 'arg2=val2']]
self.assertRaises(ValidationError, ap_shell.do_action_plan_update,
client_mock, args)
def test_do_action_plan_start(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
action_plan = 'a5199d0e-0702-4613-9234-5ae2af8dafea'
setattr(args, 'action-plan', action_plan)
ap_shell.do_action_plan_start(client_mock, args)
patch = commonutils.args_array_to_patch(
'replace', ['state=STARTING'])
client_mock.action_plan.update.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea', patch)
def test_do_action_plan_start_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
action_plan = 'not_uuid'
setattr(args, 'action-plan', action_plan)
self.assertRaises(ValidationError, ap_shell.do_action_plan_start,
client_mock, args)

View File

@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 IBM Corp
#
# 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 watcherclient.common import utils as commonutils
from watcherclient.openstack.common.apiclient.exceptions import ValidationError
from watcherclient.openstack.common import cliutils
from watcherclient.tests import utils
import watcherclient.v1.action_shell as a_shell
class ActionShellTest(utils.BaseTestCase):
def test_do_action_show(self):
actual = {}
fake_print_dict = lambda data, *args, **kwargs: actual.update(data)
with mock.patch.object(cliutils, 'print_dict', fake_print_dict):
action = object()
a_shell._print_action_show(action)
exp = ['action_type',
'alarm',
'applies_to',
'created_at',
'deleted_at',
'description',
'dst',
'next_uuid',
'parameter',
'src',
'state',
'action_plan_uuid',
'updated_at',
'uuid']
act = actual.keys()
self.assertEqual(sorted(exp), sorted(act))
def test_do_action_show_by_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.action = 'a5199d0e-0702-4613-9234-5ae2af8dafea'
a_shell.do_action_show(client_mock, args)
client_mock.action.get.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea'
)
# assert get_by_name() wasn't called
self.assertFalse(client_mock.action.get_by_name.called)
def test_do_action_show_by_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.action = 'not_uuid'
self.assertRaises(ValidationError, a_shell.do_action_show,
client_mock, args)
def test_do_action_delete(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.action = ['a5199d0e-0702-4613-9234-5ae2af8dafea']
a_shell.do_action_delete(client_mock, args)
client_mock.action.delete.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea')
def test_do_action_delete_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.action = ['not_uuid']
self.assertRaises(ValidationError, a_shell.do_action_delete,
client_mock, args)
def test_do_action_delete_multiple(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.action = ['a5199d0e-0702-4613-9234-5ae2af8dafea',
'a5199d0e-0702-4613-9234-5ae2af8dafeb']
a_shell.do_action_delete(client_mock, args)
client_mock.action.delete.assert_has_calls(
[mock.call('a5199d0e-0702-4613-9234-5ae2af8dafea'),
mock.call('a5199d0e-0702-4613-9234-5ae2af8dafeb')])
def test_do_action_delete_multiple_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.action = ['a5199d0e-0702-4613-9234-5ae2af8dafea',
'not_uuid'
'a5199d0e-0702-4613-9234-5ae2af8dafeb']
self.assertRaises(ValidationError, a_shell.do_action_delete,
client_mock, args)
client_mock.action.delete.assert_has_calls(
[mock.call('a5199d0e-0702-4613-9234-5ae2af8dafea')])
def test_do_action_update(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.action = 'a5199d0e-0702-4613-9234-5ae2af8dafea'
args.op = 'add'
args.attributes = [['arg1=val1', 'arg2=val2']]
a_shell.do_action_update(client_mock, args)
patch = commonutils.args_array_to_patch(
args.op,
args.attributes[0])
client_mock.action.update.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea', patch)
def test_do_action_update_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.action = 'not_uuid'
args.op = 'add'
args.attributes = [['arg1=val1', 'arg2=val2']]
self.assertRaises(ValidationError, a_shell.do_action_update,
client_mock, args)

View File

@ -0,0 +1,227 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import testtools
from testtools.matchers import HasLength
from watcherclient.tests import utils
import watcherclient.v1.audit
AUDIT1 = {
'id': 1,
'uuid': 'f8e47706-efcf-49a4-a5c4-af604eb492f2',
'deadline': None,
'type': 'ONE_SHOT',
'audit_template_uuid': '770ef053-ecb3-48b0-85b5-d55a2dbc6588'
}
AUDIT2 = {
'id': 2,
'uuid': 'a5199d0e-0702-4613-9234-5ae2af8dafea',
'deadline': None,
'type': 'ONE_SHOT',
'audit_template_uuid': '770ef053-ecb3-48b0-85b5-d55a2dbc6588'
}
CREATE_AUDIT = copy.deepcopy(AUDIT1)
del CREATE_AUDIT['id']
del CREATE_AUDIT['uuid']
UPDATED_AUDIT1 = copy.deepcopy(AUDIT1)
NEW_STATE = 'SUCCESS'
UPDATED_AUDIT1['state'] = NEW_STATE
fake_responses = {
'/v1/audits':
{
'GET': (
{},
{"audits": [AUDIT1]},
),
'POST': (
{},
CREATE_AUDIT,
),
},
'/v1/audits/detail':
{
'GET': (
{},
{"audits": [AUDIT1]},
),
},
'/v1/audits/%s' % AUDIT1['uuid']:
{
'GET': (
{},
AUDIT1,
),
'DELETE': (
{},
None,
),
'PATCH': (
{},
UPDATED_AUDIT1,
),
},
'/v1/audits/detail?uuid=%s' % AUDIT1['uuid']:
{
'GET': (
{},
{"audits": [AUDIT1]},
),
},
}
fake_responses_pagination = {
'/v1/audits':
{
'GET': (
{},
{"audits": [AUDIT1],
"next": "http://127.0.0.1:6385/v1/audits/?limit=1"}
),
},
'/v1/audits/?limit=1':
{
'GET': (
{},
{"audits": [AUDIT2]}
),
},
}
fake_responses_sorting = {
'/v1/audits/?sort_key=updated_at':
{
'GET': (
{},
{"audits": [AUDIT2, AUDIT1]}
),
},
'/v1/audits/?sort_dir=desc':
{
'GET': (
{},
{"audits": [AUDIT2, AUDIT1]}
),
},
}
fake_responses_filters = {
'/v1/audits/?audit_template=%s' % AUDIT2['audit_template_uuid']:
{
'GET': (
{},
{"audits": [AUDIT2]}
),
},
}
class AuditManagerTest(testtools.TestCase):
def setUp(self):
super(AuditManagerTest, self).setUp()
self.api = utils.FakeAPI(fake_responses)
self.mgr = watcherclient.v1.audit.AuditManager(self.api)
def test_audits_list(self):
audits = self.mgr.list()
expect = [
('GET', '/v1/audits', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(audits))
def test_audits_list_detail(self):
audits = self.mgr.list(detail=True)
expect = [
('GET', '/v1/audits/detail', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(audits))
def test_audits_list_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.audit.AuditManager(self.api)
audits = self.mgr.list(limit=1)
expect = [
('GET', '/v1/audits/?limit=1', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(audits, HasLength(1))
def test_audits_list_pagination_no_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.audit.AuditManager(self.api)
audits = self.mgr.list(limit=0)
expect = [
('GET', '/v1/audits', {}, None),
('GET', '/v1/audits/?limit=1', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertThat(audits, HasLength(2))
def test_audits_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.audit.AuditManager(self.api)
audits = self.mgr.list(sort_key='updated_at')
expect = [
('GET', '/v1/audits/?sort_key=updated_at', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(audits))
def test_audits_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.audit.AuditManager(self.api)
audits = self.mgr.list(sort_dir='desc')
expect = [
('GET', '/v1/audits/?sort_dir=desc', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(audits))
def test_audits_list_filter_by_audit_template(self):
self.api = utils.FakeAPI(fake_responses_filters)
self.mgr = watcherclient.v1.audit.AuditManager(self.api)
self.mgr.list(audit_template=AUDIT2['audit_template_uuid'])
expect = [
('GET', '/v1/audits/?audit_template=%s' %
AUDIT2['audit_template_uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
def test_audits_show(self):
audit = self.mgr.get(AUDIT1['uuid'])
expect = [
('GET', '/v1/audits/%s' % AUDIT1['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(AUDIT1['uuid'], audit.uuid)
def test_create(self):
audit = self.mgr.create(**CREATE_AUDIT)
expect = [
('POST', '/v1/audits', {}, CREATE_AUDIT),
]
self.assertEqual(expect, self.api.calls)
self.assertTrue(audit)

View File

@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 IBM Corp
#
# 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 watcherclient.common import utils as commonutils
from watcherclient.openstack.common.apiclient.exceptions import ValidationError
from watcherclient.openstack.common import cliutils
from watcherclient.tests import utils
import watcherclient.v1.audit_shell as a_shell
class AuditShellTest(utils.BaseTestCase):
def test_do_audit_show(self):
actual = {}
fake_print_dict = lambda data, *args, **kwargs: actual.update(data)
with mock.patch.object(cliutils, 'print_dict', fake_print_dict):
audit = object()
a_shell._print_audit_show(audit)
exp = ['created_at', 'audit_template_uuid', 'updated_at', 'uuid',
'deleted_at', 'state', 'type', 'deadline']
act = actual.keys()
self.assertEqual(sorted(exp), sorted(act))
def test_do_audit_show_by_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.audit = 'a5199d0e-0702-4613-9234-5ae2af8dafea'
a_shell.do_audit_show(client_mock, args)
client_mock.audit.get.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea'
)
# assert get_by_name() wasn't called
self.assertFalse(client_mock.audit.get_by_name.called)
def test_do_audit_show_by_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.audit = 'not_uuid'
self.assertRaises(ValidationError, a_shell.do_audit_show,
client_mock, args)
def test_do_audit_delete(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.audit = ['a5199d0e-0702-4613-9234-5ae2af8dafea']
a_shell.do_audit_delete(client_mock, args)
client_mock.audit.delete.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea')
def test_do_audit_delete_with_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.audit = ['not_uuid']
self.assertRaises(ValidationError, a_shell.do_audit_delete,
client_mock, args)
def test_do_audit_delete_multiple(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.audit = ['a5199d0e-0702-4613-9234-5ae2af8dafea',
'a5199d0e-0702-4613-9234-5ae2af8dafeb']
a_shell.do_audit_delete(client_mock, args)
client_mock.audit.delete.assert_has_calls(
[mock.call('a5199d0e-0702-4613-9234-5ae2af8dafea'),
mock.call('a5199d0e-0702-4613-9234-5ae2af8dafeb')])
def test_do_audit_delete_multiple_with_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.audit = ['a5199d0e-0702-4613-9234-5ae2af8dafea',
'not_uuid',
'a5199d0e-0702-4613-9234-5ae2af8dafeb']
self.assertRaises(ValidationError, a_shell.do_audit_delete,
client_mock, args)
client_mock.audit.delete.assert_has_calls(
[mock.call('a5199d0e-0702-4613-9234-5ae2af8dafea')])
def test_do_audit_update(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.audit = 'a5199d0e-0702-4613-9234-5ae2af8dafea'
args.op = 'add'
args.attributes = [['arg1=val1', 'arg2=val2']]
a_shell.do_audit_update(client_mock, args)
patch = commonutils.args_array_to_patch(
args.op,
args.attributes[0])
client_mock.audit.update.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea', patch)
def test_do_audit_update_with_not_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.audit = ['not_uuid']
args.op = 'add'
args.attributes = [['arg1=val1', 'arg2=val2']]
self.assertRaises(ValidationError, a_shell.do_audit_update,
client_mock, args)
def test_do_audit_create(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
a_shell.do_audit_create(client_mock, args)
client_mock.audit.create.assert_called_once_with()
def test_do_audit_create_with_deadline(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.deadline = 'deadline'
a_shell.do_audit_create(client_mock, args)
client_mock.audit.create.assert_called_once_with(
deadline='deadline')
def test_do_audit_create_with_type(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.type = 'type'
a_shell.do_audit_create(client_mock, args)
client_mock.audit.create.assert_called_once_with(type='type')

View File

@ -0,0 +1,336 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
from six.moves.urllib import parse as urlparse
import testtools
from testtools.matchers import HasLength
from watcherclient.tests import utils
import watcherclient.v1.audit_template
AUDIT_TMPL1 = {
'id': 1,
'uuid': 'f8e47706-efcf-49a4-a5c4-af604eb492f2',
'name': 'Audit Template 1',
'description': 'Audit Template 1 description',
'host_aggregate': 5,
'extra': {'automatic': False},
'goal': 'MINIMIZE_LICENSING_COST'
}
AUDIT_TMPL2 = {
'id': 2,
'uuid': 'a5199d0e-0702-4613-9234-5ae2af8dafea',
'name': 'Audit Template 2',
'description': 'Audit Template 2 description',
'host_aggregate': 8,
'extra': {'automatic': True},
'goal': 'SERVERS_CONSOLIDATION'
}
AUDIT_TMPL3 = {
'id': 3,
'uuid': '770ef053-ecb3-48b0-85b5-d55a2dbc6588',
'name': 'Audit Template 3',
'description': 'Audit Template 3 description',
'host_aggregate': 7,
'extra': {'automatic': True},
'goal': 'MINIMIZE_LICENSING_COST'
}
CREATE_AUDIT_TEMPLATE = copy.deepcopy(AUDIT_TMPL1)
del CREATE_AUDIT_TEMPLATE['id']
del CREATE_AUDIT_TEMPLATE['uuid']
UPDATED_AUDIT_TMPL1 = copy.deepcopy(AUDIT_TMPL1)
NEW_NAME = 'Audit Template_1 new name'
UPDATED_AUDIT_TMPL1['name'] = NEW_NAME
fake_responses = {
'/v1/audit_templates':
{
'GET': (
{},
{"audit_templates": [AUDIT_TMPL1]},
),
'POST': (
{},
CREATE_AUDIT_TEMPLATE,
),
},
'/v1/audit_templates/detail':
{
'GET': (
{},
{"audit_templates": [AUDIT_TMPL1]},
),
},
'/v1/audit_templates/%s' % AUDIT_TMPL1['uuid']:
{
'GET': (
{},
AUDIT_TMPL1,
),
'DELETE': (
{},
None,
),
'PATCH': (
{},
UPDATED_AUDIT_TMPL1,
),
},
urlparse.quote('/v1/audit_templates/%s' % AUDIT_TMPL1['name']):
{
'GET': (
{},
AUDIT_TMPL1,
),
'DELETE': (
{},
None,
),
'PATCH': (
{},
UPDATED_AUDIT_TMPL1,
),
},
'/v1/audit_templates/detail?name=%s' % AUDIT_TMPL1['name']:
{
'GET': (
{},
{"audit_templates": [AUDIT_TMPL1]},
),
},
'/v1/audit_templates/?name=%s' % AUDIT_TMPL1['name']:
{
'GET': (
{},
{"audit_templates": [AUDIT_TMPL1]},
),
},
'/v1/audit_templates/detail?goal=%s' % AUDIT_TMPL1['goal']:
{
'GET': (
{},
{"audit_templates": [AUDIT_TMPL1, AUDIT_TMPL3]},
),
},
'/v1/audit_templates/?goal=%s' % AUDIT_TMPL1['goal']:
{
'GET': (
{},
{"audit_templates": [AUDIT_TMPL1, AUDIT_TMPL3]},
),
}
}
fake_responses_pagination = {
'/v1/audit_templates':
{
'GET': (
{},
{"audit_templates": [AUDIT_TMPL1],
"next": "http://127.0.0.1:6385/v1/audit_templates/?limit=1"}
),
},
'/v1/audit_templates/?limit=1':
{
'GET': (
{},
{"audit_templates": [AUDIT_TMPL2]}
),
},
}
fake_responses_sorting = {
'/v1/audit_templates/?sort_key=updated_at':
{
'GET': (
{},
{"audit_templates": [AUDIT_TMPL3, AUDIT_TMPL2, AUDIT_TMPL1]}
),
},
'/v1/audit_templates/?sort_dir=desc':
{
'GET': (
{},
{"audit_templates": [AUDIT_TMPL3, AUDIT_TMPL2, AUDIT_TMPL1]}
),
},
}
class AuditTemplateManagerTest(testtools.TestCase):
def setUp(self):
super(AuditTemplateManagerTest, self).setUp()
self.api = utils.FakeAPI(fake_responses)
self.mgr = watcherclient.v1.audit_template.AuditTemplateManager(
self.api)
def test_audit_templates_list(self):
audit_templates = self.mgr.list()
expect = [
('GET', '/v1/audit_templates', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(audit_templates))
def test_audit_templates_list_by_name(self):
audit_templates = self.mgr.list(name=AUDIT_TMPL1['name'])
expect = [
('GET', '/v1/audit_templates/?name=%s' % AUDIT_TMPL1['name'],
{}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(audit_templates))
def test_audit_templates_list_detail(self):
audit_templates = self.mgr.list(detail=True)
expect = [
('GET', '/v1/audit_templates/detail', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(audit_templates))
def test_audit_templates_list_by_name_detail(self):
audit_templates = self.mgr.list(name=AUDIT_TMPL1['name'], detail=True)
expect = [
('GET',
'/v1/audit_templates/detail?name=%s' % AUDIT_TMPL1['name'],
{},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(audit_templates))
def test_audit_templates_list_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.audit_template.AuditTemplateManager(
self.api)
audit_templates = self.mgr.list(limit=1)
expect = [
('GET', '/v1/audit_templates/?limit=1', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(audit_templates, HasLength(1))
def test_audit_templates_list_pagination_no_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.audit_template.AuditTemplateManager(
self.api)
audit_templates = self.mgr.list(limit=0)
expect = [
('GET', '/v1/audit_templates', {}, None),
('GET', '/v1/audit_templates/?limit=1', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertThat(audit_templates, HasLength(2))
def test_audit_templates_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.audit_template.AuditTemplateManager(
self.api)
audit_templates = self.mgr.list(sort_key='updated_at')
expect = [
('GET', '/v1/audit_templates/?sort_key=updated_at', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(3, len(audit_templates))
def test_audit_templates_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.audit_template.AuditTemplateManager(
self.api)
audit_templates = self.mgr.list(sort_dir='desc')
expect = [
('GET', '/v1/audit_templates/?sort_dir=desc', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(3, len(audit_templates))
def test_audit_templates_show(self):
audit_template = self.mgr.get(AUDIT_TMPL1['uuid'])
expect = [
('GET', '/v1/audit_templates/%s' % AUDIT_TMPL1['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(AUDIT_TMPL1['uuid'], audit_template.uuid)
self.assertEqual(AUDIT_TMPL1['name'], audit_template.name)
self.assertEqual(AUDIT_TMPL1['description'],
audit_template.description)
self.assertEqual(AUDIT_TMPL1['host_aggregate'],
audit_template.host_aggregate)
self.assertEqual(AUDIT_TMPL1['goal'], audit_template.goal)
self.assertEqual(AUDIT_TMPL1['extra'],
audit_template.extra)
def test_audit_templates_show_by_name(self):
audit_template = self.mgr.get(urlparse.quote(AUDIT_TMPL1['name']))
expect = [
('GET',
urlparse.quote('/v1/audit_templates/%s' % AUDIT_TMPL1['name']),
{}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(AUDIT_TMPL1['uuid'], audit_template.uuid)
self.assertEqual(AUDIT_TMPL1['name'], audit_template.name)
self.assertEqual(AUDIT_TMPL1['description'],
audit_template.description)
self.assertEqual(AUDIT_TMPL1['host_aggregate'],
audit_template.host_aggregate)
self.assertEqual(AUDIT_TMPL1['goal'], audit_template.goal)
self.assertEqual(AUDIT_TMPL1['extra'],
audit_template.extra)
def test_create(self):
audit_template = self.mgr.create(**CREATE_AUDIT_TEMPLATE)
expect = [
('POST', '/v1/audit_templates', {}, CREATE_AUDIT_TEMPLATE),
]
self.assertEqual(expect, self.api.calls)
self.assertTrue(audit_template)
def test_delete(self):
audit_template = self.mgr.delete(audit_template_id=AUDIT_TMPL1['uuid'])
expect = [
('DELETE',
'/v1/audit_templates/%s' % AUDIT_TMPL1['uuid'],
{},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertIsNone(audit_template)
def test_update(self):
patch = {'op': 'replace',
'value': NEW_NAME,
'path': '/name'}
audit_template = self.mgr.update(audit_template_id=AUDIT_TMPL1['uuid'],
patch=patch)
expect = [
('PATCH',
'/v1/audit_templates/%s' % AUDIT_TMPL1['uuid'],
{},
patch),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(NEW_NAME, audit_template.name)

View File

@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 IBM Corp
#
# 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 watcherclient.common import utils as commonutils
from watcherclient.openstack.common import cliutils
from watcherclient.tests import utils
import watcherclient.v1.audit_template_shell as at_shell
class AuditTemplateShellTest(utils.BaseTestCase):
def test_do_audit_template_show(self):
actual = {}
fake_print_dict = lambda data, *args, **kwargs: actual.update(data)
with mock.patch.object(cliutils, 'print_dict', fake_print_dict):
audit_template = object()
at_shell._print_audit_template_show(audit_template)
exp = [
'uuid', 'created_at', 'updated_at', 'deleted_at',
'description', 'host_aggregate', 'name',
'extra', 'goal']
act = actual.keys()
self.assertEqual(sorted(exp), sorted(act))
def test_do_audit_template_show_by_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'audit-template', 'a5199d0e-0702-4613-9234-5ae2af8dafea')
at_shell.do_audit_template_show(client_mock, args)
client_mock.audit_template.get.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea'
)
# assert get_by_name() wasn't called
self.assertFalse(client_mock.audit_template.get_by_name.called)
def test_do_audit_template_show_by_name(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'audit-template', "a5199d0e-0702-4613-9234-5ae2af8dafea")
at_shell.do_audit_template_show(client_mock, args)
client_mock.audit_template.get.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea')
def test_do_audit_template_delete(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'audit-template',
['a5199d0e-0702-4613-9234-5ae2af8dafea'])
at_shell.do_audit_template_delete(client_mock, args)
client_mock.audit_template.delete.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea')
def test_do_audit_template_delete_multiple(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'audit-template',
['a5199d0e-0702-4613-9234-5ae2af8dafea',
'a5199d0e-0702-4613-9234-5ae2af8dafeb'])
at_shell.do_audit_template_delete(client_mock, args)
client_mock.audit_template.delete.assert_has_calls(
[mock.call('a5199d0e-0702-4613-9234-5ae2af8dafea'),
mock.call('a5199d0e-0702-4613-9234-5ae2af8dafeb')])
def test_do_audit_template_update(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'audit-template', "a5199d0e-0702-4613-9234-5ae2af8dafea")
args.op = 'add'
args.attributes = [['arg1=val1', 'arg2=val2']]
at_shell.do_audit_template_update(client_mock, args)
patch = commonutils.args_array_to_patch(
args.op,
args.attributes[0])
client_mock.audit_template.update.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea', patch)
def test_do_audit_template_create(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
at_shell.do_audit_template_create(client_mock, args)
client_mock.audit_template.create.assert_called_once_with()
def test_do_audit_template_create_with_name(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.name = 'my audit template'
at_shell.do_audit_template_create(client_mock, args)
client_mock.audit_template.create.assert_called_once_with(
name='my audit template')
def test_do_audit_template_create_with_description(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.description = 'description'
at_shell.do_audit_template_create(client_mock, args)
client_mock.audit_template.create.assert_called_once_with(
description='description')
def test_do_audit_template_create_with_aggregate(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.host_aggregate = 5
at_shell.do_audit_template_create(client_mock, args)
client_mock.audit_template.create.assert_called_once_with(
host_aggregate=5)
def test_do_audit_template_create_with_extra(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.extra = ['automatic=true']
at_shell.do_audit_template_create(client_mock, args)
client_mock.audit_template.create.assert_called_once_with(
extra={'automatic': True})

View File

@ -0,0 +1,321 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import testtools
from testtools.matchers import HasLength
from watcherclient.tests import utils
import watcherclient.v1.metric_collector
METRIC_COLLECTOR1 = {
'id': 1,
'uuid': '770ef053-ecb3-48b0-85b5-d55a2dbc6588',
'category': 'cat1',
'endpoint': 'http://metric_collector_1:6446',
}
METRIC_COLLECTOR2 = {
'id': 2,
'uuid': '67653274-eb24-c7ba-70f6-a84e73d80843',
'category': 'cat2',
}
METRIC_COLLECTOR3 = {
'id': 3,
'uuid': 'f8e47706-efcf-49a4-a5c4-af604eb492f2',
'category': 'cat2',
'endpoint': 'http://metric_collector_3:6446',
}
CREATE_METRIC_COLLECTOR = copy.deepcopy(METRIC_COLLECTOR1)
del CREATE_METRIC_COLLECTOR['id']
del CREATE_METRIC_COLLECTOR['uuid']
UPDATED_METRIC_COLLECTOR1 = copy.deepcopy(METRIC_COLLECTOR1)
NEW_ENDPOINT = 'http://metric_collector_1:6447'
UPDATED_METRIC_COLLECTOR1['endpoint'] = NEW_ENDPOINT
fake_responses = {
'/v1/metric-collectors':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR1]},
),
'POST': (
{},
CREATE_METRIC_COLLECTOR,
),
},
'/v1/metric-collectors/?category=%s' % METRIC_COLLECTOR1['category']:
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR1]},
),
},
'/v1/metric-collectors/?category=%s' % METRIC_COLLECTOR2['category']:
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR3]},
),
},
'/v1/metric-collectors/detail':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR1]},
),
},
'/v1/metric-collectors/%s' % METRIC_COLLECTOR1['uuid']:
{
'GET': (
{},
METRIC_COLLECTOR1,
),
'DELETE': (
{},
None,
),
'PATCH': (
{},
UPDATED_METRIC_COLLECTOR1,
),
},
'/v1/metric-collectors/detail?category=%s' % METRIC_COLLECTOR1['category']:
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR1]},
),
},
'/v1/metric-collectors/detail?category=%s' % METRIC_COLLECTOR2['category']:
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR3]},
),
},
}
fake_responses_pagination = {
'/v1/metric-collectors':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR1],
"next": "http://127.0.0.1:6385/v1/metric-collectors/?limit=1"}
),
},
'/v1/metric-collectors/?limit=1':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR2]}
),
},
}
fake_responses_sorting = {
'/v1/metric-collectors/?sort_key=updated_at':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR1]}
),
},
'/v1/metric-collectors/?sort_dir=desc':
{
'GET': (
{},
{"metric-collectors": [METRIC_COLLECTOR2, METRIC_COLLECTOR1]}
),
},
}
class MetricCollectorManagerTest(testtools.TestCase):
def setUp(self):
super(MetricCollectorManagerTest, self).setUp()
self.api = utils.FakeAPI(fake_responses)
self.mgr = watcherclient.v1.metric_collector \
.MetricCollectorManager(self.api)
def test_metric_collectors_list(self):
metric_collectors = self.mgr.list()
expect = [
('GET', '/v1/metric-collectors', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(metric_collectors))
def test_metric_collectors_list_by_category(self):
metric_collectors = self.mgr.list(
category=METRIC_COLLECTOR1['category']
)
expect = [
('GET',
'/v1/metric-collectors/?category=%s' %
METRIC_COLLECTOR1['category'],
{},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(metric_collectors))
def test_metric_collectors_list_by_category_bis(self):
metric_collectors = self.mgr.list(
category=METRIC_COLLECTOR2['category']
)
expect = [
('GET',
'/v1/metric-collectors/?category=%s' %
METRIC_COLLECTOR2['category'],
{},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(metric_collectors))
def test_metric_collectors_list_detail(self):
metric_collectors = self.mgr.list(detail=True)
expect = [
('GET', '/v1/metric-collectors/detail', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(metric_collectors))
def test_metric_collectors_list_by_category_detail(self):
metric_collectors = self.mgr.list(
category=METRIC_COLLECTOR1['category'],
detail=True)
expect = [
('GET',
'/v1/metric-collectors/detail?category=%s' %
METRIC_COLLECTOR1['category'],
{},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(1, len(metric_collectors))
def test_metric_collectors_list_by_category_detail_bis(self):
metric_collectors = self.mgr.list(
category=METRIC_COLLECTOR2['category'],
detail=True)
expect = [
('GET',
'/v1/metric-collectors/detail?category=%s' %
METRIC_COLLECTOR2['category'],
{},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(metric_collectors))
def test_metric_collectors_list_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.metric_collector \
.MetricCollectorManager(self.api)
metric_collectors = self.mgr.list(limit=1)
expect = [
('GET', '/v1/metric-collectors/?limit=1', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(metric_collectors, HasLength(1))
def test_metric_collectors_list_pagination_no_limit(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = watcherclient.v1.metric_collector \
.MetricCollectorManager(self.api)
metric_collectors = self.mgr.list(limit=0)
expect = [
('GET', '/v1/metric-collectors', {}, None),
('GET', '/v1/metric-collectors/?limit=1', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertThat(metric_collectors, HasLength(2))
def test_metric_collectors_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.metric_collector \
.MetricCollectorManager(self.api)
metric_collectors = self.mgr.list(sort_key='updated_at')
expect = [
('GET', '/v1/metric-collectors/?sort_key=updated_at', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(metric_collectors))
def test_metric_collectors_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = watcherclient.v1.metric_collector \
.MetricCollectorManager(self.api)
metric_collectors = self.mgr.list(sort_dir='desc')
expect = [
('GET', '/v1/metric-collectors/?sort_dir=desc', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(metric_collectors))
def test_metric_collectors_show(self):
metric_collector = self.mgr.get(METRIC_COLLECTOR1['uuid'])
expect = [
('GET', '/v1/metric-collectors/%s' %
METRIC_COLLECTOR1['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(METRIC_COLLECTOR1['uuid'], metric_collector.uuid)
self.assertEqual(METRIC_COLLECTOR1['category'],
metric_collector.category)
self.assertEqual(METRIC_COLLECTOR1['endpoint'],
metric_collector.endpoint)
def test_create(self):
metric_collector = self.mgr.create(**CREATE_METRIC_COLLECTOR)
expect = [
('POST', '/v1/metric-collectors', {}, CREATE_METRIC_COLLECTOR),
]
self.assertEqual(expect, self.api.calls)
self.assertTrue(metric_collector)
def test_delete(self):
metric_collector = self.mgr.delete(
metric_collector_id=METRIC_COLLECTOR1['uuid'])
expect = [
('DELETE', '/v1/metric-collectors/%s' %
METRIC_COLLECTOR1['uuid'], {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertIsNone(metric_collector)
def test_update(self):
patch = {'op': 'replace',
'value': NEW_ENDPOINT,
'path': '/endpoint'}
metric_collector = self.mgr.update(
metric_collector_id=METRIC_COLLECTOR1['uuid'], patch=patch)
expect = [
('PATCH', '/v1/metric-collectors/%s' %
METRIC_COLLECTOR1['uuid'], {}, patch),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(NEW_ENDPOINT, metric_collector.endpoint)

View File

@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 IBM Corp
#
# 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 watcherclient.common import utils as commonutils
from watcherclient.openstack.common import cliutils
from watcherclient.tests import utils
import watcherclient.v1.metric_collector_shell as mc_shell
class MetricCollectorShellTest(utils.BaseTestCase):
def test_do_metric_collector_show(self):
actual = {}
fake_print_dict = lambda data, *args, **kwargs: actual.update(data)
with mock.patch.object(cliutils, 'print_dict', fake_print_dict):
metric_collector = object()
mc_shell._print_metric_collector_show(metric_collector)
exp = ['uuid', 'created_at', 'updated_at', 'deleted_at',
'category', 'endpoint']
act = actual.keys()
self.assertEqual(sorted(exp), sorted(act))
def test_do_metric_collector_show_by_uuid(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.metric_collector = 'a5199d0e-0702-4613-9234-5ae2af8dafea'
mc_shell.do_metric_collector_show(client_mock, args)
client_mock.metric_collector.get.assert_called_once_with(
'a5199d0e-0702-4613-9234-5ae2af8dafea'
)
# assert get_by_name() wasn't called
self.assertFalse(client_mock.metric_collector.get_by_name.called)
def test_do_metric_collector_delete(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.metric_collector = ['metric_collector_uuid']
mc_shell.do_metric_collector_delete(client_mock, args)
client_mock.metric_collector.delete.assert_called_once_with(
'metric_collector_uuid'
)
def test_do_metric_collector_delete_multiple(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.metric_collector = ['metric_collector_uuid1',
'metric_collector_uuid2']
mc_shell.do_metric_collector_delete(client_mock, args)
client_mock.metric_collector.delete.assert_has_calls(
[mock.call('metric_collector_uuid1'),
mock.call('metric_collector_uuid2')])
def test_do_metric_collector_update(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
setattr(args, 'metric-collector', "metric_collector_uuid")
args.op = 'add'
args.attributes = [['arg1=val1', 'arg2=val2']]
mc_shell.do_metric_collector_update(client_mock, args)
patch = commonutils.args_array_to_patch(
args.op,
args.attributes[0])
client_mock.metric_collector.update.assert_called_once_with(
'metric_collector_uuid', patch)
def test_do_metric_collector_create(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
mc_shell.do_metric_collector_create(client_mock, args)
client_mock.metric_collector.create.assert_called_once_with()
def test_do_metric_collector_create_with_category(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.category = 'mc_category'
mc_shell.do_metric_collector_create(client_mock, args)
client_mock.metric_collector.create.assert_called_once_with(
category='mc_category')
def test_do_metric_collector_create_with_endpoint(self):
client_mock = mock.MagicMock()
args = mock.MagicMock()
args.endpoint = 'mc_endpoint'
mc_shell.do_metric_collector_create(client_mock, args)
client_mock.metric_collector.create.assert_called_once_with(
endpoint='mc_endpoint')

View File

View File

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from watcherclient.common import base
from watcherclient.common import utils
class Action(base.Resource):
def __repr__(self):
return "<Action %s>" % self._info
class ActionManager(base.Manager):
resource_class = Action
@staticmethod
def _path(id=None):
return '/v1/actions/%s' % id if id else '/v1/actions'
def list(self, action_plan=None, audit=None, limit=None, sort_key=None,
sort_dir=None, detail=False):
"""Retrieve a list of action.
:param action_plan: UUID of the action plan
:param audit: UUID of the audit
:param limit: The maximum number of results to return per
request, if:
1) limit > 0, the maximum number of actions to return.
2) limit == 0, return the entire list of actions.
3) limit param is NOT specified (None), the number of items
returned respect the maximum imposed by the Watcher API
(see Watcher's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:param detail: Optional, boolean whether to return detailed information
about actions.
:returns: A list of actions.
"""
if limit is not None:
limit = int(limit)
filters = utils.common_filters(limit, sort_key, sort_dir)
if action_plan is not None:
filters.append('action_plan_uuid=%s' % action_plan)
if audit is not None:
filters.append('audit_uuid=%s' % audit)
path = ''
if detail:
path += 'detail'
if filters:
path += '?' + '&'.join(filters)
if limit is None:
return self._list(self._path(path), "actions")
else:
return self._list_pagination(self._path(path), "actions",
limit=limit)
def get(self, action_id):
try:
return self._list(self._path(action_id))[0]
except IndexError:
return None
def delete(self, action_id):
return self._delete(self._path(action_id))
def update(self, action_id, patch):
return self._update(self._path(action_id), patch)

View File

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from watcherclient.common import base
from watcherclient.common import utils
# from watcherclient import exceptions as exc
class ActionPlan(base.Resource):
def __repr__(self):
return "<ActionPlan %s>" % self._info
class ActionPlanManager(base.Manager):
resource_class = ActionPlan
@staticmethod
def _path(id=None):
return '/v1/action_plans/%s' % id if id else '/v1/action_plans'
def list(self, audit=None, limit=None, sort_key=None,
sort_dir=None, detail=False):
"""Retrieve a list of action plan.
:param audit: Name of the audit
:param limit: The maximum number of results to return per
request, if:
1) limit > 0, the maximum number of action plans to return.
2) limit == 0, return the entire list of action plans.
3) limit param is NOT specified (None), the number of items
returned respect the maximum imposed by the Watcher API
(see Watcher's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:param detail: Optional, boolean whether to return detailed information
about action plans.
:returns: A list of action plans.
"""
if limit is not None:
limit = int(limit)
filters = utils.common_filters(limit, sort_key, sort_dir)
if audit is not None:
filters.append('audit_uuid=%s' % audit)
path = ''
if detail:
path += 'detail'
if filters:
path += '?' + '&'.join(filters)
if limit is None:
return self._list(self._path(path), "action_plans")
else:
return self._list_pagination(self._path(path), "action_plans",
limit=limit)
def get(self, action_plan_id):
try:
return self._list(self._path(action_plan_id))[0]
except IndexError:
return None
def delete(self, action_plan_id):
return self._delete(self._path(action_plan_id))
def update(self, action_plan_id, patch):
return self._update(self._path(action_plan_id), patch)

View File

@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# import argparse
from oslo_utils import uuidutils
from watcherclient.common import utils
from watcherclient.openstack.common.apiclient.exceptions import ValidationError
from watcherclient.openstack.common import cliutils
from watcherclient.v1 import resource_fields as res_fields
def _print_action_plan_show(action_plan):
fields = res_fields.ACTION_PLAN_FIELDS
data = dict([(f, getattr(action_plan, f, '')) for f in fields])
cliutils.print_dict(data, wrap=72)
@cliutils.arg(
'action-plan',
metavar='<action-plan>',
help="UUID of the action_plan.")
def do_action_plan_show(cc, args):
"""Show detailed information about an action plan."""
action_plan_uuid = getattr(args, 'action-plan')
if uuidutils.is_uuid_like(action_plan_uuid):
action_plan = cc.action_plan.get(action_plan_uuid)
_print_action_plan_show(action_plan)
else:
raise ValidationError()
@cliutils.arg(
'--audit',
metavar='<audit>',
help='UUID of an audit used for filtering.')
@cliutils.arg(
'--detail',
dest='detail',
action='store_true',
default=False,
help="Show detailed information about action plans.")
@cliutils.arg(
'--limit',
metavar='<limit>',
type=int,
help='Maximum number of action plans to return per request, '
'0 for no limit. Default is the maximum number used '
'by the Watcher API Service.')
@cliutils.arg(
'--sort-key',
metavar='<field>',
help='Action Plan field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
metavar='<direction>',
choices=['asc', 'desc'],
help='Sort direction: "asc" (the default) or "desc".')
def do_action_plan_list(cc, args):
"""List the action plans."""
params = {}
if args.audit is not None:
params['audit'] = args.audit
if args.detail:
fields = res_fields.ACTION_PLAN_FIELDS
field_labels = res_fields.ACTION_PLAN_FIELD_LABELS
else:
fields = res_fields.ACTION_PLAN_SHORT_LIST_FIELDS
field_labels = res_fields.ACTION_PLAN_SHORT_LIST_FIELD_LABELS
params.update(utils.common_params_for_list(args,
fields,
field_labels))
action_plan = cc.action_plan.list(**params)
cliutils.print_list(action_plan, fields,
field_labels=field_labels,
sortby_index=None)
@cliutils.arg(
'action-plan',
metavar='<action-plan>',
nargs='+',
help="UUID of the action plan.")
def do_action_plan_delete(cc, args):
"""Delete an action plan."""
for p in getattr(args, 'action-plan'):
if uuidutils.is_uuid_like(p):
cc.action_plan.delete(p)
print ('Deleted action plan %s' % p)
else:
raise ValidationError()
@cliutils.arg(
'action-plan',
metavar='<action-plan>',
help="UUID of the action plan.")
@cliutils.arg(
'op',
metavar='<op>',
choices=['add', 'replace', 'remove'],
help="Operation: 'add', 'replace', or 'remove'.")
@cliutils.arg(
'attributes',
metavar='<path=value>',
nargs='+',
action='append',
default=[],
help="Attribute to add, replace, or remove. Can be specified multiple "
"times. For 'remove', only <path> is necessary.")
def do_action_plan_update(cc, args):
"""Update information about an action plan."""
action_plan_uuid = getattr(args, 'action-plan')
if uuidutils.is_uuid_like(action_plan_uuid):
patch = utils.args_array_to_patch(args.op, args.attributes[0])
action_plan = cc.action_plan.update(action_plan_uuid, patch)
_print_action_plan_show(action_plan)
else:
raise ValidationError()
@cliutils.arg('action-plan',
metavar='<action-plan>',
help="UUID of the action plan.")
def do_action_plan_start(cc, args):
"""Execute an action plan."""
action_plan_uuid = getattr(args, 'action-plan')
if uuidutils.is_uuid_like(action_plan_uuid):
args.op = 'replace'
args.attributes = [['state=STARTING']]
patch = utils.args_array_to_patch(
args.op,
args.attributes[0])
action_plan = cc.action_plan.update(action_plan_uuid, patch)
_print_action_plan_show(action_plan)
else:
raise ValidationError()

View File

@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# import argparse
from oslo_utils import uuidutils
from watcherclient.common import utils
from watcherclient.openstack.common.apiclient.exceptions import ValidationError
from watcherclient.openstack.common import cliutils
from watcherclient.v1 import resource_fields as res_fields
def _print_action_show(action):
fields = res_fields.ACTION_FIELDS
data = dict([(f, getattr(action, f, '')) for f in fields])
cliutils.print_dict(data, wrap=72)
@cliutils.arg(
'action',
metavar='<action>',
help="UUID of the action")
def do_action_show(cc, args):
"""Show detailed information about an action."""
if uuidutils.is_uuid_like(args.action):
action = cc.action.get(args.action)
_print_action_show(action)
else:
raise ValidationError()
@cliutils.arg(
'--action-plan',
metavar='<action_plan>',
help='UUID of the action plan used for filtering.')
@cliutils.arg(
'--audit',
metavar='<audit>',
help=' UUID of the audit used for filtering.')
@cliutils.arg(
'--detail',
dest='detail',
action='store_true',
default=False,
help="Show detailed information about actions.")
@cliutils.arg(
'--limit',
metavar='<limit>',
type=int,
help='Maximum number of actions to return per request, '
'0 for no limit. Default is the maximum number used '
'by the Watcher API Service.')
@cliutils.arg(
'--sort-key',
metavar='<field>',
help='Action field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
metavar='<direction>',
choices=['asc', 'desc'],
help='Sort direction: "asc" (the default) or "desc".')
def do_action_list(cc, args):
"""List the actions."""
params = {}
if args.action_plan is not None:
params['action_plan'] = args.action_plan
if args.audit is not None:
params['audit'] = args.audit
if args.detail:
fields = res_fields.ACTION_FIELDS
field_labels = res_fields.ACTION_FIELD_LABELS
else:
fields = res_fields.ACTION_SHORT_LIST_FIELDS
field_labels = res_fields.ACTION_SHORT_LIST_FIELD_LABELS
params.update(utils.common_params_for_list(args,
fields,
field_labels))
action = cc.action.list(**params)
cliutils.print_list(action, fields,
field_labels=field_labels,
sortby_index=None)
@cliutils.arg(
'action',
metavar='<action>',
nargs='+',
help="UUID of the action.")
def do_action_delete(cc, args):
"""Delete an action."""
for p in args.action:
if uuidutils.is_uuid_like(p):
cc.action.delete(p)
print ('Deleted action %s' % p)
else:
raise ValidationError()
@cliutils.arg('action', metavar='<action>', help="UUID of the action.")
@cliutils.arg(
'op',
metavar='<op>',
choices=['add', 'replace', 'remove'],
help="Operation: 'add', 'replace', or 'remove'.")
@cliutils.arg(
'attributes',
metavar='<path=value>',
nargs='+',
action='append',
default=[],
help="Attribute to add, replace, or remove. Can be specified multiple "
"times. For 'remove', only <path> is necessary.")
def do_action_update(cc, args):
"""Update information about an action."""
if uuidutils.is_uuid_like(args.action):
patch = utils.args_array_to_patch(args.op, args.attributes[0])
action = cc.action.update(args.action, patch)
_print_action_show(action)
else:
raise ValidationError()

100
watcherclient/v1/audit.py Normal file
View File

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from watcherclient.common import base
from watcherclient.common import utils
from watcherclient import exceptions as exc
CREATION_ATTRIBUTES = ['audit_template_uuid', 'deadline', 'type']
class Audit(base.Resource):
def __repr__(self):
return "<Audit %s>" % self._info
class AuditManager(base.Manager):
resource_class = Audit
@staticmethod
def _path(id=None):
return '/v1/audits/%s' % id if id else '/v1/audits'
def list(self, audit_template=None, limit=None, sort_key=None,
sort_dir=None, detail=False):
"""Retrieve a list of audit.
:param audit_template: Name of the audit
:param name: Name of the audit
:param limit: The maximum number of results to return per
request, if:
1) limit > 0, the maximum number of audits to return.
2) limit == 0, return the entire list of audits.
3) limit param is NOT specified (None), the number of items
returned respect the maximum imposed by the Watcher API
(see Watcher's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:param detail: Optional, boolean whether to return detailed information
about audits.
:returns: A list of audits.
"""
if limit is not None:
limit = int(limit)
filters = utils.common_filters(limit, sort_key, sort_dir)
if audit_template is not None:
filters.append('audit_template=%s' % audit_template)
path = ''
if detail:
path += 'detail'
if filters:
path += '?' + '&'.join(filters)
if limit is None:
return self._list(self._path(path), "audits")
else:
return self._list_pagination(self._path(path), "audits",
limit=limit)
def create(self, **kwargs):
new = {}
for (key, value) in kwargs.items():
if key in CREATION_ATTRIBUTES:
new[key] = value
else:
raise exc.InvalidAttribute()
return self._create(self._path(), new)
def get(self, audit_id):
try:
return self._list(self._path(audit_id))[0]
except IndexError:
return None
def delete(self, audit_id):
return self._delete(self._path(audit_id))
def update(self, audit_id, patch):
return self._update(self._path(audit_id), patch)

View File

@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# import argparse
from oslo_utils import uuidutils
from watcherclient.common import utils
from watcherclient.openstack.common.apiclient.exceptions import ValidationError
from watcherclient.openstack.common import cliutils
from watcherclient.v1 import resource_fields as res_fields
def _print_audit_show(audit):
fields = res_fields.AUDIT_FIELDS
data = dict([(f, getattr(audit, f, '')) for f in fields])
cliutils.print_dict(data, wrap=72)
@cliutils.arg(
'audit',
metavar='<audit>',
help="UUID of the audit.")
def do_audit_show(cc, args):
"""Show detailed information about an audit."""
if uuidutils.is_uuid_like(args.audit):
audit = cc.audit.get(args.audit)
_print_audit_show(audit)
else:
raise ValidationError()
@cliutils.arg(
'--audit-template',
metavar='<audit_template>',
dest='audit_template',
help='Name or UUID of an audit template used for filtering.')
@cliutils.arg(
'--detail',
dest='detail',
action='store_true',
default=False,
help="Show detailed information about audits.")
@cliutils.arg(
'--limit',
metavar='<limit>',
type=int,
help='Maximum number of audits to return per request, '
'0 for no limit. Default is the maximum number used '
'by the Watcher API Service.')
@cliutils.arg(
'--sort-key',
metavar='<field>',
help='Audit field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
metavar='<direction>',
choices=['asc', 'desc'],
help='Sort direction: "asc" (the default) or "desc".')
def do_audit_list(cc, args):
"""List the audits."""
params = {}
if args.audit_template is not None:
params['audit_template'] = args.audit_template
if args.detail:
fields = res_fields.AUDIT_FIELDS
field_labels = res_fields.AUDIT_FIELD_LABELS
else:
fields = res_fields.AUDIT_SHORT_LIST_FIELDS
field_labels = res_fields.AUDIT_SHORT_LIST_FIELD_LABELS
# params.update(utils.common_params_for_list(args, fields, field_labels))
audit = cc.audit.list(**params)
cliutils.print_list(audit, fields,
field_labels=field_labels,
sortby_index=None)
@cliutils.arg(
'-a', '--audit-template',
required=True,
dest='audit_template_uuid',
metavar='<audit_template>',
help='Audit template used for this audit (name or uuid).')
@cliutils.arg(
'-d', '--deadline',
dest='deadline',
metavar='<deadline>',
help='Descrition of the audit.')
@cliutils.arg(
'-t', '--type',
dest='type',
metavar='<type>',
default='ONESHOT',
help="Audit type.")
def do_audit_create(cc, args):
"""Create a new audit."""
field_list = ['audit_template_uuid', 'type', 'deadline']
fields = dict((k, v) for (k, v) in vars(args).items()
if k in field_list and not (v is None))
audit = cc.audit.create(**fields)
field_list.append('uuid')
data = dict([(f, getattr(audit, f, '')) for f in field_list])
cliutils.print_dict(data, wrap=72)
@cliutils.arg(
'audit',
metavar='<audit>',
nargs='+',
help="UUID of the audit.")
def do_audit_delete(cc, args):
"""Delete an audit."""
for p in args.audit:
if uuidutils.is_uuid_like(p):
cc.audit.delete(p)
print ('Deleted audit %s' % p)
else:
raise ValidationError()
@cliutils.arg(
'audit',
metavar='<audit>',
help="UUID of the audit.")
@cliutils.arg(
'op',
metavar='<op>',
choices=['add', 'replace', 'remove'],
help="Operation: 'add', 'replace', or 'remove'.")
@cliutils.arg(
'attributes',
metavar='<path=value>',
nargs='+',
action='append',
default=[],
help="Attribute to add, replace, or remove. Can be specified multiple "
"times. For 'remove', only <path> is necessary.")
def do_audit_update(cc, args):
"""Update information about an audit."""
if uuidutils.is_uuid_like(args.audit):
patch = utils.args_array_to_patch(args.op, args.attributes[0])
audit = cc.audit.update(args.audit, patch)
_print_audit_show(audit)
else:
raise ValidationError()

View File

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from watcherclient.common import base
from watcherclient.common import utils
from watcherclient import exceptions as exc
CREATION_ATTRIBUTES = ['host_aggregate', 'description', 'name',
'extra', 'goal']
class AuditTemplate(base.Resource):
def __repr__(self):
return "<AuditTemplate %s>" % self._info
class AuditTemplateManager(base.Manager):
resource_class = AuditTemplate
@staticmethod
def _path(id=None):
return '/v1/audit_templates/%s' % id if id else '/v1/audit_templates'
def list(self, name=None, limit=None, sort_key=None,
sort_dir=None, detail=False):
"""Retrieve a list of audit template.
:param name: Name of the audit template
:param limit: The maximum number of results to return per
request, if:
1) limit > 0, the maximum number of audit templates to return.
2) limit == 0, return the entire list of audit_templates.
3) limit param is NOT specified (None), the number of items
returned respect the maximum imposed by the Watcher API
(see Watcher's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:param detail: Optional, boolean whether to return detailed information
about audit_templates.
:returns: A list of audit templates.
"""
if limit is not None:
limit = int(limit)
filters = utils.common_filters(limit, sort_key, sort_dir)
if name is not None:
filters.append('name=%s' % name)
path = ''
if detail:
path += 'detail'
if filters:
path += '?' + '&'.join(filters)
if limit is None:
return self._list(self._path(path), "audit_templates")
else:
return self._list_pagination(self._path(path), "audit_templates",
limit=limit)
def get(self, audit_template_id):
try:
return self._list(self._path(audit_template_id))[0]
except IndexError:
return None
def create(self, **kwargs):
new = {}
for (key, value) in kwargs.items():
if key in CREATION_ATTRIBUTES:
new[key] = value
else:
raise exc.InvalidAttribute()
return self._create(self._path(), new)
def delete(self, audit_template_id):
return self._delete(self._path(audit_template_id))
def update(self, audit_template_id, patch):
return self._update(self._path(audit_template_id), patch)

View File

@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# import argparse
from watcherclient.common import utils
from watcherclient.openstack.common import cliutils
from watcherclient.v1 import resource_fields as res_fields
def _print_audit_template_show(audit_template):
fields = res_fields.AUDIT_TEMPLATE_FIELDS
data = dict([(f, getattr(audit_template, f, '')) for f in fields])
cliutils.print_dict(data, wrap=72)
@cliutils.arg(
'audit-template',
metavar='<audit-template>',
help="Name or UUID of the audit template.")
def do_audit_template_show(cc, args):
"""Show detailed information about a audit template."""
audit_template = cc.audit_template.get(getattr(args, 'audit-template'))
_print_audit_template_show(audit_template)
@cliutils.arg(
'--detail',
dest='detail',
action='store_true',
default=False,
help="Show detailed information about audit templates.")
@cliutils.arg(
'--name',
metavar='<name>',
help='Only show information for the audit template with this name.')
@cliutils.arg(
'--goal',
metavar='<goal>',
help='Name the goal used for filtering.')
@cliutils.arg(
'--limit',
metavar='<limit>',
type=int,
help='Maximum number of audit templates to return per request, '
'0 for no limit. Default is the maximum number used '
'by the Watcher API Service.')
@cliutils.arg(
'--sort-key',
metavar='<field>',
help='Audit template field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
metavar='<direction>',
choices=['asc', 'desc'],
help='Sort direction: "asc" (the default) or "desc".')
def do_audit_template_list(cc, args):
"""List the audit templates."""
params = {}
if args.name is not None:
params['name'] = args.name
if args.goal is not None:
params['goal'] = args.goal
if args.detail:
fields = res_fields.AUDIT_TEMPLATE_FIELDS
field_labels = res_fields.AUDIT_TEMPLATE_FIELD_LABELS
else:
fields = res_fields.AUDIT_TEMPLATE_SHORT_LIST_FIELDS
field_labels = res_fields.AUDIT_TEMPLATE_SHORT_LIST_FIELD_LABELS
params.update(utils.common_params_for_list(args,
fields,
field_labels))
audit_template = cc.audit_template.list(**params)
cliutils.print_list(audit_template, fields,
field_labels=field_labels,
sortby_index=None)
@cliutils.arg(
'name',
metavar='<name>',
help='Name for this audit template.')
@cliutils.arg(
'goal',
metavar='<goal>',
help='Goal Type associated to this audit template.')
@cliutils.arg(
'-d', '--description',
metavar='<description>',
help='Descrition of the audit template.')
@cliutils.arg(
'-e', '--extra',
metavar='<key=value>',
action='append',
help="Record arbitrary key/value metadata. "
"Can be specified multiple times.")
@cliutils.arg(
'-a', '--host-aggregate',
dest='host_aggregate',
metavar='<host-aggregate>',
help='Name or ID of the host aggregate targeted by this audit template.')
def do_audit_template_create(cc, args):
"""Create a new audit template."""
field_list = ['host_aggregate', 'description', 'name', 'extra', 'goal']
fields = dict((k, v) for (k, v) in vars(args).items()
if k in field_list and not (v is None))
fields = utils.args_array_to_dict(fields, 'extra')
audit_template = cc.audit_template.create(**fields)
field_list.append('uuid')
data = dict([(f, getattr(audit_template, f, '')) for f in field_list])
cliutils.print_dict(data, wrap=72)
@cliutils.arg(
'audit-template',
metavar='<audit-template>',
nargs='+',
help="UUID or name of the audit template.")
def do_audit_template_delete(cc, args):
"""Delete an audit template."""
for p in getattr(args, 'audit-template'):
cc.audit_template.delete(p)
print ('Deleted audit template %s' % p)
@cliutils.arg(
'audit-template',
metavar='<audit-template>',
help="UUID or name of the audit template.")
@cliutils.arg(
'op',
metavar='<op>',
choices=['add', 'replace', 'remove'],
help="Operation: 'add', 'replace', or 'remove'.")
@cliutils.arg(
'attributes',
metavar='<path=value>',
nargs='+',
action='append',
default=[],
help="Attribute to add, replace, or remove. Can be specified multiple "
"times. For 'remove', only <path> is necessary.")
def do_audit_template_update(cc, args):
"""Update information about an audit template."""
patch = utils.args_array_to_patch(args.op, args.attributes[0])
audit_template = cc.audit_template.update(getattr(args, 'audit-template'),
patch)
_print_audit_template_show(audit_template)

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
#
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from watcherclient.common import http
from watcherclient.v1 import action
from watcherclient.v1 import action_plan
from watcherclient.v1 import audit
from watcherclient.v1 import audit_template
from watcherclient.v1 import metric_collector
class Client(object):
"""Client for the Watcher v1 API.
:param string endpoint: A user-supplied endpoint URL for the watcher
service.
:param function token: Provides token for authentication.
:param integer timeout: Allows customization of the timeout for client
http requests. (optional)
"""
def __init__(self, *args, **kwargs):
"""Initialize a new client for the Watcher v1 API."""
self.http_client = http._construct_http_client(*args, **kwargs)
self.audit = audit.AuditManager(self.http_client)
self.audit_template = audit_template.AuditTemplateManager(
self.http_client)
self.action = action.ActionManager(self.http_client)
self.action_plan = action_plan.ActionPlanManager(self.http_client)
self.metric_collector = metric_collector.MetricCollectorManager(
self.http_client
)

View File

@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from watcherclient.common import base
from watcherclient.common import utils
from watcherclient import exceptions as exc
CREATION_ATTRIBUTES = ['endpoint', 'category']
class MetricCollector(base.Resource):
def __repr__(self):
return "<MetricCollector %s>" % self._info
class MetricCollectorManager(base.Manager):
resource_class = MetricCollector
@staticmethod
def _path(id=None):
return \
'/v1/metric-collectors/%s' % id if id else '/v1/metric-collectors'
def list(self, category=None, limit=None, sort_key=None,
sort_dir=None, detail=False):
"""Retrieve a list of metric collector.
:param category: Optional, Metric category, to get all metric
collectors mapped with this category.
:param limit: The maximum number of results to return per
request, if:
1) limit > 0, the maximum number of metric collectors to return.
2) limit == 0, return the entire list of metriccollectors.
3) limit param is NOT specified (None), the number of items
returned respect the maximum imposed by the Watcher API
(see Watcher's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:param detail: Optional, boolean whether to return detailed information
about metric collectors.
:returns: A list of metric collectors.
"""
if limit is not None:
limit = int(limit)
filters = utils.common_filters(limit, sort_key, sort_dir)
if category is not None:
filters.append('category=%s' % category)
path = ''
if detail:
path += 'detail'
if filters:
path += '?' + '&'.join(filters)
if limit is None:
return self._list(self._path(path), "metric-collectors")
else:
return self._list_pagination(self._path(path), "metric-collectors",
limit=limit)
def get(self, metric_collector_id):
try:
return self._list(self._path(metric_collector_id))[0]
except IndexError:
return None
def create(self, **kwargs):
new = {}
for (key, value) in kwargs.items():
if key in CREATION_ATTRIBUTES:
new[key] = value
else:
raise exc.InvalidAttribute()
return self._create(self._path(), new)
def delete(self, metric_collector_id):
return self._delete(self._path(metric_collector_id))
def update(self, metric_collector_id, patch):
return self._update(self._path(metric_collector_id), patch)

View File

@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
#
# Copyright 2013 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# import argparse
from watcherclient.common import utils
from watcherclient.openstack.common import cliutils
from watcherclient.v1 import resource_fields as res_fields
def _print_metric_collector_show(metric_collector):
fields = res_fields.METRIC_COLLECTOR_FIELDS
data = dict([(f, getattr(metric_collector, f, '')) for f in fields])
cliutils.print_dict(data, wrap=72)
@cliutils.arg(
'metric_collector',
metavar='<metric_collector>',
help="UUID of the metric collector")
def do_metric_collector_show(cc, args):
"""Show detailed information about a metric collector."""
metric_collector = cc.metric_collector.get(args.metric_collector)
_print_metric_collector_show(metric_collector)
@cliutils.arg(
'--category',
metavar='<category>',
help='Only show information for metric collectors with this category.')
@cliutils.arg(
'--detail',
dest='detail',
action='store_true',
default=False,
help="Show detailed information about metric collectors.")
@cliutils.arg(
'--limit',
metavar='<limit>',
type=int,
help='Maximum number of metric collectors to return per request, '
'0 for no limit. Default is the maximum number used '
'by the Watcher API Service.')
@cliutils.arg(
'--sort-key',
metavar='<field>',
help='Metric collector field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
metavar='<direction>',
choices=['asc', 'desc'],
help='Sort direction: "asc" (the default) or "desc".')
def do_metric_collector_list(cc, args):
"""List the metric collectors."""
params = {}
if args.detail:
fields = res_fields.METRIC_COLLECTOR_FIELDS
field_labels = res_fields.METRIC_COLLECTOR_FIELD_LABELS
else:
fields = res_fields.METRIC_COLLECTOR_SHORT_LIST_FIELDS
field_labels = res_fields.METRIC_COLLECTOR_SHORT_LIST_FIELD_LABELS
params.update(utils.common_params_for_list(args,
fields,
field_labels))
metric_collector = cc.metric_collector.list(**params)
cliutils.print_list(metric_collector, fields,
field_labels=field_labels,
sortby_index=None)
@cliutils.arg(
'-c', '--category',
metavar='<category>',
required=True,
help='Metric category.')
@cliutils.arg(
'-e', '--endpoint-url',
required=True,
metavar='<goal>',
help='URL towards which publish metric data.')
def do_metric_collector_create(cc, args):
"""Create a new metric collector."""
field_list = ['category', 'endpoint']
fields = dict((k, v) for (k, v) in vars(args).items()
if k in field_list and not (v is None))
metric_collector = cc.metric_collector.create(**fields)
field_list.append('uuid')
data = dict([(f, getattr(metric_collector, f, '')) for f in field_list])
cliutils.print_dict(data, wrap=72)
@cliutils.arg(
'metric_collector',
metavar='<metric_collector>',
nargs='+',
help="UUID of the metric collector.")
def do_metric_collector_delete(cc, args):
"""Delete a metric collector."""
for p in args.metric_collector:
cc.metric_collector.delete(p)
print ('Deleted metric collector %s' % p)
@cliutils.arg(
'metric_collector',
metavar='<metric_collector>',
help="UUID of the metric collector.")
@cliutils.arg(
'op',
metavar='<op>',
choices=['add', 'replace', 'remove'],
help="Operation: 'add', 'replace', or 'remove'.")
@cliutils.arg(
'attributes',
metavar='<path=value>',
nargs='+',
action='append',
default=[],
help="Attribute to add, replace, or remove. Can be specified multiple "
"times. For 'remove', only <path> is necessary.")
def do_metric_collector_update(cc, args):
"""Update information about a metric collector."""
patch = utils.args_array_to_patch(args.op, args.attributes[0])
metric_collector = cc.metric_collector.update(
getattr(args, 'metric-collector'), patch)
_print_metric_collector_show(metric_collector)

View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 b<>com
# 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.
# Audit Template
AUDIT_TEMPLATE_FIELDS = [
'uuid', 'created_at', 'updated_at', 'deleted_at',
'description', 'host_aggregate', 'name',
'extra', 'goal']
AUDIT_TEMPLATE_FIELD_LABELS = [
'UUID', 'Created At', 'Updated At', 'Deleted At',
'Description', 'Host Aggregate ID or Name', 'Name',
'Extra', 'Goal Type']
AUDIT_TEMPLATE_SHORT_LIST_FIELDS = ['uuid', 'name']
AUDIT_TEMPLATE_SHORT_LIST_FIELD_LABELS = ['UUID', 'Name']
# Audit
AUDIT_FIELDS = ['uuid', 'created_at', 'updated_at', 'deleted_at',
'deadline', 'state', 'type', 'audit_template_uuid']
AUDIT_FIELD_LABELS = ['UUID', 'Created At', 'Updated At', 'Deleted At',
'Deadline', 'State', 'Type', 'Audit Template']
AUDIT_SHORT_LIST_FIELDS = ['uuid', 'type', 'audit_template_uuid', 'state']
AUDIT_SHORT_LIST_FIELD_LABELS = ['UUID', 'Type', 'Audit Template', 'State']
# Action Plan
ACTION_PLAN_FIELDS = ['uuid', 'created_at', 'updated_at', 'deleted_at',
'audit_uuid', 'state']
ACTION_PLAN_FIELD_LABELS = ['UUID', 'Created At', 'Updated At', 'Deleted At',
'Audit', 'State']
ACTION_PLAN_SHORT_LIST_FIELDS = ['uuid', 'audit_uuid', 'state', 'updated_at']
ACTION_PLAN_SHORT_LIST_FIELD_LABELS = ['UUID', 'Audit', 'State', 'Updated At']
# Action
ACTION_FIELDS = ['uuid', 'created_at', 'updated_at', 'deleted_at', 'next_uuid',
'description', 'alarm', 'state', 'action_plan_uuid',
'action_type', 'applies_to', 'src', 'dst', 'parameter']
ACTION_FIELD_LABELS = ['UUID', 'Created At', 'Updated At', 'Deleted At',
'Next Action', 'Description', 'Alarm', 'State',
'Action Plan', 'Action',
'Applies to', 'Hypervisor Source',
'Hypervisor Destination', 'Parameter']
ACTION_SHORT_LIST_FIELDS = ['uuid', 'next_uuid',
'state', 'action_plan_uuid', 'action_type']
ACTION_SHORT_LIST_FIELD_LABELS = ['UUID', 'Next Action', 'State',
'Action Plan', 'Action']
# Metric Collector
METRIC_COLLECTOR_FIELDS = ['uuid', 'created_at', 'updated_at', 'deleted_at',
'endpoint', 'category']
METRIC_COLLECTOR_FIELD_LABELS = ['UUID', 'Created At', 'Updated At',
'Deleted At', 'Endpoint URL',
'Metric Category']
METRIC_COLLECTOR_SHORT_LIST_FIELDS = ['uuid', 'endpoint', 'category']
METRIC_COLLECTOR_SHORT_LIST_FIELD_LABELS = ['UUID', 'Endpoint URL',
'Metric Category']

43
watcherclient/v1/shell.py Normal file
View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
#
# 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 watcherclient.common import utils
from watcherclient.v1 import action_plan_shell
from watcherclient.v1 import action_shell
from watcherclient.v1 import audit_shell
from watcherclient.v1 import audit_template_shell
# from watcherclient.v1 import metric_collector_shell
COMMAND_MODULES = [
audit_template_shell,
audit_shell,
action_plan_shell,
action_shell,
# metric_collector_shell,
]
def enhance_parser(parser, subparsers, cmd_mapper):
"""Enhance parser with API version specific options.
Take a basic (nonversioned) parser and enhance it with
commands and options specific for this version of API.
:param parser: top level parser :param subparsers: top level
parser's subparsers collection where subcommands will go
"""
for command_module in COMMAND_MODULES:
utils.define_commands_from_module(subparsers, command_module,
cmd_mapper)

18
watcherclient/version.py Normal file
View File

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