././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9468198 watcher_dashboard-16.0.0/0000775000175000017500000000000015257165054014076 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.816848 watcher_dashboard-16.0.0/.pre-commit-config.yaml0000664000175000017500000000303415257165015020354 0ustar00zuulzuul--- repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: # whitespace - id: mixed-line-ending args: ['--fix', 'lf'] exclude: '.*\.(svg)$' - id: fix-byte-order-marker # file format and permissions - id: debug-statements - id: check-json files: .*\.json$ - id: check-yaml files: .*\.(yaml|yml)$ - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable # git - id: check-added-large-files - id: check-case-conflict - id: detect-private-key - id: check-merge-conflict exclude: '.*\.(rst|inc)$' - repo: https://opendev.org/openstack/hacking rev: 8.0.0 hooks: - id: hacking additional_dependencies: [] exclude: '^(doc|releasenotes|tools)/.*$' # Pin matches CI/pre-commit reproducibility; bump when upgrading project-wide. - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.4 hooks: - id: ruff-check args: ['--fix', '--unsafe-fixes'] - id: ruff-format - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: - id: codespell args: ['--ignore-words=doc/dictionary.txt'] - repo: https://github.com/sphinx-contrib/sphinx-lint rev: v1.0.2 hooks: - id: sphinx-lint args: [--enable=default-role] files: ^doc/|^releasenotes/|^api-guide/ types: [rst] - repo: https://github.com/PyCQA/doc8 rev: v2.0.0 hooks: - id: doc8 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.816848 watcher_dashboard-16.0.0/.stestr.conf0000664000175000017500000000010415257165015016337 0ustar00zuulzuul[DEFAULT] test_path=./watcher_dashboard/test/integration top_dir=./ ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.816848 watcher_dashboard-16.0.0/.zuul.yaml0000664000175000017500000000310715257165015016035 0ustar00zuulzuul- job: name: watcher-dashboard-integration-tests-playwright parent: devstack nodeset: devstack-single-node-debian-bookworm pre-run: playbooks/playwright/pre.yaml run: playbooks/playwright/run.yaml post-run: playbooks/playwright/post.yaml required-projects: - name: openstack/horizon - name: openstack/watcher-dashboard - name: openstack/watcher - name: openstack/python-watcherclient roles: - zuul: openstack/devstack - zuul: openstack/horizon vars: zuul_copy_output: '{{ zuul.project.src_dir }}/playwright': logs devstack_plugins: watcher: https://opendev.org/openstack/watcher watcher-dashboard: https://opendev.org/openstack/watcher-dashboard devstack_services: horizon: true watcher-api: true watcher-decision-engine: true watcher-applier: true s-account: false s-container: false s-object: false s-proxy: false tox_envlist: integration-playwright tox_environment: PLAYWRIGHT_BROWSER: webkit PLAYWRIGHT_VIDEO: retain-on-failure PLAYWRIGHT_TRACE: retain-on-failure PLAYWRIGHT_HEADLESS: true - project: templates: - check-requirements - horizon-non-primary-django-jobs - openstack-python3-jobs-horizon - openstack-cover-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: jobs: - watcher-dashboard-integration-tests-playwright gate: jobs: - watcher-dashboard-integration-tests-playwright ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.8928196 watcher_dashboard-16.0.0/AUTHORS0000664000175000017500000000504415257165054015151 0ustar00zuulzuulAkihiro Motoki Alexander Chadin Alfredo Moralejo Andreas Jaeger Anh Tran Antoine Cabot BubaVV Cao Xuan Hoang Chandan Kumar (raukadah) Chandan Kumar Corey Bryant Dantali0n David TARDIVEL Doug Hellmann Erik Olof Gunnar Andersson Flavio Percoco Ghanshyam Mann Ian Wienand Ivan Anfimov Ivan Kolodyazhny Kim Bao Long Larry Rensing Li Wei M V P Nitesh Martin Kopec Michal Arbet Nguyen Hai OpenStack Release Bot Qian Min Chen Rajiv Kumar Sean McGinnis Sean Mooney ShangXiao Stephen Finucane Stephen Finucane Swapnil Kulkarni (coolsvap) Takashi Kajinami Thomas Goirand Tony Breeds Vincent Francoise Vladimir Ostroverkhov YuehuiLei YuehuiLei Yumeng Bao YumengBao Yumeng_Bao avnish caoyuan chenke gaofei gujin huang.zhiping jacky06 licanwei limin.lc lingyongxu malei manchandavishal melissaml morenod pengyuesheng petergurinov qingszhao rajat29 ricolin zhangboye zhangguoqing zhulingjie zhurong zte-hanrong ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.8728197 watcher_dashboard-16.0.0/ChangeLog0000664000175000017500000002640215257165054015654 0ustar00zuulzuulCHANGES ======= 16.0.0 ------ * Register test cleanup before assertions can fail * Improvements to Playwright artifacts * Fix audit list filter to use goal and strategy * Disable playwright parallel execution * Add Playwright tests to validate goals and strategies * Switch to admin project before Playwright tests * Use ID-based selector for login button * Add HTTPS certificate error handling for Playwright tests * Drop unused python-subunit * Add Playwright integration test for skip action workflow * Add Playwright-based E2E testing framework * Add directory for locale * tox: Bump min\_version to 4.28.0 * Remove unused Angular references * Replace status\_choices with display\_choices * Fix releasenotes tox environment missing dependencies * refactor: centralise Django settings access in config.py * test: fix efficacy\_specifications fixture to be a list * test: strengthen error-path tests and suppress log noise * test: remove Selenium and integration tests * Update Babel configuration * test: Restructure test directory with unit/ and local\_fixtures/ * style: replace autopep8 with ruff-format; restrict hacking to H-rules * ruff: Add isort config and fix per-file-ignores * Fix fontawesome.css patch * Update master for stable/2026.1 15.0.0.0rc1 ----------- * Add skip action UI controls * Add microversion 1.5 API support and status\_message display * Fix UnboundLocalError in get\_related\_efficacy\_indicators\_data * Add microversion discovery and gating for API calls * Simplify selected goal retrieval in Audit template CreateForm * Centralize strategy display\_name access into a shared helper * Rename 'wv' to 'common\_client' in client helper module * Migrate setup configuration to pyproject.toml * ruff: Enable E5 check * Use yaml format policy file * reno: Update master for unmaintained/2024.1 * Improve audit creation model and add text wrapping * Adopt Ruff for security; remove Bandit and fix style * Fix pre-commit hook failures and add pre-commit config * Minor fixes from previous code reviews * Add Start and END time fields for continuous audit * List strategies based on selected goal * Show parameter spec in the strategy info page * Remove url tags from README * Update master for stable/2025.2 * Add support for passing parameters while creating audit * Remove password parameter from watcher client initialization 14.0.0 ------ * finalize python 3.9 support removal * add pyproject.toml to support pip 23.1 * Update master for stable/2025.1 * tox: Drop envdir 13.0.0 ------ * Allow cancel audit when it is ongoing or pending * Support multiple global\_efficacy indicators in action plans table * Bump hacking * reno: Update master for unmaintained/2023.1 * Enable Watcher dashboard only when backend is on * Fix efficacy indicators in action plans * Update python versions, drop py3.8 * [devstack] use $PYTHON when configuring horizon * Drop unnecessary 'x' bit from doc config file * Update master for stable/2024.2 * reno: Update master for unmaintained/zed 12.0.0 ------ * Update master for stable/2024.1 * reno: Update master for unmaintained/xena * reno: Update master for unmaintained/wallaby * reno: Update master for unmaintained/victoria 11.0.0 ------ * reno: Update master for unmaintained/yoga * Update python classifier in setup.cfg * Drop use of features removed in Django 4.0 * Update master for stable/2023.2 10.0.0 ------ * Update master for stable/2023.1 9.0.0 ----- * Adjust tox.ini for tox4 * Replace deprecated inspect.getargspec * Switch to 2023.1 Python3 unit tests and generic template name * Update master for stable/zed * Django 4.x: Replace removed features 8.0.0 ----- * Add Python3 zed unit tests * Update master for stable/yoga 7.0.0 ----- * Add Python3 yoga unit tests * Update master for stable/xena 6.0.0 ----- * Fix broken devstack install * Enable tox cover jobs on zuul * Repair coverage report tox job * Changed minversion in tox to 3.18.0 * Manage constraints with testenv install\_command * Add Python3 xena unit tests * doc: Update our IRC server to OFTC * Fix create audit fail from audit\_template * Use py3 as the default runtime for tox * setup.cfg: Replace dashes with underscores * Dropping lower constraints testing * Update master for stable/wallaby 5.0.0 ----- * Bump py37 to py38 in tox.ini * Add Python3 wallaby unit tests * Update master for stable/victoria 4.0.0 ----- * Cleanup for Refactor-error-messages * [goal] Migrate testing to ubuntu focal * Switch to newer openstackdocstheme and reno versions * Fix hacking min version to 3.0.1 * Remove the mock * Add py38 package metadata * Add Python3 victoria unit tests * Update master for stable/ussuri 3.0.0 ----- * Cleanup py27 support * Fix pyScss version in lower-constraints.txt * s/assertItemsEqual/assertCountEqual/g * Drop Django 1.11 support * translation: drop babel extractor definitions * Drop python 2.7 support and testing * tox: Keeping going with docs and cleanup setup.cfg * Switch to official Ussuri jobs * Use Horizon project template for django jobs * TypeError exception when tests with Django22 * Update master for stable/train 2.0.0 ----- * Build pdf docs * Add blueprints link for README * Refactor error messages * Remove the unused code * Blacklist sphinx 2.1.0 (autodoc bug) * Add py37 in tox.ini and setup.cfg file * Switch to the new canonical constraints URL on master * Sync Sphinx requirement * Replace git.openstack.org URLs with opendev.org URLs * OpenDev Migration Patch * Remove py35 * Update master for stable/stein * Add python 3.7 unit test job for watcher-dashboard * Replace openstack.org git:// URLs with https:// 1.12.0 ------ * Update hacking version * Use template for lower-constraints * Update devel info: mailing list * Change openstack-dev to openstack-discuss * Removed older version of python added 3.5 * Update min tox version to 2.0 * Fix Invalid filter: parse\_isotime * switch documentation job to new PTI * import zuul job settings from project-config * Add test 'node\_modules' in gitignore * Imported Translations from Zanata * Switch test runner to django default runner * Update reno for stable/rocky 1.11.0 ------ * fix audit delete failure: add allow func to filter audit * fix indicator exception * fix audit delete failure * Add name for audit: check if audit\_name already exists 1.10.0 ------ * Add name for audit * Set interval parameter to optional and add validation before audit create * Drop mox3 from test-requirements.txt * Add release note in README * Django 2.0 support and fix lower-constraints * fix tox python3 overrides * Explicitly use django\_nose.NoseTestSuiteRunner 1.9.0 ----- * Remove mox usage from api tests * Remove mox usage from audittemplate tests * Remove mox usage from strategy tests * Remove mox usage from goal tests * Updated from global requirements * add lower-constraints job * Delete the unnecessary '-' * Install horizon directly from pypi * Update links in README * Updated from global requirements * Imported Translations from Zanata * Fix url links and spelling error in docs * Imported Translations from Zanata * Update reno for stable/queens 1.8.0 ----- * Update unreachable link in installation doc 1.7.0 ----- * Updated from global requirements * Updated from global requirements * Cleanup test-requirements 1.6.0 ----- * Update audit\_template create help message * Remove setting of version/release from releasenotes * Updated from global requirements * Drop django\_openstack\_auth from requirements.txt * removed horizon from test\_requestent.txt * Remove the dulpicate description * Imported Translations from Zanata 1.5.0 ----- * Updated from global requirements * enable watcher-dashboard service in devstack * Add formatting marks in README.rst * Fix to use "." to source script files * Updated from global requirements * Add support for cron syntax * Update reno for stable/pike 1.4.0 ----- * Update url in README.rst * Update the documentation link for doc migration * Imported Translations from Zanata * Updated from global requirements 1.3.0 ----- * Updated from global requirements * Imported Translations from Zanata * Update docs to the new standards * Remove load url from future and fix gate * Add DS\_Store to gitignore * switch to openstackdocstheme * Add rm to whitelist\_externals in tox.ini 1.2.0 ----- * Updated from global requirements * Fix watcher-dashboard deployment error * Fixing Tox and optimize the link address * Updated from global requirements 1.1.0 ----- * Updated from global requirements * Add a button to create audit template in audit creat form * Fix exception error when creating audit without audit template * Adding horizon time filters to format the time in detail page * Replace default:"—" with default:\_("-") * Using enable\_plugin instend of enable\_service * Remove the extra required=True * Remove the empty file form.py * Updated from global requirements * [Fix gate]Update test requirement * Remove unused logging import * Updated from global requirements * Update the path of watcher dashboard enable file * Refactor the watcher dashboard enable file * Update reno for stable/ocata 1.0.0 ----- * Add [watcher] tag when using OpenStack ML 0.8.0 ----- * Updated from global requirements * Remove link to modindex * 'next\_uuid' Action field is replaced by 'parents' * Updated from global requirements * Remove support for py26 * Replaces yaml.load() with yaml.safe\_load() * Fix bad parsing of HTTP response * Removed unnecessary utf-8 encoding * Remove useless ddt requirements * Remove useless pytz requirements * Add page title for some panel table * Remove the pep8 ingore * Add reno for release notes management * Remove the 'MANIFEST.in' * Add Constraints support * Add auto\_trigger option in Audit creation form 0.7.0 ----- * Show team and repo badges on README * Moved README.rst content to installation.rst doc * Remove unnecessary watcher\_dashboard/models.py file * Updated from global requirements * ceilometer has been removed from openstack-dashboard 0.6.0 ----- * Updated from global requirements * Added the audit scope field in dashboard * Updated from global requirements 0.5.0 ----- * Added action details * Updated from global requirements * Clean the code by renaming 'type' parameters * Drop \*openstack/common\* in flake8 exclude * Add new audit fields in dashboard tables * Update table's actions * Fix py27 commands failed * Replace UUID reference by name * modify the home-page info with the developer documentation * interval param should be set to None by default * Replace audit\_filter by audit * Remove 'Go to Audit Template' from Audit view 0.4.0 ----- * Remove discover from test-requirements * Updated from global requirements * Updated from global requirements * Add policies for API access control to watcher-dashboard * Do not pass interval parameter for ONESHOT audit * Add support continuously-optimization * Revert "Initializes EfficacyIndicator attributes" * Initializes EfficacyIndicator attributes * Modify error message from 'action\_plan' to 'audit' * Rename type keyword across the project * Replaced UUID of goal with name * Added efficacy-related fields to the dashboard 0.3.0 ----- * Added Goals and Strategies to Dashboard * Add fix for ONESHOT type * Removed unused 'alarm' field * Fix api test about AuditTemplate * update test-requirements to get horizon faster 0.2.0 ----- * Renamed 'TRIGGERED' state to 'PENDING' 0.1.0 ----- * Watcher Dashboard - Initial commit * Added .gitreview ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.817848 watcher_dashboard-16.0.0/HACKING.rst0000664000175000017500000000445515257165015015701 0ustar00zuulzuulContributing ============ The code repository is located at `OpenStack `__. Please go there if you want to check it out: git clone https://github.com/openstack/watcher-dashboard.git The list of bugs and blueprints is on Launchpad: ``__ We use OpenStack's Gerrit for the code contributions: ``__ and we follow the `OpenStack Gerrit Workflow `__. If you're interested in the code, here are some key places to start: * `watcher_dashboard/api.py `_ - This file contains all the API calls made to the Watcher API (through python-watcherclient). * `watcher_dashboard/infra_optim `_ - The Watcher Dashboard code is contained within this directory. Running tests ============= There are several ways to run tests for watcher-dashboard. Using ``tox``: This is the easiest way to run tests. When run, tox installs dependencies, prepares the virtual python environment, then runs test commands. The gate tests in gerrit usually also use tox to run tests. For available tox environments, see ``tox.ini``. By running ``run_tests.sh``: Tests can also be run using the ``run_tests.sh`` script, to see available options, run it with the ``--help`` option. It handles preparing the virtual environment and executing tests, but in contrast with tox, it does not install all dependencies, e.g. ``jshint`` must be installed before running the jshint testcase. Manual tests: To manually check watcher-dashboard, it is possible to run a development server for watcher-dashboard by running ``run_tests.sh --runserver``. To run the server with the settings used by the test environment: ``run_tests.sh --runserver 0.0.0.0:8000`` OpenStack Style Commandments ============================ - Step 1: Read https://www.python.org/dev/peps/pep-0008/ - Step 2: Read https://www.python.org/dev/peps/pep-0008/ again - Step 3: Read https://github.com/openstack/hacking/blob/master/HACKING.rst ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.817848 watcher_dashboard-16.0.0/LICENSE0000664000175000017500000002363715257165015015113 0ustar00zuulzuul Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9468198 watcher_dashboard-16.0.0/PKG-INFO0000644000175000017500000000376615257165054015205 0ustar00zuulzuulMetadata-Version: 2.4 Name: watcher-dashboard Version: 16.0.0 Summary: Watcher Management Dashboard Author-email: OpenStack License: Apache-2.0 Project-URL: Homepage, https://docs.openstack.org/watcher-dashboard/latest/ Project-URL: Repository, https://opendev.org/openstack/watcher-dashboard Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: pbr!=2.1.0,>=2.0.0 Requires-Dist: horizon>=18.2.0 Requires-Dist: oslo.utils>=8.0.0 Requires-Dist: PyYAML>=3.12 Requires-Dist: python-watcherclient>=4.9.0 Dynamic: license-file Dynamic: requires-dist ============================================== OpenStack Dashboard plugin for Watcher project ============================================== .. image:: https://governance.openstack.org/tc/badges/watcher-dashboard.svg .. Change things from this point on The Watcher dashboard is a Horizon plugin that will allow users to realize a wide range of cloud optimization goals. * Free software: Apache license * Documentation: https://docs.openstack.org/watcher-dashboard/latest * Source: https://opendev.org/openstack/watcher-dashboard * Bugs: https://bugs.launchpad.net/watcher-dashboard * Release Notes: https://docs.openstack.org/releasenotes/watcher-dashboard * Blueprints: https://blueprints.launchpad.net/watcher-dashboard ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.817848 watcher_dashboard-16.0.0/README.rst0000664000175000017500000000132615257165015015564 0ustar00zuulzuul============================================== OpenStack Dashboard plugin for Watcher project ============================================== .. image:: https://governance.openstack.org/tc/badges/watcher-dashboard.svg .. Change things from this point on The Watcher dashboard is a Horizon plugin that will allow users to realize a wide range of cloud optimization goals. * Free software: Apache license * Documentation: https://docs.openstack.org/watcher-dashboard/latest * Source: https://opendev.org/openstack/watcher-dashboard * Bugs: https://bugs.launchpad.net/watcher-dashboard * Release Notes: https://docs.openstack.org/releasenotes/watcher-dashboard * Blueprints: https://blueprints.launchpad.net/watcher-dashboard ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.817848 watcher_dashboard-16.0.0/babel-django.cfg0000664000175000017500000000023315257165015017057 0ustar00zuulzuul[python: watcher_dashboard/**.py] [django: watcher_dashboard/**/templates/**.html] [extractors] django = horizon.utils.babel_extract_django:extract_django ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.817848 watcher_dashboard-16.0.0/babel-djangojs.cfg0000664000175000017500000000024015257165015017412 0ustar00zuulzuul[javascript: watcher_dashboard/**.js] [angular: watcher_dashboard/**/static/**.html] [extractors] angular = horizon.utils.babel_extract_angular:extract_angular ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9318197 watcher_dashboard-16.0.0/devstack/0000775000175000017500000000000015257165054015702 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.817848 watcher_dashboard-16.0.0/devstack/local.conf.example0000664000175000017500000000017515257165015021275 0ustar00zuulzuul# settings file for watcher-dashboard plugin enable_plugin watcher-dashboard https://opendev.org/openstack/watcher-dashboard ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8188481 watcher_dashboard-16.0.0/devstack/plugin.sh0000664000175000017500000000357715257165015017545 0ustar00zuulzuul# plugin.sh - DevStack plugin.sh dispatch script watcher-dashboard WATCHER_DASHBOARD_DIR=$(cd $(dirname $BASH_SOURCE)/.. && pwd) function install_watcher_dashboard { setup_develop ${WATCHER_DASHBOARD_DIR} } function configure_watcher_dashboard { cp -a ${WATCHER_DASHBOARD_DIR}/watcher_dashboard/local/enabled/* ${DEST}/horizon/openstack_dashboard/local/enabled/ cp -a ${WATCHER_DASHBOARD_DIR}/watcher_dashboard/conf/* ${DEST}/horizon/openstack_dashboard/conf/ } function init_watcher_dashboard { # Setup alias for django-admin which could be different depending on distro $PYTHON ${DEST}/horizon/manage.py collectstatic --noinput $PYTHON ${DEST}/horizon/manage.py compress --force } # check for service enabled if is_service_enabled watcher-dashboard; then if [[ "$1" == "stack" && "$2" == "pre-install" ]]; then # Set up system services # no-op : elif [[ "$1" == "stack" && "$2" == "install" ]]; then # Perform installation of service source echo_summary "Installing Watcher Dashboard" install_watcher_dashboard elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then # Configure after the other layer 1 and 2 services have been configured echo_summary "Configurng Watcher Dashboard" configure_watcher_dashboard init_watcher_dashboard elif [[ "$1" == "stack" && "$2" == "extra" ]]; then echo_summary "Adding admin to demo project for multi-project testing" get_or_add_user_project_role admin admin demo : fi if [[ "$1" == "unstack" ]]; then rm -f ${DEST}/horizon/openstack_dashboard/local/enabled/_310* rm -f ${DEST}/horizon/openstack_dashboard/conf/watcher* fi if [[ "$1" == "clean" ]]; then # Remove state and transient data # Remember clean.sh first calls unstack.sh # no-op : fi fi ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8188481 watcher_dashboard-16.0.0/devstack/settings0000664000175000017500000000013215257165015017456 0ustar00zuulzuul# DevStack settings # Enable Watcher dashboard services enable_service watcher-dashboard ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9318197 watcher_dashboard-16.0.0/doc/0000775000175000017500000000000015257165054014643 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8188481 watcher_dashboard-16.0.0/doc/dictionary.txt0000664000175000017500000000000015257165015017534 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8188481 watcher_dashboard-16.0.0/doc/requirements.txt0000664000175000017500000000013415257165015020122 0ustar00zuulzuulopenstackdocstheme>=2.2.1 # Apache-2.0 reno>=3.1.0 # Apache-2.0 sphinx>=2.0.0,!=2.1.0 # BSD ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9318197 watcher_dashboard-16.0.0/doc/source/0000775000175000017500000000000015257165054016143 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8188481 watcher_dashboard-16.0.0/doc/source/conf.py0000664000175000017500000000765215257165015017451 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'openstackdocstheme', ] wsme_protocols = ['restjson'] # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Watcher Dashboard' copyright = 'OpenStack Foundation' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. # The full version, including alpha/beta/rc tags. # release = # The short X.Y version. # version = watcher_version.version_info.version_string() # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['watcher.'] exclude_patterns = [ # The man directory includes some snippet files that are included # in other documents during the build but that should not be # included in the toctree themselves, so tell Sphinx to ignore # them when scanning for input files. 'man/footer.rst', 'man/general-options.rst', ] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- Options for man page output -------------------------------------------- # Grouping the document tree for man pages. # List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual' man_pages = [] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' # html_static_path = ['static'] # html_theme_options = {'incubating': True} # html_theme_options = {"show_other_versions": "True"} html_theme = 'openstackdocs' # Output file base name for HTML help builder. htmlhelp_basename = f'{project}doc' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ( 'index', 'doc-watcher-dashboard.tex', f'{project} Documentation', 'OpenStack Foundation', 'manual', ) ] # Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664 latex_use_xindy = False latex_domain_indices = False latex_elements = { 'makeindex': '', 'printindex': '', 'preamble': r'\setcounter{tocdepth}{3}', } # Example configuration for intersphinx: refer to the Python standard library. # intersphinx_mapping = {'http://docs.python.org/': None} # openstackdocstheme options openstackdocs_repo_name = 'openstack/watcher-dashboard' openstackdocs_pdf_link = True openstackdocs_auto_name = False openstackdocs_bug_project = 'watcher-dashboard' openstackdocs_bug_tag = '' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9328196 watcher_dashboard-16.0.0/doc/source/contributor/0000775000175000017500000000000015257165054020515 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8188481 watcher_dashboard-16.0.0/doc/source/contributor/contributing.rst0000664000175000017500000000406615257165015023761 0ustar00zuulzuul.. Except where otherwise noted, this document is licensed under Creative Commons Attribution 3.0 License. You can view the license at: https://creativecommons.org/licenses/by/3.0/ .. _contributing: ======================= Contributing to Watcher ======================= If you're interested in contributing to the Watcher project, the following will help get you started. Contributor License Agreement ----------------------------- .. index:: single: license; agreement In order to contribute to the Watcher project, you need to have signed OpenStack's contributor's agreement. .. seealso:: * https://docs.openstack.org/infra/manual/developers.html * https://wiki.openstack.org/wiki/CLA LaunchPad Project ----------------- Most of the tools used for OpenStack depend on a launchpad.net ID for authentication. After signing up for a launchpad account, join the "openstack" team to have access to the mailing list and receive notifications of important events. .. seealso:: * https://launchpad.net/ * https://launchpad.net/watcher * https://launchpad.net/watcher-dashboard * https://launchpad.net/~openstack Project Hosting Details ----------------------- Bug tracker https://launchpad.net/watcher-dashboard Blueprints https://blueprints.launchpad.net/watcher-dashboard Mailing list (prefix subjects with ``[watcher]`` for faster responses) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss Wiki https://wiki.openstack.org/wiki/Watcher Code Hosting https://opendev.org/openstack/watcher-dashboard Code Review https://review.opendev.org/#/q/status:open+project:openstack/watcher-dashboard,n,z IRC Channel ``#openstack-watcher`` (changelog_) Weekly Meetings On Wednesdays at 14:00 UTC on even weeks in the ``#openstack-meeting-4`` IRC channel, 13:00 UTC on odd weeks in the ``#openstack-meeting-alt`` IRC channel (`meetings logs`_) .. _changelog: http://eavesdrop.openstack.org/irclogs/%23openstack-watcher/ .. _meetings logs: http://eavesdrop.openstack.org/meetings/watcher/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8188481 watcher_dashboard-16.0.0/doc/source/contributor/index.rst0000664000175000017500000000010415257165015022346 0ustar00zuulzuul.. toctree:: :maxdepth: 1 contributing playwright-testing ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8188481 watcher_dashboard-16.0.0/doc/source/contributor/playwright-testing.rst0000664000175000017500000001244615257165015025120 0ustar00zuulzuul=============================== Playwright Integration Tests =============================== This guide covers how to set up and run Playwright-based integration tests for the Watcher Dashboard. Prerequisites ============= Before running Playwright tests, create the tox virtualenv and install the browser binary along with its system dependencies. This is a one-time setup per system. .. code-block:: bash cd watcher-dashboard # 1. Create the tox venv (installs pinned Playwright version) tox -e venv -- python3 -m playwright --version # 2. Install OS-level libraries required by the browser sudo .tox/venv/bin/playwright install-deps webkit # 3. Install the browser binary .tox/venv/bin/python3 -m playwright install webkit # For other browsers, replace 'webkit' with 'chromium' or 'firefox' .. note:: In CI, these same steps are performed automatically by ``playbooks/playwright/pre.yaml``. Tox itself does not install browser binaries. Running Tests ============= Basic Usage ----------- .. code-block:: bash # Source OpenStack credentials first source ~/repos/devstack/openrc admin admin # Default: WebKit, headless — run all integration tests tox -e integration-playwright -- integration # Run a specific test module (stestr regex) tox -e integration-playwright -- test_playwright_audit_workflow # Run a single test method (full dotted path) tox -e integration-playwright -- watcher_dashboard.test.integration.test_playwright_audit_workflow.AuditWorkflowTests.test_launch_audit_and_verify_action_plan # Run tests matching a pattern tox -e integration-playwright -- "test_audit" Recording Modes --------------- Playwright captures screenshots, videos, and traces for debugging. .. list-table:: Recording Configuration :header-rows: 1 :widths: 15 25 15 45 * - Artifact - Environment Variable - Default - Description * - Screenshots - ``PLAYWRIGHT_SCREENSHOTS`` - ``True`` - Explicit screenshots taken at key test actions. Set to ``False`` to disable. * - Video - ``PLAYWRIGHT_VIDEO`` - ``off`` - Record video of browser during test execution. * - Trace - ``PLAYWRIGHT_TRACE`` - ``off`` - Detailed trace (DOM snapshots, network, console logs). .. list-table:: Video and Trace Modes :header-rows: 1 :widths: 20 80 * - Mode - Behavior * - ``off`` - No recording (default). * - ``on`` - Always record and keep artifacts. * - ``retain-on-failure`` - Record but delete if test passes. **Recommended for CI**. .. note:: CI runs use ``retain-on-failure`` for both video and trace — this is already configured in ``.zuul.yaml`` and requires no manual setup. Viewing Traces ^^^^^^^^^^^^^^ Upload trace zip files to https://trace.playwright.dev (drag and drop), or: .. code-block:: bash .tox/integration-playwright/bin/playwright show-trace \ playwright/traces/trace_.zip Environment Variables --------------------- .. list-table:: :header-rows: 1 :widths: 35 65 * - Variable - Description * - ``PLAYWRIGHT_BROWSER`` - Browser: ``chromium``, ``firefox``, or ``webkit`` (default) * - ``PLAYWRIGHT_HEADLESS`` - Headless mode: ``True`` (default) or ``False`` * - ``PLAYWRIGHT_TIMEOUT`` - Default timeout in ms (default: ``30000``) * - ``PLAYWRIGHT_OUTPUT_DIR`` - Base directory for all artifacts (default: ``playwright/``) * - ``PLAYWRIGHT_AUTH_REUSE`` - Reuse login state: ``True`` (default) or ``False`` * - ``PLAYWRIGHT_IGNORE_HTTPS_ERRORS`` - Ignore HTTPS certificate errors: ``True`` or ``False`` (default) * - ``WATCHER_DASHBOARD_URL`` - Dashboard URL (default: ``http://localhost/dashboard``) * - ``OS_USERNAME`` - Login username (default: ``admin``) * - ``OS_PASSWORD`` - Login password (default: ``secretadmin``) * - ``OS_INSECURE`` - Disable SSL verification: ``True`` (default) or ``False`` Artifacts are stored under ``PLAYWRIGHT_OUTPUT_DIR`` (default: ``playwright/``): .. code-block:: text playwright/ ├── .auth/ # Saved authentication state ├── screenshots/ # Explicit, named screenshots from tests ├── videos/ # Test execution videos └── traces/ # Trace zip files .. note:: The ``playwright/`` directory is automatically deleted at the start of each tox run to ensure clean test artifacts. Writing Tests ------------- .. code-block:: python from watcher_dashboard.test.integration import playwright_base from playwright import sync_api as playwright_sync class MyTests(playwright_base.PlaywrightTestCase): def test_create_template(self): template_name = "my_template" # Create with automatic API cleanup; returns the UUID self.create_audit_template(template_name) # Verify the template appears in the table row = self.page.get_by_role("row").filter(has_text=template_name) self.expect(row).to_be_visible() The base class logs in at the start of each test and registers API cleanup for created audit templates and audits using ``python-watcherclient``. Screenshots are taken explicitly in the base helpers and tests using ``self.take_screenshot("name")`` so the output names describe the action. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8188481 watcher_dashboard-16.0.0/doc/source/index.rst0000664000175000017500000000342215257165015020002 0ustar00zuulzuul.. Except where otherwise noted, this document is licensed under Creative Commons Attribution 3.0 License. You can view the license at: https://creativecommons.org/licenses/by/3.0/ ========================================== Welcome to Watcher Dashboard documentation ========================================== OpenStack Watcher provides a flexible and scalable resource optimization service for multi-tenant OpenStack-based clouds. Watcher provides a complete optimization loop—including everything from a metrics receiver, complex event processor and profiler, optimization processor and an action plan applier. This provides a robust framework to realize a wide range of cloud optimization goals, including the reduction of data center operating costs, increased system performance via intelligent virtual machine migration, increased energy efficiency and more! Watcher project consists of several source code repositories: * `watcher`_ - is the main repository. It contains code for Watcher API server, Watcher Decision Engine and Watcher Applier. * `python-watcherclient`_ - Client library and CLI client for Watcher. * `watcher-dashboard`_ - Watcher Horizon plugin. The documentation provided here is continually kept up-to-date based on the latest code, and may not represent the state of the project at any specific prior release. .. _watcher: https://opendev.org/openstack/watcher/ .. _python-watcherclient: https://opendev.org/openstack/python-watcherclient/ .. _watcher-dashboard: https://opendev.org/openstack/watcher-dashboard/ Install Guide ============= .. toctree:: :maxdepth: 1 install/index Developer Guide =============== .. toctree:: :maxdepth: 1 contributor/index Indices and tables ================== * :ref:`genindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9328196 watcher_dashboard-16.0.0/doc/source/install/0000775000175000017500000000000015257165054017611 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8188481 watcher_dashboard-16.0.0/doc/source/install/index.rst0000664000175000017500000000005615257165015021450 0ustar00zuulzuul.. toctree:: :maxdepth: 1 installation ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.819848 watcher_dashboard-16.0.0/doc/source/install/installation.rst0000664000175000017500000000641215257165015023044 0ustar00zuulzuulInstallation ------------ First off, create a virtual environment and install the Horizon dependencies:: $ git clone https://github.com/openstack/horizon $ cd horizon $ python tools/install_venv.py We will refer to the folder you are now in as ````. If you want more details on how to install Horizon, you can have a look at the `Horizon documentation`_, especially their `quickstart tutorial`_. Then, you need to install Watcher Dashboard on the server running Horizon. To do so, you can issue the following commands:: $ git clone https://opendev.org/openstack/watcher-dashboard $ cd watcher-dashboard $ pip install -e . We will refer to the folder you are now in as ````. The next step is now to register the Watcher Dashboard plugins against your Horizon. To do so, you can execute the ``tools/register_plugin.sh``:: $ cd $ ./tools/register_plugin.sh . This script will then create the needed symlinks within Horizon so that it can load the Watcher plugin when it starts. If you wish to have Horizon running being an Apache server, do not forget to start the service via the following command:: $ sudo service apache2 restart For more details on how to configure Horizon for a production environment, you can refer to their online `installation guide`_. .. _Horizon documentation: https://docs.openstack.org/horizon/latest .. _quickstart tutorial: https://docs.openstack.org/horizon/latest/contributor/quickstart.html .. _installation guide: https://docs.openstack.org/horizon/latest/install/index.html DevStack setup -------------- Add the following to your DevStack ``local.conf`` file :: enable_plugin watcher-dashboard https://opendev.org/openstack/watcher-dashboard Unit testing ------------ First of all, you have to create an environment to run your tests in. This step is actually part of the ``run_tests.sh`` script which creates and maintains a clean virtual environment. Here below is the basic command to run Watcher Dashboard tests:: $ ./run_tests.sh The first time you will issue the command above, you will be asked if you want to create a virtual environment. So unless you have installed everything manually (in which case you should use the ``-N`` flag), you need to accept .. note:: As of the Mitaka release, the dashboard for watcher is now maintained outside of the Horizon codebase, in this repository. Policies -------- You can enable policies on Watcher ``Optimization`` panel, by updating in the ``/openstack_dashboard/settings.py`` configuration file the following parameters .. code-block:: python POLICY_FILES = { ... 'infra-optim': 'watcher_policy.yaml', } You can also update the file ``/openstack_dashboard/conf/watcher_policy.conf`` to customize your policies. Links ----- Watcher project: https://opendev.org/openstack/watcher/ Watcher at github: https://github.com/openstack/watcher Watcher at wiki.openstack.org: https://wiki.openstack.org/wiki/Watcher Launchpad project: https://launchpad.net/watcher Join us on IRC (Internet Relay Chat):: Network: OFTC (https://www.oftc.net/) Channel: #openstack-watcher Or send an email to openstack-discuss@lists.openstack.org using [watcher] in object ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.819848 watcher_dashboard-16.0.0/manage.py0000775000175000017500000000146715257165015015710 0ustar00zuulzuul#!/usr/bin/env python # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import sys from django.core.management import execute_from_command_line if __name__ == "__main__": os.environ.setdefault( "DJANGO_SETTINGS_MODULE", "watcher_dashboard.tests.settings" ) execute_from_command_line(sys.argv) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9268198 watcher_dashboard-16.0.0/playbooks/0000775000175000017500000000000015257165054016101 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9328196 watcher_dashboard-16.0.0/playbooks/playwright/0000775000175000017500000000000015257165054020273 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.819848 watcher_dashboard-16.0.0/playbooks/playwright/post.yaml0000664000175000017500000000011415257165015022135 0ustar00zuulzuul--- - hosts: all roles: - fetch-tox-output - fetch-subunit-output ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.819848 watcher_dashboard-16.0.0/playbooks/playwright/pre.yaml0000664000175000017500000000166615257165015021753 0ustar00zuulzuul--- - hosts: all roles: - run-devstack - test-setup - ensure-tox tasks: - name: Create tox venv with Playwright dependencies ansible.builtin.command: >- {{ tox_executable }} -e venv -- python3 -m playwright --version args: chdir: "{{ zuul.project.src_dir }}" environment: TOX_CONSTRAINTS_FILE: "{{ ansible_user_dir }}/src/opendev.org/openstack/requirements/upper-constraints.txt" - name: Install Playwright system dependencies ansible.builtin.command: >- {{ zuul.project.src_dir }}/.tox/venv/bin/playwright install-deps {{ tox_environment.PLAYWRIGHT_BROWSER }} become: true - name: Install Playwright browser ansible.builtin.command: >- {{ zuul.project.src_dir }}/.tox/venv/bin/python3 -m playwright install {{ tox_environment.PLAYWRIGHT_BROWSER }} environment: PLAYWRIGHT_BROWSERS_PATH: "{{ zuul.project.src_dir }}/.playwright" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.819848 watcher_dashboard-16.0.0/playbooks/playwright/run.yaml0000664000175000017500000000004415257165015021756 0ustar00zuulzuul--- - hosts: all roles: - tox ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.849848 watcher_dashboard-16.0.0/pyproject.toml0000664000175000017500000000441115257165015017007 0ustar00zuulzuul[build-system] requires = ["pbr>=6.1.1"] build-backend = "pbr.build" [project] name = "watcher-dashboard" description = "Watcher Management Dashboard" authors = [ {name = "OpenStack", email = "openstack-discuss@lists.openstack.org"}, ] readme = {file = "README.rst", content-type = "text/x-rst"} license = {text = "Apache-2.0"} dynamic = ["version", "dependencies"] requires-python = ">=3.10" classifiers = [ "Environment :: OpenStack", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", ] [project.urls] Homepage = "https://docs.openstack.org/watcher-dashboard/latest/" Repository = "https://opendev.org/openstack/watcher-dashboard" [tool.setuptools] packages = [ "watcher_dashboard" ] [tool.ruff] line-length = 79 target-version = "py310" [tool.ruff.lint] select = ["C90", "E4", "E5", "E7", "E9", "F", "G", "I", "LOG", "S", "UP", "W"] external = ["H"] ignore = [ # UP031 / UP032 — defer %-format and str() f-string migration; revisit in a # dedicated modernization change. "UP031", "UP032", ] [tool.ruff.lint.per-file-ignores] "watcher_dashboard/tests/*" = ["S"] # test/settings.py uses star imports inherited from openstack_dashboard.test.settings "watcher_dashboard/tests/settings.py" = ["F403", "F405"] [tool.ruff.lint.mccabe] # Flag errors (`C901`) whenever the complexity level exceeds 5. max-complexity = 20 [tool.ruff.lint.isort] known-first-party = ["watcher_dashboard"] force-single-line = true lines-after-imports = 2 lines-between-types = 1 section-order = ["future", "standard-library", "third-party", "first-party"] # this is the default but just being explicit about the behavior # in case that ever changes. force-sort-within-sections = false [tool.ruff.format] quote-style = "preserve" docstring-code-format = true skip-magic-trailing-comma = true ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9268198 watcher_dashboard-16.0.0/releasenotes/0000775000175000017500000000000015257165054016567 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9338198 watcher_dashboard-16.0.0/releasenotes/notes/0000775000175000017500000000000015257165054017717 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.820848 watcher_dashboard-16.0.0/releasenotes/notes/.placeholder0000664000175000017500000000000015257165015022165 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.820848 watcher_dashboard-16.0.0/releasenotes/notes/audit-parameters-support-0a1e3b1e9d9f.yaml0000664000175000017500000000143015257165015027452 0ustar00zuulzuul--- features: - | Watcher Dashboard now supports passing strategy parameters when creating audits, matching the OpenStack CLI ``openstack optimize audit create -p ``. A new "Strategy Parameters" field accepts all the parameters as a JSON object. Server-side validation parses and validates the input, and the field remains backward compatible when left empty. The Create Audit dialog also shows available parameters for the selected strategy (when the Watcher API exposes ``parameters_spec``), including types, defaults, and descriptions from audit templates. This provides real-time guidance to users and improves parity with the CLI experience. The Audit Details view now displays the parameters that were used for the audit.././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.820848 watcher_dashboard-16.0.0/releasenotes/notes/continuous-audit-start-end-time.yaml0000664000175000017500000000073315257165015026750 0ustar00zuulzuul--- features: - | Add support for specifying start and end times when creating CONTINUOUS audits in the Watcher dashboard. The Create Audit form now shows ISO 8601 local time fields "Start time" and "End time" for CONTINUOUS audits and submits them to the Watcher API. Times are sent in ISO 8601 format and are converted to UTC by the server. - | It also makes 1.1 as minimal supported api version as it is required for the Audit operations. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.820848 watcher_dashboard-16.0.0/releasenotes/notes/drop-py-2-7-198cca7f72d16655.yaml0000664000175000017500000000033315257165015024643 0ustar00zuulzuul--- upgrade: - | Python 2.7 support has been dropped. Last release of watcher-dashboard to support py2.7 is OpenStack Train. The minimum version of Python now supported by watcher-dashboard is Python 3.6. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.820848 watcher_dashboard-16.0.0/releasenotes/notes/drop-py39-8a9c99678b3e8eeb.yaml0000664000175000017500000000031415257165015024673 0ustar00zuulzuul--- upgrade: - | watcher-dashboard now requires python 3.10 or newer. The last release to support ``3.9`` was ``2025.1``. Please ensure you have a supported python version before upgrading. ././@PaxHeader0000000000000000000000000000022000000000000010207 xustar00116 path=watcher_dashboard-16.0.0/releasenotes/notes/filter-strategies-by-goal-in-create-template-9c7b1a3e9b5a.yaml 28 mtime=1790765580.8218482 watcher_dashboard-16.0.0/releasenotes/notes/filter-strategies-by-goal-in-create-template-9c7b1a3e9b50000664000175000017500000000075115257165015032041 0ustar00zuulzuul--- fixes: - | The Create Audit Template form now filters the ``Strategy`` dropdown by the selected ``Goal``. Previously, all strategies were listed, which could lead to selecting an incompatible strategy. The UI now loads strategies dynamically after a goal is chosen, mirroring the CLI behavior of ``openstack optimize strategy list --goal ``. When no goal is selected, the Strategy field remains empty with only the default "Select Strategy" option. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8218482 watcher_dashboard-16.0.0/releasenotes/notes/fix-actions-table-display-choices-a3b2c1d4e5f6.yaml0000664000175000017500000000074415257165015031063 0ustar00zuulzuul--- fixes: - | Fixed incorrect use of ``status_choices`` parameter in state columns across all Watcher Dashboard tables (ActionPlans, Actions, and Audits). The deprecated ``status_choices`` parameter has been replaced with the correct ``display_choices`` parameter. This ensures that state values are properly displayed as translated, human-readable text (e.g., "Succeeded" instead of "SUCCEEDED") and aligns with Horizon's table column best practices. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8218482 watcher_dashboard-16.0.0/releasenotes/notes/fix-audit-list-filter-b2161426.yaml0000664000175000017500000000054615257165015025720 0ustar00zuulzuul--- fixes: - | Fixed the audit list page filter which offered "Audit Template" as a server-side filter option. The Watcher API does not accept ``audit_template`` as a query parameter for listing audits, causing an HTTP 400 error. The filter now offers "Goal" and "Strategy", which are the filter parameters the audit list API supports. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8218482 watcher_dashboard-16.0.0/releasenotes/notes/show-strategy-parameters-spec-2121648.yaml0000664000175000017500000000012715257165015027336 0ustar00zuulzuul--- fixes: - | Display the strategy Parameters spec on the Strategy Details page ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8218482 watcher_dashboard-16.0.0/releasenotes/notes/skip-actions-option-a1b2c3d4e5f6.yaml0000664000175000017500000000041115257165015026375 0ustar00zuulzuul--- features: - | Adds option to SKIP Actions from Actionplans view with or without skip reason. The controls are shown only when the Watcher API microversion supports it (>= 1.5). On older clouds, the Actions column and skip controls are hidden. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9348197 watcher_dashboard-16.0.0/releasenotes/source/0000775000175000017500000000000015257165054020067 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8218482 watcher_dashboard-16.0.0/releasenotes/source/2023.1.rst0000664000175000017500000000021015257165015021334 0ustar00zuulzuul=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2023.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8218482 watcher_dashboard-16.0.0/releasenotes/source/2023.2.rst0000664000175000017500000000020215257165015021336 0ustar00zuulzuul=========================== 2023.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8218482 watcher_dashboard-16.0.0/releasenotes/source/2024.1.rst0000664000175000017500000000021015257165015021335 0ustar00zuulzuul=========================== 2024.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2024.1 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.822848 watcher_dashboard-16.0.0/releasenotes/source/2024.2.rst0000664000175000017500000000020215257165015021337 0ustar00zuulzuul=========================== 2024.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.2 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.822848 watcher_dashboard-16.0.0/releasenotes/source/2025.1.rst0000664000175000017500000000020215257165015021337 0ustar00zuulzuul=========================== 2025.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.1 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.822848 watcher_dashboard-16.0.0/releasenotes/source/2025.2.rst0000664000175000017500000000020215257165015021340 0ustar00zuulzuul=========================== 2025.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.2 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.822848 watcher_dashboard-16.0.0/releasenotes/source/2026.1.rst0000664000175000017500000000020215257165015021340 0ustar00zuulzuul=========================== 2026.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2026.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9348197 watcher_dashboard-16.0.0/releasenotes/source/_static/0000775000175000017500000000000015257165054021515 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.822848 watcher_dashboard-16.0.0/releasenotes/source/_static/.placeholder0000664000175000017500000000000015257165015023763 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9348197 watcher_dashboard-16.0.0/releasenotes/source/_templates/0000775000175000017500000000000015257165054022224 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.822848 watcher_dashboard-16.0.0/releasenotes/source/_templates/.placeholder0000664000175000017500000000000015257165015024472 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.822848 watcher_dashboard-16.0.0/releasenotes/source/conf.py0000664000175000017500000002212415257165015021364 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # Watcher Release Notes documentation build configuration file, created by # sphinx-quickstart on Tue Nov 3 17:40:50 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['openstackdocstheme', 'reno.sphinxext'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Watcher Dashboard Release Notes' copyright = '2017, Watcher Developers' # Release notes are version independent. # The full version, including alpha/beta/rc tags. release = '' # The short X.Y version. version = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # openstackdocstheme options openstackdocs_repo_name = 'openstack/watcher-dashboard' openstackdocs_auto_name = False openstackdocs_bug_project = 'watcher-dashboard' openstackdocs_bug_tag = '' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {"show_other_versions": "True"} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'WatcherDashboardReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( 'index', 'WatcherDashboardReleaseNotes.tex', 'Watcher Dashboard Release Notes Documentation', 'Watcher Developers', 'manual', ) ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( 'index', 'watcherdashboardreleasenotes', 'Watcher Dashboard Release Notes Documentation', ['Watcher Developers'], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( 'index', 'WatcherDashboardReleaseNotes', 'Watcher Dashboard Release Notes Documentation', 'Watcher Developers', 'WatcherDashboardReleaseNotes', 'One line description of project.', 'Miscellaneous', ) ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.849848 watcher_dashboard-16.0.0/releasenotes/source/index.rst0000664000175000017500000000050115257165015021721 0ustar00zuulzuul=============================== Watcher Dashboard Release Notes =============================== .. toctree:: :maxdepth: 1 unreleased 2026.1 2025.2 2025.1 2024.2 2024.1 2023.2 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky queens pike ocata ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9268198 watcher_dashboard-16.0.0/releasenotes/source/locale/0000775000175000017500000000000015257165054021326 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9268198 watcher_dashboard-16.0.0/releasenotes/source/locale/en_GB/0000775000175000017500000000000015257165054022300 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9358196 watcher_dashboard-16.0.0/releasenotes/source/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000015257165054024065 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.822848 watcher_dashboard-16.0.0/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po0000664000175000017500000000177315257165015027123 0ustar00zuulzuul# Andi Chandler , 2017. #zanata # Andi Chandler , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: watcher-dashboard\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-08-15 08:11+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2018-08-17 09:34+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en_GB\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "Current Series Release Notes" msgstr "Current Series Release Notes" msgid "Ocata Series Release Notes" msgstr "Ocata Series Release Notes" msgid "Pike Series Release Notes" msgstr "Pike Series Release Notes" msgid "Queens Series Release Notes" msgstr "Queens Series Release Notes" msgid "Rocky Series Release Notes" msgstr "Rocky Series Release Notes" msgid "Watcher Dashboard Release Notes" msgstr "Watcher Dashboard Release Notes" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9268198 watcher_dashboard-16.0.0/releasenotes/source/locale/ko_KR/0000775000175000017500000000000015257165054022333 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9358196 watcher_dashboard-16.0.0/releasenotes/source/locale/ko_KR/LC_MESSAGES/0000775000175000017500000000000015257165054024120 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/locale/ko_KR/LC_MESSAGES/releasenotes.po0000664000175000017500000000126515257165015027152 0ustar00zuulzuul# minwook-shin , 2017. #zanata msgid "" msgstr "" "Project-Id-Version: Watcher Dashboard Release Notes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-03-01 11:28+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2017-07-31 01:05+0000\n" "Last-Translator: minwook-shin \n" "Language-Team: Korean (South Korea)\n" "Language: ko_KR\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=1; plural=0\n" msgid "Current Series Release Notes" msgstr "현재 시리즈 릴리즈 노트" msgid "Ocata Series Release Notes" msgstr "Ocata 시리즈 릴리즈 노트" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/ocata.rst0000664000175000017500000000023015257165015021700 0ustar00zuulzuul=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/pike.rst0000664000175000017500000000021715257165015021546 0ustar00zuulzuul=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/queens.rst0000664000175000017500000000022315257165015022113 0ustar00zuulzuul=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/rocky.rst0000664000175000017500000000022115257165015021740 0ustar00zuulzuul=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/stein.rst0000664000175000017500000000022115257165015021733 0ustar00zuulzuul=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/train.rst0000664000175000017500000000017615257165015021737 0ustar00zuulzuul========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/unreleased.rst0000664000175000017500000000016015257165015022742 0ustar00zuulzuul============================== Current Series Release Notes ============================== .. release-notes:: ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/ussuri.rst0000664000175000017500000000020215257165015022142 0ustar00zuulzuul=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/victoria.rst0000664000175000017500000000022015257165015022430 0ustar00zuulzuul============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: unmaintained/victoria ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/wallaby.rst0000664000175000017500000000021415257165015022246 0ustar00zuulzuul============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: unmaintained/wallaby ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/xena.rst0000664000175000017500000000020015257165015021541 0ustar00zuulzuul========================= Xena Series Release Notes ========================= .. release-notes:: :branch: unmaintained/xena ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/yoga.rst0000664000175000017500000000020015257165015021545 0ustar00zuulzuul========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: unmaintained/yoga ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/releasenotes/source/zed.rst0000664000175000017500000000017415257165015021402 0ustar00zuulzuul======================== Zed Series Release Notes ======================== .. release-notes:: :branch: unmaintained/zed ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/requirements.txt0000664000175000017500000000057415257165015017365 0ustar00zuulzuul# Requirements lower bounds listed here are our best effort to keep them up to # date but we do not test them so no guarantee of having them all correct. If # you find any incorrect lower bounds, let us know or propose a fix. pbr!=2.1.0,>=2.0.0 # Apache-2.0 horizon>=18.2.0 # Apache-2.0 oslo.utils>=8.0.0 # Apache-2.0 PyYAML>=3.12 # MIT python-watcherclient>=4.9.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/run_tests.sh0000775000175000017500000003757315257165015016477 0ustar00zuulzuul#!/bin/bash set -o errexit function usage { echo "Usage: $0 [OPTION]..." echo "Run Horizon's test suite(s)" echo "" echo " -V, --virtual-env Always use virtualenv. Install automatically" echo " if not present" echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local" echo " environment" echo " -c, --coverage Generate reports using Coverage" echo " -f, --force Force a clean re-build of the virtual" echo " environment. Useful when dependencies have" echo " been added." echo " -m, --manage Run a Django management command." echo " --makemessages Create/Update English translation files." echo " --compilemessages Compile all translation files." echo " --check-only Do not update translation files (--makemessages only)." echo " --pseudo Pseudo translate a language." echo " -p, --pep8 Just run pep8" echo " -8, --pep8-changed []" echo " Just run PEP8 and HACKING compliance check" echo " on files changed since HEAD~1 (or )" echo " -P, --no-pep8 Don't run pep8 by default" echo " -t, --tabs Check for tab characters in files." echo " -y, --pylint Just run pylint" echo " -e, --eslint Just run eslint" echo " -k, --karma Just run karma" echo " -q, --quiet Run non-interactively. (Relatively) quiet." echo " Implies -V if -N is not set." echo " --runserver Run the Django development server for" echo " openstack_dashboard in the virtual" echo " environment." echo " --docs Just build the documentation" echo " --backup-environment Make a backup of the environment on exit" echo " --restore-environment Restore the environment before running" echo " --destroy-environment Destroy the environment and exit" echo " -h, --help Print this usage message" echo "" echo "Note: with no options specified, the script will try to run the tests in" echo " a virtual environment, If no virtualenv is found, the script will ask" echo " if you would like to create one. If you prefer to run tests NOT in a" echo " virtual environment, simply pass the -N option." exit } # DEFAULTS FOR RUN_TESTS.SH # root=`pwd -P` venv=$root/.venv venv_env_version=$venv/environments with_venv=tools/with_venv.sh included_dirs="watcher_dashboard" always_venv=0 backup_env=0 command_wrapper="" destroy=0 force=0 just_pep8=0 just_pep8_changed=0 no_pep8=0 just_pylint=0 just_docs=0 just_tabs=0 just_eslint=0 just_karma=0 never_venv=0 quiet=0 restore_env=0 runserver=0 testopts="" testargs="" with_coverage=0 makemessages=0 compilemessages=0 check_only=0 pseudo=0 manage=0 # Jenkins sets a "JOB_NAME" variable, if it's not set, we'll make it "default" [ "$JOB_NAME" ] || JOB_NAME="default" function process_option { # If running manage command, treat the rest of options as arguments. if [ $manage -eq 1 ]; then testargs="$testargs $1" return 0 fi case "$1" in -h|--help) usage;; -V|--virtual-env) always_venv=1; never_venv=0;; -N|--no-virtual-env) always_venv=0; never_venv=1;; -p|--pep8) just_pep8=1;; -8|--pep8-changed) just_pep8_changed=1;; -P|--no-pep8) no_pep8=1;; -y|--pylint) just_pylint=1;; -e|--eslint) just_eslint=1;; -k|--karma) just_karma=1;; -f|--force) force=1;; -t|--tabs) just_tabs=1;; -q|--quiet) quiet=1;; -c|--coverage) with_coverage=1;; -m|--manage) manage=1;; --makemessages) makemessages=1;; --compilemessages) compilemessages=1;; --check-only) check_only=1;; --pseudo) pseudo=1;; --docs) just_docs=1;; --runserver) runserver=1;; --backup-environment) backup_env=1;; --restore-environment) restore_env=1;; --destroy-environment) destroy=1;; -*) testopts="$testopts $1";; *) testargs="$testargs $1" esac } function run_management_command { ${command_wrapper} python $root/manage.py $testopts $testargs } function run_server { echo "Starting Django development server..." ${command_wrapper} python $root/manage.py runserver $testopts $testargs echo "Server stopped." } function run_pylint { echo "Running pylint ..." PYTHONPATH=$root ${command_wrapper} pylint --rcfile=.pylintrc -f parseable $included_dirs > pylint.txt || true CODE=$? grep Global -A2 pylint.txt if [ $CODE -lt 32 ]; then echo "Completed successfully." exit 0 else echo "Completed with problems." exit $CODE fi } function run_eslint { echo "Running eslint ..." if [ "`which npm`" == '' ] ; then echo "npm is not present; please install, e.g. sudo apt-get install npm" else npm install npm run lint fi } function run_karma { echo "Running karma ..." npm install npm run test } function warn_on_flake8_without_venv { set +o errexit ${command_wrapper} python -c "import hacking" 2>/dev/null no_hacking=$? set -o errexit if [ $never_venv -eq 1 -a $no_hacking -eq 1 ]; then echo "**WARNING**:" >&2 echo "OpenStack hacking is not installed on your host. Its detection will be missed." >&2 echo "Please install or use virtual env if you need OpenStack hacking detection." >&2 fi } function run_pep8 { echo "Running flake8 ..." warn_on_flake8_without_venv DJANGO_SETTINGS_MODULE=watcher_dashboard.tests.settings ${command_wrapper} flake8 } function run_pep8_changed { # NOTE(gilliard) We want use flake8 to check the entirety of every file that has # a change in it. Unfortunately the --filenames argument to flake8 only accepts # file *names* and there are no files named (eg) "nova/compute/manager.py". The # --diff argument behaves surprisingly as well, because although you feed it a # diff, it actually checks the file on disk anyway. local base_commit=${testargs:-HEAD~1} files=$(git diff --name-only $base_commit | tr '\n' ' ') echo "Running flake8 on ${files}" warn_on_flake8_without_venv diff -u --from-file /dev/null ${files} | DJANGO_SETTINGS_MODULE=watcher_dashboard.tests.settings ${command_wrapper} flake8 --diff exit } function run_sphinx { echo "Building sphinx..." DJANGO_SETTINGS_MODULE=watcher_dashboard.tests.settings ${command_wrapper} python setup.py build_sphinx echo "Build complete." } function tab_check { TAB_VIOLATIONS=`find $included_dirs -type f -regex ".*\.\(css\|js\|py\|html\)" -print0 | xargs -0 awk '/\t/' | wc -l` if [ $TAB_VIOLATIONS -gt 0 ]; then echo "TABS! $TAB_VIOLATIONS of them! Oh no!" HORIZON_FILES=`find $included_dirs -type f -regex ".*\.\(css\|js\|py|\html\)"` for TABBED_FILE in $HORIZON_FILES do TAB_COUNT=`awk '/\t/' $TABBED_FILE | wc -l` if [ $TAB_COUNT -gt 0 ]; then echo "$TABBED_FILE: $TAB_COUNT" fi done fi return $TAB_VIOLATIONS; } function destroy_venv { echo "Cleaning environment..." echo "Removing virtualenv..." rm -rf $venv echo "Virtualenv removed." } function environment_check { echo "Checking environment." if [ -f $venv_env_version ]; then set +o errexit cat requirements.txt test-requirements.txt | cmp $venv_env_version - > /dev/null local env_check_result=$? set -o errexit if [ $env_check_result -eq 0 ]; then # If the environment exists and is up-to-date then set our variables command_wrapper="${root}/${with_venv}" echo "Environment is up to date." return 0 fi fi if [ $always_venv -eq 1 ]; then install_venv else if [ ! -e ${venv} ]; then echo -e "Environment not found. Install? (Y/n) \c" else echo -e "Your environment appears to be out of date. Update? (Y/n) \c" fi read update_env if [ "x$update_env" = "xY" -o "x$update_env" = "x" -o "x$update_env" = "xy" ]; then install_venv else # Set our command wrapper anyway. command_wrapper="${root}/${with_venv}" fi fi } function sanity_check { # Anything that should be determined prior to running the tests, server, etc. # Don't sanity-check anything environment-related in -N flag is set if [ $never_venv -eq 0 ]; then if [ ! -e ${venv} ]; then echo "Virtualenv not found at $venv. Did install_venv.py succeed?" exit 1 fi fi # Remove .pyc files. This is sanity checking because they can linger # after old files are deleted. find . -name "*.pyc" -exec rm -rf {} \; } function backup_environment { if [ $backup_env -eq 1 ]; then echo "Backing up environment \"$JOB_NAME\"..." if [ ! -e ${venv} ]; then echo "Environment not installed. Cannot back up." return 0 fi if [ -d /tmp/.horizon_environment/$JOB_NAME ]; then mv /tmp/.horizon_environment/$JOB_NAME /tmp/.horizon_environment/$JOB_NAME.old rm -rf /tmp/.horizon_environment/$JOB_NAME fi mkdir -p /tmp/.horizon_environment/$JOB_NAME cp -r $venv /tmp/.horizon_environment/$JOB_NAME/ # Remove the backup now that we've completed successfully rm -rf /tmp/.horizon_environment/$JOB_NAME.old echo "Backup completed" fi } function restore_environment { if [ $restore_env -eq 1 ]; then echo "Restoring environment from backup..." if [ ! -d /tmp/.horizon_environment/$JOB_NAME ]; then echo "No backup to restore from." return 0 fi cp -r /tmp/.horizon_environment/$JOB_NAME/.venv ./ || true echo "Environment restored successfully." fi } function install_venv { # Install with install_venv.py export PIP_DOWNLOAD_CACHE=${PIP_DOWNLOAD_CACHE-/tmp/.pip_download_cache} export PIP_USE_MIRRORS=true if [ $quiet -eq 1 ]; then export PIP_NO_INPUT=true fi echo "Fetching new src packages..." rm -rf $venv/src python tools/install_venv.py command_wrapper="$root/${with_venv}" # Make sure it worked and record the environment version sanity_check chmod -R 754 $venv cat requirements.txt test-requirements.txt > $venv_env_version } function run_tests { sanity_check if [ -z "$testargs" ]; then run_tests_all else run_tests_subset fi } function run_tests_subset { project=`echo $testargs | awk -F. '{print $1}'` ${command_wrapper} python $root/manage.py test --settings=$project.test.settings $testopts $testargs } function run_tests_all { echo "Running Horizon application tests" export NOSE_XUNIT_FILE=horizon/nosetests.xml if [ "$NOSE_WITH_HTML_OUTPUT" = '1' ]; then export NOSE_HTML_OUT_FILE='horizon_nose_results.html' fi if [ $with_coverage -eq 1 ]; then ${command_wrapper} python -m coverage.__main__ erase coverage_run="python -m coverage.__main__ run -p" fi ${command_wrapper} ${coverage_run} $root/manage.py test horizon --settings=horizon.test.settings $testopts # get results of the Horizon tests HORIZON_RESULT=$? echo "Running openstack_dashboard tests" export NOSE_XUNIT_FILE=openstack_dashboard/nosetests.xml if [ "$NOSE_WITH_HTML_OUTPUT" = '1' ]; then export NOSE_HTML_OUT_FILE='dashboard_nose_results.html' fi ${command_wrapper} ${coverage_run} $root/manage.py test openstack_dashboard --settings=watcher_dashboard.tests.settings $testopts # get results of the openstack_dashboard tests DASHBOARD_RESULT=$? if [ $with_coverage -eq 1 ]; then echo "Generating coverage reports" ${command_wrapper} python -m coverage.__main__ combine ${command_wrapper} python -m coverage.__main__ xml -i --include="horizon/*,openstack_dashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' ${command_wrapper} python -m coverage.__main__ html -i --include="horizon/*,openstack_dashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' -d reports fi # Remove the leftover coverage files from the -p flag earlier. rm -f .coverage.* PEP8_RESULT=0 if [ $no_pep8 -eq 0 ]; then run_pep8 PEP8_RESULT=$? fi TEST_RESULT=$(($HORIZON_RESULT || $DASHBOARD_RESULT || $PEP8_RESULT)) if [ $TEST_RESULT -eq 0 ]; then echo "Tests completed successfully." else echo "Tests failed." fi exit $TEST_RESULT } function babel_extract { DOMAIN=$1 KEYWORDS="-k gettext_noop -k gettext_lazy -k ngettext_lazy:1,2" KEYWORDS+=" -k gettext_noop -k gettext_lazy -k ngettext_lazy:1,2" KEYWORDS+=" -k npgettext:1c,2,3 -k pgettext_lazy:1c,2 -k npgettext_lazy:1c,2,3" ${command_wrapper} pybabel extract -F ../babel-${DOMAIN}.cfg -o locale/${DOMAIN}.pot $KEYWORDS . } function run_makemessages { echo -n "horizon: " cd horizon babel_extract django HORIZON_PY_RESULT=$? echo -n "horizon javascript: " babel_extract djangojs HORIZON_JS_RESULT=$? echo -n "openstack_dashboard: " cd ../openstack_dashboard babel_extract django DASHBOARD_RESULT=$? echo -n "openstack_dashboard javascript: " babel_extract djangojs DASHBOARD_JS_RESULT=$? cd .. if [ $check_only -eq 1 ]; then git checkout -- horizon/locale/django*.pot git checkout -- openstack_dashboard/locale/django*.pot fi exit $(($HORIZON_PY_RESULT || $HORIZON_JS_RESULT || $DASHBOARD_RESULT || $DASHBOARD_JS_RESULT)) } function run_compilemessages { cd horizon ${command_wrapper} $root/manage.py compilemessages HORIZON_PY_RESULT=$? cd ../openstack_dashboard ${command_wrapper} $root/manage.py compilemessages DASHBOARD_RESULT=$? exit $(($HORIZON_PY_RESULT || $DASHBOARD_RESULT)) } function run_pseudo { for lang in $testargs # Use English pot file as the source file/pot file just like real Horizon translations do ${command_wrapper} $root/tools/pseudo.py openstack_dashboard/locale/django.pot openstack_dashboard/locale/$lang/LC_MESSAGES/django.po $lang ${command_wrapper} $root/tools/pseudo.py openstack_dashboard/locale/djangojs.pot openstack_dashboard/locale/$lang/LC_MESSAGES/djangojs.po $lang ${command_wrapper} $root/tools/pseudo.py horizon/locale/django.pot horizon/locale/$lang/LC_MESSAGES/django.po $lang ${command_wrapper} $root/tools/pseudo.py horizon/locale/djangojs.pot horizon/locale/$lang/LC_MESSAGES/djangojs.po $lang done exit $? } # ---------PREPARE THE ENVIRONMENT------------ # # PROCESS ARGUMENTS, OVERRIDE DEFAULTS for arg in "$@"; do process_option $arg done if [ $quiet -eq 1 ] && [ $never_venv -eq 0 ] && [ $always_venv -eq 0 ] then always_venv=1 fi # If destroy is set, just blow it away and exit. if [ $destroy -eq 1 ]; then destroy_venv exit 0 fi # Ignore all of this if the -N flag was set if [ $never_venv -eq 0 ]; then # Restore previous environment if desired if [ $restore_env -eq 1 ]; then restore_environment fi # Remove the virtual environment if --force used if [ $force -eq 1 ]; then destroy_venv fi # Then check if it's up-to-date environment_check # Create a backup of the up-to-date environment if desired if [ $backup_env -eq 1 ]; then backup_environment fi fi # ---------EXERCISE THE CODE------------ # # Run management commands if [ $manage -eq 1 ]; then run_management_command exit $? fi # Build the docs if [ $just_docs -eq 1 ]; then run_sphinx exit $? fi # Update translation files if [ $makemessages -eq 1 ]; then run_makemessages exit $? fi # Compile translation files if [ $compilemessages -eq 1 ]; then run_compilemessages exit $? fi # Generate Pseudo translation if [ $pseudo -eq 1 ]; then run_pseudo exit $? fi # PEP8 if [ $just_pep8 -eq 1 ]; then run_pep8 exit $? fi if [ $just_pep8_changed -eq 1 ]; then run_pep8_changed exit $? fi # Pylint if [ $just_pylint -eq 1 ]; then run_pylint exit $? fi # ESLint if [ $just_eslint -eq 1 ]; then run_eslint exit $? fi # Karma if [ $just_karma -eq 1 ]; then run_karma exit $? fi # Tab checker if [ $just_tabs -eq 1 ]; then tab_check exit $? fi # Django development server if [ $runserver -eq 1 ]; then run_server exit $? fi # Full test suite run_tests || exit ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9478197 watcher_dashboard-16.0.0/setup.cfg0000664000175000017500000000011315257165054015712 0ustar00zuulzuul[metadata] name = watcher-dashboard [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.823848 watcher_dashboard-16.0.0/setup.py0000664000175000017500000000126115257165015015605 0ustar00zuulzuul# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import setuptools setuptools.setup(setup_requires=['pbr>=2.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.824848 watcher_dashboard-16.0.0/test-requirements.txt0000664000175000017500000000052215257165015020333 0ustar00zuulzuul# Hacking already pins down pep8, pyflakes and flake8 hacking>=7.0.0,<7.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT oslotest>=3.8.0 # Apache-2.0 oslo.utils>=4.0.0 # Apache-2.0 oslo.log>=3.37.0 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0 playwright>=1.58.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9358196 watcher_dashboard-16.0.0/tools/0000775000175000017500000000000015257165054015236 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.824848 watcher_dashboard-16.0.0/tools/install_venv.py0000664000175000017500000000447615257165015020324 0ustar00zuulzuul# Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # # Copyright 2010 OpenStack Foundation # Copyright 2013 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import sys import install_venv_common as install_venv # noqa def print_help(venv, root): help = """ OpenStack development environment setup is complete. OpenStack development uses virtualenv to track and manage Python dependencies while in development and testing. To activate the OpenStack virtualenv for the extent of your current shell session you can run: $ . %s/bin/activate Or, if you prefer, you can run commands in the virtualenv on a case by case basis by running: $ %s/tools/with_venv.sh Also, make test will automatically use the virtualenv. """ print(help % (venv, root)) def main(argv): root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) if os.environ.get('tools_path'): root = os.environ['tools_path'] venv = os.path.join(root, '.venv') if os.environ.get('venv'): venv = os.environ['venv'] pip_requires = os.path.join(root, 'requirements.txt') test_requires = os.path.join(root, 'test-requirements.txt') py_version = f"python{sys.version_info[0]}.{sys.version_info[1]}" project = 'OpenStack' install = install_venv.InstallVenv( root, venv, pip_requires, test_requires, py_version, project ) options = install.parse_args(argv) install.check_python_version() install.check_dependencies() install.create_virtualenv(no_site_packages=options.no_site_packages) install.install_dependencies() print_help(venv, root) if __name__ == '__main__': main(sys.argv) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.824848 watcher_dashboard-16.0.0/tools/install_venv_common.py0000664000175000017500000001361315257165015021665 0ustar00zuulzuul# Copyright 2013 OpenStack Foundation # Copyright 2013 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Provides methods needed by installation script for OpenStack development virtual environments. Since this script is used to bootstrap a virtualenv from the system's Python environment, it should be kept strictly compatible with Python 2.6. Synced in from openstack-common """ import optparse import os import subprocess # nosec B404 - needed for virtual environment setup import sys class InstallVenv: def __init__( self, root, venv, requirements, test_requirements, py_version, project ): self.root = root self.venv = venv self.requirements = requirements self.test_requirements = test_requirements self.py_version = py_version self.project = project def die(self, message, *args): print(message % args, file=sys.stderr) sys.exit(1) def check_python_version(self): pass def run_command_with_code( self, cmd, redirect_output=True, check_exit_code=True ): """Runs a command in an out-of-process shell. Returns the output of that command. Working directory is self.root. """ if redirect_output: stdout = subprocess.PIPE else: stdout = None proc = subprocess.Popen( # noqa: S603 cmd, cwd=self.root, stdout=stdout ) # nosec B603 output = proc.communicate()[0] if check_exit_code and proc.returncode != 0: self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) return (output, proc.returncode) def run_command(self, cmd, redirect_output=True, check_exit_code=True): return self.run_command_with_code( cmd, redirect_output, check_exit_code )[0] def get_distro(self): if os.path.exists('/etc/fedora-release') or os.path.exists( '/etc/redhat-release' ): return Fedora( self.root, self.venv, self.requirements, self.test_requirements, self.py_version, self.project, ) else: return Distro( self.root, self.venv, self.requirements, self.test_requirements, self.py_version, self.project, ) def check_dependencies(self): self.get_distro().install_virtualenv() def create_virtualenv(self, no_site_packages=True): """Creates the virtual environment and installs PIP. Creates the virtual environment and installs PIP only into the virtual environment. """ if not os.path.isdir(self.venv): print('Creating venv...', end=' ') if no_site_packages: self.run_command( ['virtualenv', '-q', '--no-site-packages', self.venv] ) else: self.run_command(['virtualenv', '-q', self.venv]) print('done.') else: print("venv already exists...") pass def pip_install(self, *args): self.run_command( ['tools/with_venv.sh', 'pip', 'install', '--upgrade'] + list(args), redirect_output=False, ) def install_dependencies(self): print('Installing dependencies with pip (this can take a while)...') # First things first, make sure our venv has the latest pip and # setuptools and pbr self.pip_install('pip>=1.4') self.pip_install('setuptools') self.pip_install('pbr') self.pip_install('-r', self.requirements, '-r', self.test_requirements) def parse_args(self, argv): """Parses command-line arguments.""" parser = optparse.OptionParser() parser.add_option( '-n', '--no-site-packages', action='store_true', help="Do not inherit packages from global Python install", ) return parser.parse_args(argv[1:])[0] class Distro(InstallVenv): def check_cmd(self, cmd): return bool( self.run_command(['which', cmd], check_exit_code=False).strip() ) def install_virtualenv(self): if self.check_cmd('virtualenv'): return if self.check_cmd('easy_install'): print('Installing virtualenv via easy_install...', end=' ') if self.run_command(['easy_install', 'virtualenv']): print('Succeeded') return else: print('Failed') self.die( f'ERROR: virtualenv not found.\n\n{self.project} development' ' requires virtualenv, please install it using your' ' favorite package management tool' ) class Fedora(Distro): """This covers all Fedora-based distributions. Includes: Fedora, RHEL, CentOS, Scientific Linux """ def check_pkg(self, pkg): return ( self.run_command_with_code( ['rpm', '-q', pkg], check_exit_code=False )[1] == 0 ) def install_virtualenv(self): if self.check_cmd('virtualenv'): return if not self.check_pkg('python-virtualenv'): self.die("Please install 'python-virtualenv'.") super().install_virtualenv() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.824848 watcher_dashboard-16.0.0/tools/register_plugin.sh0000775000175000017500000000143315257165015020775 0ustar00zuulzuul#!/bin/bash src_path=`cd "$1"; pwd` dest_path=`cd "$2"; pwd` # echo "$src_path --> $dest_path" for filepath in $src_path/watcher_dashboard/local/enabled/*.py; do filename=$(basename $filepath) if [ $filename != "__init__.py" ]; then echo $filepath src_filepath="`cd "$(dirname $filepath)"; pwd`/$filename" dest_filepath="$dest_path/openstack_dashboard/local/enabled/$filename" echo "$src_filepath --> $dest_filepath" ln -s $src_filepath $dest_filepath fi done policy_file_name='watcher_policy.yaml' src_policy_filepath=$src_path'/watcher_dashboard/conf/'$policy_file_name dest_policy_file=$dest_path'/openstack_dashboard/conf/'$policy_file_name echo "$src_policy_filepath --> $dest_policy_file" ln -s $src_policy_filepath $dest_policy_file ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.824848 watcher_dashboard-16.0.0/tools/with_venv.sh0000775000175000017500000000077215257165015017611 0ustar00zuulzuul#!/bin/bash TOOLS_PATH=${TOOLS_PATH:-$(dirname $0)} VENV_PATH=${VENV_PATH:-${TOOLS_PATH}} VENV_DIR=${VENV_NAME:-/../.venv} TOOLS=${TOOLS_PATH} VENV=${VENV:-${VENV_PATH}/${VENV_DIR}} HORIZON_DIR=${TOOLS%/tools} # This horrible mangling of the PYTHONPATH is required to get the # babel-angular-gettext extractor to work. To fix this the extractor needs to # be packaged on pypi and added to global requirements. That work is in progress. export PYTHONPATH="$HORIZON_DIR" source ${VENV}/bin/activate && "$@" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.824848 watcher_dashboard-16.0.0/tox.ini0000664000175000017500000001047015257165015015410 0ustar00zuulzuul[tox] minversion = 4.28.0 envlist = py3,pep8 [testenv] usedevelop = True setenv = DJANGO_SETTINGS_MODULE=watcher_dashboard.tests.settings # Note the hash seed is set to 0 until horizon can be tested with a # random hash seed successfully. PYTHONHASHSEED=0 allowlist_externals = /bin/bash rm find constraints = {env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = rm -f .testrepository/times.dbm find . -type f -name "*.pyc" -delete find . -type d -name "__pycache__" -delete python manage.py test --settings=watcher_dashboard.tests.settings \ --exclude-tag integration \ watcher_dashboard [testenv:pep8] description = Run style checks. skip_install = true deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure [testenv:integration{,-playwright}] # Run integration tests using Playwright # Set PLAYWRIGHT_BROWSER to choose browser: chromium, firefox, or webkit (default) # Example: PLAYWRIGHT_BROWSER=firefox tox -e integration-playwright # Note: webkit provides Safari-like testing on Linux # Set PLAYWRIGHT_VIDEO to enable video recording: off (default), on, retain-on-failure passenv = DISPLAY XAUTHORITY OS_* WATCHER_DASHBOARD_URL PLAYWRIGHT_BROWSER PLAYWRIGHT_HEADLESS PLAYWRIGHT_TIMEOUT PLAYWRIGHT_OUTPUT_DIR PLAYWRIGHT_SCREENSHOTS PLAYWRIGHT_VIDEO PLAYWRIGHT_TRACE PLAYWRIGHT_AUTH_REUSE PLAYWRIGHT_IGNORE_HTTPS_ERRORS setenv = DJANGO_SETTINGS_MODULE=watcher_dashboard.test.settings DEBUG = pw:api # Playwright configuration PLAYWRIGHT_BROWSER = {env:PLAYWRIGHT_BROWSER:webkit} PLAYWRIGHT_HEADLESS = {env:PLAYWRIGHT_HEADLESS:True} PLAYWRIGHT_TIMEOUT = {env:PLAYWRIGHT_TIMEOUT:30000} PLAYWRIGHT_SCREENSHOTS = {env:PLAYWRIGHT_SCREENSHOTS:True} PLAYWRIGHT_VIDEO = {env:PLAYWRIGHT_VIDEO:off} PLAYWRIGHT_TRACE = {env:PLAYWRIGHT_TRACE:off} PLAYWRIGHT_AUTH_REUSE = {env:PLAYWRIGHT_AUTH_REUSE:False} PLAYWRIGHT_IGNORE_HTTPS_ERRORS = {env:PLAYWRIGHT_IGNORE_HTTPS_ERRORS:False} # Store artifacts within the project directory PLAYWRIGHT_OUTPUT_DIR = {toxinidir}/playwright # Set browser cache path within the project directory PLAYWRIGHT_BROWSERS_PATH = {toxinidir}/.playwright # Dashboard and OpenStack configuration WATCHER_DASHBOARD_URL = {env:WATCHER_DASHBOARD_URL:http://localhost/dashboard} OS_USERNAME = {env:OS_USERNAME:admin} OS_PASSWORD = {env:OS_PASSWORD:secretadmin} OS_AUTH_URL = {env:OS_AUTH_URL:http://localhost/identity} OS_PROJECT_NAME = {env:OS_PROJECT_NAME:admin} OS_USER_DOMAIN_NAME = {env:OS_USER_DOMAIN_NAME:Default} OS_PROJECT_DOMAIN_NAME = {env:OS_PROJECT_DOMAIN_NAME:Default} OS_INSECURE = {env:OS_INSECURE:True} deps = {[testenv]deps} stestr allowlist_externals = rm commands = # Clean up artifacts from previous runs rm -rf {toxinidir}/playwright # Browser binaries are installed by playbooks/playwright/pre.yaml, not tox. # For local runs: python -m playwright install webkit stestr run --concurrency 1 {posargs} [testenv:venv] commands = {posargs} [testenv:releasenotes] deps = -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html [testenv:cover] commands = coverage erase coverage run --source watcher_dashboard {toxinidir}/manage.py test \ --settings=watcher_dashboard.tests.settings \ --exclude-tag integration watcher_dashboard {posargs} coverage xml coverage html -d ./cover --omit='*tests*' coverage report [testenv:docs] deps = -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html [testenv:pdf-docs] deps = {[testenv:docs]deps} allowlist_externals = rm make commands = rm -rf doc/build/pdf sphinx-build -W --keep-going -b latex doc/source doc/build/pdf make -C doc/build/pdf [testenv:debug] commands = oslo_debug_helper {posargs} [flake8] # H306 imports not in alphabetical order -- superseded by ruff I-rules which # handle mixed import styles correctly. select = H ignore = H306 show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,.ropeproject,tools ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9358196 watcher_dashboard-16.0.0/watcher_dashboard/0000775000175000017500000000000015257165054017542 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.824848 watcher_dashboard-16.0.0/watcher_dashboard/__init__.py0000664000175000017500000000000015257165015021636 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9368198 watcher_dashboard-16.0.0/watcher_dashboard/api/0000775000175000017500000000000015257165054020313 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.824848 watcher_dashboard-16.0.0/watcher_dashboard/api/__init__.py0000664000175000017500000000000015257165015022407 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.824848 watcher_dashboard-16.0.0/watcher_dashboard/api/watcher.py0000664000175000017500000004701115257165015022322 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging from django.utils.translation import gettext_lazy as _ from openstack_dashboard.api import base from watcherclient.common.apiclient import exceptions as wc_exc from watcher_dashboard import config from watcher_dashboard.common import client as common_client from watcher_dashboard.common import exceptions as watcher_exc from watcher_dashboard.utils import errors as errors_utils LOG = logging.getLogger(__name__) def watcherclient(request, api_version=None): insert_watcher_policy_file() endpoint = base.url_for(request, common_client.WATCHER_SERVICE) LOG.debug( 'watcherclient connection created using token "%s" and url "%s"', request.user.token.id, endpoint, ) # Default to minimal microversion (1.0) unless explicitly overridden. microversion = api_version or common_client.MIN_DEFAULT # Prefer centralized client helper client = common_client.get_client(request, required=microversion) return client def insert_watcher_policy_file() -> None: svc = common_client.WATCHER_SERVICE if svc not in config.get_policy_files(): config.set_policy_file(svc, 'watcher_policy.yaml') class Audit(base.APIDictWrapper): _attrs = ( 'uuid', 'name', 'created_at', 'modified_at', 'deleted_at', 'state', 'audit_type', 'audit_template_uuid', 'audit_template_name', 'interval', 'parameters', 'auto_trigger', 'goal_name', 'strategy_name', 'start_time', 'end_time', ) def __init__(self, apiresource, request=None): super().__init__(apiresource) self._request = request @classmethod def create( cls, request, audit_template_uuid, audit_type, name=None, auto_trigger=False, interval=None, parameters=None, start_time=None, end_time=None, api_version=None, ): """Create an audit in Watcher. :param request: request object :type request: django.http.HttpRequest :param audit_template_uuid: related audit template UUID :type audit_template_uuid: string :param audit_type: audit type :type audit_type: string :param interval: Audit interval (default: None) :type interval: int :param name: Name for this audit :type name: string :param parameters: Strategy parameters (default: None) :type parameters: dict :param api_version: Microversion to use for the request. :type api_version: str or None :return: the created Audit object :rtype: :py:class:`~.Audit` """ payload = { 'audit_template_uuid': audit_template_uuid, 'audit_type': audit_type, 'auto_trigger': auto_trigger, } if name: payload['name'] = name if interval: payload['interval'] = interval if parameters: payload['parameters'] = parameters if start_time: payload['start_time'] = start_time if end_time: payload['end_time'] = end_time client = watcherclient(request, api_version=api_version) return client.audit.create(**payload) @classmethod def list(cls, request, api_version=None, **filters): """Return a list of audits in Watcher. :param request: request object :type request: django.http.HttpRequest :param api_version: Microversion to use for the request. :type api_version: str or None :param filters: key/value kwargs used as filters :type filters: dict :return: list of audits, or an empty list if there are none :rtype: list of :py:class:`~.Audit` """ return watcherclient(request, api_version=api_version).audit.list( detail=True, **filters ) @classmethod @errors_utils.handle_errors(_("Unable to retrieve audit")) def get(cls, request, audit_id, api_version=None): """Return the audit that matches the ID. :param request: request object :type request: django.http.HttpRequest :param audit_id: id of audit to be retrieved :type audit_id: int :param api_version: Microversion to use for the request. :type api_version: str or None :return: matching audit, or None if no audit matches the ID :rtype: :py:class:`~.Audit` """ return watcherclient(request, api_version=api_version).audit.get( audit=audit_id ) @classmethod def delete(cls, request, audit_id): """Delete an audit :param request: request object :type request: django.http.HttpRequest :param audit_id: audit id :type audit_id: int """ return watcherclient(request).audit.delete(audit=audit_id) @classmethod def cancel(cls, request, audit_id): """Cancel an audit :param request: request object :type request: django.http.HttpRequest :param audit_id: audit id :type audit_id: int """ cancel_patch = [ {'op': 'replace', 'path': '/state', 'value': 'CANCELLED'} ] return watcherclient(request).audit.update( audit=audit_id, patch=cancel_patch ) @property def id(self): return self.uuid class AuditTemplate(base.APIDictWrapper): _attrs = ( 'uuid', 'description', 'scope', 'name', 'goal_uuid', 'goal_name', 'strategy_uuid', 'strategy_name', 'created_at', 'updated_at', 'deleted_at', ) def __init__(self, apiresource, request=None): super().__init__(apiresource) self._request = request @classmethod def create(cls, request, name, goal, strategy, description, scope): """Create an audit template in Watcher :param request: request object :type request: django.http.HttpRequest :param name: Name for this audit template :type name: string :param goal: Goal UUID or name associated to this audit template :type goal: string :param strategy: Strategy UUID or name associated to this audit template :type strategy: string :param description: Description of the audit template :type description: string :param scope: Audit scope :type scope: list of list of dict :param audit_template: audit template :type audit_template: string :return: the created Audit Template object :rtype: :py:class:`~.AuditTemplate` """ audit_template = watcherclient(request).audit_template.create( name=name, goal=goal, strategy=strategy, description=description, scope=scope, ) return audit_template @classmethod def patch(cls, request, audit_template_id, parameters): """Update an audit in Watcher :param request: request object :type request: django.http.HttpRequest :param audit_template_id: id of the audit template we want to update :type audit_template_id: string :param parameters: new values for the audit template's parameters :type parameters: dict :return: the updated Audit Template object :rtype: :py:class:`~.AuditTemplate` """ parameter_list = [ {'name': str(name), 'value': str(value)} for (name, value) in parameters.items() ] audit_template = watcherclient(request).audit_template.patch( audit_template_id, parameter_list ) return audit_template @classmethod def list(cls, request, **filters): """Return a list of audit templates in Watcher :param request: request object :type request: django.http.HttpRequest :param filters: key/value kwargs used as filters :type filters: dict :return: list of audit templates, or an empty list if there are none :rtype: list of :py:class:`~.AuditTemplate` """ return watcherclient(request).audit_template.list( detail=True, **filters ) @classmethod @errors_utils.handle_errors(_("Unable to retrieve audit template")) def get(cls, request, audit_template_id): """Return the audit template that matches the ID :param request: request object :type request: django.http.HttpRequest :param audit_template_id: id of audit template to be retrieved :type audit_template_id: int :return: matching audit template, or None if no audit template matches the ID :rtype: :py:class:`~.AuditTemplate` """ return watcherclient(request).audit_template.get( audit_template_id=audit_template_id ) @classmethod def delete(cls, request, audit_template_id): """Delete an audit_template :param request: request object :type request: django.http.HttpRequest :param audit_template_id: audit id :type audit_template_id: int """ watcherclient(request).audit_template.delete( audit_template_id=audit_template_id ) @property def id(self): return self.uuid class ActionPlan(base.APIDictWrapper): _attrs = ( 'uuid', 'created_at', 'updated_at', 'deleted_at', 'audit_uuid', 'state', 'status_message', ) def __init__(self, apiresource, request=None): super().__init__(apiresource) self._request = request @classmethod def list(cls, request, **filters): """Return a list of action plans in Watcher :param request: request object :type request: django.http.HttpRequest :param filters: key/value kwargs used as filters :type filters: dict :return: list of action plans, or an empty list if there are none :rtype: list of :py:class:`~.ActionPlan` """ return watcherclient(request).action_plan.list(detail=True, **filters) @classmethod @errors_utils.handle_errors(_("Unable to retrieve action plan")) def get(cls, request, action_plan_id, api_version=None): """Return the action plan that matches the ID. :param request: request object :type request: django.http.HttpRequest :param action_plan_id: id of action plan to be retrieved :type action_plan_id: int :param api_version: Microversion to use for the request. :type api_version: str or None :return: matching action plan, or None if no action plan matches the ID :rtype: :py:class:`~.ActionPlan` """ return watcherclient(request, api_version=api_version).action_plan.get( action_plan_id=action_plan_id ) @classmethod def delete(cls, request, action_plan_id): """Delete an action plan :param request: request object :type request: django.http.HttpRequest :param action_plan_id: audit id :type action_plan_id: int """ watcherclient(request).action_plan.delete( action_plan_id=action_plan_id ) @classmethod def start(cls, request, action_plan_id): """Start an Action Plan :param request: request object :type request: django.http.HttpRequest :param action_plan_id: audit id :type action_plan_id: int """ watcherclient(request).action_plan.start(action_plan_id) @property def id(self): return self.uuid class Action(base.APIDictWrapper): _attrs = ( 'uuid', 'created_at', 'updated_at', 'deleted_at', 'next_uuid', 'description', 'state', 'action_plan_uuid', 'action_type', 'applies_to', 'src', 'dst', 'parameter', 'status_message', ) def __init__(self, apiresource, request=None): super().__init__(apiresource) self._request = request @classmethod def list(cls, request, **filters): """Return a list of actions in Watcher :param request: request object :type request: django.http.HttpRequest :param filters: key/value kwargs used as filters :type filters: dict :return: list of actions, or an empty list if there are none :rtype: list of :py:class:`~.Action` """ return watcherclient(request).action.list(detail=True, **filters) @classmethod @errors_utils.handle_errors(_("Unable to retrieve action")) def get(cls, request, action_id, api_version=None): """Return the action that matches the ID. :param request: request object :type request: django.http.HttpRequest :param action_id: id of action to be retrieved :type action_id: int :param api_version: Microversion to use for the request. :type api_version: str or None :return: matching action, or None if no action matches the ID :rtype: :py:class:`~.Action` """ return watcherclient(request, api_version=api_version).action.get( action_id=action_id ) @classmethod def update( cls, request, action_id, state=None, reason=None, api_version=None ): """Update an action's state and/or status message. :param request: request object :type request: django.http.HttpRequest :param action_id: UUID of the action to update :type action_id: str :param state: new state value (e.g. 'SKIPPED') :type state: str or None :param reason: optional reason / status message :type reason: str or None :param api_version: microversion to use for the request :type api_version: str or None :returns: updated action, or None if server returned 405 :rtype: action resource or None :raises WatcherDashboardException: if neither state nor reason is provided """ reason = reason or None if state is None and reason is None: raise watcher_exc.WatcherDashboardException( "Either 'state' or 'reason' must be provided" ) patch = [] if state is not None: patch.append({'op': 'replace', 'path': '/state', 'value': state}) if reason is not None: patch.append( {'op': 'replace', 'path': '/status_message', 'value': reason} ) try: return watcherclient( request, api_version=api_version ).action.update(action_id=action_id, patch=patch) except wc_exc.MethodNotAllowed: LOG.info( "Watcher action update not supported by API; " "skipping update. action_id=%s", action_id, ) return None @classmethod def delete(cls, request, action_id): """Delete an action :param request: request object :type request: django.http.HttpRequest :param action_id: action_plan id :type action_id: int """ watcherclient(request).action.delete(action_id=action_id) @property def id(self): return self.uuid class Goal(base.APIDictWrapper): """Goal resource.""" _attrs = ( 'uuid', 'name', 'display_name', 'created_at', 'updated_at', 'deleted_at', 'efficacy_specification', ) def __init__(self, apiresource, request=None): super().__init__(apiresource) self._request = request @classmethod def list(cls, request, **filters): """Return a list of goals in Watcher :param request: request object :type request: django.http.HttpRequest :param filters: key/value kwargs used as filters :type filters: dict :return: list of goals, or an empty list if there are none :rtype: list of :py:class:`~.Goal` instance """ return watcherclient(request).goal.list(detail=True, **filters) @classmethod @errors_utils.handle_errors(_("Unable to retrieve goal")) def get(cls, request, goal): """Return the goal that matches the ID :param request: request object :type request: django.http.HttpRequest :param goal: uuid of goal to be retrieved :type goal: int :return: matching goal, or None if no goal matches the UUID :rtype: :py:class:`~.Goal` instance """ return watcherclient(request).goal.get(goal) @property def id(self): return self.uuid class Strategy(base.APIDictWrapper): """Strategy resource.""" _attrs = ( 'uuid', 'name', 'display_name', 'goal_uuid', 'goal_name', 'created_at', 'updated_at', 'deleted_at', 'parameters_spec', ) def __init__(self, apiresource, request=None): super().__init__(apiresource) self._request = request @classmethod def list(cls, request, **filters): """Return a list of strategies in Watcher :param request: request object :type request: django.http.HttpRequest :param filters: key/value kwargs used as filters :type filters: dict :return: list of strategies, or an empty list if there are none :rtype: list of :py:class:`~.Strategy` instances """ return watcherclient(request).strategy.list(detail=True, **filters) @classmethod @errors_utils.handle_errors(_("Unable to retrieve strategy")) def get(cls, request, strategy): """Return the strategy that matches the UUID :param request: request object :type request: django.http.HttpRequest :param strategy: uuid of strategy to be retrieved :type strategy: str :return: matching strategy, or None if no strategy matches the UUID :rtype: :py:class:`~.Strategy` instance """ return watcherclient(request).strategy.get(strategy) @property def id(self): return self.uuid class EfficacyIndicatorSpec(base.APIDictWrapper): attrs = ('name', 'description', 'unit', 'schema') class EfficacyIndicator(base.APIDictWrapper): def __init__(self, indicator): super().__init__(indicator) self.value = indicator.get('value', None) self.name = indicator.get('name', None) self.description = indicator.get('description', None) self.unit = indicator.get('unit', None) def get_strategy_display_name(strategy): """Return display_name for a strategy, falling back to name. :param strategy: A watcherclient Strategy resource object. :returns: The display name string. """ return strategy.display_name or strategy.name ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9368198 watcher_dashboard-16.0.0/watcher_dashboard/common/0000775000175000017500000000000015257165054021032 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.825848 watcher_dashboard-16.0.0/watcher_dashboard/common/__init__.py0000664000175000017500000000000015257165015023126 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.825848 watcher_dashboard-16.0.0/watcher_dashboard/common/client.py0000664000175000017500000000645415257165015022670 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at ## # http://www.apache.org/licenses/LICENSE-2.0 ## # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging from typing import Any from openstack_dashboard.api import base from watcherclient import client as wc from watcherclient import exceptions as wc_exc from watcherclient.common import api_versioning from watcher_dashboard import config LOG = logging.getLogger(__name__) # Service type used by Watcher in the catalog WATCHER_SERVICE = 'infra-optim' # Default/minimal microversion MIN_DEFAULT = '1.0' # Microversion enabling audit start/end time fields MV_START_END = '1.1' # Microversion enabling skip action support MV_SKIP_ACTION = '1.5' def get_client(request: Any, required: str = MIN_DEFAULT) -> Any: """Return a watcher client pinned to the given microversion. :param request: The current Django HTTP request. :param required: Microversion string (e.g. ``'1.0'``, ``'1.1'``). """ endpoint = base.url_for(request, WATCHER_SERVICE) insecure = config.get_ssl_no_verify() ca_file = config.get_ssl_cacert() return wc.get_client( '1', watcher_url=endpoint, insecure=insecure, ca_file=ca_file, username=request.user.username, os_auth_token=request.user.token.id, os_infra_optim_api_version=required, ) def get_max_version(request): """Discover the server's maximum supported microversion. Queries the Watcher API root endpoint and extracts the ``max_version`` field from the response body. :param request: The current Django HTTP request. :returns: The max version string, or ``None`` on failure. """ try: client = get_client(request) _resp, body = client.http_client.json_request('GET', '/') version_info = ( body.get('versions') or body.get('version') or body.get('default_version') or {} ) if isinstance(version_info, list) and version_info: version_info = version_info[0] if isinstance(version_info, dict): return version_info.get('max_version') except wc_exc.ClientException: LOG.debug('Microversion discovery failed', exc_info=True) return None def is_microversion_supported(max_ver, required): """Check whether the server supports the required microversion. :param max_ver: The server's max version string from :func:`get_max_version`, or ``None``. :param required: The microversion string to check (e.g. ``'1.1'``). :returns: ``True`` if the server's max version is at least *required*, ``False`` otherwise. """ if max_ver is None: return False try: return api_versioning.APIVersion(max_ver) >= api_versioning.APIVersion( required ) except (wc_exc.UnsupportedVersion, TypeError): return False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.825848 watcher_dashboard-16.0.0/watcher_dashboard/common/exceptions.py0000664000175000017500000000164515257165015023570 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from openstack_dashboard import exceptions from watcherclient.common.apiclient import exceptions as watcherclient NOT_FOUND = exceptions.NOT_FOUND RECOVERABLE = (*exceptions.RECOVERABLE, watcherclient.ClientException) UNAUTHORIZED = exceptions.UNAUTHORIZED class WatcherDashboardException(Exception): """Base exception for watcher-dashboard errors.""" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9368198 watcher_dashboard-16.0.0/watcher_dashboard/conf/0000775000175000017500000000000015257165054020467 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.825848 watcher_dashboard-16.0.0/watcher_dashboard/conf/watcher_policy.yaml0000664000175000017500000000256015257165015024367 0ustar00zuulzuul--- "admin_api": "role:admin or role:administrator" "show_password": "!" "action:detail": "rule:admin_api" "action:get": "rule:admin_api" "action:get_all": "rule:admin_api" "action:update": "rule:admin_api" "action_plan:delete": "rule:admin_api" "action_plan:detail": "rule:admin_api" "action_plan:get": "rule:admin_api" "action_plan:get_all": "rule:admin_api" "action_plan:update": "rule:admin_api" "action_plan:start": "rule:admin_api" "audit:create": "rule:admin_api" "audit:delete": "rule:admin_api" "audit:detail": "rule:admin_api" "audit:get": "rule:admin_api" "audit:get_all": "rule:admin_api" "audit:update": "rule:admin_api" "audit_template:create": "rule:admin_api" "audit_template:delete": "rule:admin_api" "audit_template:detail": "rule:admin_api" "audit_template:get": "rule:admin_api" "audit_template:get_all": "rule:admin_api" "audit_template:update": "rule:admin_api" "data_model:get_all": "rule:admin_api" "goal:detail": "rule:admin_api" "goal:get": "rule:admin_api" "goal:get_all": "rule:admin_api" "scoring_engine:detail": "rule:admin_api" "scoring_engine:get": "rule:admin_api" "scoring_engine:get_all": "rule:admin_api" "service:detail": "rule:admin_api" "service:get": "rule:admin_api" "service:get_all": "rule:admin_api" "strategy:detail": "rule:admin_api" "strategy:get": "rule:admin_api" "strategy:get_all": "rule:admin_api" "strategy:state": "rule:admin_api" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.825848 watcher_dashboard-16.0.0/watcher_dashboard/config.py0000664000175000017500000000634315257165015021364 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Centralised Django settings access for watcher-dashboard. All reads of Django settings that were previously scattered via ``getattr``/``setattr`` reflection helpers are consolidated here. Functions that return immutable values are cached with ``functools.cache`` so repeated look-ups are free; ``set_policy_file`` replaces ``POLICY_FILES`` with a new dict and clears that cache after each write. """ import functools from django.conf import settings from oslo_utils import strutils @functools.cache def get_ssl_no_verify() -> bool: """Return whether SSL verification should be skipped. Reads the OPENSTACK_SSL_NO_VERIFY setting. Uses oslo_utils.strutils.bool_from_string() so string values like 'False' or '0' are handled correctly. Defaults to False when the setting is absent. """ return strutils.bool_from_string( getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False), default=False ) @functools.cache def get_ssl_cacert() -> str | None: """Return the CA certificate path (OPENSTACK_SSL_CACERT), or None. Returns None when the setting is absent or empty string. Raises TypeError if a non-string, non-None value is configured. """ value = getattr(settings, 'OPENSTACK_SSL_CACERT', None) if value is None: return None if not isinstance(value, str): raise TypeError( f"OPENSTACK_SSL_CACERT must be a str or None, got {type(value)!r}" ) return value or None # treat empty string as absent @functools.cache def get_policy_files() -> dict: """Return the POLICY_FILES dict, defaulting to {}. Raises TypeError if the setting is present but not a dict. """ value = getattr(settings, 'POLICY_FILES', {}) if not isinstance(value, dict): raise TypeError(f"POLICY_FILES must be a dict, got {type(value)!r}") return dict(value) def set_policy_file(service: str, filename: str) -> None: """Register ``filename`` as the policy file for ``service``. Both arguments must be non-empty strings. Replaces ``POLICY_FILES`` with a shallow copy plus the new mapping so callers' dicts are not mutated in place. Clears the get_policy_files cache so subsequent reads reflect the updated value. """ if not service: raise ValueError("service must be a non-empty string") if not filename: raise ValueError("filename must be a non-empty string") raw = getattr(settings, 'POLICY_FILES', {}) if not isinstance(raw, dict): raise TypeError(f"POLICY_FILES must be a dict, got {type(raw)!r}") policy_files = dict(raw) policy_files[service] = filename settings.POLICY_FILES = policy_files get_policy_files.cache_clear() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9368198 watcher_dashboard-16.0.0/watcher_dashboard/content/0000775000175000017500000000000015257165054021214 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.825848 watcher_dashboard-16.0.0/watcher_dashboard/content/__init__.py0000664000175000017500000000000015257165015023310 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9378197 watcher_dashboard-16.0.0/watcher_dashboard/content/action_plans/0000775000175000017500000000000015257165054023666 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.825848 watcher_dashboard-16.0.0/watcher_dashboard/content/action_plans/__init__.py0000664000175000017500000000000015257165015025762 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.825848 watcher_dashboard-16.0.0/watcher_dashboard/content/action_plans/panel.py0000664000175000017500000000142515257165015025336 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import horizon from django.utils.translation import gettext_lazy as _ class ActionPlans(horizon.Panel): name = _("Action Plans") slug = "action_plans" permissions = ("openstack.services.infra-optim",) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/action_plans/tables.py0000664000175000017500000001675315257165015025523 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import horizon.exceptions import horizon.messages import horizon.tables from django import template from django import urls from django.template.defaultfilters import title # noqa from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from django.utils.translation import pgettext_lazy from horizon.utils import filters from watcher_dashboard.api import watcher LOG = logging.getLogger(__name__) ACTION_PLAN_STATE_DISPLAY_CHOICES = ( ("NO STATE", pgettext_lazy("State of an action plan", "No State")), ("ONGOING", pgettext_lazy("State of an action plan", "On Going")), ("SUCCEEDED", pgettext_lazy("State of an action plan", "Succeeded")), ("SUBMITTED", pgettext_lazy("State of an action plan", "Submitted")), ("FAILED", pgettext_lazy("State of an action plan", "Failed")), ("DELETED", pgettext_lazy("State of an action plan", "Deleted")), ("RECOMMENDED", pgettext_lazy("State of an action plan", "Recommended")), ) class ActionPlansFilterAction(horizon.tables.FilterAction): # server = choices query = text filter_type = "server" filter_choices = (('audit', _("Audit ="), True),) policy_rules = (("infra-optim", "action_plan:detail"),) class ArchiveActionPlan(horizon.tables.DeleteAction): verbose_name = _("Archive Action Plans") policy_rules = (("infra-optim", "action_plan:delete"),) @staticmethod def action_present(count): return ngettext_lazy( "Archive Action Plan", "Archive Action Plans", count ) @staticmethod def action_past(count): return ngettext_lazy( "Action Plan archived", "Action Plans archived", count ) def action(self, request, obj_id): watcher.ActionPlan.delete(request, obj_id) class StartActionPlan(horizon.tables.BatchAction): name = "start_action_plan" classes = ('btn-confirm',) policy_rules = (("infra-optim", "action_plan:update"),) help_text = _("Execute an action plan.") @staticmethod def action_present(count): return ngettext_lazy("Start Action Plan", "Start Action Plans", count) @staticmethod def action_past(count): return ngettext_lazy( "Action Plan started", "Action Plans started", count ) def action(self, request, action_plan_id): try: watcher.ActionPlan.start(request, action_plan_id) except Exception: msg = _('Failed to start the action plan.') LOG.info(msg) horizon.messages.warning(request, msg) def allowed(self, request, action_plan): return (action_plan is None) or ( action_plan.state in ("RECOMMENDED", "FAILED") ) class UpdateRow(horizon.tables.Row): ajax = True def get_data(self, request, action_plan_id): action_plan = None try: action_plan = watcher.Action.get(request, action_plan_id) except Exception: msg = _('Failed to get the action plan.') LOG.info(msg) horizon.messages.warning(request, msg) return action_plan def format_global_efficacy(action_plan): template_name = 'infra_optim/action_plans/_global_efficacy.html' global_efficacy_dict = {} for indicator in action_plan.global_efficacy: global_efficacy = watcher.EfficacyIndicator(indicator) if ( global_efficacy.value is not None and global_efficacy.unit is not None ): global_efficacy_dict[global_efficacy.name] = ( f"{global_efficacy.value} {global_efficacy.unit}" ) elif global_efficacy.value is not None: global_efficacy_dict[global_efficacy.name] = str( global_efficacy.value ) context = {"global_indicators": global_efficacy_dict} return template.loader.render_to_string(template_name, context) def get_audit_link(datum): try: return urls.reverse( "horizon:admin:audits:detail", kwargs={"audit_uuid": getattr(datum, "audit_uuid", None)}, ) except urls.NoReverseMatch: return None class ActionPlansTable(horizon.tables.DataTable): name = horizon.tables.Column( 'uuid', verbose_name=_("UUID"), link="horizon:admin:action_plans:detail", ) audit = horizon.tables.Column( 'audit_uuid', verbose_name=_('Audit'), link=get_audit_link ) updated_at = horizon.tables.Column( 'updated_at', filters=(filters.parse_isotime, filters.timesince_sortable), verbose_name=_("Updated At"), ) status = horizon.tables.Column( 'state', verbose_name=_('State'), display_choices=ACTION_PLAN_STATE_DISPLAY_CHOICES, ) efficacy = horizon.tables.Column( transform=format_global_efficacy, verbose_name=_('Efficacy') ) def get_object_id(self, datum): return datum.uuid class Meta: name = "action_plans" verbose_name = _("Action Plans") table_actions = ( # CancelActionPlan, ActionPlansFilterAction, StartActionPlan, ArchiveActionPlan, ) row_actions = ( StartActionPlan, # CreateActionPlans, ArchiveActionPlan, # CreateActionPlans, # DeleteActionPlans, ) row_class = UpdateRow class RelatedActionPlansTable(horizon.tables.DataTable): name = horizon.tables.Column( 'uuid', verbose_name=_("UUID"), link="horizon:admin:action_plans:detail", ) audit = horizon.tables.Column( 'audit_uuid', verbose_name=_('Audit'), link=get_audit_link ) updated_at = horizon.tables.Column( 'updated_at', filters=(filters.parse_isotime, filters.timesince_sortable), verbose_name=_("Updated At"), ) status = horizon.tables.Column( 'state', verbose_name=_('State'), display_choices=ACTION_PLAN_STATE_DISPLAY_CHOICES, ) efficacy = horizon.tables.Column( transform=format_global_efficacy, verbose_name=_('Efficacy') ) def get_object_id(self, datum): return datum.uuid class Meta: name = "related_action_plans" verbose_name = _("Related Action Plans") hidden_title = False row_actions = (StartActionPlan, ArchiveActionPlan) row_class = UpdateRow class RelatedEfficacyIndicatorsTable(horizon.tables.DataTable): name = horizon.tables.Column('name', verbose_name=_("Name")) description = horizon.tables.Column( 'description', verbose_name=_("Description") ) unit = horizon.tables.Column('unit', verbose_name=_("Unit")) value = horizon.tables.Column('value', verbose_name=_("Value")) def get_object_id(self, datum): return datum.name class Meta: name = "related_efficacy_indicators" verbose_name = _("Related Efficacy Indicators") hidden_title = False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/action_plans/tabs.py0000664000175000017500000000201215257165015025161 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ from horizon import tabs class OverviewTab(tabs.Tab): name = _("Overview") slug = "overview" template_name = "infra_optim/action_plans/_detail_overview.html" def get_context_data(self, request): return {"action_plan": self.tab_group.kwargs['action_plans']} class ActionPlanDetailTabs(tabs.TabGroup): slug = "action_plan_details" tabs = (OverviewTab,) sticky = True ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/action_plans/urls.py0000664000175000017500000000166115257165015025226 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from watcher_dashboard.content.action_plans import views urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path( r'^(?P[^/]+)/detail$', views.DetailView.as_view(), name='detail', ), re_path(r'^archive/$', views.ArchiveView.as_view(), name='archive'), ] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/action_plans/views.py0000664000175000017500000001417215257165015025377 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import horizon.exceptions import horizon.tables import horizon.tabs import horizon.workflows from django.utils.translation import gettext_lazy as _ from horizon import forms from horizon.utils import memoized from watcher_dashboard.api import watcher from watcher_dashboard.common import client as common_client from watcher_dashboard.content.action_plans import tables from watcher_dashboard.content.actions import tables as action_tables from watcher_dashboard.content.audits import forms as wforms LOG = logging.getLogger(__name__) class IndexView(horizon.tables.DataTableView): table_class = tables.ActionPlansTable template_name = 'infra_optim/action_plans/index.html' page_title = _("Action Plans") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['action_plans_count'] = self.get_action_plans_count() return context def get_data(self): action_plans = [] search_opts = self.get_filters() try: action_plans = watcher.ActionPlan.list(self.request, **search_opts) except Exception as exc: LOG.exception(exc) horizon.exceptions.handle( self.request, _("Unable to retrieve action_plan information.") ) return action_plans def get_action_plans_count(self): return len(self.get_data()) def get_filters(self): filters = {} filter_action = self.table._meta._filter_action if filter_action: filter_field = self.table.get_filter_field() if filter_action.is_api_filter(filter_field): filter_string = self.table.get_filter_string() if filter_field and filter_string: filters[filter_field] = filter_string return filters class ArchiveView(forms.ModalFormView): form_class = wforms.CreateForm form_id = "create_audit_form" modal_header = _("Create Audit") template_name = 'infra_optim/audits/create.html' page_title = _("Create Audit") submit_label = _("Create Audit") class DetailView(horizon.tables.MultiTableView): table_classes = ( action_tables.RelatedActionsTable, tables.RelatedEfficacyIndicatorsTable, ) template_name = 'infra_optim/action_plans/details.html' page_title = _("Action Plan Details: {{ action_plan.uuid }}") @memoized.memoized_method def max_version(self): return common_client.get_max_version(self.request) @memoized.memoized_method def _get_data(self): action_plan_uuid = None try: action_plan_uuid = self.kwargs['action_plan_uuid'] server_version = self.max_version() version = ( common_client.MV_SKIP_ACTION if common_client.is_microversion_supported( server_version, common_client.MV_SKIP_ACTION ) else None ) action_plan = watcher.ActionPlan.get( self.request, action_plan_uuid, api_version=version ) except Exception as exc: LOG.exception(exc) msg = ( _('Unable to retrieve details for action_plan "%s".') % action_plan_uuid ) horizon.exceptions.handle( self.request, msg, redirect=self.redirect_url ) return action_plan def get_related_wactions_data(self): try: action_plan = self._get_data() actions = watcher.Action.list( self.request, action_plan=action_plan.uuid ) except Exception as exc: LOG.exception(exc) actions = [] msg = _('Action list can not be retrieved.') horizon.exceptions.handle(self.request, msg) return actions def get_related_efficacy_indicators_data(self): efficacy_indicators = [] try: action_plan = self._get_data() efficacy_indicators = [ watcher.EfficacyIndicator(indicator) for indicator in action_plan.efficacy_indicators ] except Exception as exc: LOG.exception(exc) msg = _('Failed to get the efficacy indicators: %s') % str(exc) LOG.info(msg) horizon.messages.warning(self.request, msg) return efficacy_indicators def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) action_plan = self._get_data() context["action_plan"] = action_plan return context def get_tables(self): """Configure the actions table with skip-support flags. Horizon calls get_tables() multiple times per request: first from construct_tables() which loads data into each table, then again from get_context_data() to build the template context. We must update the existing table instance rather than replace it, otherwise the data loaded by construct_tables() is lost. """ table_dict = super().get_tables() action_plan = self._get_data() mv = self.max_version() table = table_dict['related_wactions'] table._supports_skip = common_client.is_microversion_supported( mv, common_client.MV_SKIP_ACTION ) table._parent_succeeded = action_plan.state == 'SUCCEEDED' return table_dict def get_tabs(self, request, *args, **kwargs): action_plan = self._get_data() return self.tab_group_class(request, action_plan=action_plan, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9378197 watcher_dashboard-16.0.0/watcher_dashboard/content/actions/0000775000175000017500000000000015257165054022654 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/actions/__init__.py0000664000175000017500000000000015257165015024750 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/actions/forms.py0000664000175000017500000000656115257165015024361 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging from django.utils.translation import gettext_lazy as _ from horizon import forms from horizon import messages from watcher_dashboard.api import watcher from watcher_dashboard.common import client as common_client from watcher_dashboard.common import exceptions as watcher_exc LOG = logging.getLogger(__name__) class SkipActionForm(forms.SelfHandlingForm): """Form to skip an action or update its skip reason.""" action_id = forms.CharField(widget=forms.HiddenInput()) reason = forms.CharField( label=_("Reason for skipping"), required=False, max_length=200, widget=forms.Textarea(attrs={'rows': 3}), help_text=_("Optional reason for skipping this action."), ) def clean(self): """Validate the action exists and is in a skippable state.""" cleaned = super().clean() action_id = cleaned.get('action_id') if not action_id: # Django calls clean() even when required fields fail # validation. Without this guard, None would be passed # to the API as the action ID. return cleaned action = watcher.Action.get(self.request, action_id) if action is None: raise forms.ValidationError(_('Unable to retrieve action.')) if action.state not in ('PENDING', 'SKIPPED'): raise forms.ValidationError( _('Action must be in PENDING or SKIPPED state.') ) reason = cleaned.get('reason', '').strip() if action.state == 'SKIPPED' and not reason: raise forms.ValidationError( _('A reason is required when updating a skipped action.') ) cleaned['_action'] = action return cleaned def handle(self, request, data): """Submit skip request to the Watcher API.""" action_id = data['action_id'] reason = data.get('reason', '').strip() or None action = data['_action'] state = None if action.state == 'SKIPPED' else 'SKIPPED' try: result = watcher.Action.update( request, action_id, state=state, reason=reason, api_version=common_client.MV_SKIP_ACTION, ) except watcher_exc.WatcherDashboardException as exc: LOG.info("Skip action validation error: %s", exc) messages.error(request, str(exc)) return False if result is None: messages.warning( request, _('Skip is not supported by this server.') ) return False if state is None: msg = _('Skip reason was successfully updated.') else: msg = _('Action was successfully skipped.') messages.success(request, msg) return True ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/actions/panel.py0000664000175000017500000000141015257165015024316 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import horizon from django.utils.translation import gettext_lazy as _ class Actions(horizon.Panel): name = _("Actions ") slug = "actions" permissions = ("openstack.services.infra-optim",) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/actions/tables.py0000664000175000017500000001321715257165015024501 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import horizon.exceptions import horizon.messages import horizon.tables from django import urls from django.template.defaultfilters import title # noqa from django.utils.translation import gettext_lazy as _ from django.utils.translation import pgettext_lazy from horizon.utils import filters from watcher_dashboard.api import watcher LOG = logging.getLogger(__name__) ACTION_STATE_DISPLAY_CHOICES = ( ("NO STATE", pgettext_lazy("Power state of an Instance", "No State")), ("ONGOING", pgettext_lazy("Power state of an Instance", "On Going")), ("SUCCEEDED", pgettext_lazy("Power state of an Instance", "Succeeded")), ("CANCELLED", pgettext_lazy("Power state of an Instance", "Cancelled")), ("FAILED", pgettext_lazy("Power state of an Instance", "Failed")), ("DELETED", pgettext_lazy("Power state of an Instance", "Deleted")), ("PENDING", pgettext_lazy("Power state of an Instance", "Pending")), ("SKIPPED", pgettext_lazy("Power state of an Instance", "Skipped")), ) class UpdateRow(horizon.tables.Row): ajax = True def get_data(self, request, action_id): action = None try: action = watcher.Action.get(request, action_id) except Exception: msg = _('Failed to get the action.') LOG.info(msg) horizon.messages.warning(request, msg) return action class ActionsFilterAction(horizon.tables.FilterAction): filter_type = "server" filter_choices = (('action_plan', _("Action Plan ID ="), True),) policy_rules = (("infra-optim", "action:detail"),) class SkipAction(horizon.tables.LinkAction): """Row action that opens the skip modal for an action.""" name = "skip" verbose_name = _("Skip Action") url = "horizon:admin:actions:skip" classes = ("ajax-modal", "btn-danger") policy_rules = (("infra-optim", "action:update"),) def allowed(self, request, action): """Allow skip for actions in PENDING or SKIPPED state.""" return action.state in ("PENDING", "SKIPPED") def get_action_plan_link(datum): try: return urls.reverse( "horizon:admin:action_plans:detail", kwargs={ "action_plan_uuid": getattr(datum, "action_plan_uuid", None) }, ) except urls.NoReverseMatch: return None class ActionsTable(horizon.tables.DataTable): name = horizon.tables.Column( 'uuid', verbose_name=_("UUID"), link="horizon:admin:actions:detail" ) action_type = horizon.tables.Column( 'action_type', verbose_name=_('Type'), filters=(title, filters.replace_underscores), ) state = horizon.tables.Column( 'state', verbose_name=_('State'), display_choices=ACTION_STATE_DISPLAY_CHOICES, ) action_plan = horizon.tables.Column( 'action_plan_uuid', verbose_name=_('Action Plan'), link=get_action_plan_link, ) def get_object_id(self, datum): return datum.uuid class Meta: name = "wactions" verbose_name = _("Actions") table_actions = (ActionsFilterAction,) row_class = UpdateRow class RelatedActionsTable(horizon.tables.DataTable): """Identical to the index table but with different Meta""" name = horizon.tables.Column( 'uuid', verbose_name=_("UUID"), link="horizon:admin:actions:detail" ) action_type = horizon.tables.Column( 'action_type', verbose_name=_('Type'), filters=(title, filters.replace_underscores), ) state = horizon.tables.Column( 'state', verbose_name=_('State'), display_choices=ACTION_STATE_DISPLAY_CHOICES, ) action_plan = horizon.tables.Column( 'action_plan_uuid', verbose_name=_('Action Plan'), link=get_action_plan_link, ) def __init__(self, *args, **kwargs): self._supports_skip = kwargs.pop('supports_skip', False) self._parent_succeeded = kwargs.pop('parent_succeeded', False) super().__init__(*args, **kwargs) def get_row_actions(self, datum): """Hide row actions when skip is unsupported or plan succeeded.""" if not self._supports_skip or self._parent_succeeded: return [] return super().get_row_actions(datum) @property def has_actions(self): """Hide Actions column when unsupported or plan succeeded.""" if not self._supports_skip or self._parent_succeeded: return False return len(self._meta.row_actions) > 0 def get_object_id(self, datum): return datum.uuid class Meta: name = "related_wactions" verbose_name = _("Related Actions") hidden_title = False row_actions = (SkipAction,) class ActionParametersTable(horizon.tables.DataTable): name = horizon.tables.Column('name', verbose_name=_("Parameter name")) value = horizon.tables.Column('value', verbose_name=_('Parameter value')) def get_object_id(self, datum): return datum.name class Meta: name = "parameters" verbose_name = _("Related parameters") hidden_title = False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/actions/tabs.py0000664000175000017500000000176115257165015024161 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ from horizon import tabs class OverviewTab(tabs.Tab): name = _("Overview") slug = "overview" template_name = "infra_optim/actions/_detail_overview.html" def get_context_data(self, request): return {"action": self.tab_group.kwargs['action']} class ActionDetailTabs(tabs.TabGroup): slug = "action_details" tabs = (OverviewTab,) sticky = True ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/actions/urls.py0000664000175000017500000000172715257165015024217 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from watcher_dashboard.content.actions import views urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path( r'^(?P[^/]+)/detail$', views.DetailView.as_view(), name='detail', ), re_path( r'^(?P[^/]+)/skip$', views.SkipActionView.as_view(), name='skip', ), ] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/actions/views.py0000664000175000017500000001324015257165015024360 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import collections import horizon.exceptions import horizon.tables import horizon.tabs import horizon.workflows from django.urls import reverse from django.utils.translation import gettext_lazy as _ from horizon import forms from horizon.utils import memoized from watcher_dashboard.api import watcher from watcher_dashboard.common import client as common_client from watcher_dashboard.content.actions import forms as action_forms from watcher_dashboard.content.actions import tables from watcher_dashboard.content.actions import tabs as wtabs class IndexView(horizon.tables.DataTableView): table_class = tables.ActionsTable template_name = 'infra_optim/actions/index.html' page_title = _("Actions") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['audits_count'] = self.get_actions_count() return context def get_data(self): actions = [] search_opts = self.get_filters() try: actions = watcher.Action.list(self.request, **search_opts) except Exception: horizon.exceptions.handle( self.request, _("Unable to retrieve action information.") ) return actions def get_actions_count(self): return len(self.get_data()) def get_filters(self): filters = {} filter_action = self.table._meta._filter_action if filter_action: filter_field = self.table.get_filter_field() if filter_action.is_api_filter(filter_field): filter_string = self.table.get_filter_string() if filter_field and filter_string: filters[filter_field] = filter_string return filters class DetailView(horizon.tables.MultiTableView): table_classes = [tables.ActionParametersTable] tab_group_class = wtabs.ActionDetailTabs template_name = 'infra_optim/actions/details.html' redirect_url = 'horizon:admin:actions:index' page_title = _("Action Details: {{ action.uuid }}") @memoized.memoized_method def max_version(self): return common_client.get_max_version(self.request) @memoized.memoized_method def _get_data(self): action_uuid = None try: action_uuid = self.kwargs['action_uuid'] server_version = self.max_version() version = ( common_client.MV_SKIP_ACTION if common_client.is_microversion_supported( server_version, common_client.MV_SKIP_ACTION ) else None ) action = watcher.Action.get( self.request, action_uuid, api_version=version ) except Exception: msg = ( _('Unable to retrieve details for action "%s".') % action_uuid ) horizon.exceptions.handle( self.request, msg, redirect=self.redirect_url ) return action def get_parameters_data(self): action = self._get_data() parameter_cls = collections.namedtuple( 'Parameter', field_names=['name', 'value'] ) return [ parameter_cls(name=name, value=value) for name, value in action.input_parameters.items() ] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) action = self._get_data() context["action"] = action return context def get_tabs(self, request, *args, **kwargs): action = self._get_data() # ports = self._get_ports() return self.tab_group_class( request, action=action, # ports=ports, **kwargs, ) class SkipActionView(forms.ModalFormView): """Modal view for skipping an action in an action plan.""" form_class = action_forms.SkipActionForm form_id = "skip_action_form" modal_header = _("Skip Action") template_name = 'infra_optim/actions/skip.html' submit_label = _("Skip Action") submit_url = "horizon:admin:actions:skip" cancel_url = "horizon:admin:actions:index" page_title = _("Skip Action") _action_plan_uuid = None def form_valid(self, form): """Capture action_plan_uuid before redirect to avoid extra API call.""" action = form.cleaned_data['_action'] self._action_plan_uuid = action.action_plan_uuid return super().form_valid(form) def get_success_url(self): """Redirect to parent action plan detail after skip.""" if self._action_plan_uuid: return reverse( 'horizon:admin:action_plans:detail', args=(self._action_plan_uuid,), ) return reverse('horizon:admin:actions:index') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['action_id'] = self.kwargs['action_id'] context['submit_url'] = reverse( self.submit_url, args=(self.kwargs['action_id'],) ) return context def get_initial(self): return {'action_id': self.kwargs['action_id']} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9388196 watcher_dashboard-16.0.0/watcher_dashboard/content/audit_templates/0000775000175000017500000000000015257165054024400 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.826848 watcher_dashboard-16.0.0/watcher_dashboard/content/audit_templates/__init__.py0000664000175000017500000000000015257165015026474 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audit_templates/forms.py0000664000175000017500000001156615257165015026106 0ustar00zuulzuul# Copyright 2012, Nachi Ueno, NTT MCL, Inc. # All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Forms for starting Watcher Audit Templates. """ import logging import yaml from django.core import exceptions as core_exc from django.urls import reverse from django.utils.translation import gettext_lazy as _ from horizon import exceptions from horizon import forms from horizon import messages from watcher_dashboard.api import watcher LOG = logging.getLogger(__name__) class YamlValidator: message = _('Enter a valid YAML or JSON value.') code = 'invalid' def __init__(self, message=None): if message: self.message = message def __call__(self, value): try: yaml.safe_load(value) except Exception: raise core_exc.ValidationError(self.message, code=self.code) class CreateForm(forms.SelfHandlingForm): name = forms.CharField(max_length=255, label=_("Name")) description = forms.CharField( max_length=255, label=_("Description"), required=False ) goal = forms.ChoiceField(label=_('Goal')) strategy = forms.DynamicChoiceField(label=_('Strategy'), required=False) scope = forms.CharField( label=_('Scope'), required=False, widget=forms.widgets.Textarea, validators=[YamlValidator()], ) failure_url = 'horizon:admin:audit_templates:index' def __init__(self, request, *args, **kwargs): super().__init__(request, *args, **kwargs) goals = self._get_goal_list(request) if goals: self.fields['goal'].choices = goals else: del self.fields['goal'] # Defer strategy choices to be populated dynamically based on goal # selection via AJAX in the template. Start with only the default. if 'strategy' in self.fields: self.fields['strategy'].choices = [("", _("Select Strategy"))] # If this is a POST, populate strategies for the selected goal so that # server-side validation accepts the submitted value (mirrors the AJAX # behavior in the template for client-side population). selected_goal = self.data.get('goal') if selected_goal and 'strategy' in self.fields: filtered_strategies = self._get_strategy_list_for_goal( request, selected_goal ) if filtered_strategies: self.fields['strategy'].choices = filtered_strategies def _get_goal_list(self, request): try: goals = watcher.Goal.list(self.request) except Exception as exc: msg = _('Failed to get goals list: %s') % str(exc) LOG.info(msg) messages.warning(request, msg) messages.warning(request, exc) goals = [] choices = [(goal.uuid, goal.display_name) for goal in goals] if choices: choices.insert(0, ("", _("Select Goal"))) return choices def _get_strategy_list_for_goal(self, request, goal_uuid): try: strategies = watcher.Strategy.list(self.request, goal=goal_uuid) except Exception as exc: msg = _('Failed to get strategies for selected goal.') LOG.info(msg) messages.warning(request, msg) messages.warning(request, exc) strategies = [] choices = [ (strategy.uuid, watcher.get_strategy_display_name(strategy)) for strategy in strategies ] if choices: choices.insert(0, ("", _("Select Strategy"))) return choices def handle(self, request, data): try: params = {'name': data['name']} params['description'] = data['description'] params['goal'] = data['goal'] params['strategy'] = data['strategy'] or None params['scope'] = ( [] if not data['scope'] else yaml.safe_load(data['scope']) ) audit_tpl = watcher.AuditTemplate.create(request, **params) message = _('Audit Template was successfully created.') messages.success(request, message) return audit_tpl except Exception as exc: msg = _('Failed to create audit template.') LOG.info(exc) redirect = reverse(self.failure_url) exceptions.handle(request, msg, redirect=redirect) return False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audit_templates/panel.py0000664000175000017500000000143615257165015026052 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import horizon from django.utils.translation import gettext_lazy as _ class AuditTemplates(horizon.Panel): name = _("Audit Templates") slug = "audit_templates" permissions = ("openstack.services.infra-optim",) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audit_templates/tables.py0000664000175000017500000000647315257165015026233 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import horizon.exceptions import horizon.messages import horizon.tables from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from watcher_dashboard.api import watcher class CreateAuditTemplates(horizon.tables.LinkAction): name = "create" verbose_name = _("Create Template") url = "horizon:admin:audit_templates:create" classes = ("ajax-modal", "btn-launch") policy_rules = (("infra-optim", "audit_template:create"),) class AuditTemplatesFilterAction(horizon.tables.FilterAction): filter_type = "server" filter_choices = ( ('goal', _("Goal ="), True), ('strategy', _("Strategy ="), True), ) policy_rules = (("infra-optim", "audit_template:detail"),) class LaunchAudit(horizon.tables.BatchAction): name = "launch_audit" verbose_name = _("Launch Audit") data_type_singular = _("Launch Audit") data_type_plural = _("Launch Audits") success_url = "horizon:admin:audits:index" policy_rules = (("infra-optim", "audit:create"),) @staticmethod def action_present(count): return ngettext_lazy("Launch Audit", "Launch Audits", count) @staticmethod def action_past(count): return ngettext_lazy("Launched Audit", "Launched Audits", count) def action(self, request, obj_id): params = {'audit_template_uuid': obj_id} params['audit_type'] = 'ONESHOT' params['auto_trigger'] = False watcher.Audit.create(request, **params) class ArchiveAuditTemplates(horizon.tables.DeleteAction): verbose_name = _("Archive Templates") policy_rules = (("infra-optim", "audit_template:delete"),) @staticmethod def action_present(count): return ngettext_lazy("Archive Template", "Archive Templates", count) @staticmethod def action_past(count): return ngettext_lazy("Archived Template", "Archived Templates", count) def delete(self, request, obj_id): watcher.AuditTemplate.delete(request, obj_id) class AuditTemplatesTable(horizon.tables.DataTable): name = horizon.tables.Column( 'name', verbose_name=_("Name"), link="horizon:admin:audit_templates:detail", ) goal = horizon.tables.Column( 'goal_name', verbose_name=_('Goal'), status=True ) strategy = horizon.tables.Column( 'strategy_name', verbose_name=_('Strategy'), status=True ) def get_object_id(self, datum): return datum.uuid class Meta: name = "audit_templates" verbose_name = _("Audit Templates") table_actions = ( CreateAuditTemplates, AuditTemplatesFilterAction, LaunchAudit, ArchiveAuditTemplates, ) row_actions = (LaunchAudit, ArchiveAuditTemplates) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audit_templates/tabs.py0000664000175000017500000000203015257165015025673 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ from horizon import tabs class OverviewTab(tabs.Tab): name = _("Overview") slug = "overview" template_name = "infra_optim/audit_templates/_detail_overview.html" def get_context_data(self, request): return {"audit_template": self.tab_group.kwargs['audit_template']} class AuditTemplateDetailTabs(tabs.TabGroup): slug = "audit_template_details" tabs = (OverviewTab,) sticky = True ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audit_templates/tests.py0000664000175000017500000001106215257165015026111 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock from django import urls from watcher_dashboard import api from watcher_dashboard.tests import helpers as test INDEX_URL = urls.reverse('horizon:admin:audit_templates:index') CREATE_URL = urls.reverse('horizon:admin:audit_templates:create') DETAILS_VIEW = 'horizon:admin:audit_templates:detail' class AuditTemplatesTest(test.BaseAdminViewTests): def setUp(self): super().setUp() self.goal_list = self.goals.list() self.strategy_list = self.strategies.list() @mock.patch.object(api.watcher.AuditTemplate, 'list') def test_index(self, mock_list): mock_list.return_value = self.audit_templates.list() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'infra_optim/audit_templates/index.html') audit_templates = res.context['audit_templates_table'].data self.assertCountEqual(audit_templates, self.audit_templates.list()) @mock.patch.object(api.watcher.AuditTemplate, 'list') def test_audit_template_list_unavailable(self, mock_list): mock_list.side_effect = self.exceptions.watcher logger = 'watcher_dashboard.content.audit_templates.views' with self.assertLogs(logger, level='ERROR'): resp = self.client.get(INDEX_URL) self.assertEqual(resp.status_code, 200) self.assertMessageCount(resp, error=1, warning=0) @mock.patch.object(api.watcher.Strategy, 'list') @mock.patch.object(api.watcher.Goal, 'list') def test_create_get(self, m_goal_list, m_strategy_list): m_goal_list.return_value = self.goal_list m_strategy_list.return_value = self.strategy_list res = self.client.get(CREATE_URL) self.assertTemplateUsed(res, 'infra_optim/audit_templates/create.html') @mock.patch.object(api.watcher.Strategy, 'list') @mock.patch.object(api.watcher.Goal, 'list') @mock.patch.object(api.watcher.AuditTemplate, 'create') def test_create_post(self, m_audit_create, m_goal_list, m_strategy_list): at = self.audit_templates.first() form_data = { 'name': at.name, 'goal': at.goal_uuid, 'strategy': at.strategy_uuid, 'description': at.description, 'scope': at.scope, } m_goal_list.return_value = self.goal_list m_strategy_list.return_value = self.strategy_list m_audit_create.return_value = at res = self.client.post(CREATE_URL, form_data) self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) @mock.patch.object(api.watcher.AuditTemplate, 'get') def test_details(self, m_get): at = self.audit_templates.first() at_id = at.uuid m_get.return_value = at DETAILS_URL = urls.reverse(DETAILS_VIEW, args=[at_id]) res = self.client.get(DETAILS_URL) self.assertTemplateUsed( res, 'infra_optim/audit_templates/details.html' ) audit_templates = res.context['audit_template'] self.assertCountEqual([audit_templates], [at]) @mock.patch.object(api.watcher.AuditTemplate, 'get') def test_details_exception(self, m_get): at = self.audit_templates.first() at_id = at.uuid m_get.side_effect = self.exceptions.watcher DETAILS_URL = urls.reverse(DETAILS_VIEW, args=[at_id]) logger = 'watcher_dashboard.content.audit_templates.views' with self.assertLogs(logger, level='ERROR'): res = self.client.get(DETAILS_URL) self.assertRedirectsNoFollow(res, INDEX_URL) @mock.patch.object(api.watcher.AuditTemplate, 'delete') @mock.patch.object(api.watcher.AuditTemplate, 'list') def test_delete(self, m_list, m_del): at_list = self.audit_templates.list() at = self.audit_templates.first() at_id = at.uuid m_list.return_value = at_list form_data = {'action': 'audit_templates__delete', 'object_ids': at_id} res = self.client.post(INDEX_URL, form_data) self.assertRedirectsNoFollow(res, INDEX_URL) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audit_templates/urls.py0000664000175000017500000000206615257165015025740 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from watcher_dashboard.content.audit_templates import views urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path(r'^create/$', views.CreateView.as_view(), name='create'), re_path( r'^(?P[^/]+)/detail$', views.DetailView.as_view(), name='detail', ), re_path( r'^ajax/strategies/$', views.get_strategies_for_goal, name='get_strategies_for_goal', ), ] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audit_templates/views.py0000664000175000017500000001432515257165015026111 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import json import logging import horizon.exceptions import horizon.tables import horizon.tabs import horizon.workflows from django.http import JsonResponse from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from horizon import forms from horizon.utils import memoized from watcher_dashboard.api import watcher from watcher_dashboard.common import client as common_client from watcher_dashboard.content.audit_templates import forms as wforms from watcher_dashboard.content.audit_templates import tables from watcher_dashboard.content.audit_templates import tabs as wtabs LOG = logging.getLogger(__name__) class IndexView(horizon.tables.DataTableView): table_class = tables.AuditTemplatesTable template_name = 'infra_optim/audit_templates/index.html' page_title = _("Audit Templates") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['audit_templates_count'] = self.get_count() return context def get_data(self): audit_templates = [] search_opts = self.get_filters() try: audit_templates = watcher.AuditTemplate.list( self.request, **search_opts ) except Exception as exc: LOG.exception(exc) horizon.exceptions.handle( self.request, _("Unable to retrieve audit template information."), ) return audit_templates def get_count(self): return len(self.get_data()) def get_filters(self): filters = {} filter_action = self.table._meta._filter_action if filter_action: filter_field = self.table.get_filter_field() if filter_action.is_api_filter(filter_field): filter_string = self.table.get_filter_string() if filter_field and filter_string: filters[filter_field] = filter_string return filters class CreateView(forms.ModalFormView): form_class = wforms.CreateForm form_id = "create_audit_templates_form" modal_header = _("Create Audit Template") template_name = 'infra_optim/audit_templates/create.html' success_url = reverse_lazy("horizon:admin:audit_templates:index") page_title = _("Create an Audit Template") submit_label = _("Create Audit Template") submit_url = reverse_lazy("horizon:admin:audit_templates:create") def get_object_id(self, obj): return obj.uuid class DetailView(horizon.tabs.TabbedTableView): tab_group_class = wtabs.AuditTemplateDetailTabs template_name = 'infra_optim/audit_templates/details.html' redirect_url = 'horizon:admin:audit_templates:index' page_title = _("Audit Template Details: {{ audit_template.name }}") @memoized.memoized_method def max_version(self): return common_client.get_max_version(self.request) def _get_data(self): audit_template_uuid = None try: audit_template_uuid = self.kwargs['audit_template_uuid'] audit_template = watcher.AuditTemplate.get( self.request, audit_template_uuid ) if audit_template.scope: audit_template.scope = json.dumps(audit_template.scope) except Exception as exc: LOG.exception(exc) msg = ( _('Unable to retrieve details for audit template "%s".') % audit_template_uuid ) horizon.exceptions.handle( self.request, msg, redirect=self.redirect_url ) return audit_template def get_related_audits_data(self): try: audit_template = self._get_data() server_version = self.max_version() version = ( common_client.MV_START_END if common_client.is_microversion_supported( server_version, common_client.MV_START_END ) else None ) audits = watcher.Audit.list( self.request, api_version=version, audit_template=audit_template.uuid, ) except Exception as exc: LOG.exception(exc) audits = [] msg = _('Audits list cannot be retrieved.') horizon.exceptions.handle(self.request, msg) return audits def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) audit_template = self._get_data() context["audit_template"] = audit_template return context def get_tabs(self, request, *args, **kwargs): audit_template = self._get_data() # ports = self._get_ports() return self.tab_group_class( request, audit_template=audit_template, **kwargs ) def get_strategies_for_goal(request): """AJAX endpoint to get strategies filtered by selected goal. Expects a GET parameter 'goal_uuid'. Returns a JSON with a list of strategies each containing 'uuid' and 'display_name'. """ try: goal_uuid = request.GET.get('goal_uuid') if not goal_uuid: return JsonResponse({'error': 'goal_uuid is required'}, status=400) strategies = watcher.Strategy.list(request, goal=goal_uuid) data = [ { 'uuid': strategy.uuid, 'display_name': watcher.get_strategy_display_name(strategy), } for strategy in strategies ] return JsonResponse({'strategies': data}) except Exception as exc: LOG.exception("Error getting strategies for goal") return JsonResponse({'error': str(exc)}, status=500) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9388196 watcher_dashboard-16.0.0/watcher_dashboard/content/audits/0000775000175000017500000000000015257165054022505 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audits/__init__.py0000664000175000017500000000000015257165015024601 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audits/forms.py0000664000175000017500000002305315257165015024205 0ustar00zuulzuul# Copyright 2012, Nachi Ueno, NTT MCL, Inc. # All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Forms for starting Watcher Audits. """ import json import logging from django.urls import reverse from django.utils.translation import gettext_lazy as _ from horizon import exceptions from horizon import forms from horizon import messages from watcher_dashboard.api import watcher from watcher_dashboard.common import client as common_client LOG = logging.getLogger(__name__) ADD_AUDIT_TEMPLATES_URL = "horizon:admin:audit_templates:create" class CreateForm(forms.SelfHandlingForm): audit_template = forms.DynamicChoiceField( label=_("Audit Template"), add_item_link=ADD_AUDIT_TEMPLATES_URL ) audit_name = forms.CharField( max_length=255, label=_("Name"), help_text=_( "An audit name should not duplicate with existed audits' names." ), required=False, ) audit_type = forms.ChoiceField( label=_("Audit Type"), choices=[ (None, _("Select Audit Type")), ('oneshot', _('ONESHOT')), ('continuous', _('CONTINUOUS')), ], widget=forms.Select( attrs={'class': 'switchable', 'data-slug': 'audit_type'} ), ) interval = forms.CharField( label=_("Interval (in seconds or cron format)"), help_text=_("Interval in seconds or cron format for CONTINUOUS audit"), widget=forms.TextInput( attrs={ 'class': 'switched', 'data-switch-on': 'audit_type', 'data-audit_type-continuous': _( "Interval (in seconds or cron format)" ), } ), required=False, ) parameters = forms.CharField( label=_("Strategy Parameters (JSON)"), help_text=_( "Provide strategy parameters as a JSON object. " "See examples on the right." ), widget=forms.widgets.Textarea( attrs={ 'rows': 8, 'placeholder': ( '{\n' ' "memory_threshold": 0.8,\n' ' "enable_migration": true,\n' ' "compute_nodes": [\n' ' {"src_node": "compute1", ' '"dst_node": "compute2"}\n' ' ]\n' '}' ), } ), required=False, ) start_time = forms.DateTimeField( label=_("Start time"), help_text=_( "Local time in ISO 8601 (e.g. 2025-01-02T18:30:00); " "only used for CONTINUOUS audits. Watcher converts local " "time to UTC." ), widget=forms.DateTimeInput( format="%Y-%m-%dT%H:%M:%S", attrs={ 'class': 'switched', 'data-switch-on': 'audit_type', 'data-audit_type-continuous': _( "Start time (ISO 8601, local)" ), 'placeholder': 'YYYY-MM-DDTHH:MM:SS', }, ), required=False, input_formats=[ '%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%dT%H:%M:%S.%f', ], ) end_time = forms.DateTimeField( label=_("End time"), help_text=_( "Local time in ISO 8601 (e.g. 2025-01-02T18:30:00); " "only used for CONTINUOUS audits. Watcher converts local " "time to UTC." ), widget=forms.DateTimeInput( format="%Y-%m-%dT%H:%M:%S", attrs={ 'class': 'switched', 'data-switch-on': 'audit_type', 'data-audit_type-continuous': _("End time (ISO 8601, local)"), 'placeholder': 'YYYY-MM-DDTHH:MM:SS', }, ), required=False, input_formats=[ '%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%dT%H:%M:%S.%f', ], ) failure_url = 'horizon:admin:audits:index' auto_trigger = forms.BooleanField(label=_("Auto Trigger"), required=False) def __init__(self, request, *args, **kwargs): super().__init__(request, *args, **kwargs) audit_templates = self._get_audit_template_list(request) self.fields['audit_template'].choices = audit_templates # Hide start/end time fields when the server does not # support microversion 1.1 (MV_START_END). self._max_ver = common_client.get_max_version(request) if not common_client.is_microversion_supported( self._max_ver, common_client.MV_START_END ): del self.fields['start_time'] del self.fields['end_time'] def _get_audit_template_list(self, request): try: audit_templates = watcher.AuditTemplate.list(self.request) except Exception as e: msg = _('Failed to get audit template list: %s') % str(e) LOG.info(msg) messages.warning(request, msg) audit_templates = [] choices = [ (audit_template.uuid, audit_template.name or audit_template.uuid) for audit_template in audit_templates ] if choices: choices.insert(0, ("", _("Select Audit Template"))) else: choices.insert(0, ("", _("No Audit Template found"))) return choices def _parse_parameters(self, param_string): """Parse parameters JSON string into a dictionary :param param_string: String containing a JSON object :returns: Dictionary of parsed parameters """ if not param_string or not param_string.strip(): return {} try: parsed = json.loads(param_string) except ValueError as e: raise forms.ValidationError( _('Parameters must be valid JSON: %s') % str(e) ) if not isinstance(parsed, dict): raise forms.ValidationError(_('Parameters must be a JSON object')) return parsed def clean(self): cleaned_data = super().clean() audit_type = cleaned_data.get('audit_type') if audit_type == 'continuous' and not cleaned_data.get('interval'): msg = _('Please input an interval for continuous audit') raise forms.ValidationError(msg) if audit_type == 'continuous': start_time = cleaned_data.get('start_time') end_time = cleaned_data.get('end_time') if start_time and end_time and end_time <= start_time: raise forms.ValidationError( _('End time must be later than start time') ) # Validate parameters param_string = cleaned_data.get('parameters', '') try: parsed_params = self._parse_parameters(param_string) cleaned_data['parsed_parameters'] = parsed_params except forms.ValidationError: raise return cleaned_data def handle(self, request, data): try: params = {'audit_template_uuid': data.get('audit_template')} params['audit_type'] = data['audit_type'].upper() params['auto_trigger'] = data['auto_trigger'] params['name'] = data['audit_name'] if data['audit_type'] == 'continuous': params['interval'] = data['interval'] # Convert datetimes to ISO 8601 for API (local time) if data.get('start_time'): params['start_time'] = data['start_time'].isoformat( timespec='seconds' ) if data.get('end_time'): params['end_time'] = data['end_time'].isoformat( timespec='seconds' ) else: params['interval'] = None # Add parsed parameters if they exist parsed_parameters = data.get('parsed_parameters') if parsed_parameters: params['parameters'] = parsed_parameters # Request the highest version the server supports # when start/end fields are available (>= 1.1); # fall back to the default (1.0) otherwise. api_ver = ( common_client.MV_START_END if common_client.is_microversion_supported( self._max_ver, common_client.MV_START_END ) else None ) audit = watcher.Audit.create( request, api_version=api_ver, **params ) message = _('Audit was successfully created.') messages.success(request, message) return audit except Exception as exc: if getattr(exc, 'http_status', None) == 409: msg = _('Error: Audit name already exists.') else: msg = _('Failed to create audit.') LOG.info(exc) redirect = reverse(self.failure_url) exceptions.handle(request, msg, redirect=redirect) return False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audits/panel.py0000664000175000017500000000140415257165015024152 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import horizon from django.utils.translation import gettext_lazy as _ class Audits(horizon.Panel): name = _("Audits") slug = "audits" permissions = ('openstack.services.infra-optim',) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audits/tables.py0000664000175000017500000001310115257165015024322 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import horizon.exceptions import horizon.messages import horizon.tables from django import shortcuts from django import urls from django.template.defaultfilters import title # noqa from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from django.utils.translation import pgettext_lazy from horizon.utils import filters from watcher_dashboard.api import watcher AUDIT_STATE_DISPLAY_CHOICES = ( ("NO STATE", pgettext_lazy("State of an audit", "No State")), ("ONGOING", pgettext_lazy("State of an audit", "On Going")), ("SUCCEEDED", pgettext_lazy("State of an audit", "Succeeded")), ("SUBMITTED", pgettext_lazy("State of an audit", "Submitted")), ("FAILED", pgettext_lazy("State of an audit", "Failed")), ("DELETED", pgettext_lazy("State of an audit", "Deleted")), ("PENDING", pgettext_lazy("State of an audit", "Pending")), ) class AuditsFilterAction(horizon.tables.FilterAction): # server = choices query = text filter_type = "server" filter_choices = ( ('goal', _("Goal ="), True), ('strategy', _("Strategy ="), True), ) policy_rules = (("infra-optim", "audit:detail"),) class CreateAudit(horizon.tables.LinkAction): name = "create_audit" verbose_name = _("Create Audit") url = "horizon:admin:audits:create" classes = ("ajax-modal", "btn-launch") policy_rules = (("infra-optim", "audit:create"),) class GoToActionPlan(horizon.tables.Action): name = "go_to_action_plan" verbose_name = _("Go to Action Plan") url = "horizon:admin:action_plans:detail" policy_rules = (("infra-optim", "action_plan:detail"),) def allowed(self, request, audit): return audit or audit.state in ("SUCCEEDED",) def single(self, table, request, audit_id): try: action_plans = watcher.ActionPlan.list(request, audit=audit_id) except Exception: horizon.exceptions.handle( request, _("Unable to retrieve action_plan information.") ) return "javascript:void(0);" return shortcuts.redirect( urls.reverse(self.url, args=[action_plans[0].uuid]) ) class ArchiveAudits(horizon.tables.DeleteAction): verbose_name = _("Archive Audits") policy_rules = (("infra-optim", "audit:delete"),) def allowed(self, request, audit): return audit.state not in ("ONGOING", "PENDING") @staticmethod def action_present(count): return ngettext_lazy("Archive Audit", "Archive Audits", count) @staticmethod def action_past(count): return ngettext_lazy("Archived Audit", "Archived Audits", count) def delete(self, request, obj_id): watcher.Audit.delete(request, obj_id) class CancelAudits(horizon.tables.BatchAction): name = _("Cancel") verbose_name = _("Cancel Audits") policy_rules = (("infra-optim", "audit:update"),) def allowed(self, request, audit): return audit.state in ("ONGOING", "PENDING") @staticmethod def action_present(count): return ngettext_lazy("Cancel Audit", "Cancel Audits", count) @staticmethod def action_past(count): return ngettext_lazy("Cancelled Audit", "Cancelled Audits", count) def action(self, request, obj_id): watcher.Audit.cancel(request, obj_id) class AuditsTable(horizon.tables.DataTable): uuid = horizon.tables.Column( 'uuid', verbose_name=_("UUID"), link="horizon:admin:audits:detail" ) name = horizon.tables.Column( 'name', verbose_name=_("Name"), link="horizon:admin:audits:detail" ) goal = horizon.tables.Column('goal_name', verbose_name=_('Goal')) strategy = horizon.tables.Column( 'strategy_name', verbose_name=_('Strategy') ) audit_type = horizon.tables.Column( 'audit_type', verbose_name=_('Audit Type') ) auto_trigger = horizon.tables.Column( 'auto_trigger', verbose_name=_('Auto Trigger') ) status = horizon.tables.Column( 'state', verbose_name=_('State'), display_choices=AUDIT_STATE_DISPLAY_CHOICES, ) def get_object_id(self, datum): return datum.uuid class Meta: name = "audits" verbose_name = _("Audits") launch_actions = (CreateAudit,) table_actions = launch_actions + (AuditsFilterAction,) row_actions = (GoToActionPlan, ArchiveAudits, CancelAudits) class RelatedAuditsTable(horizon.tables.DataTable): name = horizon.tables.Column( 'uuid', verbose_name=_("UUID"), link="horizon:admin:audits:detail" ) audit_template = horizon.tables.Column( 'audit_template_name', verbose_name=_('Audit Template'), filters=(title, filters.replace_underscores), ) status = horizon.tables.Column( 'state', verbose_name=_('State'), display_choices=AUDIT_STATE_DISPLAY_CHOICES, ) def get_object_id(self, datum): return datum.uuid class Meta: name = "audits" verbose_name = _("Related audits") hidden_title = False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audits/tabs.py0000664000175000017500000000175415257165015024014 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ from horizon import tabs class OverviewTab(tabs.Tab): name = _("Overview") slug = "overview" template_name = "infra_optim/audits/_detail_overview.html" def get_context_data(self, request): return {"audit": self.tab_group.kwargs['audit']} class AuditDetailTabs(tabs.TabGroup): slug = "audit_details" tabs = (OverviewTab,) sticky = True ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audits/urls.py0000664000175000017500000000205415257165015024042 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from watcher_dashboard.content.audits import views urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path(r'^create/$', views.CreateView.as_view(), name='create'), re_path( r'^(?P[^/]+)/detail$', views.DetailView.as_view(), name='detail', ), re_path( r'^get_strategy_parameters/$', views.get_strategy_parameters, name='get_strategy_parameters', ), ] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/audits/views.py0000664000175000017500000002251015257165015024211 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import json import logging import horizon.exceptions import horizon.tables import horizon.tabs import horizon.workflows import yaml from django.http import JsonResponse from django.urls import reverse from django.urls import reverse_lazy from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from horizon import forms from horizon.utils import memoized from watcher_dashboard.api import watcher from watcher_dashboard.common import client as common_client from watcher_dashboard.content.action_plans import tables as action_plan_tables from watcher_dashboard.content.audits import forms as wforms from watcher_dashboard.content.audits import tables from watcher_dashboard.content.audits import tabs as wtabs LOG = logging.getLogger(__name__) class IndexView(horizon.tables.DataTableView): table_class = tables.AuditsTable template_name = 'infra_optim/audits/index.html' page_title = _("Audits") @memoized.memoized_method def max_version(self): return common_client.get_max_version(self.request) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) create_action = { 'name': _("New Audit"), 'url': reverse('horizon:admin:audits:create'), 'icon': 'fa-plus', 'ajax_modal': True, } context['header_actions'] = [create_action] context['audits_count'] = self.get_audits_count() return context def get_data(self): audits = [] search_opts = self.get_filters() try: server_version = self.max_version() version = ( common_client.MV_START_END if common_client.is_microversion_supported( server_version, common_client.MV_START_END ) else None ) audits = watcher.Audit.list( self.request, api_version=version, **search_opts ) except Exception: horizon.exceptions.handle( self.request, _("Unable to retrieve audit information.") ) return audits def get_audits_count(self): return len(self.get_data()) def get_filters(self): filters = {} filter_action = self.table._meta._filter_action if filter_action: filter_field = self.table.get_filter_field() if filter_action.is_api_filter(filter_field): filter_string = self.table.get_filter_string() if filter_field and filter_string: filters[filter_field] = filter_string return filters class CreateView(forms.ModalFormView): form_class = wforms.CreateForm form_id = "create_audit_form" modal_header = _("Create Audit") template_name = 'infra_optim/audits/create.html' success_url = reverse_lazy("horizon:admin:audits:index") page_title = _("Create Audit") submit_label = _("Create Audit") submit_url = reverse_lazy("horizon:admin:audits:create") class DetailView(horizon.tables.MultiTableView): table_classes = (action_plan_tables.RelatedActionPlansTable,) tab_group_class = wtabs.AuditDetailTabs template_name = 'infra_optim/audits/details.html' redirect_url = 'horizon:admin:audits:index' page_title = _("Audit Details: {{ audit.uuid }}") @memoized.memoized_method def max_version(self): return common_client.get_max_version(self.request) @memoized.memoized_method def _get_data(self): audit_uuid = None try: audit_uuid = self.kwargs['audit_uuid'] server_version = self.max_version() version = ( common_client.MV_START_END if common_client.is_microversion_supported( server_version, common_client.MV_START_END ) else None ) audit = watcher.Audit.get( self.request, audit_uuid, api_version=version ) except Exception: msg = _('Unable to retrieve details for audit "%s".') % audit_uuid horizon.exceptions.handle( self.request, msg, redirect=self.redirect_url ) return audit def _render_pretty_parameters(self, params): """Return a human-friendly rendering of parameters. It is used to render parameters on the audit details page in a YAML format for readability. Rules: - If params is a JSON string, parse it first. - If result is a dict/list, pretty-print as YAML in a
 block.
        - If result is a scalar (str/int/bool), return it as-is.
        - On any error, fall back to the original params value.
        """
        try:
            obj = params
            # Parameters may be stored as a JSON-serialized string. Try to
            # decode it but tolerate non-JSON strings (e.g. plain text).
            if isinstance(params, str):
                try:
                    obj = json.loads(params)
                except Exception:
                    obj = params

            # Dicts/lists are presented as indented YAML for readability.
            if isinstance(obj, dict | list):
                dumped = yaml.safe_dump(
                    obj, default_flow_style=False, sort_keys=False
                )
                return mark_safe(  # noqa: S308  # nosec B703,B308
                    f'
{dumped}
' ) # Scalars or unknown types: return directly. if obj is not None: return obj except Exception: # Any unexpected issue: show the raw parameters. return params def get_related_action_plans_data(self): try: action_plan = self._get_data() audits = watcher.ActionPlan.list( self.request, audit=action_plan.uuid ) except Exception as exc: LOG.exception(exc) audits = [] msg = _('Action plan list cannot be retrieved.') horizon.exceptions.handle(self.request, msg) return audits def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) audit = self._get_data() context["audit"] = audit # Prepare pretty parameters rendering (YAML) for the template. The # helper encapsulates the logic so it is easier to test/maintain. context["audit_parameters_pretty"] = self._render_pretty_parameters( getattr(audit, 'parameters', None) ) return context def get_tabs(self, request, *args, **kwargs): audit = self._get_data() # ports = self._get_ports() return self.tab_group_class(request, audit=audit, **kwargs) def get_strategy_parameters(request): """AJAX endpoint to get strategy parameters based on audit template.""" try: audit_template_uuid = request.GET.get('audit_template_uuid') if not audit_template_uuid: return JsonResponse( {'error': 'Audit template UUID is required'}, status=400 ) # Get the audit template audit_template = watcher.AuditTemplate.get( request, audit_template_uuid ) if not audit_template.strategy_uuid: return JsonResponse( { 'strategy_name': audit_template.strategy_name or 'auto', 'parameters_spec': {}, 'message': ( 'No specific strategy selected. Parameters will ' 'be automatically determined.' ), } ) # Get the strategy details strategy = watcher.Strategy.get(request, audit_template.strategy_uuid) # Parse parameters_spec if it exists parameters_spec = {} if hasattr(strategy, 'parameters_spec') and strategy.parameters_spec: if isinstance(strategy.parameters_spec, dict): properties = strategy.parameters_spec.get('properties', {}) parameters_spec = properties elif isinstance(strategy.parameters_spec, str): try: parsed_spec = json.loads(strategy.parameters_spec) parameters_spec = parsed_spec.get('properties', {}) except ValueError: parameters_spec = {} return JsonResponse( { 'strategy_name': watcher.get_strategy_display_name(strategy), 'strategy_uuid': strategy.uuid, 'parameters_spec': parameters_spec, } ) except Exception as e: LOG.exception("Error getting strategy parameters") return JsonResponse({'error': str(e)}, status=500) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9398196 watcher_dashboard-16.0.0/watcher_dashboard/content/goals/0000775000175000017500000000000015257165054022321 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/goals/__init__.py0000664000175000017500000000000015257165015024415 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/goals/panel.py0000664000175000017500000000140115257165015023763 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import horizon from django.utils.translation import gettext_lazy as _ class Goals(horizon.Panel): name = _("Goals") slug = "goals" permissions = ("openstack.services.infra-optim",) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/goals/tables.py0000664000175000017500000000337615257165015024153 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.template.defaultfilters import title # noqa from django.utils.translation import gettext_lazy as _ import horizon.exceptions import horizon.messages import horizon.tables class GoalsTable(horizon.tables.DataTable): uuid = horizon.tables.Column( 'uuid', verbose_name=_("UUID"), link="horizon:admin:goals:detail" ) name = horizon.tables.Column('name', verbose_name=_('Name')) display_name = horizon.tables.Column( 'display_name', verbose_name=_('Verbose Name') ) def get_object_id(self, datum): return datum.uuid class Meta: name = "goals" verbose_name = _("Goals") class EfficacySpecificationTable(horizon.tables.DataTable): name = horizon.tables.Column('name', verbose_name=_("Name")) description = horizon.tables.Column( 'description', verbose_name=_("Description") ) unit = horizon.tables.Column('unit', verbose_name=_("Unit")) schema = horizon.tables.Column('schema', verbose_name=_("Schema")) def get_object_id(self, datum): return datum.name class Meta: name = "efficacy_specification" verbose_name = _("Efficacy specification") hidden_title = False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.827848 watcher_dashboard-16.0.0/watcher_dashboard/content/goals/tabs.py0000664000175000017500000000174715257165015023632 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ from horizon import tabs class OverviewTab(tabs.Tab): name = _("Overview") slug = "overview" template_name = "infra_optim/goals/_detail_overview.html" def get_context_data(self, request): return {"goal": self.tab_group.kwargs['goal']} class GoalDetailTabs(tabs.TabGroup): slug = "goal_details" tabs = (OverviewTab,) sticky = True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/content/goals/tests.py0000664000175000017500000000507615257165015024042 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock from django import urls from watcher_dashboard import api from watcher_dashboard.tests import helpers as test INDEX_URL = urls.reverse('horizon:admin:goals:index') DETAILS_VIEW = 'horizon:admin:goals:detail' class GoalsTest(test.BaseAdminViewTests): @mock.patch.object(api.watcher.Goal, 'list') def test_index(self, mock_list): mock_list.return_value = self.goals.list() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'infra_optim/goals/index.html') goals = res.context['goals_table'].data self.assertCountEqual(goals, self.goals.list()) @mock.patch.object(api.watcher.Goal, 'list') def test_goal_list_unavailable(self, mock_list): mock_list.side_effect = self.exceptions.watcher logger = 'watcher_dashboard.content.goals.views' with self.assertLogs(logger, level='ERROR'): resp = self.client.get(INDEX_URL) self.assertEqual(resp.status_code, 200) self.assertMessageCount(resp, error=1, warning=0) @mock.patch.object(api.watcher.Strategy, 'list') @mock.patch.object(api.watcher.Goal, 'get') def test_details(self, mock_get, mock_list): goal = self.goals.first() goal_id = goal.uuid mock_get.return_value = goal DETAILS_URL = urls.reverse(DETAILS_VIEW, args=[goal_id]) res = self.client.get(DETAILS_URL) self.assertTemplateUsed(res, 'infra_optim/goals/details.html') goals = res.context['goal'] self.assertCountEqual([goals], [goal]) @mock.patch.object(api.watcher.Goal, 'get') def test_details_exception(self, mock_get): at = self.goals.first() at_id = at.uuid mock_get.side_effect = self.exceptions.watcher DETAILS_URL = urls.reverse(DETAILS_VIEW, args=[at_id]) logger = 'watcher_dashboard.content.goals.views' with self.assertLogs(logger, level='ERROR'): res = self.client.get(DETAILS_URL) self.assertRedirectsNoFollow(res, INDEX_URL) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/content/goals/urls.py0000664000175000017500000000153215257165015023656 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from watcher_dashboard.content.goals import views urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path( r'^(?P[^/]+)/detail$', views.DetailView.as_view(), name='detail', ), ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/content/goals/views.py0000664000175000017500000001055215257165015024030 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import horizon.exceptions import horizon.tables import horizon.tabs import horizon.workflows from django.utils.translation import gettext_lazy as _ from horizon.utils import memoized from watcher_dashboard.api import watcher from watcher_dashboard.content.goals import tables from watcher_dashboard.content.goals import tabs as wtabs from watcher_dashboard.content.strategies import tables as strategies_tables LOG = logging.getLogger(__name__) class IndexView(horizon.tables.DataTableView): table_class = tables.GoalsTable template_name = 'infra_optim/goals/index.html' page_title = _("Goals") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['goals_count'] = self.get_goals_count() return context def get_data(self): goals = [] search_opts = self.get_filters() try: goals = watcher.Goal.list(self.request, **search_opts) except Exception as exc: LOG.exception(exc) horizon.exceptions.handle( self.request, _("Unable to retrieve goal information.") ) return goals def get_goals_count(self): return len(self.get_data()) def get_filters(self): filters = {} filter_action = self.table._meta._filter_action if filter_action: filter_field = self.table.get_filter_field() if filter_action.is_api_filter(filter_field): filter_string = self.table.get_filter_string() if filter_field and filter_string: filters[filter_field] = filter_string return filters class DetailView(horizon.tables.MultiTableView): table_classes = ( tables.EfficacySpecificationTable, strategies_tables.RelatedStrategiesTable, ) tab_group_class = wtabs.GoalDetailTabs template_name = 'infra_optim/goals/details.html' redirect_url = 'horizon:admin:goals:index' page_title = _("Goal Details: {{ goal.name }}") @memoized.memoized_method def _get_data(self): goal_uuid = None try: goal_uuid = self.kwargs['goal_uuid'] goal = watcher.Goal.get(self.request, goal_uuid) except Exception as exc: LOG.exception(exc) msg = _('Unable to retrieve details for goal "%s".') % goal_uuid horizon.exceptions.handle( self.request, msg, redirect=self.redirect_url ) return goal def get_related_strategies_data(self): try: goal = self._get_data() strategies = watcher.Strategy.list(self.request, goal=goal.uuid) except Exception as exc: LOG.exception(exc) strategies = [] msg = _('Strategy list cannot be retrieved.') horizon.exceptions.handle(self.request, msg) return strategies def get_efficacy_specification_data(self): try: goal = self._get_data() indicators_spec = [ watcher.EfficacyIndicatorSpec(spec) for spec in goal.efficacy_specification ] except Exception as exc: LOG.exception(exc) indicators_spec = [] msg = _('Efficacy specification cannot be retrieved.') horizon.exceptions.handle(self.request, msg) return indicators_spec def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) goal = self._get_data() context["goal"] = goal return context def get_tabs(self, request, *args, **kwargs): goal = self._get_data() # ports = self._get_ports() return self.tab_group_class( request, goal=goal, # ports=ports, **kwargs, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9398196 watcher_dashboard-16.0.0/watcher_dashboard/content/strategies/0000775000175000017500000000000015257165054023366 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/content/strategies/__init__.py0000664000175000017500000000000015257165015025462 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/content/strategies/forms.py0000664000175000017500000000000015257165015025051 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/content/strategies/panel.py0000664000175000017500000000142015257165015025031 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import horizon from django.utils.translation import gettext_lazy as _ class Strategies(horizon.Panel): name = _("Strategies") slug = "strategies" permissions = ("openstack.services.infra-optim",) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/content/strategies/tables.py0000664000175000017500000000415015257165015025207 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import horizon.exceptions import horizon.messages import horizon.tables from django.utils.translation import gettext_lazy as _ class StrategiesFilterAction(horizon.tables.FilterAction): # server = choices query = text filter_type = "server" filter_choices = (('goal', _("Goal ="), True),) policy_rules = (("infra-optim", "strategy:detail"),) class StrategiesTable(horizon.tables.DataTable): uuid = horizon.tables.Column( 'uuid', verbose_name=_("UUID"), link="horizon:admin:strategies:detail" ) name = horizon.tables.Column('name', verbose_name=_('Name')) display_name = horizon.tables.Column( 'display_name', verbose_name=_('Verbose Name') ) goal = horizon.tables.Column('goal_name', verbose_name=_("Goal")) def get_object_id(self, datum): return datum.uuid class Meta: name = "strategies" verbose_name = _("Strategies") table_actions = (StrategiesFilterAction,) class RelatedStrategiesTable(horizon.tables.DataTable): uuid = horizon.tables.Column( 'uuid', verbose_name=_("UUID"), link="horizon:admin:strategies:detail" ) name = horizon.tables.Column('name', verbose_name=_('Name')) display_name = horizon.tables.Column( 'display_name', verbose_name=_('Verbose Name') ) goal = horizon.tables.Column('goal_name', verbose_name=_("Goal")) def get_object_id(self, datum): return datum.uuid class Meta: name = "related_strategies" verbose_name = _("Related strategies") hidden_title = False ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/content/strategies/tabs.py0000664000175000017500000000177415257165015024677 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.utils.translation import gettext_lazy as _ from horizon import tabs class OverviewTab(tabs.Tab): name = _("Overview") slug = "overview" template_name = "infra_optim/strategies/_detail_overview.html" def get_context_data(self, request): return {"strategy": self.tab_group.kwargs['strategy']} class StrategyDetailTabs(tabs.TabGroup): slug = "strategy_details" tabs = (OverviewTab,) sticky = True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/content/strategies/tests.py0000664000175000017500000000542715257165015025107 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock from django import urls from watcher_dashboard import api from watcher_dashboard.tests import helpers as test INDEX_URL = urls.reverse('horizon:admin:strategies:index') DETAILS_VIEW = 'horizon:admin:strategies:detail' class StrategiesTest(test.BaseAdminViewTests): goal_list = [ 'BASIC_CONSOLIDATION', 'MINIMIZE_ENERGY_CONSUMPTION', 'BALANCE_LOAD', 'MINIMIZE_LICENSING_COST', 'PREPARED_PLAN_OPERATION', ] @mock.patch.object(api.watcher.Strategy, 'list') def test_index(self, mock_list): mock_list.return_value = self.strategies.list() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'infra_optim/strategies/index.html') strategies = res.context['strategies_table'].data self.assertCountEqual(strategies, self.strategies.list()) @mock.patch.object(api.watcher.Strategy, 'list') def test_strategy_list_unavailable(self, mock_list): mock_list.side_effect = self.exceptions.watcher logger = 'watcher_dashboard.content.strategies.views' with self.assertLogs(logger, level='ERROR'): resp = self.client.get(INDEX_URL) self.assertEqual(resp.status_code, 200) self.assertMessageCount(resp, error=1, warning=0) @mock.patch.object(api.watcher.Strategy, 'get') def test_details(self, mock_get): at = self.strategies.first() at_id = at.uuid mock_get.return_value = at DETAILS_URL = urls.reverse(DETAILS_VIEW, args=[at_id]) res = self.client.get(DETAILS_URL) self.assertTemplateUsed(res, 'infra_optim/strategies/details.html') strategies = res.context['strategy'] self.assertCountEqual([strategies], [at]) @mock.patch.object(api.watcher.Strategy, 'get') def test_details_exception(self, mock_get): at = self.strategies.first() at_id = at.uuid mock_get.side_effect = self.exceptions.watcher DETAILS_URL = urls.reverse(DETAILS_VIEW, args=[at_id]) logger = 'watcher_dashboard.content.strategies.views' with self.assertLogs(logger, level='ERROR'): res = self.client.get(DETAILS_URL) self.assertRedirectsNoFollow(res, INDEX_URL) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/content/strategies/urls.py0000664000175000017500000000154315257165015024725 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from django.urls import re_path from watcher_dashboard.content.strategies import views urlpatterns = [ re_path(r'^$', views.IndexView.as_view(), name='index'), re_path( r'^(?P[^/]+)/detail$', views.DetailView.as_view(), name='detail', ), ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/content/strategies/views.py0000664000175000017500000001261715257165015025101 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import json import logging import horizon.exceptions import horizon.tables import horizon.tabs import horizon.workflows from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from horizon.utils import memoized from watcher_dashboard.api import watcher from watcher_dashboard.content.strategies import tables from watcher_dashboard.content.strategies import tabs as wtabs LOG = logging.getLogger(__name__) class IndexView(horizon.tables.DataTableView): table_class = tables.StrategiesTable template_name = 'infra_optim/strategies/index.html' page_title = _("Strategies") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['strategies_count'] = self.get_strategies_count() return context def get_data(self): strategies = [] search_opts = self.get_filters() try: strategies = watcher.Strategy.list(self.request, **search_opts) except Exception as exc: LOG.exception(exc) horizon.exceptions.handle( self.request, _("Unable to retrieve strategy information.") ) return strategies def get_strategies_count(self): return len(self.get_data()) def get_filters(self): filters = {} filter_action = self.table._meta._filter_action if filter_action: filter_field = self.table.get_filter_field() if filter_action.is_api_filter(filter_field): filter_string = self.table.get_filter_string() if filter_field and filter_string: filters[filter_field] = filter_string return filters class DetailView(horizon.tabs.TabbedTableView): tab_group_class = wtabs.StrategyDetailTabs template_name = 'infra_optim/strategies/details.html' redirect_url = 'horizon:admin:strategies:index' page_title = _("Strategy Details: {{ strategy.name }}") @memoized.memoized_method def _get_data(self): strategy_uuid = None try: strategy_uuid = self.kwargs['strategy_uuid'] strategy = watcher.Strategy.get(self.request, strategy_uuid) except Exception as exc: LOG.exception(exc) msg = ( _('Unable to retrieve details for strategy "%s".') % strategy_uuid ) horizon.exceptions.handle( self.request, msg, redirect=self.redirect_url ) return strategy def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) strategy = self._get_data() context["strategy"] = strategy # Render parameters_spec in a readable format for the template context["strategy_parameters_spec_pretty"] = ( self._render_pretty_parameters_spec( getattr(strategy, 'parameters_spec', None) ) ) return context def get_tabs(self, request, *args, **kwargs): strategy = self._get_data() return self.tab_group_class(request, strategy=strategy, **kwargs) def _render_pretty_parameters_spec(self, spec): """Return a human-friendly rendering of parameters_spec. Rules: - If spec is a JSON string, parse it first. - If spec is a JSON schema, prefer the 'properties' mapping. - If result is a dict/list, pretty-print as indented YAML in a
.
        - If result is a scalar (str/int/bool), return it as-is.
        - On any error or empty value, return None.
        """
        try:
            obj = spec
            if not obj:
                return None

            # Parse JSON-encoded strings if needed
            if isinstance(obj, str):
                try:
                    obj = json.loads(obj)
                except Exception:
                    # Keep raw string if not JSON
                    return obj

            # If JSON schema-like, extract properties for readability
            if (
                isinstance(obj, dict)
                and 'properties' in obj
                and isinstance(obj.get('properties'), dict)
            ):
                obj = obj['properties']

            # Pretty print dict/list as YAML-like preformatted block
            if isinstance(obj, dict | list):
                try:
                    import yaml  # Lazy import

                    dumped = yaml.safe_dump(
                        obj, default_flow_style=False, sort_keys=False
                    )
                except Exception:
                    dumped = json.dumps(obj, indent=2, ensure_ascii=False)
                return mark_safe(  # noqa: S308  # nosec B703,B308
                    f'
{dumped}
' ) # Scalars return obj except Exception: return None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9398196 watcher_dashboard-16.0.0/watcher_dashboard/local/0000775000175000017500000000000015257165054020634 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8288481 watcher_dashboard-16.0.0/watcher_dashboard/local/__init__.py0000664000175000017500000000000015257165015022730 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9408197 watcher_dashboard-16.0.0/watcher_dashboard/local/enabled/0000775000175000017500000000000015257165054022226 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/local/enabled/_31000_goals_panel.py0000664000175000017500000000162515257165015025747 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # The slug of the panel to be added to HORIZON_CONFIG. Required. PANEL = 'goals' # The slug of the dashboard the PANEL associated with. Required. PANEL_DASHBOARD = 'admin' # The slug of the panel group the PANEL is associated with. PANEL_GROUP = 'watcher' # Python panel class of the PANEL to be added. ADD_PANEL = 'watcher_dashboard.content.goals.panel.Goals' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/local/enabled/_31010_strategies_panel.py0000664000175000017500000000164415257165015027016 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # The slug of the panel to be added to HORIZON_CONFIG. Required. PANEL = 'strategies' # The slug of the dashboard the PANEL associated with. Required. PANEL_DASHBOARD = 'admin' # The slug of the panel group the PANEL is associated with. PANEL_GROUP = 'watcher' # Python panel class of the PANEL to be added. ADD_PANEL = 'watcher_dashboard.content.strategies.panel.Strategies' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/local/enabled/_31020_watcher_panelgroup.py0000664000175000017500000000172015257165015027352 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from django.utils.translation import gettext_lazy as _ # The slug of the panel group to be added to HORIZON_CONFIG. Required. PANEL_GROUP = 'watcher' # The display name of the PANEL_GROUP. Required. PANEL_GROUP_NAME = _('Optimization') # The slug of the dashboard the PANEL_GROUP associated with. Required. PANEL_GROUP_DASHBOARD = 'admin' ADD_INSTALLED_APPS = ['watcher_dashboard'] AUTO_DISCOVER_STATIC_FILES = False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/local/enabled/_31030_audit_templates_panel.py0000664000175000017500000000166215257165015030032 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # The slug of the panel to be added to HORIZON_CONFIG. Required. PANEL = 'audit_templates' # The slug of the dashboard the PANEL associated with. Required. PANEL_DASHBOARD = 'admin' # The slug of the panel group the PANEL is associated with. PANEL_GROUP = 'watcher' # Python panel class of the PANEL to be added. ADD_PANEL = 'watcher_dashboard.content.audit_templates.panel.AuditTemplates' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/local/enabled/_31040_audits_panel.py0000664000175000017500000000163015257165015026133 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # The slug of the panel to be added to HORIZON_CONFIG. Required. PANEL = 'audits' # The slug of the dashboard the PANEL associated with. Required. PANEL_DASHBOARD = 'admin' # The slug of the panel group the PANEL is associated with. PANEL_GROUP = 'watcher' # Python panel class of the PANEL to be added. ADD_PANEL = 'watcher_dashboard.content.audits.panel.Audits' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/local/enabled/_31050_action_plans_panel.py0000664000175000017500000000165115257165015027320 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # The slug of the panel to be added to HORIZON_CONFIG. Required. PANEL = 'action_plans' # The slug of the dashboard the PANEL associated with. Required. PANEL_DASHBOARD = 'admin' # The slug of the panel group the PANEL is associated with. PANEL_GROUP = 'watcher' # Python panel class of the PANEL to be added. ADD_PANEL = 'watcher_dashboard.content.action_plans.panel.ActionPlans' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/local/enabled/_31060_actions_panel.py0000664000175000017500000000163315257165015026307 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # The slug of the panel to be added to HORIZON_CONFIG. Required. PANEL = 'actions' # The slug of the dashboard the PANEL associated with. Required. PANEL_DASHBOARD = 'admin' # The slug of the panel group the PANEL is associated with. PANEL_GROUP = 'watcher' # Python panel class of the PANEL to be added. ADD_PANEL = 'watcher_dashboard.content.actions.panel.Actions' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/local/enabled/__init__.py0000664000175000017500000000000015257165015024322 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9408197 watcher_dashboard-16.0.0/watcher_dashboard/locale/0000775000175000017500000000000015257165054021001 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/locale/.placeholder0000664000175000017500000000000115257165015023250 0ustar00zuulzuul ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9288197 watcher_dashboard-16.0.0/watcher_dashboard/static/0000775000175000017500000000000015257165054021031 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9288197 watcher_dashboard-16.0.0/watcher_dashboard/static/infra_optim/0000775000175000017500000000000015257165054023340 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9408197 watcher_dashboard-16.0.0/watcher_dashboard/static/infra_optim/images/0000775000175000017500000000000015257165054024605 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/static/infra_optim/images/chevron.png0000664000175000017500000000053415257165015026756 0ustar00zuulzuul‰PNG  IHDRH-ÑbKGDÿÿÿ ½§“ pHYs  šœtIMEÞ(vÿ+MiTXtCommentCreated with GIMPd.eÀIDAT(ÏÕÒ;‚@à9»£ Ñ­ÕÞ•1þoˆ6ˆ� ±Q üD"caŒ$ž �qÚݯ˜ì ’D…ÔP1?„á1Â-˾.’Äþ"Ïó7LÓÁf Ç�j1Ixó6Û¢Óù MÓ„=èá~Ïá¸3d%Lž¿„Š´šº�6@”Ï¡T Ï_BJ‰ÉxÃ�O¤b´šìaBˆOXƆ”¨7êH’ëÒÂ2&©E¯Ú\TÌÕ:`QÚ¹øŸ—{Yv�' èHïIEND®B`‚././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/static/infra_optim/images/power.png0000664000175000017500000000113615257165015026445 0ustar00zuulzuul‰PNG  IHDRàw=øbKGDÿÿÿ ½§“ pHYs  šœtIMEÞ 30cUiTXtCommentCreated with GIMPd.eÂIDATHǵ–ÛjQ†¿½g5™LHÈ•¶`L,H/Š1Ö¤ÚñAEÖZ”RéX V×h=~B·ÛEDÒ6°„aÈ�Ÿm®ß¸É‹—ëôú}´ž¼£2‚42™ Ÿ>oóm÷;§Ë§Xn6‘±©+»RŠ0 Ùþò•��]šÍ‹T+2™™‘ôéIÙ¶XÖ_mpçÞ}Þøˆã8éü�È÷}6^¿áÖí»´ÛŒ1±Ä%šÇqø½·ÇZë«[t:¿™$4¥ÖZ‚ Äø‡ $‰¸17—ce¹ÁìÜ31tÉ´{Æu³ÌÏŸa©^?HG”L*EõZ�……³xž‡1濨ʸÂ"Âìɬ\¾„çyDQ4Ö°�4Ã�jµÂ…ó5J¥ÒAŠD«B)…ÖšB¡@c©N¹\&‚©nÆ‘ ®]½B>ŸGD‚`jÒŽ4(‹ÿ\¦T×uš%û=Oz¹âÞ@Ã!›[[Çòm ‡üܽ²‰}ƦIEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9408197 watcher_dashboard-16.0.0/watcher_dashboard/static/infra_optim/scss/0000775000175000017500000000000015257165054024313 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/static/infra_optim/scss/infra_optim.scss0000664000175000017500000000020315257165015027507 0ustar00zuulzuul// /* Additional CSS for infra_optim. */ // @import "/dashboard/scss/variables"; // @import "/bootstrap/scss/bootstrap/variables"; ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9408197 watcher_dashboard-16.0.0/watcher_dashboard/static/infra_optim/tests/0000775000175000017500000000000015257165054024502 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/static/infra_optim/tests/formset_table.js0000664000175000017500000000452215257165015027666 0ustar00zuulzuulhorizon.addInitFunction(function () { module("Formset table (watcher.formset_table.js)"); test("Reenumerate rows", function () { var html = $('#qunit-fixture'); var table = html.find('table'); var input = table.find('tbody tr#flavors__row__14 input').first(); input.attr('id', 'id_flavors-3-name'); watcher.formset_table.reenumerate_rows(table, 'flavors'); equal(input.attr('id'), 'id_flavors-0-name', "Enumerate old rows ids"); input.attr('id', 'id_flavors-__prefix__-name'); watcher.formset_table.reenumerate_rows(table, 'flavors'); equal(input.attr('id'), 'id_flavors-0-name', "Enumerate new rows ids"); }); test("Delete row", function () { var html = $('#qunit-fixture'); var table = html.find('table'); var row = table.find('tbody tr').first(); var input = row.find('input#id_flavors-0-DELETE'); equal(row.css("display"), 'table-row'); equal(input.attr('checked'), undefined); watcher.formset_table.replace_delete(row); var x = input.next('a'); watcher.formset_table.delete_row.call(x); equal(row.css("display"), 'none'); equal(input.attr('checked'), 'checked'); }); test("Add row", function() { var html = $('#qunit-fixture'); var table = html.find('table'); var empty_row_html = ''; equal(table.find('tbody tr').length, 3); equal(html.find('#id_flavors-TOTAL_FORMS').val(), 3); watcher.formset_table.add_row(table, 'flavors', empty_row_html); equal(table.find('tbody tr').length, 4); equal(table.find('tbody tr:last input').attr('id'), 'id_flavors-3-name'); equal(html.find('#id_flavors-TOTAL_FORMS').val(), 4); }); test("Init formset table", function() { var html = $('#qunit-fixture'); var table = html.find('table'); watcher.formset_table.init('flavors', '', 'Add row'); equal(table.find('tfoot tr a').html(), 'Add row'); }); test("Init formset table -- no add", function() { var html = $('#qunit-fixture'); var table = html.find('table'); watcher.formset_table.init('flavors', '', ''); equal(table.find('tfoot tr a').length, 0); }); }); ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9288197 watcher_dashboard-16.0.0/watcher_dashboard/templates/0000775000175000017500000000000015257165054021540 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9418197 watcher_dashboard-16.0.0/watcher_dashboard/templates/client_side/0000775000175000017500000000000015257165054024022 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/templates/client_side/_modal_chart.html0000664000175000017500000000116615257165015027325 0ustar00zuulzuul{% extends "horizon/client_side/template.html" %} {% load horizon %} {% block id %}modal_chart_template{% endblock %} {% block template %} {% jstemplate %} {% endjstemplate %} {% endblock %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/templates/client_side/templates.html0000664000175000017500000000005615257165015026704 0ustar00zuulzuul{% include "client_side/_modal_chart.html" %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9418197 watcher_dashboard-16.0.0/watcher_dashboard/templates/formset_table/0000775000175000017500000000000015257165054024366 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/templates/formset_table/_row.html0000664000175000017500000000131215257165015026214 0ustar00zuulzuul {% for cell in row %} {% if cell.field %} {{ cell.field }} {% else %} {%if cell.wrap_list %}
    {% endif %}{{ cell.value }}{%if cell.wrap_list %}
{% endif %} {% endif %} {% if forloop.first %} {% for field in row.form.hidden_fields %} {{ field }} {% for error in field.errors %} {{ field.name }}: {{ error }} {% endfor %} {% endfor %} {% if row.form.non_field_errors %}
{{ row.form.non_field_errors }}
{% endif %} {% endif %} {% endfor %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/templates/formset_table/_table.html0000664000175000017500000000257415257165015026507 0ustar00zuulzuul{% extends 'horizon/common/_data_table.html' %} {% load i18n %} {% block table_columns %} {% if not table.is_browser_table %} {% for column in columns %} {{ column }} {% endfor %} {% endif %} {% endblock table_columns %} {% block table %} {% with table.get_formset as formset %} {{ formset.management_form }} {% if formset.non_field_errors %}
{{ formset.non_field_errors }}
{% endif %} {% endwith %} {{ block.super }} {% endblock table %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.829848 watcher_dashboard-16.0.0/watcher_dashboard/templates/formset_table/menu_formset.html0000664000175000017500000000302715257165015027756 0ustar00zuulzuul{% load i18n %} {{ formset.management_form }} {% for error in formset.non_form_errors %}
{{ error }}
{% endfor %}

Nodes to register

{% include 'infra_optim/nodes/_upload.html' with form=upload_form %}
{% for form in formset %} {% include form_template with form=form active=forloop.first %} {% endfor %}
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9288197 watcher_dashboard-16.0.0/watcher_dashboard/templates/horizon/0000775000175000017500000000000015257165054023230 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9418197 watcher_dashboard-16.0.0/watcher_dashboard/templates/horizon/common/0000775000175000017500000000000015257165054024520 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000021500000000000010213 xustar00114 path=watcher_dashboard-16.0.0/watcher_dashboard/templates/horizon/common/_items_count_domain_page_header.html 27 mtime=1790765580.830848 watcher_dashboard-16.0.0/watcher_dashboard/templates/horizon/common/_items_count_domain_page_header.0000664000175000017500000000066115257165015033044 0ustar00zuulzuul{% load i18n %} {% block page_header %} {% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9428196 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/0000775000175000017500000000000015257165054024047 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.830848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/_fullscreen_workflow.html0000664000175000017500000000317015257165015031166 0ustar00zuulzuul{% load i18n %} {% with workflow.get_entry_point as entry_point %}
{% csrf_token %} {% if REDIRECT_URL %}{% endif %}
{% block workflow-buttons %} {% endblock %}
{% block workflow-body %}
{% for step in workflow.steps %}
{{ step.render }}
{% if not forloop.last %} {% endif %} {% endfor %}
{% endblock %}
{% endwith %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.830848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/_fullscreen_workflow_base.html0000664000175000017500000000051015257165015032153 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans workflow.name %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=workflow.name %} {% endblock page_header %} {% block main %} {% include 'infra_optim/_fullscreen_workflow.html' %} {% endblock %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.830848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/_modal_form_xl.html0000664000175000017500000000374515257165015027724 0ustar00zuulzuul{# Vendored from horizon/common/_modal_form.html with line 1 changed to extend _modal_xl.html #} {% extends "infra_optim/_modal_xl.html" %} {% block content %} {% if table %} {% endif %}
{% csrf_token %}
{% endblock %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.830848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/_modal_xl.html0000664000175000017500000000213115257165015026665 0ustar00zuulzuul{# Vendored from horizon/common/_modal.html with modal-dialog-class block added on line 6 #} {% block modal-js %} {% endblock %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.830848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/_performance_chart.html0000664000175000017500000000077615257165015030565 0ustar00zuulzuul

{{ label }}

././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.830848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/_performance_chart_box.html0000664000175000017500000000514415257165015031427 0ustar00zuulzuul{% load i18n %} {% if meter_conf %}
{% trans "From" %}
{% trans "To" %}
{% for meter_label, url_part, y_max in meter_conf %}
{% include "infra_optim/_performance_chart.html" with label=meter_label y_max=y_max url=node_perf_url|add:"?"|add:url_part only %}
{% endfor %}
{% else %}

{% trans 'Metering service is not enabled.' %}

{% endif %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.830848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/_top_5_box.html0000664000175000017500000000074415257165015026774 0ustar00zuulzuul
{% include "infra_optim/_top_5_chart.html" with top_5=top_5.fan%}
{% include "infra_optim/_top_5_chart.html" with top_5=top_5.voltage %}
{% include "infra_optim/_top_5_chart.html" with top_5=top_5.temperature%}
{% include "infra_optim/_top_5_chart.html" with top_5=top_5.current %}
././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.830848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/_top_5_chart.html0000664000175000017500000000107615257165015027304 0ustar00zuulzuul{% load i18n %} {% load chart_helpers %}

{% trans 'Top 5 Nodes' %} ({{ top_5.label }}):

{% if top_5.data %} {% for d in top_5.data %} {% endfor %}
{{ d.node_uuid|truncatechars:6 }} {{ d.value}} {{ top_5.unit }} {%if d.direction %} {% endif %}
{% else %} {% trans 'No data available.' %} {% endif %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.830848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/_workflow_base.html0000664000175000017500000000050015257165015027730 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans workflow.name %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=workflow.name %} {% endblock page_header %} {% block main %} {% include 'horizon/common/_workflow.html' %} {% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9428196 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/action_plans/0000775000175000017500000000000015257165054026521 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/action_plans/_details_overview.html0000664000175000017500000000165115257165015033121 0ustar00zuulzuul{% load i18n sizeformat %}

{% trans "Action Plan Overview" %}

{% trans "ID" %}
{{ action_plan.uuid|default:_("-") }}
{% url 'horizon:admin:audits:detail' action_plan.audit_uuid as audit_url %}
{% trans "Audit ID" %}
{{ action_plan.audit_uuid|default:_("-") }}
{% trans "State" %}
{{ action_plan.state|default:_("-") }}
{% if action_plan.status_message %}
{% trans "Status Message" %}
{{ action_plan.status_message }}
{% endif %}
{% trans "Created At" %}
{{ action_plan.created_at|parse_isotime|default:_("-") }}
{% trans "Update At" %}
{{ action_plan.updated_at|parse_isotime|default:_("-") }}
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/action_plans/_global_efficacy.html0000664000175000017500000000023715257165015032636 0ustar00zuulzuul{% load i18n %}
    {% for name, value in global_indicators.items %}
  • {{ name }} {{ value }}
  • {% endfor %}
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/action_plans/create.html0000664000175000017500000000051615257165015030651 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans "Create Flavor" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Create Action Plan") %} {% endblock page_header %} {% block main %} {% include 'horizon/common/_workflow.html' %} {% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/action_plans/details.html0000664000175000017500000000070415257165015031032 0ustar00zuulzuul{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Action Plan Details"%}{% endblock %} {% block main %}
{% include "infra_optim/action_plans/_details_overview.html" %}
{{ related_efficacy_indicators_table.render }}
{{ related_wactions_table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/action_plans/index.html0000664000175000017500000000061015257165015030510 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Action Plans' %}{% endblock %} {% block page_header %} {% include 'horizon/common/_items_count_domain_page_header.html' with title=_('Action Plans') items_count=action_plans_count %} {% endblock page_header %} {% block main %}
{{ action_plans_table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9438198 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/actions/0000775000175000017500000000000015257165054025507 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/actions/_skip.html0000664000175000017500000000061215257165015027476 0ustar00zuulzuul{% extends "infra_optim/_modal_form_xl.html" %} {% load i18n %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Skip this action in the action plan." %}

{% trans "Skipped actions will not be executed. You may optionally provide a reason (max 200 characters). You can also update the reason for actions already in SKIPPED state." %}

{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/actions/details.html0000664000175000017500000000276315257165015030027 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Actions: ' %}{{ action.uuid }}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Actions: ')|add:action.uuid %} {% endblock page_header %} {% block main %}

{% trans "Action Info" %}

{% trans "UUID" %}
{{ action.uuid|default:_("-") }}
{% trans "Type" %}
{{ action.action_type|default:_("-") }}
{% url 'horizon:admin:action_plans:detail' action.action_plan_uuid as action_plan_url %}
{% trans "Action Plan" %}
{{ action.action_plan_uuid|default:_("-") }}
{% trans "State" %}
{{ action.state|default:_("-") }}
{% if action.status_message %}
{% if action.state == "SKIPPED" %}{% trans "Skip Reason" %}{% else %}{% trans "Status Message" %}{% endif %}
{{ action.status_message }}
{% endif %}
{% trans "Created At" %}
{{ action.created_at|parse_isotime|default:_("-") }}
{% trans "Update At" %}
{{ action.updated_at|parse_isotime|default:_("-") }}
{{ parameters_table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/actions/index.html0000664000175000017500000000056115257165015027503 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Actions' %}{% endblock %} {% block page_header %} {% include 'horizon/common/_items_count_domain_page_header.html' with title=_('Actions') items_count=actions_count %} {% endblock page_header %} {% block main %}
{{ wactions_table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/actions/skip.html0000664000175000017500000000050215257165015027335 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans "Skip Action" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Skip Action") %} {% endblock page_header %} {% block main %} {% include "infra_optim/actions/_skip.html" %} {% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9438198 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/actions_history/0000775000175000017500000000000015257165054027270 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/actions_history/details.html0000664000175000017500000000165615257165015031610 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Audits: ' %}{{ audits.name }}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Audits: ')|add:audits.name %} {% endblock page_header %} {% block main %}

{% trans "Hardware Info" %}

{% trans "Severity" %}
{{ audits.cpu_arch|default:_("-") }}
{% trans "CPUs" %}
{{ audits.vcpus|default:_("-") }}
{% trans "Memory" %}
{{ audits.ram_bytes|filesizeformat|default:_("-") }}
{% trans "Disk" %}
{{ audits.disk_bytes|filesizeformat|default:_("-") }}
{{ table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/actions_history/index.html0000664000175000017500000000164315257165015031266 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Audits' %}{% endblock %} {% block page_header %} {% include 'horizon/common/_items_count_domain_page_header.html' with title=_('Audits') items_count=flavors_count %} {% endblock page_header %} {% block main %} {% if suggested_flavors_count %}
{{ suggested_flavors_count }}× Suggested Flavor
{{ suggested_flavors_table.render }}
{% endif %}
{{ audits_table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9438198 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/audit_templates/0000775000175000017500000000000015257165054027233 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/audit_templates/_create.html0000664000175000017500000000726415257165015031531 0ustar00zuulzuul{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Creates an audit template with specified parameters." %}

{% trans "Define the optimization goal to achieve, among those which are available." %} {% trans "Optionally, you can select the strategy used to achieve your goal. If not set, a strategy will be automatically selected among those which can be used for your goal" %}

{% trans "Audit scope:" %}

{% trans "Creates an audit template with specified parameters." %}

{% trans "YAML example:" %} --- - compute: - host_aggregates: - id: 1 - id: 2 - id: 3 - availability_zones: - name: AZ1 - name: AZ2 - exclude: - instances: - uuid: UUID1 - uuid: UUID2 - compute_nodes: - name: compute1

{% trans "JSON example:" %} [{'compute': [{'host_aggregates': [ {'id': 1}, {'id': 2}, {'id': 3}]}, {'availability_zones': [ {'name': 'AZ1'}, {'name': 'AZ2'}]}, {'exclude': [ {'instances': [ {'uuid': 'UUID1'}, {'uuid': 'UUID2'} ]}, {'compute_nodes': [ {'name': 'compute1'} ]} ]}] }]

{% endblock %} {% block modal-footer %} {{ block.super }} {% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/audit_templates/create.html0000664000175000017500000000030515257165015031357 0ustar00zuulzuul{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Create Template" %}{% endblock %} {% block main %} {% include 'infra_optim/audit_templates/_create.html' %} {% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/audit_templates/details.html0000664000175000017500000000263615257165015031552 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Audit Templates: ' %}{{ audit_template.name }}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Audit Templates: ')|add:audit_template.name %} {% endblock page_header %} {% block main %}

{% trans "Audit Template Info" %}

{% trans "Name" %}
{{ audit_template.name|default:_("-") }}
{% trans "UUID" %}
{{ audit_template.uuid|default:_("-") }}
{% trans "Goal" %}
{{ audit_template.goal_name|default:_("-") }}
{% trans "Strategy" %}
{{ audit_template.strategy_name|default:_("-") }}
{% trans "Audit Scope" %}
{{ audit_template.scope|linebreaks }}
{% trans "Created At" %}
{{ audit_template.created_at|parse_isotime|default:_("-") }}
{% trans "Update At" %}
{{ audit_template.updated_at|parse_isotime|default:_("-") }}
{% trans "Deleted At" %}
{{ audit_template.deleted_at|parse_isotime|default:_("-") }}
{{ table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/audit_templates/index.html0000664000175000017500000000060415257165015031225 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Audit Templates' %}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Audit Templates') items_count=audit_templates_count %} {% endblock page_header %} {% block main %}
{{ audit_templates_table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9438198 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/audits/0000775000175000017500000000000015257165054025340 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/audits/_create.html0000664000175000017500000001344015257165015027627 0ustar00zuulzuul{% extends "infra_optim/_modal_form_xl.html" %} {% load i18n %} {% block modal-dialog-class %}modal-xl{% endblock %} {% block modal-body-right %}

{% trans "Description:" %}

{% trans "Creates a audit with specified parameters." %}

{% trans "If you check 'Auto Trigger' option, the action plan, recommended by the audit, will be automatically started." %}

{% trans "Strategy Parameters (JSON):" %}

{% trans "Strategy parameters control how the optimization strategy behaves during audit execution." %}

{% trans "Parameter format:" %}
{% trans "Enter a JSON object with parameter names as keys." %}

{% trans "Example:" %}

{
  "memory_threshold": 0.8,
  "cpu_threshold": 0.9,
  "enable_migration": true,
  "compute_nodes": [
    {"src_node": "compute1", "dst_node": "compute2"}
  ]
}
  
{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/audits/create.html0000664000175000017500000000027015257165015027465 0ustar00zuulzuul{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Create Audit" %}{% endblock %} {% block main %} {% include 'infra_optim/audits/_create.html' %} {% endblock %}././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/audits/details.html0000664000175000017500000000350215257165015027650 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Audits: ' %}{{ audit.uuid }}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Audits: ')|add:audit.uuid %} {% endblock page_header %} {% block main %}

{% trans "Audit Info" %}

{% trans "UUID" %}
{{ audit.uuid|default:_("-") }}
{% trans "Goal" %}
{{ audit.goal_name|default:_("-") }}
{% trans "Strategy" %}
{{ audit.strategy_name|default:_("-") }}
{% trans "Type" %}
{{ audit.audit_type|default:_("-") }}
{% if audit_parameters_pretty %}
{% trans "Parameters" %}
{{ audit_parameters_pretty|safe }}
{% endif %}
{% trans "Auto Trigger" %}
{{ audit.auto_trigger }}
{% if audit.start_time or audit.end_time %}
{% trans "Start Time" %}
{{ audit.start_time|parse_isotime|default:_("-") }}
{% trans "End Time" %}
{{ audit.end_time|parse_isotime|default:_("-") }}
{% endif %} {% url 'horizon:admin:audit_templates:detail' audit.audit_template_uuid as audit_template_url %}
{% trans "State" %}
{{ audit.state|default:_("-") }}
{% trans "Created At" %}
{{ audit.created_at|parse_isotime|default:_("-") }}
{% trans "Update At" %}
{{ audit.updated_at|parse_isotime|default:_("-") }}
{{ related_action_plans_table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765580.8318481 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/audits/index.html0000664000175000017500000000052715257165015027336 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Audits' %}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Audits') items_count=audits_count %} {% endblock page_header %} {% block main %}
{{ audits_table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.832848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/base.html0000664000175000017500000000060115257165015025641 0ustar00zuulzuul{% extends 'base.html' %} {% block css %} {{block.super}} {% load compress %} {% compress css %} {% endcompress %} {% endblock %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.832848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/base_detail.html0000664000175000017500000000075015257165015027170 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% block main %}
{% block breadcrumbs %}{% endblock %}
{% block actions %}{% endblock %}

{% block name %}{% endblock %}

{% block overall_usage %}{% endblock %}
{{ tab_group.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9448197 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/goals/0000775000175000017500000000000015257165054025154 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.832848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/goals/details.html0000664000175000017500000000204415257165015027464 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Goals: ' %}{{ goal.display_name }}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Goals: ')|add:goal.display_name %} {% endblock page_header %} {% block main %}

{% trans "Goal Info" %}

{% trans "UUID" %}
{{ goal.uuid|default:_("-") }}
{% trans "Name" %}
{{ goal.display_name|default:_("-") }}
{% trans "Created At" %}
{{ goal.created_at|parse_isotime|default:_("-") }}
{% trans "Update At" %}
{{ goal.updated_at|parse_isotime|default:_("-") }}
{{ efficacy_specification_table.render }}
{{ related_strategies_table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.832848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/goals/index.html0000664000175000017500000000052115257165015027144 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Goals' %}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Goals') items_count=goal_count %} {% endblock page_header %} {% block main %}
{{ goals_table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.832848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/header_actions.html0000664000175000017500000000061215257165015027701 0ustar00zuulzuul ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9448197 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/logs/0000775000175000017500000000000015257165054025013 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.832848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/logs/index.html0000664000175000017500000000060715257165015027010 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Logs' %}{% endblock %} {% block page_header %} {% include 'horizon/common/_items_count_domain_page_header.html' with title=_('Logs') items_count=flavors_count %} {% endblock page_header %} {% block main %}
{{ table.render }}
{% endblock %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.832848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/qunit.html0000664000175000017500000001570615257165015026103 0ustar00zuulzuul Watcher QUnit Test Suite {% include "horizon/_conf.html" %} {% comment %}Load test modules here.{% endcomment %} {% comment %}End test modules.{% endcomment %} {% include "horizon/_scripts.html" %} {% include "infra_optim/_scripts.html" %}

Watcher JavaScript Tests

    Flavors

    Flavor Name VCPU RAM (MB) Root Disk (GB) Ephemeral Disk (GB) Swap Disk (MB) Max. VMs Delete
    -
    -
    -
    Displaying 3 items
    ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9448197 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/strategies/0000775000175000017500000000000015257165054026221 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.832848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/strategies/details.html0000664000175000017500000000270515257165015030535 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Strategies: ' %}{{ strategy.display_name }}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Strategies: ')|add:strategy.display_name %} {% endblock page_header %} {% block main %}

    {% trans "Strategy Info" %}

    {% trans "UUID" %}
    {{ strategy.uuid|default:_("-") }}
    {% trans "Name" %}
    {{ strategy.display_name|default:_("-") }}
    {% trans "Goal" %}
    {% url 'horizon:admin:goals:detail' strategy.goal_name as goal_url %}
    {{ strategy.goal_name|default:_("-") }}
    {% trans "Created At" %}
    {{ strategy.created_at|parse_isotime|default:_("-") }}
    {% trans "Update At" %}
    {{ strategy.updated_at|parse_isotime|default:_("-") }}
    {% trans "Parameters Spec" %}
    {% if strategy_parameters_spec_pretty %} {{ strategy_parameters_spec_pretty|safe }} {% else %} {{ _("-") }} {% endif %}
    {{ table.render }}
    {% endblock %} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.832848 watcher_dashboard-16.0.0/watcher_dashboard/templates/infra_optim/strategies/index.html0000664000175000017500000000054415257165015030216 0ustar00zuulzuul{% extends 'infra_optim/base.html' %} {% load i18n %} {% block title %}{% trans 'Strategies' %}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Strategies') items_count=strategy_count %} {% endblock page_header %} {% block main %}
    {{ strategies_table.render }}
    {% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9448197 watcher_dashboard-16.0.0/watcher_dashboard/templatetags/0000775000175000017500000000000015257165054022234 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.832848 watcher_dashboard-16.0.0/watcher_dashboard/templatetags/__init__.py0000664000175000017500000000117515257165015024346 0ustar00zuulzuul# Copyright (c) 2018 Ultimum Technologies s.r.o # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from horizon.utils import filters # noqa ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9298196 watcher_dashboard-16.0.0/watcher_dashboard/test/0000775000175000017500000000000015257165054020521 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9448197 watcher_dashboard-16.0.0/watcher_dashboard/test/integration/0000775000175000017500000000000015257165054023044 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.832848 watcher_dashboard-16.0.0/watcher_dashboard/test/integration/__init__.py0000664000175000017500000000000015257165015025140 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.850848 watcher_dashboard-16.0.0/watcher_dashboard/test/integration/playwright_base.py0000664000175000017500000005414715257165015026612 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Base test class for Playwright integration tests.""" import os import re import time import uuid from urllib.parse import urljoin from urllib.parse import urlparse import fixtures from keystoneauth1 import identity from keystoneauth1 import session as keystone_session from oslo_log import log from oslotest import base from playwright import sync_api from watcherclient import client as watcher_client from watcherclient.common.apiclient import exceptions as watcher_exceptions from watcher_dashboard.test.integration import playwright_config as config LOG = log.getLogger(__name__) class PlaywrightFixture(fixtures.Fixture): """Fixture to manage Playwright browser lifecycle. This handles starting Playwright, launching the browser, creating the context (with video/tracing configuration), and creating the page. """ def __init__(self, use_auth_reuse=False, test_name=None): super().__init__() self.use_auth_reuse = use_auth_reuse self.test_name = test_name self.playwright = None self.browser = None self.context = None self.page = None def _setUp(self): # 1. Start Playwright self.playwright = sync_api.sync_playwright().start() self.addCleanup(self.playwright.stop) # 2. Launch Browser browser_type = config.get_browser() launchers = { 'chromium': self.playwright.chromium, 'firefox': self.playwright.firefox, 'webkit': self.playwright.webkit, } launcher = launchers[browser_type] self.browser = launcher.launch(headless=config.is_headless()) self.addCleanup(self.browser.close) # 3. Configure Context (Video, Viewport, Auth) video_dir = self._get_video_dir() context_kwargs = { 'viewport': {'width': 1920, 'height': 1080}, 'ignore_https_errors': config.ignore_https_errors(), } if video_dir: context_kwargs.update( { 'record_video_dir': video_dir, 'record_video_size': {'width': 1920, 'height': 1080}, } ) # Handle Auth State Reuse auth_path = config.get_auth_state_path() if self.use_auth_reuse and os.path.exists(auth_path): context_kwargs['storage_state'] = auth_path LOG.info("Reusing auth state from: %s", auth_path) self.context = self.browser.new_context(**context_kwargs) self.context.set_default_timeout(config.get_timeout()) self.addCleanup(self._close_context_and_finalize_artifacts) # 4. Start Tracing if enabled if config.get_trace_mode() != 'off': self.context.tracing.start( screenshots=True, snapshots=True, sources=True, name=self.test_name, ) # 5. Create Page self.page = self.context.new_page() def _get_video_dir(self): """Return the video recording directory, or None if disabled.""" if config.get_video_mode() == 'off': return None video_dir = config.get_video_dir() os.makedirs(video_dir, exist_ok=True) return video_dir def _close_context_and_finalize_artifacts(self): """Close context and save artifacts with human-readable names. All artifacts (traces, videos) are kept unconditionally because fixtures don't easily know the test's pass/fail status. To delete on success, hook into testtools.TestCase.addOnException. """ trace_mode = config.get_trace_mode() if trace_mode != 'off': trace_dir = config.get_trace_dir() os.makedirs(trace_dir, exist_ok=True) safe_name = re.sub( r'[^A-Za-z0-9_.-]+', '_', self.test_name or str(id(self)) ) trace_path = os.path.join(trace_dir, f"trace_{safe_name}.zip") try: self.context.tracing.stop(path=trace_path) except (OSError, AttributeError) as e: LOG.warning("Failed to save trace: %s", e) self.context.close() if self.page.video: safe_name = re.sub( r'[^A-Za-z0-9_.-]+', '_', self.test_name or str(id(self)) ) video_dir = config.get_video_dir() video_path = os.path.join(video_dir, f"video_{safe_name}.webm") self.page.video.save_as(video_path) self.page.video.delete() LOG.info("Video saved to: %s", video_path) class WatcherClientFixture(fixtures.Fixture): """Session-scoped fixture for Watcher API client. Provides a python-watcherclient instance authenticated via keystoneauth1. Used for fast, reliable resource cleanup via Watcher API. """ def __init__(self): super().__init__() self.client = None def _setUp(self): auth = identity.Password( auth_url=config.get_auth_url(), username=config.get_username(), password=config.get_password(), project_name=config.get_project_name(), user_domain_name=config.get_user_domain_name(), project_domain_name=config.get_project_domain_name(), ) session = keystone_session.Session( auth=auth, verify=not config.is_insecure() ) self.client = watcher_client.Client('1', session=session) LOG.info("Watcher API client initialized for test cleanup") class PlaywrightTestCase(base.BaseTestCase): """Base test case for Watcher Dashboard Integration tests. Uses oslotest and fixtures for standard resource management. """ # Convenience alias for Playwright expect assertions expect = sync_api.expect # Subclasses can set this to False to skip the automatic login in setUp(). auto_login = True @property def watcher_client(self): """Get the Watcher API client for cleanup operations. :returns: Watcher API client instance """ return self._watcher_client_fixture.client def setUp(self): """Set up test fixtures.""" super().setUp() self.dashboard_url = config.get_dashboard_url() self.screenshot_dir = config.get_screenshot_dir() # Initialize Watcher API client at test level self._watcher_client_fixture = self.useFixture(WatcherClientFixture()) # Initialize Playwright via Fixture self.pw_fixture = self.useFixture( PlaywrightFixture( use_auth_reuse=config.is_auth_reuse_enabled(), test_name=self.id(), ) ) self.page = self.pw_fixture.page # Ensure screenshot dir exists if config.is_screenshot_enabled(): os.makedirs(self.screenshot_dir, exist_ok=True) self._screenshot_counter = 0 self._test_screenshot_dir = self._get_test_screenshot_dir() if self._test_screenshot_dir: os.makedirs(self._test_screenshot_dir, exist_ok=True) if self.auto_login: self.login() def login(self): """Log in to the Dashboard. Uses auth reuse if configured.""" self.page.goto(self.dashboard_url) self.take_screenshot("dashboard_loaded") if '/auth/login' not in self.page.url: LOG.info("Already authenticated, skipping login form.") self.take_screenshot("dashboard_authenticated") self._switch_to_admin_project() return self.take_screenshot("login_page") self.page.get_by_label("User Name").fill(config.get_username()) self.take_screenshot("login_username_filled") self.page.get_by_label("Password").fill(config.get_password()) self.take_screenshot("login_password_filled") # Custom dashboard could have different login button name # Using locator will help to avoid checking for specific button # label. self.page.locator("#loginBtn").click() self.take_screenshot("login_submit") # Wait for login to complete sync_api.expect(self.page).not_to_have_url( re.compile(r".*/auth/login.*"), timeout=config.get_timeout() ) self.take_screenshot("dashboard_logged_in") # Save auth state if enabled if config.is_auth_reuse_enabled(): auth_path = config.get_auth_state_path() os.makedirs(os.path.dirname(auth_path), exist_ok=True) self.pw_fixture.context.storage_state(path=auth_path) # Switch to admin project - Watcher requires admin privileges self._switch_to_admin_project() def _switch_to_admin_project(self): """Switch to the admin project if not already there.""" try: admin_panel_url = f"{self.dashboard_url}/admin/" admin_switch_link = self.page.locator( f"a[href*='/auth/switch/']" f":has-text('{config.get_project_name()}')" ) if admin_switch_link.count() == 0: LOG.info("Already in admin project") self.page.goto(admin_panel_url) self.page.wait_for_load_state( "networkidle", timeout=config.get_timeout() ) self.take_screenshot("admin_panel_loaded") return switch_url = admin_switch_link.first.get_attribute("href") if not switch_url: self.take_screenshot("no_switch_url") raise RuntimeError("Could not get href from admin switch link") self.take_screenshot("before_project_switch") switch_url = urljoin(self.dashboard_url + "/", switch_url) admin_path = ( urlparse(self.dashboard_url).path.rstrip("/") + "/admin/" ) switch_url = switch_url.split("?")[0] + f"?next={admin_path}" LOG.info("Switching to admin project via: %s", switch_url) self.page.goto(switch_url) self.page.wait_for_load_state( "networkidle", timeout=config.get_timeout() ) self.take_screenshot("admin_panel_loaded") except sync_api.Error as e: LOG.error("Could not switch to admin project: %s", e) self.take_screenshot("project_switch_failed") raise def take_screenshot(self, name): """Helper to take numbered screenshots.""" if not config.is_screenshot_enabled(): return self._screenshot_counter += 1 filename = f"{self._screenshot_counter:03d}_{name}.png" if self._test_screenshot_dir: base_dir = self._test_screenshot_dir else: base_dir = self.screenshot_dir path = os.path.join(base_dir, filename) self.page.screenshot(path=path) return path def _get_test_screenshot_dir(self): """Return a per-test screenshot directory. This avoids filename collisions when tests run in parallel and when multiple tests generate the same screenshot names. """ if not config.is_screenshot_enabled(): return None test_id = self.id() safe_test_id = re.sub(r'[^A-Za-z0-9_.-]+', '_', test_id) return os.path.join(self.screenshot_dir, safe_test_id) def _generate_unique_name(self, prefix): """Generate a unique resource name for test isolation.""" return f"{prefix}_{uuid.uuid4().hex[:8]}" def _extract_uuid_from_href(self, href): """Extract UUID from URL/path text.""" match = re.search( r'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-' r'[0-9a-f]{4}-[0-9a-f]{12})', href or '', re.IGNORECASE, ) if not match: self.fail("Could not extract UUID from href: %s" % href) return match.group(1) def wait_for_audit_terminal_state( self, timeout=300, poll_interval=5, terminal_states=None ): """Wait for audit to reach any terminal state on current page.""" states = terminal_states or ( 'SUCCEEDED', 'FAILED', 'CANCELLED', 'DELETED', ) start_time = time.time() while time.time() - start_time < timeout: self.page.reload() for state in states: if self.page.get_by_text(state, exact=True).count() > 0: return state time.sleep(poll_interval) raise TimeoutError( "Audit did not reach terminal states %s in %ss" % (states, timeout) ) def wait_for_action_plan_terminal_state( self, timeout=300, poll_interval=5, terminal_states=None ): """Wait for action plan terminal state, excluding in-table matches.""" states = terminal_states or ( 'SUCCEEDED', 'FAILED', 'CANCELLED', 'DELETED', ) start_time = time.time() while time.time() - start_time < timeout: self.page.reload() try: for state in states: total = self.page.get_by_text(state, exact=True).count() if total == 0: continue in_tables = ( self.page.get_by_role("table") .get_by_text(state, exact=True) .count() ) if total > in_tables: return state except (sync_api.Error, AttributeError): pass time.sleep(poll_interval) raise TimeoutError( "Action plan did not reach terminal states %s in %ss" % (states, timeout) ) def create_audit_template( self, name, goal_name="Dummy goal", strategy_name="Dummy strategy" ): """Create an audit template and register cleanup. :param name: Name of the audit template to create :type name: str :param goal_name: Goal name to select in the dropdown :type goal_name: str :param strategy_name: Strategy name to select in the dropdown :type strategy_name: str """ self.page.goto(f"{self.dashboard_url}/admin/audit_templates/") self.take_screenshot("audit_templates_page_loaded") self.page.get_by_role("link", name="Create Template").click() self.take_screenshot("audit_template_modal_open") modal_header = self.page.get_by_role( "heading", name="Create Audit Template" ) sync_api.expect(modal_header).to_be_visible() self.page.get_by_label("Name").fill(name) self.take_screenshot("audit_template_name_filled") self.page.get_by_label("Goal").select_option(label=goal_name) self.take_screenshot("audit_template_goal_selected") self.page.get_by_label("Strategy").select_option(label=strategy_name) self.take_screenshot("audit_template_strategy_selected") self.page.get_by_role("button", name="Create Audit Template").click() self.addCleanup(self._delete_audit_template_via_api, name) self.take_screenshot("audit_template_submit") self.wait_for_success_message() self.take_screenshot("audit_template_created") return self.watcher_client.audit_template.get(name).uuid def create_audit(self, template_name, audit_name, audit_type="ONESHOT"): """Create an audit and register cleanup. :param template_name: Name of the audit template to use :type template_name: str :param audit_name: Name for the audit :type audit_name: str :param audit_type: Type of audit :type audit_type: str :returns: The audit name """ self.page.goto(f"{self.dashboard_url}/admin/audits/") self.take_screenshot("audits_page_loaded") # The button to open modal - prefer user-visible text over classes. try: create_link = self.page.get_by_role("link", name="Create Audit") create_link.click() except sync_api.TimeoutError: create_link = self.page.get_by_role("link", name="New Audit") create_link.click() self.take_screenshot("audit_modal_open") modal_header = self.page.get_by_role("heading", name="Create Audit") sync_api.expect(modal_header).to_be_visible() # Select template self.page.get_by_label("Template").select_option(label=template_name) self.take_screenshot("audit_template_selected") # Fill audit name self.page.get_by_label("Name").fill(audit_name) self.take_screenshot("audit_name_filled") # Select audit type - must always be explicitly selected as no default self.page.get_by_label("Type").select_option(label=audit_type) self.take_screenshot("audit_type_selected") # Submit the form self.page.get_by_role("button", name="Create Audit").click() self.take_screenshot("audit_submit") self.addCleanup(self._cleanup_audit_by_name, audit_name) self.wait_for_success_message() self.take_screenshot("audit_created") return self._get_audit_uuid_via_api(audit_name) def _cleanup_audit_by_name(self, audit_name): """Clean up an audit and its associated resources by name. Resolves the audit UUID from its name, then deletes action plans and the audit itself in the correct order. :param audit_name: Name of the audit to clean up :type audit_name: str """ try: audit_uuid = self._get_audit_uuid_via_api(audit_name) self._delete_action_plans_for_audit_via_api(audit_uuid) self._delete_audit_via_api(audit_uuid) except watcher_exceptions.ClientException as exc: LOG.warning( "API cleanup: audit '%s' not found, skipping: %s", audit_name, exc, ) def _delete_audit_template_via_api(self, template_id): """Delete an audit template using the Watcher API. :param template_id: Name or UUID of the audit template to delete :type template_id: str """ LOG.info("API cleanup: deleting audit template id=%s", template_id) try: self.watcher_client.audit_template.delete(template_id) LOG.info("API cleanup: deleted audit template id=%s", template_id) except watcher_exceptions.ClientException as exc: LOG.error( "API cleanup FAILED for audit template id=%s: %s", template_id, exc, ) def _delete_audit_via_api(self, audit_uuid): """Delete an audit using the Watcher API. :param audit_uuid: UUID of the audit to delete :type audit_uuid: str """ LOG.info("API cleanup: deleting audit uuid=%s", audit_uuid) try: self.watcher_client.audit.delete(audit_uuid) LOG.info("API cleanup: deleted audit uuid=%s", audit_uuid) except watcher_exceptions.ClientException as exc: LOG.error( "API cleanup FAILED for audit uuid=%s: %s", audit_uuid, exc ) def _delete_action_plans_for_audit_via_api(self, audit_uuid): """Delete action plans associated with a specific audit. :param audit_uuid: UUID of the audit whose action plans to delete :type audit_uuid: str """ LOG.info( "API cleanup: deleting action plans for audit uuid=%s...", audit_uuid, ) try: action_plans = self.watcher_client.action_plan.list( audit=audit_uuid ) for action_plan in action_plans: self.watcher_client.action_plan.delete(action_plan.uuid) LOG.info( "API cleanup: deleted action plan (uuid=%s)", action_plan.uuid, ) except watcher_exceptions.ClientException as exc: LOG.error("API cleanup FAILED for action plans: %s", exc) def _get_audit_uuid_via_api(self, audit_name): """Look up an audit UUID by name via the Watcher API. :param audit_name: Name of the audit :type audit_name: str :returns: The audit UUID :rtype: str """ return self.watcher_client.audit.get(audit_name).uuid def wait_for_success_message(self, timeout=None): """Wait for success message to appear. :param timeout: Timeout in milliseconds (default: config timeout) :type timeout: int """ if timeout is None: timeout = config.get_timeout() # Try multiple selectors for success message success_msg = ( self.page.get_by_role("alert") .or_(self.page.get_by_text("Success")) .or_(self.page.get_by_text("Successfully")) .first ) sync_api.expect(success_msg).to_be_visible(timeout=timeout) def wait_for_audit_status(self, status, timeout=120, poll_interval=5): """Wait for audit status with polling. :param status: Expected status string :type status: str :param timeout: Maximum time to wait in seconds :type timeout: int :param poll_interval: Time between polls in seconds :type poll_interval: int :raises TimeoutError: If status is not reached within timeout """ start_time = time.time() while time.time() - start_time < timeout: self.page.reload() if self.page.get_by_text(status, exact=True).count() > 0: return time.sleep(poll_interval) raise TimeoutError("Audit did not reach %s in %ss" % (status, timeout)) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.833848 watcher_dashboard-16.0.0/watcher_dashboard/test/integration/playwright_config.py0000664000175000017500000001505115257165015027134 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Centralized configuration for Playwright integration tests.""" import functools import os from oslo_utils import strutils VALID_BROWSERS = ('chromium', 'firefox', 'webkit') VALID_VIDEO_MODES = ('off', 'on', 'retain-on-failure') VALID_TRACE_MODES = ('off', 'on', 'retain-on-failure') # OpenStack Configuration (via environment variables) @functools.cache def get_username(): """Get username for dashboard login from environment. :returns: Username from OS_USERNAME environment variable """ return os.environ['OS_USERNAME'] @functools.cache def get_password(): """Get password for dashboard login from environment. :returns: Password from OS_PASSWORD environment variable """ return os.environ['OS_PASSWORD'] @functools.cache def get_auth_url(): """Get Keystone auth URL from environment. :returns: Auth URL from OS_AUTH_URL environment variable """ return os.environ['OS_AUTH_URL'] @functools.cache def get_project_name(): """Get project name from environment. :returns: Project name from OS_PROJECT_NAME environment variable """ return os.environ['OS_PROJECT_NAME'] @functools.cache def get_user_domain_name(): """Get user domain name from environment. :returns: Domain from OS_USER_DOMAIN_NAME environment variable """ return os.environ['OS_USER_DOMAIN_NAME'] @functools.cache def get_project_domain_name(): """Get project domain name from environment. :returns: Domain from OS_PROJECT_DOMAIN_NAME environment variable """ return os.environ['OS_PROJECT_DOMAIN_NAME'] # Playwright Output Directory (single base, subdirs auto-created) @functools.cache def get_output_dir(): """Get the base directory for all Playwright artifacts. Subdirectories (screenshots, videos, traces, .auth) are created automatically by the test framework. :returns: Base output directory path """ return os.environ['PLAYWRIGHT_OUTPUT_DIR'] @functools.cache def get_screenshot_dir(): """Get the directory for saving screenshots. :returns: Screenshot directory path """ return os.path.join(get_output_dir(), 'screenshots') @functools.cache def get_video_dir(): """Get the directory for saving video recordings. :returns: Video directory path """ return os.path.join(get_output_dir(), 'videos') @functools.cache def get_trace_dir(): """Get the directory for saving trace files. :returns: Trace directory path """ return os.path.join(get_output_dir(), 'traces') @functools.cache def get_auth_state_path(): """Get the path for storing authentication state. :returns: Auth state file path """ return os.path.join(get_output_dir(), '.auth', 'state.json') @functools.cache def get_dashboard_url(): """Get the Watcher Dashboard URL from environment variable. :returns: Dashboard URL """ return os.environ['WATCHER_DASHBOARD_URL'] @functools.cache def get_browser(): """Get the browser type for Playwright. Supported browsers: chromium, firefox, webkit :returns: Browser type :raises ValueError: If browser type is not supported """ browser = os.environ['PLAYWRIGHT_BROWSER'].lower() if browser not in VALID_BROWSERS: raise ValueError( "Invalid browser '%s'. Must be one of: %s" % (browser, VALID_BROWSERS) ) return browser @functools.cache def is_headless(): """Check if browser should run in headless mode. :returns: True if headless mode is enabled """ return strutils.bool_from_string( os.environ['PLAYWRIGHT_HEADLESS'], strict=True ) @functools.cache def get_timeout(): """Get the default timeout for Playwright operations in milliseconds. :returns: Timeout in milliseconds """ return int(os.environ['PLAYWRIGHT_TIMEOUT']) @functools.cache def is_screenshot_enabled(): """Check if screenshot generation is enabled. :returns: True if screenshots should be taken """ return strutils.bool_from_string( os.environ['PLAYWRIGHT_SCREENSHOTS'], strict=True ) @functools.cache def get_video_mode(): """Get the video recording mode. :returns: Video mode: 'off', 'on', or 'retain-on-failure' :raises ValueError: If video mode is not supported """ mode = os.environ['PLAYWRIGHT_VIDEO'].lower() if mode not in VALID_VIDEO_MODES: raise ValueError( "Invalid video mode '%s'. Must be one of: %s" % (mode, VALID_VIDEO_MODES) ) return mode @functools.cache def get_trace_mode(): """Get the trace recording mode. :returns: Trace mode: 'off', 'on', or 'retain-on-failure' :raises ValueError: If trace mode is not supported """ mode = os.environ['PLAYWRIGHT_TRACE'].lower() if mode not in VALID_TRACE_MODES: raise ValueError( "Invalid trace mode '%s'. Must be one of: %s" % (mode, VALID_TRACE_MODES) ) return mode @functools.cache def is_auth_reuse_enabled(): """Check if authentication state reuse is enabled. When enabled, the first login will save browser state and subsequent tests will reuse it, avoiding repeated logins. :returns: True if auth state reuse is enabled (default: True) """ value = os.environ.get('PLAYWRIGHT_AUTH_REUSE', 'True') return strutils.bool_from_string(value, default=True) @functools.cache def is_insecure(): """Check if SSL certificate verification should be disabled. :returns: True if SSL verification should be disabled """ return strutils.bool_from_string(os.environ['OS_INSECURE'], strict=True) @functools.cache def ignore_https_errors(): """Check if HTTPS certificate errors should be ignored. When enabled, Playwright will ignore HTTPS certificate validation errors. Useful for testing with self-signed certificates. :returns: True if HTTPS errors should be ignored (default: False) """ value = os.environ.get('PLAYWRIGHT_IGNORE_HTTPS_ERRORS', 'False') return strutils.bool_from_string(value, default=False) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.833848 watcher_dashboard-16.0.0/watcher_dashboard/test/integration/test_existing_goals.py0000664000175000017500000001363015257165015027474 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Integration tests for Goals using Playwright.""" from django import test from watcher_dashboard.test.integration import playwright_base from watcher_dashboard.test.integration import playwright_config as config @test.tag('integration') class GoalsTests(playwright_base.PlaywrightTestCase): """Integration tests for Goals using Playwright.""" def test_goals_match_watcher_api(self): """Test that goals displayed in UI match those from Watcher API. This test verifies that the Horizon dashboard correctly displays all goals registered in Watcher, ensuring consistency between the API and the UI representation. Steps: 1. Fetch goals list from Watcher API 2. Navigate to Admin > Optimization > Goals in Horizon 3. Verify goal count matches between API and UI 4. Verify each goal from API is visible in UI with correct name and display_name 5. For each goal, verify efficacy specifications match API Expected Result: All goals from Watcher API should be visible in the dashboard with matching names, display names, and efficacy specifications. """ # Fetch goals from Watcher API as source of truth api_goals = self.watcher_client.goal.list(detail=True) self.assertGreater(len(api_goals), 0, "No goals found in Watcher API") # Navigate to Goals page through the menu # This ensures Horizon loads all panels correctly self.page.get_by_text("Optimization").click() self.take_screenshot("optimization_clicked") self.page.get_by_role("link", name="Goals").click() self.take_screenshot("goals_page_loaded") # Verify goal count matches between API and UI ui_goal_rows = self.page.locator("tbody tr") self.expect(ui_goal_rows).to_have_count(len(api_goals)) self.take_screenshot("goals_count_verified") # Verify each goal from API is correctly displayed in UI for idx, api_goal in enumerate(api_goals): goal_name = api_goal.name display_name = api_goal.display_name goal_uuid = api_goal.uuid # Locate the row by goal name row = self.page.locator(f"tbody tr[data-display='{goal_name}']") self.expect(row).to_be_visible() # Verify the display name is visible in the row self.expect( row.get_by_text(display_name, exact=True) ).to_be_visible() # Click on the goal UUID to view details uuid_link = row.get_by_role("link", name=goal_uuid) self.expect(uuid_link).to_be_visible() uuid_link.click() self.take_screenshot(f"goal_{idx}_{goal_name}_details_opened") # Verify efficacy specifications match API self._verify_efficacy_specifications(api_goal, goal_name, idx) # Go back to the goals list self.page.go_back() self.page.wait_for_load_state( "networkidle", timeout=config.get_timeout() ) self.take_screenshot(f"goal_{idx}_back_to_list") self.take_screenshot("all_goals_verified") def _verify_efficacy_specifications(self, api_goal, goal_name, idx): """Verify that efficacy specifications in UI match API data. :param api_goal: Goal object from Watcher API :param goal_name: Name of the goal for screenshot naming :param idx: Index for screenshot naming """ api_specs = api_goal.efficacy_specification if not api_specs: # No efficacy specifications - verify UI shows this # Look for "No items to display" or similar message no_data_msg = self.page.get_by_text("No items to display") self.expect(no_data_msg).to_be_visible() self.take_screenshot(f"goal_{idx}_{goal_name}_no_efficacy_specs") return # Find the efficacy specifications table by its ID spec_table = self.page.locator("table#efficacy_specification") self.expect(spec_table).to_be_visible() # Verify count matches spec_rows = spec_table.locator("tbody tr") self.expect(spec_rows).to_have_count(len(api_specs)) self.take_screenshot(f"goal_{idx}_{goal_name}_efficacy_specs_count_ok") # Verify each specification for spec_idx, api_spec in enumerate(api_specs): spec_name = api_spec['name'] spec_description = api_spec['description'] spec_unit = api_spec.get('unit') or '-' spec_schema = api_spec['schema'] # Find the row for this specification spec_row = spec_table.locator( f"tbody tr:has(td:text-is('{spec_name}'))" ) self.expect(spec_row).to_be_visible() # Verify description self.expect( spec_row.get_by_text(spec_description, exact=True) ).to_be_visible() # Verify unit self.expect( spec_row.get_by_text(spec_unit, exact=True) ).to_be_visible() # Verify schema self.expect( spec_row.get_by_text(spec_schema, exact=True) ).to_be_visible() self.take_screenshot( f"goal_{idx}_{goal_name}_all_efficacy_specs_verified" ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.834848 watcher_dashboard-16.0.0/watcher_dashboard/test/integration/test_existing_strategies.py0000664000175000017500000001563115257165015030544 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Integration tests for Strategies using Playwright.""" import json import yaml from django import test from watcher_dashboard.test.integration import playwright_base from watcher_dashboard.test.integration import playwright_config as config @test.tag('integration') class StrategiesTests(playwright_base.PlaywrightTestCase): """Integration tests for Strategies using Playwright.""" def test_strategies_match_watcher_api(self): """Test that strategies displayed in UI match those from Watcher API. This test verifies that the Horizon dashboard correctly displays all strategies registered in Watcher, ensuring consistency between the API and the UI representation. Steps: 1. Fetch strategies list from Watcher API 2. Navigate to Admin > Optimization > Strategies in Horizon 3. Verify strategy count matches between API and UI 4. Verify each strategy from API is visible in UI with correct name and display_name 5. For each strategy, verify parameter specs match API Expected Result: All strategies from Watcher API should be visible in the dashboard with matching names, display names, and parameter specifications. """ # Fetch strategies from Watcher API as source of truth api_strategies = self.watcher_client.strategy.list(detail=True) self.assertGreater( len(api_strategies), 0, "No strategies found in Watcher API" ) # Navigate to Strategies page through the menu # This ensures Horizon loads all panels correctly self.page.get_by_text("Optimization").click() self.take_screenshot("optimization_clicked") self.page.get_by_role("link", name="Strategies").click() self.take_screenshot("strategies_page_loaded") # Verify strategy count matches between API and UI ui_strategy_rows = self.page.locator("tbody tr") self.expect(ui_strategy_rows).to_have_count(len(api_strategies)) self.take_screenshot("strategies_count_verified") # Verify each strategy from API is correctly displayed in UI for idx, api_strategy in enumerate(api_strategies): strategy_name = api_strategy.name display_name = api_strategy.display_name strategy_uuid = api_strategy.uuid # Locate the row by strategy name row = self.page.locator( f"tbody tr[data-display='{strategy_name}']" ) self.expect(row).to_be_visible() # Verify the display name is visible in the row self.expect( row.get_by_text(display_name, exact=True) ).to_be_visible() # Click on the strategy UUID to view details uuid_link = row.get_by_role("link", name=strategy_uuid) self.expect(uuid_link).to_be_visible() uuid_link.click() self.take_screenshot( f"strategy_{idx}_{strategy_name}_details_opened" ) # Verify parameter specs match API self._verify_parameter_specs(api_strategy, strategy_name, idx) # Go back to the strategies list self.page.go_back() self.page.wait_for_load_state( "networkidle", timeout=config.get_timeout() ) self.take_screenshot(f"strategy_{idx}_back_to_list") self.take_screenshot("all_strategies_verified") def _verify_parameter_specs(self, api_strategy, strategy_name, idx): """Verify that parameter specs in UI match API data. :param api_strategy: Strategy object from Watcher API :param strategy_name: Name of the strategy for screenshot naming :param idx: Index for screenshot naming """ api_specs = api_strategy.parameters_spec # The UI shows parameters_spec as a YAML/JSON formatted string # Find the definition term "Parameters Spec" params_spec_dt = self.page.locator("dt:has-text('Parameters Spec')") self.expect(params_spec_dt).to_be_visible() # Get the corresponding definition data (dd element) params_spec_dd = params_spec_dt.locator( "xpath=following-sibling::dd[1]" ) self.expect(params_spec_dd).to_be_visible() # Get the text content from the UI ui_spec_text = params_spec_dd.inner_text().strip() self.take_screenshot( f"strategy_{idx}_{strategy_name}_parameter_specs_visible" ) if not api_specs: # No parameter specs from API # UI should show empty or dash self.assertIn( ui_spec_text, ['-', '', 'None'], f"Expected empty params for {strategy_name}, " f"got: {ui_spec_text}", ) self.take_screenshot( f"strategy_{idx}_{strategy_name}_no_parameter_specs" ) return # Parse both API and UI specs for comparison # API specs is already a dict api_spec_dict = api_specs # UI shows it as YAML format, parse it try: ui_spec_dict = yaml.safe_load(ui_spec_text) except yaml.YAMLError: # If YAML parsing fails, try JSON try: ui_spec_dict = json.loads(ui_spec_text) except json.JSONDecodeError: self.fail( f"Failed to parse UI parameter spec for {strategy_name}: " f"{ui_spec_text}" ) # The UI might strip the top-level $schema, type, and required fields # and only show the properties content. Let's check both formats. if 'properties' in api_spec_dict and ui_spec_dict == api_spec_dict.get( 'properties' ): # UI shows only the properties part self.take_screenshot( f"strategy_{idx}_{strategy_name}_parameter_specs_verified" ) return # Full comparison including top-level fields self.assertEqual( api_spec_dict, ui_spec_dict, f"Parameter specs mismatch for {strategy_name}. " f"API: {api_spec_dict}, UI: {ui_spec_dict}", ) self.take_screenshot( f"strategy_{idx}_{strategy_name}_parameter_specs_verified" ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.834848 watcher_dashboard-16.0.0/watcher_dashboard/test/integration/test_playwright_audit_workflow.py0000664000175000017500000000761015257165015031770 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Integration tests for audit workflow using Playwright.""" from django import test from watcher_dashboard.test.integration import playwright_base @test.tag('integration') class AuditWorkflowTests(playwright_base.PlaywrightTestCase): """Integration tests for audit workflow using Playwright.""" def test_create_audit_template(self): """Test creating an audit template via UI. Steps: 1. Navigate to the Audit Templates index page 2. Create a new audit template with a unique name 3. Verify the template appears in the table Expected Result: The audit template should be created successfully and visible in the index table. """ template_name = self._generate_unique_name("test_template") self.create_audit_template(template_name) # Verify template appears in table row = self.page.get_by_role("row").filter(has_text=template_name) self.expect(row).to_be_visible() self.take_screenshot("audit_template_row_visible") def test_launch_audit_and_verify_action_plan(self): """Test launching audit and verifying action plan creation. Steps: 1. Create a prerequisite audit template 2. Create an audit using the template 3. Wait for the audit to reach SUCCEEDED status 4. Navigate to action plans page and verify an action plan exists Expected Result: The audit should complete successfully and generate an action plan visible in the action plans table. """ template_name = self._generate_unique_name("test_template") self.create_audit_template(template_name) audit_name = self._generate_unique_name("test_audit") self.create_audit(template_name, audit_name) # Wait for audit to complete self.wait_for_audit_status("Succeeded") # Verify action plan exists self.page.goto(f"{self.dashboard_url}/admin/action_plans/") self.take_screenshot("action_plans_page_loaded") first_row = ( self.page.get_by_role("row") .filter(has=self.page.get_by_role("cell")) .first ) self.expect(first_row).to_be_visible() self.take_screenshot("action_plans_visible") def test_audit_template_form_validation(self): """Test that audit template form shows required field validation. Steps: 1. Navigate to the Audit Templates index page 2. Open the create audit template modal 3. Attempt to submit the form without filling required fields Expected Result: The modal should remain visible indicating the form was not submitted due to validation errors. """ self.page.goto(f"{self.dashboard_url}/admin/audit_templates/") # Open create modal self.page.get_by_role("link", name="Create Template").click() # Try to submit without filling required fields self.page.get_by_role("button", name="Create Audit Template").click() # Modal should still be visible (form not submitted) modal_header = self.page.get_by_role( "heading", name="Create Audit Template" ) self.expect(modal_header).to_be_visible() self.take_screenshot("validation_error") ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.835848 watcher_dashboard-16.0.0/watcher_dashboard/test/integration/test_playwright_skip_action_workflow.py0000664000175000017500000001477015257165015033172 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Integration tests for skip action workflow using Playwright.""" import re from django import test from watcher_dashboard.test.integration import playwright_base @test.tag('integration') class SkipActionWorkflowTests(playwright_base.PlaywrightTestCase): """Integration tests for skip action workflow.""" def test_skip_action_workflow(self): """Test skipping an action and verifying plan succeeds with skip msg. Steps: 1. Create an audit template and audit, wait for SUCCEEDED 2. Navigate to the action plan and skip the first action 3. Verify the action shows SKIPPED state and skip reason 4. Start the action plan and wait for SUCCEEDED 5. Verify the status message reflects the skipped action Expected Result: The skipped action should show SKIPPED state with the reason, and the action plan should succeed with a status message indicating one or more actions were skipped. """ skip_reason = "this action is skipped in CI testing" template_name = self._generate_unique_name("dummy_skip_template") self.create_audit_template( name=template_name, goal_name="Dummy goal", strategy_name="Dummy strategy", ) audit_name = self._generate_unique_name("dummy_skip_audit") self.create_audit( template_name=template_name, audit_name=audit_name, audit_type="ONESHOT", ) self.page.goto(f"{self.dashboard_url}/admin/audits/") row = self.page.get_by_role("row").filter(has_text=audit_name).first self.expect(row).to_be_visible() row.get_by_role("link", name=audit_name).click() self.take_screenshot("audit_detail_loaded") final_audit_state = self.wait_for_audit_terminal_state(timeout=300) self.assertEqual("SUCCEEDED", final_audit_state) self.take_screenshot("audit_terminal_state_succeeded") action_plans_table = self.page.get_by_role( "table", name="Related Action Plans" ) action_plan_row = action_plans_table.locator("tbody tr").first self.expect(action_plan_row).to_be_visible() action_plan_link = action_plan_row.get_by_role("link").first self.expect(action_plan_link).to_be_visible() action_plan_href = action_plan_link.get_attribute("href") action_plan_uuid = self._extract_uuid_from_href(action_plan_href) action_plan_link.click() self.take_screenshot("action_plan_detail_loaded") actions_table = self.page.get_by_role("table", name="Related Actions") first_action_row = actions_table.locator("tbody tr").first self.expect(first_action_row).to_be_visible() action_link = first_action_row.get_by_role("link").first self.expect(action_link).to_be_visible() action_href = action_link.get_attribute("href") action_uuid = self._extract_uuid_from_href(action_href) skip_btn = first_action_row.get_by_role("link", name="Skip Action") self.expect(skip_btn).to_be_visible() skip_btn.click() self.take_screenshot("skip_action_modal_open") modal_header = self.page.get_by_role("heading", name="Skip Action") self.expect(modal_header).to_be_visible() self.page.get_by_label("Reason for skipping").fill(skip_reason) self.take_screenshot("skip_reason_filled") self.page.get_by_role("button", name="Skip Action").click() self.wait_for_success_message() self.take_screenshot("skip_action_submitted") self.page.goto( f"{self.dashboard_url}/admin/actions/{action_uuid}/detail" ) self.take_screenshot("action_detail_loaded") self.expect(self.page.get_by_text("State")).to_be_visible() self.expect( self.page.get_by_text("SKIPPED", exact=True) ).to_be_visible() self.expect(self.page.get_by_text("Skip Reason")).to_be_visible() self.expect( self.page.get_by_text( re.compile(r"(skipped.*CI testing|Action skipped by user\.?)") ) ).to_be_visible() self.page.goto(f"{self.dashboard_url}/admin/audits/") audit_row = ( self.page.get_by_role("row").filter(has_text=audit_name).first ) self.expect(audit_row).to_be_visible() audit_row.get_by_role("link", name=audit_name).click() self.take_screenshot("audit_detail_reloaded") plan_row = ( self.page.get_by_role("table", name="Related Action Plans") .locator("tbody tr") .filter(has_text=action_plan_uuid) .first ) self.expect(plan_row).to_be_visible() start_control = plan_row.get_by_role( "button", name="Start Action Plan" ).or_(plan_row.get_by_role("link", name="Start Action Plan")) self.expect(start_control.first).to_be_visible() start_control.first.click() self.wait_for_success_message() self.take_screenshot("action_plan_started") self.page.goto( f"{self.dashboard_url}/admin/action_plans/" f"{action_plan_uuid}/detail" ) self.take_screenshot("action_plan_detail_after_start") self.expect( self.page.get_by_role("heading", name="Action Plan Overview") ).to_be_visible() final_plan_state = self.wait_for_action_plan_terminal_state( timeout=300 ) self.assertEqual( "SUCCEEDED", final_plan_state, "Action plan did not succeed; state=%s" % final_plan_state, ) self.take_screenshot("action_plan_succeeded") # Backend may set the message slightly after plan reaches SUCCEEDED self.expect( self.page.get_by_text( re.compile(r"one or more actions were skipped\.?", re.I) ) ).to_be_visible(timeout=30000) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9458196 watcher_dashboard-16.0.0/watcher_dashboard/tests/0000775000175000017500000000000015257165054020704 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.835848 watcher_dashboard-16.0.0/watcher_dashboard/tests/__init__.py0000664000175000017500000000000015257165015023000 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.835848 watcher_dashboard-16.0.0/watcher_dashboard/tests/helpers.py0000664000175000017500000000362015257165015022716 0ustar00zuulzuul# Copyright 2012 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # # Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from openstack_dashboard.test import helpers from watcher_dashboard import api from watcher_dashboard.tests.local_fixtures import logging_fixture from watcher_dashboard.tests.test_data import utils class WatcherTestsMixin: def setUp(self): logging_fixture.setup_standard_logging(self) super().setUp() def _setup_test_data(self): super()._setup_test_data() utils.load_test_data(self) class TestCase(WatcherTestsMixin, helpers.TestCase): pass class APITestCase(WatcherTestsMixin, helpers.APITestCase): def setUp(self): super().setUp() self._original_watcherclient = api.watcher.watcherclient api.watcher.watcherclient = lambda request, *args, **kwargs: ( self.stub_watcherclient() ) def tearDown(self): super().tearDown() api.watcher.watcherclient = self._original_watcherclient def stub_watcherclient(self): if not hasattr(self, "watcherclient"): self.watcherclient = mock.Mock() return self.watcherclient class BaseAdminViewTests(WatcherTestsMixin, helpers.BaseAdminViewTests): pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9458196 watcher_dashboard-16.0.0/watcher_dashboard/tests/local_fixtures/0000775000175000017500000000000015257165054023727 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.835848 watcher_dashboard-16.0.0/watcher_dashboard/tests/local_fixtures/__init__.py0000664000175000017500000000000015257165015026023 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.835848 watcher_dashboard-16.0.0/watcher_dashboard/tests/local_fixtures/fixtures.py0000664000175000017500000000366715257165015026163 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Test mixin classes for watcher-dashboard unit tests. Provides mixins that manage test isolation for modules that use memoized caching, preventing cached state from leaking between test cases. """ from watcher_dashboard import config class ConfigMemoizedCache: """Mixin that clears memoized config caches before and after each test. Use this mixin in test cases that exercise ``watcher_dashboard.config`` functions decorated with ``@functools.lru_cache`` or similar caching decorators. It ensures each test starts and ends with a clean cache state, preventing cross-test pollution. Example:: from watcher_dashboard.tests.local_fixtures.fixtures import ( ConfigMemoizedCache, ) class TestConfigFunctions(ConfigMemoizedCache, TestCase): def test_get_ssl_no_verify_default(self): result = config.get_ssl_no_verify() self.assertFalse(result) """ def setUp(self): super().setUp() self._clear_config_caches() def tearDown(self): super().tearDown() self._clear_config_caches() def _clear_config_caches(self): """Clear all ``functools`` caches on ``watcher_dashboard.config``.""" for attr_name in dir(config): attr = getattr(config, attr_name) if callable(attr) and hasattr(attr, 'cache_clear'): attr.cache_clear() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.835848 watcher_dashboard-16.0.0/watcher_dashboard/tests/local_fixtures/logging_fixture.py0000664000175000017500000000521515257165015027475 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Logging redirection for watcher-dashboard tests. Adapted from ``watcher.tests.fixtures.watcher`` (``StandardLogging`` / ``NullHandler``): quiet local runs by default, still exercises debug log formatting, and honors ``OS_DEBUG`` like upstream. Uses ``addCleanup`` so this works with Django's ``TestCase`` as well as testtools-based cases (no ``useFixture`` dependency). """ from __future__ import annotations import logging as std_logging import os from io import StringIO class NullHandler(std_logging.Handler): """Format debug records without persisting them (catches format errors).""" def handle(self, record) -> None: # format() exercises the formatter so broken format strings fail fast; # emit() is a no-op so nothing is actually written self.format(record) def emit(self, record) -> None: pass def createLock(self) -> None: self.lock = None def setup_standard_logging(testcase) -> None: """Configure root logging for one test; teardown via ``addCleanup``.""" root = std_logging.getLogger() saved_level = root.level saved_handlers = list(root.handlers) for h in saved_handlers: root.removeHandler(h) root.setLevel(std_logging.DEBUG) if os.environ.get("OS_DEBUG") in ("True", "true", "1", "yes"): level = std_logging.DEBUG else: level = std_logging.INFO fmt = "%(asctime)s %(levelname)s [%(name)s] %(message)s" stream = StringIO() main_handler = std_logging.StreamHandler(stream) main_handler.setFormatter(std_logging.Formatter(fmt)) main_handler.setLevel(level) root.addHandler(main_handler) extra_handlers: list[std_logging.Handler] = [] if level > std_logging.DEBUG: null_h = NullHandler() null_h.setLevel(std_logging.DEBUG) root.addHandler(null_h) extra_handlers.append(null_h) def _restore() -> None: root.removeHandler(main_handler) for h in extra_handlers: root.removeHandler(h) for h in saved_handlers: root.addHandler(h) root.setLevel(saved_level) testcase.addCleanup(_restore) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.835848 watcher_dashboard-16.0.0/watcher_dashboard/tests/settings.py0000664000175000017500000000331615257165015023116 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import warnings # Deprecation noise during ``update_dashboards()`` import-time setup; avoid # forcing the root logger level (see ``StandardLogging`` in test helpers). warnings.filterwarnings( "ignore", message=r".*default_dashboard is DEPRECATED.*", category=DeprecationWarning, ) from horizon.test.settings import * # noqa from openstack_dashboard.test.settings import * # noqa from horizon.test.settings import settings_utils # noqa from openstack_dashboard.test.settings import HORIZON_CONFIG # noqa # Load the pluggable dashboard settings import openstack_dashboard.enabled # noqa: E402 import watcher_dashboard.local.enabled # noqa: E402 INSTALLED_APPS = ( 'django.contrib.contenttypes', 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.staticfiles', 'django.contrib.messages', 'django.contrib.humanize', 'openstack_auth', 'compressor', 'horizon', 'openstack_dashboard', ) INSTALLED_APPS = list(INSTALLED_APPS) # Make sure it's mutable settings_utils.update_dashboards( [watcher_dashboard.local.enabled, openstack_dashboard.enabled], HORIZON_CONFIG, INSTALLED_APPS, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9458196 watcher_dashboard-16.0.0/watcher_dashboard/tests/test_data/0000775000175000017500000000000015257165054022654 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.836848 watcher_dashboard-16.0.0/watcher_dashboard/tests/test_data/__init__.py0000664000175000017500000000000015257165015024750 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.836848 watcher_dashboard-16.0.0/watcher_dashboard/tests/test_data/exceptions.py0000664000175000017500000000161415257165015025406 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from openstack_dashboard.test.test_data import exceptions from watcherclient.common.apiclient import exceptions as wexceptions def data(TEST): TEST.exceptions = exceptions.data watcher_exception = wexceptions.ClientException TEST.exceptions.watcher = exceptions.create_stubbed_exception( watcher_exception ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.836848 watcher_dashboard-16.0.0/watcher_dashboard/tests/test_data/utils.py0000664000175000017500000000360115257165015024363 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from openstack_dashboard.test.test_data import keystone_data from openstack_dashboard.test.test_data import utils def load_test_data(load_onto=None): from watcher_dashboard.tests.test_data import exceptions from watcher_dashboard.tests.test_data import watcher_data # The order of these loaders matters, some depend on others. loaders = (exceptions.data, keystone_data.data, watcher_data.data) if load_onto: for data_func in loaders: data_func(load_onto) return load_onto else: return utils.TestData(*loaders) class TestDataContainer(utils.TestDataContainer): def filter(self, filtered=None, **kwargs): """Returns objects in this container""" """whose attributes match the given keyword arguments. """ if filtered is None: filtered = self._objects try: key, value = kwargs.popitem() except KeyError: # We're out of filters, return return filtered def get_match(obj): return key in obj and obj.get(key) == value return self.filter(filtered=filter(get_match, filtered), **kwargs) def delete(self): """Delete the first object from this container and return a list""" self._objects.remove(self._objects[0]) return self._objects ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.836848 watcher_dashboard-16.0.0/watcher_dashboard/tests/test_data/watcher_data.py0000664000175000017500000001746115257165015025662 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy from watcher_dashboard.api import watcher from watcher_dashboard.tests.test_data import utils def data(TEST): TEST.service_catalog.append( { "type": "infra-optim", "name": "watcher", "endpoints_links": [], "endpoints": [ { "region": "RegionOne", "adminURL": "http://admin.watcher.example.com:9322", "internalURL": "http://int.watcher.example.com:9322", "publicURL": "http://public.watcher.example.com:9322", }, { "region": "RegionTwo", "adminURL": "http://admin.watcher2.example.com:9322", "internalURL": "http://int.watcher2.example.com:9322", "publicURL": "http://public.watcher2.example.com:9322", }, ], } ) TEST.goals = utils.TestDataContainer() TEST.efficacy_specifications = utils.TestDataContainer() TEST.api_goals = utils.TestDataContainer() efficacy_specifications_dict1 = {'name': 'spec1'} efficacy_specifications_dict2 = {'name': 'spec2'} spec1 = watcher.EfficacyIndicatorSpec(efficacy_specifications_dict1) spec2 = watcher.EfficacyIndicatorSpec(efficacy_specifications_dict2) goal_dict1 = { 'uuid': 'gggggggg-1111-1111-1111-gggggggggggg', 'name': 'MINIMIZE_LICENSING_COST', 'display_name': 'Dummy', 'efficacy_specification': [spec1], } goal_dict2 = { 'uuid': 'gggggggg-2222-2222-2222-gggggggggggg', 'name': 'SERVER_CONSOLIDATION', 'display_name': 'Server consolidation', 'efficacy_specification': [spec2], } TEST.api_goals.add(goal_dict1) TEST.api_goals.add(goal_dict2) _goal_dict1 = copy.deepcopy(goal_dict1) _goal_dict2 = copy.deepcopy(goal_dict2) TEST.strategies = utils.TestDataContainer() TEST.api_strategies = utils.TestDataContainer() strategy_dict1 = { 'uuid': 'ssssssss-1111-1111-1111-ssssssssssss', 'name': 'minimize_licensing_cost1', 'goal_uuid': 'gggggggg-1111-1111-1111-gggggggggggg', 'display_name': 'Fake licensing cost strategy1', } strategy_dict2 = { 'uuid': 'ssssssss-2222-2222-2222-ssssssssssss', 'name': 'minimize_licensing_cost2', 'goal_uuid': 'gggggggg-1111-1111-1111-gggggggggggg', 'display_name': 'Fake licensing cost strategy2', } strategy_dict3 = { 'uuid': 'ssssssss-3333-3333-3333-ssssssssssss', 'name': 'sercon', 'goal_uuid': 'gggggggg-2222-2222-2222-gggggggggggg', 'display_name': 'Fake Sercon', } TEST.api_strategies.add(strategy_dict1) TEST.api_strategies.add(strategy_dict2) TEST.api_strategies.add(strategy_dict3) _strategy_dict1 = copy.deepcopy(strategy_dict1) _strategy_dict2 = copy.deepcopy(strategy_dict2) _strategy_dict3 = copy.deepcopy(strategy_dict3) TEST.audit_templates = utils.TestDataContainer() TEST.api_audit_templates = utils.TestDataContainer() audit_template_dict = { 'uuid': '11111111-1111-1111-1111-111111111111', 'name': 'Audit Template 1', 'description': 'Audit Template 1 description', 'scope': '', 'goal_uuid': 'gggggggg-1111-1111-1111-gggggggggggg', 'strategy_uuid': 'ssssssss-1111-1111-1111-ssssssssssss', } audit_template_dict2 = { 'uuid': '11111111-2222-2222-2222-111111111111', 'name': 'Audit Template 2', 'description': 'Audit Template 2 description', 'scope': '', 'goal_uuid': 'gggggggg-1111-1111-1111-gggggggggggg', 'strategy_uuid': 'ssssssss-2222-2222-2222-ssssssssssss', } audit_template_dict3 = { 'uuid': '11111111-3333-3333-3333-111111111111', 'name': 'Audit Template 1', 'description': 'Audit Template 3 description', 'scope': '', 'goal_uuid': 'gggggggg-2222-2222-2222-gggggggggggg', 'strategy_uuid': None, } TEST.api_audit_templates.add(audit_template_dict) TEST.api_audit_templates.add(audit_template_dict2) TEST.api_audit_templates.add(audit_template_dict3) _audit_template_dict = copy.deepcopy(audit_template_dict) _audit_template_dict2 = copy.deepcopy(audit_template_dict2) _audit_template_dict3 = copy.deepcopy(audit_template_dict3) TEST.audits = utils.TestDataContainer() TEST.api_audits = utils.TestDataContainer() audit_dict = { 'uuid': '22222222-2222-2222-2222-222222222222', 'audit_type': 'ONESHOT', 'name': 'Audit 1', 'audit_template_uuid': '11111111-1111-1111-1111-111111111111', 'interval': None, } audit_dict2 = { 'uuid': '33333333-3333-3333-3333-333333333333', 'audit_type': 'CONTINUOUS', 'name': 'Audit 2', 'audit_template_uuid': '11111111-1111-1111-1111-111111111111', 'interval': 60, } TEST.api_audits.add(audit_dict) TEST.api_audits.add(audit_dict2) _audit_dict = copy.deepcopy(audit_dict) TEST.action_plans = utils.TestDataContainer() TEST.api_action_plans = utils.TestDataContainer() action_plan_dict = { 'uuid': '33333333-3333-3333-3333-333333333333', 'state': 'RECOMMENDED', 'first_action_uuid': '44444444-4444-4444-4444-111111111111', 'audit_uuid': '33333333-3333-3333-3333-333333333333', } TEST.api_action_plans.add(action_plan_dict) _action_plan_dict = copy.deepcopy(action_plan_dict) TEST.actions = utils.TestDataContainer() TEST.api_actions = utils.TestDataContainer() action_dict1 = { 'uuid': '44444444-4444-4444-4444-111111111111', 'state': 'PENDING', 'next_uuid': '44444444-4444-4444-4444-222222222222', 'action_plan_uuid': '33333333-3333-3333-3333-333333333333', 'status_message': None, } TEST.api_actions.add(action_dict1) action_dict2 = { 'uuid': '44444444-4444-4444-4444-222222222222', 'state': 'PENDING', 'next_uuid': None, 'action_plan_uuid': '33333333-3333-3333-3333-333333333333', 'status_message': None, } TEST.api_actions.add(action_dict2) action2 = watcher.Action(action_dict2) action1 = watcher.Action(action_dict1) TEST.actions.add(action1) TEST.actions.add(action2) _action_plan_dict['actions'] = [action1, action2] action_plan = watcher.ActionPlan(_action_plan_dict) _audit_dict['action_plans'] = [action_plan] audit = watcher.Audit(_audit_dict) goal1 = watcher.Goal(_goal_dict1) goal2 = watcher.Goal(_goal_dict2) strategy1 = watcher.Strategy(_strategy_dict1) strategy2 = watcher.Strategy(_strategy_dict2) strategy3 = watcher.Strategy(_strategy_dict3) audit_template1 = watcher.AuditTemplate(_audit_template_dict) audit_template2 = watcher.AuditTemplate(_audit_template_dict2) audit_template3 = watcher.AuditTemplate(_audit_template_dict3) TEST.goals.add(goal1) TEST.goals.add(goal2) TEST.strategies.add(strategy1) TEST.strategies.add(strategy2) TEST.strategies.add(strategy3) TEST.audit_templates.add(audit_template1) TEST.audit_templates.add(audit_template2) TEST.audit_templates.add(audit_template3) TEST.audits.add(audit) TEST.action_plans.add(watcher.ActionPlan(action_plan)) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.836848 watcher_dashboard-16.0.0/watcher_dashboard/tests/test_formset_table.py0000664000175000017500000000337115257165015025144 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import django.forms from horizon import tables from horizon.tables import formset as hformset from watcher_dashboard.tests import helpers as test class FormsetTableTests(test.TestCase): def test_populate(self): """Create a FormsetDataTable and populate it with data.""" class TableObj: pass obj = TableObj() obj.name = 'test object' obj.value = 42 obj.id = 4 class TableForm(django.forms.Form): name = django.forms.CharField() value = django.forms.IntegerField() TableFormset = django.forms.formsets.formset_factory( TableForm, extra=0 ) class Table(hformset.FormsetDataTable): formset_class = TableFormset name = tables.Column('name') value = tables.Column('value') class Meta: name = 'table' table = Table(self.request) table.data = [obj] formset = table.get_formset() self.assertEqual(len(formset), 1) form = formset[0] form_data = form.initial self.assertEqual(form_data['name'], 'test object') self.assertEqual(form_data['value'], 42) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9458196 watcher_dashboard-16.0.0/watcher_dashboard/tests/unit/0000775000175000017500000000000015257165054021663 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.836848 watcher_dashboard-16.0.0/watcher_dashboard/tests/unit/__init__.py0000664000175000017500000000000015257165015023757 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9468198 watcher_dashboard-16.0.0/watcher_dashboard/tests/unit/api/0000775000175000017500000000000015257165054022434 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.836848 watcher_dashboard-16.0.0/watcher_dashboard/tests/unit/api/__init__.py0000664000175000017500000000000015257165015024530 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.836848 watcher_dashboard-16.0.0/watcher_dashboard/tests/unit/api/test_client.py0000664000175000017500000000617315257165015025327 0ustar00zuulzuul# Copyright 2025, Red Hat, Inc. # All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from watcher_dashboard import api from watcher_dashboard.tests import helpers as test class UnitWatcherClientAPITests(test.APITestCase): def test_audit_create_defaults_to_min_version(self): with mock.patch( 'watcher_dashboard.api.watcher.watcherclient', autospec=True ) as wc: client_mock = mock.Mock() wc.return_value = client_mock client_mock.audit.create = mock.Mock(return_value={}) api.watcher.Audit.create( self.request, audit_template_uuid='tpl', audit_type='continuous', name='n', interval='60', ) wc.assert_called_with(self.request, api_version=None) client_mock.audit.create.assert_called_once() def test_audit_create_passes_explicit_api_version(self): with mock.patch( 'watcher_dashboard.api.watcher.watcherclient', autospec=True ) as wc: client_mock = mock.Mock() wc.return_value = client_mock client_mock.audit.create = mock.Mock(return_value={}) api.watcher.Audit.create( self.request, audit_template_uuid='tpl', audit_type='continuous', name='n', interval='60', start_time='2025-01-01T10:00:00', end_time='2025-01-01T11:00:00', api_version='1.1', ) wc.assert_called_with(self.request, api_version='1.1') client_mock.audit.create.assert_called_once() def test_audit_list_passes_explicit_api_version(self): with mock.patch( 'watcher_dashboard.api.watcher.watcherclient', autospec=True ) as wc: client_mock = mock.Mock() wc.return_value = client_mock client_mock.audit.list = mock.Mock(return_value=[]) api.watcher.Audit.list(self.request, api_version='1.1') wc.assert_called_with(self.request, api_version='1.1') def test_audit_get_passes_explicit_api_version(self): with mock.patch( 'watcher_dashboard.api.watcher.watcherclient', autospec=True ) as wc: client_mock = mock.Mock() wc.return_value = client_mock client_mock.audit.get = mock.Mock(return_value={}) api.watcher.Audit.get( self.request, 'audit-uuid', api_version='1.1' ) wc.assert_called_with(self.request, api_version='1.1') ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.836848 watcher_dashboard-16.0.0/watcher_dashboard/tests/unit/api/test_client_utils.py0000664000175000017500000000501015257165015026534 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from watcher_dashboard.common import client as common_client from watcher_dashboard.tests import helpers as test class MicroversionSupportTests(test.TestCase): def test_is_microversion_supported(self): self.assertTrue(common_client.is_microversion_supported('1.1', '1.0')) self.assertTrue(common_client.is_microversion_supported('1.1', '1.1')) self.assertFalse(common_client.is_microversion_supported('1.0', '1.1')) self.assertFalse(common_client.is_microversion_supported(None, '1.1')) def test_is_microversion_supported_start_end(self): self.assertTrue( common_client.is_microversion_supported( '1.1', common_client.MV_START_END ) ) self.assertTrue( common_client.is_microversion_supported( '1.5', common_client.MV_START_END ) ) self.assertFalse( common_client.is_microversion_supported( '1.0', common_client.MV_START_END ) ) self.assertFalse( common_client.is_microversion_supported( None, common_client.MV_START_END ) ) def test_is_microversion_supported_skip_action(self): self.assertTrue( common_client.is_microversion_supported( '1.5', common_client.MV_SKIP_ACTION ) ) self.assertTrue( common_client.is_microversion_supported( '2.0', common_client.MV_SKIP_ACTION ) ) self.assertFalse( common_client.is_microversion_supported( '1.4', common_client.MV_SKIP_ACTION ) ) self.assertFalse( common_client.is_microversion_supported( '1.1', common_client.MV_SKIP_ACTION ) ) self.assertFalse( common_client.is_microversion_supported( None, common_client.MV_SKIP_ACTION ) ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.836848 watcher_dashboard-16.0.0/watcher_dashboard/tests/unit/api/test_watcher.py0000664000175000017500000005275415257165015025514 0ustar00zuulzuul# Copyright (c) 2016 b<>com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock from watcher_dashboard import api from watcher_dashboard.tests import helpers as test class WatcherAPITests(test.APITestCase): def test_goal_list(self): goals = {'goals': self.api_goals.list()} watcherclient = self.stub_watcherclient() watcherclient.goal.list = mock.Mock(return_value=goals) ret_val = api.watcher.Goal.list(self.request) self.assertIsInstance(ret_val, dict) self.assertIn('goals', ret_val) for n in ret_val['goals']: self.assertIsInstance(n, dict) watcherclient.goal.list.assert_called_with(detail=True) def test_goal_get(self): goal = self.api_goals.first() goal_id = self.api_goals.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.goal.get = mock.Mock(return_value=goal) ret_val = api.watcher.Goal.get(self.request, goal_id) self.assertIsInstance(ret_val, dict) watcherclient.goal.get.assert_called_with(goal_id) def test_strategy_list(self): strategies = {'strategies': self.api_strategies.list()} watcherclient = self.stub_watcherclient() watcherclient.strategy.list = mock.Mock(return_value=strategies) ret_val = api.watcher.Strategy.list(self.request) self.assertIn('strategies', ret_val) for n in ret_val['strategies']: self.assertIsInstance(n, dict) watcherclient.strategy.list.assert_called_with(detail=True) def test_strategy_get(self): strategy = self.api_strategies.first() strategy_id = self.api_strategies.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.strategy.get = mock.Mock(return_value=strategy) ret_val = api.watcher.Strategy.get(self.request, strategy_id) self.assertIsInstance(ret_val, dict) watcherclient.strategy.get.assert_called_with(strategy_id) def test_audit_template_list(self): audit_templates = {'audit_templates': self.api_audit_templates.list()} watcherclient = self.stub_watcherclient() watcherclient.audit_template.list = mock.Mock( return_value=audit_templates ) ret_val = api.watcher.AuditTemplate.list(self.request) self.assertIn('audit_templates', ret_val) for n in ret_val['audit_templates']: self.assertIsInstance(n, dict) watcherclient.audit_template.list.assert_called_with(detail=True) def test_audit_template_list_with_filters(self): search_opts = {'name': 'Audit Template 1'} audit_templates = { 'audit_templates': self.api_audit_templates.filter(**search_opts) } watcherclient = self.stub_watcherclient() watcherclient.audit_template.list = mock.Mock( return_value=audit_templates ) ret_val = api.watcher.AuditTemplate.list(self.request, **search_opts) self.assertIn('audit_templates', ret_val) for n in ret_val['audit_templates']: self.assertIsInstance(n, dict) self.assertEqual(ret_val, audit_templates) watcherclient.audit_template.list.assert_called_with( detail=True, **search_opts ) def test_audit_template_get(self): audit_template = self.api_audit_templates.first() audit_template_id = self.api_audit_templates.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.audit_template.get = mock.Mock( return_value=audit_template ) ret_val = api.watcher.AuditTemplate.get( self.request, audit_template_id ) self.assertIsInstance(ret_val, dict) watcherclient.audit_template.get.assert_called_with( audit_template_id=audit_template_id ) def test_audit_template_create(self): audit_template = self.api_audit_templates.first() name = audit_template['name'] goal = audit_template['goal_uuid'] strategy = audit_template['strategy_uuid'] description = audit_template['description'] scope = audit_template['scope'] watcherclient = self.stub_watcherclient() watcherclient.audit_template.create = mock.Mock( return_value=audit_template ) ret_val = api.watcher.AuditTemplate.create( self.request, name, goal, strategy, description, scope ) self.assertIsInstance(ret_val, dict) watcherclient.audit_template.create.assert_called_with( name=name, goal=goal, strategy=strategy, description=description, scope=scope, ) def test_audit_template_patch(self): audit_template = self.api_audit_templates.first() audit_template_id = self.api_audit_templates.first()['uuid'] form_data = {'name': 'new Audit Template 1'} watcherclient = self.stub_watcherclient() watcherclient.audit_template.patch = mock.Mock( return_value=audit_template ) ret_val = api.watcher.AuditTemplate.patch( self.request, audit_template_id, form_data ) self.assertIsInstance(ret_val, dict) watcherclient.audit_template.patch.assert_called_with( audit_template_id, [{'name': 'name', 'value': 'new Audit Template 1'}], ) def test_audit_template_delete(self): audit_template_list = self.api_audit_templates.list() audit_template_id = self.api_audit_templates.first()['uuid'] deleted_at_list = self.api_audit_templates.delete() watcherclient = self.stub_watcherclient() watcherclient.audit_template.delete = mock.Mock() api.watcher.AuditTemplate.delete(self.request, audit_template_id) self.assertEqual(audit_template_list, deleted_at_list) self.assertEqual(len(audit_template_list), len(deleted_at_list)) watcherclient.audit_template.delete.assert_called_with( audit_template_id=audit_template_id ) def test_audit_list(self): audits = {'audits': self.api_audits.list()} watcherclient = self.stub_watcherclient() watcherclient.audit.list = mock.Mock(return_value=audits) ret_val = api.watcher.Audit.list(self.request) self.assertIn('audits', ret_val) for n in ret_val['audits']: self.assertIsInstance(n, dict) watcherclient.audit.list.assert_called_with(detail=True) def test_audit_get(self): audit = self.api_audits.first() audit_id = self.api_audits.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.audit.get = mock.Mock(return_value=audit) ret_val = api.watcher.Audit.get(self.request, audit_id) self.assertIsInstance(ret_val, dict) watcherclient.audit.get.assert_called_with(audit=audit_id) def test_audit_create(self): audit = self.api_audits.first() audit_template_id = self.api_audit_templates.first()['uuid'] audit_type = self.api_audits.first()['audit_type'] audit_name = self.api_audits.first()['name'] audit_template_uuid = audit_template_id watcherclient = self.stub_watcherclient() watcherclient.audit.create = mock.Mock(return_value=audit) ret_val = api.watcher.Audit.create( self.request, audit_template_uuid, audit_type, audit_name ) self.assertIsInstance(ret_val, dict) watcherclient.audit.create.assert_called_with( audit_template_uuid=audit_template_uuid, audit_type=audit_type, auto_trigger=False, name=audit_name, ) def test_audit_create_with_interval(self): audit = self.api_audits.list()[1] audit_template_id = self.api_audit_templates.first()['uuid'] audit_type = self.api_audits.first()['audit_type'] audit_name = self.api_audits.first()['name'] interval = audit['interval'] audit_template_uuid = audit_template_id watcherclient = self.stub_watcherclient() watcherclient.audit.create = mock.Mock(return_value=audit) ret_val = api.watcher.Audit.create( self.request, audit_template_uuid, audit_type, audit_name, False, interval, ) self.assertIsInstance(ret_val, dict) watcherclient.audit.create.assert_called_with( audit_template_uuid=audit_template_uuid, audit_type=audit_type, auto_trigger=False, interval=interval, name=audit_name, ) def test_audit_create_with_auto_trigger(self): audit = self.api_audits.list()[1] audit_template_id = self.api_audit_templates.first()['uuid'] audit_type = self.api_audits.first()['audit_type'] audit_name = self.api_audits.first()['name'] audit_template_uuid = audit_template_id watcherclient = self.stub_watcherclient() watcherclient.audit.create = mock.Mock(return_value=audit) ret_val = api.watcher.Audit.create( self.request, audit_template_uuid, audit_type, audit_name, True ) self.assertIsInstance(ret_val, dict) watcherclient.audit.create.assert_called_with( audit_template_uuid=audit_template_uuid, audit_type=audit_type, auto_trigger=True, name=audit_name, ) def test_audit_create_with_parameters(self): audit = self.api_audits.first() audit_template_id = self.api_audit_templates.first()['uuid'] audit_type = self.api_audits.first()['audit_type'] audit_name = self.api_audits.first()['name'] audit_template_uuid = audit_template_id parameters = {'memory_threshold': 0.8, 'cpu_threshold': 0.9} watcherclient = self.stub_watcherclient() watcherclient.audit.create = mock.Mock(return_value=audit) ret_val = api.watcher.Audit.create( self.request, audit_template_uuid, audit_type, audit_name, False, None, parameters, ) self.assertIsInstance(ret_val, dict) watcherclient.audit.create.assert_called_with( audit_template_uuid=audit_template_uuid, audit_type=audit_type, auto_trigger=False, name=audit_name, parameters=parameters, ) def test_audit_create_with_start_end_time(self): audit = self.api_audits.list()[1] audit_template_id = self.api_audit_templates.first()['uuid'] audit_type = self.api_audits.first()['audit_type'] audit_name = self.api_audits.first()['name'] interval = audit['interval'] start_time = '2025-01-01 10:00:00' end_time = '2025-01-02 18:30:00' audit_template_uuid = audit_template_id watcherclient = self.stub_watcherclient() watcherclient.audit.create = mock.Mock(return_value=audit) ret_val = api.watcher.Audit.create( self.request, audit_template_uuid, audit_type, audit_name, False, interval, start_time=start_time, end_time=end_time, ) self.assertIsInstance(ret_val, dict) watcherclient.audit.create.assert_called_with( audit_template_uuid=audit_template_uuid, audit_type=audit_type, auto_trigger=False, interval=interval, name=audit_name, start_time=start_time, end_time=end_time, ) def test_audit_create_with_complex_parameters(self): audit = self.api_audits.first() audit_template_id = self.api_audit_templates.first()['uuid'] audit_type = self.api_audits.first()['audit_type'] audit_name = self.api_audits.first()['name'] audit_template_uuid = audit_template_id # Test complex JSON parameters like arrays and objects parameters = { 'memory_threshold': 0.8, 'enable_migration': True, 'compute_nodes': [{'src_node': 's01', 'dst_node': 'd01'}], 'excluded_instances': ['instance1', 'instance2'], } watcherclient = self.stub_watcherclient() watcherclient.audit.create = mock.Mock(return_value=audit) ret_val = api.watcher.Audit.create( self.request, audit_template_uuid, audit_type, audit_name, False, None, parameters, ) self.assertIsInstance(ret_val, dict) watcherclient.audit.create.assert_called_with( audit_template_uuid=audit_template_uuid, audit_type=audit_type, auto_trigger=False, name=audit_name, parameters=parameters, ) def test_audit_show_with_parameters(self): """Test that audit detail view shows parameters when present""" audit = self.api_audits.first() audit_id = audit['uuid'] # Add parameters to the audit data audit_with_params = dict(audit) audit_with_params['parameters'] = { 'memory_threshold': 0.8, 'cpu_threshold': 0.9, } watcherclient = self.stub_watcherclient() watcherclient.audit.get = mock.Mock(return_value=audit_with_params) ret_val = api.watcher.Audit.get(self.request, audit_id) self.assertIsInstance(ret_val, dict) expected_params = {'memory_threshold': 0.8, 'cpu_threshold': 0.9} self.assertEqual(ret_val['parameters'], expected_params) watcherclient.audit.get.assert_called_with(audit=audit_id) def test_audit_delete(self): audit_id = self.api_audits.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.audit.delete = mock.Mock() api.watcher.Audit.delete(self.request, audit_id) watcherclient.audit.delete.assert_called_with(audit=audit_id) def test_audit_cancel(self): audit_id = self.api_audits.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.audit.update = mock.Mock() api.watcher.Audit.cancel(self.request, audit_id) watcherclient.audit.update.assert_called_with( audit=audit_id, patch=[{'op': 'replace', 'path': '/state', 'value': 'CANCELLED'}], ) def test_action_plan_list(self): action_plans = {'action_plans': self.api_action_plans.list()} watcherclient = self.stub_watcherclient() watcherclient.action_plan.list = mock.Mock(return_value=action_plans) ret_val = api.watcher.ActionPlan.list(self.request) self.assertIn('action_plans', ret_val) for n in ret_val['action_plans']: self.assertIsInstance(n, dict) watcherclient.action_plan.list.assert_called_with(detail=True) def test_action_plan_get(self): action_plan = self.api_action_plans.first() action_plan_id = self.api_action_plans.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.action_plan.get = mock.Mock(return_value=action_plan) ret_val = api.watcher.ActionPlan.get(self.request, action_plan_id) self.assertIsInstance(ret_val, dict) watcherclient.action_plan.get.assert_called_with( action_plan_id=action_plan_id ) def test_action_plan_start(self): action_plan_id = self.api_action_plans.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.action_plan.start = mock.Mock() api.watcher.ActionPlan.start(self.request, action_plan_id) watcherclient.action_plan.start.assert_called_with(action_plan_id) def test_action_plan_delete(self): action_plan_id = self.api_action_plans.first()['uuid'] watcherclient = self.stub_watcherclient() watcherclient.action_plan.delete = mock.Mock() api.watcher.ActionPlan.delete(self.request, action_plan_id) watcherclient.action_plan.delete.assert_called_with( action_plan_id=action_plan_id ) def test_action_list(self): actions = {'actions': self.api_actions.list()} watcherclient = self.stub_watcherclient() watcherclient.action.list = mock.Mock(return_value=actions) ret_val = api.watcher.Action.list(self.request) self.assertIn('actions', ret_val) for n in ret_val['actions']: self.assertIsInstance(n, dict) watcherclient.action.list.assert_called_with(detail=True) def test_action_get_with_api_version(self): action = self.api_actions.first() action_id = action['uuid'] watcherclient = self.stub_watcherclient() watcherclient.action.get = mock.Mock(return_value=action) with mock.patch( 'watcher_dashboard.api.watcher.watcherclient', autospec=True ) as wc: wc.return_value = watcherclient api.watcher.Action.get(self.request, action_id, api_version='1.5') wc.assert_called_with(self.request, api_version='1.5') def test_action_update_state_only(self): action_id = self.api_actions.first()['uuid'] with mock.patch( 'watcher_dashboard.api.watcher.watcherclient', autospec=True ) as wc: client_mock = mock.Mock() wc.return_value = client_mock client_mock.action.update = mock.Mock(return_value={}) api.watcher.Action.update( self.request, action_id, state='SKIPPED', api_version='1.5' ) client_mock.action.update.assert_called_with( action_id=action_id, patch=[ {'op': 'replace', 'path': '/state', 'value': 'SKIPPED'} ], ) def test_action_update_state_and_reason(self): action_id = self.api_actions.first()['uuid'] with mock.patch( 'watcher_dashboard.api.watcher.watcherclient', autospec=True ) as wc: client_mock = mock.Mock() wc.return_value = client_mock client_mock.action.update = mock.Mock(return_value={}) api.watcher.Action.update( self.request, action_id, state='SKIPPED', reason='Not needed', api_version='1.5', ) client_mock.action.update.assert_called_with( action_id=action_id, patch=[ {'op': 'replace', 'path': '/state', 'value': 'SKIPPED'}, { 'op': 'replace', 'path': '/status_message', 'value': 'Not needed', }, ], ) def test_action_update_no_args_raises(self): from watcher_dashboard.common import exceptions as watcher_exc action_id = self.api_actions.first()['uuid'] self.assertRaises( watcher_exc.WatcherDashboardException, api.watcher.Action.update, self.request, action_id, ) def test_action_update_method_not_allowed(self): from watcherclient.common.apiclient import exceptions as wc_exc action_id = self.api_actions.first()['uuid'] with mock.patch( 'watcher_dashboard.api.watcher.watcherclient', autospec=True ) as wc: client_mock = mock.Mock() wc.return_value = client_mock client_mock.action.update = mock.Mock( side_effect=wc_exc.MethodNotAllowed() ) result = api.watcher.Action.update( self.request, action_id, state='SKIPPED', api_version='1.5' ) self.assertIsNone(result) def test_get_strategy_display_name(self): strategy = api.watcher.Strategy( { 'uuid': 'test-uuid', 'name': 'zone_migration', 'display_name': 'Zone Migration Strategy', } ) self.assertEqual( api.watcher.get_strategy_display_name(strategy), 'Zone Migration Strategy', ) def test_get_strategy_display_name_fallback(self): strategy = api.watcher.Strategy( {'uuid': 'test-uuid', 'name': 'zone_migration', 'display_name': ''} ) self.assertEqual( api.watcher.get_strategy_display_name(strategy), 'zone_migration' ) def test_get_strategy_display_name_none_fallback(self): strategy = api.watcher.Strategy( { 'uuid': 'test-uuid', 'name': 'zone_migration', 'display_name': None, } ) self.assertEqual( api.watcher.get_strategy_display_name(strategy), 'zone_migration' ) def test_get_strategy_display_name_missing_attrs(self): strategy = api.watcher.Strategy({'uuid': 'test-uuid'}) self.assertRaises( AttributeError, api.watcher.get_strategy_display_name, strategy ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.837848 watcher_dashboard-16.0.0/watcher_dashboard/tests/unit/test_config.py0000664000175000017500000000677415257165015024554 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from django.conf import settings from django.test import TestCase from django.test import override_settings from watcher_dashboard import config from watcher_dashboard.tests.local_fixtures.fixtures import ConfigMemoizedCache class ConfigTests(ConfigMemoizedCache, TestCase): # --- get_ssl_no_verify --- def test_get_ssl_no_verify_default(self): # OPENSTACK_SSL_NO_VERIFY is absent from test settings; expect False. result = config.get_ssl_no_verify() self.assertFalse(result) @override_settings(OPENSTACK_SSL_NO_VERIFY=True) def test_get_ssl_no_verify_true(self): result = config.get_ssl_no_verify() self.assertTrue(result) @override_settings(OPENSTACK_SSL_NO_VERIFY=1) def test_get_ssl_no_verify_coerces_truthy_int(self): result = config.get_ssl_no_verify() self.assertIs(result, True) # --- get_ssl_cacert --- def test_get_ssl_cacert_default(self): # OPENSTACK_SSL_CACERT is absent from test settings; expect None. result = config.get_ssl_cacert() self.assertIsNone(result) @override_settings(OPENSTACK_SSL_CACERT='') def test_get_ssl_cacert_empty_string(self): result = config.get_ssl_cacert() self.assertIsNone(result) @override_settings(OPENSTACK_SSL_CACERT='/etc/ssl/certs/ca.pem') def test_get_ssl_cacert_value(self): result = config.get_ssl_cacert() self.assertEqual(result, '/etc/ssl/certs/ca.pem') @override_settings(OPENSTACK_SSL_CACERT=42) def test_get_ssl_cacert_invalid_type(self): self.assertRaises(TypeError, config.get_ssl_cacert) # --- get_policy_files --- @override_settings(POLICY_FILES={}) def test_get_policy_files_default(self): # POLICY_FILES is an empty dict; function should return {}. result = config.get_policy_files() self.assertEqual(result, {}) @override_settings(POLICY_FILES='not-a-dict') def test_get_policy_files_invalid_type(self): self.assertRaises(TypeError, config.get_policy_files) # --- set_policy_file --- @override_settings(POLICY_FILES={}) def test_set_policy_file(self): config.set_policy_file('infra-optim', 'watcher_policy.yaml') result = config.get_policy_files() self.assertEqual(result.get('infra-optim'), 'watcher_policy.yaml') @override_settings(POLICY_FILES={'other': 'other.yaml'}) def test_set_policy_file_shallow_copy(self): before = settings.POLICY_FILES config.set_policy_file('infra-optim', 'watcher_policy.yaml') self.assertEqual(before, {'other': 'other.yaml'}) self.assertEqual( config.get_policy_files().get('infra-optim'), 'watcher_policy.yaml' ) def test_set_policy_file_empty_service(self): self.assertRaises(ValueError, config.set_policy_file, '', 'some.yaml') def test_set_policy_file_empty_filename(self): self.assertRaises(ValueError, config.set_policy_file, 'svc', '') ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.837848 watcher_dashboard-16.0.0/watcher_dashboard/tests/urls.py0000664000175000017500000000134415257165015022242 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import openstack_dashboard.urls from django.conf import urls from django.urls import re_path urlpatterns = [re_path(r'', urls.include(openstack_dashboard.urls))] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9468198 watcher_dashboard-16.0.0/watcher_dashboard/utils/0000775000175000017500000000000015257165054020702 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.837848 watcher_dashboard-16.0.0/watcher_dashboard/utils/__init__.py0000664000175000017500000000000015257165015022776 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.837848 watcher_dashboard-16.0.0/watcher_dashboard/utils/errors.py0000664000175000017500000000571515257165015022575 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import functools import inspect import horizon.exceptions def handle_errors(error_message, error_default=None, request_arg=None): """A decorator for adding default error handling to API calls. It wraps the original method in a try-except block, with horizon's error handling added. Note: it should only be used on functions or methods that take request as their argument (it has to be named "request", or ``request_arg`` has to be provided, indicating which argument is the request). The decorated method accepts a number of additional parameters: :param _error_handle: whether to handle the errors in this call :param _error_message: override the error message :param _error_default: override the default value returned on error :param _error_redirect: specify a redirect url for errors :param _error_ignore: ignore known errors """ def decorator(func): # XXX This is an ugly hack for finding the 'request' argument. if request_arg is None: for _request_arg, name in enumerate( inspect.getfullargspec(func).args ): if name == 'request': break else: raise RuntimeError( "The handle_errors decorator requires 'request' as " "an argument of the function or method being decorated" ) else: _request_arg = request_arg @functools.wraps(func) def wrapper(*args, **kwargs): _error_handle = kwargs.pop('_error_handle', True) _error_message = kwargs.pop('_error_message', error_message) _error_default = kwargs.pop('_error_default', error_default) _error_redirect = kwargs.pop('_error_redirect', None) _error_ignore = kwargs.pop('_error_ignore', False) if not _error_handle: return func(*args, **kwargs) try: return func(*args, **kwargs) except Exception: request = args[_request_arg] horizon.exceptions.handle( request, _error_message, ignore=_error_ignore, redirect=_error_redirect, ) return _error_default wrapper.wrapped = func return wrapper return decorator ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.837848 watcher_dashboard-16.0.0/watcher_dashboard/utils/tests.py0000664000175000017500000000556515257165015022426 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections from watcher_dashboard.tests import helpers from watcher_dashboard.utils import utils class UtilsTests(helpers.TestCase): def test_de_camel_case(self): ret = utils.de_camel_case('CamelCaseString') self.assertEqual(ret, 'Camel Case String') ret = utils.de_camel_case('SecureSSLConnection') self.assertEqual(ret, 'Secure SSL Connection') ret = utils.de_camel_case('xxXXxx') self.assertEqual(ret, 'xx X Xxx') ret = utils.de_camel_case('XXX') self.assertEqual(ret, 'XXX') ret = utils.de_camel_case('NON Camel Case') self.assertEqual(ret, 'NON Camel Case') def test_list_to_dict(self): Item = collections.namedtuple('Item', 'id') ret = utils.list_to_dict([Item('foo'), Item('bar'), Item('bar')]) self.assertEqual(ret, {'foo': Item('foo'), 'bar': Item('bar')}) def test_length(self): ret = utils.length(iter([])) self.assertEqual(ret, 0) ret = utils.length(iter([1, 2, 3])) self.assertEqual(ret, 3) def test_check_image_type(self): Image = collections.namedtuple('Image', 'properties') ret = utils.check_image_type(Image({'type': 'Picasso'}), 'Picasso') self.assertTrue(ret) ret = utils.check_image_type(Image({'type': 'Picasso'}), 'Van Gogh') self.assertFalse(ret) ret = utils.check_image_type(Image({}), 'Van Gogh') self.assertTrue(ret) def test_filter_items(self): Item = collections.namedtuple('Item', 'index') items = [Item(i) for i in range(7)] ret = list(utils.filter_items(items, index=1)) self.assertEqual(ret, [Item(1)]) ret = list(utils.filter_items(items, index__in=(1, 2, 3))) self.assertEqual(ret, [Item(1), Item(2), Item(3)]) ret = list(utils.filter_items(items, index__not_in=(1, 2, 3))) self.assertEqual(ret, [Item(0), Item(4), Item(5), Item(6)]) def test_safe_int_cast(self): ret = utils.safe_int_cast(1) self.assertEqual(ret, 1) ret = utils.safe_int_cast('1') self.assertEqual(ret, 1) ret = utils.safe_int_cast('') self.assertEqual(ret, 0) ret = utils.safe_int_cast(None) self.assertEqual(ret, 0) ret = utils.safe_int_cast(object()) self.assertEqual(ret, 0) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.837848 watcher_dashboard-16.0.0/watcher_dashboard/utils/utils.py0000664000175000017500000000570715257165015022422 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re CAMEL_RE = re.compile(r'([A-Z][a-z]+|[A-Z]+(?=[A-Z\s]|$))') def de_camel_case(text): """Convert CamelCase names to human-readable format.""" return ' '.join(w.strip() for w in CAMEL_RE.split(text) if w.strip()) def list_to_dict(object_list, key_attribute='id'): """Converts an object list to a dict :param object_list: list of objects to be put into a dict :type object_list: list :param key_attribute: object attribute used as index by dict :type key_attribute: str :return: dict containing the objects in the list :rtype: dict """ return dict((getattr(o, key_attribute), o) for o in object_list) def length(iterator): """A length function for iterators Returns the number of items in the specified iterator. Note that this function consumes the iterator in the process. """ return sum(1 for _item in iterator) def check_image_type(image, image_type): """Check if image 'type' property matches passed-in image_type. If image has no 'type' property' return True, as we cannot be sure what type of image it is. """ return image.properties.get('type', image_type) == image_type def filter_items(items, **kwargs): """Filters the list of items and returns the filtered list. Example usage: >>> class Item(object): ... def __init__(self, index): ... self.index = index ... ... def __repr__(self): ... return '' % self.index >>> items = [Item(i) for i in range(7)] >>> list(filter_items(items, index=1)) [] >>> list(filter_items(items, index__in=(1, 2, 3))) [, , ] >>> list(filter_items(items, index__not_in=(1, 2, 3))) [, , , ] """ for item in items: for name, value in kwargs.items(): if name.endswith('__in'): if getattr(item, name[: -len('__in')]) not in value: break elif name.endswith('__not_in'): if getattr(item, name[: -len('__not_in')]) in value: break else: if getattr(item, name) != value: break else: yield item def safe_int_cast(value): try: return int(value) except (TypeError, ValueError): return 0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1790765580.837848 watcher_dashboard-16.0.0/watcher_dashboard/version.py0000664000175000017500000000135515257165015021602 0ustar00zuulzuul# Copyright 2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from pbr import version version_info = version.VersionInfo('watcher_dashboard') __version__ = version_info.cached_version_string() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9368198 watcher_dashboard-16.0.0/watcher_dashboard.egg-info/0000775000175000017500000000000015257165054021234 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9018197 watcher_dashboard-16.0.0/watcher_dashboard.egg-info/PKG-INFO0000644000175000017500000000376615257165054022343 0ustar00zuulzuulMetadata-Version: 2.4 Name: watcher-dashboard Version: 16.0.0 Summary: Watcher Management Dashboard Author-email: OpenStack License: Apache-2.0 Project-URL: Homepage, https://docs.openstack.org/watcher-dashboard/latest/ Project-URL: Repository, https://opendev.org/openstack/watcher-dashboard Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: pbr!=2.1.0,>=2.0.0 Requires-Dist: horizon>=18.2.0 Requires-Dist: oslo.utils>=8.0.0 Requires-Dist: PyYAML>=3.12 Requires-Dist: python-watcherclient>=4.9.0 Dynamic: license-file Dynamic: requires-dist ============================================== OpenStack Dashboard plugin for Watcher project ============================================== .. image:: https://governance.openstack.org/tc/badges/watcher-dashboard.svg .. Change things from this point on The Watcher dashboard is a Horizon plugin that will allow users to realize a wide range of cloud optimization goals. * Free software: Apache license * Documentation: https://docs.openstack.org/watcher-dashboard/latest * Source: https://opendev.org/openstack/watcher-dashboard * Bugs: https://bugs.launchpad.net/watcher-dashboard * Release Notes: https://docs.openstack.org/releasenotes/watcher-dashboard * Blueprints: https://blueprints.launchpad.net/watcher-dashboard ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9238198 watcher_dashboard-16.0.0/watcher_dashboard.egg-info/SOURCES.txt0000664000175000017500000002263415257165054023127 0ustar00zuulzuul.pre-commit-config.yaml .stestr.conf .zuul.yaml AUTHORS ChangeLog HACKING.rst LICENSE README.rst babel-django.cfg babel-djangojs.cfg manage.py pyproject.toml requirements.txt run_tests.sh setup.cfg setup.py test-requirements.txt tox.ini devstack/local.conf.example devstack/plugin.sh devstack/settings doc/dictionary.txt doc/requirements.txt doc/source/conf.py doc/source/index.rst doc/source/contributor/contributing.rst doc/source/contributor/index.rst doc/source/contributor/playwright-testing.rst doc/source/install/index.rst doc/source/install/installation.rst playbooks/playwright/post.yaml playbooks/playwright/pre.yaml playbooks/playwright/run.yaml releasenotes/notes/.placeholder releasenotes/notes/audit-parameters-support-0a1e3b1e9d9f.yaml releasenotes/notes/continuous-audit-start-end-time.yaml releasenotes/notes/drop-py-2-7-198cca7f72d16655.yaml releasenotes/notes/drop-py39-8a9c99678b3e8eeb.yaml releasenotes/notes/filter-strategies-by-goal-in-create-template-9c7b1a3e9b5a.yaml releasenotes/notes/fix-actions-table-display-choices-a3b2c1d4e5f6.yaml releasenotes/notes/fix-audit-list-filter-b2161426.yaml releasenotes/notes/show-strategy-parameters-spec-2121648.yaml releasenotes/notes/skip-actions-option-a1b2c3d4e5f6.yaml releasenotes/source/2023.1.rst releasenotes/source/2023.2.rst releasenotes/source/2024.1.rst releasenotes/source/2024.2.rst releasenotes/source/2025.1.rst releasenotes/source/2025.2.rst releasenotes/source/2026.1.rst releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/wallaby.rst releasenotes/source/xena.rst releasenotes/source/yoga.rst releasenotes/source/zed.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po releasenotes/source/locale/ko_KR/LC_MESSAGES/releasenotes.po tools/install_venv.py tools/install_venv_common.py tools/register_plugin.sh tools/with_venv.sh watcher_dashboard/__init__.py watcher_dashboard/config.py watcher_dashboard/version.py watcher_dashboard.egg-info/PKG-INFO watcher_dashboard.egg-info/SOURCES.txt watcher_dashboard.egg-info/dependency_links.txt watcher_dashboard.egg-info/not-zip-safe watcher_dashboard.egg-info/pbr.json watcher_dashboard.egg-info/requires.txt watcher_dashboard.egg-info/top_level.txt watcher_dashboard/api/__init__.py watcher_dashboard/api/watcher.py watcher_dashboard/common/__init__.py watcher_dashboard/common/client.py watcher_dashboard/common/exceptions.py watcher_dashboard/conf/watcher_policy.yaml watcher_dashboard/content/__init__.py watcher_dashboard/content/action_plans/__init__.py watcher_dashboard/content/action_plans/panel.py watcher_dashboard/content/action_plans/tables.py watcher_dashboard/content/action_plans/tabs.py watcher_dashboard/content/action_plans/urls.py watcher_dashboard/content/action_plans/views.py watcher_dashboard/content/actions/__init__.py watcher_dashboard/content/actions/forms.py watcher_dashboard/content/actions/panel.py watcher_dashboard/content/actions/tables.py watcher_dashboard/content/actions/tabs.py watcher_dashboard/content/actions/urls.py watcher_dashboard/content/actions/views.py watcher_dashboard/content/audit_templates/__init__.py watcher_dashboard/content/audit_templates/forms.py watcher_dashboard/content/audit_templates/panel.py watcher_dashboard/content/audit_templates/tables.py watcher_dashboard/content/audit_templates/tabs.py watcher_dashboard/content/audit_templates/tests.py watcher_dashboard/content/audit_templates/urls.py watcher_dashboard/content/audit_templates/views.py watcher_dashboard/content/audits/__init__.py watcher_dashboard/content/audits/forms.py watcher_dashboard/content/audits/panel.py watcher_dashboard/content/audits/tables.py watcher_dashboard/content/audits/tabs.py watcher_dashboard/content/audits/urls.py watcher_dashboard/content/audits/views.py watcher_dashboard/content/goals/__init__.py watcher_dashboard/content/goals/panel.py watcher_dashboard/content/goals/tables.py watcher_dashboard/content/goals/tabs.py watcher_dashboard/content/goals/tests.py watcher_dashboard/content/goals/urls.py watcher_dashboard/content/goals/views.py watcher_dashboard/content/strategies/__init__.py watcher_dashboard/content/strategies/forms.py watcher_dashboard/content/strategies/panel.py watcher_dashboard/content/strategies/tables.py watcher_dashboard/content/strategies/tabs.py watcher_dashboard/content/strategies/tests.py watcher_dashboard/content/strategies/urls.py watcher_dashboard/content/strategies/views.py watcher_dashboard/local/__init__.py watcher_dashboard/local/enabled/_31000_goals_panel.py watcher_dashboard/local/enabled/_31010_strategies_panel.py watcher_dashboard/local/enabled/_31020_watcher_panelgroup.py watcher_dashboard/local/enabled/_31030_audit_templates_panel.py watcher_dashboard/local/enabled/_31040_audits_panel.py watcher_dashboard/local/enabled/_31050_action_plans_panel.py watcher_dashboard/local/enabled/_31060_actions_panel.py watcher_dashboard/local/enabled/__init__.py watcher_dashboard/locale/.placeholder watcher_dashboard/static/infra_optim/images/chevron.png watcher_dashboard/static/infra_optim/images/power.png watcher_dashboard/static/infra_optim/scss/infra_optim.scss watcher_dashboard/static/infra_optim/tests/formset_table.js watcher_dashboard/templates/client_side/_modal_chart.html watcher_dashboard/templates/client_side/templates.html watcher_dashboard/templates/formset_table/_row.html watcher_dashboard/templates/formset_table/_table.html watcher_dashboard/templates/formset_table/menu_formset.html watcher_dashboard/templates/horizon/common/_items_count_domain_page_header.html watcher_dashboard/templates/infra_optim/_fullscreen_workflow.html watcher_dashboard/templates/infra_optim/_fullscreen_workflow_base.html watcher_dashboard/templates/infra_optim/_modal_form_xl.html watcher_dashboard/templates/infra_optim/_modal_xl.html watcher_dashboard/templates/infra_optim/_performance_chart.html watcher_dashboard/templates/infra_optim/_performance_chart_box.html watcher_dashboard/templates/infra_optim/_top_5_box.html watcher_dashboard/templates/infra_optim/_top_5_chart.html watcher_dashboard/templates/infra_optim/_workflow_base.html watcher_dashboard/templates/infra_optim/base.html watcher_dashboard/templates/infra_optim/base_detail.html watcher_dashboard/templates/infra_optim/header_actions.html watcher_dashboard/templates/infra_optim/qunit.html watcher_dashboard/templates/infra_optim/action_plans/_details_overview.html watcher_dashboard/templates/infra_optim/action_plans/_global_efficacy.html watcher_dashboard/templates/infra_optim/action_plans/create.html watcher_dashboard/templates/infra_optim/action_plans/details.html watcher_dashboard/templates/infra_optim/action_plans/index.html watcher_dashboard/templates/infra_optim/actions/_skip.html watcher_dashboard/templates/infra_optim/actions/details.html watcher_dashboard/templates/infra_optim/actions/index.html watcher_dashboard/templates/infra_optim/actions/skip.html watcher_dashboard/templates/infra_optim/actions_history/details.html watcher_dashboard/templates/infra_optim/actions_history/index.html watcher_dashboard/templates/infra_optim/audit_templates/_create.html watcher_dashboard/templates/infra_optim/audit_templates/create.html watcher_dashboard/templates/infra_optim/audit_templates/details.html watcher_dashboard/templates/infra_optim/audit_templates/index.html watcher_dashboard/templates/infra_optim/audits/_create.html watcher_dashboard/templates/infra_optim/audits/create.html watcher_dashboard/templates/infra_optim/audits/details.html watcher_dashboard/templates/infra_optim/audits/index.html watcher_dashboard/templates/infra_optim/goals/details.html watcher_dashboard/templates/infra_optim/goals/index.html watcher_dashboard/templates/infra_optim/logs/index.html watcher_dashboard/templates/infra_optim/strategies/details.html watcher_dashboard/templates/infra_optim/strategies/index.html watcher_dashboard/templatetags/__init__.py watcher_dashboard/test/integration/__init__.py watcher_dashboard/test/integration/playwright_base.py watcher_dashboard/test/integration/playwright_config.py watcher_dashboard/test/integration/test_existing_goals.py watcher_dashboard/test/integration/test_existing_strategies.py watcher_dashboard/test/integration/test_playwright_audit_workflow.py watcher_dashboard/test/integration/test_playwright_skip_action_workflow.py watcher_dashboard/tests/__init__.py watcher_dashboard/tests/helpers.py watcher_dashboard/tests/settings.py watcher_dashboard/tests/test_formset_table.py watcher_dashboard/tests/urls.py watcher_dashboard/tests/local_fixtures/__init__.py watcher_dashboard/tests/local_fixtures/fixtures.py watcher_dashboard/tests/local_fixtures/logging_fixture.py watcher_dashboard/tests/test_data/__init__.py watcher_dashboard/tests/test_data/exceptions.py watcher_dashboard/tests/test_data/utils.py watcher_dashboard/tests/test_data/watcher_data.py watcher_dashboard/tests/unit/__init__.py watcher_dashboard/tests/unit/test_config.py watcher_dashboard/tests/unit/api/__init__.py watcher_dashboard/tests/unit/api/test_client.py watcher_dashboard/tests/unit/api/test_client_utils.py watcher_dashboard/tests/unit/api/test_watcher.py watcher_dashboard/utils/__init__.py watcher_dashboard/utils/errors.py watcher_dashboard/utils/tests.py watcher_dashboard/utils/utils.py././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9018197 watcher_dashboard-16.0.0/watcher_dashboard.egg-info/dependency_links.txt0000664000175000017500000000000115257165054025302 0ustar00zuulzuul ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9018197 watcher_dashboard-16.0.0/watcher_dashboard.egg-info/not-zip-safe0000664000175000017500000000000115257165054023462 0ustar00zuulzuul ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9018197 watcher_dashboard-16.0.0/watcher_dashboard.egg-info/pbr.json0000664000175000017500000000005615257165054022713 0ustar00zuulzuul{"git_version": "6c6d80a", "is_release": true}././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9018197 watcher_dashboard-16.0.0/watcher_dashboard.egg-info/requires.txt0000664000175000017500000000013615257165054023634 0ustar00zuulzuulpbr!=2.1.0,>=2.0.0 horizon>=18.2.0 oslo.utils>=8.0.0 PyYAML>=3.12 python-watcherclient>=4.9.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1790765611.9028196 watcher_dashboard-16.0.0/watcher_dashboard.egg-info/top_level.txt0000664000175000017500000000002215257165054023760 0ustar00zuulzuulwatcher_dashboard