├── watcherclient ├── common │ ├── __init__.py │ ├── apiclient │ │ └── __init__.py │ ├── command.py │ └── base.py ├── osc │ ├── __init__.py │ └── plugin.py ├── tests │ ├── __init__.py │ ├── unit │ │ ├── __init__.py │ │ ├── common │ │ │ ├── __init__.py │ │ │ └── test_api_versioning.py │ │ ├── v1 │ │ │ ├── __init__.py │ │ │ ├── test_data_model.py │ │ │ ├── base.py │ │ │ ├── test_service_shell.py │ │ │ ├── test_data_model_shell.py │ │ │ ├── test_scoring_engine_shell.py │ │ │ ├── test_service.py │ │ │ ├── test_goal_shell.py │ │ │ ├── test_scoring_engine.py │ │ │ ├── test_goal.py │ │ │ └── test_strategy.py │ │ ├── test_import.py │ │ ├── keystone_client_fixtures.py │ │ ├── utils.py │ │ └── test_utils.py │ └── client_functional │ │ ├── __init__.py │ │ └── v1 │ │ ├── __init__.py │ │ ├── test_goal.py │ │ ├── test_scoring_engine.py │ │ ├── test_strategy.py │ │ ├── test_service.py │ │ ├── test_audit_template.py │ │ ├── test_action_plan.py │ │ └── base.py ├── version.py ├── _i18n.py ├── __init__.py ├── v1 │ ├── data_model.py │ ├── __init__.py │ ├── client.py │ ├── service.py │ ├── goal.py │ ├── scoring_engine.py │ ├── data_model_shell.py │ ├── action.py │ ├── strategy.py │ ├── service_shell.py │ ├── audit.py │ ├── audit_template.py │ ├── scoring_engine_shell.py │ ├── action_plan.py │ ├── goal_shell.py │ ├── strategy_shell.py │ └── resource_fields.py └── exceptions.py ├── .stestr.conf ├── .gitreview ├── pyproject.toml ├── HACKING.rst ├── .coveragerc ├── releasenotes └── notes │ ├── drop-py39-8a9c99678b3e8eeb.yaml │ ├── drop-py-2-7-f7078b44cf99cae1.yaml │ └── add-action-update-command-b8c74d5e6f234a21.yaml ├── test-requirements.txt ├── requirements.txt ├── doc ├── requirements.txt └── source │ ├── reference │ ├── index.rst │ └── api_v1.rst │ ├── installation.rst │ ├── index.rst │ ├── cli │ ├── index.rst │ ├── openstack_cli.rst │ └── watcher.rst │ ├── contributing.rst │ └── conf.py ├── CONTRIBUTING.rst ├── setup.py ├── .gitignore ├── tools └── watcher.bash_completion ├── .zuul.yaml ├── tox.ini ├── README.rst └── setup.cfg /watcherclient/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /watcherclient/osc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /watcherclient/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /watcherclient/common/apiclient/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /watcherclient/tests/client_functional/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /watcherclient/tests/client_functional/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=./watcherclient/tests/unit 3 | top_dir=./ 4 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.opendev.org 3 | port=29418 4 | project=openstack/python-watcherclient.git 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pbr>=6.0.0", "setuptools>=64.0.0"] 3 | build-backend = "pbr.build" 4 | -------------------------------------------------------------------------------- /HACKING.rst: -------------------------------------------------------------------------------- 1 | python-watcherclient Style Commandments 2 | ======================================= 3 | 4 | Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = watcherclient 4 | omit = 5 | watcherclient/tests/* 6 | 7 | [report] 8 | ignore_errors = True 9 | exclude_lines = 10 | @abc.abstract 11 | raise NotImplementedError 12 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-py39-8a9c99678b3e8eeb.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | watcher client now requires python 3.10 or newer. 5 | The last release to support ``3.9`` was ``2025.1``. 6 | Please ensure you have a supported python version before upgrading. 7 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage!=4.4,>=4.0 # Apache-2.0 2 | fixtures>=3.0.0 # Apache-2.0/BSD 3 | hacking>=7.0.0,<7.1.0 # Apache-2.0 4 | oslotest>=3.2.0 # Apache-2.0 5 | stestr>=2.0.0 # Apache-2.0 6 | testscenarios>=0.4 # Apache-2.0/BSD 7 | testtools>=2.2.0 # MIT 8 | tempest>=17.1.0 # Apache-2.0 9 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-py-2-7-f7078b44cf99cae1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Python 2.7 support has been dropped. Last release of python-watcherclient 5 | to support py2.7 is OpenStack Train. The minimum version of Python now 6 | supported by python-watcherclient is Python 3.6. 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cliff!=2.9.0,>=2.11.0 # Apache-2.0 2 | osc-lib>=1.10.0 # Apache-2.0 3 | oslo.i18n>=3.20.0 # Apache-2.0 4 | oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 5 | oslo.utils>=3.36.0 # Apache-2.0 6 | pbr!=2.1.0,>=3.1.1 # Apache-2.0 7 | keystoneauth1>=3.4.0 # Apache-2.0 8 | PyYAML>=3.13 # MIT 9 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | # The order of packages is significant, because pip processes them in the order 2 | # of appearance. Changing the order has an impact on the overall integration 3 | # process, which may cause wedges in the gate later. 4 | 5 | openstackdocstheme>=2.2.1 # Apache-2.0 6 | sphinx>=2.0.0,!=2.1.0 # BSD 7 | sphinxcontrib-apidoc>=0.2.0 # BSD 8 | 9 | -------------------------------------------------------------------------------- /doc/source/reference/index.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Python Library Reference 3 | ========================== 4 | 5 | In order to use the python api directly, you must first obtain an auth 6 | token and identify which endpoint you wish to speak to. Once you have 7 | done so, you can use the API like so. 8 | 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | api/modules 14 | api_v1 15 | -------------------------------------------------------------------------------- /doc/source/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | If you have `virtualenvwrapper `_ installed:: 6 | 7 | $ mkvirtualenv python-watcherclient 8 | $ git clone https://opendev.org/openstack/python-watcherclient 9 | $ cd python-watcherclient && python setup.py install 10 | $ pip install -r ./requirements.txt 11 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | Python bindings to the OpenStack Watcher API 2 | ============================================ 3 | 4 | This is a client for OpenStack Watcher API. There's a Python API 5 | (the :mod:`watcherclient` modules), and a command-line script 6 | (installed as :program:`watcher`). Each implements the entire 7 | OpenStack Watcher API. 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | cli/index 13 | reference/index 14 | installation 15 | contributing 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | If you would like to contribute to the development of OpenStack, 2 | you must follow the steps in this page: 3 | 4 | https://docs.openstack.org/infra/manual/developers.html 5 | 6 | Once those steps have been completed, changes to OpenStack 7 | should be submitted for review via the Gerrit tool, following 8 | the workflow documented at: 9 | 10 | https://docs.openstack.org/infra/manual/developers.html#development-workflow 11 | 12 | Pull requests submitted through GitHub will be ignored. 13 | 14 | Bugs should be filed on Launchpad, not GitHub: 15 | 16 | https://bugs.launchpad.net/python-watcherclient 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import setuptools 17 | 18 | setuptools.setup( 19 | setup_requires=['pbr>=2.0.0'], 20 | pbr=True) 21 | -------------------------------------------------------------------------------- /watcherclient/version.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 2 | # The Cloudscaling Group, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy 6 | # of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from pbr import version 17 | 18 | version_info = version.VersionInfo('python-watcherclient') 19 | __version__ = version_info.version_string() 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | .eggs 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage* 27 | .tox 28 | nosetests.xml 29 | .stestr/ 30 | .venv 31 | .testrepository/ 32 | 33 | # Translations 34 | *.mo 35 | 36 | # Mr Developer 37 | .mr.developer.cfg 38 | .project 39 | .pydevproject 40 | 41 | # Complexity 42 | output/*.html 43 | output/*/index.html 44 | 45 | # Sphinx 46 | doc/build 47 | doc/source/reference/api 48 | 49 | # pbr generates these 50 | AUTHORS 51 | ChangeLog 52 | 53 | # Editors 54 | *~ 55 | .*.swp 56 | .*sw? 57 | 58 | sftp-config.json 59 | /.idea/ 60 | /cover/ 61 | 62 | # Desktop Service Store 63 | *.DS_Store 64 | 65 | # Atom 66 | .remote-sync.json 67 | -------------------------------------------------------------------------------- /releasenotes/notes/add-action-update-command-b8c74d5e6f234a21.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | Added support for updating action state through the new 5 | ``openstack optimize action update`` command. This feature allows 6 | operators to manually change action states. The command 7 | supports the following options: 8 | 9 | * ``--state `` - New state for the action (required) 10 | * ``--reason `` - Optional reason for the state change 11 | 12 | Currently, the only use case for this update is to Skip an action 13 | before starting an Action Plan with an optional reason by setting 14 | the state to SKIPPED: 15 | 16 | $ openstack optimize action update --state SKIPPED --reason "Manual skip" 17 | 18 | This feature requires Watcher API microversion 1.5 or higher. 19 | 20 | upgrade: 21 | - | 22 | The maximum supported API version has been increased from 1.1 to 1.5 23 | to support the new action update functionality. This change maintains 24 | full backward compatibility with existing deployments. 25 | -------------------------------------------------------------------------------- /tools/watcher.bash_completion: -------------------------------------------------------------------------------- 1 | _watcher_opts="" # lazy init 2 | _watcher_flags="" # lazy init 3 | _watcher_opts_exp="" # lazy init 4 | _watcher() 5 | { 6 | local cur prev nbc cflags 7 | COMPREPLY=() 8 | cur="${COMP_WORDS[COMP_CWORD]}" 9 | prev="${COMP_WORDS[COMP_CWORD-1]}" 10 | 11 | if [ "x$_watcher_opts" == "x" ] ; then 12 | nbc="`watcher bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`" 13 | _watcher_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`" 14 | _watcher_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`" 15 | _watcher_opts_exp="`echo "$_watcher_opts" | tr ' ' '|'`" 16 | fi 17 | 18 | if [[ " ${COMP_WORDS[@]} " =~ " "($_watcher_opts_exp)" " && "$prev" != "help" ]] ; then 19 | COMPLETION_CACHE=$HOME/.cache/python-watcherclient/*/*-cache 20 | cflags="$_watcher_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') 21 | COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) 22 | else 23 | COMPREPLY=($(compgen -W "${_watcher_opts}" -- ${cur})) 24 | fi 25 | return 0 26 | } 27 | complete -F _watcher watcher 28 | -------------------------------------------------------------------------------- /.zuul.yaml: -------------------------------------------------------------------------------- 1 | - job: 2 | name: python-watcherclient-functional 3 | parent: devstack-tox-functional 4 | timeout: 7200 5 | required-projects: 6 | - openstack/watcher 7 | - openstack/python-watcherclient 8 | vars: 9 | # Run cross-project watcherclient functional tests on watcher repo. 10 | zuul_work_dir: src/opendev.org/openstack/python-watcherclient 11 | openrc_enable_export: true 12 | devstack_plugins: 13 | watcher: https://opendev.org/openstack/watcher 14 | devstack_services: 15 | watcher-api: true 16 | watcher-decision-engine: true 17 | watcher-applier: true 18 | s-account: false 19 | s-container: false 20 | s-object: false 21 | s-proxy: false 22 | irrelevant-files: 23 | - ^.*\.rst$ 24 | - ^doc/.*$ 25 | - ^releasenotes/.*$ 26 | 27 | 28 | - project: 29 | templates: 30 | - openstack-cover-jobs 31 | - openstack-python3-jobs 32 | - publish-openstack-docs-pti 33 | - check-requirements 34 | - openstackclient-plugin-jobs 35 | check: 36 | jobs: 37 | - python-watcherclient-functional 38 | -------------------------------------------------------------------------------- /doc/source/cli/index.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Command-line Tool Reference 3 | ============================= 4 | 5 | In order to use the CLI, you must provide your OpenStack username, 6 | password, tenant, and auth endpoint. Use the corresponding 7 | configuration options (``--os-username``, ``--os-password``, 8 | ``--os-tenant-id``, and ``--os-auth-url``) or set them in environment 9 | variables:: 10 | 11 | export OS_USERNAME=user 12 | export OS_PASSWORD=pass 13 | export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b 14 | export OS_AUTH_URL=http://auth.example.com:5000/v3/ 15 | 16 | The command line tool will attempt to reauthenticate using your 17 | provided credentials for every request. You can override this behavior 18 | by manually supplying an auth token using ``--os-watcher-url`` and 19 | ``--os-auth-token``. You can alternatively set these environment 20 | variables:: 21 | 22 | export OS_WATCHER_URL=http://watcher.example.org:9322/ 23 | export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 24 | 25 | Once you've configured your authentication parameters, you can run 26 | ``watcher help`` to see a complete listing of available commands. 27 | 28 | .. toctree:: 29 | 30 | watcher 31 | openstack_cli 32 | details 33 | -------------------------------------------------------------------------------- /watcherclient/_i18n.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Hewlett-Packard Development Company, L.P. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import oslo_i18n 17 | 18 | DOMAIN = "watcherclient" 19 | 20 | _translators = oslo_i18n.TranslatorFactory(domain=DOMAIN) 21 | 22 | # The primary translation function using the well-known name "_" 23 | _ = _translators.primary 24 | 25 | # The contextual translation function using the name "_C" 26 | # requires oslo.i18n >=2.1.0 27 | _C = _translators.contextual_form 28 | 29 | # The plural translation function using the name "_P" 30 | # requires oslo.i18n >=2.1.0 31 | _P = _translators.plural_form 32 | 33 | 34 | def get_available_languages(): 35 | return oslo_i18n.get_available_languages(DOMAIN) 36 | -------------------------------------------------------------------------------- /watcherclient/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2013 Hewlett-Packard Development Company, L.P. 3 | # All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import pbr.version 18 | 19 | from watcherclient import client 20 | from watcherclient.common import api_versioning 21 | from watcherclient import exceptions 22 | 23 | 24 | __version__ = pbr.version.VersionInfo( 25 | 'python-watcherclient').version_string() 26 | 27 | __all__ = ['client', 'exceptions', ] 28 | 29 | API_MIN_VERSION = api_versioning.APIVersion("1.0") 30 | # The max version should be the latest version that is supported in the client, 31 | # not necessarily the latest that the server can provide. This is only bumped 32 | # when client supported the max version, and bumped sequentially, otherwise 33 | # the client may break due to server side new version may include some 34 | # backward incompatible change. 35 | API_MAX_VERSION = api_versioning.APIVersion("1.5") 36 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/test_import.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | from watcherclient.tests.unit import utils 15 | 16 | module_str = 'watcherclient' 17 | 18 | 19 | class ImportTest(utils.BaseTestCase): 20 | 21 | def check_exported_symbols(self, exported_symbols): 22 | self.assertIn('client', exported_symbols) 23 | self.assertIn('exceptions', exported_symbols) 24 | 25 | def test_import_objects(self): 26 | module = __import__(module_str) 27 | exported_symbols = dir(module) 28 | self.check_exported_symbols(exported_symbols) 29 | 30 | def test_default_import(self): 31 | default_imports = __import__(module_str, globals(), locals(), ['*']) 32 | exported_symbols = dir(default_imports) 33 | self.check_exported_symbols(exported_symbols) 34 | 35 | def test_import__all__(self): 36 | module = __import__(module_str) 37 | self.check_exported_symbols(module.__all__) 38 | -------------------------------------------------------------------------------- /watcherclient/common/command.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 NEC Corporation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import abc 16 | import logging 17 | 18 | from cliff import command 19 | from cliff import lister 20 | from cliff import show 21 | 22 | 23 | class CommandMeta(abc.ABCMeta): 24 | 25 | def __new__(mcs, name, bases, cls_dict): 26 | if 'log' not in cls_dict: 27 | cls_dict['log'] = logging.getLogger( 28 | cls_dict['__module__'] + '.' + name) 29 | return super(CommandMeta, mcs).__new__(mcs, name, bases, cls_dict) 30 | 31 | 32 | class Command(command.Command, metaclass=CommandMeta): 33 | 34 | def run(self, parsed_args): 35 | self.log.debug('run(%s)', parsed_args) 36 | return super(Command, self).run(parsed_args) 37 | 38 | 39 | class Lister(Command, lister.Lister): 40 | pass 41 | 42 | 43 | class ShowOne(Command, show.ShowOne): 44 | def get_parser(self, prog_name, formatter_class=None): 45 | parser = super(ShowOne, self).get_parser(prog_name) 46 | if formatter_class: 47 | parser.formatter_class = formatter_class 48 | return parser 49 | -------------------------------------------------------------------------------- /watcherclient/tests/client_functional/v1/test_goal.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Servionica 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from watcherclient.tests.client_functional.v1 import base 17 | 18 | 19 | class GoalTests(base.TestCase): 20 | """Functional tests for goal.""" 21 | 22 | dummy_name = 'dummy' 23 | list_fields = ['UUID', 'Name', 'Display name'] 24 | 25 | def test_goal_list(self): 26 | raw_output = self.watcher('goal list') 27 | self.assertIn(self.dummy_name, raw_output) 28 | self.assert_table_structure([raw_output], self.list_fields) 29 | 30 | def test_goal_detailed_list(self): 31 | raw_output = self.watcher('goal list --detail') 32 | self.assertIn(self.dummy_name, raw_output) 33 | self.assert_table_structure( 34 | [raw_output], self.list_fields + ['Efficacy specification']) 35 | 36 | def test_goal_show(self): 37 | raw_output = self.watcher('goal show %s' % self.dummy_name) 38 | self.assertIn(self.dummy_name, raw_output) 39 | self.assert_table_structure( 40 | [raw_output], self.list_fields + ['Efficacy specification']) 41 | self.assertNotIn('server_consolidation', raw_output) 42 | -------------------------------------------------------------------------------- /watcherclient/tests/client_functional/v1/test_scoring_engine.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Servionica 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from watcherclient.tests.client_functional.v1 import base 17 | 18 | 19 | class ScoringEngineTests(base.TestCase): 20 | """Functional tests for scoring engine.""" 21 | 22 | dummy_name = 'dummy_scorer' 23 | list_fields = ['UUID', 'Name', 'Description'] 24 | detailed_list_fields = list_fields + ['Metainfo'] 25 | 26 | def test_scoringengine_list(self): 27 | raw_output = self.watcher('scoringengine list') 28 | self.assertIn(self.dummy_name, raw_output) 29 | self.assert_table_structure([raw_output], self.list_fields) 30 | 31 | def test_scoringengine_detailed_list(self): 32 | raw_output = self.watcher('scoringengine list --detail') 33 | self.assertIn(self.dummy_name, raw_output) 34 | self.assert_table_structure([raw_output], self.detailed_list_fields) 35 | 36 | def test_scoringengine_show(self): 37 | raw_output = self.watcher('scoringengine show %s' % self.dummy_name) 38 | self.assertIn(self.dummy_name, raw_output) 39 | self.assert_table_structure([raw_output], self.detailed_list_fields) 40 | self.assertNotIn('dummy_avg_scorer', raw_output) 41 | -------------------------------------------------------------------------------- /doc/source/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | ==================================== 4 | Contributing to python-watcherclient 5 | ==================================== 6 | 7 | If you're interested in contributing to the python-watcherclient project, 8 | the following will help get you started. 9 | 10 | 11 | Developer Certificate of Origin 12 | ------------------------------- 13 | 14 | .. index:: 15 | single: license; agreement 16 | 17 | In order to contribute to the python-watcherclient project, you need to adhere 18 | to the `Developer Certificate of Origin`_. OpenStack utilizes the Developer 19 | Certificate of Origin (DCO) as a lightweight means to confirm that you are 20 | entitled to contribute the code you submit. This ensures that you are 21 | providing your contributions under the project's license and that you have 22 | the right to do so. 23 | 24 | .. _Developer Certificate of Origin: https://developercertificate.org/ 25 | 26 | .. seealso:: 27 | 28 | * https://docs.openstack.org/contributors/common/dco.html 29 | 30 | LaunchPad Project 31 | ----------------- 32 | 33 | Most of the tools used for OpenStack depend on a launchpad.net ID for 34 | authentication. After signing up for a launchpad account, join the 35 | "openstack" team to have access to the mailing list and receive 36 | notifications of important events. 37 | 38 | .. seealso:: 39 | 40 | * http://launchpad.net 41 | * http://launchpad.net/python-watcherclient 42 | * http://launchpad.net/~openstack 43 | 44 | 45 | Project Hosting Details 46 | ------------------------- 47 | 48 | Bug tracker 49 | https://launchpad.net/python-watcherclient 50 | 51 | Mailing list (prefix subjects with ``[watcher]`` for faster responses) 52 | http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss 53 | 54 | Code Hosting 55 | https://opendev.org/openstack/python-watcherclient 56 | 57 | Code Review 58 | https://review.opendev.org/#/q/status:open+project:openstack/python-watcherclient,n,z 59 | 60 | -------------------------------------------------------------------------------- /watcherclient/v1/data_model.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 ZTE Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | from watcherclient.common import base 15 | from watcherclient.common import utils 16 | 17 | 18 | class DataModel(base.Resource): 19 | def __repr__(self): 20 | return "" % self._info 21 | 22 | 23 | class DataModelManager(base.Manager): 24 | resource_class = DataModel 25 | 26 | @staticmethod 27 | def _path(filters=None): 28 | if filters: 29 | path = '/v1/data_model/%s' % filters 30 | else: 31 | path = '/v1/data_model' 32 | return path 33 | 34 | def list(self, data_model_type='compute', audit=None): 35 | """Retrieve a list of data model. 36 | 37 | :param data_model_type: The type of data model user wants to list. 38 | Supported values: compute. 39 | Future support values: storage, baremetal. 40 | The default value is compute. 41 | :param audit: The UUID of the audit, used to filter data model 42 | by the scope in audit. 43 | 44 | :returns: A list of data model. 45 | 46 | """ 47 | path = '' 48 | filters = utils.common_filters() 49 | 50 | if audit: 51 | filters.append('audit_uuid=%s' % audit) 52 | filters.append('data_model_type=%s' % data_model_type) 53 | 54 | path += '?' + '&'.join(filters) 55 | 56 | return self._list(self._path(path))[0] 57 | -------------------------------------------------------------------------------- /watcherclient/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 b<>com 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from watcherclient.v1 import action 17 | from watcherclient.v1 import action_plan 18 | from watcherclient.v1 import audit 19 | from watcherclient.v1 import audit_template 20 | from watcherclient.v1 import data_model 21 | from watcherclient.v1 import goal 22 | from watcherclient.v1 import scoring_engine 23 | from watcherclient.v1 import service 24 | from watcherclient.v1 import strategy 25 | 26 | Action = action.Action 27 | ActionManager = action.ActionManager 28 | ActionPlan = action_plan.ActionPlan 29 | ActionPlanManager = action_plan.ActionPlanManager 30 | Audit = audit.Audit 31 | AuditManager = audit.AuditManager 32 | AuditTemplate = audit_template.AuditTemplate 33 | AuditTemplateManager = audit_template.AuditTemplateManager 34 | Goal = goal.Goal 35 | GoalManager = goal.GoalManager 36 | ScoringEngine = scoring_engine.ScoringEngine 37 | ScoringEngineManager = scoring_engine.ScoringEngineManager 38 | Service = service.Service 39 | ServiceManager = service.ServiceManager 40 | Strategy = strategy.Strategy 41 | StrategyManager = strategy.StrategyManager 42 | DataModel = data_model.DataModel 43 | DataModelManager = data_model.DataModelManager 44 | 45 | __all__ = ( 46 | "Action", "ActionManager", "ActionPlan", "ActionPlanManager", 47 | "Audit", "AuditManager", "AuditTemplate", "AuditTemplateManager", 48 | "Goal", "GoalManager", "ScoringEngine", "ScoringEngineManager", 49 | "Service", "ServiceManager", "Strategy", "StrategyManager", 50 | "DataModel", "DataModelManager") 51 | -------------------------------------------------------------------------------- /watcherclient/tests/client_functional/v1/test_strategy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Servionica 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from watcherclient.tests.client_functional.v1 import base 17 | 18 | 19 | class StrategyTests(base.TestCase): 20 | """Functional tests for strategy.""" 21 | 22 | dummy_name = 'dummy' 23 | basic_strategy = 'basic' 24 | list_fields = ['UUID', 'Name', 'Display name', 'Goal'] 25 | state_fields = ['Datasource', 'Metrics', 'CDM', 'Name'] 26 | 27 | def test_strategy_list(self): 28 | raw_output = self.watcher('strategy list') 29 | self.assertIn(self.dummy_name, raw_output) 30 | self.assert_table_structure([raw_output], self.list_fields) 31 | 32 | def test_strategy_detailed_list(self): 33 | raw_output = self.watcher('strategy list --detail') 34 | self.assertIn(self.dummy_name, raw_output) 35 | self.assert_table_structure([raw_output], 36 | self.list_fields + ['Parameters spec']) 37 | 38 | def test_strategy_show(self): 39 | raw_output = self.watcher('strategy show %s' % self.dummy_name) 40 | self.assertIn(self.dummy_name, raw_output) 41 | self.assert_table_structure([raw_output], 42 | self.list_fields + ['Parameters spec']) 43 | self.assertNotIn('basic', raw_output) 44 | 45 | def test_strategy_state(self): 46 | raw_output = self.watcher('strategy state %s' % self.basic_strategy) 47 | self.assertIn(self.basic_strategy, raw_output) 48 | self.assert_table_structure([raw_output], self.state_fields) 49 | -------------------------------------------------------------------------------- /watcherclient/tests/client_functional/v1/test_service.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Servionica 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from watcherclient.tests.client_functional.v1 import base 17 | 18 | 19 | class ServiceTests(base.TestCase): 20 | """Functional tests for service.""" 21 | 22 | decision_engine_name = 'watcher-decision-engine' 23 | applier_name = 'watcher-applier' 24 | list_fields = ['ID', 'Name', 'Host', 'Status'] 25 | 26 | def test_service_list(self): 27 | raw_output = self.watcher('service list') 28 | self.assertIn(self.decision_engine_name, raw_output) 29 | self.assertIn(self.applier_name, raw_output) 30 | self.assert_table_structure([raw_output], self.list_fields) 31 | 32 | def test_service_detailed_list(self): 33 | raw_output = self.watcher('service list --detail') 34 | self.assertIn(self.decision_engine_name, raw_output) 35 | self.assertIn(self.applier_name, raw_output) 36 | self.assert_table_structure([raw_output], 37 | self.list_fields + ['Last seen up']) 38 | 39 | def test_service_show(self): 40 | # TODO(alexchadin): this method should be refactored since Watcher will 41 | # get HA support soon. 42 | raw_output = self.watcher('service show %s' 43 | % self.decision_engine_name) 44 | self.assertIn(self.decision_engine_name, raw_output) 45 | self.assert_table_structure([raw_output], 46 | self.list_fields + ['Last seen up']) 47 | self.assertNotIn(self.applier_name, raw_output) 48 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.18.0 3 | envlist = py3,pep8 4 | 5 | [testenv] 6 | usedevelop = True 7 | passenv = ZUUL_CACHE_DIR 8 | REQUIREMENTS_PIP_LOCATION 9 | install_command = pip install {opts} {packages} 10 | deps = 11 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 12 | -r{toxinidir}/test-requirements.txt 13 | -r{toxinidir}/requirements.txt 14 | allowlist_externals = 15 | rm 16 | commands = rm -f .testrepository/times.dbm 17 | # The --test-path is defined in .stestr.conf 18 | stestr run {posargs} 19 | stestr slowest 20 | 21 | [testenv:pep8] 22 | basepython = python3 23 | commands = flake8 24 | 25 | [testenv:venv] 26 | basepython = python3 27 | commands = {posargs} 28 | 29 | [testenv:cover] 30 | basepython = python3 31 | setenv = 32 | PYTHON=coverage run --source watcherclient --parallel-mode 33 | commands = 34 | stestr run {posargs} 35 | coverage combine 36 | coverage html -d cover 37 | coverage xml -o cover/coverage.xml 38 | coverage report 39 | 40 | [testenv:docs] 41 | basepython = python3 42 | deps = 43 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 44 | -r{toxinidir}/doc/requirements.txt 45 | commands = sphinx-build -W -b html doc/source doc/build/html 46 | 47 | 48 | [testenv:pdf-docs] 49 | basepython = python3 50 | deps = {[testenv:docs]deps} 51 | allowlist_externals = 52 | rm 53 | make 54 | commands = 55 | rm -rf doc/build/pdf 56 | sphinx-build -W -b latex doc/source doc/build/pdf 57 | make -C doc/build/pdf 58 | 59 | [testenv:debug] 60 | basepython = python3 61 | commands = oslo_debug_helper -t watcherclient/tests/unit {posargs} 62 | 63 | [flake8] 64 | # E123, E125 skipped as they are invalid PEP-8. 65 | show-source = True 66 | enable-extensions = H203,H106 67 | ignore = E123,E125,W504 68 | builtins = _ 69 | exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build 70 | 71 | [testenv:wheel] 72 | basepython = python3 73 | commands = python setup.py bdist_wheel 74 | 75 | [hacking] 76 | import_exceptions = watcherclient._i18n 77 | 78 | [testenv:functional] 79 | passenv = OS_* 80 | commands = 81 | stestr --test-path=./watcherclient/tests/client_functional/ run --concurrency=1 {posargs} 82 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/v1/test_data_model.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 ZTE corporation. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | import testtools 16 | 17 | from watcherclient.tests.unit import utils 18 | import watcherclient.v1.data_model 19 | 20 | DATA_MODEL = { 21 | 'context': [{ 22 | "server_uuid": "1bf91464-9b41-428d-a11e-af691e5563bb", 23 | "server_name": "fake-name", 24 | "server_state": "active", 25 | "node_uuid": "253e5dd0-9384-41ab-af13-4f2c2ce26112", 26 | "node_hostname": "localhost.localdomain", 27 | }] 28 | } 29 | 30 | AUDIT = "81332bfc-36f8-444d-99e2-b7285d602528" 31 | 32 | fake_responses = { 33 | '/v1/data_model/?data_model_type=compute': 34 | { 35 | 'GET': ( 36 | {}, 37 | DATA_MODEL, 38 | ), 39 | }, 40 | '/v1/data_model/?audit_uuid=%s&data_model_type=compute' % AUDIT: 41 | { 42 | 'GET': ( 43 | {}, 44 | DATA_MODEL, 45 | ), 46 | }, 47 | } 48 | 49 | 50 | class DataModelManagerTest(testtools.TestCase): 51 | 52 | def setUp(self): 53 | super(DataModelManagerTest, self).setUp() 54 | self.api = utils.FakeAPI(fake_responses) 55 | self.mgr = watcherclient.v1.data_model.DataModelManager(self.api) 56 | 57 | def test_data_model_list(self): 58 | data_model = self.mgr.list() 59 | expect = [ 60 | ('GET', '/v1/data_model/?data_model_type=compute', {}, None), 61 | ] 62 | self.assertEqual(expect, self.api.calls) 63 | self.assertEqual(1, len(data_model.context)) 64 | 65 | def test_data_model_list_audit(self): 66 | data_model = self.mgr.list( 67 | audit='%s' % AUDIT) 68 | expect = [ 69 | ('GET', '/v1/data_model/?' 70 | 'audit_uuid=81332bfc-36f8-444d-99e2-b7285d602528' 71 | '&data_model_type=compute', 72 | {}, None), 73 | ] 74 | self.assertEqual(expect, self.api.calls) 75 | self.assertEqual(1, len(data_model.context)) 76 | -------------------------------------------------------------------------------- /doc/source/cli/openstack_cli.rst: -------------------------------------------------------------------------------- 1 | ===================================================================== 2 | :program:`openstack` Command-Line Interface (CLI) with Watcher plugin 3 | ===================================================================== 4 | 5 | .. program:: openstack 6 | .. highlight:: bash 7 | 8 | SYNOPSIS 9 | ======== 10 | 11 | :program:`openstack` [options] :program:`optimize` [command-options] 12 | 13 | :program:`openstack help optimize` 14 | 15 | :program:`openstack help optimize` 16 | 17 | 18 | DESCRIPTION 19 | =========== 20 | 21 | The :program:`openstack` command-line interface (CLI) can interact with the 22 | OpenStack infra-optim Service (Watcher), by using our additional plugin 23 | (included into the python-watcherclient package). 24 | 25 | In order to use the CLI, you must provide your OpenStack username, password, 26 | project (historically called tenant), and auth endpoint. You can use 27 | configuration options :option:``--os-username``, :option:``--os-password``, 28 | :option:``--os-tenant-id`` (or :option:``--os-tenant-name``), 29 | and :option:``--os-auth-url``, or set the corresponding 30 | environment variables:: 31 | 32 | $ export OS_USERNAME=user 33 | $ export OS_PASSWORD=password 34 | $ export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b # or OS_TENANT_NAME 35 | $ export OS_TENANT_NAME=project # or OS_TENANT_ID 36 | $ export OS_AUTH_URL=http://auth.example.com:5000/v3/ 37 | 38 | The command-line tool will attempt to reauthenticate using the provided 39 | credentials for every request. You can override this behavior by manually 40 | supplying an auth token using :option:``--watcher-url`` and 41 | :option:``--os-auth-token``, or by setting the corresponding environment variables:: 42 | 43 | export WATCHER_URL=http://watcher.example.org:9322/ 44 | export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 45 | 46 | Since Keystone can return multiple regions in the Service Catalog, you can 47 | specify the one you want with :option:``--os-region-name`` or set the following 48 | environment variable. (It defaults to the first in the list returned.) 49 | :: 50 | 51 | $ export OS_REGION_NAME=region 52 | 53 | OPTIONS 54 | ======= 55 | 56 | To get a list of available (sub)commands and options, run:: 57 | 58 | $ openstack help optimize 59 | 60 | To get usage and options of a command, run:: 61 | 62 | $ openstack help optimize 63 | 64 | 65 | EXAMPLES 66 | ======== 67 | 68 | Get information about the audit-create command:: 69 | 70 | $ openstack help optimize audit create 71 | 72 | 73 | Get a list of available goal:: 74 | 75 | $ openstack optimize goal list 76 | 77 | 78 | Get a list of audits:: 79 | 80 | $ openstack optimize audit list 81 | 82 | -------------------------------------------------------------------------------- /watcherclient/v1/client.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 OpenStack LLC. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from watcherclient._i18n import _ 17 | from watcherclient.common import httpclient 18 | from watcherclient import exceptions 19 | from watcherclient import v1 20 | 21 | 22 | class Client(object): 23 | """Client for the Watcher v1 API. 24 | 25 | :param string endpoint: A user-supplied endpoint URL for the watcher 26 | service. 27 | :param function token: Provides token for authentication. 28 | :param integer timeout: Allows customization of the timeout for client 29 | http requests. (optional) 30 | """ 31 | 32 | def __init__(self, endpoint=None, *args, **kwargs): 33 | """Initialize a new client for the Watcher v1 API.""" 34 | if kwargs.get('os_infra_optim_api_version'): 35 | kwargs['api_version_select_state'] = "user" 36 | else: 37 | if not endpoint: 38 | raise exceptions.EndpointException( 39 | _("Must provide 'endpoint' if os_infra_optim_api_version " 40 | "isn't specified")) 41 | 42 | # If the user didn't specify a version, use a cached version if 43 | # one has been stored 44 | host, netport = httpclient.get_server(endpoint) 45 | kwargs['api_version_select_state'] = "default" 46 | kwargs['os_infra_optim_api_version'] = httpclient.DEFAULT_VER 47 | 48 | self.http_client = httpclient._construct_http_client( 49 | endpoint, *args, **kwargs) 50 | 51 | self.audit = v1.AuditManager(self.http_client) 52 | self.audit_template = v1.AuditTemplateManager(self.http_client) 53 | self.action = v1.ActionManager(self.http_client) 54 | self.action_plan = v1.ActionPlanManager(self.http_client) 55 | self.goal = v1.GoalManager(self.http_client) 56 | self.scoring_engine = v1.ScoringEngineManager(self.http_client) 57 | self.service = v1.ServiceManager(self.http_client) 58 | self.strategy = v1.StrategyManager(self.http_client) 59 | self.data_model = v1.DataModelManager(self.http_client) 60 | -------------------------------------------------------------------------------- /watcherclient/v1/service.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2016 Servionica 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from watcherclient.common import base 17 | from watcherclient.common import utils 18 | 19 | 20 | class Service(base.Resource): 21 | def __repr__(self): 22 | return "" % self._info 23 | 24 | 25 | class ServiceManager(base.Manager): 26 | resource_class = Service 27 | 28 | @staticmethod 29 | def _path(service=None): 30 | return ('/v1/services/%s' % service 31 | if service else '/v1/services') 32 | 33 | def list(self, limit=None, sort_key=None, sort_dir=None, detail=False): 34 | """Retrieve a list of services. 35 | 36 | :param limit: The maximum number of results to return per 37 | request, if: 38 | 39 | 1) limit > 0, the maximum number of services to return. 40 | 2) limit == 0, return the entire list of services. 41 | 3) limit param is NOT specified (None), the number of items 42 | returned respect the maximum imposed by the Watcher API 43 | (see Watcher's api.max_limit option). 44 | 45 | :param sort_key: Optional, field used for sorting. 46 | 47 | :param sort_dir: Optional, direction of sorting, either 'asc' (the 48 | default) or 'desc'. 49 | 50 | :param detail: Optional, boolean whether to return detailed information 51 | about services. 52 | 53 | :returns: A list of services. 54 | 55 | """ 56 | if limit is not None: 57 | limit = int(limit) 58 | 59 | filters = utils.common_filters(limit, sort_key, sort_dir) 60 | 61 | path = '' 62 | if detail: 63 | path += 'detail' 64 | if filters: 65 | path += '?' + '&'.join(filters) 66 | 67 | if limit is None: 68 | return self._list(self._path(path), "services") 69 | else: 70 | return self._list_pagination(self._path(path), "services", 71 | limit=limit) 72 | 73 | def get(self, service): 74 | try: 75 | return self._list(self._path(service))[0] 76 | except IndexError: 77 | return None 78 | -------------------------------------------------------------------------------- /watcherclient/v1/goal.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2013 Red Hat, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from watcherclient.common import base 17 | from watcherclient.common import utils 18 | 19 | 20 | class Goal(base.Resource): 21 | def __repr__(self): 22 | return "" % self._info 23 | 24 | 25 | class GoalManager(base.Manager): 26 | resource_class = Goal 27 | 28 | @staticmethod 29 | def _path(goal=None): 30 | return '/v1/goals/%s' % goal if goal else '/v1/goals' 31 | 32 | def list(self, limit=None, sort_key=None, sort_dir=None, detail=False, 33 | marker=None): 34 | """Retrieve a list of goal. 35 | 36 | :param limit: The maximum number of results to return per 37 | request, if: 38 | 39 | 1) limit > 0, the maximum number of audits to return. 40 | 2) limit == 0, return the entire list of audits. 41 | 3) limit param is NOT specified (None), the number of items 42 | returned respect the maximum imposed by the Watcher API 43 | (see Watcher's api.max_limit option). 44 | 45 | :param sort_key: Optional, field used for sorting. 46 | 47 | :param sort_dir: Optional, direction of sorting, either 'asc' (the 48 | default) or 'desc'. 49 | 50 | :param detail: Optional, boolean whether to return detailed information 51 | about audits. 52 | 53 | :param marker: Optional, UUID of the last goal in the previous page. 54 | 55 | :returns: A list of goals. 56 | 57 | """ 58 | if limit is not None: 59 | limit = int(limit) 60 | 61 | filters = utils.common_filters(limit, sort_key, sort_dir, marker) 62 | path = '' 63 | if detail: 64 | path += 'detail' 65 | if filters: 66 | path += '?' + '&'.join(filters) 67 | 68 | if limit is None: 69 | return self._list(self._path(path), "goals") 70 | else: 71 | return self._list_pagination(self._path(path), "goals", 72 | limit=limit) 73 | 74 | def get(self, goal): 75 | try: 76 | return self._list(self._path(goal))[0] 77 | except IndexError: 78 | return None 79 | -------------------------------------------------------------------------------- /watcherclient/v1/scoring_engine.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2016 Intel 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from watcherclient.common import base 17 | from watcherclient.common import utils 18 | 19 | 20 | class ScoringEngine(base.Resource): 21 | def __repr__(self): 22 | return "" % self._info 23 | 24 | 25 | class ScoringEngineManager(base.Manager): 26 | resource_class = ScoringEngine 27 | 28 | @staticmethod 29 | def _path(scoring_engine=None): 30 | return ('/v1/scoring_engines/%s' % scoring_engine 31 | if scoring_engine else '/v1/scoring_engines') 32 | 33 | def list(self, limit=None, sort_key=None, sort_dir=None, detail=False): 34 | """Retrieve a list of scoring engines. 35 | 36 | :param limit: The maximum number of results to return per 37 | request, if: 38 | 39 | 1) limit > 0, the maximum number of scoring engines to return. 40 | 2) limit == 0, return the entire list of scoring engines. 41 | 3) limit param is NOT specified (None), the number of items 42 | returned respect the maximum imposed by the Watcher API 43 | (see Watcher's api.max_limit option). 44 | 45 | :param sort_key: Optional, field used for sorting. 46 | 47 | :param sort_dir: Optional, direction of sorting, either 'asc' (the 48 | default) or 'desc'. 49 | 50 | :param detail: Optional, boolean whether to return detailed information 51 | about scoring engines. 52 | 53 | :returns: A list of scoring engines. 54 | 55 | """ 56 | if limit is not None: 57 | limit = int(limit) 58 | 59 | filters = utils.common_filters(limit, sort_key, sort_dir) 60 | 61 | path = '' 62 | if detail: 63 | path += 'detail' 64 | if filters: 65 | path += '?' + '&'.join(filters) 66 | 67 | if limit is None: 68 | return self._list(self._path(path), "scoring_engines") 69 | else: 70 | return self._list_pagination(self._path(path), "scoring_engines", 71 | limit=limit) 72 | 73 | def get(self, scoring_engine_name): 74 | try: 75 | return self._list(self._path(scoring_engine_name))[0] 76 | except IndexError: 77 | return None 78 | -------------------------------------------------------------------------------- /doc/source/cli/watcher.rst: -------------------------------------------------------------------------------- 1 | =============================================== 2 | :program:`watcher` Command-Line Interface (CLI) 3 | =============================================== 4 | 5 | .. program:: watcher 6 | .. highlight:: bash 7 | 8 | SYNOPSIS 9 | ======== 10 | 11 | :program:`watcher` [options] [command-options] 12 | 13 | :program:`watcher help` 14 | 15 | :program:`watcher help` 16 | 17 | 18 | DESCRIPTION 19 | =========== 20 | 21 | The :program:`watcher` command-line interface (CLI) interacts with the 22 | OpenStack infra-optim Service (Watcher). 23 | 24 | In order to use the CLI, you must provide your OpenStack username, password, 25 | project (historically called tenant), and auth endpoint. You can use 26 | configuration options :option:``--os-username``, :option:``--os-password``, 27 | :option:``--os-tenant-id`` (or :option:``--os-tenant-name``), 28 | and :option:``--os-auth-url``, or set the corresponding 29 | environment variables:: 30 | 31 | $ export OS_USERNAME=user 32 | $ export OS_PASSWORD=password 33 | $ export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b # or OS_TENANT_NAME 34 | $ export OS_TENANT_NAME=project # or OS_TENANT_ID 35 | $ export OS_AUTH_URL=http://auth.example.com:5000/v3/ 36 | 37 | The command-line tool will attempt to reauthenticate using the provided 38 | credentials for every request. You can override this behavior by manually 39 | supplying an auth token using :option:``--watcher-url`` and 40 | :option:``--os-auth-token``, or by setting the corresponding environment variables:: 41 | 42 | $ export WATCHER_URL=http://watcher.example.org:9322/ 43 | $ export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 44 | 45 | Since Keystone can return multiple regions in the Service Catalog, you can 46 | specify the one you want with :option:``--os-region-name`` or set the following 47 | environment variable. (It defaults to the first in the list returned.) 48 | :: 49 | 50 | $ export OS_REGION_NAME=region 51 | 52 | Watcher CLI supports bash completion. The command-line tool can automatically 53 | fill partially typed commands. To use this feature, source the below file 54 | (available at 55 | https://opendev.org/openstack/python-watcherclient/src/branch/master/tools/watcher.bash_completion) 56 | to your terminal and then bash completion should work:: 57 | 58 | $ . watcher.bash_completion 59 | 60 | To avoid doing this every time, add this to your ``.bashrc`` or copy the 61 | watcher.bash_completion file to the default bash completion scripts directory 62 | on your linux distribution. 63 | 64 | OPTIONS 65 | ======= 66 | 67 | To get a list of available (sub)commands and options, run:: 68 | 69 | $ watcher help 70 | 71 | To get usage and options of a command, run:: 72 | 73 | $ watcher help 74 | 75 | 76 | EXAMPLES 77 | ======== 78 | 79 | Get information about the audit-create command:: 80 | 81 | $ watcher help audit create 82 | 83 | Get a list of available goal:: 84 | 85 | $ watcher goal list 86 | 87 | Get a list of audits:: 88 | 89 | $ watcher audit list 90 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/v1/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 b<>com 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import shlex 17 | from unittest import mock 18 | 19 | from osc_lib import utils as oscutils 20 | from oslo_serialization import jsonutils 21 | 22 | from watcherclient.common import httpclient 23 | from watcherclient.tests.unit import utils 24 | 25 | 26 | class CommandTestCase(utils.BaseTestCase): 27 | 28 | def setUp(self, os_infra_optim_api_version='1.0'): 29 | super(CommandTestCase, self).setUp() 30 | 31 | self.fake_env = { 32 | 'debug': False, 33 | 'insecure': False, 34 | 'no_auth': False, 35 | 'os_auth_token': '', 36 | 'os_auth_url': 'http://127.0.0.1:5000/v2.0', 37 | 'os_endpoint_override': 'http://watcher-endpoint:9322', 38 | 'os_username': 'test', 39 | 'os_password': 'test', 40 | 'timeout': 600, 41 | 'os_infra_optim_api_version': os_infra_optim_api_version} 42 | self.m_env = mock.Mock( 43 | name='m_env', 44 | side_effect=lambda x, *args, **kwargs: self.fake_env.get( 45 | x.lower(), kwargs.get('default', ''))) 46 | self.p_env = mock.patch.object(oscutils, 'env', self.m_env) 47 | self.p_env.start() 48 | self.addCleanup(self.p_env.stop) 49 | 50 | self.p_construct_http_client = mock.patch.object( 51 | httpclient, '_construct_http_client') 52 | self.m_construct_http_client = self.p_construct_http_client.start() 53 | self.addCleanup(self.p_construct_http_client.stop) 54 | 55 | def run_cmd(self, cmd, formatting='json'): 56 | if formatting and formatting != 'table': 57 | formatter_arg = " -f %s" % formatting 58 | formatter = jsonutils.loads 59 | else: 60 | formatter_arg = '' 61 | formatter = str 62 | formatted_cmd = "%(cmd)s%(formatter)s" % dict( 63 | cmd=cmd, formatter=formatter_arg) 64 | 65 | exit_code = self.cmd.run(shlex.split(formatted_cmd)) 66 | 67 | try: 68 | raw_data = self.stdout.getvalue() 69 | formatted_output = formatter(self.stdout.getvalue()) 70 | except Exception: 71 | self.fail("Formatting error (`%s` -> '%s')" % 72 | (raw_data, formatting)) 73 | return exit_code, formatted_output 74 | 75 | def resource_as_dict(self, resource, columns=(), column_headers=()): 76 | mapping = dict(zip(columns, column_headers)) 77 | return {mapping[k]: v for k, v in resource.to_dict().items() 78 | if not columns or columns and k in mapping} 79 | -------------------------------------------------------------------------------- /watcherclient/v1/data_model_shell.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 ZTE Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | from watcherclient._i18n import _ 16 | from watcherclient.common import command 17 | from watcherclient import exceptions 18 | from watcherclient.v1 import resource_fields as res_fields 19 | 20 | 21 | class ListDataModel(command.Lister): 22 | """List information on retrieved data model.""" 23 | 24 | def get_parser(self, prog_name): 25 | parser = super(ListDataModel, self).get_parser(prog_name) 26 | parser.add_argument( 27 | '--type', 28 | metavar='', 29 | dest='type', 30 | help=_('Type of Datamodel user want to list. ' 31 | 'Supported values: compute. ' 32 | 'Future support values: storage, baremetal. ' 33 | 'Default type is compute.')) 34 | parser.add_argument( 35 | '--audit', 36 | metavar='', 37 | dest='audit', 38 | help=_('UUID of the audit')) 39 | parser.add_argument( 40 | '--detail', 41 | dest='detail', 42 | action='store_true', 43 | default=False, 44 | help=_("Show detailed information about data model.")) 45 | return parser 46 | 47 | def get_tuple(self, dic, fields): 48 | ret_tup = [] 49 | for item in fields: 50 | ret_tup.append(dic.get(item)) 51 | return tuple(ret_tup) 52 | 53 | def take_action(self, parsed_args): 54 | client = getattr(self.app.client_manager, "infra-optim") 55 | allowed_type = ['compute', 'storage', 'baremetal'] 56 | params = {} 57 | if parsed_args.audit: 58 | params["audit"] = parsed_args.audit 59 | if parsed_args.type: 60 | if parsed_args.type not in allowed_type: 61 | raise exceptions.CommandError( 62 | 'Type %s error, ' 63 | 'Please check the valid type!' % parsed_args.type) 64 | params["data_model_type"] = parsed_args.type 65 | try: 66 | data_model = client.data_model.list(**params) 67 | except exceptions.HTTPNotFound as exc: 68 | raise exceptions.CommandError(str(exc)) 69 | # TODO(chenker) Add Storage MODEL_FIELDS when using Storage Datamodel. 70 | if parsed_args.detail: 71 | fields = res_fields.COMPUTE_MODEL_LIST_FIELDS 72 | field_labels = res_fields.COMPUTE_MODEL_LIST_FIELD_LABELS 73 | else: 74 | fields = res_fields.COMPUTE_MODEL_SHORT_LIST_FIELDS 75 | field_labels = res_fields.COMPUTE_MODEL_SHORT_LIST_FIELD_LABELS 76 | return (field_labels, 77 | (self.get_tuple(item, fields) for item in data_model.context)) 78 | -------------------------------------------------------------------------------- /watcherclient/v1/action.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2013 Red Hat, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from watcherclient.common import base 17 | from watcherclient.common import utils 18 | 19 | 20 | class Action(base.Resource): 21 | def __repr__(self): 22 | return "" % self._info 23 | 24 | 25 | class ActionManager(base.Manager): 26 | resource_class = Action 27 | 28 | @staticmethod 29 | def _path(id=None): 30 | return '/v1/actions/%s' % id if id else '/v1/actions' 31 | 32 | def list(self, action_plan=None, audit=None, limit=None, sort_key=None, 33 | sort_dir=None, detail=False, marker=None): 34 | """Retrieve a list of action. 35 | 36 | :param action_plan: UUID of the action plan 37 | :param audit: UUID of the audit 38 | :param limit: The maximum number of results to return per 39 | request, if: 40 | 41 | 1) limit > 0, the maximum number of actions to return. 42 | 2) limit == 0, return the entire list of actions. 43 | 3) limit param is NOT specified (None), the number of items 44 | returned respect the maximum imposed by the Watcher API 45 | (see Watcher's api.max_limit option). 46 | 47 | :param sort_key: Optional, field used for sorting. 48 | 49 | :param sort_dir: Optional, direction of sorting, either 'asc' (the 50 | default) or 'desc'. 51 | 52 | :param detail: Optional, boolean whether to return detailed information 53 | about actions. 54 | 55 | :param marker: Optional, UUID of the last action in the previous page. 56 | 57 | :returns: A list of actions. 58 | 59 | """ 60 | if limit is not None: 61 | limit = int(limit) 62 | 63 | filters = utils.common_filters(limit, sort_key, sort_dir, marker) 64 | if action_plan is not None: 65 | filters.append('action_plan_uuid=%s' % action_plan) 66 | if audit is not None: 67 | filters.append('audit_uuid=%s' % audit) 68 | 69 | path = '' 70 | if detail: 71 | path += 'detail' 72 | if filters: 73 | path += '?' + '&'.join(filters) 74 | 75 | if limit is None: 76 | return self._list(self._path(path), "actions") 77 | else: 78 | return self._list_pagination(self._path(path), "actions", 79 | limit=limit) 80 | 81 | def get(self, action_id): 82 | try: 83 | return self._list(self._path(action_id))[0] 84 | except IndexError: 85 | return None 86 | 87 | def update(self, action_id, patch): 88 | return self._update(self._path(action_id), patch) 89 | -------------------------------------------------------------------------------- /doc/source/reference/api_v1.rst: -------------------------------------------------------------------------------- 1 | .. _api_v1: 2 | 3 | ======================== 4 | watcherclient Python API 5 | ======================== 6 | 7 | The watcherclient python API lets you access watcher, the OpenStack 8 | TODEFINE Service. 9 | 10 | For example, to manipulate audits, you interact with an `watcherclient.v1.audit`_ object. 11 | You obtain access to audits via attributes of the `watcherclient.v1.client.Client`_ object. 12 | 13 | Usage 14 | ===== 15 | 16 | Get a Client object 17 | ------------------- 18 | First, create an `watcherclient.v1.client.Client`_ instance by passing your 19 | credentials to `watcherclient.client.get_client()`_. By default, the 20 | Watcher system is configured so that only administrators (users with 21 | 'admin' role) have access. 22 | 23 | There are two different sets of credentials that can be used:: 24 | 25 | * watcher endpoint and auth token 26 | * Identity Service (keystone) credentials 27 | 28 | Using watcher endpoint and auth token 29 | ..................................... 30 | 31 | An auth token and the watcher endpoint can be used to authenticate:: 32 | 33 | * os_auth_token: authentication token (from Identity Service) 34 | * watcher_url: watcher API endpoint, eg http://watcher.example.org:9322/v1 35 | 36 | To create the client, you can use the API like so:: 37 | 38 | >>> from watcherclient import client 39 | >>> 40 | >>> kwargs = {'os_auth_token': '3bcc3d3a03f44e3d8377f9247b0ad155' 41 | >>> 'watcher_url': 'http://watcher.example.org:9322/'} 42 | >>> watcher = client.get_client(1, **kwargs) 43 | 44 | Using Identity Service (keystone) credentials 45 | ............................................. 46 | 47 | These Identity Service credentials can be used to authenticate:: 48 | 49 | * os_username: name of user 50 | * os_password: user's password 51 | * os_auth_url: Identity Service endpoint for authorization 52 | * os_tenant_{name|id}: name or ID of tenant 53 | 54 | To create a client, you can use the API like so:: 55 | 56 | >>> from watcherclient import client 57 | >>> 58 | >>> kwargs = {'os_username': 'name', 59 | >>> 'os_password': 'password', 60 | >>> 'os_auth_url': 'http://keystone.example.org:5000/', 61 | >>> 'os_tenant_name': 'tenant'} 62 | >>> watcher = client.get_client(1, **kwargs) 63 | 64 | Perform watcher operations 65 | -------------------------- 66 | 67 | Once you have an watcher `Client`_, you can perform various tasks:: 68 | 69 | >>> watcher.action.list() # list of actions 70 | >>> watcher.action_plan.list() # list of action_plan 71 | >>> watcher.audit.get(audit_uuid_or_name) # information about a particular audit 72 | 73 | When the `Client`_ needs to propagate an exception, it will usually 74 | raise an instance listed in `watcherclient.exceptions`_. 75 | 76 | Refer to the modules themselves, for more details. 77 | 78 | ===================== 79 | watcherclient Modules 80 | ===================== 81 | 82 | .. _watcherclient.v1.audit: api/watcherclient.v1.audit.html#watcherclient.v1.audit.Audit 83 | .. _watcherclient.v1.client.Client: api/watcherclient.v1.client.html#watcherclient.v1.client.Client 84 | .. _Client: api/watcherclient.v1.client.html#watcherclient.v1.client.Client 85 | .. _watcherclient.client.get_client(): api/watcherclient.client.html#watcherclient.client.get_client 86 | .. _watcherclient.exceptions: api/watcherclient.exceptions.html 87 | -------------------------------------------------------------------------------- /watcherclient/osc/plugin.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | 14 | import logging 15 | 16 | from osc_lib import utils 17 | 18 | import watcherclient 19 | from watcherclient.common import api_versioning 20 | from watcherclient.common import httpclient 21 | from watcherclient import exceptions 22 | 23 | LOG = logging.getLogger(__name__) 24 | 25 | DEFAULT_API_VERSION = httpclient.LATEST_VERSION 26 | API_VERSION_OPTION = 'os_infra_optim_api_version' 27 | API_NAME = 'infra-optim' 28 | API_VERSIONS = { 29 | '1': 'watcherclient.v1.client.Client', 30 | } 31 | 32 | 33 | def make_client(instance): 34 | """Returns an infra-optim service client.""" 35 | 36 | version = api_versioning.APIVersion(instance._api_version[API_NAME]) 37 | 38 | infraoptim_client_class = utils.get_client_class( 39 | API_NAME, 40 | version.ver_major, 41 | API_VERSIONS) 42 | LOG.debug('Instantiating infraoptim client: %s', infraoptim_client_class) 43 | 44 | client = infraoptim_client_class( 45 | os_infra_optim_api_version=instance._api_version[API_NAME], 46 | session=instance.session, 47 | region_name=instance._region_name, 48 | ) 49 | 50 | return client 51 | 52 | 53 | def build_option_parser(parser): 54 | """Hook to add global options.""" 55 | parser.add_argument('--os-infra-optim-api-version', 56 | metavar='', 57 | default=utils.env( 58 | 'OS_INFRA_OPTIM_API_VERSION', 59 | default=DEFAULT_API_VERSION), 60 | help=('Watcher API version, default=' + 61 | DEFAULT_API_VERSION + 62 | ' (Env: OS_INFRA_OPTIM_API_VERSION)')) 63 | return parser 64 | 65 | 66 | def check_api_version(check_version): 67 | """Validate version supplied by user 68 | 69 | Returns: 70 | * True if version is OK 71 | * False if the version has not been checked and the previous plugin 72 | check should be performed 73 | * throws an exception if the version is no good 74 | """ 75 | 76 | infra_api_version = api_versioning.get_api_version(check_version) 77 | 78 | # Bypass X.latest format microversion 79 | if not infra_api_version.is_latest(): 80 | if infra_api_version > api_versioning.APIVersion("2.0"): 81 | if not infra_api_version.matches( 82 | watcherclient.API_MIN_VERSION, 83 | watcherclient.API_MAX_VERSION, 84 | ): 85 | msg = "versions supported by client: %(min)s - %(max)s" % { 86 | "min": watcherclient.API_MIN_VERSION.get_string(), 87 | "max": watcherclient.API_MAX_VERSION.get_string(), 88 | } 89 | raise exceptions.CommandError(msg) 90 | return True 91 | -------------------------------------------------------------------------------- /watcherclient/v1/strategy.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2013 Red Hat, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from urllib import parse 17 | 18 | from watcherclient.common import base 19 | from watcherclient.common import utils 20 | 21 | 22 | class Strategy(base.Resource): 23 | def __repr__(self): 24 | return "" % self._info 25 | 26 | 27 | class StrategyManager(base.Manager): 28 | resource_class = Strategy 29 | 30 | @staticmethod 31 | def _path(strategy=None, state=False): 32 | if strategy: 33 | path = '/v1/strategies/%s' % strategy 34 | if state: 35 | path = '/v1/strategies/%s/state' % strategy 36 | else: 37 | path = '/v1/strategies' 38 | return path 39 | 40 | def list(self, goal=None, limit=None, sort_key=None, 41 | sort_dir=None, detail=False, marker=None): 42 | """Retrieve a list of strategy. 43 | 44 | :param goal: The UUID of the goal to filter by 45 | :param limit: The maximum number of results to return per 46 | request, if: 47 | 48 | 1) limit > 0, the maximum number of audits to return. 49 | 2) limit == 0, return the entire list of audits. 50 | 3) limit param is NOT specified (None), the number of items 51 | returned respect the maximum imposed by the Watcher API 52 | (see Watcher's api.max_limit option). 53 | 54 | :param sort_key: Optional, field used for sorting. 55 | 56 | :param sort_dir: Optional, direction of sorting, either 'asc' (the 57 | default) or 'desc'. 58 | 59 | :param detail: Optional, boolean whether to return detailed information 60 | about audits. 61 | :param marker: Optional, UUID of the last strategy in the previous 62 | page. 63 | :returns: A list of audits. 64 | 65 | """ 66 | if limit is not None: 67 | limit = int(limit) 68 | 69 | filters = utils.common_filters(limit, sort_key, sort_dir, marker) 70 | 71 | if goal: 72 | filters.append(parse.urlencode(dict(goal=goal))) 73 | 74 | path = '' 75 | if detail: 76 | path += 'detail' 77 | if filters: 78 | path += '?' + '&'.join(filters) 79 | 80 | if limit is None: 81 | return self._list(self._path(path), "strategies") 82 | else: 83 | return self._list_pagination(self._path(path), "strategies", 84 | limit=limit) 85 | 86 | def get(self, strategy): 87 | try: 88 | return self._list(self._path(strategy))[0] 89 | except IndexError: 90 | return None 91 | 92 | def state(self, strategy): 93 | return self._list(self._path(strategy, state=True)) 94 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/keystone_client_fixtures.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | from oslo_serialization import jsonutils 14 | from oslo_utils import uuidutils 15 | 16 | from keystoneauth1.fixture import v2 as ks_v2_fixture 17 | from keystoneauth1.fixture import v3 as ks_v3_fixture 18 | 19 | # these are copied from python-keystoneclient tests 20 | BASE_HOST = 'http://keystone.example.com' 21 | BASE_URL = "%s:5000/" % BASE_HOST 22 | UPDATED = '2013-03-06T00:00:00Z' 23 | 24 | V2_URL = "%sv2.0" % BASE_URL 25 | V2_DESCRIBED_BY_HTML = {'href': 'http://docs.openstack.org/api/' 26 | 'openstack-identity-service/2.0/content/', 27 | 'rel': 'describedby', 28 | 'type': 'text/html'} 29 | V2_DESCRIBED_BY_PDF = {'href': 'http://docs.openstack.org/api/openstack-ident' 30 | 'ity-service/2.0/identity-dev-guide-2.0.pdf', 31 | 'rel': 'describedby', 32 | 'type': 'application/pdf'} 33 | 34 | V2_VERSION = {'id': 'v2.0', 35 | 'links': [{'href': V2_URL, 'rel': 'self'}, 36 | V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF], 37 | 'status': 'stable', 38 | 'updated': UPDATED} 39 | 40 | V3_URL = "%sv3" % BASE_URL 41 | V3_MEDIA_TYPES = [{'base': 'application/json', 42 | 'type': 'application/vnd.openstack.identity-v3+json'}, 43 | {'base': 'application/xml', 44 | 'type': 'application/vnd.openstack.identity-v3+xml'}] 45 | 46 | V3_VERSION = {'id': 'v3.0', 47 | 'links': [{'href': V3_URL, 'rel': 'self'}], 48 | 'media-types': V3_MEDIA_TYPES, 49 | 'status': 'stable', 50 | 'updated': UPDATED} 51 | 52 | TOKENID = uuidutils.generate_uuid(dashed=False) 53 | 54 | 55 | def _create_version_list(versions): 56 | return jsonutils.dumps({'versions': {'values': versions}}) 57 | 58 | 59 | def _create_single_version(version): 60 | return jsonutils.dumps({'version': version}) 61 | 62 | 63 | V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION]) 64 | V2_VERSION_LIST = _create_version_list([V2_VERSION]) 65 | 66 | V3_VERSION_ENTRY = _create_single_version(V3_VERSION) 67 | V2_VERSION_ENTRY = _create_single_version(V2_VERSION) 68 | 69 | 70 | def keystone_request_callback(request, uri, headers): 71 | response_headers = {"content-type": "application/json"} 72 | token_id = TOKENID 73 | if uri == BASE_URL: 74 | return (200, headers, V3_VERSION_LIST) 75 | elif uri == BASE_URL + "/v2.0": 76 | v2_token = ks_v2_fixture.Token(token_id) 77 | return (200, response_headers, jsonutils.dumps(v2_token)) 78 | elif uri == BASE_URL + "/v3": 79 | v3_token = ks_v3_fixture.Token() 80 | response_headers["X-Subject-Token"] = token_id 81 | return (201, response_headers, jsonutils.dumps(v3_token)) 82 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | 15 | # -- General configuration ---------------------------------------------------- 16 | 17 | # Add any Sphinx extension module names here, as strings. They can be 18 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 19 | extensions = ['sphinx.ext.autodoc', 20 | 'sphinx.ext.viewcode', 21 | 'sphinxcontrib.apidoc', 22 | 'openstackdocstheme', 23 | ] 24 | # autodoc generation is a bit aggressive and a nuisance when doing heavy 25 | # text edit cycles. 26 | # execute "export SPHINX_DEBUG=1" in your terminal to disable 27 | 28 | # sphinxcontrib.apidoc options 29 | apidoc_module_dir = '../../watcherclient' 30 | apidoc_output_dir = 'reference/api' 31 | apidoc_excluded_paths = [ 32 | 'tests/*'] 33 | apidoc_separate_modules = True 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = '.rst' 40 | 41 | # The master toctree document. 42 | master_doc = 'index' 43 | 44 | # General information about the project. 45 | project = 'python-watcherclient' 46 | copyright = 'OpenStack Foundation' 47 | 48 | # A list of ignored prefixes for module index sorting. 49 | modindex_common_prefix = ['watcherclient.'] 50 | 51 | # If true, '()' will be appended to :func: etc. cross-reference text. 52 | add_function_parentheses = True 53 | 54 | # If true, the current module name will be prepended to all description 55 | # unit titles (such as .. function::). 56 | add_module_names = True 57 | 58 | # The name of the Pygments (syntax highlighting) style to use. 59 | pygments_style = 'native' 60 | 61 | # -- Options for HTML output -------------------------------------------------- 62 | 63 | # The theme to use for HTML and HTML Help pages. Major themes that come with 64 | # Sphinx are currently 'default' and 'sphinxdoc'. 65 | # html_theme_path = ["."] 66 | # html_theme = '_theme' 67 | # html_static_path = ['_static'] 68 | html_theme = 'openstackdocs' 69 | # html_theme_path = [openstackdocstheme.get_html_theme_path()] 70 | 71 | # Output file base name for HTML help builder. 72 | htmlhelp_basename = '%sdoc' % project 73 | 74 | 75 | # Grouping the document tree into LaTeX files. List of tuples 76 | # (source start file, target name, title, author, documentclass 77 | # [howto/manual]). 78 | latex_documents = [ 79 | ( 80 | 'index', 81 | '%s.tex' % project, 82 | '%s Documentation' % project, 83 | 'OpenStack Foundation', 'manual' 84 | ), 85 | ] 86 | 87 | # Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664 88 | latex_use_xindy = False 89 | 90 | latex_domain_indices = False 91 | 92 | latex_elements = { 93 | 'makeindex': '', 94 | 'printindex': '', 95 | 'preamble': r'\setcounter{tocdepth}{3}', 96 | } 97 | 98 | # openstackdocstheme options 99 | openstackdocs_repo_name = 'openstack/python-watcherclient' 100 | openstackdocs_pdf_link = True 101 | openstackdocs_bug_project = 'python-watcherclient' 102 | openstackdocs_bug_tag = '' 103 | 104 | 105 | #html_theme_options = {"show_other_versions": "True"} 106 | -------------------------------------------------------------------------------- /watcherclient/v1/service_shell.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Servionica 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from osc_lib import utils 17 | 18 | from watcherclient._i18n import _ 19 | from watcherclient.common import command 20 | from watcherclient.common import utils as common_utils 21 | from watcherclient import exceptions 22 | from watcherclient.v1 import resource_fields as res_fields 23 | 24 | 25 | class ShowService(command.ShowOne): 26 | """Show detailed information about a given service.""" 27 | 28 | def get_parser(self, prog_name): 29 | parser = super(ShowService, self).get_parser(prog_name) 30 | parser.add_argument( 31 | 'service', 32 | metavar='', 33 | help=_('ID or name of the service'), 34 | ) 35 | return parser 36 | 37 | def take_action(self, parsed_args): 38 | client = getattr(self.app.client_manager, "infra-optim") 39 | 40 | try: 41 | service = client.service.get(parsed_args.service) 42 | except exceptions.HTTPNotFound as exc: 43 | raise exceptions.CommandError(str(exc)) 44 | 45 | columns = res_fields.SERVICE_FIELDS 46 | column_headers = res_fields.SERVICE_FIELD_LABELS 47 | 48 | return column_headers, utils.get_item_properties(service, columns) 49 | 50 | 51 | class ListService(command.Lister): 52 | """List information on retrieved services.""" 53 | 54 | def get_parser(self, prog_name): 55 | parser = super(ListService, self).get_parser(prog_name) 56 | parser.add_argument( 57 | '--detail', 58 | dest='detail', 59 | action='store_true', 60 | default=False, 61 | help=_("Show detailed information about each service.")) 62 | parser.add_argument( 63 | '--limit', 64 | metavar='', 65 | type=int, 66 | help=_('Maximum number of services to return per request, ' 67 | '0 for no limit. Default is the maximum number used ' 68 | 'by the Watcher API Service.')) 69 | parser.add_argument( 70 | '--sort-key', 71 | metavar='', 72 | help=_('Goal field that will be used for sorting.')) 73 | parser.add_argument( 74 | '--sort-dir', 75 | metavar='', 76 | choices=['asc', 'desc'], 77 | help='Sort direction: "asc" (the default) or "desc".') 78 | 79 | return parser 80 | 81 | def take_action(self, parsed_args): 82 | client = getattr(self.app.client_manager, "infra-optim") 83 | 84 | params = {} 85 | if parsed_args.detail: 86 | fields = res_fields.SERVICE_FIELDS 87 | field_labels = res_fields.SERVICE_FIELD_LABELS 88 | else: 89 | fields = res_fields.SERVICE_SHORT_LIST_FIELDS 90 | field_labels = res_fields.SERVICE_SHORT_LIST_FIELD_LABELS 91 | 92 | params.update( 93 | common_utils.common_params_for_list( 94 | parsed_args, fields, field_labels)) 95 | 96 | try: 97 | data = client.service.list(**params) 98 | except exceptions.HTTPNotFound as ex: 99 | raise exceptions.CommandError(str(ex)) 100 | 101 | return (field_labels, 102 | (utils.get_item_properties(item, fields) for item in data)) 103 | -------------------------------------------------------------------------------- /watcherclient/tests/client_functional/v1/test_audit_template.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Servionica 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from oslo_utils import uuidutils 17 | 18 | from watcherclient.tests.client_functional.v1 import base 19 | 20 | 21 | class AuditTemplateTests(base.TestCase): 22 | """Functional tests for audit template.""" 23 | 24 | dummy_name = 'dummy' 25 | list_fields = ['UUID', 'Name', 'Goal', 'Strategy'] 26 | detailed_list_fields = list_fields + ['Created At', 'Updated At', 27 | 'Deleted At', 'Description', 28 | 'Audit Scope'] 29 | audit_template_name = 'a' + uuidutils.generate_uuid() 30 | 31 | @classmethod 32 | def setUpClass(cls): 33 | cls.watcher('audittemplate create %s dummy -s dummy' 34 | % cls.audit_template_name) 35 | 36 | @classmethod 37 | def tearDownClass(cls): 38 | cls.watcher('audittemplate delete %s' % cls.audit_template_name) 39 | 40 | def test_audit_template_list(self): 41 | raw_output = self.watcher('audittemplate list') 42 | self.assert_table_structure([raw_output], self.list_fields) 43 | 44 | def test_audit_template_detailed_list(self): 45 | raw_output = self.watcher('audittemplate list --detail') 46 | self.assert_table_structure([raw_output], self.detailed_list_fields) 47 | 48 | def test_audit_template_show(self): 49 | audit_template = self.watcher( 50 | 'audittemplate show %s' % self.audit_template_name) 51 | self.assertIn(self.audit_template_name, audit_template) 52 | self.assert_table_structure([audit_template], 53 | self.detailed_list_fields) 54 | 55 | def test_audit_template_update(self): 56 | raw_output = self.watcher('audittemplate update %s replace ' 57 | 'description="Updated Desc"' 58 | % self.audit_template_name) 59 | audit_template_output = self.parse_show_as_object(raw_output) 60 | assert audit_template_output['Description'] == 'Updated Desc' 61 | 62 | 63 | class AuditTemplateActiveTests(base.TestCase): 64 | 65 | audit_template_name = 'b' + uuidutils.generate_uuid() 66 | list_fields = ['UUID', 'Name', 'Goal', 'Strategy'] 67 | detailed_list_fields = list_fields + ['Created At', 'Updated At', 68 | 'Deleted At', 'Description', 69 | 'Audit Scope'] 70 | 71 | def _create_audit_template(self): 72 | self.watcher('audittemplate create %s dummy -s dummy ' 73 | '-d "Test Audit Template"' % self.audit_template_name) 74 | 75 | def _delete_audit_template(self): 76 | self.watcher('audittemplate delete %s' % self.audit_template_name) 77 | 78 | def test_create_audit_template(self): 79 | raw_output = self.watcher('audittemplate create %s dummy ' 80 | '-s dummy -d "Test Audit Template"' 81 | % self.audit_template_name) 82 | self.assert_table_structure([raw_output], self.detailed_list_fields) 83 | self._delete_audit_template() 84 | 85 | def test_delete_audit_template(self): 86 | self._create_audit_template() 87 | raw_output = self.watcher('audittemplate delete %s' 88 | % self.audit_template_name) 89 | self.assertOutput('', raw_output) 90 | -------------------------------------------------------------------------------- /watcherclient/v1/audit.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2013 Red Hat, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from watcherclient.common import base 17 | from watcherclient.common import utils 18 | from watcherclient import exceptions as exc 19 | 20 | 21 | CREATION_ATTRIBUTES = ['audit_template_uuid', 'audit_type', 'interval', 22 | 'parameters', 'goal', 'strategy', 'auto_trigger', 23 | 'name', 'start_time', 'end_time', 'force'] 24 | 25 | 26 | class Audit(base.Resource): 27 | def __repr__(self): 28 | return "" % self._info 29 | 30 | 31 | class AuditManager(base.Manager): 32 | resource_class = Audit 33 | 34 | @staticmethod 35 | def _path(id=None): 36 | return '/v1/audits/%s' % id if id else '/v1/audits' 37 | 38 | def list(self, audit_template=None, limit=None, sort_key=None, 39 | sort_dir=None, detail=False, goal=None, strategy=None, 40 | marker=None): 41 | """Retrieve a list of audit. 42 | 43 | :param audit_template: Name of the audit template 44 | :param limit: The maximum number of results to return per 45 | request, if: 46 | 47 | 1) limit > 0, the maximum number of audits to return. 48 | 2) limit == 0, return the entire list of audits. 49 | 3) limit param is NOT specified (None), the number of items 50 | returned respect the maximum imposed by the Watcher API 51 | (see Watcher's api.max_limit option). 52 | 53 | :param sort_key: Optional, field used for sorting. 54 | 55 | :param sort_dir: Optional, direction of sorting, either 'asc' (the 56 | default) or 'desc'. 57 | 58 | :param detail: Optional, boolean whether to return detailed information 59 | about audits. 60 | 61 | :param marker: Optional, UUID of the last audit in the previous page. 62 | 63 | :returns: A list of audits. 64 | 65 | """ 66 | if limit is not None: 67 | limit = int(limit) 68 | 69 | filters = utils.common_filters(limit, sort_key, sort_dir, marker) 70 | if audit_template is not None: 71 | filters.append('audit_template=%s' % audit_template) 72 | if goal is not None: 73 | filters.append('goal=%s' % goal) 74 | if strategy is not None: 75 | filters.append('strategy=%s' % strategy) 76 | 77 | path = '' 78 | if detail: 79 | path += 'detail' 80 | if filters: 81 | path += '?' + '&'.join(filters) 82 | 83 | if limit is None: 84 | return self._list(self._path(path), "audits") 85 | else: 86 | return self._list_pagination(self._path(path), "audits", 87 | limit=limit) 88 | 89 | def create(self, **kwargs): 90 | new = {} 91 | for (key, value) in kwargs.items(): 92 | if key in CREATION_ATTRIBUTES: 93 | new[key] = value 94 | else: 95 | raise exc.InvalidAttribute() 96 | return self._create(self._path(), new) 97 | 98 | def get(self, audit): 99 | try: 100 | return self._list(self._path(audit))[0] 101 | except IndexError: 102 | return None 103 | 104 | def delete(self, audit): 105 | return self._delete(self._path(audit)) 106 | 107 | def update(self, audit, patch): 108 | return self._update(self._path(audit), patch) 109 | -------------------------------------------------------------------------------- /watcherclient/v1/audit_template.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2013 Red Hat, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from watcherclient.common import base 17 | from watcherclient.common import utils 18 | from watcherclient import exceptions as exc 19 | 20 | CREATION_ATTRIBUTES = ['description', 'name', 'goal', 'strategy', 'scope'] 21 | 22 | 23 | class AuditTemplate(base.Resource): 24 | def __repr__(self): 25 | return "" % self._info 26 | 27 | 28 | class AuditTemplateManager(base.Manager): 29 | resource_class = AuditTemplate 30 | 31 | @staticmethod 32 | def _path(id_=None): 33 | return '/v1/audit_templates/%s' % id_ if id_ else '/v1/audit_templates' 34 | 35 | def list(self, name=None, goal=None, strategy=None, limit=None, 36 | sort_key=None, sort_dir=None, detail=False, marker=None): 37 | """Retrieve a list of audit template. 38 | 39 | :param name: Name of the audit template 40 | :param limit: The maximum number of results to return per 41 | request, if: 42 | 43 | 1) limit > 0, the maximum number of audit templates to return. 44 | 2) limit == 0, return the entire list of audit_templates. 45 | 3) limit param is NOT specified (None), the number of items 46 | returned respect the maximum imposed by the Watcher API 47 | (see Watcher's api.max_limit option). 48 | 49 | :param sort_key: Optional, field used for sorting. 50 | 51 | :param sort_dir: Optional, direction of sorting, either 'asc' (the 52 | default) or 'desc'. 53 | 54 | :param detail: Optional, boolean whether to return detailed information 55 | about audit_templates. 56 | 57 | :param marker: Optional, UUID of the last audit template of 58 | the previous page. 59 | 60 | :returns: A list of audit templates. 61 | 62 | """ 63 | if limit is not None: 64 | limit = int(limit) 65 | 66 | filters = utils.common_filters(limit, sort_key, sort_dir, marker) 67 | if name is not None: 68 | filters.append('name=%s' % name) 69 | if goal is not None: 70 | filters.append("goal=%s" % goal) 71 | if strategy is not None: 72 | filters.append("strategy=%s" % strategy) 73 | 74 | path = '' 75 | if detail: 76 | path += 'detail' 77 | if filters: 78 | path += '?' + '&'.join(filters) 79 | 80 | if limit is None: 81 | return self._list(self._path(path), "audit_templates") 82 | else: 83 | return self._list_pagination(self._path(path), "audit_templates", 84 | limit=limit) 85 | 86 | def get(self, audit_template_id): 87 | try: 88 | return self._list(self._path(audit_template_id))[0] 89 | except IndexError: 90 | return None 91 | 92 | def create(self, **kwargs): 93 | new = {} 94 | for (key, value) in kwargs.items(): 95 | if key in CREATION_ATTRIBUTES: 96 | new[key] = value 97 | else: 98 | raise exc.InvalidAttribute() 99 | return self._create(self._path(), new) 100 | 101 | def delete(self, audit_template_id): 102 | return self._delete(self._path(audit_template_id)) 103 | 104 | def update(self, audit_template_id, patch): 105 | return self._update(self._path(audit_template_id), patch) 106 | -------------------------------------------------------------------------------- /watcherclient/v1/scoring_engine_shell.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Intel 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from osc_lib import utils 17 | 18 | from watcherclient._i18n import _ 19 | from watcherclient.common import command 20 | from watcherclient.common import utils as common_utils 21 | from watcherclient import exceptions 22 | from watcherclient.v1 import resource_fields as res_fields 23 | 24 | 25 | class ShowScoringEngine(command.ShowOne): 26 | """Show detailed information about a given scoring engine.""" 27 | 28 | def get_parser(self, prog_name): 29 | parser = super(ShowScoringEngine, self).get_parser(prog_name) 30 | parser.add_argument( 31 | 'scoring_engine', 32 | metavar='', 33 | help=_('Name of the scoring engine'), 34 | ) 35 | return parser 36 | 37 | def take_action(self, parsed_args): 38 | client = getattr(self.app.client_manager, "infra-optim") 39 | 40 | try: 41 | scoring_engine = client.scoring_engine.get( 42 | parsed_args.scoring_engine) 43 | except exceptions.HTTPNotFound as exc: 44 | raise exceptions.CommandError(str(exc)) 45 | 46 | columns = res_fields.SCORING_ENGINE_FIELDS 47 | column_headers = res_fields.SCORING_ENGINE_FIELD_LABELS 48 | 49 | return column_headers, utils.get_item_properties(scoring_engine, 50 | columns) 51 | 52 | 53 | class ListScoringEngine(command.Lister): 54 | """List information on retrieved scoring engines.""" 55 | 56 | def get_parser(self, prog_name): 57 | parser = super(ListScoringEngine, self).get_parser(prog_name) 58 | parser.add_argument( 59 | '--detail', 60 | dest='detail', 61 | action='store_true', 62 | default=False, 63 | help=_("Show detailed information about scoring engines.")) 64 | parser.add_argument( 65 | '--limit', 66 | metavar='', 67 | type=int, 68 | help=_('Maximum number of actions to return per request, ' 69 | '0 for no limit. Default is the maximum number used ' 70 | 'by the Watcher API Service.')) 71 | parser.add_argument( 72 | '--sort-key', 73 | metavar='', 74 | help=_('Action field that will be used for sorting.')) 75 | parser.add_argument( 76 | '--sort-dir', 77 | metavar='', 78 | choices=['asc', 'desc'], 79 | help=_('Sort direction: "asc" (the default) or "desc".')) 80 | 81 | return parser 82 | 83 | def take_action(self, parsed_args): 84 | client = getattr(self.app.client_manager, "infra-optim") 85 | 86 | params = {} 87 | if parsed_args.detail: 88 | fields = res_fields.SCORING_ENGINE_FIELDS 89 | field_labels = res_fields.SCORING_ENGINE_FIELD_LABELS 90 | else: 91 | fields = res_fields.SCORING_ENGINE_SHORT_LIST_FIELDS 92 | field_labels = res_fields.SCORING_ENGINE_SHORT_LIST_FIELD_LABELS 93 | 94 | params.update( 95 | common_utils.common_params_for_list( 96 | parsed_args, fields, field_labels)) 97 | 98 | try: 99 | data = client.scoring_engine.list(**params) 100 | except exceptions.HTTPNotFound as ex: 101 | raise exceptions.CommandError(str(ex)) 102 | 103 | return (field_labels, 104 | (utils.get_item_properties(item, fields) for item in data)) 105 | -------------------------------------------------------------------------------- /watcherclient/v1/action_plan.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2013 Red Hat, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from watcherclient.common import base 17 | from watcherclient.common import utils 18 | # from watcherclient import exceptions as exc 19 | 20 | 21 | class ActionPlan(base.Resource): 22 | def __repr__(self): 23 | return "" % self._info 24 | 25 | 26 | class ActionPlanManager(base.Manager): 27 | resource_class = ActionPlan 28 | 29 | @staticmethod 30 | def _path(id=None, q_param=None): 31 | if id and q_param: 32 | return '/v1/action_plans/%s/%s' % (id, q_param) 33 | elif id: 34 | return '/v1/action_plans/%s' % id 35 | else: 36 | return '/v1/action_plans' 37 | 38 | def list(self, audit=None, limit=None, sort_key=None, 39 | sort_dir=None, detail=False, marker=None): 40 | """Retrieve a list of action plan. 41 | 42 | :param audit: Name of the audit 43 | :param limit: The maximum number of results to return per 44 | request, if: 45 | 46 | 1) limit > 0, the maximum number of action plans to return. 47 | 2) limit == 0, return the entire list of action plans. 48 | 3) limit param is NOT specified (None), the number of items 49 | returned respect the maximum imposed by the Watcher API 50 | (see Watcher's api.max_limit option). 51 | 52 | :param sort_key: Optional, field used for sorting. 53 | 54 | :param sort_dir: Optional, direction of sorting, either 'asc' (the 55 | default) or 'desc'. 56 | 57 | :param detail: Optional, boolean whether to return detailed information 58 | about action plans. 59 | 60 | :param marker: The last actionplan UUID of the previous page; 61 | displays list of actionplans after "marker". 62 | 63 | :returns: A list of action plans. 64 | 65 | """ 66 | if limit is not None: 67 | limit = int(limit) 68 | 69 | filters = utils.common_filters(limit, sort_key, sort_dir, marker) 70 | if audit is not None: 71 | filters.append('audit_uuid=%s' % audit) 72 | 73 | path = '' 74 | if detail: 75 | path += 'detail' 76 | if filters: 77 | path += '?' + '&'.join(filters) 78 | 79 | if limit is None: 80 | return self._list(self._path(path), "action_plans") 81 | else: 82 | return self._list_pagination(self._path(path), "action_plans", 83 | limit=limit) 84 | 85 | def get(self, action_plan_id): 86 | try: 87 | return self._list(self._path(action_plan_id))[0] 88 | except IndexError: 89 | return None 90 | 91 | def delete(self, action_plan_id): 92 | return self._delete(self._path(action_plan_id)) 93 | 94 | def update(self, action_plan_id, patch): 95 | return self._update(self._path(action_plan_id), patch) 96 | 97 | def start(self, action_plan_id): 98 | return self._start(self._path(action_plan_id, 'start')) 99 | 100 | def cancel(self, action_plan_id): 101 | action_plan = self.get(action_plan_id) 102 | if action_plan.state == "ONGOING": 103 | patch = [{'op': 'replace', 'value': 'CANCELLING', 104 | 'path': '/state'}] 105 | else: 106 | patch = [{'op': 'replace', 'value': 'CANCELLED', 'path': '/state'}] 107 | return self._update(self._path(action_plan_id), patch) 108 | -------------------------------------------------------------------------------- /watcherclient/tests/client_functional/v1/test_action_plan.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Servionica 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from oslo_utils import uuidutils 17 | 18 | import functools 19 | 20 | from tempest.lib.common.utils import test_utils 21 | 22 | from watcherclient.tests.client_functional.v1 import base 23 | 24 | 25 | class ActionPlanTests(base.TestCase): 26 | """Functional tests for action plan.""" 27 | 28 | dummy_name = 'dummy' 29 | list_fields = ['UUID', 'Audit', 'State', 'Updated At', 'Global efficacy'] 30 | detailed_list_fields = list_fields + ['Created At', 'Deleted At', 31 | 'Strategy', 'Efficacy indicators', 32 | 'Hostname'] 33 | audit_template_name = 'a' + uuidutils.generate_uuid() 34 | audit_uuid = None 35 | 36 | @classmethod 37 | def setUpClass(cls): 38 | template_raw_output = cls.watcher( 39 | 'audittemplate create %s dummy -s dummy' % cls.audit_template_name) 40 | template_output = cls.parse_show_as_object(template_raw_output) 41 | audit_raw_output = cls.watcher('audit create -a %s' 42 | % template_output['Name']) 43 | audit_output = cls.parse_show_as_object(audit_raw_output) 44 | cls.audit_uuid = audit_output['UUID'] 45 | audit_created = test_utils.call_until_true( 46 | func=functools.partial(cls.has_audit_created, cls.audit_uuid), 47 | duration=600, 48 | sleep_for=2) 49 | if not audit_created: 50 | raise Exception('Audit has not been succeeded') 51 | 52 | @classmethod 53 | def tearDownClass(cls): 54 | # Delete action plan 55 | output = cls.parse_show( 56 | cls.watcher('actionplan list --audit %s' % cls.audit_uuid)) 57 | action_plan_uuid = list(output[0])[0] 58 | raw_output = cls.watcher('actionplan delete %s' % action_plan_uuid) 59 | cls.assertOutput('', raw_output) 60 | # Delete audit 61 | raw_output = cls.watcher('audit delete %s' % cls.audit_uuid) 62 | cls.assertOutput('', raw_output) 63 | # Delete Template 64 | raw_output = cls.watcher( 65 | 'audittemplate delete %s' % cls.audit_template_name) 66 | cls.assertOutput('', raw_output) 67 | 68 | def test_action_plan_list(self): 69 | raw_output = self.watcher('actionplan list') 70 | self.assert_table_structure([raw_output], self.list_fields) 71 | 72 | def test_action_plan_detailed_list(self): 73 | raw_output = self.watcher('actionplan list --detail') 74 | self.assert_table_structure([raw_output], self.detailed_list_fields) 75 | 76 | def test_action_plan_show(self): 77 | action_plan_list = self.parse_show(self.watcher('actionplan list')) 78 | action_plan_uuid = list(action_plan_list[0])[0] 79 | actionplan = self.watcher('actionplan show %s' % action_plan_uuid) 80 | self.assertIn(action_plan_uuid, actionplan) 81 | self.assert_table_structure([actionplan], 82 | self.detailed_list_fields) 83 | 84 | def test_action_plan_start(self): 85 | output = self.parse_show(self.watcher('actionplan list --audit %s' 86 | % self.audit_uuid)) 87 | action_plan_uuid = list(output[0])[0] 88 | self.watcher('actionplan start %s' % action_plan_uuid) 89 | raw_output = self.watcher('actionplan show %s' % action_plan_uuid) 90 | self.assert_table_structure([raw_output], self.detailed_list_fields) 91 | 92 | self.assertTrue(test_utils.call_until_true( 93 | func=functools.partial( 94 | self.has_actionplan_succeeded, action_plan_uuid), 95 | duration=600, 96 | sleep_for=2 97 | )) 98 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/v1/test_service_shell.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Servionica 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import datetime 17 | import io 18 | from unittest import mock 19 | 20 | from watcherclient import shell 21 | from watcherclient.tests.unit.v1 import base 22 | from watcherclient import v1 as resource 23 | from watcherclient.v1 import resource_fields 24 | 25 | SERVICE_1 = { 26 | 'name': 'watcher-applier', 27 | 'host': 'controller', 28 | 'status': 'ACTIVE', 29 | 'last_seen_up': None, 30 | 'created_at': datetime.datetime.now().isoformat(), 31 | 'updated_at': None, 32 | 'deleted_at': None, 33 | } 34 | 35 | SERVICE_2 = { 36 | 'name': 'watcher-decision-engine', 37 | 'host': 'controller', 38 | 'status': 'FAILED', 39 | 'last_seen_up': None, 40 | 'created_at': datetime.datetime.now().isoformat(), 41 | 'updated_at': None, 42 | 'deleted_at': None, 43 | } 44 | 45 | 46 | class ServiceShellTest(base.CommandTestCase): 47 | 48 | SHORT_LIST_FIELDS = resource_fields.SERVICE_SHORT_LIST_FIELDS 49 | SHORT_LIST_FIELD_LABELS = ( 50 | resource_fields.SERVICE_SHORT_LIST_FIELD_LABELS) 51 | FIELDS = resource_fields.SERVICE_FIELDS 52 | FIELD_LABELS = resource_fields.SERVICE_FIELD_LABELS 53 | 54 | def setUp(self): 55 | super(self.__class__, self).setUp() 56 | 57 | p_service_manager = mock.patch.object(resource, 'ServiceManager') 58 | self.m_service_mgr_cls = p_service_manager.start() 59 | self.addCleanup(p_service_manager.stop) 60 | 61 | self.m_service_mgr = mock.Mock() 62 | self.m_service_mgr_cls.return_value = self.m_service_mgr 63 | 64 | self.stdout = io.StringIO() 65 | self.cmd = shell.WatcherShell(stdout=self.stdout) 66 | 67 | def test_do_service_list(self): 68 | service1 = resource.Service(mock.Mock(), SERVICE_1) 69 | service2 = resource.Service(mock.Mock(), SERVICE_2) 70 | self.m_service_mgr.list.return_value = [ 71 | service1, service2] 72 | 73 | exit_code, results = self.run_cmd('service list') 74 | 75 | for res in results: 76 | del res['ID'] 77 | 78 | self.assertEqual(0, exit_code) 79 | self.assertEqual( 80 | [self.resource_as_dict(service1, self.SHORT_LIST_FIELDS, 81 | self.SHORT_LIST_FIELD_LABELS), 82 | self.resource_as_dict(service2, self.SHORT_LIST_FIELDS, 83 | self.SHORT_LIST_FIELD_LABELS)], 84 | results) 85 | 86 | self.m_service_mgr.list.assert_called_once_with(detail=False) 87 | 88 | def test_do_service_list_detail(self): 89 | service1 = resource.Service(mock.Mock(), SERVICE_1) 90 | service2 = resource.Service(mock.Mock(), SERVICE_2) 91 | self.m_service_mgr.list.return_value = [ 92 | service1, service2] 93 | 94 | exit_code, results = self.run_cmd('service list --detail') 95 | 96 | for res in results: 97 | del res['ID'] 98 | 99 | self.assertEqual(0, exit_code) 100 | self.assertEqual( 101 | [self.resource_as_dict(service1, self.FIELDS, 102 | self.FIELD_LABELS), 103 | self.resource_as_dict(service2, self.FIELDS, 104 | self.FIELD_LABELS)], 105 | results) 106 | 107 | self.m_service_mgr.list.assert_called_once_with(detail=True) 108 | 109 | def test_do_service_show_by_name(self): 110 | service = resource.Service(mock.Mock(), SERVICE_1) 111 | self.m_service_mgr.get.return_value = service 112 | 113 | exit_code, result = self.run_cmd('service show watcher-applier') 114 | 115 | del result['ID'] 116 | 117 | self.assertEqual(0, exit_code) 118 | self.assertEqual( 119 | self.resource_as_dict(service, self.FIELDS, self.FIELD_LABELS), 120 | result) 121 | self.m_service_mgr.get.assert_called_once_with('watcher-applier') 122 | -------------------------------------------------------------------------------- /watcherclient/exceptions.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | from watcherclient.common.apiclient import exceptions 15 | 16 | 17 | # NOTE(akurilin): This alias is left here since v.0.1.3 to support backwards 18 | # compatibility. 19 | InvalidEndpoint = exceptions.EndpointException 20 | CommunicationError = exceptions.ConnectionRefused 21 | HTTPBadRequest = exceptions.BadRequest 22 | HTTPInternalServerError = exceptions.InternalServerError 23 | HTTPNotFound = exceptions.NotFound 24 | HTTPServiceUnavailable = exceptions.ServiceUnavailable 25 | 26 | 27 | CommandError = exceptions.CommandError 28 | """Error in CLI tool. 29 | 30 | An alias of :py:exc:`watcherclient.common.apiclient.CommandError` 31 | """ 32 | 33 | Unauthorized = exceptions.Unauthorized 34 | """HTTP 401 - Unauthorized. 35 | 36 | Similar to 403 Forbidden, but specifically for use when authentication 37 | is required and has failed or has not yet been provided. 38 | An alias of :py:exc:`watcherclient.common.apiclient.Unauthorized` 39 | """ 40 | 41 | InternalServerError = exceptions.InternalServerError 42 | """HTTP 500 - Internal Server Error. 43 | 44 | A generic error message, given when no more specific message is suitable. 45 | An alias of :py:exc:`watcherclient.common.apiclient.InternalServerError` 46 | """ 47 | 48 | ValidationError = exceptions.ValidationError 49 | """Error in validation on API client side. 50 | 51 | A generic error message, given when no more specific message is suitable. 52 | An alias of :py:exc:`watcherclient.common.apiclient.ValidationError` 53 | """ 54 | 55 | Conflict = exceptions.Conflict 56 | ConnectionRefused = exceptions.ConnectionRefused 57 | EndpointException = exceptions.EndpointException 58 | EndpointNotFound = exceptions.EndpointNotFound 59 | ServiceUnavailable = exceptions.ServiceUnavailable 60 | 61 | 62 | class UnsupportedVersion(Exception): 63 | """Unsupported API Version 64 | 65 | Indicates that the user is trying to use an unsupported version of the API. 66 | """ 67 | pass 68 | 69 | 70 | class AmbiguousAuthSystem(exceptions.ClientException): 71 | """Could not obtain token and endpoint using provided credentials.""" 72 | pass 73 | 74 | 75 | # Alias for backwards compatibility 76 | AmbigiousAuthSystem = AmbiguousAuthSystem 77 | 78 | 79 | class InvalidAttribute(exceptions.ClientException): 80 | pass 81 | 82 | 83 | def from_response(response, message=None, traceback=None, method=None, 84 | url=None): 85 | """Return an HttpError instance based on response from httplib/requests.""" 86 | 87 | error_body = {} 88 | if message: 89 | error_body['message'] = message 90 | if traceback: 91 | error_body['details'] = traceback 92 | 93 | if hasattr(response, 'status') and not hasattr(response, 'status_code'): 94 | # NOTE(akurilin): These modifications around response object give 95 | # ability to get all necessary information in method `from_response` 96 | # from common code, which expecting response object from `requests` 97 | # library instead of object from `httplib/httplib2` library. 98 | response.status_code = response.status 99 | response.headers = { 100 | 'Content-Type': response.getheader('content-type', "")} 101 | 102 | if hasattr(response, 'status_code'): 103 | # NOTE(hongbin): This allows SessionClient to handle faultstring. 104 | response.json = lambda: {'error': error_body} 105 | 106 | if (response.headers.get('Content-Type', '').startswith('text/') and 107 | not hasattr(response, 'text')): 108 | # NOTE(clif_h): There seems to be a case in the 109 | # common.apiclient.exceptions module where if the 110 | # content-type of the response is text/* then it expects 111 | # the response to have a 'text' attribute, but that 112 | # doesn't always seem to necessarily be the case. 113 | # This is to work around that problem. 114 | response.text = '' 115 | 116 | return exceptions.from_response(response, method, url) 117 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Team and repository tags 3 | ======================== 4 | 5 | .. image:: https://governance.openstack.org/tc/badges/python-watcherclient.svg 6 | :target: https://governance.openstack.org/tc/reference/tags/index.html 7 | 8 | .. Change things from this point on 9 | 10 | ==================== 11 | python-watcherclient 12 | ==================== 13 | 14 | Client for resource optimization service for OpenStack. 15 | 16 | OpenStack Watcher provides a flexible and scalable resource optimization 17 | service for multi-tenant OpenStack-based clouds. 18 | Watcher provides a complete optimization loop-including everything from a 19 | metrics receiver, complex event processor and profiler, optimization processor 20 | and an action plan applier. This provides a robust framework to realize a wide 21 | range of cloud optimization goals, including the reduction of data center 22 | operating costs, increased system performance via intelligent virtual machine 23 | migration, increased energy efficiency and more! 24 | 25 | * Free software: Apache license 26 | * Wiki: https://wiki.openstack.org/wiki/Watcher 27 | * Source: https://opendev.org/openstack/python-watcherclient 28 | * Bugs: https://bugs.launchpad.net/watcher 29 | 30 | Installation 31 | ============ 32 | 33 | Install the prerequisite packages 34 | --------------------------------- 35 | 36 | On Ubuntu (tested on 14.04-64) 37 | 38 | .. code:: 39 | 40 | sudo apt-get install python-dev libssl-dev python-pip git-core libmysqlclient-dev libffi-dev 41 | 42 | On Fedora-based distributions e.g., Fedora/RHEL/CentOS/Scientific Linux (tested on CentOS 6.5) 43 | 44 | .. code:: 45 | 46 | sudo yum install python-virtualenv openssl-devel python-pip git gcc libffi-devel mysql-devel postgresql-devel 47 | 48 | On openSUSE-based distributions (SLES 12, openSUSE 13.1, Factory or Tumbleweed) 49 | 50 | .. code:: 51 | 52 | sudo zypper install gcc git libmysqlclient-devel libopenssl-devel postgresql-devel python-devel python-pip 53 | 54 | Install the Watcher client 55 | -------------------------- 56 | 57 | You can install the Watcher CLI with the following command: 58 | 59 | .. code:: 60 | 61 | sudo pip install python-watcherclient 62 | 63 | 64 | You can also use the `OpenStack client `_ 65 | with Watcher (our watcher plugin for OpenStack client is included in the 66 | python-watcherclient package). To install it, you have just to run this command: 67 | 68 | .. code:: 69 | 70 | sudo pip install python-openstackclient 71 | 72 | Configuration 73 | ============= 74 | 75 | Create a **creds** file containing your OpenStack credentials: 76 | 77 | .. code:: 78 | 79 | export OS_IDENTITY_API_VERSION=3 80 | export OS_AUTH_URL=http://:5000/v3 81 | export OS_PROJECT_DOMAIN_ID=default 82 | export OS_USER_DOMAIN_ID=default 83 | export OS_USERNAME=admin 84 | export OS_PASSWORD= 85 | export OS_PROJECT_NAME= 86 | 87 | Source these credentials into your current shell session: 88 | 89 | .. code:: 90 | 91 | # source creds 92 | 93 | You should be able to launch the following command which gets the list of 94 | previously created Audit Templates: 95 | 96 | .. code:: 97 | 98 | # watcher audittemplate list 99 | 100 | or:: 101 | 102 | # openstack optimize audittemplate list 103 | +--------------------------------+------+----------------------+----------+ 104 | | UUID | Name | Goal | Strategy | 105 | +--------------------------------+------+----------------------+----------+ 106 | +--------------------------------+------+----------------------+----------+ 107 | 108 | 109 | You can view the entire list of available Watcher commands and options using 110 | this command: 111 | 112 | .. code:: 113 | 114 | # watcher help 115 | 116 | or:: 117 | 118 | # openstack help optimize 119 | 120 | 121 | Troubleshootings 122 | ================ 123 | 124 | If any watcher command fails, you can obtain more details with the **--debug** 125 | option : 126 | 127 | .. code:: 128 | 129 | # watcher --debug audittemplate list 130 | 131 | or:: 132 | 133 | # openstack --debug optimize audittemplate list 134 | 135 | 136 | Install the openstack CLI : 137 | 138 | .. code:: 139 | 140 | # pip install python-openstackclient 141 | 142 | Make sure that your Openstack credentials are correct. If so, you should be able 143 | to verify that the watcher user has been declared in your Openstack keystone : 144 | 145 | .. code:: 146 | 147 | # openstack user list 148 | 149 | and that the watcher endpoints have been declared as well : 150 | 151 | .. code:: 152 | 153 | # openstack endpoint list 154 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/v1/test_data_model_shell.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 ZTE Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import io 17 | from unittest import mock 18 | 19 | from watcherclient import shell 20 | from watcherclient.tests.unit.v1 import base 21 | from watcherclient import v1 as resource 22 | from watcherclient.v1 import resource_fields 23 | 24 | 25 | DATA_MODEL = { 26 | 'context': [{ 27 | "server_uuid": "1bf91464-9b41-428d-a11e-af691e5563bb", 28 | "server_name": "fake-name", 29 | "server_state": "active", 30 | "server_vcpus": "1", 31 | "server_memory": "512", 32 | "server_disk": "1", 33 | "node_uuid": "253e5dd0-9384-41ab-af13-4f2c2ce26112", 34 | "node_hostname": "localhost.localdomain", 35 | "node_vcpus": "4", 36 | "node_vcpu_ratio": "16.0", 37 | "node_memory": "16383", 38 | "node_memory_ratio": "1.5", 39 | "node_disk": "37", 40 | "node_disk_ratio": "1.0", 41 | "node_state": "up", 42 | }] 43 | } 44 | 45 | LIST_RESULT = [{ 46 | "Server UUID": "1bf91464-9b41-428d-a11e-af691e5563bb", 47 | "Server Name": "fake-name", 48 | "Server Vcpus": "1", 49 | "Server Memory": "512", 50 | "Server Disk": "1", 51 | "Server State": "active", 52 | "Node UUID": "253e5dd0-9384-41ab-af13-4f2c2ce26112", 53 | "Node Host Name": "localhost.localdomain", 54 | "Node Vcpus": "4", 55 | "Node Vcpu Ratio": "16.0", 56 | "Node Memory": "16383", 57 | "Node Memory Ratio": "1.5", 58 | "Node Disk": "37", 59 | "Node Disk Ratio": "1.0", 60 | "Node State": "up", 61 | }] 62 | 63 | SHORT_LIST_RESULT = [{ 64 | "Server UUID": "1bf91464-9b41-428d-a11e-af691e5563bb", 65 | "Server Name": "fake-name", 66 | "Server State": "active", 67 | "Node UUID": "253e5dd0-9384-41ab-af13-4f2c2ce26112", 68 | "Node Host Name": "localhost.localdomain", 69 | }] 70 | 71 | 72 | class DataModelShellTest(base.CommandTestCase): 73 | 74 | SHORT_LIST_FIELDS = resource_fields.COMPUTE_MODEL_SHORT_LIST_FIELDS 75 | SHORT_LIST_FIELD_LABELS = ( 76 | resource_fields.COMPUTE_MODEL_SHORT_LIST_FIELD_LABELS) 77 | FIELDS = resource_fields.COMPUTE_MODEL_LIST_FIELDS 78 | FIELD_LABELS = resource_fields.COMPUTE_MODEL_LIST_FIELD_LABELS 79 | 80 | def setUp(self): 81 | super(self.__class__, self).setUp() 82 | 83 | p_data_model_manager = mock.patch.object( 84 | resource, 'DataModelManager') 85 | 86 | self.m_data_model_mgr_cls = p_data_model_manager.start() 87 | 88 | self.addCleanup(p_data_model_manager.stop) 89 | 90 | self.m_data_model_mgr = mock.Mock() 91 | 92 | self.m_data_model_mgr_cls.return_value = self.m_data_model_mgr 93 | 94 | self.stdout = io.StringIO() 95 | self.cmd = shell.WatcherShell(stdout=self.stdout) 96 | 97 | def test_do_data_model_list(self): 98 | data_model = resource.DataModel(mock.Mock(), DATA_MODEL) 99 | self.m_data_model_mgr.list.return_value = data_model 100 | 101 | exit_code, results = self.run_cmd('datamodel list') 102 | 103 | self.assertEqual(0, exit_code) 104 | expect_values = sorted(SHORT_LIST_RESULT[0].values()) 105 | result_values = sorted(results[0].values()) 106 | self.assertEqual(expect_values, result_values) 107 | 108 | def test_do_data_model_list_detail(self): 109 | data_model = resource.DataModel(mock.Mock(), DATA_MODEL) 110 | self.m_data_model_mgr.list.return_value = data_model 111 | 112 | exit_code, results = self.run_cmd('datamodel list --detail') 113 | 114 | self.assertEqual(0, exit_code) 115 | expect_values = sorted(LIST_RESULT[0].values()) 116 | result_values = sorted(results[0].values()) 117 | self.assertEqual(expect_values, result_values) 118 | 119 | def test_do_data_model_list_filter_by_audit(self): 120 | data_model = resource.DataModel(mock.Mock(), DATA_MODEL) 121 | self.m_data_model_mgr.list.return_value = data_model 122 | 123 | exit_code, results = self.run_cmd( 124 | 'datamodel list --audit ' 125 | '770ef053-ecb3-48b0-85b5-d55a2dbc6588') 126 | 127 | self.assertEqual(0, exit_code) 128 | expect_values = sorted(SHORT_LIST_RESULT[0].values()) 129 | result_values = sorted(results[0].values()) 130 | self.assertEqual(expect_values, result_values) 131 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 OpenStack LLC. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import copy 17 | import io 18 | import os 19 | from unittest import mock 20 | 21 | import fixtures 22 | from oslo_utils import strutils 23 | import testtools 24 | 25 | 26 | class BaseTestCase(testtools.TestCase): 27 | 28 | def setUp(self): 29 | super(BaseTestCase, self).setUp() 30 | self.useFixture(fixtures.FakeLogger()) 31 | 32 | # If enabled, stdout and/or stderr is captured and will appear in 33 | # test results if that test fails. 34 | if strutils.bool_from_string(os.environ.get('OS_STDOUT_CAPTURE')): 35 | stdout = self.useFixture(fixtures.StringStream('stdout')).stream 36 | self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) 37 | if strutils.bool_from_string(os.environ.get('OS_STDERR_CAPTURE')): 38 | stderr = self.useFixture(fixtures.StringStream('stderr')).stream 39 | self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) 40 | 41 | 42 | class FakeAPI(object): 43 | def __init__(self, responses): 44 | self.responses = responses 45 | self.calls = [] 46 | 47 | def _request(self, method, url, headers=None, body=None): 48 | call = (method, url, headers or {}, body) 49 | self.calls.append(call) 50 | return self.responses[url][method] 51 | 52 | def raw_request(self, *args, **kwargs): 53 | response = self._request(*args, **kwargs) 54 | body_iter = iter(io.StringIO(response[1])) 55 | return FakeResponse(response[0]), body_iter 56 | 57 | def json_request(self, *args, **kwargs): 58 | response = self._request(*args, **kwargs) 59 | return FakeResponse(response[0]), response[1] 60 | 61 | 62 | class FakeConnection(object): 63 | def __init__(self, response=None): 64 | self._response = response 65 | self._last_request = None 66 | 67 | def request(self, method, conn_url, **kwargs): 68 | self._last_request = (method, conn_url, kwargs) 69 | 70 | def setresponse(self, response): 71 | self._response = response 72 | 73 | def getresponse(self): 74 | return self._response 75 | 76 | def __repr__(self): 77 | return ("FakeConnection(response=%s)" % (self._response)) 78 | 79 | 80 | class FakeResponse(object): 81 | def __init__(self, headers, body=None, version=None, status=None, 82 | reason=None): 83 | """Fake object to help testing. 84 | 85 | :param headers: dict representing HTTP response headers 86 | :param body: file-like object 87 | """ 88 | self.headers = headers 89 | self.body = body 90 | self.raw = mock.Mock() 91 | self.raw.version = version 92 | self.status_code = status 93 | self.reason = reason 94 | 95 | def getheaders(self): 96 | return copy.deepcopy(self.headers).items() 97 | 98 | def getheader(self, key, default): 99 | return self.headers.get(key, default) 100 | 101 | def read(self, amt): 102 | return self.body.read(amt) 103 | 104 | def __repr__(self): 105 | return ("FakeResponse(%s, body=%s, version=%s, status=%s, reason=%s)" % 106 | (self.headers, self.body, self.version, self.status, 107 | self.reason)) 108 | 109 | 110 | class FakeSessionResponse(object): 111 | 112 | def __init__(self, headers, content=None, status_code=None, version=None): 113 | self.headers = headers 114 | self.content = content 115 | self.status_code = status_code 116 | self.raw = mock.Mock() 117 | self.raw.version = version 118 | self.reason = '' 119 | 120 | def iter_content(self, chunk_size): 121 | return iter(self.content) 122 | 123 | 124 | class FakeSession(object): 125 | 126 | def __init__(self, headers, content=None, status_code=None, version=None): 127 | self.headers = headers 128 | self.content = content 129 | self.status_code = status_code 130 | self.version = version 131 | self.verify = False 132 | self.cert = ('test_cert', 'test_key') 133 | 134 | def request(self, url, method, **kwargs): 135 | request = FakeSessionResponse( 136 | self.headers, self.content, self.status_code, self.version) 137 | return request 138 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/v1/test_scoring_engine_shell.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Intel 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import datetime 17 | import io 18 | from unittest import mock 19 | 20 | from watcherclient import shell 21 | from watcherclient.tests.unit.v1 import base 22 | from watcherclient import v1 as resource 23 | from watcherclient.v1 import resource_fields 24 | 25 | SCORING_ENGINE_1 = { 26 | 'uuid': '5b558998-57ed-11e6-9ca8-08002722cb22', 27 | 'name': 'se-01', 28 | 'description': 'Scoring Engine 0.1', 29 | 'metainfo': '{ "columns": ["cpu", "mem", "pci"] }', 30 | 'created_at': datetime.datetime.now().isoformat(), 31 | 'updated_at': None, 32 | 'deleted_at': None, 33 | } 34 | 35 | SCORING_ENGINE_2 = { 36 | 'uuid': '1f856554-57ee-11e6-ac72-08002722cb22', 37 | 'name': 'se-02', 38 | 'description': 'Some other Scoring Engine', 39 | 'metainfo': 'mode=simplified', 40 | 'created_at': datetime.datetime.now().isoformat(), 41 | 'updated_at': None, 42 | 'deleted_at': None, 43 | } 44 | 45 | 46 | class ScoringEngineShellTest(base.CommandTestCase): 47 | 48 | SHORT_LIST_FIELDS = resource_fields.SCORING_ENGINE_SHORT_LIST_FIELDS 49 | SHORT_LIST_FIELD_LABELS = ( 50 | resource_fields.SCORING_ENGINE_SHORT_LIST_FIELD_LABELS) 51 | FIELDS = resource_fields.SCORING_ENGINE_FIELDS 52 | FIELD_LABELS = resource_fields.SCORING_ENGINE_FIELD_LABELS 53 | 54 | def setUp(self): 55 | super(self.__class__, self).setUp() 56 | 57 | p_se_manager = mock.patch.object( 58 | resource, 'ScoringEngineManager') 59 | self.m_se_mgr_cls = p_se_manager.start() 60 | self.addCleanup(p_se_manager.stop) 61 | 62 | self.m_se_mgr = mock.Mock() 63 | self.m_se_mgr_cls.return_value = self.m_se_mgr 64 | 65 | self.stdout = io.StringIO() 66 | self.cmd = shell.WatcherShell(stdout=self.stdout) 67 | 68 | def test_do_scoringengine_list(self): 69 | se1 = resource.ScoringEngine(mock.Mock(), SCORING_ENGINE_1) 70 | se2 = resource.ScoringEngine(mock.Mock(), SCORING_ENGINE_2) 71 | self.m_se_mgr.list.return_value = [ 72 | se1, se2] 73 | 74 | exit_code, results = self.run_cmd('scoringengine list') 75 | 76 | self.assertEqual(0, exit_code) 77 | self.assertEqual( 78 | [self.resource_as_dict(se1, self.SHORT_LIST_FIELDS, 79 | self.SHORT_LIST_FIELD_LABELS), 80 | self.resource_as_dict(se2, self.SHORT_LIST_FIELDS, 81 | self.SHORT_LIST_FIELD_LABELS)], 82 | results) 83 | 84 | self.m_se_mgr.list.assert_called_once_with(detail=False) 85 | 86 | def test_do_scoringengine_list_detail(self): 87 | se1 = resource.Goal(mock.Mock(), SCORING_ENGINE_1) 88 | se2 = resource.Goal(mock.Mock(), SCORING_ENGINE_2) 89 | self.m_se_mgr.list.return_value = [ 90 | se1, se2] 91 | 92 | exit_code, results = self.run_cmd('scoringengine list --detail') 93 | 94 | self.assertEqual(0, exit_code) 95 | self.assertEqual( 96 | [self.resource_as_dict(se1, self.FIELDS, self.FIELD_LABELS), 97 | self.resource_as_dict(se2, self.FIELDS, self.FIELD_LABELS)], 98 | results) 99 | 100 | self.m_se_mgr.list.assert_called_once_with(detail=True) 101 | 102 | def test_do_scoringengine_show_by_name(self): 103 | scoringengine = resource.Goal(mock.Mock(), SCORING_ENGINE_1) 104 | self.m_se_mgr.get.return_value = scoringengine 105 | 106 | exit_code, result = self.run_cmd('scoringengine show se-01') 107 | 108 | self.assertEqual(0, exit_code) 109 | self.assertEqual( 110 | self.resource_as_dict(scoringengine, self.FIELDS, 111 | self.FIELD_LABELS), 112 | result) 113 | self.m_se_mgr.get.assert_called_once_with('se-01') 114 | 115 | def test_do_scoringengine_show_by_uuid(self): 116 | scoringengine = resource.Goal(mock.Mock(), SCORING_ENGINE_1) 117 | self.m_se_mgr.get.return_value = scoringengine 118 | 119 | exit_code, result = self.run_cmd( 120 | 'scoringengine show 5b558998-57ed-11e6-9ca8-08002722cb22') 121 | 122 | self.assertEqual(0, exit_code) 123 | self.assertEqual( 124 | self.resource_as_dict(scoringengine, self.FIELDS, 125 | self.FIELD_LABELS), 126 | result) 127 | self.m_se_mgr.get.assert_called_once_with( 128 | '5b558998-57ed-11e6-9ca8-08002722cb22') 129 | -------------------------------------------------------------------------------- /watcherclient/common/base.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2012 OpenStack LLC. 3 | # All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """ 18 | Base utilities to build API operation managers and objects on top of. 19 | """ 20 | 21 | import copy 22 | from urllib import parse as urlparse 23 | 24 | from watcherclient.common.apiclient import base 25 | 26 | 27 | def getid(obj): 28 | """Wrapper to get object's ID. 29 | 30 | Abstracts the common pattern of allowing both an object or an 31 | object's ID (UUID) as a parameter when dealing with relationships. 32 | """ 33 | return getattr(obj, 'id', obj) 34 | 35 | 36 | class Manager(object): 37 | """Provides CRUD operations with a particular API.""" 38 | resource_class = None 39 | 40 | def __init__(self, api): 41 | self.api = api 42 | 43 | def _create(self, url, body): 44 | resp, body = self.api.json_request('POST', url, body=body) 45 | if body: 46 | return self.resource_class(self, body) 47 | 48 | def _format_body_data(self, body, response_key): 49 | if response_key: 50 | try: 51 | data = body[response_key] 52 | except KeyError: 53 | return [] 54 | else: 55 | data = body 56 | 57 | if not isinstance(data, list): 58 | data = [data] 59 | 60 | return data 61 | 62 | def _list_pagination(self, url, response_key=None, obj_class=None, 63 | limit=None): 64 | """Retrieve a list of items. 65 | 66 | The Watcher API is configured to return a maximum number of 67 | items per request, (see Watcher's api.max_limit option). This 68 | iterates over the 'next' link (pagination) in the responses, 69 | to get the number of items specified by 'limit'. If 'limit' 70 | is None this function will continue pagination until there are 71 | no more values to be returned. 72 | 73 | :param url: a partial URL, e.g. '/nodes' 74 | :param response_key: the key to be looked up in response 75 | dictionary, e.g. 'nodes' 76 | :param obj_class: class for constructing the returned objects. 77 | :param limit: maximum number of items to return. If None returns 78 | everything. 79 | 80 | """ 81 | if obj_class is None: 82 | obj_class = self.resource_class 83 | 84 | if limit is not None: 85 | limit = int(limit) 86 | 87 | object_list = [] 88 | object_count = 0 89 | limit_reached = False 90 | while url: 91 | resp, body = self.api.json_request('GET', url) 92 | data = self._format_body_data(body, response_key) 93 | for obj in data: 94 | object_list.append(obj_class(self, obj, loaded=True)) 95 | object_count += 1 96 | if limit and object_count >= limit: 97 | # break the for loop 98 | limit_reached = True 99 | break 100 | 101 | # break the while loop and return 102 | if limit_reached: 103 | break 104 | 105 | url = body.get('next') 106 | if url: 107 | # NOTE(lucasagomes): We need to edit the URL to remove 108 | # the scheme and netloc 109 | url_parts = list(urlparse.urlparse(url)) 110 | url_parts[0] = url_parts[1] = '' 111 | url = urlparse.urlunparse(url_parts) 112 | 113 | return object_list 114 | 115 | def _list(self, url, response_key=None, obj_class=None, body=None): 116 | resp, body = self.api.json_request('GET', url) 117 | 118 | if obj_class is None: 119 | obj_class = self.resource_class 120 | 121 | data = self._format_body_data(body, response_key) 122 | return [obj_class(self, res, loaded=True) for res in data if res] 123 | 124 | def _update(self, url, body, method='PATCH', response_key=None): 125 | resp, body = self.api.json_request(method, url, body=body) 126 | # PATCH/PUT requests may not return a body 127 | if body: 128 | return self.resource_class(self, body) 129 | 130 | def _delete(self, url): 131 | self.api.raw_request('DELETE', url) 132 | 133 | def _start(self, url, body=None, method='POST'): 134 | resp, body = self.api.json_request(method, url, body={}) 135 | if body: 136 | return self.resource_class(self, body) 137 | 138 | 139 | class Resource(base.Resource): 140 | """Represents a particular instance of an object (tenant, user, etc). 141 | 142 | This is pretty much just a bag for attributes. 143 | """ 144 | 145 | def to_dict(self): 146 | return copy.deepcopy(self._info) 147 | -------------------------------------------------------------------------------- /watcherclient/tests/client_functional/v1/base.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import os 14 | 15 | import re 16 | import shlex 17 | import subprocess 18 | import testtools 19 | 20 | from tempest.lib.cli import output_parser 21 | from tempest.lib import exceptions 22 | 23 | 24 | def credentials(): 25 | # You can get credentials from OS environment. 26 | creds_dict = { 27 | '--os-username': os.environ.get('OS_USERNAME'), 28 | '--os-password': os.environ.get('OS_PASSWORD'), 29 | '--os-project-name': os.environ.get('OS_PROJECT_NAME'), 30 | '--os-auth-url': os.environ.get('OS_AUTH_URL'), 31 | '--os-project-domain-name': os.environ.get('OS_PROJECT_DOMAIN_ID'), 32 | '--os-user-domain-name': os.environ.get('OS_USER_DOMAIN_ID'), 33 | } 34 | return [x for sub in creds_dict.items() for x in sub] 35 | 36 | 37 | def execute(cmd, fail_ok=False, merge_stderr=True): 38 | """Executes specified command for the given action.""" 39 | cmdlist = shlex.split(cmd) 40 | cmdlist.extend(credentials()) 41 | stdout = subprocess.PIPE 42 | stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE 43 | proc = subprocess.Popen(cmdlist, stdout=stdout, stderr=stderr) 44 | result, result_err = proc.communicate() 45 | result = result.decode('utf-8') 46 | if not fail_ok and proc.returncode != 0: 47 | raise exceptions.CommandFailed(proc.returncode, cmd, result, 48 | result_err) 49 | return result 50 | 51 | 52 | class TestCase(testtools.TestCase): 53 | 54 | delimiter_line = re.compile(r'^\+\-[\+\-]+\-\+$') 55 | 56 | api_version = 1.0 57 | 58 | @classmethod 59 | def watcher(cls, cmd, fail_ok=False): 60 | """Executes watcherclient command for the given action.""" 61 | return execute( 62 | 'openstack optimize --os-infra-optim-api-version {0} {1}'.format( 63 | cls.api_version, cmd), fail_ok=fail_ok) 64 | 65 | @classmethod 66 | def get_opts(cls, fields, format='value'): 67 | return ' -f {0} {1}'.format(format, 68 | ' '.join(['-c ' + it for it in fields])) 69 | 70 | @classmethod 71 | def assertOutput(cls, expected, actual): 72 | if expected != actual: 73 | raise Exception('{0} != {1}'.format(expected, actual)) 74 | 75 | @classmethod 76 | def assertInOutput(cls, expected, actual): 77 | if expected not in actual: 78 | raise Exception('{0} not in {1}'.format(expected, actual)) 79 | 80 | def assert_table_structure(self, items, field_names): 81 | """Verify that all items have keys listed in field_names.""" 82 | for item in items: 83 | for field in field_names: 84 | self.assertIn(field, item) 85 | 86 | def assert_show_fields(self, items, field_names): 87 | """Verify that all items have keys listed in field_names.""" 88 | for item in items: 89 | for key in item.keys(): 90 | self.assertIn(key, field_names) 91 | 92 | def assert_show_structure(self, items, field_names): 93 | """Verify that all field_names listed in keys of all items.""" 94 | if isinstance(items, list): 95 | o = {} 96 | for d in items: 97 | o.update(d) 98 | else: 99 | o = items 100 | item_keys = o.keys() 101 | for field in field_names: 102 | self.assertIn(field, item_keys) 103 | 104 | @staticmethod 105 | def parse_show_as_object(raw_output): 106 | """Return a dict with values parsed from cli output.""" 107 | items = TestCase.parse_show(raw_output) 108 | o = {} 109 | for item in items: 110 | o.update(item) 111 | return o 112 | 113 | @staticmethod 114 | def parse_show(raw_output): 115 | """Return list of dicts with item values parsed from cli output.""" 116 | 117 | items = [] 118 | table_ = output_parser.table(raw_output) 119 | for row in table_['values']: 120 | item = {} 121 | item[row[0]] = row[1] 122 | items.append(item) 123 | return items 124 | 125 | def parse_listing(self, raw_output): 126 | """Return list of dicts with basic item parsed from cli output.""" 127 | return output_parser.listing(raw_output) 128 | 129 | def has_actionplan_succeeded(self, ap_uuid): 130 | return self.parse_show_as_object( 131 | self.watcher('actionplan show %s' % ap_uuid) 132 | )['State'] == 'SUCCEEDED' 133 | 134 | @classmethod 135 | def has_audit_created(cls, audit_uuid): 136 | audit = cls.parse_show_as_object( 137 | cls.watcher('audit show %s' % audit_uuid)) 138 | if audit['Audit Type'] == 'ONESHOT': 139 | return audit['State'] == 'SUCCEEDED' 140 | else: 141 | return audit['State'] == 'ONGOING' 142 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = python-watcherclient 3 | summary = Python client library for Watcher API 4 | description_file = 5 | README.rst 6 | author = OpenStack 7 | author_email = openstack-discuss@lists.openstack.org 8 | home_page = https://docs.openstack.org/python-watcherclient/latest/ 9 | python_requires = >=3.10 10 | classifier = 11 | Environment :: OpenStack 12 | Intended Audience :: Information Technology 13 | Intended Audience :: System Administrators 14 | License :: OSI Approved :: Apache Software License 15 | Operating System :: POSIX :: Linux 16 | Programming Language :: Python 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3.10 19 | Programming Language :: Python :: 3.11 20 | Programming Language :: Python :: 3.12 21 | 22 | [files] 23 | packages = 24 | watcherclient 25 | 26 | [entry_points] 27 | console_scripts = 28 | watcher = watcherclient.shell:main 29 | 30 | openstack.cli.extension = 31 | infra_optim = watcherclient.osc.plugin 32 | 33 | # Entry points for the 'openstack' command 34 | openstack.infra_optim.v1 = 35 | optimize_goal_show = watcherclient.v1.goal_shell:ShowGoal 36 | optimize_goal_list = watcherclient.v1.goal_shell:ListGoal 37 | 38 | optimize_strategy_show = watcherclient.v1.strategy_shell:ShowStrategy 39 | optimize_strategy_list = watcherclient.v1.strategy_shell:ListStrategy 40 | optimize_strategy_state = watcherclient.v1.strategy_shell:StateStrategy 41 | 42 | optimize_audittemplate_show = watcherclient.v1.audit_template_shell:ShowAuditTemplate 43 | optimize_audittemplate_list = watcherclient.v1.audit_template_shell:ListAuditTemplate 44 | optimize_audittemplate_create = watcherclient.v1.audit_template_shell:CreateAuditTemplate 45 | optimize_audittemplate_update = watcherclient.v1.audit_template_shell:UpdateAuditTemplate 46 | optimize_audittemplate_delete = watcherclient.v1.audit_template_shell:DeleteAuditTemplate 47 | 48 | optimize_audit_show = watcherclient.v1.audit_shell:ShowAudit 49 | optimize_audit_list = watcherclient.v1.audit_shell:ListAudit 50 | optimize_audit_create = watcherclient.v1.audit_shell:CreateAudit 51 | optimize_audit_update = watcherclient.v1.audit_shell:UpdateAudit 52 | optimize_audit_delete = watcherclient.v1.audit_shell:DeleteAudit 53 | 54 | optimize_actionplan_show = watcherclient.v1.action_plan_shell:ShowActionPlan 55 | optimize_actionplan_delete = watcherclient.v1.action_plan_shell:DeleteActionPlan 56 | optimize_actionplan_list = watcherclient.v1.action_plan_shell:ListActionPlan 57 | optimize_actionplan_update = watcherclient.v1.action_plan_shell:UpdateActionPlan 58 | optimize_actionplan_start = watcherclient.v1.action_plan_shell:StartActionPlan 59 | optimize_actionplan_cancel = watcherclient.v1.action_plan_shell:CancelActionPlan 60 | 61 | optimize_action_show = watcherclient.v1.action_shell:ShowAction 62 | optimize_action_list = watcherclient.v1.action_shell:ListAction 63 | optimize_action_update = watcherclient.v1.action_shell:UpdateAction 64 | 65 | optimize_scoringengine_show = watcherclient.v1.scoring_engine_shell:ShowScoringEngine 66 | optimize_scoringengine_list = watcherclient.v1.scoring_engine_shell:ListScoringEngine 67 | 68 | optimize_service_show = watcherclient.v1.service_shell:ShowService 69 | optimize_service_list = watcherclient.v1.service_shell:ListService 70 | 71 | optimize_datamodel_list = watcherclient.v1.data_model_shell:ListDataModel 72 | 73 | # The same as above but used by the 'watcher' command 74 | watcherclient.v1 = 75 | goal_show = watcherclient.v1.goal_shell:ShowGoal 76 | goal_list = watcherclient.v1.goal_shell:ListGoal 77 | 78 | strategy_show = watcherclient.v1.strategy_shell:ShowStrategy 79 | strategy_list = watcherclient.v1.strategy_shell:ListStrategy 80 | strategy_state = watcherclient.v1.strategy_shell:StateStrategy 81 | 82 | audittemplate_show = watcherclient.v1.audit_template_shell:ShowAuditTemplate 83 | audittemplate_list = watcherclient.v1.audit_template_shell:ListAuditTemplate 84 | audittemplate_create = watcherclient.v1.audit_template_shell:CreateAuditTemplate 85 | audittemplate_update = watcherclient.v1.audit_template_shell:UpdateAuditTemplate 86 | audittemplate_delete = watcherclient.v1.audit_template_shell:DeleteAuditTemplate 87 | 88 | audit_show = watcherclient.v1.audit_shell:ShowAudit 89 | audit_list = watcherclient.v1.audit_shell:ListAudit 90 | audit_create = watcherclient.v1.audit_shell:CreateAudit 91 | audit_update = watcherclient.v1.audit_shell:UpdateAudit 92 | audit_delete = watcherclient.v1.audit_shell:DeleteAudit 93 | 94 | actionplan_show = watcherclient.v1.action_plan_shell:ShowActionPlan 95 | actionplan_list = watcherclient.v1.action_plan_shell:ListActionPlan 96 | actionplan_update = watcherclient.v1.action_plan_shell:UpdateActionPlan 97 | actionplan_start = watcherclient.v1.action_plan_shell:StartActionPlan 98 | actionplan_delete = watcherclient.v1.action_plan_shell:DeleteActionPlan 99 | actionplan_cancel = watcherclient.v1.action_plan_shell:CancelActionPlan 100 | 101 | action_show = watcherclient.v1.action_shell:ShowAction 102 | action_list = watcherclient.v1.action_shell:ListAction 103 | action_update = watcherclient.v1.action_shell:UpdateAction 104 | 105 | scoringengine_show = watcherclient.v1.scoring_engine_shell:ShowScoringEngine 106 | scoringengine_list = watcherclient.v1.scoring_engine_shell:ListScoringEngine 107 | 108 | service_show = watcherclient.v1.service_shell:ShowService 109 | service_list = watcherclient.v1.service_shell:ListService 110 | 111 | datamodel_list = watcherclient.v1.data_model_shell:ListDataModel 112 | 113 | [pbr] 114 | autodoc_index_modules = True 115 | autodoc_exclude_modules = 116 | watcherclient.tests.* 117 | api_doc_dir = reference/api 118 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/v1/test_service.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Red Hat, Inc. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | 17 | import testtools 18 | from testtools import matchers 19 | 20 | from watcherclient.tests.unit import utils 21 | import watcherclient.v1.service 22 | 23 | SERVICE1 = { 24 | 'id': 1, 25 | 'name': 'watcher-applier', 26 | 'host': 'controller', 27 | 'status': 'ACTIVE', 28 | } 29 | 30 | SERVICE2 = { 31 | 'id': 2, 32 | 'name': 'watcher-decision-engine', 33 | 'host': 'controller', 34 | 'status': 'FAILED', 35 | } 36 | 37 | fake_responses = { 38 | '/v1/services': 39 | { 40 | 'GET': ( 41 | {}, 42 | {"services": [SERVICE1]}, 43 | ), 44 | }, 45 | '/v1/services/detail': 46 | { 47 | 'GET': ( 48 | {}, 49 | {"services": [SERVICE1]}, 50 | ) 51 | }, 52 | '/v1/services/%s' % SERVICE1['id']: 53 | { 54 | 'GET': ( 55 | {}, 56 | SERVICE1, 57 | ), 58 | }, 59 | '/v1/services/%s' % SERVICE1['name']: 60 | { 61 | 'GET': ( 62 | {}, 63 | SERVICE1, 64 | ), 65 | }, 66 | } 67 | 68 | fake_responses_pagination = { 69 | '/v1/services': 70 | { 71 | 'GET': ( 72 | {}, 73 | {"services": [SERVICE1], 74 | "next": "http://127.0.0.1:6385/v1/services/?limit=1"} 75 | ), 76 | }, 77 | '/v1/services/?limit=1': 78 | { 79 | 'GET': ( 80 | {}, 81 | {"services": [SERVICE2]} 82 | ), 83 | }, 84 | } 85 | 86 | fake_responses_sorting = { 87 | '/v1/services/?sort_key=id': 88 | { 89 | 'GET': ( 90 | {}, 91 | {"services": [SERVICE1, SERVICE2]} 92 | ), 93 | }, 94 | '/v1/services/?sort_dir=desc': 95 | { 96 | 'GET': ( 97 | {}, 98 | {"services": [SERVICE2, SERVICE1]} 99 | ), 100 | }, 101 | } 102 | 103 | 104 | class ServiceManagerTest(testtools.TestCase): 105 | 106 | def setUp(self): 107 | super(ServiceManagerTest, self).setUp() 108 | self.api = utils.FakeAPI(fake_responses) 109 | self.mgr = watcherclient.v1.service.ServiceManager(self.api) 110 | 111 | def test_services_list(self): 112 | services = self.mgr.list() 113 | expect = [ 114 | ('GET', '/v1/services', {}, None), 115 | ] 116 | self.assertEqual(expect, self.api.calls) 117 | self.assertEqual(1, len(services)) 118 | 119 | def test_services_list_detail(self): 120 | services = self.mgr.list(detail=True) 121 | expect = [ 122 | ('GET', '/v1/services/detail', {}, None), 123 | ] 124 | self.assertEqual(expect, self.api.calls) 125 | self.assertEqual(1, len(services)) 126 | 127 | def test_services_list_limit(self): 128 | self.api = utils.FakeAPI(fake_responses_pagination) 129 | self.mgr = watcherclient.v1.service.ServiceManager(self.api) 130 | services = self.mgr.list(limit=1) 131 | expect = [ 132 | ('GET', '/v1/services/?limit=1', {}, None), 133 | ] 134 | self.assertEqual(expect, self.api.calls) 135 | self.assertThat(services, matchers.HasLength(1)) 136 | 137 | def test_services_list_pagination_no_limit(self): 138 | self.api = utils.FakeAPI(fake_responses_pagination) 139 | self.mgr = watcherclient.v1.service.ServiceManager(self.api) 140 | services = self.mgr.list(limit=0) 141 | expect = [ 142 | ('GET', '/v1/services', {}, None), 143 | ('GET', '/v1/services/?limit=1', {}, None) 144 | ] 145 | self.assertEqual(expect, self.api.calls) 146 | self.assertThat(services, matchers.HasLength(2)) 147 | 148 | def test_services_list_sort_key(self): 149 | self.api = utils.FakeAPI(fake_responses_sorting) 150 | self.mgr = watcherclient.v1.service.ServiceManager(self.api) 151 | services = self.mgr.list(sort_key='id') 152 | expect = [ 153 | ('GET', '/v1/services/?sort_key=id', {}, None) 154 | ] 155 | self.assertEqual(expect, self.api.calls) 156 | self.assertEqual(2, len(services)) 157 | 158 | def test_services_list_sort_dir(self): 159 | self.api = utils.FakeAPI(fake_responses_sorting) 160 | self.mgr = watcherclient.v1.service.ServiceManager(self.api) 161 | services = self.mgr.list(sort_dir='desc') 162 | expect = [ 163 | ('GET', '/v1/services/?sort_dir=desc', {}, None) 164 | ] 165 | self.assertEqual(expect, self.api.calls) 166 | self.assertEqual(2, len(services)) 167 | 168 | def test_services_show(self): 169 | service = self.mgr.get(SERVICE1['id']) 170 | expect = [ 171 | ('GET', '/v1/services/%s' % SERVICE1['id'], {}, None), 172 | ] 173 | self.assertEqual(expect, self.api.calls) 174 | self.assertEqual(SERVICE1['id'], service.id) 175 | 176 | def test_services_show_by_name(self): 177 | service = self.mgr.get(SERVICE1['name']) 178 | expect = [ 179 | ('GET', '/v1/services/%s' % SERVICE1['name'], {}, None), 180 | ] 181 | self.assertEqual(expect, self.api.calls) 182 | self.assertEqual(SERVICE1['name'], service.name) 183 | -------------------------------------------------------------------------------- /watcherclient/v1/goal_shell.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2013 Red Hat, Inc. 3 | # All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import io 18 | 19 | from osc_lib import utils 20 | 21 | from watcherclient._i18n import _ 22 | from watcherclient.common import command 23 | from watcherclient.common import utils as common_utils 24 | from watcherclient import exceptions 25 | from watcherclient.v1 import resource_fields as res_fields 26 | 27 | 28 | class ShowGoal(command.ShowOne): 29 | """Show detailed information about a given goal.""" 30 | 31 | def get_parser(self, prog_name): 32 | parser = super(ShowGoal, self).get_parser(prog_name) 33 | parser.add_argument( 34 | 'goal', 35 | metavar='', 36 | help=_('UUID or name of the goal'), 37 | ) 38 | return parser 39 | 40 | def _format_indicator_spec_table(self, spec, parsed_args): 41 | out = io.StringIO() 42 | self.formatter.emit_one( 43 | column_names=list(field.capitalize() for field in spec.keys()), 44 | data=utils.get_dict_properties(spec, spec.keys()), 45 | stdout=out, 46 | parsed_args=parsed_args, 47 | ) 48 | return out.getvalue() or '' 49 | 50 | def take_action(self, parsed_args): 51 | client = getattr(self.app.client_manager, "infra-optim") 52 | 53 | try: 54 | goal = client.goal.get(parsed_args.goal) 55 | except exceptions.HTTPNotFound as exc: 56 | raise exceptions.CommandError(str(exc)) 57 | 58 | columns = res_fields.GOAL_FIELDS 59 | column_headers = res_fields.GOAL_FIELD_LABELS 60 | 61 | if parsed_args.formatter == 'table': 62 | indicator_specs = '' 63 | # Format complex data types: 64 | for indicator_spec in goal.efficacy_specification: 65 | indicator_specs += self._format_indicator_spec_table( 66 | indicator_spec, parsed_args) 67 | # Update the raw efficacy specs with the formatted one 68 | goal.efficacy_specification = indicator_specs 69 | 70 | return column_headers, utils.get_item_properties(goal, columns) 71 | 72 | 73 | class ListGoal(command.Lister): 74 | """List information on retrieved goals.""" 75 | 76 | def get_parser(self, prog_name): 77 | parser = super(ListGoal, self).get_parser(prog_name) 78 | parser.add_argument( 79 | '--detail', 80 | dest='detail', 81 | action='store_true', 82 | default=False, 83 | help=_("Show detailed information about each goal.")) 84 | parser.add_argument( 85 | '--limit', 86 | metavar='', 87 | type=int, 88 | help=_('Maximum number of goals to return per request, ' 89 | '0 for no limit. Default is the maximum number used ' 90 | 'by the Watcher API Service.')) 91 | parser.add_argument( 92 | '--sort-key', 93 | metavar='', 94 | help=_('Goal field that will be used for sorting.')) 95 | parser.add_argument( 96 | '--sort-dir', 97 | metavar='', 98 | choices=['asc', 'desc'], 99 | help=_('Sort direction: "asc" (the default) or "desc".')) 100 | parser.add_argument( 101 | '--marker', 102 | dest='marker', 103 | metavar='', 104 | default=None, 105 | help=_('UUID of the last goal in the previous page; ' 106 | 'displays list of goals after "marker".')) 107 | 108 | return parser 109 | 110 | def _format_indicator_spec_table(self, goal, parsed_args): 111 | out = io.StringIO() 112 | efficacy_specification = goal.efficacy_specification 113 | fields = ['name', 'unit'] 114 | self.formatter.emit_list( 115 | column_names=list(field.capitalize() 116 | for field in fields), 117 | data=[utils.get_dict_properties(spec, fields) 118 | for spec in efficacy_specification], 119 | stdout=out, 120 | parsed_args=parsed_args, 121 | ) 122 | return out.getvalue() or '' 123 | 124 | def take_action(self, parsed_args): 125 | client = getattr(self.app.client_manager, "infra-optim") 126 | 127 | if parsed_args.detail: 128 | fields = res_fields.GOAL_FIELDS 129 | field_labels = res_fields.GOAL_FIELD_LABELS 130 | else: 131 | fields = res_fields.GOAL_SHORT_LIST_FIELDS 132 | field_labels = res_fields.GOAL_SHORT_LIST_FIELD_LABELS 133 | 134 | params = {} 135 | params.update( 136 | common_utils.common_params_for_list( 137 | parsed_args, fields, field_labels)) 138 | 139 | try: 140 | data = client.goal.list(**params) 141 | except exceptions.HTTPNotFound as ex: 142 | raise exceptions.CommandError(str(ex)) 143 | 144 | if parsed_args.formatter == 'table': 145 | for goal in data: 146 | # Update the raw efficacy specs with the formatted one 147 | goal.efficacy_specification = ( 148 | self._format_indicator_spec_table(goal, parsed_args)) 149 | 150 | return (field_labels, 151 | (utils.get_item_properties(item, fields) for item in data)) 152 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/v1/test_goal_shell.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 b<>com 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import datetime 17 | import io 18 | from unittest import mock 19 | 20 | from watcherclient import shell 21 | from watcherclient.tests.unit.v1 import base 22 | from watcherclient import v1 as resource 23 | from watcherclient.v1 import resource_fields 24 | 25 | GOAL_1 = { 26 | 'uuid': "fc087747-61be-4aad-8126-b701731ae836", 27 | 'name': "SERVER_CONSOLIDATION", 28 | 'display_name': 'Server Consolidation', 29 | 'efficacy_specification': [ 30 | {'description': 'Indicator 1', 'name': 'indicator1', 31 | 'schema': 'Range(min=0, max=100, min_included=True, ' 32 | 'max_included=True, msg=None)', 33 | 'unit': '%'} 34 | ], 35 | 'created_at': datetime.datetime.now().isoformat(), 36 | 'updated_at': None, 37 | 'deleted_at': None, 38 | } 39 | 40 | GOAL_2 = { 41 | 'uuid': "407b03b1-63c6-49b2-adaf-4df5c0090047", 42 | 'name': "COST_OPTIMIZATION", 43 | 'display_name': 'Cost Optimization', 44 | 'efficacy_specification': [ 45 | {'description': 'Indicator 2', 'name': 'indicator2', 46 | 'schema': 'Range(min=0, max=100, min_included=True, ' 47 | 'max_included=True, msg=None)', 48 | 'unit': '%'} 49 | ], 50 | 'created_at': datetime.datetime.now().isoformat(), 51 | 'updated_at': None, 52 | 'deleted_at': None, 53 | } 54 | 55 | 56 | class GoalShellTest(base.CommandTestCase): 57 | 58 | SHORT_LIST_FIELDS = resource_fields.GOAL_SHORT_LIST_FIELDS 59 | SHORT_LIST_FIELD_LABELS = ( 60 | resource_fields.GOAL_SHORT_LIST_FIELD_LABELS) 61 | FIELDS = resource_fields.GOAL_FIELDS 62 | FIELD_LABELS = resource_fields.GOAL_FIELD_LABELS 63 | 64 | def setUp(self): 65 | super(self.__class__, self).setUp() 66 | 67 | p_goal_manager = mock.patch.object( 68 | resource, 'GoalManager') 69 | self.m_goal_mgr_cls = p_goal_manager.start() 70 | self.addCleanup(p_goal_manager.stop) 71 | 72 | self.m_goal_mgr = mock.Mock() 73 | self.m_goal_mgr_cls.return_value = self.m_goal_mgr 74 | 75 | self.stdout = io.StringIO() 76 | self.cmd = shell.WatcherShell(stdout=self.stdout) 77 | 78 | def test_do_goal_list(self): 79 | goal1 = resource.Goal(mock.Mock(), GOAL_1) 80 | goal2 = resource.Goal(mock.Mock(), GOAL_2) 81 | self.m_goal_mgr.list.return_value = [ 82 | goal1, goal2] 83 | 84 | exit_code, results = self.run_cmd('goal list') 85 | 86 | self.assertEqual(0, exit_code) 87 | self.assertEqual( 88 | [self.resource_as_dict(goal1, self.SHORT_LIST_FIELDS, 89 | self.SHORT_LIST_FIELD_LABELS), 90 | self.resource_as_dict(goal2, self.SHORT_LIST_FIELDS, 91 | self.SHORT_LIST_FIELD_LABELS)], 92 | results) 93 | 94 | self.m_goal_mgr.list.assert_called_once_with(detail=False) 95 | 96 | def test_do_goal_list_marker(self): 97 | goal2 = resource.Goal(mock.Mock(), GOAL_2) 98 | self.m_goal_mgr.list.return_value = [goal2] 99 | 100 | exit_code, results = self.run_cmd( 101 | 'goal list --marker fc087747-61be-4aad-8126-b701731ae836') 102 | 103 | self.assertEqual(0, exit_code) 104 | self.assertEqual( 105 | [self.resource_as_dict(goal2, self.SHORT_LIST_FIELDS, 106 | self.SHORT_LIST_FIELD_LABELS)], 107 | results) 108 | 109 | self.m_goal_mgr.list.assert_called_once_with( 110 | detail=False, 111 | marker='fc087747-61be-4aad-8126-b701731ae836') 112 | 113 | def test_do_goal_list_detail(self): 114 | goal1 = resource.Goal(mock.Mock(), GOAL_1) 115 | goal2 = resource.Goal(mock.Mock(), GOAL_2) 116 | self.m_goal_mgr.list.return_value = [ 117 | goal1, goal2] 118 | 119 | exit_code, results = self.run_cmd('goal list --detail') 120 | 121 | self.assertEqual(0, exit_code) 122 | self.assertEqual( 123 | [self.resource_as_dict(goal1, self.FIELDS, self.FIELD_LABELS), 124 | self.resource_as_dict(goal2, self.FIELDS, self.FIELD_LABELS)], 125 | results) 126 | 127 | self.m_goal_mgr.list.assert_called_once_with(detail=True) 128 | 129 | def test_do_goal_show_by_name(self): 130 | goal = resource.Goal(mock.Mock(), GOAL_1) 131 | self.m_goal_mgr.get.return_value = goal 132 | 133 | exit_code, result = self.run_cmd('goal show SERVER_CONSOLIDATION') 134 | 135 | self.assertEqual(0, exit_code) 136 | self.assertEqual( 137 | self.resource_as_dict(goal, self.FIELDS, 138 | self.FIELD_LABELS), 139 | result) 140 | self.m_goal_mgr.get.assert_called_once_with('SERVER_CONSOLIDATION') 141 | 142 | def test_do_goal_show_by_uuid(self): 143 | goal = resource.Goal(mock.Mock(), GOAL_1) 144 | self.m_goal_mgr.get.return_value = goal 145 | 146 | exit_code, result = self.run_cmd( 147 | 'goal show fc087747-61be-4aad-8126-b701731ae836') 148 | 149 | self.assertEqual(0, exit_code) 150 | self.assertEqual( 151 | self.resource_as_dict(goal, self.FIELDS, 152 | self.FIELD_LABELS), 153 | result) 154 | self.m_goal_mgr.get.assert_called_once_with( 155 | 'fc087747-61be-4aad-8126-b701731ae836') 156 | -------------------------------------------------------------------------------- /watcherclient/v1/strategy_shell.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 b<>com 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from osc_lib import utils 17 | from oslo_serialization import jsonutils 18 | 19 | from watcherclient._i18n import _ 20 | from watcherclient.common import command 21 | from watcherclient.common import utils as common_utils 22 | from watcherclient import exceptions 23 | from watcherclient.v1 import resource_fields as res_fields 24 | 25 | 26 | class ShowStrategy(command.ShowOne): 27 | """Show detailed information about a given strategy.""" 28 | 29 | def get_parser(self, prog_name): 30 | parser = super(ShowStrategy, self).get_parser(prog_name) 31 | parser.add_argument( 32 | 'strategy', 33 | metavar='', 34 | help=_('UUID or name of the strategy'), 35 | ) 36 | return parser 37 | 38 | def _format_spec(self, strategy): 39 | parameters_spec = strategy.parameters_spec.get('properties') 40 | if parameters_spec: 41 | return jsonutils.dumps(parameters_spec, indent=2) 42 | 43 | return {} 44 | 45 | def take_action(self, parsed_args): 46 | client = getattr(self.app.client_manager, "infra-optim") 47 | 48 | try: 49 | strategy = client.strategy.get(parsed_args.strategy) 50 | except exceptions.HTTPNotFound as exc: 51 | raise exceptions.CommandError(str(exc)) 52 | 53 | strategy.parameters_spec = self._format_spec(strategy) 54 | columns = res_fields.STRATEGY_FIELDS 55 | column_headers = res_fields.STRATEGY_FIELD_LABELS 56 | 57 | return column_headers, utils.get_item_properties(strategy, columns) 58 | 59 | 60 | class StateStrategy(command.Lister): 61 | """Retrieve information about strategy requirements.""" 62 | 63 | def get_parser(self, prog_name): 64 | parser = super(StateStrategy, self).get_parser(prog_name) 65 | parser.add_argument( 66 | 'strategy', 67 | metavar='', 68 | help=_('Name of the strategy'), 69 | ) 70 | return parser 71 | 72 | def _format_spec(self, requirements): 73 | for req in requirements: 74 | if isinstance(req.state, list): 75 | req.state = jsonutils.dumps(req.state, indent=2) 76 | return requirements 77 | 78 | def take_action(self, parsed_args): 79 | client = getattr(self.app.client_manager, "infra-optim") 80 | 81 | try: 82 | requirements = client.strategy.state(parsed_args.strategy) 83 | except exceptions.HTTPNotFound as exc: 84 | raise exceptions.CommandError(str(exc)) 85 | requirements = self._format_spec(requirements) 86 | columns = res_fields.STRATEGY_STATE_FIELDS 87 | column_headers = res_fields.STRATEGY_STATE_FIELD_LABELS 88 | 89 | return (column_headers, 90 | (utils.get_item_properties(item, columns) 91 | for item in requirements)) 92 | 93 | 94 | class ListStrategy(command.Lister): 95 | """List information on retrieved strategies.""" 96 | 97 | def get_parser(self, prog_name): 98 | parser = super(ListStrategy, self).get_parser(prog_name) 99 | parser.add_argument( 100 | '--goal', 101 | metavar='', 102 | dest='goal', 103 | help=_('UUID or name of the goal')) 104 | parser.add_argument( 105 | '--detail', 106 | dest='detail', 107 | action='store_true', 108 | default=False, 109 | help=_("Show detailed information about each strategy.")) 110 | parser.add_argument( 111 | '--limit', 112 | metavar='', 113 | type=int, 114 | help=_('Maximum number of strategies to return per request, ' 115 | '0 for no limit. Default is the maximum number used ' 116 | 'by the Watcher API Service.')) 117 | parser.add_argument( 118 | '--sort-key', 119 | metavar='', 120 | help=_('Goal field that will be used for sorting.')) 121 | parser.add_argument( 122 | '--sort-dir', 123 | metavar='', 124 | choices=['asc', 'desc'], 125 | help='Sort direction: "asc" (the default) or "desc".') 126 | parser.add_argument( 127 | '--marker', 128 | dest='marker', 129 | metavar='', 130 | default=None, 131 | help=_('UUID of the last strategy in the previous page; ' 132 | 'displays list of strategies after "marker".')) 133 | return parser 134 | 135 | def take_action(self, parsed_args): 136 | client = getattr(self.app.client_manager, "infra-optim") 137 | 138 | params = {} 139 | if parsed_args.detail: 140 | fields = res_fields.STRATEGY_FIELDS 141 | field_labels = res_fields.STRATEGY_FIELD_LABELS 142 | else: 143 | fields = res_fields.STRATEGY_SHORT_LIST_FIELDS 144 | field_labels = res_fields.STRATEGY_SHORT_LIST_FIELD_LABELS 145 | 146 | if parsed_args.goal: 147 | params["goal"] = parsed_args.goal 148 | 149 | params.update( 150 | common_utils.common_params_for_list( 151 | parsed_args, fields, field_labels)) 152 | 153 | try: 154 | data = client.strategy.list(**params) 155 | except exceptions.HTTPNotFound as ex: 156 | raise exceptions.CommandError(str(ex)) 157 | 158 | return (field_labels, 159 | (utils.get_item_properties(item, fields) for item in data)) 160 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/v1/test_scoring_engine.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2016 Intel 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | 17 | import testtools 18 | from testtools import matchers 19 | 20 | from watcherclient.tests.unit import utils 21 | import watcherclient.v1.scoring_engine 22 | 23 | SE1 = { 24 | 'uuid': '5b558998-57ed-11e6-9ca8-08002722cb22', 25 | 'name': 'se-01', 26 | 'description': 'Some Scoring Engine' 27 | } 28 | 29 | SE2 = { 30 | 'uuid': '1f856554-57ee-11e6-ac72-08002722cb22', 31 | 'name': 'se-02', 32 | 'description': 'Some Other Scoring Engine' 33 | } 34 | 35 | fake_responses = { 36 | '/v1/scoring_engines': 37 | { 38 | 'GET': ( 39 | {}, 40 | {"scoring_engines": [SE1]}, 41 | ), 42 | }, 43 | '/v1/scoring_engines/detail': 44 | { 45 | 'GET': ( 46 | {}, 47 | {"scoring_engines": [SE1]}, 48 | ) 49 | }, 50 | '/v1/scoring_engines/%s' % SE1['uuid']: 51 | { 52 | 'GET': ( 53 | {}, 54 | SE1, 55 | ), 56 | }, 57 | '/v1/scoring_engines/%s' % SE1['name']: 58 | { 59 | 'GET': ( 60 | {}, 61 | SE1, 62 | ), 63 | }, 64 | } 65 | 66 | fake_responses_pagination = { 67 | '/v1/scoring_engines': 68 | { 69 | 'GET': ( 70 | {}, 71 | {"scoring_engines": [SE1], 72 | "next": "http://127.0.0.1:9322/v1/scoring_engines/?limit=1"} 73 | ), 74 | }, 75 | '/v1/scoring_engines/?limit=1': 76 | { 77 | 'GET': ( 78 | {}, 79 | {"scoring_engines": [SE2]} 80 | ), 81 | }, 82 | } 83 | 84 | fake_responses_sorting = { 85 | '/v1/scoring_engines/?sort_key=id': 86 | { 87 | 'GET': ( 88 | {}, 89 | {"scoring_engines": [SE1, SE2]} 90 | ), 91 | }, 92 | '/v1/scoring_engines/?sort_dir=desc': 93 | { 94 | 'GET': ( 95 | {}, 96 | {"scoring_engines": [SE2, SE1]} 97 | ), 98 | }, 99 | } 100 | 101 | 102 | class ScoringEngineManagerTest(testtools.TestCase): 103 | 104 | def setUp(self): 105 | super(ScoringEngineManagerTest, self).setUp() 106 | self.api = utils.FakeAPI(fake_responses) 107 | self.mgr = \ 108 | watcherclient.v1.scoring_engine.ScoringEngineManager(self.api) 109 | 110 | def test_scoring_engines_list(self): 111 | scoring_engines = self.mgr.list() 112 | expect = [ 113 | ('GET', '/v1/scoring_engines', {}, None), 114 | ] 115 | self.assertEqual(expect, self.api.calls) 116 | self.assertEqual(1, len(scoring_engines)) 117 | 118 | def test_scoring_engines_list_detail(self): 119 | scoring_engines = self.mgr.list(detail=True) 120 | expect = [ 121 | ('GET', '/v1/scoring_engines/detail', {}, None), 122 | ] 123 | self.assertEqual(expect, self.api.calls) 124 | self.assertEqual(1, len(scoring_engines)) 125 | 126 | def test_scoring_engines_list_limit(self): 127 | self.api = utils.FakeAPI(fake_responses_pagination) 128 | self.mgr = \ 129 | watcherclient.v1.scoring_engine.ScoringEngineManager(self.api) 130 | scoring_engines = self.mgr.list(limit=1) 131 | expect = [ 132 | ('GET', '/v1/scoring_engines/?limit=1', {}, None), 133 | ] 134 | self.assertEqual(expect, self.api.calls) 135 | self.assertThat(scoring_engines, matchers.HasLength(1)) 136 | 137 | def test_scoring_engines_list_pagination_no_limit(self): 138 | self.api = utils.FakeAPI(fake_responses_pagination) 139 | self.mgr = \ 140 | watcherclient.v1.scoring_engine.ScoringEngineManager(self.api) 141 | scoring_engines = self.mgr.list(limit=0) 142 | expect = [ 143 | ('GET', '/v1/scoring_engines', {}, None), 144 | ('GET', '/v1/scoring_engines/?limit=1', {}, None) 145 | ] 146 | self.assertEqual(expect, self.api.calls) 147 | self.assertThat(scoring_engines, matchers.HasLength(2)) 148 | 149 | def test_scoring_engines_list_sort_key(self): 150 | self.api = utils.FakeAPI(fake_responses_sorting) 151 | self.mgr = \ 152 | watcherclient.v1.scoring_engine.ScoringEngineManager(self.api) 153 | scoring_engines = self.mgr.list(sort_key='id') 154 | expect = [ 155 | ('GET', '/v1/scoring_engines/?sort_key=id', {}, None) 156 | ] 157 | self.assertEqual(expect, self.api.calls) 158 | self.assertEqual(2, len(scoring_engines)) 159 | 160 | def test_scoring_engines_list_sort_dir(self): 161 | self.api = utils.FakeAPI(fake_responses_sorting) 162 | self.mgr = \ 163 | watcherclient.v1.scoring_engine.ScoringEngineManager(self.api) 164 | scoring_engines = self.mgr.list(sort_dir='desc') 165 | expect = [ 166 | ('GET', '/v1/scoring_engines/?sort_dir=desc', {}, None) 167 | ] 168 | self.assertEqual(expect, self.api.calls) 169 | self.assertEqual(2, len(scoring_engines)) 170 | 171 | def test_scoring_engines_show(self): 172 | scoring_engine = self.mgr.get(SE1['uuid']) 173 | expect = [ 174 | ('GET', '/v1/scoring_engines/%s' % SE1['uuid'], {}, None), 175 | ] 176 | self.assertEqual(expect, self.api.calls) 177 | self.assertEqual(SE1['uuid'], scoring_engine.uuid) 178 | 179 | def test_scoring_engines_show_by_name(self): 180 | scoring_engine = self.mgr.get(SE1['name']) 181 | expect = [ 182 | ('GET', '/v1/scoring_engines/%s' % SE1['name'], {}, None), 183 | ] 184 | self.assertEqual(expect, self.api.calls) 185 | self.assertEqual(SE1['name'], scoring_engine.name) 186 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/v1/test_goal.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Red Hat, Inc. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | 17 | import testtools 18 | from testtools import matchers 19 | 20 | from watcherclient.tests.unit import utils 21 | import watcherclient.v1.goal 22 | 23 | GOAL1 = { 24 | 'uuid': "fc087747-61be-4aad-8126-b701731ae836", 25 | 'name': "SERVER_CONSOLIDATION", 26 | 'display_name': 'Server Consolidation' 27 | } 28 | 29 | GOAL2 = { 30 | 'uuid': "407b03b1-63c6-49b2-adaf-4df5c0090047", 31 | 'name': "COST_OPTIMIZATION", 32 | 'display_name': 'Cost Optimization' 33 | } 34 | 35 | fake_responses = { 36 | '/v1/goals': 37 | { 38 | 'GET': ( 39 | {}, 40 | {"goals": [GOAL1]}, 41 | ), 42 | }, 43 | '/v1/goals/detail': 44 | { 45 | 'GET': ( 46 | {}, 47 | {"goals": [GOAL1]}, 48 | ) 49 | }, 50 | '/v1/goals/%s' % GOAL1['uuid']: 51 | { 52 | 'GET': ( 53 | {}, 54 | GOAL1, 55 | ), 56 | }, 57 | '/v1/goals/%s' % GOAL1['name']: 58 | { 59 | 'GET': ( 60 | {}, 61 | GOAL1, 62 | ), 63 | }, 64 | } 65 | 66 | fake_responses_pagination = { 67 | '/v1/goals': 68 | { 69 | 'GET': ( 70 | {}, 71 | {"goals": [GOAL1], 72 | "next": "http://127.0.0.1:9322/v1/goals/?limit=1"} 73 | ), 74 | }, 75 | '/v1/goals/?limit=1': 76 | { 77 | 'GET': ( 78 | {}, 79 | {"goals": [GOAL2]} 80 | ), 81 | }, 82 | } 83 | 84 | fake_responses_sorting = { 85 | '/v1/goals/?sort_key=id': 86 | { 87 | 'GET': ( 88 | {}, 89 | {"goals": [GOAL1, GOAL2]} 90 | ), 91 | }, 92 | '/v1/goals/?sort_dir=desc': 93 | { 94 | 'GET': ( 95 | {}, 96 | {"goals": [GOAL2, GOAL1]} 97 | ), 98 | }, 99 | } 100 | 101 | fake_responses_marker = { 102 | '/v1/goals/?marker=fc087747-61be-4aad-8126-b701731ae836': 103 | { 104 | 'GET': ( 105 | {}, 106 | {"goals": [GOAL2]} 107 | ), 108 | }, 109 | } 110 | 111 | 112 | class GoalManagerTest(testtools.TestCase): 113 | 114 | def setUp(self): 115 | super(GoalManagerTest, self).setUp() 116 | self.api = utils.FakeAPI(fake_responses) 117 | self.mgr = watcherclient.v1.goal.GoalManager(self.api) 118 | 119 | def test_goals_list(self): 120 | goals = self.mgr.list() 121 | expect = [ 122 | ('GET', '/v1/goals', {}, None), 123 | ] 124 | self.assertEqual(expect, self.api.calls) 125 | self.assertEqual(1, len(goals)) 126 | 127 | def test_goals_list_detail(self): 128 | goals = self.mgr.list(detail=True) 129 | expect = [ 130 | ('GET', '/v1/goals/detail', {}, None), 131 | ] 132 | self.assertEqual(expect, self.api.calls) 133 | self.assertEqual(1, len(goals)) 134 | 135 | def test_goals_list_limit(self): 136 | self.api = utils.FakeAPI(fake_responses_pagination) 137 | self.mgr = watcherclient.v1.goal.GoalManager(self.api) 138 | goals = self.mgr.list(limit=1) 139 | expect = [ 140 | ('GET', '/v1/goals/?limit=1', {}, None), 141 | ] 142 | self.assertEqual(expect, self.api.calls) 143 | self.assertThat(goals, matchers.HasLength(1)) 144 | 145 | def test_goals_list_marker(self): 146 | self.api = utils.FakeAPI(fake_responses_marker) 147 | self.mgr = watcherclient.v1.goal.GoalManager(self.api) 148 | goals = self.mgr.list(marker=GOAL1['uuid']) 149 | expect = [ 150 | ('GET', '/v1/goals/?marker=fc087747-61be-4aad-8126-b701731ae836', 151 | {}, None), 152 | ] 153 | self.assertEqual(expect, self.api.calls) 154 | self.assertEqual(1, len(goals)) 155 | 156 | def test_goals_list_pagination_no_limit(self): 157 | self.api = utils.FakeAPI(fake_responses_pagination) 158 | self.mgr = watcherclient.v1.goal.GoalManager(self.api) 159 | goals = self.mgr.list(limit=0) 160 | expect = [ 161 | ('GET', '/v1/goals', {}, None), 162 | ('GET', '/v1/goals/?limit=1', {}, None) 163 | ] 164 | self.assertEqual(expect, self.api.calls) 165 | self.assertThat(goals, matchers.HasLength(2)) 166 | 167 | def test_goals_list_sort_key(self): 168 | self.api = utils.FakeAPI(fake_responses_sorting) 169 | self.mgr = watcherclient.v1.goal.GoalManager(self.api) 170 | goals = self.mgr.list(sort_key='id') 171 | expect = [ 172 | ('GET', '/v1/goals/?sort_key=id', {}, None) 173 | ] 174 | self.assertEqual(expect, self.api.calls) 175 | self.assertEqual(2, len(goals)) 176 | 177 | def test_goals_list_sort_dir(self): 178 | self.api = utils.FakeAPI(fake_responses_sorting) 179 | self.mgr = watcherclient.v1.goal.GoalManager(self.api) 180 | goals = self.mgr.list(sort_dir='desc') 181 | expect = [ 182 | ('GET', '/v1/goals/?sort_dir=desc', {}, None) 183 | ] 184 | self.assertEqual(expect, self.api.calls) 185 | self.assertEqual(2, len(goals)) 186 | 187 | def test_goals_show(self): 188 | goal = self.mgr.get(GOAL1['uuid']) 189 | expect = [ 190 | ('GET', '/v1/goals/%s' % GOAL1['uuid'], {}, None), 191 | ] 192 | self.assertEqual(expect, self.api.calls) 193 | self.assertEqual(GOAL1['uuid'], goal.uuid) 194 | 195 | def test_goals_show_by_name(self): 196 | goal = self.mgr.get(GOAL1['name']) 197 | expect = [ 198 | ('GET', '/v1/goals/%s' % GOAL1['name'], {}, None), 199 | ] 200 | self.assertEqual(expect, self.api.calls) 201 | self.assertEqual(GOAL1['name'], goal.name) 202 | -------------------------------------------------------------------------------- /watcherclient/v1/resource_fields.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 b<>com 3 | # All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | 18 | # Audit Template 19 | AUDIT_TEMPLATE_FIELDS = [ 20 | 'uuid', 'created_at', 'updated_at', 'deleted_at', 21 | 'description', 'name', 'goal_name', 'strategy_name', 'scope'] 22 | 23 | AUDIT_TEMPLATE_FIELD_LABELS = [ 24 | 'UUID', 'Created At', 'Updated At', 'Deleted At', 25 | 'Description', 'Name', 'Goal', 'Strategy', 'Audit Scope'] 26 | 27 | AUDIT_TEMPLATE_SHORT_LIST_FIELDS = [ 28 | 'uuid', 'name', 'goal_name', 'strategy_name'] 29 | 30 | AUDIT_TEMPLATE_SHORT_LIST_FIELD_LABELS = ['UUID', 'Name', 'Goal', 'Strategy'] 31 | 32 | # Audit 33 | AUDIT_FIELDS = ['uuid', 'name', 'created_at', 'updated_at', 'deleted_at', 34 | 'state', 'audit_type', 'parameters', 'interval', 'goal_name', 35 | 'strategy_name', 'scope', 'auto_trigger', 'next_run_time', 36 | 'hostname', 'start_time', 'end_time', 'force', 37 | 'status_message'] 38 | 39 | AUDIT_FIELD_LABELS = ['UUID', 'Name', 'Created At', 'Updated At', 'Deleted At', 40 | 'State', 'Audit Type', 'Parameters', 'Interval', 'Goal', 41 | 'Strategy', 'Audit Scope', 'Auto Trigger', 42 | 'Next Run Time', 'Hostname', 'Start Time', 'End Time', 43 | 'Force', 'Status Message'] 44 | 45 | AUDIT_SHORT_LIST_FIELDS = ['uuid', 'name', 'audit_type', 46 | 'state', 'goal_name', 'strategy_name', 47 | 'auto_trigger'] 48 | 49 | AUDIT_SHORT_LIST_FIELD_LABELS = ['UUID', 'Name', 'Audit Type', 'State', 'Goal', 50 | 'Strategy', 'Auto Trigger'] 51 | 52 | # Action Plan 53 | ACTION_PLAN_FIELDS = ['uuid', 'created_at', 'updated_at', 'deleted_at', 54 | 'audit_uuid', 'strategy_name', 'state', 55 | 'efficacy_indicators', 'global_efficacy', 'hostname', 56 | 'status_message'] 57 | 58 | ACTION_PLAN_FIELD_LABELS = ['UUID', 'Created At', 'Updated At', 'Deleted At', 59 | 'Audit', 'Strategy', 'State', 60 | 'Efficacy indicators', 'Global efficacy', 61 | 'Hostname', 'Status Message'] 62 | 63 | ACTION_PLAN_SHORT_LIST_FIELDS = ['uuid', 'audit_uuid', 'state', 64 | 'updated_at', 'global_efficacy'] 65 | 66 | ACTION_PLAN_SHORT_LIST_FIELD_LABELS = ['UUID', 'Audit', 'State', 67 | 'Updated At', 'Global efficacy'] 68 | 69 | GLOBAL_EFFICACY_FIELDS = ['value', 'unit', 'name', 'description'] 70 | 71 | # Action 72 | ACTION_FIELDS = ['uuid', 'created_at', 'updated_at', 'deleted_at', 'parents', 73 | 'state', 'action_plan_uuid', 'action_type', 74 | 'input_parameters', 'description', 'status_message'] 75 | 76 | ACTION_FIELD_LABELS = ['UUID', 'Created At', 'Updated At', 'Deleted At', 77 | 'Parents', 'State', 'Action Plan', 'Action', 78 | 'Parameters', 'Description', 'Status Message'] 79 | 80 | ACTION_SHORT_LIST_FIELDS = ['uuid', 'parents', 81 | 'state', 'action_plan_uuid', 'action_type'] 82 | 83 | ACTION_SHORT_LIST_FIELD_LABELS = ['UUID', 'Parents', 'State', 84 | 'Action Plan', 'Action'] 85 | # Goals 86 | 87 | GOAL_FIELDS = ['uuid', 'name', 'display_name', 'efficacy_specification'] 88 | 89 | GOAL_FIELD_LABELS = ['UUID', 'Name', 'Display name', 'Efficacy specification'] 90 | 91 | GOAL_SHORT_LIST_FIELDS = ['uuid', 'name', 'display_name'] 92 | 93 | GOAL_SHORT_LIST_FIELD_LABELS = ['UUID', 'Name', 'Display name'] 94 | 95 | # Strategies 96 | 97 | STRATEGY_FIELDS = ['uuid', 'name', 'display_name', 'goal_name', 98 | 'parameters_spec'] 99 | 100 | STRATEGY_FIELD_LABELS = ['UUID', 'Name', 'Display name', 'Goal', 101 | 'Parameters spec'] 102 | 103 | # Data Model 104 | 105 | COMPUTE_MODEL_LIST_FIELDS = [ 106 | 'server_uuid', 'server_name', 'server_vcpus', 107 | 'server_memory', 'server_disk', 'server_state', 'node_uuid', 108 | 'node_hostname', 'node_vcpus', 'node_vcpu_ratio', 'node_memory', 109 | 'node_memory_ratio', 'node_disk', 'node_disk_ratio', 'node_state'] 110 | 111 | COMPUTE_MODEL_LIST_FIELD_LABELS = [ 112 | 'Server_UUID', 'Server Name', 'Server Vcpus', 113 | 'Server Memory', 'Server Disk', 'Server State', 'Node UUID', 114 | 'Node Host Name', 'Node Vcpus', 'Node Vcpu Ratio', 'Node Memory', 115 | 'Node Memory Ratio', 'Node Disk', 'Node Disk Ratio', 'Node State'] 116 | 117 | COMPUTE_MODEL_SHORT_LIST_FIELDS = [ 118 | 'server_uuid', 'server_name', 119 | 'server_state', 'node_uuid', 'node_hostname'] 120 | 121 | COMPUTE_MODEL_SHORT_LIST_FIELD_LABELS = [ 122 | 'Server UUID', 'Server Name', 123 | 'Server State', 'Node UUID', 'Node Host Name'] 124 | 125 | STRATEGY_SHORT_LIST_FIELDS = ['uuid', 'name', 'display_name', 'goal_name'] 126 | 127 | STRATEGY_SHORT_LIST_FIELD_LABELS = ['UUID', 'Name', 'Display name', 'Goal'] 128 | 129 | STRATEGY_STATE_FIELDS = ['type', 'state', 'mandatory', 'comment'] 130 | 131 | STRATEGY_STATE_FIELD_LABELS = ['Type', 'State', 'Mandatory', 'Comment'] 132 | 133 | # Metric Collector 134 | METRIC_COLLECTOR_FIELDS = ['uuid', 'created_at', 'updated_at', 'deleted_at', 135 | 'endpoint', 'category'] 136 | 137 | METRIC_COLLECTOR_FIELD_LABELS = ['UUID', 'Created At', 'Updated At', 138 | 'Deleted At', 'Endpoint URL', 139 | 'Metric Category'] 140 | 141 | METRIC_COLLECTOR_SHORT_LIST_FIELDS = ['uuid', 'endpoint', 'category'] 142 | 143 | METRIC_COLLECTOR_SHORT_LIST_FIELD_LABELS = ['UUID', 'Endpoint URL', 144 | 'Metric Category'] 145 | # Scoring Engines 146 | SCORING_ENGINE_FIELDS = ['uuid', 'name', 'description', 'metainfo'] 147 | SCORING_ENGINE_FIELD_LABELS = ['UUID', 'Name', 'Description', 'Metainfo'] 148 | 149 | SCORING_ENGINE_SHORT_LIST_FIELDS = ['uuid', 'name', 'description'] 150 | SCORING_ENGINE_SHORT_LIST_FIELD_LABELS = ['UUID', 'Name', 'Description'] 151 | 152 | # Services 153 | SERVICE_FIELDS = ['id', 'name', 'host', 'status', 'last_seen_up'] 154 | SERVICE_FIELD_LABELS = ['ID', 'Name', 'Host', 'Status', 'Last seen up'] 155 | SERVICE_SHORT_LIST_FIELDS = ['id', 'name', 'host', 'status'] 156 | SERVICE_SHORT_LIST_FIELD_LABELS = ['ID', 'Name', 'Host', 'Status'] 157 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/v1/test_strategy.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Red Hat, Inc. 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | 17 | import testtools 18 | from testtools import matchers 19 | 20 | from watcherclient.tests.unit import utils 21 | import watcherclient.v1.strategy 22 | 23 | STRATEGY1 = { 24 | 'uuid': '2cf86250-d309-4b81-818e-1537f3dba6e5', 25 | 'name': 'basic', 26 | 'display_name': 'Basic consolidation', 27 | 'strategy_id': 'SERVER_CONSOLIDATION', 28 | } 29 | 30 | STRATEGY2 = { 31 | 'uuid': 'b20bb987-ea8f-457a-a4ea-ab3ffdfeff8b', 32 | 'name': 'dummy', 33 | 'display_name': 'Dummy', 34 | 'strategy_id': 'DUMMY', 35 | } 36 | 37 | fake_responses = { 38 | '/v1/strategies': 39 | { 40 | 'GET': ( 41 | {}, 42 | {"strategies": [STRATEGY1]}, 43 | ), 44 | }, 45 | '/v1/strategies/detail': 46 | { 47 | 'GET': ( 48 | {}, 49 | {"strategies": [STRATEGY1]}, 50 | ) 51 | }, 52 | '/v1/strategies/%s' % STRATEGY1['uuid']: 53 | { 54 | 'GET': ( 55 | {}, 56 | STRATEGY1, 57 | ), 58 | }, 59 | '/v1/strategies/%s' % STRATEGY1['name']: 60 | { 61 | 'GET': ( 62 | {}, 63 | STRATEGY1, 64 | ), 65 | }, 66 | '/v1/strategies/%s/state' % STRATEGY1['name']: 67 | { 68 | 'GET': ( 69 | {}, 70 | STRATEGY1, 71 | ), 72 | }, 73 | } 74 | 75 | fake_responses_pagination = { 76 | '/v1/strategies': 77 | { 78 | 'GET': ( 79 | {}, 80 | {"strategies": [STRATEGY1], 81 | "next": "http://127.0.0.1:9322/v1/strategies/?limit=1"} 82 | ), 83 | }, 84 | '/v1/strategies/?limit=1': 85 | { 86 | 'GET': ( 87 | {}, 88 | {"strategies": [STRATEGY2]} 89 | ), 90 | }, 91 | } 92 | 93 | fake_responses_sorting = { 94 | '/v1/strategies/?sort_key=id': 95 | { 96 | 'GET': ( 97 | {}, 98 | {"strategies": [STRATEGY1, STRATEGY2]} 99 | ), 100 | }, 101 | '/v1/strategies/?sort_dir=desc': 102 | { 103 | 'GET': ( 104 | {}, 105 | {"strategies": [STRATEGY2, STRATEGY1]} 106 | ), 107 | }, 108 | } 109 | 110 | fake_responses_marker = { 111 | '/v1/strategies/?marker=2cf86250-d309-4b81-818e-1537f3dba6e5': 112 | { 113 | 'GET': ( 114 | {}, 115 | {"strategies": [STRATEGY2]} 116 | ), 117 | }, 118 | } 119 | 120 | 121 | class StrategyManagerTest(testtools.TestCase): 122 | 123 | def setUp(self): 124 | super(StrategyManagerTest, self).setUp() 125 | self.api = utils.FakeAPI(fake_responses) 126 | self.mgr = watcherclient.v1.strategy.StrategyManager(self.api) 127 | 128 | def test_strategies_list(self): 129 | strategies = self.mgr.list() 130 | expect = [ 131 | ('GET', '/v1/strategies', {}, None), 132 | ] 133 | self.assertEqual(expect, self.api.calls) 134 | self.assertEqual(1, len(strategies)) 135 | 136 | def test_strategies_list_detail(self): 137 | strategies = self.mgr.list(detail=True) 138 | expect = [ 139 | ('GET', '/v1/strategies/detail', {}, None), 140 | ] 141 | self.assertEqual(expect, self.api.calls) 142 | self.assertEqual(1, len(strategies)) 143 | 144 | def test_strategies_list_marker(self): 145 | self.api = utils.FakeAPI(fake_responses_marker) 146 | self.mgr = watcherclient.v1.strategy.StrategyManager(self.api) 147 | strategies = self.mgr.list(marker=STRATEGY1['uuid']) 148 | expect = [ 149 | ('GET', 150 | '/v1/strategies/?marker=2cf86250-d309-4b81-818e-1537f3dba6e5', 151 | {}, 152 | None), 153 | ] 154 | self.assertEqual(expect, self.api.calls) 155 | self.assertEqual(1, len(strategies)) 156 | 157 | def test_strategies_list_limit(self): 158 | self.api = utils.FakeAPI(fake_responses_pagination) 159 | self.mgr = watcherclient.v1.strategy.StrategyManager(self.api) 160 | strategies = self.mgr.list(limit=1) 161 | expect = [ 162 | ('GET', '/v1/strategies/?limit=1', {}, None), 163 | ] 164 | self.assertEqual(expect, self.api.calls) 165 | self.assertThat(strategies, matchers.HasLength(1)) 166 | 167 | def test_strategies_list_pagination_no_limit(self): 168 | self.api = utils.FakeAPI(fake_responses_pagination) 169 | self.mgr = watcherclient.v1.strategy.StrategyManager(self.api) 170 | strategies = self.mgr.list(limit=0) 171 | expect = [ 172 | ('GET', '/v1/strategies', {}, None), 173 | ('GET', '/v1/strategies/?limit=1', {}, None) 174 | ] 175 | self.assertEqual(expect, self.api.calls) 176 | self.assertThat(strategies, matchers.HasLength(2)) 177 | 178 | def test_strategies_list_sort_key(self): 179 | self.api = utils.FakeAPI(fake_responses_sorting) 180 | self.mgr = watcherclient.v1.strategy.StrategyManager(self.api) 181 | strategies = self.mgr.list(sort_key='id') 182 | expect = [ 183 | ('GET', '/v1/strategies/?sort_key=id', {}, None) 184 | ] 185 | self.assertEqual(expect, self.api.calls) 186 | self.assertEqual(2, len(strategies)) 187 | 188 | def test_strategies_list_sort_dir(self): 189 | self.api = utils.FakeAPI(fake_responses_sorting) 190 | self.mgr = watcherclient.v1.strategy.StrategyManager(self.api) 191 | strategies = self.mgr.list(sort_dir='desc') 192 | expect = [ 193 | ('GET', '/v1/strategies/?sort_dir=desc', {}, None) 194 | ] 195 | self.assertEqual(expect, self.api.calls) 196 | self.assertEqual(2, len(strategies)) 197 | 198 | def test_strategies_show(self): 199 | strategy = self.mgr.get(STRATEGY1['uuid']) 200 | expect = [ 201 | ('GET', '/v1/strategies/%s' % STRATEGY1['uuid'], {}, None), 202 | ] 203 | self.assertEqual(expect, self.api.calls) 204 | self.assertEqual(STRATEGY1['uuid'], strategy.uuid) 205 | 206 | def test_strategies_show_by_name(self): 207 | strategy = self.mgr.get(STRATEGY1['name']) 208 | expect = [ 209 | ('GET', '/v1/strategies/%s' % STRATEGY1['name'], {}, None), 210 | ] 211 | self.assertEqual(expect, self.api.calls) 212 | self.assertEqual(STRATEGY1['name'], strategy.name) 213 | 214 | def test_strategies_state(self): 215 | self.mgr.state(STRATEGY1['name']) 216 | expect = [ 217 | ('GET', '/v1/strategies/%s/state' % STRATEGY1['name'], {}, None), 218 | ] 219 | self.assertEqual(expect, self.api.calls) 220 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/common/test_api_versioning.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Mirantis 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from unittest import mock 17 | 18 | from watcherclient.common import api_versioning 19 | from watcherclient import exceptions 20 | from watcherclient.tests.unit import utils 21 | 22 | 23 | class APIVersionTestCase(utils.BaseTestCase): 24 | def test_valid_version_strings(self): 25 | def _test_string(version, exp_major, exp_minor): 26 | v = api_versioning.APIVersion(version) 27 | self.assertEqual(v.ver_major, exp_major) 28 | self.assertEqual(v.ver_minor, exp_minor) 29 | 30 | _test_string("1.1", 1, 1) 31 | _test_string("2.10", 2, 10) 32 | _test_string("5.234", 5, 234) 33 | _test_string("12.5", 12, 5) 34 | _test_string("2.0", 2, 0) 35 | _test_string("2.200", 2, 200) 36 | 37 | def test_null_version(self): 38 | v = api_versioning.APIVersion() 39 | self.assertTrue(v.is_null()) 40 | 41 | def test_invalid_version_strings(self): 42 | self.assertRaises(exceptions.UnsupportedVersion, 43 | api_versioning.APIVersion, "2") 44 | 45 | self.assertRaises(exceptions.UnsupportedVersion, 46 | api_versioning.APIVersion, "200") 47 | 48 | self.assertRaises(exceptions.UnsupportedVersion, 49 | api_versioning.APIVersion, "2.1.4") 50 | 51 | self.assertRaises(exceptions.UnsupportedVersion, 52 | api_versioning.APIVersion, "200.23.66.3") 53 | 54 | self.assertRaises(exceptions.UnsupportedVersion, 55 | api_versioning.APIVersion, "5 .3") 56 | 57 | self.assertRaises(exceptions.UnsupportedVersion, 58 | api_versioning.APIVersion, "5. 3") 59 | 60 | self.assertRaises(exceptions.UnsupportedVersion, 61 | api_versioning.APIVersion, "5.03") 62 | 63 | self.assertRaises(exceptions.UnsupportedVersion, 64 | api_versioning.APIVersion, "02.1") 65 | 66 | self.assertRaises(exceptions.UnsupportedVersion, 67 | api_versioning.APIVersion, "2.001") 68 | 69 | self.assertRaises(exceptions.UnsupportedVersion, 70 | api_versioning.APIVersion, "") 71 | 72 | self.assertRaises(exceptions.UnsupportedVersion, 73 | api_versioning.APIVersion, " 2.1") 74 | 75 | self.assertRaises(exceptions.UnsupportedVersion, 76 | api_versioning.APIVersion, "2.1 ") 77 | 78 | def test_version_comparisons(self): 79 | v1 = api_versioning.APIVersion("2.0") 80 | v2 = api_versioning.APIVersion("2.5") 81 | v3 = api_versioning.APIVersion("5.23") 82 | v4 = api_versioning.APIVersion("2.0") 83 | v_null = api_versioning.APIVersion() 84 | 85 | self.assertLess(v1, v2) 86 | self.assertGreater(v3, v2) 87 | self.assertNotEqual(v1, v2) 88 | self.assertEqual(v1, v4) 89 | self.assertNotEqual(v1, v_null) 90 | self.assertEqual(v_null, v_null) 91 | self.assertRaises(TypeError, v1.__le__, "2.1") 92 | 93 | def test_version_matches(self): 94 | v1 = api_versioning.APIVersion("2.0") 95 | v2 = api_versioning.APIVersion("2.5") 96 | v3 = api_versioning.APIVersion("2.45") 97 | v4 = api_versioning.APIVersion("3.3") 98 | v5 = api_versioning.APIVersion("3.23") 99 | v6 = api_versioning.APIVersion("2.0") 100 | v7 = api_versioning.APIVersion("3.3") 101 | v8 = api_versioning.APIVersion("4.0") 102 | v_null = api_versioning.APIVersion() 103 | 104 | self.assertTrue(v2.matches(v1, v3)) 105 | self.assertTrue(v2.matches(v1, v_null)) 106 | self.assertTrue(v1.matches(v6, v2)) 107 | self.assertTrue(v4.matches(v2, v7)) 108 | self.assertTrue(v4.matches(v_null, v7)) 109 | self.assertTrue(v4.matches(v_null, v8)) 110 | self.assertFalse(v1.matches(v2, v3)) 111 | self.assertFalse(v5.matches(v2, v4)) 112 | self.assertFalse(v2.matches(v3, v1)) 113 | 114 | self.assertRaises(ValueError, v_null.matches, v1, v3) 115 | 116 | def test_get_string(self): 117 | v1_string = "3.23" 118 | v1 = api_versioning.APIVersion(v1_string) 119 | self.assertEqual(v1_string, v1.get_string()) 120 | 121 | self.assertRaises(ValueError, 122 | api_versioning.APIVersion().get_string) 123 | 124 | 125 | class GetAPIVersionTestCase(utils.BaseTestCase): 126 | def test_get_available_client_versions(self): 127 | output = api_versioning.get_available_major_versions() 128 | self.assertNotEqual([], output) 129 | 130 | def test_wrong_format(self): 131 | self.assertRaises(exceptions.UnsupportedVersion, 132 | api_versioning.get_api_version, "something_wrong") 133 | 134 | def test_wrong_major_version(self): 135 | self.assertRaises(exceptions.UnsupportedVersion, 136 | api_versioning.get_api_version, "2") 137 | 138 | @mock.patch("watcherclient.common.api_versioning.APIVersion") 139 | def test_only_major_part_is_presented(self, mock_apiversion): 140 | version = 7 141 | self.assertEqual(mock_apiversion.return_value, 142 | api_versioning.get_api_version(version)) 143 | mock_apiversion.assert_called_once_with("%s.0" % str(version)) 144 | 145 | @mock.patch("watcherclient.common.api_versioning.APIVersion") 146 | def test_major_and_minor_parts_is_presented(self, mock_apiversion): 147 | version = "2.7" 148 | self.assertEqual(mock_apiversion.return_value, 149 | api_versioning.get_api_version(version)) 150 | mock_apiversion.assert_called_once_with(version) 151 | 152 | 153 | class APIVersionFunctionsTestCase(utils.BaseTestCase): 154 | def test_action_update_supported_true(self): 155 | # Test versions >= 1.5 support action update 156 | self.assertTrue(api_versioning.action_update_supported("1.5")) 157 | self.assertTrue(api_versioning.action_update_supported("1.6")) 158 | self.assertTrue(api_versioning.action_update_supported("2.0")) 159 | 160 | def test_action_update_supported_false(self): 161 | # Test versions < 1.5 do not support action update 162 | self.assertFalse(api_versioning.action_update_supported("1.0")) 163 | self.assertFalse(api_versioning.action_update_supported("1.1")) 164 | self.assertFalse(api_versioning.action_update_supported("1.4")) 165 | 166 | def test_action_update_supported_edge_case(self): 167 | # Test exact boundary 168 | self.assertTrue(api_versioning.action_update_supported("1.5")) 169 | self.assertFalse(api_versioning.action_update_supported("1.4")) 170 | -------------------------------------------------------------------------------- /watcherclient/tests/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2013 OpenStack LLC. 3 | # All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from unittest import mock 18 | 19 | from watcherclient.common.apiclient import exceptions as exc 20 | from watcherclient.common import utils 21 | from watcherclient.tests.unit import utils as test_utils 22 | 23 | 24 | class UtilsTest(test_utils.BaseTestCase): 25 | 26 | def test_args_array_to_dict(self): 27 | my_args = { 28 | 'matching_metadata': ['str=foo', 'int=1', 'bool=true', 29 | 'list=[1, 2, 3]', 'dict={"foo": "bar"}'], 30 | 'other': 'value' 31 | } 32 | cleaned_dict = utils.args_array_to_dict(my_args, 33 | "matching_metadata") 34 | self.assertEqual({ 35 | 'matching_metadata': {'str': 'foo', 'int': 1, 'bool': True, 36 | 'list': [1, 2, 3], 'dict': {'foo': 'bar'}}, 37 | 'other': 'value' 38 | }, cleaned_dict) 39 | 40 | def test_args_array_to_patch(self): 41 | my_args = { 42 | 'attributes': ['str=foo', 'int=1', 'bool=true', 43 | 'list=[1, 2, 3]', 'dict={"foo": "bar"}'], 44 | 'op': 'add', 45 | } 46 | patch = utils.args_array_to_patch(my_args['op'], 47 | my_args['attributes']) 48 | self.assertEqual([{'op': 'add', 'value': 'foo', 'path': '/str'}, 49 | {'op': 'add', 'value': 1, 'path': '/int'}, 50 | {'op': 'add', 'value': True, 'path': '/bool'}, 51 | {'op': 'add', 'value': [1, 2, 3], 'path': '/list'}, 52 | {'op': 'add', 'value': {"foo": "bar"}, 53 | 'path': '/dict'}], patch) 54 | 55 | def test_args_array_to_patch_format_error(self): 56 | my_args = { 57 | 'attributes': ['foobar'], 58 | 'op': 'add', 59 | } 60 | self.assertRaises(exc.CommandError, utils.args_array_to_patch, 61 | my_args['op'], my_args['attributes']) 62 | 63 | def test_args_array_to_patch_remove(self): 64 | my_args = { 65 | 'attributes': ['/foo', 'extra/bar'], 66 | 'op': 'remove', 67 | } 68 | patch = utils.args_array_to_patch(my_args['op'], 69 | my_args['attributes']) 70 | self.assertEqual([{'op': 'remove', 'path': '/foo'}, 71 | {'op': 'remove', 'path': '/extra/bar'}], patch) 72 | 73 | def test_split_and_deserialize(self): 74 | ret = utils.split_and_deserialize('str=foo') 75 | self.assertEqual(('str', 'foo'), ret) 76 | 77 | ret = utils.split_and_deserialize('int=1') 78 | self.assertEqual(('int', 1), ret) 79 | 80 | ret = utils.split_and_deserialize('bool=false') 81 | self.assertEqual(('bool', False), ret) 82 | 83 | ret = utils.split_and_deserialize('list=[1, "foo", 2]') 84 | self.assertEqual(('list', [1, "foo", 2]), ret) 85 | 86 | ret = utils.split_and_deserialize('dict={"foo": 1}') 87 | self.assertEqual(('dict', {"foo": 1}), ret) 88 | 89 | ret = utils.split_and_deserialize('str_int="1"') 90 | self.assertEqual(('str_int', "1"), ret) 91 | 92 | def test_split_and_deserialize_fail(self): 93 | self.assertRaises(exc.CommandError, 94 | utils.split_and_deserialize, 'foo:bar') 95 | 96 | 97 | class CommonParamsForListTest(test_utils.BaseTestCase): 98 | def setUp(self): 99 | super(CommonParamsForListTest, self).setUp() 100 | self.args = mock.Mock(limit=None, marker=None, 101 | sort_key=None, sort_dir=None) 102 | self.args.detail = False 103 | self.expected_params = {'detail': False} 104 | 105 | def test_nothing_set(self): 106 | self.assertEqual(self.expected_params, 107 | utils.common_params_for_list(self.args, [], [])) 108 | 109 | def test_limit(self): 110 | self.args.limit = 42 111 | self.expected_params.update({'limit': 42}) 112 | self.assertEqual(self.expected_params, 113 | utils.common_params_for_list(self.args, [], [])) 114 | 115 | def test_invalid_limit(self): 116 | self.args.limit = -42 117 | self.assertRaises(exc.CommandError, 118 | utils.common_params_for_list, 119 | self.args, [], []) 120 | 121 | def test_marker(self): 122 | self.args.marker = 'e420a881-d7df-4de2-bbf3-378cc13d9b3a' 123 | self.expected_params.update( 124 | {'marker': 'e420a881-d7df-4de2-bbf3-378cc13d9b3a'}) 125 | self.assertEqual(self.expected_params, 126 | utils.common_params_for_list(self.args, [], [])) 127 | 128 | def test_sort_key_and_sort_dir(self): 129 | self.args.sort_key = 'field' 130 | self.args.sort_dir = 'desc' 131 | self.expected_params.update({'sort_key': 'field', 'sort_dir': 'desc'}) 132 | self.assertEqual(self.expected_params, 133 | utils.common_params_for_list(self.args, 134 | ['field'], 135 | [])) 136 | 137 | def test_sort_key_allows_label(self): 138 | self.args.sort_key = 'Label' 139 | self.expected_params.update({'sort_key': 'field'}) 140 | self.assertEqual(self.expected_params, 141 | utils.common_params_for_list(self.args, 142 | ['field', 'field2'], 143 | ['Label', 'Label2'])) 144 | 145 | def test_sort_key_invalid(self): 146 | self.args.sort_key = 'something' 147 | self.assertRaises(exc.CommandError, 148 | utils.common_params_for_list, 149 | self.args, 150 | ['field', 'field2'], 151 | []) 152 | 153 | def test_sort_dir_invalid(self): 154 | self.args.sort_dir = 'something' 155 | self.assertRaises(exc.CommandError, 156 | utils.common_params_for_list, 157 | self.args, 158 | [], 159 | []) 160 | 161 | def test_detail(self): 162 | self.args.detail = True 163 | self.expected_params['detail'] = True 164 | self.assertEqual(self.expected_params, 165 | utils.common_params_for_list(self.args, [], [])) 166 | 167 | 168 | class CommonFiltersTest(test_utils.BaseTestCase): 169 | def test_limit(self): 170 | result = utils.common_filters(limit=42) 171 | self.assertEqual(['limit=42'], result) 172 | 173 | def test_limit_0(self): 174 | result = utils.common_filters(limit=0) 175 | self.assertEqual([], result) 176 | 177 | def test_other(self): 178 | for key in ('sort_key', 'sort_dir'): 179 | result = utils.common_filters(**{key: 'test'}) 180 | self.assertEqual(['%s=test' % key], result) 181 | --------------------------------------------------------------------------------