Compare commits

..

2 Commits

Author SHA1 Message Date
5ab4ac10a2 Update TOX/UPPER_CONSTRAINTS_FILE for stable/train
Update the URL to the upper-constraints file to point to the redirect
rule on releases.openstack.org so that anyone working on this branch
will switch to the correct upper-constraints list automatically when
the requirements repository branches.

Until the requirements repository has as stable/train branch, tests will
continue to use the upper-constraints list on master.

Change-Id: I0abb4375f76549f4b06d0acfa257e754955736f7
2019-09-12 09:30:47 +00:00
d7bf948ed5 Update .gitreview for stable/train
Change-Id: I3ec1fb97dd1cd49622381ba475e9882101028054
2019-09-12 09:30:44 +00:00
66 changed files with 408 additions and 1637 deletions

View File

@@ -2,4 +2,4 @@
host=review.opendev.org
port=29418
project=openstack/python-watcherclient.git
defaultbranch=stable/2026.1
defaultbranch=stable/train

View File

@@ -1,37 +1,12 @@
- job:
name: python-watcherclient-functional
parent: devstack-tox-functional
timeout: 7200
required-projects:
- openstack/watcher
- openstack/python-watcherclient
vars:
# Run cross-project watcherclient functional tests on watcher repo.
zuul_work_dir: src/opendev.org/openstack/python-watcherclient
openrc_enable_export: true
devstack_plugins:
watcher: https://opendev.org/openstack/watcher
devstack_services:
watcher-api: true
watcher-decision-engine: true
watcher-applier: true
s-account: false
s-container: false
s-object: false
s-proxy: false
irrelevant-files:
- ^.*\.rst$
- ^doc/.*$
- ^releasenotes/.*$
- project:
templates:
- openstack-cover-jobs
- openstack-python3-jobs
- openstack-lower-constraints-jobs
- openstack-python-jobs
- openstack-python3-train-jobs
- publish-openstack-docs-pti
- check-requirements
- openstackclient-plugin-jobs
check:
jobs:
- python-watcherclient-functional
- watcherclient-tempest-functional

2
babel.cfg Normal file
View File

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

View File

@@ -2,7 +2,6 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
openstackdocstheme>=2.2.1 # Apache-2.0
sphinx>=2.0.0,!=2.1.0 # BSD
sphinxcontrib-apidoc>=0.2.0 # BSD
openstackdocstheme>=1.18.1 # Apache-2.0
sphinx!=1.6.6,!=1.6.7,>=1.6.5,<2.0.0;python_version=='2.7' # BSD
sphinx!=1.6.6,!=1.6.7,>=1.6.5;python_version>='3.4' # BSD

View File

@@ -24,7 +24,7 @@ The watcher client is the command-line interface (CLI) for the
Infrastructure Optimization service (watcher) API
and its extensions.
This chapter documents watcherclient version ``4.9.0``.
This chapter documents :command:`watcher` version ``1.3.0``.
For help on a specific :command:`watcher` command, enter:
@@ -214,37 +214,6 @@ Show detailed information about a given action.
``-h, --help``
show this help message and exit
.. _watcher_action_update:
watcher action update
---------------------
.. code-block:: console
usage: watcher action update [-h] [-f {html,json,shell,table,value,yaml}]
[-c COLUMN] [--max-width <integer>] [--fit-width]
[--print-empty] [--noindent] [--prefix PREFIX]
[--state <state>] [--reason <reason>] <action>
Update action command.
**Positional arguments:**
``<action>``
UUID of the action
**Optional arguments:**
``-h, --help``
show this help message and exit
``--state <state>``
New state for the action (e.g., SKIPPED)
``--reason <reason>``
Reason for the action state change.
.. _watcher_actionplan_cancel:
watcher actionplan cancel
@@ -651,43 +620,38 @@ Create new audit template.
``--scope <path>``
Part of the cluster on which an audit will be done.
Can be provided either in yaml or json file.
YAML example:
::
---
- host_aggregates:
- id: 1
- id: 2
- id: 3
YAML example:
---- host_aggregates:
- id: 1
- id: 2
- id: 3
- availability_zones:
- name: AZ1
- name: AZ2
- name: AZ1
- name: AZ2
- exclude:
- instances:
- uuid: UUID1
- uuid: UUID2
- compute_nodes:
- name: compute1
JSON example:
::
[
{'host_aggregates': [
{'id': 1},
{'id': 2},
{'id': 3}]},
{'availability_zones': [
{'name': 'AZ1'},
{'name': 'AZ2'}]},
{'exclude': [
{'instances': [
{'uuid': 'UUID1'},
{'uuid': 'UUID2'}]},
{'compute_nodes': [
{'name': 'compute1'}]}]}
]
- instances:
- uuid: UUID1
- uuid: UUID2
- compute_nodes:
- name: compute1
JSON example:
[{'host_aggregates': [
{'id': 1},
{'id': 2},
{'id': 3}]},
{'availability_zones': [
{'name': 'AZ1'},
{'name': 'AZ2'}]},
{'exclude': [
{'instances': [
{'uuid': 'UUID1'},
{'uuid': 'UUID2'}
]},
{'compute_nodes': [
{'name': 'compute1'}
]}
]}]
.. _watcher_audittemplate_delete:

View File

@@ -12,26 +12,20 @@
# 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',
'sphinxcontrib.apidoc',
'openstackdocstheme',
]
# 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
# sphinxcontrib.apidoc options
apidoc_module_dir = '../../watcherclient'
apidoc_output_dir = 'reference/api'
apidoc_excluded_paths = [
'tests/*']
apidoc_separate_modules = True
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -42,8 +36,18 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = 'python-watcherclient'
copyright = 'OpenStack Foundation'
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.']
@@ -56,7 +60,7 @@ add_function_parentheses = True
add_module_names = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'native'
pygments_style = 'sphinx'
# -- Options for HTML output --------------------------------------------------
@@ -79,27 +83,17 @@ latex_documents = [
(
'index',
'%s.tex' % project,
'%s Documentation' % project,
'OpenStack Foundation', 'manual'
u'%s Documentation' % project,
u'OpenStack Foundation', 'manual'
),
]
# Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664
latex_use_xindy = False
latex_domain_indices = False
latex_elements = {
'makeindex': '',
'printindex': '',
'preamble': r'\setcounter{tocdepth}{3}',
}
# openstackdocstheme options
openstackdocs_repo_name = 'openstack/python-watcherclient'
openstackdocs_pdf_link = True
openstackdocs_bug_project = 'python-watcherclient'
openstackdocs_bug_tag = ''
repository_name = 'openstack/python-watcherclient'
bug_project = 'python-watcherclient'
bug_tag = ''
# Must set this variable to include year, month, day, hours, and minutes.
html_last_updated_fmt = '%Y-%m-%d %H:%M'
#html_theme_options = {"show_other_versions": "True"}

View File

@@ -8,24 +8,19 @@ If you're interested in contributing to the python-watcherclient project,
the following will help get you started.
Developer Certificate of Origin
-------------------------------
Contributor License Agreement
-----------------------------
.. index::
single: license; agreement
In order to contribute to the python-watcherclient project, you need to adhere
to the `Developer Certificate of Origin`_. OpenStack utilizes the Developer
Certificate of Origin (DCO) as a lightweight means to confirm that you are
entitled to contribute the code you submit. This ensures that you are
providing your contributions under the project's license and that you have
the right to do so.
.. _Developer Certificate of Origin: https://developercertificate.org/
In order to contribute to the python-watcherclient project, you need to have
signed OpenStack's contributor's agreement.
.. seealso::
* https://docs.openstack.org/contributors/common/dco.html
* https://docs.openstack.org/infra/manual/developers.html
* https://wiki.openstack.org/CLA
LaunchPad Project
-----------------

View File

@@ -0,0 +1,8 @@
======================
Python API Reference
======================
.. toctree::
:maxdepth: 2
autoindex

View File

@@ -10,5 +10,5 @@ done so, you can use the API like so.
.. toctree::
:maxdepth: 2
api/modules
api/index
api_v1

89
lower-constraints.txt Normal file
View File

@@ -0,0 +1,89 @@
alabaster==0.7.10
appdirs==1.4.3
asn1crypto==0.23.0
Babel==2.5.3
certifi==2018.1.18
cffi==1.7.0
chardet==3.0.4
cliff==2.11.0
cmd2==0.8.2
coverage==4.0
cryptography==2.1
debtcollector==1.19.0
decorator==4.2.1
deprecation==2.0
docutils==0.11
dogpile.cache==0.6.5
dulwich==0.15.0
extras==1.0.0
fasteners==0.7.0
fixtures==3.0.0
flake8==2.5.5
future==0.16.0
hacking==0.12.0
idna==2.6
imagesize==0.7.1
iso8601==0.1.12
Jinja2==2.10
jmespath==0.9.3
jsonpatch==1.21
jsonpointer==2.0
jsonschema==2.6.0
keystoneauth1==3.4.0
linecache2==1.0.0
MarkupSafe==1.0
mccabe==0.2.1
mock==2.0.0
monotonic==1.4
mox3==0.20.0
msgpack-python==0.4.0
munch==2.2.0
netaddr==0.7.19
netifaces==0.10.6
openstackdocstheme==1.18.1
openstacksdk==0.12.0
os-client-config==1.29.0
os-service-types==1.2.0
os-testr==1.0.0
osc-lib==1.10.0
oslo.concurrency==3.25.0
oslo.config==5.2.0
oslo.context==2.19.2
oslo.i18n==3.20.0
oslo.log==3.36.0
oslo.serialization==2.18.0
oslo.utils==3.36.0
oslotest==3.2.0
packaging==17.1
paramiko==2.0.0
pbr==3.1.1
pep8==1.5.7
pyasn1==0.1.8
pycparser==2.18
pyflakes==0.8.1
Pygments==2.2.0
pyinotify==0.9.6
pyparsing==2.2.0
pyperclip==1.6.0
python-dateutil==2.5.3
python-mimeparse==1.6.0
python-subunit==1.0.0
pytz==2018.3
PyYAML==3.12
requests==2.18.4
requestsexceptions==1.4.0
rfc3986==0.3.1
simplejson==3.13.2
six==1.11.0
snowballstemmer==1.2.1
Sphinx==1.6.5
sphinxcontrib-websupport==1.0.1
stestr==2.0.0
stevedore==1.28.0
tempest==17.1.0
testscenarios==0.4
testtools==2.2.0
traceback2==1.4.0
unittest2==1.1.0
urllib3==1.22
wrapt==1.10.11

View File

@@ -1,3 +0,0 @@
[build-system]
requires = ["pbr>=6.0.0", "setuptools>=64.0.0"]
build-backend = "pbr.build"

View File

@@ -1,24 +0,0 @@
---
features:
- |
Added support for updating action state through the new
``openstack optimize action update`` command. This feature allows
operators to manually change action states. The command
supports the following options:
* ``--state <state>`` - New state for the action (required)
* ``--reason <reason>`` - Optional reason for the state change
Currently, the only use case for this update is to Skip an action
before starting an Action Plan with an optional reason by setting
the state to SKIPPED:
$ openstack optimize action update --state SKIPPED --reason "Manual skip" <action-uuid>
This feature requires Watcher API microversion 1.5 or higher.
upgrade:
- |
The maximum supported API version has been increased from 1.1 to 1.5
to support the new action update functionality. This change maintains
full backward compatibility with existing deployments.

View File

