├── .coveragerc ├── .gitignore ├── .gitreview ├── .pre-commit-config.yaml ├── .stestr.conf ├── .zuul.yaml ├── CONTRIBUTING.rst ├── HACKING.rst ├── LICENSE ├── README.rst ├── aodhclient ├── __init__.py ├── client.py ├── exceptions.py ├── i18n.py ├── noauth.py ├── osc.py ├── shell.py ├── tests │ ├── __init__.py │ ├── functional │ │ ├── __init__.py │ │ ├── base.py │ │ ├── test_alarm.py │ │ ├── test_alarm_history.py │ │ ├── test_capabilities.py │ │ └── test_metrics.py │ └── unit │ │ ├── __init__.py │ │ ├── test_alarm_cli.py │ │ ├── test_alarm_history.py │ │ ├── test_alarm_manager.py │ │ ├── test_exceptions.py │ │ ├── test_metrics.py │ │ ├── test_quota.py │ │ ├── test_shell.py │ │ └── test_utils.py ├── utils.py └── v2 │ ├── __init__.py │ ├── alarm.py │ ├── alarm_cli.py │ ├── alarm_history.py │ ├── alarm_history_cli.py │ ├── base.py │ ├── capabilities.py │ ├── capabilities_cli.py │ ├── client.py │ ├── metrics.py │ ├── metrics_cli.py │ ├── quota.py │ └── quota_cli.py ├── bindep.txt ├── doc ├── requirements.txt └── source │ ├── api.rst │ ├── conf.py │ ├── contributing.rst │ ├── index.rst │ ├── installation.rst │ └── shell.rst ├── pyproject.toml ├── releasenotes ├── notes │ ├── .placeholder │ ├── add-alarm-metrics-command-349e75fbf26171d5.yaml │ ├── add-pagination-support-fcdf1cef0cfa5ca9.yaml │ ├── drop-py-2-7-8f26d7e1e8dc83c2.yaml │ ├── drop-python-3-6-and-3-7-c70234384bc69b1d.yaml │ ├── merge-search-to-list-d44cd65ede348c3e.yaml │ ├── osc-support-9f9dae2d2203f307.yaml │ ├── remove-ceilometer-alarms-02049eef189c2812.yaml │ ├── remove-py38-39f51350ec165758.yaml │ ├── split-alarm-query-and-list-5998020b88ddc9f5.yaml │ ├── support-get-set-state-interfaces-67419b925ffd6877.yaml │ └── ussuri-add-threshold-alarm-47e012620fd611ea.yaml └── source │ ├── 2023.1.rst │ ├── 2023.2.rst │ ├── 2024.1.rst │ ├── 2024.2.rst │ ├── 2025.1.rst │ ├── _static │ └── .placeholder │ ├── _templates │ └── .placeholder │ ├── conf.py │ ├── index.rst │ ├── mitaka.rst │ ├── newton.rst │ ├── ocata.rst │ ├── pike.rst │ ├── queens.rst │ ├── rocky.rst │ ├── stein.rst │ ├── train.rst │ ├── unreleased.rst │ ├── ussuri.rst │ ├── victoria.rst │ ├── wallaby.rst │ ├── xena.rst │ ├── yoga.rst │ └── zed.rst ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── tools └── fix_ca_bundle.sh └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = aodhclient 4 | omit = aodhclient/tests/* 5 | 6 | [report] 7 | ignore_errors = True 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | .eggs 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 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | # Complexity 41 | output/*.html 42 | output/*/index.html 43 | 44 | # Sphinx 45 | doc/build 46 | releasenotes/build 47 | 48 | # pbr generates these 49 | AUTHORS 50 | ChangeLog 51 | 52 | # Editors 53 | *~ 54 | .*.swp 55 | .*sw? 56 | 57 | # generated docs 58 | doc/source/ref/ 59 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.opendev.org 3 | port=29418 4 | project=openstack/python-aodhclient.git 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | # Replaces or checks mixed line ending 7 | - id: mixed-line-ending 8 | args: ['--fix', 'lf'] 9 | exclude: '.*\.(svg)$' 10 | # Forbid files which have a UTF-8 byte-order marker 11 | - id: check-byte-order-marker 12 | # Checks that non-binary executables have a proper shebang 13 | - id: check-executables-have-shebangs 14 | # Check for files that contain merge conflict strings. 15 | - id: check-merge-conflict 16 | # Check for debugger imports and py37+ breakpoint() 17 | # calls in python source 18 | - id: debug-statements 19 | - id: check-yaml 20 | files: .*\.(yaml|yml)$ 21 | - repo: https://opendev.org/openstack/hacking 22 | rev: 7.0.0 23 | hooks: 24 | - id: hacking 25 | additional_dependencies: [] 26 | -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=./aodhclient/tests/unit 3 | top_dir=./ 4 | -------------------------------------------------------------------------------- /.zuul.yaml: -------------------------------------------------------------------------------- 1 | - job: 2 | name: aodhclient-dsvm-functional 3 | parent: devstack-tox-functional 4 | description: | 5 | Devstack-based functional tests for aodhclient. 6 | required-projects: 7 | - openstack/python-aodhclient 8 | - openstack/aodh 9 | # We neeed ceilometer's devstack plugin to install gnocchi 10 | - openstack/ceilometer 11 | - gnocchixyz/gnocchi 12 | - openstack-k8s-operators/sg-core 13 | - openstack/devstack-plugin-prometheus 14 | timeout: 4200 15 | vars: 16 | devstack_localrc: 17 | CEILOMETER_BACKENDS: "gnocchi,sg-core" 18 | PROMETHEUS_SERVICE_SCRAPE_TARGETS: prometheus,sg-core 19 | PROMETHEUS_CUSTOM_SCRAPE_TARGETS: "localhost:3000,localhost:9090" 20 | devstack_plugins: 21 | aodh: https://opendev.org/openstack/aodh 22 | ceilometer: https://opendev.org/openstack/ceilometer 23 | sg-core: https://github.com/openstack-k8s-operators/sg-core 24 | devstack-plugin-prometheus: https://opendev.org/openstack/devstack-plugin-prometheus 25 | devstack_services: 26 | node_exporter: false 27 | zuul_copy_output: 28 | /etc/prometheus/prometheus.yml: logs 29 | /etc/openstack/prometheus.yaml: logs 30 | devstack_local_conf: 31 | post-config: 32 | $AODH_CONF: 33 | DEFAULT: 34 | enable_evaluation_results_metrics: True 35 | 36 | - project: 37 | queue: telemetry 38 | templates: 39 | - check-requirements 40 | - openstack-python3-jobs 41 | - publish-openstack-docs-pti 42 | - release-notes-jobs-python3 43 | - openstackclient-plugin-jobs 44 | check: 45 | jobs: 46 | - aodhclient-dsvm-functional: 47 | irrelevant-files: &ac-irrelevant-files 48 | - ^\.gitreview$ 49 | - ^(test-|)requirements.txt$ 50 | - ^setup.cfg$ 51 | - ^doc/.*$ 52 | - ^.*\.rst$ 53 | - ^releasenotes/.*$ 54 | gate: 55 | jobs: 56 | - aodhclient-dsvm-functional: 57 | irrelevant-files: *ac-irrelevant-files 58 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | If you would like to contribute to the development of OpenStack, you must 2 | follow the steps in this page: 3 | 4 | https://docs.openstack.org/infra/manual/developers.html 5 | 6 | If you already have a good understanding of how the system works and your 7 | OpenStack accounts are set up, you can skip to the development workflow 8 | section of this documentation to learn how changes to OpenStack should be 9 | submitted for review via the Gerrit tool: 10 | 11 | https://docs.openstack.org/infra/manual/developers.html#development-workflow 12 | 13 | Pull requests submitted through GitHub will be ignored. 14 | 15 | Bugs should be filed on Launchpad, not GitHub: 16 | 17 | https://storyboard.openstack.org/#!/project/openstack/python-aodhclient 18 | -------------------------------------------------------------------------------- /HACKING.rst: -------------------------------------------------------------------------------- 1 | aodhclient Style Commandments 2 | ================================ 3 | 4 | Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | aodhclient 3 | ========== 4 | 5 | Python bindings to the OpenStack Aodh API 6 | 7 | This is a client for OpenStack Aodh API. There's a `Python API 8 | `_ (the 9 | aodhclient module), and a `command-line script 10 | `_ (installed 11 | as aodh). Each implements the entire OpenStack Aodh API. 12 | 13 | * Free software: Apache license 14 | * Release notes: https://releases.openstack.org/teams/telemetry.html 15 | * Documentation: https://docs.openstack.org/python-aodhclient/latest/ 16 | * Source: https://opendev.org/openstack/python-aodhclient 17 | * Bugs: https://storyboard.openstack.org/#!/project/openstack/python-aodhclient 18 | -------------------------------------------------------------------------------- /aodhclient/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 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 pbr.version 16 | 17 | 18 | __version__ = pbr.version.VersionInfo( 19 | 'aodhclient').version_string() 20 | -------------------------------------------------------------------------------- /aodhclient/client.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 keystoneauth1 import adapter 14 | from oslo_utils import importutils 15 | from osprofiler import web 16 | 17 | from aodhclient import exceptions 18 | 19 | 20 | def Client(version, *args, **kwargs): 21 | module = 'aodhclient.v%s.client' % version 22 | module = importutils.import_module(module) 23 | client_class = getattr(module, 'Client') 24 | return client_class(*args, **kwargs) 25 | 26 | 27 | class SessionClient(adapter.Adapter): 28 | def request(self, url, method, **kwargs): 29 | kwargs.setdefault('headers', kwargs.get('headers', {})) 30 | # NOTE(sileht): The standard call raises errors from 31 | # keystoneauth, where we need to raise the aodhclient errors. 32 | raise_exc = kwargs.pop('raise_exc', True) 33 | kwargs['headers'].update(web.get_trace_id_headers()) 34 | resp = super(SessionClient, self).request(url, 35 | method, 36 | raise_exc=False, 37 | **kwargs) 38 | 39 | if raise_exc and resp.status_code >= 400: 40 | raise exceptions.from_response(resp, url, method) 41 | return resp 42 | -------------------------------------------------------------------------------- /aodhclient/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 | 15 | class ClientException(Exception): 16 | """The base exception class for all exceptions this library raises.""" 17 | message = 'Unknown Error' 18 | http_status = 'N/A' 19 | 20 | def __init__(self, message=None, request_id=None, 21 | url=None, method=None): 22 | self.message = message or self.__class__.message 23 | self.request_id = request_id 24 | self.url = url 25 | self.method = method 26 | 27 | # NOTE(jd) for backward compat 28 | @property 29 | def code(self): 30 | return self.http_status 31 | 32 | def __str__(self): 33 | formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) 34 | if self.request_id: 35 | formatted_string += " (Request-ID: %s)" % self.request_id 36 | 37 | return formatted_string 38 | 39 | 40 | class RetryAfterException(ClientException): 41 | """The base exception for ClientExceptions that use Retry-After header.""" 42 | def __init__(self, *args, **kwargs): 43 | try: 44 | self.retry_after = int(kwargs.pop('retry_after')) 45 | except (KeyError, ValueError): 46 | self.retry_after = 0 47 | 48 | super(RetryAfterException, self).__init__(*args, **kwargs) 49 | 50 | 51 | class MutipleMeaningException(object): 52 | """An mixin for exception that can be enhanced by reading the details""" 53 | 54 | 55 | class CommandError(Exception): 56 | pass 57 | 58 | 59 | class BadRequest(ClientException): 60 | """HTTP 400 - Bad request: you sent some malformed data.""" 61 | http_status = 400 62 | message = "Bad request" 63 | 64 | 65 | class Unauthorized(ClientException): 66 | """HTTP 401 - Unauthorized: bad credentials.""" 67 | http_status = 401 68 | message = "Unauthorized" 69 | 70 | 71 | class Forbidden(ClientException): 72 | """HTTP 403 - Forbidden: 73 | 74 | your credentials don't give you access to this resource. 75 | """ 76 | http_status = 403 77 | message = "Forbidden" 78 | 79 | 80 | class NotFound(ClientException): 81 | """HTTP 404 - Not found""" 82 | http_status = 404 83 | message = "Not found" 84 | 85 | 86 | class MethodNotAllowed(ClientException): 87 | """HTTP 405 - Method Not Allowed""" 88 | http_status = 405 89 | message = "Method Not Allowed" 90 | 91 | 92 | class NotAcceptable(ClientException): 93 | """HTTP 406 - Not Acceptable""" 94 | http_status = 406 95 | message = "Not Acceptable" 96 | 97 | 98 | class Conflict(ClientException): 99 | """HTTP 409 - Conflict""" 100 | http_status = 409 101 | message = "Conflict" 102 | 103 | 104 | class OverLimit(RetryAfterException): 105 | """HTTP 413 - Over limit: 106 | 107 | you're over the API limits for this time period. 108 | """ 109 | http_status = 413 110 | message = "Over limit" 111 | 112 | 113 | class RateLimit(RetryAfterException): 114 | """HTTP 429 - Rate limit: 115 | 116 | you've sent too many requests for this time period. 117 | """ 118 | http_status = 429 119 | message = "Rate limit" 120 | 121 | 122 | class NoUniqueMatch(Exception): 123 | pass 124 | 125 | 126 | class NotImplemented(ClientException): 127 | """HTTP 501 - Not Implemented: 128 | 129 | the server does not support this operation. 130 | """ 131 | http_status = 501 132 | message = "Not Implemented" 133 | 134 | 135 | _error_classes = [BadRequest, Unauthorized, Forbidden, NotFound, 136 | MethodNotAllowed, NotAcceptable, Conflict, OverLimit, 137 | RateLimit, NotImplemented] 138 | _error_classes_enhanced = {} 139 | _code_map = dict( 140 | (c.http_status, (c, _error_classes_enhanced.get(c, []))) 141 | for c in _error_classes) 142 | 143 | 144 | def from_response(response, url, method=None): 145 | """Return an instance of ClientException on an requests response. 146 | 147 | Usage:: 148 | 149 | resp, body = requests.request(...) 150 | if resp.status_code != 200: 151 | raise exception_from_response(resp) 152 | """ 153 | 154 | if response.status_code: 155 | cls, enhanced_classes = _code_map.get(response.status_code, 156 | (ClientException, [])) 157 | 158 | req_id = response.headers.get("x-openstack-request-id") 159 | content_type = response.headers.get("Content-Type", "").split(";")[0] 160 | 161 | kwargs = { 162 | 'method': method, 163 | 'url': url, 164 | 'request_id': req_id, 165 | } 166 | 167 | if "retry-after" in response.headers: 168 | kwargs['retry_after'] = response.headers.get('retry-after') 169 | 170 | if content_type == "application/json": 171 | try: 172 | body = response.json() 173 | except ValueError: 174 | pass 175 | else: 176 | desc = body.get('error_message', {}).get('faultstring') 177 | for enhanced_cls in enhanced_classes: 178 | if enhanced_cls.match.match(desc): 179 | cls = enhanced_cls 180 | break 181 | kwargs['message'] = desc 182 | elif content_type.startswith("text/"): 183 | kwargs['message'] = response.text 184 | 185 | if not kwargs.get('message'): 186 | kwargs.pop('message', None) 187 | 188 | exception = cls(**kwargs) 189 | if isinstance(exception, ClientException) and response.status_code: 190 | exception.http_status = response.status_code 191 | return exception 192 | -------------------------------------------------------------------------------- /aodhclient/i18n.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 oslo_i18n as i18n 14 | 15 | _translators = i18n.TranslatorFactory(domain='aodhclient') 16 | 17 | # The primary translation function using the well-known name "_" 18 | _ = _translators.primary 19 | -------------------------------------------------------------------------------- /aodhclient/noauth.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 | import os 15 | 16 | from keystoneauth1 import loading 17 | from keystoneauth1 import plugin 18 | 19 | 20 | class AodhNoAuthPlugin(plugin.BaseAuthPlugin): 21 | """No authentication plugin for Aodh 22 | 23 | This is a keystoneauth plugin that instead of 24 | doing authentication, it just fill the 'x-user-id' 25 | and 'x-project-id' headers with the user provided one. 26 | """ 27 | def __init__(self, user_id, project_id, roles, endpoint): 28 | self._user_id = user_id 29 | self._project_id = project_id 30 | self._endpoint = endpoint 31 | self._roles = roles 32 | 33 | def get_token(self, session, **kwargs): 34 | return '' 35 | 36 | def get_headers(self, session, **kwargs): 37 | return {'x-user-id': self._user_id, 38 | 'x-project-id': self._project_id, 39 | 'x-roles': self._roles} 40 | 41 | def get_user_id(self, session, **kwargs): 42 | return self._user_id 43 | 44 | def get_project_id(self, session, **kwargs): 45 | return self._project_id 46 | 47 | def get_endpoint(self, session, **kwargs): 48 | return self._endpoint 49 | 50 | 51 | class AodhOpt(loading.Opt): 52 | @property 53 | def argparse_args(self): 54 | return ['--%s' % o.name for o in self._all_opts] 55 | 56 | @property 57 | def argparse_default(self): 58 | # select the first ENV that is not false-y or return None 59 | for o in self._all_opts: 60 | v = os.environ.get('AODH_%s' % o.name.replace('-', '_').upper()) 61 | if v: 62 | return v 63 | return self.default 64 | 65 | 66 | class AodhNoAuthLoader(loading.BaseLoader): 67 | plugin_class = AodhNoAuthPlugin 68 | 69 | def get_options(self): 70 | options = super(AodhNoAuthLoader, self).get_options() 71 | options.extend([ 72 | AodhOpt('user-id', help='User ID', required=True), 73 | AodhOpt('project-id', help='Project ID', required=True), 74 | AodhOpt('roles', help='Roles', default="admin"), 75 | AodhOpt('aodh-endpoint', help='Aodh endpoint', 76 | dest="endpoint", required=True), 77 | ]) 78 | return options 79 | -------------------------------------------------------------------------------- /aodhclient/osc.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 OpenStack Foundation 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 | """OpenStackClient plugin for Telemetry Alarming service.""" 16 | 17 | from osc_lib import utils 18 | 19 | 20 | DEFAULT_ALARMING_API_VERSION = '2' 21 | API_VERSION_OPTION = 'os_alarming_api_version' 22 | API_NAME = "alarming" 23 | API_VERSIONS = { 24 | "2": "aodhclient.v2.client.Client", 25 | } 26 | 27 | 28 | def make_client(instance): 29 | """Returns an queues service client.""" 30 | version = instance._api_version[API_NAME] 31 | try: 32 | version = int(version) 33 | except ValueError: 34 | version = float(version) 35 | 36 | aodh_client = utils.get_client_class( 37 | API_NAME, 38 | version, 39 | API_VERSIONS) 40 | # NOTE(sileht): ensure setup of the session is done 41 | instance.setup_auth() 42 | return aodh_client(session=instance.session, 43 | interface=instance.interface, 44 | region_name=instance.region_name) 45 | 46 | 47 | def build_option_parser(parser): 48 | """Hook to add global options.""" 49 | parser.add_argument( 50 | '--os-alarming-api-version', 51 | metavar='', 52 | default=utils.env( 53 | 'OS_ALARMING_API_VERSION', 54 | default=DEFAULT_ALARMING_API_VERSION), 55 | help=('Queues API version, default=' + 56 | DEFAULT_ALARMING_API_VERSION + 57 | ' (Env: OS_ALARMING_API_VERSION)')) 58 | return parser 59 | -------------------------------------------------------------------------------- /aodhclient/shell.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 OpenStack Foundation 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 logging 17 | import os 18 | import sys 19 | import warnings 20 | 21 | from cliff import app 22 | from cliff import commandmanager 23 | from keystoneauth1 import exceptions 24 | from keystoneauth1 import loading 25 | 26 | from aodhclient import __version__ 27 | from aodhclient import client 28 | from aodhclient import noauth 29 | from aodhclient.v2 import alarm_cli 30 | from aodhclient.v2 import alarm_history_cli 31 | from aodhclient.v2 import capabilities_cli 32 | from aodhclient.v2 import metrics_cli 33 | 34 | 35 | class AodhCommandManager(commandmanager.CommandManager): 36 | SHELL_COMMANDS = { 37 | "alarm create": alarm_cli.CliAlarmCreate, 38 | "alarm delete": alarm_cli.CliAlarmDelete, 39 | "alarm list": alarm_cli.CliAlarmList, 40 | "alarm show": alarm_cli.CliAlarmShow, 41 | "alarm update": alarm_cli.CliAlarmUpdate, 42 | "alarm state get": alarm_cli.CliAlarmStateGet, 43 | "alarm state set": alarm_cli.CliAlarmStateSet, 44 | "alarm-history show": alarm_history_cli.CliAlarmHistoryShow, 45 | "alarm-history search": alarm_history_cli.CliAlarmHistorySearch, 46 | "capabilities list": capabilities_cli.CliCapabilitiesList, 47 | "alarm metrics": metrics_cli.CliMetrics, 48 | } 49 | 50 | def load_commands(self, namespace): 51 | for name, command_class in self.SHELL_COMMANDS.items(): 52 | self.add_command(name, command_class) 53 | 54 | 55 | class AodhShell(app.App): 56 | def __init__(self): 57 | super(AodhShell, self).__init__( 58 | description='Aodh command line client', 59 | version=__version__, 60 | command_manager=AodhCommandManager('aodhclient'), 61 | deferred_help=True, 62 | ) 63 | 64 | self._client = None 65 | 66 | def build_option_parser(self, description, version): 67 | """Return an argparse option parser for this application. 68 | 69 | Subclasses may override this method to extend 70 | the parser with more global options. 71 | 72 | :param description: full description of the application 73 | :paramtype description: str 74 | :param version: version number for the application 75 | :paramtype version: str 76 | """ 77 | parser = super(AodhShell, self).build_option_parser( 78 | description, version, argparse_kwargs={'allow_abbrev': False}) 79 | # Global arguments, one day this should go to keystoneauth1 80 | parser.add_argument( 81 | '--os-region-name', 82 | metavar='', 83 | dest='region_name', 84 | default=os.environ.get('OS_REGION_NAME'), 85 | help='Authentication region name (Env: OS_REGION_NAME)') 86 | parser.add_argument( 87 | '--os-interface', 88 | metavar='', 89 | dest='interface', 90 | choices=['admin', 'public', 'internal'], 91 | default=os.environ.get('OS_INTERFACE'), 92 | help='Select an interface type.' 93 | ' Valid interface types: [admin, public, internal].' 94 | ' (Env: OS_INTERFACE)') 95 | parser.add_argument( 96 | '--aodh-api-version', 97 | default=os.environ.get('AODH_API_VERSION', '2'), 98 | help='Defaults to env[AODH_API_VERSION] or 2.') 99 | loading.register_session_argparse_arguments(parser=parser) 100 | plugin = loading.register_auth_argparse_arguments( 101 | parser=parser, argv=sys.argv, default="password") 102 | 103 | if not isinstance(plugin, noauth.AodhNoAuthLoader): 104 | parser.add_argument( 105 | '--aodh-endpoint', 106 | metavar='', 107 | dest='endpoint', 108 | default=os.environ.get('AODH_ENDPOINT'), 109 | help='Aodh endpoint (Env: AODH_ENDPOINT)') 110 | 111 | return parser 112 | 113 | @property 114 | def client(self): 115 | # NOTE(sileht): we lazy load the client to not 116 | # load/connect auth stuffs 117 | if self._client is None: 118 | if hasattr(self.options, "endpoint"): 119 | endpoint_override = self.options.endpoint 120 | else: 121 | endpoint_override = None 122 | auth_plugin = loading.load_auth_from_argparse_arguments( 123 | self.options) 124 | session = loading.load_session_from_argparse_arguments( 125 | self.options, auth=auth_plugin) 126 | 127 | self._client = client.Client(self.options.aodh_api_version, 128 | session=session, 129 | interface=self.options.interface, 130 | region_name=self.options.region_name, 131 | endpoint_override=endpoint_override) 132 | return self._client 133 | 134 | def clean_up(self, cmd, result, err): 135 | if isinstance(err, exceptions.HttpError) and err.details: 136 | print(err.details, file=sys.stderr) 137 | 138 | def configure_logging(self): 139 | if self.options.debug: 140 | # --debug forces verbose_level 3 141 | # Set this here so cliff.app.configure_logging() can work 142 | self.options.verbose_level = 3 143 | 144 | super(AodhShell, self).configure_logging() 145 | root_logger = logging.getLogger('') 146 | 147 | # Set logging to the requested level 148 | if self.options.verbose_level == 0: 149 | # --quiet 150 | root_logger.setLevel(logging.ERROR) 151 | warnings.simplefilter("ignore") 152 | elif self.options.verbose_level == 1: 153 | # This is the default case, no --debug, --verbose or --quiet 154 | root_logger.setLevel(logging.WARNING) 155 | warnings.simplefilter("ignore") 156 | elif self.options.verbose_level == 2: 157 | # One --verbose 158 | root_logger.setLevel(logging.INFO) 159 | warnings.simplefilter("once") 160 | elif self.options.verbose_level >= 3: 161 | # Two or more --verbose 162 | root_logger.setLevel(logging.DEBUG) 163 | 164 | # Hide some useless message 165 | requests_log = logging.getLogger("requests") 166 | cliff_log = logging.getLogger('cliff') 167 | stevedore_log = logging.getLogger('stevedore') 168 | iso8601_log = logging.getLogger("iso8601") 169 | 170 | cliff_log.setLevel(logging.ERROR) 171 | stevedore_log.setLevel(logging.ERROR) 172 | iso8601_log.setLevel(logging.ERROR) 173 | 174 | if self.options.debug: 175 | requests_log.setLevel(logging.DEBUG) 176 | else: 177 | requests_log.setLevel(logging.ERROR) 178 | 179 | 180 | def main(args=None): 181 | if args is None: 182 | args = sys.argv[1:] 183 | return AodhShell().run(args) 184 | -------------------------------------------------------------------------------- /aodhclient/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/python-aodhclient/4ab0fce572933e4682e0ceb5f7625a39af2527be/aodhclient/tests/__init__.py -------------------------------------------------------------------------------- /aodhclient/tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/python-aodhclient/4ab0fce572933e4682e0ceb5f7625a39af2527be/aodhclient/tests/functional/__init__.py -------------------------------------------------------------------------------- /aodhclient/tests/functional/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 | import time 15 | 16 | import os_client_config 17 | from oslo_utils import uuidutils 18 | from tempest.lib.cli import base 19 | from tempest.lib import exceptions 20 | 21 | 22 | class AodhClient(object): 23 | """Aodh Client for tempest-lib 24 | 25 | This client doesn't use any authentication system 26 | """ 27 | 28 | def __init__(self): 29 | self.cli_dir = os.environ.get('AODH_CLIENT_EXEC_DIR') 30 | self.endpoint = os.environ.get('AODH_ENDPOINT') 31 | self.cloud = os.environ.get('OS_ADMIN_CLOUD', 'devstack-admin') 32 | self.user_id = uuidutils.generate_uuid() 33 | self.project_id = uuidutils.generate_uuid() 34 | 35 | def aodh(self, action, flags='', params='', 36 | fail_ok=False, merge_stderr=False): 37 | auth_args = [] 38 | if self.cloud is None: 39 | auth_args.append("--os-auth-type none") 40 | elif self.cloud != '': 41 | conf = os_client_config.OpenStackConfig() 42 | creds = conf.get_one_cloud(cloud=self.cloud).get_auth_args() 43 | auth_args.append(f"--os-auth-url {creds['auth_url']}") 44 | auth_args.append(f"--os-username {creds['username']}") 45 | auth_args.append(f"--os-password {creds['password']}") 46 | auth_args.append(f"--os-project-name {creds['project_name']}") 47 | auth_args.append(f"--os-user-domain-id {creds['user_domain_id']}") 48 | auth_args.append("--os-project-domain-id " 49 | f"{creds['project_domain_id']}") 50 | endpoint_arg = "--aodh-endpoint %s" % self.endpoint 51 | 52 | flags = " ".join(auth_args + [endpoint_arg] + [flags]) 53 | 54 | return base.execute("aodh", action, flags, params, fail_ok, 55 | merge_stderr, self.cli_dir) 56 | 57 | 58 | class ClientTestBase(base.ClientTestBase): 59 | """Base class for aodhclient tests. 60 | 61 | Establishes the aodhclient and retrieves the essential environment 62 | information. 63 | """ 64 | 65 | def _get_clients(self): 66 | return AodhClient() 67 | 68 | def retry_aodh(self, retry, *args, **kwargs): 69 | result = "" 70 | while not result.strip() and retry > 0: 71 | result = self.aodh(*args, **kwargs) 72 | if not result: 73 | time.sleep(1) 74 | retry -= 1 75 | return result 76 | 77 | def aodh(self, *args, **kwargs): 78 | return self.clients.aodh(*args, **kwargs) 79 | 80 | def get_token(self): 81 | cloud = os.environ.get('OS_ADMIN_CLOUD', 'devstack-admin') 82 | if cloud is not None and cloud != "": 83 | conf = os_client_config.OpenStackConfig() 84 | region_conf = conf.get_one_cloud(cloud=cloud) 85 | return region_conf.get_auth().get_token(region_conf.get_session()) 86 | else: 87 | return "" 88 | 89 | def details_multiple(self, output_lines, with_label=False): 90 | """Return list of dicts with item details from cli output tables. 91 | 92 | If with_label is True, key '__label' is added to each items dict. 93 | For more about 'label' see OutputParser.tables(). 94 | 95 | NOTE(sileht): come from tempest-lib just because cliff use 96 | Field instead of Property as first columun header. 97 | """ 98 | items = [] 99 | tables_ = self.parser.tables(output_lines) 100 | for table_ in tables_: 101 | if ('Field' not in table_['headers'] 102 | or 'Value' not in table_['headers']): 103 | raise exceptions.InvalidStructure() 104 | item = {} 105 | for value in table_['values']: 106 | item[value[0]] = value[1] 107 | if with_label: 108 | item['__label'] = table_['label'] 109 | items.append(item) 110 | return items 111 | -------------------------------------------------------------------------------- /aodhclient/tests/functional/test_alarm_history.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 aodhclient.tests.functional import base 17 | 18 | 19 | class AlarmHistoryTest(base.ClientTestBase): 20 | 21 | def test_help(self): 22 | self.aodh("help", params="alarm-history show") 23 | self.aodh("help", params="alarm-history search") 24 | 25 | def test_alarm_history_scenario(self): 26 | 27 | PROJECT_ID = uuidutils.generate_uuid() 28 | RESOURCE_ID = uuidutils.generate_uuid() 29 | 30 | result = self.aodh(u'alarm', 31 | params=(u"create " 32 | "--type gnocchi_resources_threshold " 33 | "--name history1 --metric cpu_util " 34 | "--threshold 5 " 35 | "--resource-id %s --resource-type generic " 36 | "--aggregation-method last " 37 | "--project-id %s" 38 | % (RESOURCE_ID, PROJECT_ID))) 39 | alarm = self.details_multiple(result)[0] 40 | ALARM_ID = alarm['alarm_id'] 41 | result = self.aodh(u'alarm', 42 | params=(u"create " 43 | "--type gnocchi_resources_threshold " 44 | "--name history2 --metric cpu_util " 45 | "--threshold 10 " 46 | "--resource-id %s --resource-type generic " 47 | "--aggregation-method last " 48 | "--project-id %s" 49 | % (RESOURCE_ID, PROJECT_ID))) 50 | alarm = self.details_multiple(result)[0] 51 | ALARM_ID2 = alarm['alarm_id'] 52 | 53 | # LIST WITH PAGINATION 54 | # list with limit 55 | result = self.aodh('alarm-history', 56 | params=("show %s --limit 1" % ALARM_ID)) 57 | alarm_list = self.parser.listing(result) 58 | self.assertEqual(1, len(alarm_list)) 59 | # list with sort key=timestamp, dir=asc 60 | result = self.aodh('alarm-history', 61 | params=("show %s --sort timestamp:asc" % ALARM_ID)) 62 | alarm_history_list = self.parser.listing(result) 63 | timestamp = [r['timestamp'] for r in alarm_history_list] 64 | sorted_timestamp = sorted(timestamp) 65 | self.assertEqual(sorted_timestamp, timestamp) 66 | # list with sort key=type dir = desc and key=timestamp, dir=asc 67 | result = self.aodh('alarm-history', 68 | params=("show %s --sort type:desc " 69 | "--sort timestamp:asc" % ALARM_ID)) 70 | alarm_history_list = self.parser.listing(result) 71 | creation = alarm_history_list.pop(-1) 72 | timestamp = [r['timestamp'] for r in alarm_history_list] 73 | sorted_timestamp = sorted(timestamp) 74 | self.assertEqual(sorted_timestamp, timestamp) 75 | self.assertEqual('creation', creation['type']) 76 | 77 | # TEST FIELDS 78 | result = self.aodh( 79 | 'alarm-history', params=("show %s" % ALARM_ID)) 80 | history = self.parser.listing(result)[0] 81 | for key in ["timestamp", "type", "detail", "event_id"]: 82 | self.assertIn(key, history) 83 | 84 | # SHOW 85 | result = self.aodh( 86 | 'alarm-history', params=("show %s" % ALARM_ID)) 87 | history = self.parser.listing(result)[0] 88 | self.assertEqual('creation', history['type']) 89 | self.assertEqual('history1', 90 | jsonutils.loads(history['detail'])['name']) 91 | 92 | result = self.aodh( 93 | 'alarm-history', params=("show %s" % ALARM_ID2)) 94 | history = self.parser.listing(result)[0] 95 | self.assertEqual('creation', history['type']) 96 | self.assertEqual('history2', 97 | jsonutils.loads(history['detail'])['name']) 98 | 99 | # SEARCH ALL 100 | result = self.aodh('alarm-history', params=("search")) 101 | self.assertIn(ALARM_ID, 102 | [r['alarm_id'] for r in self.parser.listing(result)]) 103 | self.assertIn(ALARM_ID2, 104 | [r['alarm_id'] for r in self.parser.listing(result)]) 105 | 106 | # SEARCH 107 | result = self.aodh('alarm-history', 108 | params=("search --query " 109 | "alarm_id=%s" 110 | % ALARM_ID)) 111 | history = self.parser.listing(result)[0] 112 | self.assertEqual(ALARM_ID, history["alarm_id"]) 113 | self.assertEqual('creation', history['type']) 114 | self.assertEqual('history1', 115 | jsonutils.loads(history['detail'])['name']) 116 | 117 | # CLEANUP 118 | self.aodh('alarm', params="delete %s" % ALARM_ID) 119 | self.aodh('alarm', params="delete %s" % ALARM_ID2) 120 | -------------------------------------------------------------------------------- /aodhclient/tests/functional/test_capabilities.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 aodhclient.tests.functional import base 14 | 15 | 16 | class CapabilitiesClientTest(base.ClientTestBase): 17 | def test_capabilities_scenario(self): 18 | # GET 19 | result = self.aodh('capabilities', params="list") 20 | caps = self.parser.listing(result) 21 | self.assertEqual(2, len(caps)) 22 | caps_list = sorted([cap['Field'] for cap in caps]) 23 | self.assertEqual('alarm_storage', caps_list[0]) 24 | self.assertEqual('api', caps_list[1]) 25 | -------------------------------------------------------------------------------- /aodhclient/tests/functional/test_metrics.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 aodhclient.tests.functional import base 14 | 15 | 16 | class MetricsTest(base.ClientTestBase): 17 | 18 | def test_metrics_scenario(self): 19 | # Test that the metrics command doesn't return errors 20 | params = "metrics" 21 | self.aodh('alarm', params=params) 22 | -------------------------------------------------------------------------------- /aodhclient/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/python-aodhclient/4ab0fce572933e4682e0ceb5f7625a39af2527be/aodhclient/tests/unit/__init__.py -------------------------------------------------------------------------------- /aodhclient/tests/unit/test_alarm_cli.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright IBM 2016. 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 argparse 17 | from unittest import mock 18 | 19 | import testtools 20 | 21 | from aodhclient.v2 import alarm_cli 22 | 23 | 24 | class CliAlarmCreateTest(testtools.TestCase): 25 | 26 | def setUp(self): 27 | super(CliAlarmCreateTest, self).setUp() 28 | self.app = mock.Mock() 29 | self.parser = mock.Mock() 30 | self.cli_alarm_create = ( 31 | alarm_cli.CliAlarmCreate(self.app, self.parser)) 32 | 33 | @mock.patch.object(argparse.ArgumentParser, 'error') 34 | def test_validate_args_gnocchi_resources_threshold(self, mock_arg): 35 | # Cover the test case of the method _validate_args for 36 | # gnocchi_resources_threshold 37 | parser = self.cli_alarm_create.get_parser('aodh alarm create') 38 | test_parsed_args = parser.parse_args([ 39 | '--name', 'gnocchi_resources_threshold_test', 40 | '--type', 'gnocchi_resources_threshold', 41 | '--metric', 'cpu', 42 | '--aggregation-method', 'last', 43 | '--resource-type', 'generic', 44 | '--threshold', '80' 45 | ]) 46 | self.cli_alarm_create._validate_args(test_parsed_args) 47 | mock_arg.assert_called_once_with( 48 | 'gnocchi_resources_threshold requires --metric, ' 49 | '--threshold, --resource-id, --resource-type and ' 50 | '--aggregation-method') 51 | 52 | @mock.patch.object(argparse.ArgumentParser, 'error') 53 | def test_validate_args_threshold(self, mock_arg): 54 | # Cover the test case of the method _validate_args for 55 | # threshold 56 | parser = self.cli_alarm_create.get_parser('aodh alarm create') 57 | test_parsed_args = parser.parse_args([ 58 | '--name', 'threshold_test', 59 | '--type', 'threshold', 60 | '--threshold', '80' 61 | ]) 62 | self.cli_alarm_create._validate_args(test_parsed_args) 63 | mock_arg.assert_called_once_with( 64 | 'Threshold alarm requires -m/--meter-name and ' 65 | '--threshold parameters. Meter name can be ' 66 | 'found in Ceilometer') 67 | 68 | @mock.patch.object(argparse.ArgumentParser, 'error') 69 | def test_validate_args_composite(self, mock_arg): 70 | # Cover the test case of the method _validate_args for 71 | # composite 72 | parser = self.cli_alarm_create.get_parser('aodh alarm create') 73 | test_parsed_args = parser.parse_args([ 74 | '--name', 'composite_test', 75 | '--type', 'composite' 76 | ]) 77 | self.cli_alarm_create._validate_args(test_parsed_args) 78 | mock_arg.assert_called_once_with( 79 | 'Composite alarm requires --composite-rule parameter') 80 | 81 | @mock.patch.object(argparse.ArgumentParser, 'error') 82 | def test_validate_args_gno_agg_by_resources_threshold(self, mock_arg): 83 | # Cover the test case of the method _validate_args for 84 | # gnocchi_aggregation_by_resources_threshold 85 | parser = self.cli_alarm_create.get_parser('aodh alarm create') 86 | test_parsed_args = parser.parse_args([ 87 | '--name', 'gnocchi_aggregation_by_resources_threshold_test', 88 | '--type', 'gnocchi_aggregation_by_resources_threshold', 89 | '--metric', 'cpu', 90 | '--aggregation-method', 'last', 91 | '--resource-type', 'generic', 92 | '--threshold', '80' 93 | ]) 94 | self.cli_alarm_create._validate_args(test_parsed_args) 95 | mock_arg.assert_called_once_with( 96 | 'gnocchi_aggregation_by_resources_threshold requires ' 97 | '--metric, --threshold, --aggregation-method, --query and ' 98 | '--resource-type') 99 | 100 | @mock.patch.object(argparse.ArgumentParser, 'error') 101 | def test_validate_args_gno_agg_by_metrics_threshold(self, mock_arg): 102 | # Cover the test case of the method _validate_args for 103 | # gnocchi_aggregation_by_metrics_threshold 104 | parser = self.cli_alarm_create.get_parser('aodh alarm create') 105 | test_parsed_args = parser.parse_args([ 106 | '--name', 'gnocchi_aggregation_by_metrics_threshold_test', 107 | '--type', 'gnocchi_aggregation_by_metrics_threshold', 108 | '--resource-type', 'generic', 109 | '--threshold', '80' 110 | ]) 111 | self.cli_alarm_create._validate_args(test_parsed_args) 112 | mock_arg.assert_called_once_with( 113 | 'gnocchi_aggregation_by_metrics_threshold requires ' 114 | '--metric, --threshold and --aggregation-method') 115 | 116 | @mock.patch.object(argparse.ArgumentParser, 'error') 117 | def test_validate_args_prometheus(self, mock_arg): 118 | # Cover the test case of the method _validate_args for 119 | # prometheus 120 | parser = self.cli_alarm_create.get_parser('aodh alarm create') 121 | test_parsed_args = parser.parse_args([ 122 | '--name', 'prom_test', 123 | '--type', 'prometheus', 124 | '--comparison-operator', 'gt', 125 | '--threshold', '666', 126 | ]) 127 | self.cli_alarm_create._validate_args(test_parsed_args) 128 | mock_arg.assert_called_once_with( 129 | 'Prometheus alarm requires --query and --threshold parameters.') 130 | 131 | def test_alarm_from_args(self): 132 | # The test case to cover the method _alarm_from_args 133 | parser = self.cli_alarm_create.get_parser('aodh alarm create') 134 | test_parsed_args = parser.parse_args([ 135 | '--type', 'event', 136 | '--name', 'alarm_from_args_test', 137 | '--project-id', '01919bbd-8b0e-451c-be28-abe250ae9b1b', 138 | '--user-id', '01919bbd-8b0e-451c-be28-abe250ae9c1c', 139 | '--description', 'For Test', 140 | '--state', 'ok', 141 | '--severity', 'critical', 142 | '--enabled', 'True', 143 | '--alarm-action', 'http://something/alarm', 144 | '--ok-action', 'http://something/ok', 145 | '--repeat-action', 'True', 146 | '--insufficient-data-action', 147 | 'http://something/insufficient', 148 | '--time-constraint', 149 | 'name=cons1;start="0 11 * * *";duration=300;description=desc1', 150 | '--evaluation-periods', '60', 151 | '--comparison-operator', 'le', 152 | '--threshold', '80', 153 | '--event-type', 'event', 154 | '--query', 'resource=fake-resource-id', 155 | '--granularity', '60', 156 | '--aggregation-method', 'last', 157 | '--metric', 'cpu', 158 | '--resource-id', '01919bbd-8b0e-451c-be28-abe250ae9c1c', 159 | '--resource-type', 'generic', 160 | '--stack-id', '0809ab348-8b0e-451c-be28-abe250ae9c1c', 161 | '--pool-id', '79832aabf-343ba-be28-abe250ae9c1c', 162 | '--autoscaling-group-id', 'abe250ae9c1c-79832aabf-343ba-be28' 163 | ]) 164 | 165 | # Output for the test 166 | alarm = { 167 | 'name': 'alarm_from_args_test', 168 | 'project_id': '01919bbd-8b0e-451c-be28-abe250ae9b1b', 169 | 'user_id': '01919bbd-8b0e-451c-be28-abe250ae9c1c', 170 | 'description': 'For Test', 171 | 'state': 'ok', 172 | 'severity': 'critical', 173 | 'enabled': True, 174 | 'alarm_actions': ['http://something/alarm'], 175 | 'ok_actions': ['http://something/ok'], 176 | 'insufficient_data_actions': 177 | ['http://something/insufficient'], 178 | 'time_constraints': [{'description': 'desc1', 179 | 'duration': '300', 180 | 'name': 'cons1', 181 | 'start': '0 11 * * *'}], 182 | 'repeat_actions': True, 183 | 'event_rule': { 184 | 'event_type': 'event', 185 | 'query': [{'field': 'resource', 186 | 'op': 'eq', 187 | 'type': '', 188 | 'value': 'fake-resource-id'}] 189 | }, 190 | 'threshold_rule': {'comparison_operator': 'le', 191 | 'evaluation_periods': 60, 192 | 'query': [{'field': 'resource', 193 | 'op': 'eq', 194 | 'type': '', 195 | 'value': 'fake-resource-id'}], 196 | 'threshold': 80.0}, 197 | 'prometheus_rule': {'comparison_operator': 'le', 198 | 'query': [{'field': 'resource', 199 | 'op': 'eq', 200 | 'type': '', 201 | 'value': 'fake-resource-id'}], 202 | 'threshold': 80.0}, 203 | 'gnocchi_resources_threshold_rule': { 204 | 'granularity': '60', 205 | 'metric': 'cpu', 206 | 'aggregation_method': 'last', 207 | 'evaluation_periods': 60, 208 | 'resource_id': '01919bbd-8b0e-451c-be28-abe250ae9c1c', 209 | 'comparison_operator': 'le', 210 | 'threshold': 80.0, 211 | 'resource_type': 'generic' 212 | }, 213 | 'gnocchi_aggregation_by_metrics_threshold_rule': { 214 | 'granularity': '60', 215 | 'aggregation_method': 'last', 216 | 'evaluation_periods': 60, 217 | 'comparison_operator': 'le', 218 | 'threshold': 80.0, 219 | 'metrics': ['cpu'], 220 | }, 221 | 'loadbalancer_member_health_rule': { 222 | 'autoscaling_group_id': 'abe250ae9c1c-79832aabf-343ba-be28', 223 | 'pool_id': '79832aabf-343ba-be28-abe250ae9c1c', 224 | 'stack_id': '0809ab348-8b0e-451c-be28-abe250ae9c1c' 225 | }, 226 | 'gnocchi_aggregation_by_resources_threshold_rule': { 227 | 'granularity': '60', 228 | 'metric': 'cpu', 229 | 'aggregation_method': 'last', 230 | 'evaluation_periods': 60, 231 | 'comparison_operator': 'le', 232 | 'threshold': 80.0, 233 | 'query': [{'field': 'resource', 234 | 'op': 'eq', 235 | 'type': '', 236 | 'value': 'fake-resource-id'}], 237 | 'resource_type': 'generic' 238 | }, 239 | 'composite_rule': None, 240 | 'type': 'event' 241 | } 242 | alarm_rep = self.cli_alarm_create._alarm_from_args(test_parsed_args) 243 | self.assertEqual(alarm, alarm_rep) 244 | 245 | def test_alarm_from_args_for_prometheus(self): 246 | # The test case to cover the method _alarm_from_args 247 | parser = self.cli_alarm_create.get_parser('aodh alarm create') 248 | test_parsed_args = parser.parse_args([ 249 | '--name', 'alarm_prom', 250 | '--type', 'prometheus', 251 | '--comparison-operator', 'gt', 252 | '--threshold', '666', 253 | '--query', r'some_metric{some_label="some_value"}' 254 | ]) 255 | 256 | prom_rule = {'comparison_operator': 'gt', 257 | 'query': r'some_metric{some_label="some_value"}', 258 | 'threshold': 666.0} 259 | 260 | alarm_rep = self.cli_alarm_create._alarm_from_args(test_parsed_args) 261 | self.assertEqual(prom_rule, alarm_rep['prometheus_rule']) 262 | 263 | def test_validate_time_constraint(self): 264 | starts = ['0 11 * * *', ' 0 11 * * * ', 265 | '"0 11 * * *"', '\'0 11 * * *\''] 266 | for start in starts: 267 | string = 'name=const1;start=%s;duration=1' % start 268 | expected = dict(name='const1', 269 | start='0 11 * * *', 270 | duration='1') 271 | self.assertEqual( 272 | expected, 273 | self.cli_alarm_create.validate_time_constraint(string)) 274 | 275 | def test_validate_time_constraint_with_bad_format(self): 276 | string = 'name=const2;start="0 11 * * *";duration:2' 277 | self.assertRaises(argparse.ArgumentTypeError, 278 | self.cli_alarm_create.validate_time_constraint, 279 | string) 280 | -------------------------------------------------------------------------------- /aodhclient/tests/unit/test_alarm_history.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright IBM 2016. 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 | from unittest import mock 18 | 19 | import testtools 20 | 21 | from aodhclient.v2 import alarm_history 22 | 23 | 24 | class AlarmHistoryManagerTest(testtools.TestCase): 25 | 26 | def setUp(self): 27 | super(AlarmHistoryManagerTest, self).setUp() 28 | self.client = mock.Mock() 29 | 30 | @mock.patch.object(alarm_history.AlarmHistoryManager, '_get') 31 | def test_get(self, mock_ahm): 32 | ahm = alarm_history.AlarmHistoryManager(self.client) 33 | ahm.get('01919bbd-8b0e-451c-be28-abe250ae9b1b') 34 | mock_ahm.assert_called_with( 35 | 'v2/alarms/01919bbd-8b0e-451c-be28-abe250ae9b1b/history') 36 | 37 | @mock.patch.object(alarm_history.AlarmHistoryManager, '_post') 38 | def test_search(self, mock_ahm): 39 | ahm = alarm_history.AlarmHistoryManager(self.client) 40 | q = ('{"and": [{"=": {"type": "gnocchi_resources_threshold"}}, ' 41 | '{"=": {"alarm_id": "87bacbcb-a09c-4cb9-86d0-ad410dd8ad98"}}]}') 42 | ahm.search(q) 43 | expected_called_data = ( 44 | '{"filter": "{\\"and\\": [' 45 | '{\\"=\\": {\\"type\\": \\"gnocchi_resources_threshold\\"}}, ' 46 | '{\\"=\\": {\\"alarm_id\\": ' 47 | '\\"87bacbcb-a09c-4cb9-86d0-ad410dd8ad98\\"}}]}"}') 48 | mock_ahm.assert_called_with( 49 | 'v2/query/alarms/history', 50 | data=expected_called_data, 51 | headers={'Content-Type': 'application/json'}) 52 | -------------------------------------------------------------------------------- /aodhclient/tests/unit/test_alarm_manager.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright IBM 2016. 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 testtools 17 | from unittest import mock 18 | 19 | from aodhclient.v2 import alarm 20 | 21 | 22 | class AlarmManagerTest(testtools.TestCase): 23 | 24 | def setUp(self): 25 | super(AlarmManagerTest, self).setUp() 26 | self.client = mock.Mock() 27 | self.alarms = { 28 | 'event_alarm': { 29 | 'gnocchi_aggregation_by_metrics_threshold_rule': {}, 30 | 'gnocchi_resources_threshold_rule': {}, 31 | 'name': 'event_alarm', 32 | 'gnocchi_aggregation_by_resources_threshold_rule': {}, 33 | 'event_rule': {}, 34 | 'type': 'event'} 35 | } 36 | self.results = { 37 | "result1": { 38 | "event_rule": {} 39 | }, 40 | } 41 | 42 | @mock.patch.object(alarm.AlarmManager, '_get') 43 | def test_list(self, mock_am): 44 | am = alarm.AlarmManager(self.client) 45 | am.list() 46 | mock_am.assert_called_with('v2/alarms') 47 | 48 | @mock.patch.object(alarm.AlarmManager, '_post') 49 | def test_query(self, mock_am): 50 | am = alarm.AlarmManager(self.client) 51 | query = '{"=": {"type": "event"}}' 52 | am.query(query) 53 | url = 'v2/query/alarms' 54 | expected_value = ('{"filter": "{\\"=\\": {\\"type\\":' 55 | ' \\"event\\"}}"}') 56 | headers_value = {'Content-Type': "application/json"} 57 | mock_am.assert_called_with( 58 | url, 59 | data=expected_value, 60 | headers=headers_value) 61 | 62 | @mock.patch.object(alarm.AlarmManager, '_get') 63 | def test_list_with_filters(self, mock_am): 64 | am = alarm.AlarmManager(self.client) 65 | filters = dict(type='gnocchi_resources_threshold', severity='low') 66 | am.list(filters=filters) 67 | expected_url = ( 68 | "v2/alarms?q.field=severity&q.op=eq&q.value=low&" 69 | "q.field=type&q.op=eq&q.value=gnocchi_resources_threshold") 70 | mock_am.assert_called_with(expected_url) 71 | 72 | @mock.patch.object(alarm.AlarmManager, '_get') 73 | def test_get(self, mock_am): 74 | am = alarm.AlarmManager(self.client) 75 | am.get('01919bbd-8b0e-451c-be28-abe250ae9b1b') 76 | mock_am.assert_called_with( 77 | 'v2/alarms/01919bbd-8b0e-451c-be28-abe250ae9b1b') 78 | 79 | @mock.patch.object(alarm.AlarmManager, '_delete') 80 | def test_delete(self, mock_am): 81 | am = alarm.AlarmManager(self.client) 82 | am.delete('01919bbd-8b0e-451c-be28-abe250ae9b1b') 83 | mock_am.assert_called_with( 84 | 'v2/alarms/01919bbd-8b0e-451c-be28-abe250ae9b1b') 85 | 86 | def test_clean_rules_event_alarm(self): 87 | am = alarm.AlarmManager(self.client) 88 | alarm_value = self.alarms.get('event_alarm') 89 | am._clean_rules('event', alarm_value) 90 | alarm_value.pop('type') 91 | alarm_value.pop('name') 92 | result = self.results.get("result1") 93 | self.assertEqual(alarm_value, result) 94 | -------------------------------------------------------------------------------- /aodhclient/tests/unit/test_exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Hewlett Packard Enterprise Development Company, L.P. 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 | from unittest import mock 16 | 17 | from oslotest import base 18 | 19 | from aodhclient import exceptions 20 | 21 | 22 | class AodhclientExceptionsTest(base.BaseTestCase): 23 | def test_string_format_base_exception(self): 24 | # ensure http_status has initial value N/A 25 | self.assertEqual('Unknown Error (HTTP N/A)', 26 | '%s' % exceptions.ClientException()) 27 | 28 | def test_no_match_exception_from_response(self): 29 | resp = mock.MagicMock(status_code=520) 30 | resp.headers = { 31 | 'Content-Type': 'text/plain', 32 | 'x-openstack-request-id': 'fake-request-id' 33 | } 34 | resp.text = 'Of course I still love you' 35 | e = exceptions.from_response(resp, 'http://no.where:2333/v2/alarms') 36 | self.assertIsInstance(e, exceptions.ClientException) 37 | self.assertEqual('Of course I still love you (HTTP 520) ' 38 | '(Request-ID: fake-request-id)', '%s' % e) 39 | -------------------------------------------------------------------------------- /aodhclient/tests/unit/test_metrics.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 | import testtools 15 | from unittest import mock 16 | 17 | from aodhclient.v2 import metrics_cli 18 | 19 | 20 | class MetricsTest(testtools.TestCase): 21 | def setUp(self): 22 | super(MetricsTest, self).setUp() 23 | self.app = mock.Mock() 24 | self.metrics_mgr_mock = self.app.client_manager.alarming.metrics 25 | self.parser = mock.Mock() 26 | self.metrics = ( 27 | metrics_cli.CliMetrics(self.app, self.parser)) 28 | 29 | def test_metrics(self): 30 | self.metrics_mgr_mock.get.return_value = { 31 | "evaluation_results": [ 32 | { 33 | "alarm_id": "b8e17f58-089a-43fc-a96b-e9bcac4d4b53", 34 | "project_id": "2dd8edd6c8c24f49bf04670534f6b357", 35 | "state_counters": { 36 | "insufficient data": 1646, 37 | "ok": 0, 38 | "alarm": 0 39 | } 40 | }, 41 | { 42 | "alarm_id": "fa386719-67e3-42ff-aec8-17e547dac77a", 43 | "project_id": "2dd8edd6c8c24f49bf04670534f6b357", 44 | "state_counters": { 45 | "insufficient data": 4, 46 | "ok": 923, 47 | "alarm": 0 48 | } 49 | }, 50 | { 51 | "alarm_id": "0788bf8b-fc1f-4889-b5fd-5acd4d287bc3", 52 | "project_id": "d45b070bcce04ca99546128a40854e7c", 53 | "state_counters": { 54 | "insufficient data": 1646, 55 | "ok": 0, 56 | "alarm": 0 57 | } 58 | } 59 | ] 60 | } 61 | ret = self.metrics.take_action([]) 62 | 63 | self.metrics_mgr_mock.get.assert_called_once_with() 64 | self.assertIn('name', ret[0]) 65 | self.assertIn('evaluation_result', ret[1][0]) 66 | -------------------------------------------------------------------------------- /aodhclient/tests/unit/test_quota.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 | import testtools 15 | from unittest import mock 16 | 17 | from aodhclient import exceptions 18 | from aodhclient.v2 import quota_cli 19 | 20 | 21 | class QuotaShowTest(testtools.TestCase): 22 | def setUp(self): 23 | super(QuotaShowTest, self).setUp() 24 | self.app = mock.Mock() 25 | self.quota_mgr_mock = self.app.client_manager.alarming.quota 26 | self.parser = mock.Mock() 27 | self.quota_show = ( 28 | quota_cli.QuotaShow(self.app, self.parser)) 29 | 30 | def test_quota_show(self): 31 | self.quota_mgr_mock.list.return_value = { 32 | "project_id": "fake_project", 33 | "quotas": [ 34 | { 35 | "limit": 20, 36 | "resource": "alarms" 37 | } 38 | ] 39 | } 40 | parser = self.quota_show.get_parser('') 41 | args = parser.parse_args(['--project', 'fake_project']) 42 | # Something like [('alarms',), (20,)] 43 | ret = list(self.quota_show.take_action(args)) 44 | 45 | self.quota_mgr_mock.list.assert_called_once_with( 46 | project='fake_project') 47 | self.assertIn('alarms', ret[0]) 48 | self.assertIn(20, ret[1]) 49 | 50 | 51 | class QuotaSetTest(testtools.TestCase): 52 | def setUp(self): 53 | super(QuotaSetTest, self).setUp() 54 | self.app = mock.Mock() 55 | self.quota_mgr_mock = self.app.client_manager.alarming.quota 56 | self.parser = mock.Mock() 57 | self.quota_set = ( 58 | quota_cli.QuotaSet(self.app, self.parser)) 59 | 60 | def test_quota_set(self): 61 | self.quota_mgr_mock.create.return_value = { 62 | "project_id": "fake_project", 63 | "quotas": [ 64 | { 65 | "limit": 20, 66 | "resource": "alarms" 67 | } 68 | ] 69 | } 70 | 71 | parser = self.quota_set.get_parser('') 72 | args = parser.parse_args(['fake_project', '--alarm', '20']) 73 | ret = list(self.quota_set.take_action(args)) 74 | 75 | self.quota_mgr_mock.create.assert_called_once_with( 76 | 'fake_project', [{'resource': 'alarms', 'limit': 20}]) 77 | self.assertIn('alarms', ret[0]) 78 | self.assertIn(20, ret[1]) 79 | 80 | def test_quota_set_invalid_quota(self): 81 | parser = self.quota_set.get_parser('') 82 | args = parser.parse_args(['fake_project', '--alarm', '-2']) 83 | 84 | self.assertRaises(exceptions.CommandError, 85 | self.quota_set.take_action, 86 | args) 87 | -------------------------------------------------------------------------------- /aodhclient/tests/unit/test_shell.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Hewlett Packard Enterprise Development Company, L.P. 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 io 16 | import sys 17 | from unittest import mock 18 | 19 | from keystoneauth1 import exceptions 20 | import testtools 21 | 22 | from aodhclient import shell 23 | 24 | 25 | class CliTest(testtools.TestCase): 26 | 27 | @mock.patch('sys.stderr', io.StringIO()) 28 | def test_cli_http_error_with_details(self): 29 | shell.AodhShell().clean_up( 30 | None, None, exceptions.HttpError('foo', details='bar')) 31 | stderr_lines = sys.stderr.getvalue().splitlines() 32 | self.assertEqual(1, len(stderr_lines)) 33 | self.assertEqual('bar', stderr_lines[0]) 34 | 35 | @mock.patch('sys.stderr', io.StringIO()) 36 | def test_cli_http_error_without_details(self): 37 | shell.AodhShell().clean_up(None, None, exceptions.HttpError('foo')) 38 | stderr_lines = sys.stderr.getvalue().splitlines() 39 | self.assertEqual(0, len(stderr_lines)) 40 | -------------------------------------------------------------------------------- /aodhclient/tests/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 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 | from oslotest import base 16 | 17 | from aodhclient import utils 18 | 19 | 20 | class SearchQueryBuilderTest(base.BaseTestCase): 21 | def _do_test(self, expr, expected): 22 | req = utils.search_query_builder(expr) 23 | self.assertEqual(expected, req) 24 | 25 | def test_search_query_builder(self): 26 | self._do_test('foo=bar', {"=": {"foo": "bar"}}) 27 | self._do_test('foo!=1', {"!=": {"foo": 1.0}}) 28 | self._do_test('foo=True', {"=": {"foo": True}}) 29 | self._do_test('foo=null', {"=": {"foo": None}}) 30 | self._do_test('foo="null"', {"=": {"foo": "null"}}) 31 | 32 | self._do_test('not (foo="quote" or foo="what!" ' 33 | 'or bar="who?")', 34 | {"not": {"or": [ 35 | {"=": {"bar": "who?"}}, 36 | {"=": {"foo": "what!"}}, 37 | {"=": {"foo": "quote"}}, 38 | ]}}) 39 | 40 | self._do_test('(foo="quote" or not foo="what!" ' 41 | 'or bar="who?") and cat="meme"', 42 | {"and": [ 43 | {"=": {"cat": "meme"}}, 44 | {"or": [ 45 | {"=": {"bar": "who?"}}, 46 | {"not": {"=": {"foo": "what!"}}}, 47 | {"=": {"foo": "quote"}}, 48 | ]} 49 | ]}) 50 | 51 | self._do_test('foo="quote" or foo="what!" ' 52 | 'or bar="who?" and cat="meme"', 53 | {"or": [ 54 | {"and": [ 55 | {"=": {"cat": "meme"}}, 56 | {"=": {"bar": "who?"}}, 57 | ]}, 58 | {"=": {"foo": "what!"}}, 59 | {"=": {"foo": "quote"}}, 60 | ]}) 61 | 62 | self._do_test('foo="quote" and foo="what!" ' 63 | 'or bar="who?" or cat="meme"', 64 | {'or': [ 65 | {'=': {'cat': 'meme'}}, 66 | {'=': {'bar': 'who?'}}, 67 | {'and': [ 68 | {'=': {'foo': 'what!'}}, 69 | {'=': {'foo': 'quote'}} 70 | ]} 71 | ]}) 72 | 73 | 74 | class CliQueryToArray(base.BaseTestCase): 75 | def test_cli_query_to_arrary(self): 76 | cli_query = "this<=34;that=string::foo" 77 | ret_array = utils.cli_to_array(cli_query) 78 | expected_query = [ 79 | {"field": "this", "type": "", "value": "34", "op": "le"}, 80 | {"field": "that", "type": "string", "value": "foo", "op": "eq"}] 81 | self.assertEqual(expected_query, ret_array) 82 | -------------------------------------------------------------------------------- /aodhclient/utils.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 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 | import re 15 | from urllib import parse as urllib_parse 16 | 17 | import pyparsing as pp 18 | 19 | uninary_operators = ("not", ) 20 | binary_operator = (u">=", u"<=", u"!=", u">", u"<", u"=", u"==", u"eq", u"ne", 21 | u"lt", u"gt", u"ge", u"le") 22 | multiple_operators = (u"and", u"or") 23 | 24 | operator = pp.Regex(u"|".join(binary_operator)) 25 | null = pp.Regex("None|none|null").setParseAction(pp.replaceWith(None)) 26 | boolean = "False|True|false|true" 27 | boolean = pp.Regex(boolean).setParseAction(lambda t: t[0].lower() == "true") 28 | hex_string = lambda n: pp.Word(pp.hexnums, exact=n) # noqa: E731 29 | uuid = pp.Combine(hex_string(8) + ("-" + hex_string(4)) * 3 + 30 | "-" + hex_string(12)) 31 | number = r"[+-]?\d+(:?\.\d*)?(:?[eE][+-]?\d+)?" 32 | number = pp.Regex(number).setParseAction(lambda t: float(t[0])) 33 | identifier = pp.Word(pp.alphas, pp.alphanums + "_") 34 | quoted_string = pp.QuotedString('"') | pp.QuotedString("'") 35 | comparison_term = pp.Forward() 36 | in_list = pp.Group(pp.Suppress('[') + 37 | pp.Optional(pp.delimitedList(comparison_term)) + 38 | pp.Suppress(']'))("list") 39 | comparison_term << (null | boolean | uuid | identifier | number | 40 | quoted_string) 41 | condition = pp.Group(comparison_term + operator + comparison_term) 42 | 43 | expr = pp.infixNotation(condition, [ 44 | ("not", 1, pp.opAssoc.RIGHT, ), 45 | ("and", 2, pp.opAssoc.LEFT, ), 46 | ("or", 2, pp.opAssoc.LEFT, ), 47 | ]) 48 | 49 | OP_LOOKUP = {'!=': 'ne', 50 | '>=': 'ge', 51 | '<=': 'le', 52 | '>': 'gt', 53 | '<': 'lt', 54 | '=': 'eq'} 55 | 56 | OP_LOOKUP_KEYS = '|'.join(sorted(OP_LOOKUP.keys(), key=len, reverse=True)) 57 | OP_SPLIT_RE = re.compile(r'(%s)' % OP_LOOKUP_KEYS) 58 | 59 | 60 | def _parsed_query2dict(parsed_query): 61 | result = None 62 | while parsed_query: 63 | part = parsed_query.pop() 64 | if part in binary_operator: 65 | result = {part: {parsed_query.pop(): result}} 66 | 67 | elif part in multiple_operators: 68 | if result.get(part): 69 | result[part].append( 70 | _parsed_query2dict(parsed_query.pop())) 71 | else: 72 | result = {part: [result]} 73 | 74 | elif part in uninary_operators: 75 | result = {part: result} 76 | elif isinstance(part, pp.ParseResults): 77 | kind = part.getName() 78 | if kind == "list": 79 | res = part.asList() 80 | else: 81 | res = _parsed_query2dict(part) 82 | if result is None: 83 | result = res 84 | elif isinstance(result, dict): 85 | list(result.values())[0].append(res) 86 | else: 87 | result = part 88 | return result 89 | 90 | 91 | def search_query_builder(query): 92 | parsed_query = expr.parseString(query)[0] 93 | return _parsed_query2dict(parsed_query) 94 | 95 | 96 | def list2cols(cols, objs): 97 | return cols, [tuple([o[k] for k in cols]) 98 | for o in objs] 99 | 100 | 101 | def format_string_list(objs, field): 102 | objs[field] = ", ".join(objs[field]) 103 | 104 | 105 | def format_dict_list(objs, field): 106 | objs[field] = "\n".join( 107 | "- " + ", ".join("%s: %s" % (k, v) 108 | for k, v in elem.items()) 109 | for elem in objs[field]) 110 | 111 | 112 | def format_move_dict_to_root(obj, field): 113 | for attr in obj[field]: 114 | obj["%s/%s" % (field, attr)] = obj[field][attr] 115 | del obj[field] 116 | 117 | 118 | def format_archive_policy(ap): 119 | format_dict_list(ap, "definition") 120 | format_string_list(ap, "aggregation_methods") 121 | 122 | 123 | def dict_from_parsed_args(parsed_args, attrs): 124 | d = {} 125 | for attr in attrs: 126 | if attr == "metric": 127 | if parsed_args.metrics: 128 | value = parsed_args.metrics[0] 129 | else: 130 | value = None 131 | else: 132 | value = getattr(parsed_args, attr) 133 | if value is not None: 134 | if value == [""]: 135 | # NOTE(jake): As options like --alarm-actions is an array, 136 | # their value can be array with empty string if user set option 137 | # to ''. In this case we set it to None here so that a None 138 | # value gets sent to API. 139 | d[attr] = None 140 | else: 141 | d[attr] = value 142 | return d 143 | 144 | 145 | def dict_to_querystring(objs): 146 | return "&".join(["%s=%s" % (k, v) 147 | for k, v in objs.items() 148 | if v is not None]) 149 | 150 | 151 | def cli_to_array(cli_query): 152 | """Convert CLI list of queries to the Python API format. 153 | 154 | This will convert the following: 155 | "this<=34;that=string::foo" 156 | to 157 | "[{field=this,op=le,value=34,type=''}, 158 | {field=that,op=eq,value=foo,type=string}]" 159 | 160 | """ 161 | 162 | opts = [] 163 | queries = cli_query.split(';') 164 | for q in queries: 165 | try: 166 | field, q_operator, type_value = OP_SPLIT_RE.split(q, maxsplit=1) 167 | except ValueError: 168 | raise ValueError('Invalid or missing operator in query %(q)s,' 169 | 'the supported operators are: %(k)s' % 170 | {'q': q, 'k': OP_LOOKUP.keys()}) 171 | if not field: 172 | raise ValueError('Missing field in query %s' % q) 173 | if not type_value: 174 | raise ValueError('Missing value in query %s' % q) 175 | opt = dict(field=field, op=OP_LOOKUP[q_operator]) 176 | 177 | if '::' not in type_value: 178 | opt['type'], opt['value'] = '', type_value 179 | else: 180 | opt['type'], _, opt['value'] = type_value.partition('::') 181 | 182 | if opt['type'] and opt['type'] not in ( 183 | 'string', 'integer', 'float', 'datetime', 'boolean'): 184 | err = ('Invalid value type %(type)s, the type of value' 185 | 'should be one of: integer, string, float, datetime,' 186 | ' boolean.' % opt) 187 | raise ValueError(err) 188 | opts.append(opt) 189 | return opts 190 | 191 | 192 | def get_pagination_options(limit=None, marker=None, sorts=None): 193 | options = [] 194 | if limit: 195 | options.append("limit=%d" % limit) 196 | if marker: 197 | options.append("marker=%s" % urllib_parse.quote(marker)) 198 | for sort in sorts or []: 199 | options.append("sort=%s" % urllib_parse.quote(sort)) 200 | return "&".join(options) 201 | 202 | 203 | def get_client(obj): 204 | if hasattr(obj.app, 'client_manager'): 205 | # NOTE(liusheng): cliff objects loaded by OSC 206 | return obj.app.client_manager.alarming 207 | else: 208 | # TODO(liusheng): Remove this when OSC is able 209 | # to install the aodh client binary itself 210 | return obj.app.client 211 | -------------------------------------------------------------------------------- /aodhclient/v2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstack/python-aodhclient/4ab0fce572933e4682e0ceb5f7625a39af2527be/aodhclient/v2/__init__.py -------------------------------------------------------------------------------- /aodhclient/v2/alarm.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 oslo_serialization import jsonutils 15 | 16 | from aodhclient import utils 17 | from aodhclient.v2 import alarm_cli 18 | from aodhclient.v2 import base 19 | 20 | 21 | class AlarmManager(base.Manager): 22 | 23 | url = "v2/alarms" 24 | 25 | @staticmethod 26 | def _filtersdict_to_url(filters): 27 | urls = [] 28 | for k, v in sorted(filters.items()): 29 | url = "q.field=%s&q.op=eq&q.value=%s" % (k, v) 30 | urls.append(url) 31 | return '&'.join(urls) 32 | 33 | def list(self, filters=None, limit=None, 34 | marker=None, sorts=None): 35 | """List alarms. 36 | 37 | :param filters: A dict includes filters parameters, for example, 38 | {'type': 'gnocchi_resources_threshold', 39 | 'severity': 'low'} represent filters to query alarms 40 | with type='gnocchi_resources_threshold' and 41 | severity='low'. 42 | :type filters: dict 43 | :param limit: maximum number of resources to return 44 | :type limit: int 45 | :param marker: the last item of the previous page; we return the next 46 | results after this value. 47 | :type marker: str 48 | :param sorts: list of resource attributes to order by. 49 | :type sorts: list of str 50 | """ 51 | pagination = utils.get_pagination_options(limit, marker, sorts) 52 | filter_string = (self._filtersdict_to_url(filters) if 53 | filters else "") 54 | url = self.url 55 | options = [] 56 | if filter_string: 57 | options.append(filter_string) 58 | if pagination: 59 | options.append(pagination) 60 | if options: 61 | url += "?" + "&".join(options) 62 | return self._get(url).json() 63 | 64 | def query(self, query=None): 65 | """Query alarms. 66 | 67 | :param query: A json format complex query expression, like this: 68 | '{"=":{"type":"gnocchi_resources_threshold"}}', this 69 | expression is used to query all the 70 | gnocchi_resources_threshold type alarms. 71 | :type query: json 72 | """ 73 | query = {'filter': query} 74 | url = "v2/query/alarms" 75 | return self._post(url, 76 | headers={'Content-Type': "application/json"}, 77 | data=jsonutils.dumps(query)).json() 78 | 79 | def get(self, alarm_id): 80 | """Get an alarm 81 | 82 | :param alarm_id: ID of the alarm 83 | :type alarm_id: str 84 | """ 85 | return self._get(self.url + '/' + alarm_id).json() 86 | 87 | @staticmethod 88 | def _clean_rules(alarm_type, alarm): 89 | for rule in alarm_cli.ALARM_TYPES: 90 | if rule != alarm_type: 91 | alarm.pop('%s_rule' % rule, None) 92 | 93 | def create(self, alarm): 94 | """Create an alarm 95 | 96 | :param alarm: the alarm 97 | :type alarm: dict 98 | """ 99 | self._clean_rules(alarm['type'], alarm) 100 | return self._post( 101 | self.url, headers={'Content-Type': "application/json"}, 102 | data=jsonutils.dumps(alarm)).json() 103 | 104 | def update(self, alarm_id, alarm_update): 105 | """Update an alarm 106 | 107 | :param alarm_id: ID of the alarm 108 | :type alarm_id: str 109 | :param attributes: Attributes of the alarm 110 | :type attributes: dict 111 | """ 112 | alarm = self._get(self.url + '/' + alarm_id).json() 113 | if 'type' not in alarm_update: 114 | self._clean_rules(alarm['type'], alarm_update) 115 | else: 116 | self._clean_rules(alarm_update['type'], alarm_update) 117 | 118 | if 'prometheus_rule' in alarm_update: 119 | rule = alarm_update.get('prometheus_rule') 120 | alarm['prometheus_rule'].update(rule) 121 | alarm_update.pop('prometheus_rule') 122 | elif 'threshold_rule' in alarm_update: 123 | alarm['threshold_rule'].update(alarm_update.get('threshold_rule')) 124 | alarm_update.pop('threshold_rule') 125 | elif 'event_rule' in alarm_update: 126 | if ('type' in alarm_update and 127 | alarm_update['type'] != alarm['type']): 128 | alarm.pop('%s_rule' % alarm['type'], None) 129 | alarm['event_rule'] = alarm_update['event_rule'] 130 | else: 131 | alarm['event_rule'].update(alarm_update.get('event_rule')) 132 | alarm_update.pop('event_rule') 133 | elif 'gnocchi_resources_threshold_rule' in alarm_update: 134 | if ('type' in alarm_update and 135 | alarm_update['type'] != alarm['type']): 136 | alarm.pop('%s_rule' % alarm['type'], None) 137 | alarm['gnocchi_resources_threshold_rule'] = alarm_update[ 138 | 'gnocchi_resources_threshold_rule'] 139 | else: 140 | alarm['gnocchi_resources_threshold_rule'].update( 141 | alarm_update.get('gnocchi_resources_threshold_rule')) 142 | alarm_update.pop('gnocchi_resources_threshold_rule') 143 | elif 'gnocchi_aggregation_by_metrics_threshold_rule' in alarm_update: 144 | if ('type' in alarm_update and 145 | alarm_update['type'] != alarm['type']): 146 | alarm.pop('%s_rule' % alarm['type'], None) 147 | alarm['gnocchi_aggregation_by_metrics_threshold_rule'] = \ 148 | alarm_update[ 149 | 'gnocchi_aggregation_by_metrics_threshold_rule'] 150 | else: 151 | alarm['gnocchi_aggregation_by_metrics_threshold_rule'].update( 152 | alarm_update.get( 153 | 'gnocchi_aggregation_by_metrics_threshold_rule')) 154 | alarm_update.pop('gnocchi_aggregation_by_metrics_threshold_rule') 155 | elif 'gnocchi_aggregation_by_resources_threshold_rule' in alarm_update: 156 | if ('type' in alarm_update and 157 | alarm_update['type'] != alarm['type']): 158 | alarm.pop('%s_rule' % alarm['type'], None) 159 | alarm['gnocchi_aggregation_by_resources_threshold_rule'] = \ 160 | alarm_update[ 161 | 'gnocchi_aggregation_by_resources_threshold_rule'] 162 | else: 163 | alarm['gnocchi_aggregation_by_resources_threshold_rule'].\ 164 | update(alarm_update.get( 165 | 'gnocchi_aggregation_by_resources_threshold_rule')) 166 | alarm_update.pop( 167 | 'gnocchi_aggregation_by_resources_threshold_rule') 168 | elif 'composite_rule' in alarm_update: 169 | if ('type' in alarm_update and 170 | alarm_update['type'] != alarm['type']): 171 | alarm.pop('%s_rule' % alarm['type'], None) 172 | if alarm_update['composite_rule'] is not None: 173 | alarm['composite_rule'] = alarm_update[ 174 | 'composite_rule'] 175 | alarm_update.pop('composite_rule') 176 | elif 'loadbalancer_member_health_rule' in alarm_update: 177 | if ('type' in alarm_update and 178 | alarm_update['type'] != alarm['type']): 179 | alarm.pop('%s_rule' % alarm['type'], None) 180 | if alarm_update['loadbalancer_member_health_rule'] is not None: 181 | alarm['loadbalancer_member_health_rule'] = alarm_update[ 182 | 'loadbalancer_member_health_rule'] 183 | alarm_update.pop('loadbalancer_member_health_rule') 184 | 185 | alarm.update(alarm_update) 186 | 187 | return self._put( 188 | self.url + '/' + alarm_id, 189 | headers={'Content-Type': "application/json"}, 190 | data=jsonutils.dumps(alarm)).json() 191 | 192 | def delete(self, alarm_id): 193 | """Delete an alarm 194 | 195 | :param alarm_id: ID of the alarm 196 | :type alarm_id: str 197 | """ 198 | self._delete(self.url + '/' + alarm_id) 199 | 200 | def get_state(self, alarm_id): 201 | """Get the state of an alarm 202 | 203 | :param alarm_id: ID of the alarm 204 | :type alarm_id: str 205 | """ 206 | return self._get(self.url + '/' + alarm_id + '/state').json() 207 | 208 | def set_state(self, alarm_id, state): 209 | """Set the state of an alarm 210 | 211 | :param alarm_id: ID of the alarm 212 | :type alarm_id: str 213 | :param state: the state to be updated to the alarm 214 | :type state: str 215 | """ 216 | return self._put(self.url + '/' + alarm_id + '/state', 217 | headers={'Content-Type': "application/json"}, 218 | data='"%s"' % state 219 | ).json() 220 | -------------------------------------------------------------------------------- /aodhclient/v2/alarm_cli.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 | import argparse 15 | 16 | from cliff import command 17 | from cliff import lister 18 | from cliff import show 19 | from oslo_serialization import jsonutils 20 | from oslo_utils import strutils 21 | from oslo_utils import uuidutils 22 | 23 | from aodhclient import exceptions 24 | from aodhclient.i18n import _ 25 | from aodhclient import utils 26 | 27 | ALARM_TYPES = ['prometheus', 'event', 'composite', 'threshold', 28 | 'gnocchi_resources_threshold', 29 | 'gnocchi_aggregation_by_metrics_threshold', 30 | 'gnocchi_aggregation_by_resources_threshold', 31 | 'loadbalancer_member_health'] 32 | ALARM_STATES = ['ok', 'alarm', 'insufficient data'] 33 | ALARM_SEVERITY = ['low', 'moderate', 'critical'] 34 | ALARM_OPERATORS = ['lt', 'le', 'eq', 'ne', 'ge', 'gt'] 35 | ALARM_OP_MAP = dict(zip(ALARM_OPERATORS, ('<', '<=', '=', '!=', '>=', '>'))) 36 | STATISTICS = ['max', 'min', 'avg', 'sum', 'count'] 37 | ALARM_LIST_COLS = ['alarm_id', 'type', 'name', 'state', 'severity', 'enabled'] 38 | 39 | 40 | class CliAlarmList(lister.Lister): 41 | """List alarms""" 42 | 43 | @staticmethod 44 | def split_filter_param(param): 45 | key, eq_op, value = param.partition('=') 46 | if not eq_op: 47 | msg = 'Malformed parameter(%s). Use the key=value format.' % param 48 | raise ValueError(msg) 49 | return key, value 50 | 51 | def get_parser(self, prog_name): 52 | parser = super(CliAlarmList, self).get_parser(prog_name) 53 | exclusive_group = parser.add_mutually_exclusive_group() 54 | exclusive_group.add_argument("--query", 55 | help="Rich query supported by aodh, " 56 | "e.g. project_id!=my-id " 57 | "user_id=foo or user_id=bar") 58 | exclusive_group.add_argument('--filter', dest='filter', 59 | metavar='', 60 | type=self.split_filter_param, 61 | action='append', 62 | help='Filter parameters to apply on' 63 | ' returned alarms.') 64 | parser.add_argument("--limit", type=int, metavar="", 65 | help="Number of resources to return " 66 | "(Default is server default)") 67 | parser.add_argument("--marker", metavar="", 68 | help="Last item of the previous listing. " 69 | "Return the next results after this value," 70 | "the supported marker is alarm_id.") 71 | parser.add_argument("--sort", action="append", 72 | metavar="", 73 | help="Sort of resource attribute, " 74 | "e.g. name:asc") 75 | return parser 76 | 77 | def take_action(self, parsed_args): 78 | if parsed_args.query: 79 | if any([parsed_args.limit, parsed_args.sort, parsed_args.marker]): 80 | raise exceptions.CommandError( 81 | "Query and pagination options are mutually " 82 | "exclusive.") 83 | query = jsonutils.dumps( 84 | utils.search_query_builder(parsed_args.query)) 85 | alarms = utils.get_client(self).alarm.query(query=query) 86 | else: 87 | filters = dict(parsed_args.filter) if parsed_args.filter else None 88 | alarms = utils.get_client(self).alarm.list( 89 | filters=filters, sorts=parsed_args.sort, 90 | limit=parsed_args.limit, marker=parsed_args.marker) 91 | return utils.list2cols(ALARM_LIST_COLS, alarms) 92 | 93 | 94 | def _format_alarm(alarm): 95 | if alarm.get('composite_rule'): 96 | composite_rule = jsonutils.dumps(alarm['composite_rule'], indent=2) 97 | alarm['composite_rule'] = composite_rule 98 | return alarm 99 | for alarm_type in ALARM_TYPES: 100 | if alarm.get('%s_rule' % alarm_type): 101 | alarm.update(alarm.pop('%s_rule' % alarm_type)) 102 | if alarm["time_constraints"]: 103 | alarm["time_constraints"] = jsonutils.dumps(alarm["time_constraints"], 104 | sort_keys=True, 105 | indent=2) 106 | # only works for threshold and event alarm 107 | if isinstance(alarm.get('query'), list): 108 | query_rows = [] 109 | for q in alarm['query']: 110 | op = ALARM_OP_MAP.get(q['op'], q['op']) 111 | query_rows.append('%s %s %s' % (q['field'], op, q['value'])) 112 | alarm['query'] = ' AND\n'.join(query_rows) 113 | return alarm 114 | 115 | 116 | def _find_alarm_by_name(client, name): 117 | # then try to get entity as name 118 | query = jsonutils.dumps({"=": {"name": name}}) 119 | alarms = client.alarm.query(query) 120 | if len(alarms) > 1: 121 | msg = (_("Multiple alarms matches found for '%s', " 122 | "use an ID to be more specific.") % name) 123 | raise exceptions.NoUniqueMatch(msg) 124 | elif not alarms: 125 | msg = (_("Alarm %s not found") % name) 126 | raise exceptions.NotFound(msg) 127 | else: 128 | return alarms[0] 129 | 130 | 131 | def _find_alarm_id_by_name(client, name): 132 | alarm = _find_alarm_by_name(client, name) 133 | return alarm['alarm_id'] 134 | 135 | 136 | def _check_name_and_id_coexist(parsed_args, action): 137 | if parsed_args.id and parsed_args.name: 138 | raise exceptions.CommandError( 139 | "You should provide only one of " 140 | "alarm ID and alarm name(--name) " 141 | "to %s an alarm." % action) 142 | 143 | 144 | def _check_name_and_id_exist(parsed_args, action): 145 | if not parsed_args.id and not parsed_args.name: 146 | msg = (_("You need to specify one of " 147 | "alarm ID and alarm name(--name) " 148 | "to %s an alarm.") % action) 149 | raise exceptions.CommandError(msg) 150 | 151 | 152 | def _check_name_and_id(parsed_args, action): 153 | _check_name_and_id_coexist(parsed_args, action) 154 | _check_name_and_id_exist(parsed_args, action) 155 | 156 | 157 | def _add_name_to_parser(parser, required=False): 158 | parser.add_argument('--name', metavar='', 159 | required=required, 160 | help='Name of the alarm') 161 | return parser 162 | 163 | 164 | def _add_id_to_parser(parser): 165 | parser.add_argument("id", nargs='?', 166 | metavar='', 167 | help="ID or name of an alarm.") 168 | return parser 169 | 170 | 171 | class CliAlarmShow(show.ShowOne): 172 | """Show an alarm""" 173 | 174 | def get_parser(self, prog_name): 175 | return _add_name_to_parser( 176 | _add_id_to_parser( 177 | super(CliAlarmShow, self).get_parser(prog_name))) 178 | 179 | def take_action(self, parsed_args): 180 | _check_name_and_id(parsed_args, 'query') 181 | c = utils.get_client(self) 182 | if parsed_args.name: 183 | alarm = _find_alarm_by_name(c, parsed_args.name) 184 | else: 185 | if uuidutils.is_uuid_like(parsed_args.id): 186 | try: 187 | alarm = c.alarm.get(alarm_id=parsed_args.id) 188 | except exceptions.NotFound: 189 | # Maybe it's a name 190 | alarm = _find_alarm_by_name(c, parsed_args.id) 191 | else: 192 | alarm = _find_alarm_by_name(c, parsed_args.id) 193 | 194 | return self.dict2columns(_format_alarm(alarm)) 195 | 196 | 197 | class CliAlarmCreate(show.ShowOne): 198 | """Create an alarm""" 199 | 200 | create = True 201 | 202 | def get_parser(self, prog_name): 203 | parser = _add_name_to_parser( 204 | super(CliAlarmCreate, self).get_parser(prog_name), 205 | required=self.create) 206 | 207 | parser.add_argument('-t', '--type', metavar='', 208 | required=self.create, 209 | choices=ALARM_TYPES, 210 | help='Type of alarm, should be one of: ' 211 | '%s.' % ', '.join(ALARM_TYPES)) 212 | parser.add_argument('--project-id', metavar='', 213 | help='Project to associate with alarm ' 214 | '(configurable by admin users only)') 215 | parser.add_argument('--user-id', metavar='', 216 | help='User to associate with alarm ' 217 | '(configurable by admin users only)') 218 | parser.add_argument('--description', metavar='', 219 | help='Free text description of the alarm') 220 | parser.add_argument('--state', metavar='', 221 | choices=ALARM_STATES, 222 | help='State of the alarm, one of: ' 223 | + str(ALARM_STATES)) 224 | parser.add_argument('--severity', metavar='', 225 | choices=ALARM_SEVERITY, 226 | help='Severity of the alarm, one of: ' 227 | + str(ALARM_SEVERITY)) 228 | parser.add_argument('--enabled', type=strutils.bool_from_string, 229 | metavar='{True|False}', 230 | help=('True if alarm evaluation is enabled')) 231 | parser.add_argument('--alarm-action', dest='alarm_actions', 232 | metavar='', action='append', 233 | help=('URL to invoke when state transitions to ' 234 | 'alarm. May be used multiple times')) 235 | parser.add_argument('--ok-action', dest='ok_actions', 236 | metavar='', action='append', 237 | help=('URL to invoke when state transitions to ' 238 | 'OK. May be used multiple times')) 239 | parser.add_argument('--insufficient-data-action', 240 | dest='insufficient_data_actions', 241 | metavar='', action='append', 242 | help=('URL to invoke when state transitions to ' 243 | 'insufficient data. May be used multiple ' 244 | 'times')) 245 | parser.add_argument( 246 | '--time-constraint', dest='time_constraints', 247 | metavar='