pax_global_header00006660000000000000000000000064151046131500014506gustar00rootroot0000000000000052 comment=aaebb77ea7da21c01d8995784c2ba62b73643874 watcher-dashboard-14.0.0+git20251111.18.aaebb77/000077500000000000000000000000001510461315000202045ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/.gitignore000066400000000000000000000012151510461315000221730ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build .eggs eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage cover/ .tox nosetests.xml .testrepository .venv node_modules # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Complexity output/*.html output/*/index.html # Sphinx doc/build # pbr generates these AUTHORS ChangeLog # Editors *~ .*.swp .*sw? # Horizon related *.lock watcher_dashboard/test/.secret_key_store # Files created by releasenotes build releasenotes/build # DS_Store on Mac computers *.DS_Store watcher-dashboard-14.0.0+git20251111.18.aaebb77/.gitreview000066400000000000000000000001241510461315000222070ustar00rootroot00000000000000[gerrit] host=review.opendev.org port=29418 project=openstack/watcher-dashboard.git watcher-dashboard-14.0.0+git20251111.18.aaebb77/.pre-commit-config.yaml000066400000000000000000000033511510461315000244670ustar00rootroot00000000000000--- repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: # whitespace - id: trailing-whitespace - id: mixed-line-ending args: ['--fix', 'lf'] exclude: '.*\.(svg)$' - id: check-byte-order-marker # file format and permissions - id: check-ast - 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://github.com/Lucas-C/pre-commit-hooks rev: v1.5.5 hooks: - id: remove-tabs exclude: '.*\.(svg)$' - repo: https://opendev.org/openstack/hacking rev: 7.0.0 hooks: - id: hacking additional_dependencies: [] exclude: '^(doc|releasenotes|tools)/.*$' - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.12.1 hooks: - id: ruff-check args: ['--fix', '--unsafe-fixes'] - repo: https://github.com/hhatto/autopep8 rev: v2.3.2 hooks: - id: autopep8 files: '^.*\.py$' - 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.0 hooks: - id: sphinx-lint args: [--enable=default-role] files: ^doc/|^releasenotes/|^api-guide/ types: [rst] - repo: https://github.com/PyCQA/doc8 rev: v1.1.2 hooks: - id: doc8 watcher-dashboard-14.0.0+git20251111.18.aaebb77/.zuul.yaml000066400000000000000000000003471510461315000221510ustar00rootroot00000000000000- project: templates: - check-requirements - horizon-non-primary-django-jobs - openstack-python3-jobs-horizon - openstack-cover-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 watcher-dashboard-14.0.0+git20251111.18.aaebb77/HACKING.rst000066400000000000000000000044551510461315000220120ustar00rootroot00000000000000Contributing ============ 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/LICENSE000066400000000000000000000236371510461315000212240ustar00rootroot00000000000000 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. watcher-dashboard-14.0.0+git20251111.18.aaebb77/README.rst000066400000000000000000000013261510461315000216750ustar00rootroot00000000000000============================================== 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/babel-django.cfg000066400000000000000000000001231510461315000231660ustar00rootroot00000000000000[python: watcher_dashboard/**.py] [django: watcher_dashboard/**/templates/**.html] watcher-dashboard-14.0.0+git20251111.18.aaebb77/babel-djangojs.cfg000066400000000000000000000001251510461315000235250ustar00rootroot00000000000000[javascript: watcher_dashboard/**.js] [angular: watcher_dashboard/**/static/**.html] watcher-dashboard-14.0.0+git20251111.18.aaebb77/devstack/000077500000000000000000000000001510461315000220105ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/devstack/local.conf.example000066400000000000000000000001751510461315000254060ustar00rootroot00000000000000# settings file for watcher-dashboard plugin enable_plugin watcher-dashboard https://opendev.org/openstack/watcher-dashboard watcher-dashboard-14.0.0+git20251111.18.aaebb77/devstack/plugin.sh000066400000000000000000000034131510461315000236430ustar00rootroot00000000000000# 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 # no-op : 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/devstack/settings000066400000000000000000000001321510461315000235670ustar00rootroot00000000000000# DevStack settings # Enable Watcher dashboard services enable_service watcher-dashboard watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/000077500000000000000000000000001510461315000207515ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/dictionary.txt000066400000000000000000000000001510461315000236450ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/requirements.txt000066400000000000000000000001031510461315000242270ustar00rootroot00000000000000openstackdocstheme>=2.2.1 # Apache-2.0 sphinx>=2.0.0,!=2.1.0 # BSD watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/source/000077500000000000000000000000001510461315000222515ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/source/conf.py000066400000000000000000000076131510461315000235570ustar00rootroot00000000000000# 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 = '' watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/source/contributor/000077500000000000000000000000001510461315000246235ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/source/contributor/contributing.rst000066400000000000000000000040661510461315000300720ustar00rootroot00000000000000.. 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/ watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/source/contributor/index.rst000066400000000000000000000000561510461315000264650ustar00rootroot00000000000000.. toctree:: :maxdepth: 1 contributing watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/source/index.rst000066400000000000000000000034221510461315000241130ustar00rootroot00000000000000.. 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` watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/source/install/000077500000000000000000000000001510461315000237175ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/source/install/index.rst000066400000000000000000000000561510461315000255610ustar00rootroot00000000000000.. toctree:: :maxdepth: 1 installation watcher-dashboard-14.0.0+git20251111.18.aaebb77/doc/source/install/installation.rst000066400000000000000000000106271510461315000271600ustar00rootroot00000000000000Installation ------------ 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 Integration testing ------------------- Before being able to run integration tests, you need to have a Horizon server running with Watcher Dashboard plugin configured. To do so, you can run a test server using the following command:: $ ./run_tests.sh --runserver 0.0.0.0:8000 By default, integration tests expect to find a running Horizon server at ``http://localhost:8000/`` but this can be customized by editing the ``watcher_dashboard/test/integration_tests/horizon.conf`` configuration file. Likewise, this Horizon will be looking, by default, for a Keystone backend at ``http://localhost:5000/v2.0``. So in order to customize its location, you will have to edit ``watcher_dashboard/test/settings.py`` by updating the ``OPENSTACK_KEYSTONE_URL`` variable. To run integration tests:: $ ./run_tests.sh --integration You can use PhantomJS as a headless browser to execute your integration tests. On an Ubuntu distribution you can install it via the following command:: $ sudo apt-get install phantomjs Then you can run your integration tests like this:: $ ./run_tests.sh --integration --selenium-headless Please note that these commands are also available via ``tox``. .. 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 POLICY_FILES = { ... 'infra-optim': 'watcher_policy.json', } 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/manage.py000077500000000000000000000015011510461315000220060ustar00rootroot00000000000000#!/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.test.settings") execute_from_command_line(sys.argv) watcher-dashboard-14.0.0+git20251111.18.aaebb77/pyproject.toml000066400000000000000000000014701510461315000231220ustar00rootroot00000000000000[build-system] requires = ["pbr>=6.0.0", "setuptools>=64.0.0"] build-backend = "pbr.build" [tool.ruff] line-length = 79 target-version = "py310" [tool.ruff.lint] select = ["E4", "E7", "E9", "F", "S", "U", "W", "C90"] ignore = [ # we only use asserts for type narrowing "S101", # we do not use random number geneerators for crypto "S311", # S104 Possible binding to all interfaces "S104", # S105 Possible hardcoded password assigned to variable" "S105", # S106 Possible hardcoded password assigned to argument "S106", # S110 `try`-`except`-`pass` detected, consider logging the exception "S110", ] [tool.ruff.lint.per-file-ignores] "watcher_dashboard/tests/*" = ["S"] [tool.ruff.lint.mccabe] # Flag errors (`C901`) whenever the complexity level exceeds 5. max-complexity = 20watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/000077500000000000000000000000001510461315000226755ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/notes/000077500000000000000000000000001510461315000240255ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/notes/.placeholder000066400000000000000000000000001510461315000262760ustar00rootroot00000000000000audit-parameters-support-0a1e3b1e9d9f.yaml000066400000000000000000000014301510461315000335040ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/notes--- 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.continuous-audit-start-end-time.yaml000066400000000000000000000007331510461315000330020ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/notes--- 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. watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/notes/drop-py-2-7-198cca7f72d16655.yaml000066400000000000000000000003331510461315000307540ustar00rootroot00000000000000--- 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. watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/notes/drop-py39-8a9c99678b3e8eeb.yaml000066400000000000000000000003141510461315000310040ustar00rootroot00000000000000--- 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. filter-strategies-by-goal-in-create-template-9c7b1a3e9b5a.yaml000066400000000000000000000007511510461315000371750ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/notes--- 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. show-strategy-parameters-spec-2121648.yaml000066400000000000000000000001271510461315000333700ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/notes--- fixes: - | Display the strategy Parameters spec on the Strategy Details page watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/000077500000000000000000000000001510461315000241755ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/2023.1.rst000066400000000000000000000002101510461315000254450ustar00rootroot00000000000000=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2023.1 watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/2023.2.rst000066400000000000000000000002021510461315000254470ustar00rootroot00000000000000=========================== 2023.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.2 watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/2024.1.rst000066400000000000000000000002021510461315000254470ustar00rootroot00000000000000=========================== 2024.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.1 watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/2024.2.rst000066400000000000000000000002021510461315000254500ustar00rootroot00000000000000=========================== 2024.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.2 watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/2025.1.rst000066400000000000000000000002021510461315000254500ustar00rootroot00000000000000=========================== 2025.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.1 watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/2025.2.rst000066400000000000000000000002021510461315000254510ustar00rootroot00000000000000=========================== 2025.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.2 watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/_static/000077500000000000000000000000001510461315000256235ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/_static/.placeholder000066400000000000000000000000001510461315000300740ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/_templates/000077500000000000000000000000001510461315000263325ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/_templates/.placeholder000066400000000000000000000000001510461315000306030ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/conf.py000066400000000000000000000217561510461315000255070ustar00rootroot00000000000000# 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/'] watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/index.rst000066400000000000000000000004671510461315000260450ustar00rootroot00000000000000=============================== Watcher Dashboard Release Notes =============================== .. toctree:: :maxdepth: 1 unreleased 2025.2 2025.1 2024.2 2024.1 2023.2 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky queens pike ocata watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/locale/000077500000000000000000000000001510461315000254345ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/locale/en_GB/000077500000000000000000000000001510461315000264065ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/locale/en_GB/LC_MESSAGES/000077500000000000000000000000001510461315000301735ustar00rootroot00000000000000releasenotes.po000066400000000000000000000017731510461315000331550ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/locale/en_GB/LC_MESSAGES# 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" watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/locale/ko_KR/000077500000000000000000000000001510461315000264415ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/locale/ko_KR/LC_MESSAGES/000077500000000000000000000000001510461315000302265ustar00rootroot00000000000000releasenotes.po000066400000000000000000000012651510461315000332040ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/locale/ko_KR/LC_MESSAGES# 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 ě‹śë¦¬ě¦ ë¦´ë¦¬ě¦ ë…¸íŠ¸" watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/ocata.rst000066400000000000000000000002301510461315000260110ustar00rootroot00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/pike.rst000066400000000000000000000002171510461315000256570ustar00rootroot00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/queens.rst000066400000000000000000000002231510461315000262240ustar00rootroot00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/rocky.rst000066400000000000000000000002211510461315000260510ustar00rootroot00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/stein.rst000066400000000000000000000002211510461315000260440ustar00rootroot00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/train.rst000066400000000000000000000001761510461315000260500ustar00rootroot00000000000000========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/unreleased.rst000066400000000000000000000001601510461315000270530ustar00rootroot00000000000000============================== Current Series Release Notes ============================== .. release-notes:: watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/ussuri.rst000066400000000000000000000002021510461315000262530ustar00rootroot00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/victoria.rst000066400000000000000000000002201510461315000265410ustar00rootroot00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: unmaintained/victoria watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/wallaby.rst000066400000000000000000000002141510461315000263570ustar00rootroot00000000000000============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: unmaintained/wallaby watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/xena.rst000066400000000000000000000002001510461315000256520ustar00rootroot00000000000000========================= Xena Series Release Notes ========================= .. release-notes:: :branch: unmaintained/xena watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/yoga.rst000066400000000000000000000002001510461315000256560ustar00rootroot00000000000000========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: unmaintained/yoga watcher-dashboard-14.0.0+git20251111.18.aaebb77/releasenotes/source/zed.rst000066400000000000000000000001741510461315000255130ustar00rootroot00000000000000======================== Zed Series Release Notes ======================== .. release-notes:: :branch: unmaintained/zed watcher-dashboard-14.0.0+git20251111.18.aaebb77/requirements.txt000066400000000000000000000005351510461315000234730ustar00rootroot00000000000000# 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 PyYAML>=3.12 # MIT python-watcherclient>=1.1.0 # Apache-2.0 watcher-dashboard-14.0.0+git20251111.18.aaebb77/run_tests.sh000077500000000000000000000431361510461315000226000ustar00rootroot00000000000000#!/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 " --only-selenium Run only the Selenium unit tests" echo " --with-selenium Run unit tests including Selenium tests" echo " --selenium-headless Run Selenium tests headless" echo " --selenium-phantomjs Run Selenium tests using phantomjs (headless)" echo " --integration Run the integration tests (requires a running " echo " OpenStack environment)" 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 only_selenium=0 with_selenium=0 selenium_headless=0 selenium_phantomjs=0 integration=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;; --only-selenium) only_selenium=1;; --with-selenium) with_selenium=1;; --selenium-headless) selenium_headless=1;; --selenium-phantomjs) selenium_phantomjs=1;; --integration) integration=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.test.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.test.settings ${command_wrapper} flake8 --diff exit } function run_sphinx { echo "Building sphinx..." DJANGO_SETTINGS_MODULE=watcher_dashboard.test.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 [ $with_selenium -eq 1 ]; then export WITH_SELENIUM=1 elif [ $only_selenium -eq 1 ]; then export WITH_SELENIUM=1 export SKIP_UNITTESTS=1 fi if [ $with_selenium -eq 0 -a $integration -eq 0 ]; then testopts="$testopts --exclude=watcher_dashboard/test/integration_tests/ " fi if [ $selenium_headless -eq 1 ]; then export SELENIUM_HEADLESS=1 fi if [ $selenium_phantomjs -eq 1 ]; then export SELENIUM_PHANTOMJS=1 fi 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.test.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 ] && [ $only_selenium -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 run_integration_tests { export INTEGRATION_TESTS=1 if [ $selenium_headless -eq 1 ]; then export SELENIUM_HEADLESS=1 fi if [ $selenium_phantomjs -eq 1 ]; then export SELENIUM_PHANTOMJS=1 fi echo "Running Watcher Horizon integration tests..." if [ -z "$testargs" ]; then ${command_wrapper} nosetests watcher_dashboard/test/integration_tests/tests else ${command_wrapper} nosetests $testargs fi exit 0 } 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 # Integration tests if [ $integration -eq 1 ]; then run_integration_tests exit $? fi # Django development server if [ $runserver -eq 1 ]; then run_server exit $? fi # Full test suite run_tests || exit watcher-dashboard-14.0.0+git20251111.18.aaebb77/setup.cfg000066400000000000000000000020131510461315000220210ustar00rootroot00000000000000[metadata] name = watcher-dashboard summary = Watcher Management Dashboard description_file = README.rst author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/watcher-dashboard/latest python_requires = >=3.10 classifier = Development Status :: 5 - Production/Stable Environment :: OpenStack Framework :: Django Intended Audience :: Developers Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: OS Independent Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: 3 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Topic :: Internet :: WWW/HTTP [files] packages = watcher_dashboard [nosetests] verbosity=2 detailed-errors=1 watcher-dashboard-14.0.0+git20251111.18.aaebb77/setup.py000066400000000000000000000012711510461315000217170ustar00rootroot00000000000000# 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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/test-requirements.txt000066400000000000000000000005201510461315000244420ustar00rootroot00000000000000# 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 python-subunit>=1.0.0 # Apache-2.0/BSD selenium>=2.50.1 # Apache-2.0 testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT # This also needs xvfb library installed on your OS xvfbwrapper>=0.1.3 #license: MIT watcher-dashboard-14.0.0+git20251111.18.aaebb77/tools/000077500000000000000000000000001510461315000213445ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/tools/install_venv.py000066400000000000000000000045271510461315000244320ustar00rootroot00000000000000# 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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/tools/install_venv_common.py000066400000000000000000000134331510461315000257760ustar00rootroot00000000000000# 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() watcher-dashboard-14.0.0+git20251111.18.aaebb77/tools/register_plugin.sh000077500000000000000000000014331510461315000251060ustar00rootroot00000000000000#!/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.json' 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/tools/with_venv.sh000077500000000000000000000007721510461315000237220ustar00rootroot00000000000000#!/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 && "$@" watcher-dashboard-14.0.0+git20251111.18.aaebb77/tox.ini000066400000000000000000000046321510461315000215240ustar00rootroot00000000000000[tox] minversion = 3.18.0 envlist = py3,pep8 # Automatic envs (pyXX) will only use the python version appropriate to that # env and ignore basepython inherited from [testenv] if we set # ignore_basepython_conflict. ignore_basepython_conflict = True [testenv] basepython = python3 usedevelop = True setenv = DJANGO_SETTINGS_MODULE=watcher_dashboard.test.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 install_command = pip install -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} {opts} {packages} 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.test.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:venv] commands = {posargs} [testenv:releasenotes] 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.test.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] # F405 TEMPLATES may be undefined, or defined from star imports # (because it is not easy to avoid this in openstack_dashboard.test.settings) # W504 line break after binary operator ignore = F405,W504 show-source = True builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,.ropeproject,tools watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/000077500000000000000000000000001510461315000236505ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/__init__.py000066400000000000000000000000001510461315000257470ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/api/000077500000000000000000000000001510461315000244215ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/api/__init__.py000066400000000000000000000000001510461315000265200ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/api/watcher.py000066400000000000000000000406721510461315000264410ustar00rootroot00000000000000# 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.conf import settings from django.utils.translation import gettext_lazy as _ from openstack_dashboard.api import base from watcher_dashboard.common import client as wv from watcher_dashboard.utils import errors as errors_utils LOG = logging.getLogger(__name__) WATCHER_SERVICE = 'infra-optim' def watcherclient(request, api_version=None): insert_watcher_policy_file() endpoint = base.url_for(request, 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 wv.MIN_DEFAULT # Prefer centralized client helper client = wv.get_client(request, required=microversion) return client def insert_watcher_policy_file(): policy_files = getattr(settings, 'POLICY_FILES', {}) policy_files['infra-optim'] = 'watcher_policy.json' setattr(settings, 'POLICY_FILES', policy_files) 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): """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 :return: the created Audit object :rtype: :py:class:`~.Audit` """ # Build the parameters to pass to watcherclient 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 # Use microversion 1.1 to support start/end_time client = watcherclient(request, api_version=wv.MV_START_END) return client.audit.create(**payload) @classmethod def list(cls, request, **filters): """Return a list of audits 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 audits, or an empty list if there are none :rtype: list of :py:class:`~.Audit` """ return watcherclient( request, api_version=wv.MV_START_END ).audit.list( detail=True, **filters ) @classmethod @errors_utils.handle_errors(_("Unable to retrieve audit")) def get(cls, request, audit_id): """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 :return: matching audit, or None if no audit matches the ID :rtype: :py:class:`~.Audit` """ return watcherclient( request, api_version=wv.MV_START_END ).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') 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): """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 :return: matching action plan, or None if no action plan matches the ID :rtype: :py:class:`~.ActionPlan` """ return watcherclient(request).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') 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): """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 :return: matching action, or None if no action matches the ID :rtype: :py:class:`~.Action` """ return watcherclient(request).action.get(action_id=action_id) @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_specifications') 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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/common/000077500000000000000000000000001510461315000251405ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/common/__init__.py000066400000000000000000000000001510461315000272370ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/common/client.py000066400000000000000000000031631510461315000267730ustar00rootroot00000000000000# 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.conf import settings from openstack_dashboard.api import base from watcherclient import client as wc LOG = logging.getLogger(__name__) # Service type used by Watcher in the catalog WATCHER_SERVICE = 'infra-optim' # Common header name used by OpenStack microversioning HEADER_NAME = 'OpenStack-API-Version' # Default/minimal microversion MIN_DEFAULT = '1.0' # Microversion enabling audit start/end time fields MV_START_END = '1.1' def get_client(request, required=MIN_DEFAULT): """Return a watcher client pinned to the given microversion. 'required' can be '1.0', '1.1', or 'latest'. """ endpoint = base.url_for(request, WATCHER_SERVICE) insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) ca_file = getattr(settings, 'OPENSTACK_SSL_CACERT', None) 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, ) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/common/exceptions.py000066400000000000000000000015061510461315000276750ustar00rootroot00000000000000# 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/conf/000077500000000000000000000000001510461315000245755ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/conf/watcher_policy.json000066400000000000000000000021621510461315000305050ustar00rootroot00000000000000{ "admin_api": "role:admin or role:administrator", "show_password": "!", "default": "rule:admin_api", "action:detail": "rule:default", "action:get": "rule:default", "action:get_all": "rule:default", "action_plan:create": "rule:default", "action_plan:detail": "rule:default", "action_plan:get": "rule:default", "action_plan:get_all": "rule:default", "action_plan:update": "rule:default", "audit:create": "rule:default", "audit:delete": "rule:default", "audit:detail": "rule:default", "audit:get": "rule:default", "audit:get_all": "rule:default", "audit:update": "rule:default", "audit_template:create": "rule:default", "audit_template:delete": "rule:default", "audit_template:detail": "rule:default", "audit_template:get": "rule:default", "audit_template:get_all": "rule:default", "audit_template:update": "rule:default", "goal:detail": "rule:default", "goal:get": "rule:default", "goal:get_all": "rule:default", "strategy:detail": "rule:default", "strategy:get": "rule:default", "strategy:get_all": "rule:default" } watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/000077500000000000000000000000001510461315000253225ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/__init__.py000066400000000000000000000000001510461315000274210ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/action_plans/000077500000000000000000000000001510461315000277745ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/action_plans/__init__.py000066400000000000000000000000001510461315000320730ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/action_plans/panel.py000066400000000000000000000014241510461315000314460ustar00rootroot00000000000000# 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 _ import horizon class ActionPlans(horizon.Panel): name = _("Action Plans") slug = "action_plans" permissions = ("openstack.services.infra-optim",) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/action_plans/tables.py000066400000000000000000000173401510461315000316250ustar00rootroot00000000000000# 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 from django import template from django.template.defaultfilters import title # noqa from django import urls from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from django.utils.translation import pgettext_lazy import horizon.exceptions import horizon.messages import horizon.tables 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'), status=True, status_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'), status=True, status_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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/action_plans/tabs.py000066400000000000000000000020121510461315000312720ustar00rootroot00000000000000# 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/action_plans/urls.py000066400000000000000000000016661510461315000313440ustar00rootroot00000000000000# 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'), ] watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/action_plans/views.py000066400000000000000000000116371510461315000315130ustar00rootroot00000000000000# 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 from django.utils.translation import gettext_lazy as _ import horizon.exceptions from horizon import forms import horizon.tables import horizon.tabs from horizon.utils import memoized import horizon.workflows from watcher_dashboard.api import watcher 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 _get_data(self): action_plan_uuid = None try: action_plan_uuid = self.kwargs['action_plan_uuid'] action_plan = watcher.ActionPlan.get( self.request, action_plan_uuid) 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): 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 LOG.info('*********************************') LOG.info(action_plan) LOG.info('*********************************') return context def get_tabs(self, request, *args, **kwargs): action_plan = self._get_data() return self.tab_group_class( request, action_plan=action_plan, **kwargs) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/actions/000077500000000000000000000000001510461315000267625ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/actions/__init__.py000066400000000000000000000000001510461315000310610ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/actions/panel.py000066400000000000000000000014071510461315000304350ustar00rootroot00000000000000# 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 _ import horizon class Actions(horizon.Panel): name = _("Actions ") slug = "actions" permissions = ("openstack.services.infra-optim",) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/actions/tables.py000066400000000000000000000106071510461315000306120ustar00rootroot00000000000000# 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 from django.template.defaultfilters import title # noqa from django import urls from django.utils.translation import gettext_lazy as _ from django.utils.translation import pgettext_lazy import horizon.exceptions import horizon.messages import horizon.tables 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")), ) 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"),) 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'), status_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'), status_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 = "related_wactions" verbose_name = _("Related Actions") hidden_title = False 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/actions/tabs.py000066400000000000000000000017611510461315000302720ustar00rootroot00000000000000# 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/actions/urls.py000066400000000000000000000015271510461315000303260ustar00rootroot00000000000000# 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'), ] watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/actions/views.py000066400000000000000000000071541510461315000305000ustar00rootroot00000000000000# 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 from django.utils.translation import gettext_lazy as _ import horizon.exceptions import horizon.tables import horizon.tabs from horizon.utils import memoized import horizon.workflows from watcher_dashboard.api import watcher 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 _get_data(self): action_uuid = None try: action_uuid = self.kwargs['action_uuid'] action = watcher.Action.get(self.request, action_uuid) 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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audit_templates/000077500000000000000000000000001510461315000305065ustar00rootroot00000000000000__init__.py000066400000000000000000000000001510461315000325260ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audit_templateswatcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audit_templates/forms.py000066400000000000000000000133671510461315000322200ustar00rootroot00000000000000# 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 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 import yaml 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 hasattr(self, 'data') else None ) 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(self, request, goals): try: strategies = watcher.Strategy.list(self.request) except Exception as exc: msg = _('Failed to get the list of available strategies.') LOG.info(msg) messages.warning(request, msg) messages.warning(request, exc) strategies = [] _goals = {} for goal in goals: _goals[goal[0]] = goal[1] choices = [ (strategy.uuid, strategy.display_name + ' (GOAL: ' + _goals[strategy.goal_uuid] + ')') for strategy in strategies ] if choices: choices.insert(0, ("", _("Select Strategy"))) 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, getattr(strategy, 'display_name', None) or getattr(strategy, 'name', '') ) 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audit_templates/panel.py000066400000000000000000000014351510461315000321620ustar00rootroot00000000000000# 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 _ import horizon class AuditTemplates(horizon.Panel): name = _("Audit Templates") slug = "audit_templates" permissions = ("openstack.services.infra-optim",) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audit_templates/tables.py000066400000000000000000000070611510461315000323360ustar00rootroot00000000000000# 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 django.utils.translation import ngettext_lazy import horizon.exceptions import horizon.messages import horizon.tables 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, ) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audit_templates/tabs.py000066400000000000000000000020301510461315000320040ustar00rootroot00000000000000# 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audit_templates/tests.py000066400000000000000000000104641510461315000322270ustar00rootroot00000000000000# 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.test 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 resp = self.client.get(INDEX_URL) 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]) 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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audit_templates/urls.py000066400000000000000000000020341510461315000320440ustar00rootroot00000000000000# 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'), ] watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audit_templates/views.py000066400000000000000000000133031510461315000322150ustar00rootroot00000000000000# 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 from django.http import JsonResponse from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ import horizon.exceptions from horizon import forms import horizon.tables import horizon.tabs import horizon.workflows from watcher_dashboard.api import watcher 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 }}") def _get_data(self): audit_template_uuid = None try: LOG.info(self.kwargs) 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() audits = watcher.Audit.list( self.request, 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': ( getattr(strategy, 'display_name', None) or getattr(strategy, 'name', '') ), } 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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audits/000077500000000000000000000000001510461315000266135ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audits/__init__.py000066400000000000000000000000001510461315000307120ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audits/forms.py000066400000000000000000000220171510461315000303150ustar00rootroot00000000000000# 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 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 # Keep fields visible; API microversion is enforced per-call in backend 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 audit = watcher.Audit.create(request, **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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audits/panel.py000066400000000000000000000014031510461315000302620ustar00rootroot00000000000000# 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 _ import horizon class Audits(horizon.Panel): name = _("Audits") slug = "audits" permissions = ('openstack.services.infra-optim',) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audits/tables.py000066400000000000000000000136341510461315000304460ustar00rootroot00000000000000# 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 import shortcuts from django.template.defaultfilters import title # noqa from django import urls from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy from django.utils.translation import pgettext_lazy import horizon.exceptions import horizon.messages import horizon.tables 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 = ( ('audit_template', _("Audit Template ="), 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'), status=True, status_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'), status=True, status_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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audits/tabs.py000066400000000000000000000017541510461315000301250ustar00rootroot00000000000000# 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audits/urls.py000066400000000000000000000020361510461315000301530ustar00rootroot00000000000000# 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'), ] watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/audits/views.py000066400000000000000000000204561510461315000303310ustar00rootroot00000000000000# 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 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 _ import horizon.exceptions from horizon import forms import horizon.tables import horizon.tabs from horizon.utils import memoized import horizon.workflows import yaml from watcher_dashboard.api import watcher 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") 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: audits = watcher.Audit.list(self.request, **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 _get_data(self): audit_uuid = None try: audit_uuid = self.kwargs['audit_uuid'] audit = watcher.Audit.get(self.request, audit_uuid) 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': strategy.display_name or strategy.name, '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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/goals/000077500000000000000000000000001510461315000264275ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/goals/__init__.py000066400000000000000000000000001510461315000305260ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/goals/panel.py000066400000000000000000000014001510461315000300730ustar00rootroot00000000000000# 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 _ import horizon class Goals(horizon.Panel): name = _("Goals") slug = "goals" permissions = ("openstack.services.infra-optim",) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/goals/tables.py000066400000000000000000000035241510461315000302570ustar00rootroot00000000000000# 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/goals/tabs.py000066400000000000000000000017471510461315000277430ustar00rootroot00000000000000# 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/goals/tests.py000066400000000000000000000044531510461315000301510ustar00rootroot00000000000000# 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.test 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 resp = self.client.get(INDEX_URL) 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]) res = self.client.get(DETAILS_URL) self.assertRedirectsNoFollow(res, INDEX_URL) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/goals/urls.py000066400000000000000000000015231510461315000277670ustar00rootroot00000000000000# 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'), ] watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/goals/views.py000066400000000000000000000105261510461315000301420ustar00rootroot00000000000000# 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 from django.utils.translation import gettext_lazy as _ import horizon.exceptions import horizon.tables import horizon.tabs from horizon.utils import memoized import horizon.workflows 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: 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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/strategies/000077500000000000000000000000001510461315000274745ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/strategies/__init__.py000066400000000000000000000000001510461315000315730ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/strategies/forms.py000066400000000000000000000000001510461315000311620ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/strategies/panel.py000066400000000000000000000014171510461315000311500ustar00rootroot00000000000000# 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 _ import horizon class Strategies(horizon.Panel): name = _("Strategies") slug = "strategies" permissions = ("openstack.services.infra-optim",) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/strategies/tables.py000066400000000000000000000043711510461315000313250ustar00rootroot00000000000000# 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 _ import horizon.exceptions import horizon.messages import horizon.tables 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/strategies/tabs.py000066400000000000000000000017741510461315000310100ustar00rootroot00000000000000# 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/strategies/tests.py000066400000000000000000000050371510461315000312150ustar00rootroot00000000000000# 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.test 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 resp = self.client.get(INDEX_URL) 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]) res = self.client.get(DETAILS_URL) self.assertRedirectsNoFollow(res, INDEX_URL) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/strategies/urls.py000066400000000000000000000015341510461315000310360ustar00rootroot00000000000000# 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'), ] watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/content/strategies/views.py000066400000000000000000000125741510461315000312140ustar00rootroot00000000000000# 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 from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ import horizon.exceptions import horizon.tables import horizon.tabs from horizon.utils import memoized import horizon.workflows 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/local/000077500000000000000000000000001510461315000247425ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/local/__init__.py000066400000000000000000000000001510461315000270410ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/local/enabled/000077500000000000000000000000001510461315000263345ustar00rootroot00000000000000_31000_goals_panel.py000066400000000000000000000016251510461315000320010ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/local/enabled# 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' _31010_strategies_panel.py000066400000000000000000000016441510461315000330500ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/local/enabled# 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' _31020_watcher_panelgroup.py000066400000000000000000000022261510461315000334060ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/local/enabled# 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'] # ADD_ANGULAR_MODULES = [ # 'horizon.dashboard.watcher' # ] # ADD_JS_FILES = [ # 'horizon/lib/angular/angular-route.js' # ] # ADD_SCSS_FILES = [ # 'dashboard/watcher/watcher.scss' # ] AUTO_DISCOVER_STATIC_FILES = False _31030_audit_templates_panel.py000066400000000000000000000016621510461315000340640ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/local/enabled# 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' _31040_audits_panel.py000066400000000000000000000016301510461315000321650ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/local/enabled# 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' _31050_action_plans_panel.py000066400000000000000000000016511510461315000333520ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/local/enabled# 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' _31060_actions_panel.py000066400000000000000000000016331510461315000323410ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/local/enabled# 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' watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/local/enabled/__init__.py000066400000000000000000000000001510461315000304330ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/static/000077500000000000000000000000001510461315000251375ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/static/infra_optim/000077500000000000000000000000001510461315000274465ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/static/infra_optim/images/000077500000000000000000000000001510461315000307135ustar00rootroot00000000000000chevron.png000066400000000000000000000005341510461315000330100ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/static/infra_optim/images‰PNG  IHDRH-ŃbKGD˙˙˙ ˝§“ pHYs  šśtIMEŢ(v˙+MiTXtCommentCreated with GIMPd.eŔIDAT(ĎŐŇ;‚@ŕ9»Ł Ń­ŐŢ•1ţo6Ź ±Q üD"caŚ$ž ŤqÚÝŻě ’D…ÔP1?„á1Â-Ëľ.’Äţ"Ďó7LÓÁf Çťj1Ixó6ۢÓů MÓ„=čá~Ďá¸3d%Lžż„Š´šşť6@”ϡT Ď_BJ‰ÉxĂO¤b´šěaBOXƆ”¨7ęH’ëŇÂ2&©EŻÚ\TĚŐ:`QÚąřź—{Yvť' čHďIEND®B`‚power.png000066400000000000000000000011361510461315000324770ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/static/infra_optim/images‰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`‚watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/static/infra_optim/scss/000077500000000000000000000000001510461315000304215ustar00rootroot00000000000000infra_optim.scss000066400000000000000000000002031510461315000335410ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/static/infra_optim/scss// /* Additional CSS for infra_optim. */ // @import "/dashboard/scss/variables"; // @import "/bootstrap/scss/bootstrap/variables"; watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/static/infra_optim/tests/000077500000000000000000000000001510461315000306105ustar00rootroot00000000000000formset_table.js000066400000000000000000000045221510461315000337200ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/static/infra_optim/testshorizon.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); }); }); watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/000077500000000000000000000000001510461315000256465ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/client_side/000077500000000000000000000000001510461315000301305ustar00rootroot00000000000000_modal_chart.html000066400000000000000000000011661510461315000333570ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/client_side{% extends "horizon/client_side/template.html" %} {% load horizon %} {% block id %}modal_chart_template{% endblock %} {% block template %} {% jstemplate %} {% endjstemplate %} {% endblock %} templates.html000066400000000000000000000000561510461315000327360ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/client_side{% include "client_side/_modal_chart.html" %} watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/formset_table/000077500000000000000000000000001510461315000304745ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/formset_table/_row.html000066400000000000000000000013121510461315000323250ustar00rootroot00000000000000 {% 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 %} _table.html000066400000000000000000000025741510461315000325410ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/formset_table{% 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 %} menu_formset.html000066400000000000000000000030271510461315000340100ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/formset_table{% 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 %}
watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/horizon/000077500000000000000000000000001510461315000273365ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/horizon/common/000077500000000000000000000000001510461315000306265ustar00rootroot00000000000000_items_count_domain_page_header.html000066400000000000000000000006611510461315000377630ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/horizon/common{% load i18n %} {% block page_header %} {% endblock %} watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/000077500000000000000000000000001510461315000301555ustar00rootroot00000000000000_fullscreen_workflow.html000066400000000000000000000031701510461315000352200ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim{% 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 %} _fullscreen_workflow_base.html000066400000000000000000000005101510461315000362050ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim{% 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 %} _modal_form_xl.html000066400000000000000000000040641510461315000337510ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim{# 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 %} _modal_xl.html000066400000000000000000000021311510461315000327170ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim{# Vendored from horizon/common/_modal.html with modal-dialog-class block added on line 6 #} {% block modal-js %} {% endblock %} _performance_chart.html000066400000000000000000000007761510461315000346170ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim

{{ label }}

_performance_chart_box.html000066400000000000000000000051441510461315000354610ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim{% 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 %} _top_5_box.html000066400000000000000000000007441510461315000330260ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim
{% 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 %}
_top_5_chart.html000066400000000000000000000010761510461315000333360ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim{% 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 %} _workflow_base.html000066400000000000000000000005001510461315000337620ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim{% 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 %} action_plans/000077500000000000000000000000001510461315000325505ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim_details_overview.html000066400000000000000000000014111510461315000371450ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/action_plans{% 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:_("-") }}
{% trans "Created At" %}
{{ action_plan.created_at|parse_isotime|default:_("-") }}
{% trans "Update At" %}
{{ action_plan.updated_at|parse_isotime|default:_("-") }}
_global_efficacy.html000066400000000000000000000002371510461315000366700ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/action_plans{% load i18n %}
    {% for name, value in global_indicators.items %}
  • {{ name }} {{ value }}
  • {% endfor %}