@@ -1,6 +0,0 @@
---
upgrade:
- |
Python 2.7 support has been dropped. Last release of python-watcherclient
to support py2.7 is OpenStack Train. The minimum version of Python now
supported by python-watcherclient is Python 3.6.

View File

@@ -1,6 +0,0 @@
---
upgrade:
- |
watcher client now requires python 3.10 or newer.
The last release to support ``3.9`` was ``2025.1``.
Please ensure you have a supported python version before upgrading.

View File

@@ -1,3 +1,8 @@
# 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.
Babel!=2.4.0,>=2.5.3 # BSD
cliff!=2.9.0,>=2.11.0 # Apache-2.0
osc-lib>=1.10.0 # Apache-2.0
oslo.i18n>=3.20.0 # Apache-2.0
@@ -5,4 +10,5 @@ oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
oslo.utils>=3.36.0 # Apache-2.0
pbr!=2.1.0,>=3.1.1 # Apache-2.0
keystoneauth1>=3.4.0 # Apache-2.0
PyYAML>=3.13 # MIT
six>=1.11.0 # MIT
PyYAML>=3.12 # MIT

View File

@@ -1,12 +1,11 @@
[metadata]
name = python-watcherclient
summary = Python client library for Watcher API
description_file =
description-file =
README.rst
author = OpenStack
author_email = openstack-discuss@lists.openstack.org
home_page = https://docs.openstack.org/python-watcherclient/latest/
python_requires = >=3.10
author-email = openstack-discuss@lists.openstack.org
home-page = https://docs.openstack.org/python-watcherclient/latest/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
@@ -14,12 +13,11 @@ classifier =
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 :: 3
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Python :: 3.13
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
[files]
packages =
@@ -62,7 +60,6 @@ openstack.infra_optim.v1 =
optimize_action_show = watcherclient.v1.action_shell:ShowAction
optimize_action_list = watcherclient.v1.action_shell:ListAction
optimize_action_update = watcherclient.v1.action_shell:UpdateAction
optimize_scoringengine_show = watcherclient.v1.scoring_engine_shell:ShowScoringEngine
optimize_scoringengine_list = watcherclient.v1.scoring_engine_shell:ListScoringEngine
@@ -102,7 +99,6 @@ watcherclient.v1 =
action_show = watcherclient.v1.action_shell:ShowAction
action_list = watcherclient.v1.action_shell:ListAction
action_update = watcherclient.v1.action_shell:UpdateAction
scoringengine_show = watcherclient.v1.scoring_engine_shell:ShowScoringEngine
scoringengine_list = watcherclient.v1.scoring_engine_shell:ListScoringEngine
@@ -111,3 +107,32 @@ watcherclient.v1 =
service_list = watcherclient.v1.service_shell:ListService
datamodel_list = watcherclient.v1.data_model_shell:ListDataModel
[pbr]
autodoc_index_modules = True
autodoc_exclude_modules =
watcherclient.tests.*
api_doc_dir = reference/api
[build_sphinx]
source-dir = doc/source
build-dir = doc/build
all_files = 1
warning-is-error = 1
[bdist_wheel]
universal = 1
[compile_catalog]
directory = watcherclient/locale
domain = watcherclient
[update_catalog]
domain = watcherclient
output_dir = watcherclient/locale
input_file = watcherclient/locale/watcherclient.pot
[extract_messages]
keywords = _ gettext ngettext l_ lazy_gettext _LI _LW _LE _LC
mapping_file = babel.cfg
output_file = watcherclient/locale/watcherclient.pot

View File

@@ -13,8 +13,17 @@
# 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>=2.0.0'],
pbr=True)

View File

@@ -1,7 +1,13 @@
# 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.
coverage!=4.4,>=4.0 # Apache-2.0
fixtures>=3.0.0 # Apache-2.0/BSD
hacking>=7.0.0,<7.1.0 # Apache-2.0
hacking>=1.1.0,<1.2.0 # Apache-2.0
mock>=2.0.0 # BSD
oslotest>=3.2.0 # Apache-2.0
python-subunit>=1.0.0 # Apache-2.0/BSD
stestr>=2.0.0 # Apache-2.0
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=2.2.0 # MIT

41
tox.ini
View File

@@ -1,17 +1,20 @@
[tox]
minversion = 3.18.0
envlist = py3,pep8
minversion = 2.0
envlist = py36,py37,py27,pep8
skipsdist = True
[testenv]
usedevelop = True
passenv = ZUUL_CACHE_DIR
REQUIREMENTS_PIP_LOCATION
install_command = pip install {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
deps =
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2026.1}
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/train}
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt
allowlist_externals =
whitelist_externals =
rm
commands = rm -f .testrepository/times.dbm
# The --test-path is defined in .stestr.conf
@@ -40,21 +43,10 @@ commands =
[testenv:docs]
basepython = python3
deps =
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/2026.1}
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/train}
-r{toxinidir}/requirements.txt
-r{toxinidir}/doc/requirements.txt
commands = sphinx-build -W -b html doc/source doc/build/html
[testenv:pdf-docs]
basepython = python3
deps = {[testenv:docs]deps}
allowlist_externals =
rm
make
commands =
rm -rf doc/build/pdf
sphinx-build -W -b latex doc/source doc/build/pdf
make -C doc/build/pdf
commands = python setup.py build_sphinx
[testenv:debug]
basepython = python3
@@ -64,7 +56,7 @@ commands = oslo_debug_helper -t watcherclient/tests/unit {posargs}
# E123, E125 skipped as they are invalid PEP-8.
show-source = True
enable-extensions = H203,H106
ignore = E123,E125,W504
ignore = E123,E125
builtins = _
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
@@ -75,7 +67,10 @@ commands = python setup.py bdist_wheel
[hacking]
import_exceptions = watcherclient._i18n
[testenv:functional]
passenv = OS_*
commands =
stestr --test-path=./watcherclient/tests/client_functional/ run --concurrency=1 {posargs}
[testenv:lower-constraints]
basepython = python3
install_command = pip install -U {opts} {packages}
deps =
-c{toxinidir}/lower-constraints.txt
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt

View File

@@ -32,4 +32,4 @@ API_MIN_VERSION = api_versioning.APIVersion("1.0")
# when client supported the max version, and bumped sequentially, otherwise
# the client may break due to server side new version may include some
# backward incompatible change.
API_MAX_VERSION = api_versioning.APIVersion("1.5")
API_MAX_VERSION = api_versioning.APIVersion("1.1")

View File

@@ -28,7 +28,6 @@ if not LOG.handlers:
MINOR_1_START_END_TIMING = '1.1'
MINOR_2_FORCE_AUDIT = '1.2'
MINOR_5_ACTION_UPDATE = '1.5'
HEADER_NAME = "OpenStack-API-Version"
# key is a deprecated version and value is an alternative version.
DEPRECATED_VERSIONS = {}
@@ -55,15 +54,6 @@ def launch_audit_forced(requested_version):
APIVersion(MINOR_2_FORCE_AUDIT))
def action_update_supported(requested_version):
"""Check if we should support action update functionality.
Version 1.5 of the API added support for updating action state.
"""
return (APIVersion(requested_version) >=
APIVersion(MINOR_5_ACTION_UPDATE))
class APIVersion(object):
"""This class represents an API Version Request.

View File

@@ -39,9 +39,10 @@ Base utilities to build API operation managers and objects on top of.
import abc
import copy
from urllib import parse
from oslo_utils import strutils
import six
from six.moves.urllib import parse
from watcherclient._i18n import _
from watcherclient.common.apiclient import exceptions
@@ -223,7 +224,8 @@ class BaseManager(HookableMixin):
return self.client.delete(url)
class ManagerWithFind(BaseManager, metaclass=abc.ABCMeta):
@six.add_metaclass(abc.ABCMeta)
class ManagerWithFind(BaseManager):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod

View File

@@ -36,6 +36,8 @@ Exception definitions.
import inspect
import sys
import six
from watcherclient._i18n import _
@@ -455,7 +457,7 @@ def from_response(response, method, url):
kwargs["message"] = (error.get("message") or
error.get("faultstring"))
kwargs["details"] = (error.get("details") or
str(body))
six.text_type(body))
elif content_type.startswith("text/"):
kwargs["details"] = response.text

View File

@@ -19,7 +19,8 @@ Base utilities to build API operation managers and objects on top of.
"""
import copy
from urllib import parse as urlparse
import six.moves.urllib.parse as urlparse
from watcherclient.common.apiclient import base

View File

@@ -18,6 +18,7 @@ import logging
from cliff import command
from cliff import lister
from cliff import show
import six
class CommandMeta(abc.ABCMeta):
@@ -29,7 +30,8 @@ class CommandMeta(abc.ABCMeta):
return super(CommandMeta, mcs).__new__(mcs, name, bases, cls_dict)
class Command(command.Command, metaclass=CommandMeta):
@six.add_metaclass(CommandMeta)
class Command(command.Command):
def run(self, parsed_args):
self.log.debug('run(%s)', parsed_args)

View File

