push initial version
Change-Id: Ifecc2c7dd6bd859ba6ef327fddd891982382df3b
This commit is contained in:
parent
d8ea04cf39
commit
3957331e52
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal 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
7
.testr.conf
Normal 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
16
CONTRIBUTING.rst
Normal 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
4
HACKING.rst
Normal 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
6
MANIFEST.in
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
include AUTHORS
|
||||||
|
include ChangeLog
|
||||||
|
exclude .gitignore
|
||||||
|
exclude .gitreview
|
||||||
|
|
||||||
|
global-exclude *.pyc
|
13
README.rst
Normal file
13
README.rst
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
===============================
|
||||||
|
python-watcherclient
|
||||||
|
===============================
|
||||||
|
|
||||||
|
Python client library for Watcher API
|
||||||
|
|
||||||
|
* Free software: Apache license
|
||||||
|
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
* TODO
|
94
doc/source/api_v1.rst
Normal file
94
doc/source/api_v1.rst
Normal 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
89
doc/source/cli.rst
Normal 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
89
doc/source/conf.py
Normal 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'
|
||||||
|
),
|
||||||
|
]
|
54
doc/source/contributing.rst
Normal file
54
doc/source/contributing.rst
Normal 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
50
doc/source/index.rst
Normal 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
|
10
doc/source/installation.rst
Normal file
10
doc/source/installation.rst
Normal 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
1
doc/source/readme.rst
Normal file
@ -0,0 +1 @@
|
|||||||
|
.. include:: ../../README.rst
|
10
openstack-common.conf
Normal file
10
openstack-common.conf
Normal 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
10
requirements.txt
Normal 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
37
setup.cfg
Normal 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
30
setup.py
Executable 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
18
test-requirements.txt
Normal 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
|
27
tools/watcher.bash_completion
Normal file
27
tools/watcher.bash_completion
Normal 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
35
tox.ini
Normal 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
26
watcherclient/__init__.py
Normal 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
123
watcherclient/client.py
Normal 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)
|
0
watcherclient/common/__init__.py
Normal file
0
watcherclient/common/__init__.py
Normal file
146
watcherclient/common/base.py
Normal file
146
watcherclient/common/base.py
Normal 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)
|
384
watcherclient/common/http.py
Normal file
384
watcherclient/common/http.py
Normal 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)
|
179
watcherclient/common/utils.py
Normal file
179
watcherclient/common/utils.py
Normal 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
|
75
watcherclient/exceptions.py
Normal file
75
watcherclient/exceptions.py
Normal 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)
|
0
watcherclient/openstack/__init__.py
Normal file
0
watcherclient/openstack/__init__.py
Normal file
0
watcherclient/openstack/common/__init__.py
Normal file
0
watcherclient/openstack/common/__init__.py
Normal file
45
watcherclient/openstack/common/_i18n.py
Normal file
45
watcherclient/openstack/common/_i18n.py
Normal 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
|
234
watcherclient/openstack/common/apiclient/auth.py
Normal file
234
watcherclient/openstack/common/apiclient/auth.py
Normal 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
|
||||||
|
"""
|
532
watcherclient/openstack/common/apiclient/base.py
Normal file
532
watcherclient/openstack/common/apiclient/base.py
Normal 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)
|
388
watcherclient/openstack/common/apiclient/client.py
Normal file
388
watcherclient/openstack/common/apiclient/client.py
Normal 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)
|
479
watcherclient/openstack/common/apiclient/exceptions.py
Normal file
479
watcherclient/openstack/common/apiclient/exceptions.py
Normal 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)
|
190
watcherclient/openstack/common/apiclient/fake_client.py
Normal file
190
watcherclient/openstack/common/apiclient/fake_client.py
Normal 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,
|
||||||
|
})
|
100
watcherclient/openstack/common/apiclient/utils.py
Normal file
100
watcherclient/openstack/common/apiclient/utils.py
Normal 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)
|
271
watcherclient/openstack/common/cliutils.py
Normal file
271
watcherclient/openstack/common/cliutils.py
Normal 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)
|
479
watcherclient/openstack/common/gettextutils.py
Normal file
479
watcherclient/openstack/common/gettextutils.py
Normal 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)
|
73
watcherclient/openstack/common/importutils.py
Normal file
73
watcherclient/openstack/common/importutils.py
Normal 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
|
316
watcherclient/openstack/common/strutils.py
Normal file
316
watcherclient/openstack/common/strutils.py
Normal 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
510
watcherclient/shell.py
Normal 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()
|
0
watcherclient/tests/__init__.py
Normal file
0
watcherclient/tests/__init__.py
Normal file
81
watcherclient/tests/keystone_client_fixtures.py
Normal file
81
watcherclient/tests/keystone_client_fixtures.py
Normal 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))
|
142
watcherclient/tests/test_client.py
Normal file
142
watcherclient/tests/test_client.py
Normal 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)
|
283
watcherclient/tests/test_http.py
Normal file
283
watcherclient/tests/test_http.py
Normal 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))
|
38
watcherclient/tests/test_import.py
Normal file
38
watcherclient/tests/test_import.py
Normal 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__)
|
306
watcherclient/tests/test_shell.py
Normal file
306
watcherclient/tests/test_shell.py
Normal 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)
|
173
watcherclient/tests/test_utils.py
Normal file
173
watcherclient/tests/test_utils.py
Normal 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)
|
144
watcherclient/tests/utils.py
Normal file
144
watcherclient/tests/utils.py
Normal 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)
|
0
watcherclient/tests/v1/__init__.py
Normal file
0
watcherclient/tests/v1/__init__.py
Normal file
277
watcherclient/tests/v1/test_action.py
Normal file
277
watcherclient/tests/v1/test_action.py
Normal 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)
|
204
watcherclient/tests/v1/test_action_plan.py
Normal file
204
watcherclient/tests/v1/test_action_plan.py
Normal 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)
|
148
watcherclient/tests/v1/test_action_plan_shell.py
Normal file
148
watcherclient/tests/v1/test_action_plan_shell.py
Normal 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)
|
132
watcherclient/tests/v1/test_action_shell.py
Normal file
132
watcherclient/tests/v1/test_action_shell.py
Normal 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)
|
227
watcherclient/tests/v1/test_audit.py
Normal file
227
watcherclient/tests/v1/test_audit.py
Normal 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)
|
144
watcherclient/tests/v1/test_audit_shell.py
Normal file
144
watcherclient/tests/v1/test_audit_shell.py
Normal 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')
|
336
watcherclient/tests/v1/test_audit_template.py
Normal file
336
watcherclient/tests/v1/test_audit_template.py
Normal 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)
|
137
watcherclient/tests/v1/test_audit_template_shell.py
Normal file
137
watcherclient/tests/v1/test_audit_template_shell.py
Normal 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})
|
321
watcherclient/tests/v1/test_metric_collector.py
Normal file
321
watcherclient/tests/v1/test_metric_collector.py
Normal 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)
|
107
watcherclient/tests/v1/test_metric_collector_shell.py
Normal file
107
watcherclient/tests/v1/test_metric_collector_shell.py
Normal 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')
|
0
watcherclient/v1/__init__.py
Normal file
0
watcherclient/v1/__init__.py
Normal file
90
watcherclient/v1/action.py
Normal file
90
watcherclient/v1/action.py
Normal 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)
|
88
watcherclient/v1/action_plan.py
Normal file
88
watcherclient/v1/action_plan.py
Normal 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)
|
156
watcherclient/v1/action_plan_shell.py
Normal file
156
watcherclient/v1/action_plan_shell.py
Normal 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()
|
137
watcherclient/v1/action_shell.py
Normal file
137
watcherclient/v1/action_shell.py
Normal 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
100
watcherclient/v1/audit.py
Normal 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)
|
162
watcherclient/v1/audit_shell.py
Normal file
162
watcherclient/v1/audit_shell.py
Normal 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()
|
100
watcherclient/v1/audit_template.py
Normal file
100
watcherclient/v1/audit_template.py
Normal 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)
|
167
watcherclient/v1/audit_template_shell.py
Normal file
167
watcherclient/v1/audit_template_shell.py
Normal 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)
|
46
watcherclient/v1/client.py
Normal file
46
watcherclient/v1/client.py
Normal 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
|
||||||
|
)
|
101
watcherclient/v1/metric_collector.py
Normal file
101
watcherclient/v1/metric_collector.py
Normal 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)
|
144
watcherclient/v1/metric_collector_shell.py
Normal file
144
watcherclient/v1/metric_collector_shell.py
Normal 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)
|
84
watcherclient/v1/resource_fields.py
Normal file
84
watcherclient/v1/resource_fields.py
Normal 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
43
watcherclient/v1/shell.py
Normal 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
18
watcherclient/version.py
Normal 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')
|
Loading…
Reference in New Issue
Block a user