create.html000066400000000000000000000005161510461315000347030ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/action_plans{% 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 %} details.html000066400000000000000000000007041510461315000350640ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/action_plans{% 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 %} index.html000066400000000000000000000006101510461315000345420ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/action_plans{% 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 %} watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/actions/000077500000000000000000000000001510461315000316155ustar00rootroot00000000000000details.html000066400000000000000000000024151510461315000340530ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/actions{% 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:_("-") }}
{% trans "Created At" %}
{{ action.created_at|parse_isotime|default:_("-") }}
{% trans "Update At" %}
{{ action.updated_at|parse_isotime|default:_("-") }}
{{ parameters_table.render }}
{% endblock %} index.html000066400000000000000000000005611510461315000335350ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/actions{% 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 %} actions_history/000077500000000000000000000000001510461315000333175ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optimdetails.html000066400000000000000000000016561510461315000356420ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/actions_history{% 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 %} index.html000066400000000000000000000016431510461315000353200ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/actions_history{% 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 %} audit_templates/000077500000000000000000000000001510461315000332625ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim_create.html000066400000000000000000000072641510461315000355630ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/audit_templates{% 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 %} create.html000066400000000000000000000003051510461315000354110ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/audit_templates{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Create Template" %}{% endblock %} {% block main %} {% include 'infra_optim/audit_templates/_create.html' %} {% endblock %} details.html000066400000000000000000000026361510461315000356040ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/audit_templates{% 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 %} index.html000066400000000000000000000006041510461315000352570ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/audit_templates{% 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 %} watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/audits/000077500000000000000000000000001510461315000314465ustar00rootroot00000000000000_create.html000066400000000000000000000134401510461315000336610ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/audits{% 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 %} create.html000066400000000000000000000002701510461315000335170ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/audits{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Create Audit" %}{% endblock %} {% block main %} {% include 'infra_optim/audits/_create.html' %} {% endblock %}details.html000066400000000000000000000035021510461315000337020ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/audits{% 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 %} index.html000066400000000000000000000005271510461315000333700ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/audits{% 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 %} watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/base.html000066400000000000000000000006021510461315000317530ustar00rootroot00000000000000{% extends 'base.html' %} {% block css %} {{block.super}} {% load compress %} {% compress css %} {% endcompress %} {% endblock %} base_detail.html000066400000000000000000000007501510461315000332220ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim{% extends 'infra_optim/base.html' %} {% block main %}
{% block breadcrumbs %}{% endblock %}
{% block actions %}{% endblock %}