@@ -14,24 +14,24 @@
# under the License.
import copy
from distutils import version
import functools
import hashlib
import http.client
import io
import logging
import os
import re
import socket
import ssl
import textwrap
import time
from urllib import parse as urlparse
from keystoneauth1 import adapter
from keystoneauth1 import exceptions as kexceptions
from oslo_serialization import jsonutils
from oslo_utils import strutils
import requests
import six
from six.moves import http_client
import six.moves.urllib.parse as urlparse
from watcherclient._i18n import _
from watcherclient.common import api_versioning
@@ -40,8 +40,8 @@ from watcherclient import exceptions
# Record the latest version that this client was tested with.
DEFAULT_VER = '1.latest'
# Minor version 4 for adding webhook API
LAST_KNOWN_API_VERSION = 5
# Minor version 2 for adding force option to audit
LAST_KNOWN_API_VERSION = 2
LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION)
LOG = logging.getLogger(__name__)
@@ -62,7 +62,7 @@ SUPPORTED_ENDPOINT_SCHEME = ('http', 'https')
def _trim_endpoint_api_version(url):
"""Trim API version and trailing slash from endpoint."""
return re.sub(f'{API_VERSION}$', '', url.rstrip('/'))
return url.rstrip('/').rstrip(API_VERSION)
def _extract_error_json(body):
@@ -123,19 +123,16 @@ class VersionNegotiationMixin(object):
% {'req': self.os_infra_optim_api_version,
'min': min_ver, 'max': max_ver}))
negotiated_ver = api_versioning.APIVersion(
self.os_infra_optim_api_version)
min_ver = api_versioning.APIVersion(min_ver)
max_ver = api_versioning.APIVersion(max_ver)
if negotiated_ver > max_ver:
negotiated_ver = max_ver
negotiated_ver = str(
min(version.StrictVersion(self.os_infra_optim_api_version),
version.StrictVersion(max_ver)))
if negotiated_ver < min_ver:
negotiated_ver = min_ver
# server handles microversions, but doesn't support
# the requested version, so try a negotiated version
self.api_version_select_state = 'negotiated'
self.os_infra_optim_api_version = negotiated_ver.get_string()
LOG.debug('Negotiated API version is %s', negotiated_ver.get_string())
self.os_infra_optim_api_version = negotiated_ver
LOG.debug('Negotiated API version is %s', negotiated_ver)
return negotiated_ver
@@ -250,7 +247,7 @@ class HTTPClient(VersionNegotiationMixin):
if not self.session.verify:
curl.append('-k')
elif isinstance(self.session.verify, str):
elif isinstance(self.session.verify, six.string_types):
curl.append('--cacert %s' % self.session.verify)
if self.session.cert:
@@ -328,7 +325,7 @@ class HTTPClient(VersionNegotiationMixin):
# to servers that did not support microversions. Details here:
# http://specs.openstack.org/openstack/watcher-specs/specs/kilo/api-microversions.html#use-case-3b-new-client-communicating-with-a-old-watcher-user-specified # noqa
if resp.status_code == http.client.NOT_ACCEPTABLE:
if resp.status_code == http_client.NOT_ACCEPTABLE:
negotiated_ver = self.negotiate_version(self.session, resp)
kwargs['headers']['OpenStack-API-Version'] = (
' '.join(['infra-optim', negotiated_ver]))
@@ -360,21 +357,21 @@ class HTTPClient(VersionNegotiationMixin):
]
body_str = ''.join(body_list)
self.log_http_response(resp, body_str)
body_iter = io.StringIO(body_str)
body_iter = six.StringIO(body_str)
else:
self.log_http_response(resp)
if resp.status_code >= http.client.BAD_REQUEST:
if resp.status_code >= http_client.BAD_REQUEST:
error_json = _extract_error_json(body_str)
raise exceptions.from_response(
resp, error_json.get('faultstring'),
error_json.get('debuginfo'), method, url)
elif resp.status_code in (http.client.MOVED_PERMANENTLY,
http.client.FOUND,
http.client.USE_PROXY):
elif resp.status_code in (http_client.MOVED_PERMANENTLY,
http_client.FOUND,
http_client.USE_PROXY):
# Redirected. Reissue the request to the new location.
return self._http_request(resp['location'], method, **kwargs)
elif resp.status_code == http.client.MULTIPLE_CHOICES:
elif resp.status_code == http_client.MULTIPLE_CHOICES:
raise exceptions.from_response(resp, method=method, url=url)
return resp, body_iter
@@ -390,8 +387,8 @@ class HTTPClient(VersionNegotiationMixin):
resp, body_iter = self._http_request(url, method, **kwargs)
content_type = resp.headers.get('Content-Type')
if (resp.status_code in (http.client.NO_CONTENT,
http.client.RESET_CONTENT) or
if (resp.status_code in (http_client.NO_CONTENT,
http_client.RESET_CONTENT) or
content_type is None):
return resp, list()
@@ -413,7 +410,7 @@ class HTTPClient(VersionNegotiationMixin):
return self._http_request(url, method, **kwargs)
class VerifiedHTTPSConnection(http.client.HTTPSConnection):
class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection):
"""httplib-compatible connection using client-side SSL authentication
:see http://code.activestate.com/recipes/
@@ -422,8 +419,9 @@ class VerifiedHTTPSConnection(http.client.HTTPSConnection):
def __init__(self, host, port, key_file=None, cert_file=None,
ca_file=None, timeout=None, insecure=False):
super(VerifiedHTTPSConnection, self).__init__(
self, host, port, key_file=key_file, cert_file=cert_file)
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:
@@ -437,6 +435,11 @@ class VerifiedHTTPSConnection(http.client.HTTPSConnection):
"""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)
@@ -444,21 +447,17 @@ class VerifiedHTTPSConnection(http.client.HTTPSConnection):
self.sock = sock
self._tunnel()
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
if self.insecure is True:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
kwargs = {'cert_reqs': ssl.CERT_NONE}
else:
context.load_verify_locations(self.ca_file)
kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file}
if self.cert_file:
kwargs['certfile'] = self.cert_file
if self.key_file:
context.load_cert_chain(self.cert_file, self.key_file)
else:
context.load_cert_chain(self.cert_file)
kwargs['keyfile'] = self.key_file
self.sock = context.wrap_socket(sock)
self.sock = ssl.wrap_socket(sock, **kwargs)
@staticmethod
def get_system_ca_file():
@@ -504,7 +503,7 @@ class SessionClient(VersionNegotiationMixin, adapter.LegacyJsonAdapter):
def _http_request(self, url, method, **kwargs):
kwargs.setdefault('user_agent', USER_AGENT)
kwargs.setdefault('auth', self.auth)
if isinstance(self.endpoint_override, str):
if isinstance(self.endpoint_override, six.string_types):
kwargs.setdefault(
'endpoint_override',
_trim_endpoint_api_version(self.endpoint_override)
@@ -528,22 +527,22 @@ class SessionClient(VersionNegotiationMixin, adapter.LegacyJsonAdapter):
resp = self.session.request(url, method,
raise_exc=False, **kwargs)
if resp.status_code == http.client.NOT_ACCEPTABLE:
if resp.status_code == http_client.NOT_ACCEPTABLE:
negotiated_ver = self.negotiate_version(self.session, resp)
kwargs['headers']['OpenStack-API-Version'] = (
' '.join(['infra-optim', negotiated_ver]))
return self._http_request(url, method, **kwargs)
if resp.status_code >= http.client.BAD_REQUEST:
if resp.status_code >= http_client.BAD_REQUEST:
error_json = _extract_error_json(resp.content)
raise exceptions.from_response(
resp, error_json.get('faultstring'),
error_json.get('debuginfo'), method, url)
elif resp.status_code in (http.client.MOVED_PERMANENTLY,
http.client.FOUND, http.client.USE_PROXY):
elif resp.status_code in (http_client.MOVED_PERMANENTLY,
http_client.FOUND, http_client.USE_PROXY):
# Redirected. Reissue the request to the new location.
location = resp.headers.get('location')
resp = self._http_request(location, method, **kwargs)
elif resp.status_code == http.client.MULTIPLE_CHOICES:
elif resp.status_code == http_client.MULTIPLE_CHOICES:
raise exceptions.from_response(resp, method=method, url=url)
return resp
@@ -559,7 +558,7 @@ class SessionClient(VersionNegotiationMixin, adapter.LegacyJsonAdapter):
body = resp.content
content_type = resp.headers.get('content-type', None)
status = resp.status_code
if (status in (http.client.NO_CONTENT, http.client.RESET_CONTENT) or
if (status in (http_client.NO_CONTENT, http_client.RESET_CONTENT) or
content_type is None):
return resp, list()
if 'application/json' in content_type:

View File

@@ -14,6 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import print_function
import argparse
import os
import uuid

View File

@@ -71,7 +71,6 @@ class AmbiguousAuthSystem(exceptions.ClientException):
"""Could not obtain token and endpoint using provided credentials."""
pass
# Alias for backwards compatibility
AmbigiousAuthSystem = AmbiguousAuthSystem

View File

@@ -203,9 +203,7 @@ class WatcherShell(app.App):
LOG.info("END return value: %s", ret_val)
def main(argv=None):
if argv is None:
argv = sys.argv[1:]
def main(argv=sys.argv[1:]):
watcher_app = WatcherShell()
return watcher_app.run(argv)

View File

@@ -1,141 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import re
import shlex
import subprocess
import testtools
from tempest.lib.cli import output_parser
from tempest.lib import exceptions
def credentials():
# You can get credentials from OS environment.
creds_dict = {
'--os-username': os.environ.get('OS_USERNAME'),
'--os-password': os.environ.get('OS_PASSWORD'),
'--os-project-name': os.environ.get('OS_PROJECT_NAME'),
'--os-auth-url': os.environ.get('OS_AUTH_URL'),
'--os-project-domain-name': os.environ.get('OS_PROJECT_DOMAIN_ID'),
'--os-user-domain-name': os.environ.get('OS_USER_DOMAIN_ID'),
}
return [x for sub in creds_dict.items() for x in sub]
def execute(cmd, fail_ok=False, merge_stderr=True):
"""Executes specified command for the given action."""
cmdlist = shlex.split(cmd)
cmdlist.extend(credentials())
stdout = subprocess.PIPE
stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE
proc = subprocess.Popen(cmdlist, stdout=stdout, stderr=stderr)
result, result_err = proc.communicate()
result = result.decode('utf-8')
if not fail_ok and proc.returncode != 0:
raise exceptions.CommandFailed(proc.returncode, cmd, result,
result_err)
return result
class TestCase(testtools.TestCase):
delimiter_line = re.compile(r'^\+\-[\+\-]+\-\+$')
api_version = 1.0
@classmethod
def watcher(cls, cmd, fail_ok=False):
"""Executes watcherclient command for the given action."""
return execute(
'openstack optimize --os-infra-optim-api-version {0} {1}'.format(
cls.api_version, cmd), fail_ok=fail_ok)
@classmethod
def get_opts(cls, fields, format='value'):
return ' -f {0} {1}'.format(format,
' '.join(['-c ' + it for it in fields]))
@classmethod
def assertOutput(cls, expected, actual):
if expected != actual:
raise Exception('{0} != {1}'.format(expected, actual))
@classmethod
def assertInOutput(cls, expected, actual):
if expected not in actual:
raise Exception('{0} not in {1}'.format(expected, actual))
def assert_table_structure(self, items, field_names):
"""Verify that all items have keys listed in field_names."""
for item in items:
for field in field_names:
self.assertIn(field, item)
def assert_show_fields(self, items, field_names):
"""Verify that all items have keys listed in field_names."""
for item in items:
for key in item.keys():
self.assertIn(key, field_names)
def assert_show_structure(self, items, field_names):
"""Verify that all field_names listed in keys of all items."""
if isinstance(items, list):
o = {}
for d in items:
o.update(d)
else:
o = items
item_keys = o.keys()
for field in field_names:
self.assertIn(field, item_keys)
@staticmethod
def parse_show_as_object(raw_output):
"""Return a dict with values parsed from cli output."""
items = TestCase.parse_show(raw_output)
o = {}
for item in items:
o.update(item)
return o
@staticmethod
def parse_show(raw_output):
"""Return list of dicts with item values parsed from cli output."""
items = []
table_ = output_parser.table(raw_output)
for row in table_['values']:
item = {}
item[row[0]] = row[1]
items.append(item)
return items
def parse_listing(self, raw_output):
"""Return list of dicts with basic item parsed from cli output."""
return output_parser.listing(raw_output)
def has_actionplan_succeeded(self, ap_uuid):
return self.parse_show_as_object(
self.watcher('actionplan show %s' % ap_uuid)
)['State'] == 'SUCCEEDED'
@classmethod
def has_audit_created(cls, audit_uuid):
audit = cls.parse_show_as_object(
cls.watcher('audit show %s' % audit_uuid))
if audit['Audit Type'] == 'ONESHOT':
return audit['State'] == 'SUCCEEDED'
else:
return audit['State'] == 'ONGOING'

View File

@@ -1,220 +0,0 @@
# Copyright (c) 2016 Servionica
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_utils import uuidutils
import functools
from tempest.lib.common.utils import test_utils
from watcherclient.tests.client_functional.v1 import base
class ActionTests(base.TestCase):
"""Functional tests for action."""
dummy_name = 'dummy'
list_fields = ['UUID', 'Parents', 'State', 'Action Plan', 'Action']
detailed_list_fields = list_fields + ['Created At', 'Updated At',
'Deleted At', 'Parameters']
audit_template_name = 'a' + uuidutils.generate_uuid()
audit_uuid = None
@classmethod
def setUpClass(cls):
template_raw_output = cls.watcher(
'audittemplate create %s dummy -s dummy' % cls.audit_template_name)
template_output = cls.parse_show_as_object(template_raw_output)
audit_output = cls.parse_show_as_object(cls.watcher(
'audit create -a %s' % template_output['Name']))
cls.audit_uuid = audit_output['UUID']
audit_created = test_utils.call_until_true(
func=functools.partial(cls.has_audit_created, cls.audit_uuid),
duration=600,
sleep_for=2)
if not audit_created:
raise Exception('Audit has not been succeeded')
@classmethod
def tearDownClass(cls):
# Delete Action Plan and all related actions.
output = cls.parse_show(
cls.watcher('actionplan list --audit %s' % cls.audit_uuid))
action_plan_uuid = list(output[0])[0]
raw_output = cls.watcher('actionplan delete %s' % action_plan_uuid)
cls.assertOutput('', raw_output)
# Delete audit
raw_output = cls.watcher('audit delete %s' % cls.audit_uuid)
cls.assertOutput('', raw_output)
# Delete Template
raw_output = cls.watcher(
'audittemplate delete %s' % cls.audit_template_name)
cls.assertOutput('', raw_output)
def test_action_list(self):
raw_output = self.watcher('action list')
self.assert_table_structure([raw_output], self.list_fields)
def test_action_detailed_list(self):
raw_output = self.watcher('action list --detail')
self.assert_table_structure([raw_output], self.detailed_list_fields)
def test_action_show(self):
action_list = self.parse_show(self.watcher('action list --audit %s'
% self.audit_uuid))
action_uuid = list(action_list[0])[0]
action = self.watcher('action show %s' % action_uuid)
self.assertIn(action_uuid, action)
self.assert_table_structure([action],
self.detailed_list_fields)
class ActionUpdateTests(base.TestCase):
"""Functional tests for action update functionality."""
# Use API version 1.5 for action update tests
api_version = 1.5
dummy_name = 'dummy'
audit_template_name = 'b' + uuidutils.generate_uuid()
audit_uuid = None
action_uuid = None
@classmethod
def setUpClass(cls):
# Create audit template
template_raw_output = cls.watcher(
'audittemplate create %s dummy -s dummy' % cls.audit_template_name)
template_output = cls.parse_show_as_object(template_raw_output)
# Create audit
audit_output = cls.parse_show_as_object(cls.watcher(
'audit create -a %s' % template_output['Name']))
cls.audit_uuid = audit_output['UUID']
# Wait for audit to complete
audit_created = test_utils.call_until_true(
func=functools.partial(cls.has_audit_created, cls.audit_uuid),
duration=600,
sleep_for=2)
if not audit_created:
raise Exception('Audit has not been succeeded')
# Get an action to test updates on
action_list = cls.parse_show(cls.watcher('action list --audit %s'
% cls.audit_uuid))
if action_list:
cls.action_uuid = list(action_list[0])[0]
@classmethod
def tearDownClass(cls):
# Clean up: Delete Action Plan and all related actions
if cls.audit_uuid:
output = cls.parse_show(
cls.watcher('actionplan list --audit %s' % cls.audit_uuid))
if output:
action_plan_uuid = list(output[0])[0]
raw_output = cls.watcher(
'actionplan delete %s' % action_plan_uuid)
cls.assertOutput('', raw_output)
# Delete audit
raw_output = cls.watcher('audit delete %s' % cls.audit_uuid)
cls.assertOutput('', raw_output)
# Delete template
raw_output = cls.watcher(
'audittemplate delete %s' % cls.audit_template_name)
cls.assertOutput('', raw_output)
def test_action_update_with_state_and_reason(self):
"""Test updating action state with reason using API 1.5"""
if not self.action_uuid:
self.skipTest("No actions available for testing")
# Update action state to SKIPPED with reason
raw_output = self.watcher(
'action update --state SKIPPED --reason "Functional test skip" %s'
% self.action_uuid)
# Verify the action was updated
action = self.parse_show_as_object(
self.watcher('action show %s' % self.action_uuid))
self.assertEqual('SKIPPED', action['State'])
self.assertEqual('Action skipped by user. Reason: Functional test '
'skip', action['Status Message'])
# Verify output contains the action UUID
self.assertIn(self.action_uuid, raw_output)
def test_action_update_with_state_only(self):
"""Test updating action state without reason"""
if not self.action_uuid:
self.skipTest("No actions available for testing")
# Update action state to SKIPPED without reason
raw_output = self.watcher(
'action update --state SKIPPED %s' % self.action_uuid)
# Verify the action was updated
action = self.parse_show_as_object(
self.watcher('action show %s' % self.action_uuid))
self.assertEqual('SKIPPED', action['State'])
# Verify output contains the action UUID
self.assertIn(self.action_uuid, raw_output)
def test_action_update_missing_state_fails(self):
"""Test that action update fails when no state is provided"""
if not self.action_uuid:
self.skipTest("No actions available for testing")
# This should fail because --state is required
raw_output = self.watcher(
'action update %s' % self.action_uuid, fail_ok=True)
# Should contain error message about missing state
self.assertIn(
'At least one field update is required for this operation',
raw_output)
def test_action_update_nonexistent_action_fails(self):
"""Test that action update fails for non-existent action"""
fake_uuid = uuidutils.generate_uuid()
# This should fail because the action doesn't exist
raw_output = self.watcher(
'action update --state SKIPPED %s' % fake_uuid, fail_ok=True)
# Should contain error message about action not found
self.assertIn('404', raw_output)
class ActionUpdateApiVersionTests(base.TestCase):
"""Test action update functionality with different API versions."""
# Use API version 1.0 to test version checking
api_version = 1.0
def test_action_update_unsupported_api_version(self):
"""Test that action update fails with API version < 1.5"""
fake_uuid = uuidutils.generate_uuid()
# This should fail because API version 1.0 doesn't support updates
raw_output = self.watcher(
'action update --state SKIPPED %s' % fake_uuid, fail_ok=True)
# Should contain error message about unsupported API version
self.assertIn('not supported in API version', raw_output)
self.assertIn('Minimum required version is 1.5', raw_output)

View File

@@ -1,97 +0,0 @@
# Copyright (c) 2016 Servionica
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_utils import uuidutils
import functools
from tempest.lib.common.utils import test_utils
from watcherclient.tests.client_functional.v1 import base
class ActionPlanTests(base.TestCase):
"""Functional tests for action plan."""
dummy_name = 'dummy'
list_fields = ['UUID', 'Audit', 'State', 'Updated At', 'Global efficacy']
detailed_list_fields = list_fields + ['Created At', 'Deleted At',
'Strategy', 'Efficacy indicators',
'Hostname']
audit_template_name = 'a' + uuidutils.generate_uuid()
audit_uuid = None
@classmethod
def setUpClass(cls):
template_raw_output = cls.watcher(
'audittemplate create %s dummy -s dummy' % cls.audit_template_name)
template_output = cls.parse_show_as_object(template_raw_output)
audit_raw_output = cls.watcher('audit create -a %s'
% template_output['Name'])
audit_output = cls.parse_show_as_object(audit_raw_output)
cls.audit_uuid = audit_output['UUID']
audit_created = test_utils.call_until_true(
func=functools.partial(cls.has_audit_created, cls.audit_uuid),
duration=600,
sleep_for=2)
if not audit_created:
raise Exception('Audit has not been succeeded')
@classmethod
def tearDownClass(cls):
# Delete action plan
output = cls.parse_show(
cls.watcher('actionplan list --audit %s' % cls.audit_uuid))
action_plan_uuid = list(output[0])[0]
raw_output = cls.watcher('actionplan delete %s' % action_plan_uuid)
cls.assertOutput('', raw_output)
# Delete audit
raw_output = cls.watcher('audit delete %s' % cls.audit_uuid)
cls.assertOutput('', raw_output)
# Delete Template
raw_output = cls.watcher(
'audittemplate delete %s' % cls.audit_template_name)
cls.assertOutput('', raw_output)
def test_action_plan_list(self):
raw_output = self.watcher('actionplan list')
self.assert_table_structure([raw_output], self.list_fields)
def test_action_plan_detailed_list(self):
raw_output = self.watcher('actionplan list --detail')
self.assert_table_structure([raw_output], self.detailed_list_fields)
def test_action_plan_show(self):
action_plan_list = self.parse_show(self.watcher('actionplan list'))
action_plan_uuid = list(action_plan_list[0])[0]
actionplan = self.watcher('actionplan show %s' % action_plan_uuid)
self.assertIn(action_plan_uuid, actionplan)
self.assert_table_structure([actionplan],
self.detailed_list_fields)
def test_action_plan_start(self):
output = self.parse_show(self.watcher('actionplan list --audit %s'
% self.audit_uuid))
action_plan_uuid = list(output[0])[0]
self.watcher('actionplan start %s' % action_plan_uuid)
raw_output = self.watcher('actionplan show %s' % action_plan_uuid)
self.assert_table_structure([raw_output], self.detailed_list_fields)
self.assertTrue(test_utils.call_until_true(
func=functools.partial(
self.has_actionplan_succeeded, action_plan_uuid),
duration=600,
sleep_for=2
))

View File

@@ -1,212 +0,0 @@
# Copyright (c) 2016 Servionica
#
# 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 datetime import datetime
from dateutil import tz
import functools
from oslo_utils import uuidutils
from tempest.lib.common.utils import test_utils
from watcherclient.tests.client_functional.v1 import base
class AuditTests(base.TestCase):
"""Functional tests for audit."""
dummy_name = 'dummy'
list_fields = ['UUID', 'Name', 'Audit Type', 'State', 'Goal', 'Strategy']
detailed_list_fields = list_fields + ['Created At', 'Updated At',
'Deleted At', 'Parameters',
'Interval', 'Audit Scope',
'Next Run Time', 'Hostname']
audit_template_name = 'a' + uuidutils.generate_uuid()
audit_uuid = None
@classmethod
def setUpClass(cls):
raw_output = cls.watcher('audittemplate create %s dummy -s dummy'
% cls.audit_template_name)
template_output = cls.parse_show_as_object(raw_output)
audit_raw_output = cls.watcher(
'audit create -a %s' % template_output['Name'])
audit_output = cls.parse_show_as_object(audit_raw_output)
cls.audit_uuid = audit_output['UUID']
audit_created = test_utils.call_until_true(
func=functools.partial(cls.has_audit_created, cls.audit_uuid),
duration=600,
sleep_for=2)
if not audit_created:
raise Exception('Audit has not been succeeded')
@classmethod
def tearDownClass(cls):
output = cls.parse_show(
cls.watcher('actionplan list --audit %s' % cls.audit_uuid))
action_plan_uuid = list(output[0])[0]
cls.watcher('actionplan delete %s' % action_plan_uuid)
cls.watcher('audit delete %s' % cls.audit_uuid)
cls.watcher('audittemplate delete %s' % cls.audit_template_name)
def test_audit_list(self):
raw_output = self.watcher('audit list')
self.assert_table_structure([raw_output], self.list_fields)
def test_audit_detailed_list(self):
raw_output = self.watcher('audit list --detail')
self.assert_table_structure([raw_output], self.detailed_list_fields)
def test_audit_show(self):
audit = self.watcher('audit show ' + self.audit_uuid)
self.assertIn(self.audit_uuid, audit)
self.assert_table_structure([audit], self.detailed_list_fields)
def test_audit_update(self):
audit_raw_output = self.watcher('audit update %s add interval=2'
% self.audit_uuid)
audit_output = self.parse_show_as_object(audit_raw_output)
assert int(audit_output['Interval']) == 2
class AuditTestsV11(AuditTests):
"""This class tests v1.1 of Watcher API"""
api_version = 1.1
detailed_list_fields = AuditTests.list_fields + [
'Created At', 'Updated At', 'Deleted At', 'Parameters', 'Interval',
'Audit Scope', 'Next Run Time', 'Hostname', 'Start Time', 'End Time']
def test_audit_detailed_list(self):
raw_output = self.watcher('audit list --detail')
self.assert_table_structure([raw_output], self.detailed_list_fields)
def test_audit_show(self):
audit = self.watcher('audit show ' + self.audit_uuid)
self.assertIn(self.audit_uuid, audit)
self.assert_table_structure([audit], self.detailed_list_fields)
def test_audit_update(self):
local_time = datetime.now(tz.tzlocal())
local_time_str = local_time.strftime("%Y-%m-%dT%H:%M:%S")
utc_time = (local_time - local_time.utcoffset())
utc_time_str = utc_time.strftime("%Y-%m-%dT%H:%M:%S")
audit_raw_output = self.watcher(
'audit update {0} replace end_time="{1}"'.format(self.audit_uuid,
local_time_str))
audit_output = self.parse_show_as_object(audit_raw_output)
assert audit_output['End Time'] == utc_time_str
class AuditTestsV12(AuditTestsV11):
"""This class tests v1.2 of Watcher API"""
api_version = 1.2
@classmethod
def setUpClass(cls):
raw_output = cls.watcher('audittemplate create %s dummy -s dummy'
% cls.audit_template_name)
template_output = cls.parse_show_as_object(raw_output)
audit_raw_output = cls.watcher(
'audit create --force -a %s' % template_output['Name'])
audit_output = cls.parse_show_as_object(audit_raw_output)
cls.audit_uuid = audit_output['UUID']
audit_created = test_utils.call_until_true(
func=functools.partial(cls.has_audit_created, cls.audit_uuid),
duration=600,
sleep_for=2)
if not audit_created:
raise Exception('Audit has not been succeeded')
class AuditActiveTests(base.TestCase):
list_fields = ['UUID', 'Name', 'Audit Type', 'State', 'Goal', 'Strategy']
detailed_list_fields = list_fields + ['Created At', 'Updated At',
'Deleted At', 'Parameters',
'Interval', 'Audit Scope']
audit_template_name = 'a' + uuidutils.generate_uuid()
@classmethod
def setUpClass(cls):
cls.watcher('audittemplate create %s dummy -s dummy'
% cls.audit_template_name)
@classmethod
def tearDownClass(cls):
cls.watcher('audittemplate delete %s' % cls.audit_template_name)
def _create_audit(self):
return self.parse_show_as_object(
self.watcher('audit create -a %s'
% self.audit_template_name))['UUID']
def _delete_audit(self, audit_uuid):
self.assertTrue(test_utils.call_until_true(
func=functools.partial(
self.has_audit_created, audit_uuid),
duration=600,
sleep_for=2
))
output = self.parse_show(
self.watcher('actionplan list --audit %s' % audit_uuid))
action_plan_uuid = list(output[0])[0]
self.watcher('actionplan delete %s' % action_plan_uuid)
self.watcher('audit delete %s' % audit_uuid)
def test_create_oneshot_audit(self):
audit = self.watcher('audit create -a %s' % self.audit_template_name)
audit_uuid = self.parse_show_as_object(audit)['UUID']
self.assert_table_structure([audit], self.detailed_list_fields)
self._delete_audit(audit_uuid)
def test_delete_oneshot_audit(self):
audit_uuid = self._create_audit()
self.assertTrue(test_utils.call_until_true(
func=functools.partial(
self.has_audit_created, audit_uuid),
duration=600,
sleep_for=2
))
raw_output = self.watcher('audit delete %s' % audit_uuid)
self.assertOutput('', raw_output)
output = self.parse_show(
self.watcher('actionplan list --audit %s' % audit_uuid))
action_plan_uuid = list(output[0])[0]
self.watcher('actionplan delete %s' % action_plan_uuid)
def test_continuous_audit(self):
audit = self.watcher('audit create -a %s -t CONTINUOUS -i 600'
% self.audit_template_name)
audit_uuid = self.parse_show_as_object(audit)['UUID']
self.assert_table_structure([audit], self.detailed_list_fields)
self.assertTrue(test_utils.call_until_true(
func=functools.partial(
self.has_audit_created, audit_uuid),
duration=600,
sleep_for=2
))
audit_state = self.parse_show_as_object(
self.watcher('audit show %s' % audit_uuid))['State']
if audit_state == 'ONGOING':
self.watcher('audit update %s replace state=CANCELLED'
% audit_uuid)
raw_output = self.watcher('audit delete %s' % audit_uuid)
self.assertOutput('', raw_output)
outputs = self.parse_listing(
self.watcher('actionplan list --audit %s' % audit_uuid))
for actionplan in outputs:
self.watcher('actionplan delete %s' % actionplan['UUID'])

View File

@@ -1,89 +0,0 @@
# Copyright (c) 2016 Servionica
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_utils import uuidutils
from watcherclient.tests.client_functional.v1 import base
class AuditTemplateTests(base.TestCase):
"""Functional tests for audit template."""
dummy_name = 'dummy'
list_fields = ['UUID', 'Name', 'Goal', 'Strategy']
detailed_list_fields = list_fields + ['Created At', 'Updated At',
'Deleted At', 'Description',
'Audit Scope']
audit_template_name = 'a' + uuidutils.generate_uuid()
@classmethod
def setUpClass(cls):
cls.watcher('audittemplate create %s dummy -s dummy'
% cls.audit_template_name)
@classmethod
def tearDownClass(cls):
cls.watcher('audittemplate delete %s' % cls.audit_template_name)
def test_audit_template_list(self):
raw_output = self.watcher('audittemplate list')
self.assert_table_structure([raw_output], self.list_fields)
def test_audit_template_detailed_list(self):
raw_output = self.watcher('audittemplate list --detail')
self.assert_table_structure([raw_output], self.detailed_list_fields)
def test_audit_template_show(self):
audit_template = self.watcher(
'audittemplate show %s' % self.audit_template_name)
self.assertIn(self.audit_template_name, audit_template)
self.assert_table_structure([audit_template],
self.detailed_list_fields)
def test_audit_template_update(self):
raw_output = self.watcher('audittemplate update %s replace '
'description="Updated Desc"'
% self.audit_template_name)
audit_template_output = self.parse_show_as_object(raw_output)
assert audit_template_output['Description'] == 'Updated Desc'
class AuditTemplateActiveTests(base.TestCase):
audit_template_name = 'b' + uuidutils.generate_uuid()
list_fields = ['UUID', 'Name', 'Goal', 'Strategy']
detailed_list_fields = list_fields + ['Created At', 'Updated At',
'Deleted At', 'Description',
'Audit Scope']
def _create_audit_template(self):
self.watcher('audittemplate create %s dummy -s dummy '
'-d "Test Audit Template"' % self.audit_template_name)
def _delete_audit_template(self):
self.watcher('audittemplate delete %s' % self.audit_template_name)
def test_create_audit_template(self):
raw_output = self.watcher('audittemplate create %s dummy '
'-s dummy -d "Test Audit Template"'
% self.audit_template_name)
self.assert_table_structure([raw_output], self.detailed_list_fields)
self._delete_audit_template()
def test_delete_audit_template(self):
self._create_audit_template()
raw_output = self.watcher('audittemplate delete %s'
% self.audit_template_name)
self.assertOutput('', raw_output)

View File

@@ -1,41 +0,0 @@
# Copyright (c) 2016 Servionica
#
# 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.client_functional.v1 import base
class GoalTests(base.TestCase):
"""Functional tests for goal."""
dummy_name = 'dummy'
list_fields = ['UUID', 'Name', 'Display name']
def test_goal_list(self):
raw_output = self.watcher('goal list')
self.assertIn(self.dummy_name, raw_output)
self.assert_table_structure([raw_output], self.list_fields)
def test_goal_detailed_list(self):
raw_output = self.watcher('goal list --detail')
self.assertIn(self.dummy_name, raw_output)
self.assert_table_structure(
[raw_output], self.list_fields + ['Efficacy specification'])
def test_goal_show(self):
raw_output = self.watcher('goal show %s' % self.dummy_name)
self.assertIn(self.dummy_name, raw_output)
self.assert_table_structure(
[raw_output], self.list_fields + ['Efficacy specification'])
self.assertNotIn('server_consolidation', raw_output)

View File

@@ -1,40 +0,0 @@
# Copyright (c) 2016 Servionica
#
# 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.client_functional.v1 import base
class ScoringEngineTests(base.TestCase):
"""Functional tests for scoring engine."""
dummy_name = 'dummy_scorer'
list_fields = ['UUID', 'Name', 'Description']
detailed_list_fields = list_fields + ['Metainfo']
def test_scoringengine_list(self):
raw_output = self.watcher('scoringengine list')
self.assertIn(self.dummy_name, raw_output)
self.assert_table_structure([raw_output], self.list_fields)
def test_scoringengine_detailed_list(self):
raw_output = self.watcher('scoringengine list --detail')
self.assertIn(self.dummy_name, raw_output)
self.assert_table_structure([raw_output], self.detailed_list_fields)
def test_scoringengine_show(self):
raw_output = self.watcher('scoringengine show %s' % self.dummy_name)
self.assertIn(self.dummy_name, raw_output)
self.assert_table_structure([raw_output], self.detailed_list_fields)
self.assertNotIn('dummy_avg_scorer', raw_output)

View File

@@ -1,47 +0,0 @@
# Copyright (c) 2016 Servionica
#
# 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.client_functional.v1 import base
class ServiceTests(base.TestCase):
"""Functional tests for service."""
decision_engine_name = 'watcher-decision-engine'
applier_name = 'watcher-applier'
list_fields = ['ID', 'Name', 'Host', 'Status']
def test_service_list(self):
raw_output = self.watcher('service list')
self.assertIn(self.decision_engine_name, raw_output)
self.assertIn(self.applier_name, raw_output)
self.assert_table_structure([raw_output], self.list_fields)
def test_service_detailed_list(self):
raw_output = self.watcher('service list --detail')
self.assertIn(self.decision_engine_name, raw_output)
self.assertIn(self.applier_name, raw_output)
self.assert_table_structure([raw_output],
self.list_fields + ['Last seen up'])
def test_service_show(self):
# TODO(alexchadin): this method should be refactored since Watcher will
# get HA support soon.
raw_output = self.watcher('service show %s'
% self.decision_engine_name)
self.assertIn(self.decision_engine_name, raw_output)
self.assert_table_structure([raw_output],
self.list_fields + ['Last seen up'])
self.assertNotIn(self.applier_name, raw_output)

View File

@@ -1,48 +0,0 @@
# Copyright (c) 2016 Servionica
#
# 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.client_functional.v1 import base
class StrategyTests(base.TestCase):
"""Functional tests for strategy."""
dummy_name = 'dummy'
basic_strategy = 'basic'
list_fields = ['UUID', 'Name', 'Display name', 'Goal']
state_fields = ['Datasource', 'Metrics', 'CDM', 'Name']
def test_strategy_list(self):
raw_output = self.watcher('strategy list')
self.assertIn(self.dummy_name, raw_output)
self.assert_table_structure([raw_output], self.list_fields)
def test_strategy_detailed_list(self):
raw_output = self.watcher('strategy list --detail')
self.assertIn(self.dummy_name, raw_output)
self.assert_table_structure([raw_output],
self.list_fields + ['Parameters spec'])
def test_strategy_show(self):
raw_output = self.watcher('strategy show %s' % self.dummy_name)
self.assertIn(self.dummy_name, raw_output)
self.assert_table_structure([raw_output],
self.list_fields + ['Parameters spec'])
self.assertNotIn('basic', raw_output)
def test_strategy_state(self):
raw_output = self.watcher('strategy state %s' % self.basic_strategy)
self.assertIn(self.basic_strategy, raw_output)
self.assert_table_structure([raw_output], self.state_fields)

View File

@@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
import mock
from watcherclient.common import api_versioning
from watcherclient import exceptions
@@ -148,22 +148,3 @@ class GetAPIVersionTestCase(utils.BaseTestCase):
self.assertEqual(mock_apiversion.return_value,
api_versioning.get_api_version(version))
mock_apiversion.assert_called_once_with(version)
class APIVersionFunctionsTestCase(utils.BaseTestCase):
def test_action_update_supported_true(self):
# Test versions >= 1.5 support action update
self.assertTrue(api_versioning.action_update_supported("1.5"))
self.assertTrue(api_versioning.action_update_supported("1.6"))
self.assertTrue(api_versioning.action_update_supported("2.0"))
def test_action_update_supported_false(self):
# Test versions < 1.5 do not support action update
self.assertFalse(api_versioning.action_update_supported("1.0"))
self.assertFalse(api_versioning.action_update_supported("1.1"))
self.assertFalse(api_versioning.action_update_supported("1.4"))
def test_action_update_supported_edge_case(self):
# Test exact boundary
self.assertTrue(api_versioning.action_update_supported("1.5"))
self.assertFalse(api_versioning.action_update_supported("1.4"))

View File

@@ -10,7 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
import mock
from keystoneauth1 import loading as kaloading
@@ -358,8 +358,3 @@ class ClientTest(utils.BaseTestCase):
client = httpclient.HTTPClient(endpoint)
conn_url = client._make_connection_url(url)
self.assertEqual(expected_url, conn_url)
def test_port_ends_with_one(self):
endpoint = "http://localhost:8081/"
http_client = httpclient.HTTPClient(endpoint)
self.assertEqual(endpoint, http_client._make_connection_url(""))

View File

@@ -14,7 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
import mock
from watcherclient.common.apiclient import exceptions as exc
from watcherclient.common import utils

View File

@@ -14,12 +14,12 @@
# under the License.
import copy
import io
import os
from unittest import mock
import fixtures
import mock
from oslo_utils import strutils
import six
import testtools
@@ -51,7 +51,7 @@ class FakeAPI(object):
def raw_request(self, *args, **kwargs):
response = self._request(*args, **kwargs)
body_iter = iter(io.StringIO(response[1]))
body_iter = iter(six.StringIO(response[1]))
return FakeResponse(response[0]), body_iter
def json_request(self, *args, **kwargs):

View File

@@ -14,8 +14,8 @@
# limitations under the License.
import shlex
from unittest import mock
import mock
from osc_lib import utils as oscutils
from oslo_serialization import jsonutils

View File

@@ -92,10 +92,6 @@ fake_responses = {
{},
None,
),
'PATCH': (
{},
ACTION1,
),
},
'/v1/actions/detail?action_plan_uuid=%s' % ACTION1['action_plan']:
{
@@ -268,12 +264,3 @@ class ActionManagerTest(testtools.TestCase):
self.assertEqual(ACTION1['uuid'], action.uuid)
self.assertEqual(ACTION1['action_plan'], action.action_plan)
self.assertEqual(ACTION1['next'], action.next)
def test_actions_update(self):
patch = [{'op': 'replace', 'path': '/state', 'value': 'SKIPPED'}]
action = self.mgr.update(ACTION1['uuid'], patch)
expect = [
('PATCH', '/v1/actions/%s' % ACTION1['uuid'], {}, patch),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(ACTION1['uuid'], action.uuid)

View File

@@ -14,7 +14,7 @@
# under the License.
import copy
from unittest import mock
import mock
import testtools
from testtools import matchers

View File

@@ -14,11 +14,10 @@
# limitations under the License.
import datetime
import io
from unittest import mock
import mock
import six
from oslo_utils.uuidutils import generate_uuid
from watcherclient import exceptions
from watcherclient import shell
from watcherclient.tests.unit.v1 import base
@@ -79,9 +78,8 @@ class ActionPlanShellTest(base.CommandTestCase):
FIELD_LABELS = resource_fields.ACTION_PLAN_FIELD_LABELS
GLOBAL_EFFICACY_FIELDS = resource_fields.GLOBAL_EFFICACY_FIELDS
def setUp(self, os_infra_optim_api_version='1.0'):
super(ActionPlanShellTest, self).setUp(
os_infra_optim_api_version=os_infra_optim_api_version)
def setUp(self):
super(self.__class__, self).setUp()
p_audit_manager = mock.patch.object(resource, 'AuditManager')
p_audit_template_manager = mock.patch.object(
@@ -105,7 +103,7 @@ class ActionPlanShellTest(base.CommandTestCase):
self.m_audit_template_mgr_cls.return_value = self.m_audit_template_mgr
self.m_action_plan_mgr_cls.return_value = self.m_action_plan_mgr
self.stdout = io.StringIO()
self.stdout = six.StringIO()
self.cmd = shell.WatcherShell(stdout=self.stdout)
def test_do_action_plan_list(self):
@@ -337,14 +335,3 @@ class ActionPlanShellTest(base.CommandTestCase):
self.assertEqual(1, exit_code)
self.assertEqual('', result)
class ActionPlanShellTest15(ActionPlanShellTest):
def setUp(self):
super(ActionPlanShellTest15, self).setUp(
os_infra_optim_api_version='1.5')
v15 = dict(status_message=None)
for action_plan in (ACTION_PLAN_1, ACTION_PLAN_2):
action_plan.update(v15)
self.FIELDS.extend(['status_message'])
self.FIELD_LABELS.extend(['Status Message'])

View File

@@ -14,9 +14,8 @@
# under the License.
import datetime
import io
import unittest
from unittest import mock
import mock
import six
from watcherclient import exceptions
from watcherclient import shell
@@ -79,9 +78,8 @@ class ActionShellTest(base.CommandTestCase):
FIELDS = resource_fields.ACTION_FIELDS
FIELD_LABELS = resource_fields.ACTION_FIELD_LABELS
def setUp(self, os_infra_optim_api_version='1.0'):
super(ActionShellTest, self).setUp(
os_infra_optim_api_version=os_infra_optim_api_version)
def setUp(self):
super(self.__class__, self).setUp()
p_action_manager = mock.patch.object(resource, 'ActionManager')
p_action_plan_manager = mock.patch.object(
@@ -97,7 +95,7 @@ class ActionShellTest(base.CommandTestCase):
self.m_action_mgr_cls.return_value = self.m_action_mgr
self.m_action_plan_mgr_cls.return_value = self.m_action_plan_mgr
self.stdout = io.StringIO()
self.stdout = six.StringIO()
self.cmd = shell.WatcherShell(stdout=self.stdout)
def test_do_action_list(self):
@@ -178,106 +176,3 @@ class ActionShellTest(base.CommandTestCase):
self.assertEqual(1, exit_code)
self.assertEqual('', result)
def test_do_action_update_unsupported_version(self):
exit_code, result = self.run_cmd(
'action update --state SKIPPED '
'770ef053-ecb3-48b0-85b5-d55a2dbc6588',
formatting=None)
self.assertEqual(1, exit_code)
self.assertEqual('', result)
class ActionShellTest15(ActionShellTest):
def setUp(self):
super(ActionShellTest15, self).setUp(os_infra_optim_api_version='1.5')
v15 = dict(status_message=None)
for action in (ACTION_1, ACTION_2, ACTION_3):
action.update(v15)
self.FIELDS.extend(['status_message'])
self.FIELD_LABELS.extend(['Status Message'])
def test_do_action_update_with_state_only(self):
action = resource.Action(mock.Mock(), ACTION_1)
self.m_action_mgr.update.return_value = action
exit_code, result = self.run_cmd(
'action update --state SKIPPED '
'770ef053-ecb3-48b0-85b5-d55a2dbc6588')
self.assertEqual(0, exit_code)
self.assertEqual(
self.resource_as_dict(action, self.FIELDS, self.FIELD_LABELS),
result)
expected_patch = [
{'op': 'replace', 'path': '/state', 'value': 'SKIPPED'}
]
self.m_action_mgr.update.assert_called_once_with(
'770ef053-ecb3-48b0-85b5-d55a2dbc6588', expected_patch)
def test_do_action_update_with_state_and_reason(self):
action = resource.Action(mock.Mock(), ACTION_1)
self.m_action_mgr.update.return_value = action
exit_code, result = self.run_cmd(
'action update --state SKIPPED --reason "Manual skip" '
'770ef053-ecb3-48b0-85b5-d55a2dbc6588')
self.assertEqual(0, exit_code)
self.assertEqual(
self.resource_as_dict(action, self.FIELDS, self.FIELD_LABELS),
result)
expected_patch = [
{'op': 'replace', 'path': '/state', 'value': 'SKIPPED'},
{'op': 'replace', 'path': '/status_message',
'value': 'Manual skip'}
]
self.m_action_mgr.update.assert_called_once_with(
'770ef053-ecb3-48b0-85b5-d55a2dbc6588', expected_patch)
def test_do_action_update_with_reason_only(self):
action = resource.Action(mock.Mock(), ACTION_1)
self.m_action_mgr.update.return_value = action
exit_code, result = self.run_cmd(
'action update --reason "Manual skip" '
'770ef053-ecb3-48b0-85b5-d55a2dbc6588')
self.assertEqual(0, exit_code)
self.assertEqual(
self.resource_as_dict(action, self.FIELDS, self.FIELD_LABELS),
result)
expected_patch = [
{'op': 'replace', 'path': '/status_message',
'value': 'Manual skip'}
]
self.m_action_mgr.update.assert_called_once_with(
'770ef053-ecb3-48b0-85b5-d55a2dbc6588', expected_patch)
def test_do_action_update_no_fields_to_update(self):
exit_code, result = self.run_cmd(
'action update 770ef053-ecb3-48b0-85b5-d55a2dbc6588',
formatting=None)
self.assertEqual(1, exit_code)
self.assertEqual('', result)
def test_do_action_update_action_not_found(self):
self.m_action_mgr.update.side_effect = exceptions.HTTPNotFound
exit_code, result = self.run_cmd(
'action update --state SKIPPED not_found_uuid',
formatting=None)
self.assertEqual(1, exit_code)
self.assertEqual('', result)
@unittest.skip("Action update is supported in API version 1.5")
def test_do_action_update_unsupported_version(self):
pass

56
watcherclient/tests/unit/v1/test_audit_shell.py Normal file → Executable file
View File

@@ -14,8 +14,8 @@
# under the License.
import datetime
import io
from unittest import mock
import mock
import six
from watcherclient import shell
from watcherclient.tests.unit.v1 import base
@@ -163,7 +163,7 @@ class AuditShellTest(base.CommandTestCase):
self.m_audit_template_mgr_cls.return_value = self.m_audit_template_mgr
# stdout mock
self.stdout = io.StringIO()
self.stdout = six.StringIO()
self.cmd = shell.WatcherShell(stdout=self.stdout)
def test_do_audit_list(self):
@@ -426,23 +426,6 @@ class AuditShellTest(base.CommandTestCase):
parameters={'para1': 10, 'para2': 20}
)
def test_do_audit_create_with_type_event(self):
audit = resource.Audit(mock.Mock(), self.AUDIT_1)
self.m_audit_mgr.create.return_value = audit
exit_code, result = self.run_cmd(
'audit create -g fc087747-61be-4aad-8126-b701731ae836 -t EVENT')
self.assertEqual(0, exit_code)
self.assertEqual(
self.resource_as_dict(audit, self.FIELDS, self.FIELD_LABELS),
result)
self.m_audit_mgr.create.assert_called_once_with(
goal='fc087747-61be-4aad-8126-b701731ae836',
auto_trigger=False,
audit_type='EVENT'
)
def test_do_audit_create_with_type_continuous(self):
audit = resource.Audit(mock.Mock(), self.AUDIT_1)
self.m_audit_mgr.create.return_value = audit
@@ -494,9 +477,8 @@ class AuditShellTestv11(AuditShellTest):
class AuditShellTestv12(AuditShellTest):
def setUp(self, os_infra_optim_api_version='1.2'):
super(AuditShellTestv12, self).setUp(
os_infra_optim_api_version=os_infra_optim_api_version)
def setUp(self):
super(AuditShellTestv12, self).setUp(os_infra_optim_api_version='1.2')
v11 = dict(start_time=None, end_time=None)
v12 = dict(force=False)
for audit in (self.AUDIT_1, self.AUDIT_2, self.AUDIT_3):
@@ -660,24 +642,6 @@ class AuditShellTestv12(AuditShellTest):
force=False
)
def test_do_audit_create_with_type_event(self):
audit = resource.Audit(mock.Mock(), self.AUDIT_1)
self.m_audit_mgr.create.return_value = audit
exit_code, result = self.run_cmd(
'audit create -g fc087747-61be-4aad-8126-b701731ae836 -t EVENT')
self.assertEqual(0, exit_code)
self.assertEqual(
self.resource_as_dict(audit, self.FIELDS, self.FIELD_LABELS),
result)
self.m_audit_mgr.create.assert_called_once_with(
goal='fc087747-61be-4aad-8126-b701731ae836',
auto_trigger=False,
audit_type='EVENT',
force=False
)
def test_do_audit_create_with_name(self):
audit = resource.Audit(mock.Mock(), self.AUDIT_1)
self.m_audit_mgr.create.return_value = audit
@@ -698,13 +662,3 @@ class AuditShellTestv12(AuditShellTest):
name='my_audit',
force=False
)
class AuditShellTestv15(AuditShellTestv12):
def setUp(self):
super(AuditShellTestv15, self).setUp(os_infra_optim_api_version='1.5')
v15 = dict(status_message=None)
for audit in (self.AUDIT_1, self.AUDIT_2, self.AUDIT_3):
audit.update(v15)
self.FIELDS.extend(['status_message'])
self.FIELD_LABELS.extend(['Status Message'])

View File

@@ -14,8 +14,8 @@
# under the License.
import copy
from urllib import parse as urlparse
from six.moves.urllib import parse as urlparse
from testtools import matchers
from watcherclient.tests.unit import utils

View File

@@ -14,8 +14,8 @@
# under the License.
import datetime
import io
from unittest import mock
import mock
import six
from watcherclient import shell
from watcherclient.tests.unit.v1 import base
@@ -107,7 +107,7 @@ class AuditTemplateShellTest(base.CommandTestCase):
self.m_audit_template_mgr_cls.return_value = self.m_audit_template_mgr
# stdout mock
self.stdout = io.StringIO()
self.stdout = six.StringIO()
self.cmd = shell.WatcherShell(stdout=self.stdout)
def test_do_audit_template_list(self):

View File

@@ -12,9 +12,8 @@
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import io
from unittest import mock
import mock
import six
from watcherclient import shell
from watcherclient.tests.unit.v1 import base
@@ -91,7 +90,7 @@ class DataModelShellTest(base.CommandTestCase):
self.m_data_model_mgr_cls.return_value = self.m_data_model_mgr
self.stdout = io.StringIO()
self.stdout = six.StringIO()
self.cmd = shell.WatcherShell(stdout=self.stdout)
def test_do_data_model_list(self):

View File

@@ -14,8 +14,8 @@
# limitations under the License.
import datetime
import io
from unittest import mock
import mock
import six
from watcherclient import shell
from watcherclient.tests.unit.v1 import base
@@ -72,7 +72,7 @@ class GoalShellTest(base.CommandTestCase):
self.m_goal_mgr = mock.Mock()
self.m_goal_mgr_cls.return_value = self.m_goal_mgr
self.stdout = io.StringIO()
self.stdout = six.StringIO()
self.cmd = shell.WatcherShell(stdout=self.stdout)
def test_do_goal_list(self):

View File

@@ -14,8 +14,8 @@
# limitations under the License.
import datetime
import io
from unittest import mock
import mock
import six
from watcherclient import shell
from watcherclient.tests.unit.v1 import base
@@ -62,7 +62,7 @@ class ScoringEngineShellTest(base.CommandTestCase):
self.m_se_mgr = mock.Mock()
self.m_se_mgr_cls.return_value = self.m_se_mgr
self.stdout = io.StringIO()
self.stdout = six.StringIO()
self.cmd = shell.WatcherShell(stdout=self.stdout)
def test_do_scoringengine_list(self):

View File

@@ -14,8 +14,8 @@
# limitations under the License.
import datetime
import io
from unittest import mock
import mock
import six
from watcherclient import shell
from watcherclient.tests.unit.v1 import base
@@ -61,7 +61,7 @@ class ServiceShellTest(base.CommandTestCase):
self.m_service_mgr = mock.Mock()
self.m_service_mgr_cls.return_value = self.m_service_mgr
self.stdout = io.StringIO()
self.stdout = six.StringIO()
self.cmd = shell.WatcherShell(stdout=self.stdout)
def test_do_service_list(self):

View File

@@ -14,8 +14,8 @@
# limitations under the License.
import datetime
import io
from unittest import mock
import mock
import six
from oslo_serialization import jsonutils
@@ -69,7 +69,7 @@ class StrategyShellTest(base.CommandTestCase):
self.m_strategy_mgr = mock.Mock()
self.m_strategy_mgr_cls.return_value = self.m_strategy_mgr
self.stdout = io.StringIO()
self.stdout = six.StringIO()
self.cmd = shell.WatcherShell(stdout=self.stdout)
def test_do_strategy_list(self):

View File

@@ -83,6 +83,3 @@ class ActionManager(base.Manager):
return self._list(self._path(action_id))[0]
except IndexError:
return None
def update(self, action_id, patch):
return self._update(self._path(action_id), patch)

View File

@@ -13,31 +13,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import io
from cliff.formatters import yaml_format
from osc_lib import utils
from oslo_utils import uuidutils
import six
from watcherclient._i18n import _
from watcherclient.common import api_versioning
from watcherclient.common import command
from watcherclient.common import utils as common_utils
from watcherclient import exceptions
from watcherclient.v1 import resource_fields as res_fields
def drop_unsupported_field(app_args, fields, field_labels):
fields = copy.copy(fields)
field_labels = copy.copy(field_labels)
api_ver = app_args.os_infra_optim_api_version
if not api_versioning.action_update_supported(api_ver):
fields.remove('status_message')
field_labels.remove('Status Message')
return fields, field_labels
def format_global_efficacy(global_efficacy):
formatted_global_eff = {}
for eff in global_efficacy:
@@ -64,7 +51,7 @@ class ShowActionPlan(command.ShowOne):
return parser
def _format_indicators(self, action_plan, parsed_args):
out = io.StringIO()
out = six.StringIO()
efficacy_indicators = action_plan.efficacy_indicators
fields = ['name', 'description', 'value', 'unit']
yaml_format.YAMLFormatter().emit_list(
@@ -79,7 +66,7 @@ class ShowActionPlan(command.ShowOne):
def _format_global_efficacy(self, global_efficacy, parsed_args):
formatted_global_efficacy = format_global_efficacy(global_efficacy)
out = io.StringIO()
out = six.StringIO()
yaml_format.YAMLFormatter().emit_one(
column_names=list(resource.capitalize()
for resource in formatted_global_efficacy),
@@ -113,8 +100,6 @@ class ShowActionPlan(command.ShowOne):
columns = res_fields.ACTION_PLAN_FIELDS
column_headers = res_fields.ACTION_PLAN_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action_plan, columns)
@@ -158,7 +143,7 @@ class ListActionPlan(command.Lister):
return parser
def _format_indicators(self, action_plan, parsed_args):
out = io.StringIO()
out = six.StringIO()
efficacy_indicators = action_plan.efficacy_indicators
fields = ['name', 'value', 'unit']
yaml_format.YAMLFormatter().emit_list(
@@ -173,7 +158,7 @@ class ListActionPlan(command.Lister):
def _format_global_efficacy(self, global_efficacy, parsed_args):
formatted_global_efficacy = format_global_efficacy(global_efficacy)
out = io.StringIO()
out = six.StringIO()
yaml_format.YAMLFormatter().emit_one(
column_names=list(resource.capitalize()
for resource in formatted_global_efficacy),
@@ -192,8 +177,6 @@ class ListActionPlan(command.Lister):
if parsed_args.detail:
fields = res_fields.ACTION_PLAN_FIELDS
field_labels = res_fields.ACTION_PLAN_FIELD_LABELS
fields, field_labels = drop_unsupported_field(
self.app_args, fields, field_labels)
else:
fields = res_fields.ACTION_PLAN_SHORT_LIST_FIELDS
field_labels = res_fields.ACTION_PLAN_SHORT_LIST_FIELD_LABELS
@@ -255,8 +238,6 @@ class UpdateActionPlan(command.ShowOne):
columns = res_fields.ACTION_PLAN_FIELDS
column_headers = res_fields.ACTION_PLAN_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action_plan, columns)
@@ -283,8 +264,6 @@ class StartActionPlan(command.ShowOne):
columns = res_fields.ACTION_PLAN_FIELDS
column_headers = res_fields.ACTION_PLAN_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action_plan, columns)
@@ -334,7 +313,5 @@ class CancelActionPlan(command.ShowOne):
columns = res_fields.ACTION_PLAN_FIELDS
column_headers = res_fields.ACTION_PLAN_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action_plan, columns)

View File

@@ -13,29 +13,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
from osc_lib import utils
from oslo_utils import uuidutils
from watcherclient._i18n import _
from watcherclient.common import api_versioning
from watcherclient.common import command
from watcherclient.common import utils as common_utils
from watcherclient import exceptions
from watcherclient.v1 import resource_fields as res_fields
def drop_unsupported_field(app_args, fields, field_labels):
fields = copy.copy(fields)
field_labels = copy.copy(field_labels)
api_ver = app_args.os_infra_optim_api_version
if not api_versioning.action_update_supported(api_ver):
fields.remove('status_message')
field_labels.remove('Status Message')
return fields, field_labels
class ShowAction(command.ShowOne):
"""Show detailed information about a given action."""
@@ -58,8 +44,6 @@ class ShowAction(command.ShowOne):
columns = res_fields.ACTION_FIELDS
column_headers = res_fields.ACTION_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action, columns)
@@ -120,8 +104,6 @@ class ListAction(command.Lister):
if parsed_args.detail:
fields = res_fields.ACTION_FIELDS
field_labels = res_fields.ACTION_FIELD_LABELS
fields, field_labels = drop_unsupported_field(
self.app_args, fields, field_labels)
else:
fields = res_fields.ACTION_SHORT_LIST_FIELDS
field_labels = res_fields.ACTION_SHORT_LIST_FIELD_LABELS
@@ -137,67 +119,3 @@ class ListAction(command.Lister):
return (field_labels,
(utils.get_item_properties(item, fields) for item in data))
class UpdateAction(command.ShowOne):
"""Update action command."""
def get_parser(self, prog_name):
parser = super(UpdateAction, self).get_parser(prog_name)
parser.add_argument(
'action',
metavar='<action>',
help=_('UUID of the action'))
parser.add_argument(
'--state',
metavar='<state>',
help=_('New state for the action (e.g., SKIPPED)'))
parser.add_argument(
'--reason',
metavar='<reason>',
help=_('Reason for the state change'))
return parser
def take_action(self, parsed_args):
client = getattr(self.app.client_manager, "infra-optim")
# Check if action update is supported in the requested API version
api_ver = self.app_args.os_infra_optim_api_version
if not api_versioning.action_update_supported(api_ver):
raise exceptions.CommandError(
_("Action update is not supported in API version %s. "
"Minimum required version is 1.5.") % api_ver)
if not parsed_args.state and not parsed_args.reason:
raise exceptions.CommandError(
_("At least one field update is required for this operation"))
if not uuidutils.is_uuid_like(parsed_args.action):
raise exceptions.ValidationError()
patch = []
if parsed_args.state:
patch.append({
'op': 'replace',
'path': '/state',
'value': parsed_args.state
})
if parsed_args.reason:
patch.append({
'op': 'replace',
'path': '/status_message',
'value': parsed_args.reason
})
try:
action = client.action.update(parsed_args.action, patch)
except exceptions.HTTPNotFound as exc:
raise exceptions.CommandError(str(exc))
columns = res_fields.ACTION_FIELDS
column_headers = res_fields.ACTION_FIELD_LABELS
columns, column_headers = drop_unsupported_field(
self.app_args, columns, column_headers)
return column_headers, utils.get_item_properties(action, columns)

View File

@@ -38,9 +38,6 @@ def drop_unsupported_field(app_args, fields, field_labels):
if not api_versioning.launch_audit_forced(api_ver):
fields.remove('force')
field_labels.remove('Force')
if not api_versioning.action_update_supported(api_ver):
fields.remove('status_message')
field_labels.remove('Status Message')
return fields, field_labels
@@ -170,8 +167,8 @@ class CreateAudit(command.ShowOne):
dest='audit_type',
metavar='<audit_type>',
default='ONESHOT',
choices=['ONESHOT', 'CONTINUOUS', 'EVENT'],
help=_("Audit type. It must be ONESHOT, CONTINUOUS or EVENT. "
choices=['ONESHOT', 'CONTINUOUS'],
help=_("Audit type. It must be ONESHOT or CONTINUOUS. "
"Default is ONESHOT."))
parser.add_argument(
'-p', '--parameter',
@@ -185,7 +182,7 @@ class CreateAudit(command.ShowOne):
dest='interval',
metavar='<interval>',
help=_('Audit interval (in seconds or cron format). '
'Cron interval can be used like: ``*/5 * * * *``. '
'Cron inteval can be used like: "*/5 * * * *". '
'Only used if the audit is CONTINUOUS.'))
parser.add_argument(
'-g', '--goal',

View File

@@ -167,8 +167,8 @@ class CreateAuditTemplate(command.ShowOne):
metavar='<path>',
help=_("Part of the cluster on which an audit will be done.\n"
"Can be provided either in yaml or json file.\n"
"YAML example::\n"
"\n"
"YAML example:\n"
"---\n"
" - compute:\n"
" - host_aggregates:\n"
" - id: 1\n"
@@ -201,49 +201,48 @@ class CreateAuditTemplate(command.ShowOne):
" - uuid: UUID1\n"
" - uuid: UUID2\n"
"\n"
"JSON example::\n"
"\n"
" [\n"
" {\"compute\":\n"
" [{\"host_aggregates\": [\n"
" {\"id\": 1},\n"
" {\"id\": 2},\n"
" {\"id\": 3}]},\n"
" {\"availability_zones\": [\n"
" {\"name\": \"AZ1\"},\n"
" {\"name\": \"AZ2\"}]},\n"
" {\"exclude\": [\n"
" {\"instances\": [\n"
" {\"uuid\": \"UUID1\"},\n"
" {\"uuid\": \"UUID2\"}\n"
" ]},\n"
" {\"compute_nodes\": [\n"
" {\"name\": \"compute1\"}\n"
" ]}\n"
" ]}]\n"
" },\n"
" {\"storage\":\n"
" [{\"availability_zones\": [\n"
" {\"name\": \"AZ1\"},\n"
" {\"name\": \"AZ2\"}]},\n"
" {\"volume_types\": [\n"
" {\"name\": \"lvm1\"},\n"
" {\"name\": \"lvm2\"}]},\n"
" {\"exclude\": [\n"
" {\"storage_pools\": [\n"
" {\"name\": \"host0@backend0#pool0\"},\n"
" {\"name\": \"host1@backend1#pool1\"}\n"
" ]},\n"
" {\"volumes\": [\n"
" {\"uuid\": \"UUID1\"},\n"
" {\"uuid\": \"UUID2\"}\n"
" ]},\n"
" {\"projects\": [\n"
" {\"uuid\": \"UUID1\"},\n"
" {\"uuid\": \"UUID2\"}\n"
" ]},\n"
" ]}]\n"
" }\n"
"JSON example:\n"
"[\n"
" {\"compute\":\n"
" [{\"host_aggregates\": [\n"
" {\"id\": 1},\n"
" {\"id\": 2},\n"
" {\"id\": 3}]},\n"
" {\"availability_zones\": [\n"
" {\"name\": \"AZ1\"},\n"
" {\"name\": \"AZ2\"}]},\n"
" {\"exclude\": [\n"
" {\"instances\": [\n"
" {\"uuid\": \"UUID1\"},\n"
" {\"uuid\": \"UUID2\"}\n"
" ]},\n"
" {\"compute_nodes\": [\n"
" {\"name\": \"compute1\"}\n"
" ]}\n"
" ]}]\n"
" },\n"
" {\"storage\":\n"
" [{\"availability_zones\": [\n"
" {\"name\": \"AZ1\"},\n"
" {\"name\": \"AZ2\"}]},\n"
" {\"volume_types\": [\n"
" {\"name\": \"lvm1\"},\n"
" {\"name\": \"lvm2\"}]},\n"
" {\"exclude\": [\n"
" {\"storage_pools\": [\n"
" {\"name\": \"host0@backend0#pool0\"},\n"
" {\"name\": \"host1@backend1#pool1\"}\n"
" ]},\n"
" {\"volumes\": [\n"
" {\"uuid\": \"UUID1\"},\n"
" {\"uuid\": \"UUID2\"}\n"
" ]},\n"
" {\"projects\": [\n"
" {\"uuid\": \"UUID1\"},\n"
" {\"uuid\": \"UUID2\"}\n"
" ]},\n"
" ]}]\n"
" }\n"
" ]\n"
)
)

View File

@@ -14,9 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import io
from osc_lib import utils
import six
from watcherclient._i18n import _
from watcherclient.common import command
@@ -38,7 +37,7 @@ class ShowGoal(command.ShowOne):
return parser
def _format_indicator_spec_table(self, spec, parsed_args):
out = io.StringIO()
out = six.StringIO()
self.formatter.emit_one(
column_names=list(field.capitalize() for field in spec.keys()),
data=utils.get_dict_properties(spec, spec.keys()),
@@ -108,7 +107,7 @@ class ListGoal(command.Lister):
return parser
def _format_indicator_spec_table(self, goal, parsed_args):
out = io.StringIO()
out = six.StringIO()
efficacy_specification = goal.efficacy_specification
fields = ['name', 'unit']
self.formatter.emit_list(

14
watcherclient/v1/resource_fields.py Normal file → Executable file
View File

@@ -33,14 +33,13 @@ AUDIT_TEMPLATE_SHORT_LIST_FIELD_LABELS = ['UUID', 'Name', 'Goal', 'Strategy']
AUDIT_FIELDS = ['uuid', 'name', 'created_at', 'updated_at', 'deleted_at',
'state', 'audit_type', 'parameters', 'interval', 'goal_name',
'strategy_name', 'scope', 'auto_trigger', 'next_run_time',
'hostname', 'start_time', 'end_time', 'force',
'status_message']
'hostname', 'start_time', 'end_time', 'force']
AUDIT_FIELD_LABELS = ['UUID', 'Name', 'Created At', 'Updated At', 'Deleted At',
'State', 'Audit Type', 'Parameters', 'Interval', 'Goal',
'Strategy', 'Audit Scope', 'Auto Trigger',
'Next Run Time', 'Hostname', 'Start Time', 'End Time',
'Force', 'Status Message']
'Force']
AUDIT_SHORT_LIST_FIELDS = ['uuid', 'name', 'audit_type',
'state', 'goal_name', 'strategy_name',
@@ -52,13 +51,12 @@ AUDIT_SHORT_LIST_FIELD_LABELS = ['UUID', 'Name', 'Audit Type', 'State', 'Goal',
# Action Plan
ACTION_PLAN_FIELDS = ['uuid', 'created_at', 'updated_at', 'deleted_at',
'audit_uuid', 'strategy_name', 'state',
'efficacy_indicators', 'global_efficacy', 'hostname',
'status_message']
'efficacy_indicators', 'global_efficacy', 'hostname']
ACTION_PLAN_FIELD_LABELS = ['UUID', 'Created At', 'Updated At', 'Deleted At',
'Audit', 'Strategy', 'State',
'Efficacy indicators', 'Global efficacy',
'Hostname', 'Status Message']
'Hostname']
ACTION_PLAN_SHORT_LIST_FIELDS = ['uuid', 'audit_uuid', 'state',
'updated_at', 'global_efficacy']
@@ -71,11 +69,11 @@ GLOBAL_EFFICACY_FIELDS = ['value', 'unit', 'name', 'description']
# Action
ACTION_FIELDS = ['uuid', 'created_at', 'updated_at', 'deleted_at', 'parents',
'state', 'action_plan_uuid', 'action_type',
'input_parameters', 'description', 'status_message']
'input_parameters', 'description']
ACTION_FIELD_LABELS = ['UUID', 'Created At', 'Updated At', 'Deleted At',
'Parents', 'State', 'Action Plan', 'Action',
'Parameters', 'Description', 'Status Message']
'Parameters', 'Description']
ACTION_SHORT_LIST_FIELDS = ['uuid', 'parents',
'state', 'action_plan_uuid', 'action_type']

View File

@@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from urllib import parse
import six.moves.urllib.parse as parse
from watcherclient.common import base
from watcherclient.common import utils

View File

@@ -71,7 +71,7 @@ class StateStrategy(command.Lister):
def _format_spec(self, requirements):
for req in requirements:
if isinstance(req.state, list):
if type(req.state) == list:
req.state = jsonutils.dumps(req.state, indent=2)
return requirements