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