{% block name %}{% endblock %}

{% block overall_usage %}{% endblock %}
{{ tab_group.render }}
{% endblock %} watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/goals/000077500000000000000000000000001510461315000312625ustar00rootroot00000000000000details.html000066400000000000000000000020441510461315000335160ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/goals{% 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 %} index.html000066400000000000000000000005211510461315000331760ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/goals{% 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 %} header_actions.html000066400000000000000000000006121510461315000337330ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/logs/000077500000000000000000000000001510461315000311215ustar00rootroot00000000000000index.html000066400000000000000000000006071510461315000330420ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/logs{% 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 %} watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/qunit.html000066400000000000000000000157221510461315000322120ustar00rootroot00000000000000 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
    watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/strategies/000077500000000000000000000000001510461315000323275ustar00rootroot00000000000000details.html000066400000000000000000000027051510461315000345670ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/strategies{% 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 %} index.html000066400000000000000000000005441510461315000342500ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templates/infra_optim/strategies{% 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 %} watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templatetags/000077500000000000000000000000001510461315000263425ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/templatetags/__init__.py000066400000000000000000000011751510461315000304570ustar00rootroot00000000000000# 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/000077500000000000000000000000001510461315000246275ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/__init__.py000066400000000000000000000000001510461315000267260ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/api_tests/000077500000000000000000000000001510461315000266225ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/api_tests/__init__.py000066400000000000000000000000001510461315000307210ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/api_tests/test_client.py000066400000000000000000000045451510461315000315210ustar00rootroot00000000000000# 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.test import helpers as test class WatcherClientAPITests(test.APITestCase): def test_audit_create_uses_1_1_when_start_end_present(self): watcherclient = self.stub_watcherclient() watcherclient.audit.create = mock.Mock(return_value={}) with mock.patch('watcher_dashboard.api.watcher.watcherclient') 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') wc.assert_called_with(self.request, api_version='1.1') client_mock.audit.create.assert_called_once() def test_audit_create_uses_1_1_even_without_start_end(self): with mock.patch('watcher_dashboard.api.watcher.watcherclient') 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='1.1') client_mock.audit.create.assert_called_once() watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/api_tests/test_watcher.py000066400000000000000000000417531510461315000317020ustar00rootroot00000000000000# 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.test 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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/helpers.py000066400000000000000000000033401510461315000266430ustar00rootroot00000000000000# 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.test.test_data import utils class WatcherTestsMixin: 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/000077500000000000000000000000001510461315000303745ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/__init__.py000066400000000000000000000000001510461315000324730ustar00rootroot00000000000000horizon.conf000066400000000000000000000034151510461315000326570ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests# # Configuration filed based on Tempest's tempest.conf.sample # [dashboard] # Where the dashboard can be found (string value) dashboard_url=http://localhost:8000/ # Dashboard help page url (string value) help_url=https://docs.openstack.org/ [selenium] # Timeout in seconds to wait for a page to become available # (integer value) page_timeout=30 # Output directory for screenshots. # (string value) screenshots_directory=integration_tests_screenshots # Implicit timeout to wait until element become available, # this timeout is used for every find_element, find_elements call. # (integer value) implicit_wait=10 # Explicit timeout is used for long lasting operations, # methods using explicit timeout are usually prefixed with 'wait', # those methods ignore implicit_wait when looking up web elements. # (integer value) explicit_wait=300 [image] # http accessible image (string value) http_image=http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-uec.tar.gz [identity] # Username to use for non-admin API requests. (string value) username=demo # API key to use when authenticating. (string value) password=secretadmin # Administrative Username to use for admin API requests. # (string value) admin_username=admin # API key to use when authenticating as admin. (string value) admin_password=secretadmin [scenario] # ssh username for image file (string value) ssh_user=cirros [launch_instances] #available zone to launch instances available_zone=nova #image_name to launch instances image_name=cirros-0.3.4-x86_64-uec (24.0 MB) [volume] volume_type=lvmdriver-1 volume_size=1 [plugin] is_plugin=true plugin_page_path=watcher_dashboard.test.integration_tests.pages plugin_page_structure={"Admin": {"Optimization": {"_": ["Audit Templates", "Audits", "Action Plans", "Actions"] } } } watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/pages/000077500000000000000000000000001510461315000314735ustar00rootroot00000000000000__init__.py000066400000000000000000000000001510461315000335130ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/pagesadmin/000077500000000000000000000000001510461315000325045ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/pages__init__.py000066400000000000000000000000001510461315000346030ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/pages/adminoptimization/000077500000000000000000000000001510461315000352325ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/pages/admin__init__.py000066400000000000000000000000001510461315000373310ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/pages/admin/optimizationauditspage.py000066400000000000000000000054661510461315000377450ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/pages/admin/optimization# 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.integration_tests.regions import forms from openstack_dashboard.test.integration_tests.regions import tables from openstack_dashboard.test.integration_tests.pages import basepage class AuditsTable(tables.TableRegion): name = "audits" @tables.bind_table_action('launch_audit') def launch_audit(self, launch_button): launch_button.click() return forms.BaseFormRegion(self.driver, self.conf) @tables.bind_row_action('go_to_action_plan') def go_to_action_plan(self, goto_button): goto_button.click() return forms.BaseFormRegion(self.driver, self.conf) @tables.bind_row_action('go_to_audit_template') def go_to_audit_template(self, goto_button): goto_button.click() return forms.BaseFormRegion(self.driver, self.conf) class AuditsPage(basepage.BaseNavigationPage): DEFAULT_ID = "auto" AUDIT_TABLE_NAME_COLUMN = 'name' AUDIT_TABLE_TEMPLATE_COLUMN_INDEX = 1 def __init__(self, driver, conf): super().__init__(driver, conf) self._page_title = "Audits" @property def audits_table(self): return AuditsTable(self.driver, self.conf) def _get_audit_row(self, name): return self.audits_table.get_row(self.AUDIT_TABLE_NAME_COLUMN, name) def create_audit(self, name, id_=DEFAULT_ID, vcpus=None, ram=None, root_disk=None, ephemeral_disk=None, swap_disk=None): create_audit_form = self.audits_table.create_audit() create_audit_form.name.text = name if id_ is not None: create_audit_form.audit_id.text = id_ create_audit_form.vcpus.value = vcpus create_audit_form.memory_mb.value = ram create_audit_form.disk_gb.value = root_disk create_audit_form.eph_gb.value = ephemeral_disk create_audit_form.swap_mb.value = swap_disk create_audit_form.submit() self.wait_till_popups_disappear() def delete_audit(self, name): row = self._get_audit_row(name) row.mark() confirm_delete_audits_form = self.audits_table.delete_audit() confirm_delete_audits_form.submit() self.wait_till_popups_disappear() def is_audit_present(self, name): return bool(self._get_audit_row(name)) audittemplatespage.py000066400000000000000000000103461510461315000414720ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/pages/admin/optimization# 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 selenium.webdriver.common import by from openstack_dashboard.test.integration_tests.pages import basepage from openstack_dashboard.test.integration_tests.regions import forms from openstack_dashboard.test.integration_tests.regions import tables class AuditTemplatesTable(tables.TableRegion): name = 'audit_templates' CREATE_AUDIT_TEMPLATE_FORM_FIELDS = ("name", "description", "goal_id", "strategy_id") @tables.bind_table_action('create') def create_audit_template(self, create_button): create_button.click() return forms.FormRegion( self.driver, self.conf, field_mappings=self.CREATE_AUDIT_TEMPLATE_FORM_FIELDS) @tables.bind_table_action('delete') def delete_audit_template(self, delete_button): delete_button.click() return forms.BaseFormRegion(self.driver, self.conf, None) @tables.bind_row_action('launch_audit') def launch_audit(self, launch_button, row): launch_button.click() return forms.BaseFormRegion(self.driver, self.conf) class AudittemplatesPage(basepage.BaseNavigationPage): DEFAULT_DESCRIPTION = "Fake description from integration tests" DEFAULT_GOAL = "SERVER_CONSOLIDATION" AUDITS_PAGE_TITLE = "Audits - OpenStack Dashboard" AUDIT_TEMPLATE_INFO_SUB_TITLE = "Audit Template Info" # Set fields name attribute CREATE_AUDIT_TEMPLATE_FORM_FIELDS = ( "name", "description", "goal" ) _audittemplates_info_title_locator = (by.By.CSS_SELECTOR, 'div.detail>h4') def __init__(self, driver, conf): super().__init__(driver, conf) self._page_title = "Audit Templates" @property def audittemplates_table(self): return AuditTemplatesTable(self.driver, self.conf) @property def audit_templates__action_create_form(self): return forms.FormRegion(self.driver, self.conf, None, self.CREATE_AUDIT_TEMPLATE_FORM_FIELDS) def _get_row_with_audit_template_name(self, name): self._turn_off_implicit_wait() row = self.audittemplates_table.get_row("name", name) self._turn_on_implicit_wait() return row def delete_audit_template(self, name): row = self._get_row_with_audit_template_name(name) row.mark() confirm_delete_audit_template_form = ( self.audittemplates_table.delete_audit_template()) confirm_delete_audit_template_form.submit() def create_audit_template(self, name, description=DEFAULT_DESCRIPTION, goal_id=DEFAULT_GOAL): self.audittemplates_table.create_audit_template() self.audit_templates__action_create_form.name.text = name self.audit_templates__action_create_form.description.text = description self.audit_templates__action_create_form.goal_id.value = goal_id self.audit_templates__action_create_form.submit() def is_audit_template_present(self, name): return bool( self._get_row_with_audit_template_name(name)) def launch_audit(self, name): row = self._get_row_with_audit_template_name(name) self.audittemplates_table.launch_audit(row) # Check that the name appears in Audits page return (self.driver.title == self.AUDITS_PAGE_TITLE) \ and (name in self.driver.page_source) def show_audit_template_info(self, name): self.driver.find_element_by_link_text(name).click() info_line = self._get_element(*self._audittemplates_info_title_locator) return self._is_text_visible( info_line, self.AUDIT_TEMPLATE_INFO_SUB_TITLE) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/tests/000077500000000000000000000000001510461315000315365ustar00rootroot00000000000000__init__.py000066400000000000000000000000001510461315000335560ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/teststest_audit_template_panel.py000066400000000000000000000060741510461315000372570ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/integration_tests/tests# 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 uuid from openstack_dashboard.test.integration_tests import helpers class AuditTemplateCreatePanelTests(helpers.AdminTestCase): def test_create_audit_template(self): """Test the audit template panel: * Loads the audit template panel * Creates a new audit template with random-generated name * Checks that this audit template is in list * Deletes this audit template (in tearDown) * Checks that the audit template is removed """ audit_template_name = f"audit_template_{uuid.uuid1()}" audit_template_page = \ self.home_pg.go_to_optimization_audittemplatespage() audit_template_page.create_audit_template(audit_template_name) self.assertTrue(audit_template_page.is_audit_template_present( audit_template_name)) class AuditTemplatePanelTests(helpers.AdminTestCase): def setUp(self): super().setUp() self.audit_template_name = f"audit_template_{uuid.uuid1()}" audit_template_page = \ self.home_pg.go_to_optimization_audittemplatespage() audit_template_page.create_audit_template(self.audit_template_name) def tearDown(self): audit_template_page = \ self.home_pg.go_to_optimization_audittemplatespage() audit_template_page.delete_audit_template(self.audit_template_name) # Uncomment this line when button will be implemented self.assertFalse(audit_template_page.is_audit_template_present( self.audit_template_name)) super().tearDown() def test_show_audit_template_info(self): """Test the audit template panel information page * Loads the audit template panel * Click on link behind the audit template name * Checks the info page (only the "Audit Template Info" title for now) """ audit_template_page = \ self.home_pg.go_to_optimization_audittemplatespage() self.assertTrue( audit_template_page.show_audit_template_info( self.audit_template_name)) def test_launch_audit(self): """Test the audit template panel "Launch Audit" row button * Loads the audit template panel * Click on the button "Launch Audit" * Checks the audits page for audit template name in page """ audit_template_page = \ self.home_pg.go_to_optimization_audittemplatespage() self.assertTrue( audit_template_page.launch_audit(self.audit_template_name)) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/selenium.py000066400000000000000000000037721510461315000270330ustar00rootroot00000000000000# 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 from horizon.test import helpers as test from selenium.common import exceptions as selenium_exceptions class BrowserTests(test.SeleniumTestCase): def test_jasmine(self): url = "{}{}".format(self.live_server_url, "/jasmine/") self.selenium.get(url) wait = self.ui.WebDriverWait(self.selenium, 10) def jasmine_done(driver): text = driver.find_element_by_id("jasmine-testresult").text return "Tests completed" in text wait.until(jasmine_done) failed_elem = self.selenium.find_element_by_class_name("failed") failed = int(failed_elem.text) if failed: self.log_failure_messages() self.assertEqual(failed, 0) def log_failure_messages(self): logger = logging.getLogger('selenium') logger.error("Errors found during jasmine test:") fail_elems = self.selenium.find_elements_by_class_name("fail") for elem in fail_elems: try: module = elem.find_element_by_class_name("module-name").text except selenium_exceptions.NoSuchElementException: continue message = elem.find_element_by_class_name("test-message").text source = elem.find_element_by_tag_name("pre").text logger.error( "Module: %s, message: %s, source: %s", module, message, source, ) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/settings.py000066400000000000000000000026411510461315000270440ustar00rootroot00000000000000# # 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.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 import watcher_dashboard.local.enabled 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, ) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/test_data/000077500000000000000000000000001510461315000265775ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/test_data/__init__.py000066400000000000000000000000001510461315000306760ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/test_data/exceptions.py000066400000000000000000000016071510461315000313360ustar00rootroot00000000000000# 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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/test_data/utils.py000066400000000000000000000036361510461315000303210ustar00rootroot00000000000000# 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.test.test_data import exceptions from watcher_dashboard.test.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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/test_data/watcher_data.py000066400000000000000000000171041510461315000316020ustar00rootroot00000000000000# 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.test.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_specifications': spec1 } goal_dict2 = { 'uuid': 'gggggggg-2222-2222-2222-gggggggggggg', 'name': 'SERVER_CONSOLIDATION', 'display_name': 'Server consolidation', 'efficacy_specifications': 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' } 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' } 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)) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/test_formset_table.py000066400000000000000000000034371510461315000310750ustar00rootroot00000000000000# 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.test 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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/test/urls.py000066400000000000000000000013501510461315000261650ustar00rootroot00000000000000# # 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 urls from django.urls import re_path import openstack_dashboard.urls urlpatterns = [ re_path(r'', urls.include(openstack_dashboard.urls)) ] watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/utils/000077500000000000000000000000001510461315000250105ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/utils/__init__.py000066400000000000000000000000001510461315000271070ustar00rootroot00000000000000watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/utils/errors.py000066400000000000000000000056411510461315000267040ustar00rootroot00000000000000# # 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/utils/tests.py000066400000000000000000000055641510461315000265360ustar00rootroot00000000000000# # 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.test 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) watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/utils/utils.py000066400000000000000000000056761510461315000265400ustar00rootroot00000000000000# # 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 watcher-dashboard-14.0.0+git20251111.18.aaebb77/watcher_dashboard/version.py000066400000000000000000000013551510461315000257130ustar00rootroot00000000000000# 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()