├── .github └── workflows │ ├── docker.yaml │ └── tox.yaml ├── .gitignore ├── .stestr.conf ├── CONTRIBUTING.rst ├── Dockerfile ├── LICENSE ├── README.rst ├── grafana_cloud_dashboard.json ├── grafana_project_dashboard.json ├── os_capacity ├── __init__.py ├── prometheus.py └── tests │ ├── __init__.py │ └── unit │ ├── __init__.py │ └── test_prometheus.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt └── tox.ini /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker image 2 | # Run the tasks on every push 3 | on: push 4 | jobs: 5 | build_push_api: 6 | name: Build and push image 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | id-token: write # needed for signing the images with GitHub OIDC Token 11 | packages: write # required for pushing container images 12 | security-events: write # required for pushing SARIF files 13 | 14 | steps: 15 | - name: Check out the repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Login to GitHub Container Registry 19 | uses: docker/login-action@v3 20 | with: 21 | registry: ghcr.io 22 | username: ${{ github.actor }} 23 | password: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | - name: Calculate metadata for image 26 | id: image-meta 27 | uses: docker/metadata-action@v5 28 | with: 29 | images: ghcr.io/stackhpc/os-capacity 30 | # Produce the branch name or tag and the SHA as tags 31 | tags: | 32 | type=ref,event=branch 33 | type=ref,event=tag 34 | type=sha,prefix= 35 | 36 | - name: Build and push image 37 | uses: azimuth-cloud/github-actions/docker-multiarch-build-push@master 38 | with: 39 | cache-key: os-capacity 40 | context: . 41 | platforms: linux/amd64 42 | push: true 43 | tags: ${{ steps.image-meta.outputs.tags }} 44 | labels: ${{ steps.image-meta.outputs.labels }} 45 | github-token: ${{ secrets.GITHUB_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/workflows/tox.yaml: -------------------------------------------------------------------------------- 1 | name: Tox unit tests 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Tox unit tests and linting 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ['3.12'] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | python -m pip install tox 25 | 26 | - name: Test with tox 27 | run: tox 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .stestr 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # vim 105 | *.swp 106 | *.swo 107 | -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=./os_capacity/tests 3 | top_dir=./ 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | os-capacity does not currently follow the upstream OpenStack development 2 | process, but we will still be incredibly grateful for any contributions. 3 | 4 | Please raise issues and submit pull requests via Github. 5 | 6 | Thanks in advance! 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 AS build-image 2 | 3 | RUN apt-get update && \ 4 | apt-get upgrade -y && \ 5 | apt-get install python3-venv python3-dev gcc git -y && \ 6 | rm -rf /var/lib/apt/lists/* 7 | 8 | # build into a venv we can copy across 9 | RUN python3 -m venv /opt/venv 10 | ENV PATH="/opt/venv/bin:$PATH" 11 | 12 | COPY ./requirements.txt /os-capacity/requirements.txt 13 | RUN pip install -U pip setuptools 14 | RUN pip install --requirement /os-capacity/requirements.txt 15 | 16 | COPY . /os-capacity 17 | RUN pip install -U /os-capacity 18 | 19 | # 20 | # Now the image we run with 21 | # 22 | FROM ubuntu:24.04 AS run-image 23 | 24 | RUN apt-get update && \ 25 | apt-get upgrade -y && \ 26 | apt-get install --no-install-recommends python3 tini ca-certificates -y && \ 27 | rm -rf /var/lib/apt/lists/* 28 | 29 | # Copy accross the venv 30 | COPY --from=build-image /opt/venv /opt/venv 31 | ENV PATH="/opt/venv/bin:$PATH" 32 | 33 | # Create the user that will be used to run the app 34 | ENV APP_UID=1001 35 | ENV APP_GID=1001 36 | ENV APP_USER=app 37 | ENV APP_GROUP=app 38 | RUN groupadd --gid $APP_GID $APP_GROUP && \ 39 | useradd \ 40 | --no-create-home \ 41 | --no-user-group \ 42 | --gid $APP_GID \ 43 | --shell /sbin/nologin \ 44 | --uid $APP_UID \ 45 | $APP_USER 46 | 47 | # Don't buffer stdout and stderr as it breaks realtime logging 48 | ENV PYTHONUNBUFFERED=1 49 | 50 | USER $APP_UID 51 | ENTRYPOINT ["tini", "--"] 52 | CMD ["os_capacity"] 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | os-capacity 2 | =========== 3 | 4 | This is a prototype prometheus exporter 5 | to extract capacity information from OpenStack Placement. 6 | 7 | It includes support for both baremetal flavors 8 | and flavors with PCPU resources implied. 9 | 10 | Install 11 | ------- 12 | 13 | First lets get the code downloaded: 14 | 15 | .. code:: 16 | 17 | git clone https://github.com/stackhpc/os-capacity.git 18 | cd os-capacity 19 | 20 | Now lets get that installed inside a virtual environment: 21 | 22 | .. code:: 23 | 24 | python3 -m venv .venv 25 | source .venv/bin/activate 26 | pip install -U . 27 | 28 | Prometheus Exporter 29 | ------------------- 30 | 31 | Assuming you have clouds.yaml in the right place, 32 | and those credentials have read access to the Placement API, Nova API and Keystone APIs, 33 | you can run the exporter doing something like this: 34 | 35 | .. code:: 36 | 37 | export OS_CLIENT_CONFIG_FILE=myappcred.yaml 38 | export OS_CLOUD=openstack 39 | 40 | ./os_capacity/prometheus.py & 41 | curl localhost:9000 > mytestrun 42 | cat mytestrun 43 | 44 | Or just run via docker or similar::: 45 | 46 | docker run -d --name os_capacity \ 47 | --mount type=bind,source=/etc/kolla/os-capacity/,target=/etc/openstack \ 48 | --env OS_CLOUD=openstack --env OS_CLIENT_CONFIG_FILE=/etc/openstack/clouds.yaml \ 49 | -p 9000:9000 ghcr.io/stackhpc/os-capacity:master 50 | curl localhost:9000 51 | 52 | 53 | We aslo have the following optional environment variables: 54 | 55 | * OS_CAPACITY_EXPORTER_PORT = 9000 56 | * OS_CAPACITY_EXPORTER_LISTEN_ADDRESS = "0.0.0.0" 57 | * OS_CAPACITY_SKIP_AGGREGATE_LOOKUP = 0 58 | * OS_CAPACITY_SKIP_PROJECT_USAGE = 0 59 | * OS_CAPACITY_SKIP_HOST_USAGE = 0 60 | 61 | Here is some example output from the exporter::: 62 | 63 | # HELP openstack_free_capacity_by_flavor_total Free capacity if you fill the cloud full of each flavor 64 | # TYPE openstack_free_capacity_by_flavor_total gauge 65 | openstack_free_capacity_by_flavor_total{flavor_name="amphora"} 821.0 66 | openstack_free_capacity_by_flavor_total{flavor_name="bmtest"} 1.0 67 | openstack_free_capacity_by_flavor_total{flavor_name="large"} 46.0 68 | openstack_free_capacity_by_flavor_total{flavor_name="medium"} 94.0 69 | openstack_free_capacity_by_flavor_total{flavor_name="small"} 191.0 70 | openstack_free_capacity_by_flavor_total{flavor_name="tiny"} 385.0 71 | openstack_free_capacity_by_flavor_total{flavor_name="xlarge"} 19.0 72 | openstack_free_capacity_by_flavor_total{flavor_name="pinnned.full"} 1.0 73 | openstack_free_capacity_by_flavor_total{flavor_name="pinnned.half"} 2.0 74 | openstack_free_capacity_by_flavor_total{flavor_name="pinned.large"} 2.0 75 | openstack_free_capacity_by_flavor_total{flavor_name="pinned.quarter"} 4.0 76 | openstack_free_capacity_by_flavor_total{flavor_name="pinned.tiny"} 53.0 77 | ... 78 | # HELP openstack_free_capacity_hypervisor_by_flavor Free capacity for each hypervisor if you fill remaining space full of each flavor 79 | # TYPE openstack_free_capacity_hypervisor_by_flavor gauge 80 | openstack_free_capacity_hypervisor_by_flavor{az_aggregate="regular",flavor_name="amphora",hypervisor="ctrl1",project_aggregate="test"} 263.0 81 | ... 82 | # HELP openstack_project_filter_aggregate Mapping of project_ids to aggregates in the host free capacity info. 83 | # TYPE openstack_project_filter_aggregate gauge 84 | openstack_project_filter_aggregate{aggregate="test",project_id="c6992a4f9f5a45fab23114d032fca40b"} 1.0 85 | ... 86 | # HELP openstack_project_usage Current placement allocations per project. 87 | # TYPE openstack_project_usage gauge 88 | openstack_project_usage{placement_resource="VCPU",project_id="c6992a4f9f5a45fab23114d032fca40b",project_name="test"} 136.0 89 | openstack_project_usage{placement_resource="MEMORY_MB",project_id="c6992a4f9f5a45fab23114d032fca40b",project_name="test"} 278528.0 90 | openstack_project_usage{placement_resource="DISK_GB",project_id="c6992a4f9f5a45fab23114d032fca40b",project_name="test"} 1440.0 91 | ... 92 | # HELP openstack_project_quota Current quota set to limit max resource allocations per project. 93 | # TYPE openstack_project_quota gauge 94 | openstack_project_quota{project_id="c6992a4f9f5a45fab23114d032fca40b",project_name="test",quota_resource="CPUS"} -1.0 95 | openstack_project_quota{project_id="c6992a4f9f5a45fab23114d032fca40b",project_name="test",quota_resource="MEMORY_MB"} -1.0 96 | ... 97 | # HELP openstack_hypervisor_placement_allocated Currently allocated resource for each provider in placement. 98 | # TYPE openstack_hypervisor_placement_allocated gauge 99 | openstack_hypervisor_placement_allocated{hypervisor="ctrl1",resource="VCPU"} 65.0 100 | openstack_hypervisor_placement_allocated{hypervisor="ctrl1",resource="MEMORY_MB"} 132096.0 101 | openstack_hypervisor_placement_allocated{hypervisor="ctrl1",resource="DISK_GB"} 485.0 102 | ... 103 | # HELP openstack_hypervisor_placement_allocatable_capacity The total allocatable resource in the placement inventory. 104 | # TYPE openstack_hypervisor_placement_allocatable_capacity gauge 105 | openstack_hypervisor_placement_allocatable_capacity{hypervisor="ctrl1",resource="VCPU"} 320.0 106 | openstack_hypervisor_placement_allocatable_capacity{hypervisor="ctrl1",resource="MEMORY_MB"} 622635.0 107 | openstack_hypervisor_placement_allocatable_capacity{hypervisor="ctrl1",resource="DISK_GB"} 19551.0 108 | 109 | Example of a prometheus scrape config::: 110 | 111 | - job_name: os_capacity 112 | relabel_configs: 113 | - regex: ([^:]+):\d+ 114 | source_labels: 115 | - __address__ 116 | target_label: instance 117 | static_configs: 118 | - targets: 119 | - localhost:9000 120 | scrape_interval: 2m 121 | scrape_timeout: 1m 122 | 123 | Once that is in prometheus, and its not timing out, you can visualise the data 124 | by importing this grafana dashboard: 125 | https://raw.githubusercontent.com/stackhpc/os-capacity/master/grafana_cloud_dashboard.json 126 | -------------------------------------------------------------------------------- /grafana_cloud_dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "Prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__elements": {}, 13 | "__requires": [ 14 | { 15 | "type": "panel", 16 | "id": "bargauge", 17 | "name": "Bar gauge", 18 | "version": "" 19 | }, 20 | { 21 | "type": "grafana", 22 | "id": "grafana", 23 | "name": "Grafana", 24 | "version": "9.1.2" 25 | }, 26 | { 27 | "type": "panel", 28 | "id": "piechart", 29 | "name": "Pie chart", 30 | "version": "" 31 | }, 32 | { 33 | "type": "datasource", 34 | "id": "prometheus", 35 | "name": "Prometheus", 36 | "version": "1.0.0" 37 | }, 38 | { 39 | "type": "panel", 40 | "id": "timeseries", 41 | "name": "Time series", 42 | "version": "" 43 | } 44 | ], 45 | "annotations": { 46 | "list": [ 47 | { 48 | "builtIn": 1, 49 | "datasource": { 50 | "type": "grafana", 51 | "uid": "-- Grafana --" 52 | }, 53 | "enable": true, 54 | "hide": true, 55 | "iconColor": "rgba(0, 211, 255, 1)", 56 | "name": "Annotations & Alerts", 57 | "target": { 58 | "limit": 100, 59 | "matchAny": false, 60 | "tags": [], 61 | "type": "dashboard" 62 | }, 63 | "type": "dashboard" 64 | } 65 | ] 66 | }, 67 | "editable": true, 68 | "fiscalYearStartMonth": 0, 69 | "graphTooltip": 0, 70 | "id": null, 71 | "links": [], 72 | "liveNow": false, 73 | "panels": [ 74 | { 75 | "collapsed": false, 76 | "gridPos": { 77 | "h": 1, 78 | "w": 24, 79 | "x": 0, 80 | "y": 0 81 | }, 82 | "id": 13, 83 | "panels": [], 84 | "title": "Summary Hypervisor Capacity", 85 | "type": "row" 86 | }, 87 | { 88 | "datasource": { 89 | "type": "prometheus", 90 | "uid": "${DS_PROMETHEUS}" 91 | }, 92 | "fieldConfig": { 93 | "defaults": { 94 | "color": { 95 | "mode": "palette-classic" 96 | }, 97 | "custom": { 98 | "hideFrom": { 99 | "legend": false, 100 | "tooltip": false, 101 | "viz": false 102 | } 103 | }, 104 | "mappings": [], 105 | "unit": "decmbytes" 106 | }, 107 | "overrides": [] 108 | }, 109 | "gridPos": { 110 | "h": 10, 111 | "w": 8, 112 | "x": 0, 113 | "y": 1 114 | }, 115 | "id": 7, 116 | "options": { 117 | "legend": { 118 | "displayMode": "table", 119 | "placement": "bottom", 120 | "showLegend": true, 121 | "values": [ 122 | "value", 123 | "percent" 124 | ] 125 | }, 126 | "pieType": "pie", 127 | "reduceOptions": { 128 | "calcs": [ 129 | "lastNotNull" 130 | ], 131 | "fields": "", 132 | "values": false 133 | }, 134 | "tooltip": { 135 | "mode": "single", 136 | "sort": "none" 137 | } 138 | }, 139 | "targets": [ 140 | { 141 | "datasource": { 142 | "type": "prometheus", 143 | "uid": "${DS_PROMETHEUS}" 144 | }, 145 | "editorMode": "code", 146 | "expr": "SUM(openstack_hypervisor_placement_allocatable_capacity{resource=\"MEMORY_MB\"} - on(hypervisor) openstack_hypervisor_placement_allocated{resource=\"MEMORY_MB\"})", 147 | "hide": false, 148 | "legendFormat": "Free Memory", 149 | "range": true, 150 | "refId": "A" 151 | }, 152 | { 153 | "datasource": { 154 | "type": "prometheus", 155 | "uid": "${DS_PROMETHEUS}" 156 | }, 157 | "editorMode": "code", 158 | "expr": "SUM(openstack_hypervisor_placement_allocated{resource=\"MEMORY_MB\"})", 159 | "hide": false, 160 | "legendFormat": "Allocated Memory", 161 | "range": true, 162 | "refId": "B" 163 | } 164 | ], 165 | "title": "Total Memory", 166 | "type": "piechart" 167 | }, 168 | { 169 | "datasource": { 170 | "type": "prometheus", 171 | "uid": "${DS_PROMETHEUS}" 172 | }, 173 | "description": "", 174 | "fieldConfig": { 175 | "defaults": { 176 | "color": { 177 | "mode": "palette-classic" 178 | }, 179 | "custom": { 180 | "hideFrom": { 181 | "legend": false, 182 | "tooltip": false, 183 | "viz": false 184 | } 185 | }, 186 | "mappings": [], 187 | "unit": "none" 188 | }, 189 | "overrides": [] 190 | }, 191 | "gridPos": { 192 | "h": 10, 193 | "w": 9, 194 | "x": 8, 195 | "y": 1 196 | }, 197 | "id": 9, 198 | "options": { 199 | "legend": { 200 | "displayMode": "table", 201 | "placement": "bottom", 202 | "showLegend": true, 203 | "values": [ 204 | "percent", 205 | "value" 206 | ] 207 | }, 208 | "pieType": "pie", 209 | "reduceOptions": { 210 | "calcs": [ 211 | "lastNotNull" 212 | ], 213 | "fields": "", 214 | "values": false 215 | }, 216 | "tooltip": { 217 | "mode": "single", 218 | "sort": "none" 219 | } 220 | }, 221 | "targets": [ 222 | { 223 | "datasource": { 224 | "type": "prometheus", 225 | "uid": "${DS_PROMETHEUS}" 226 | }, 227 | "editorMode": "code", 228 | "expr": "SUM(openstack_hypervisor_placement_allocatable_capacity{resource=\"VCPU\"} - on(hypervisor) openstack_hypervisor_placement_allocated{resource=\"VCPU\"})", 229 | "hide": false, 230 | "legendFormat": "Free VCPU", 231 | "range": true, 232 | "refId": "A" 233 | }, 234 | { 235 | "datasource": { 236 | "type": "prometheus", 237 | "uid": "${DS_PROMETHEUS}" 238 | }, 239 | "editorMode": "code", 240 | "expr": "SUM(openstack_hypervisor_placement_allocatable_capacity{resource=\"PCPU\"} - on(hypervisor) openstack_hypervisor_placement_allocated{resource=\"PCPU\"})", 241 | "hide": false, 242 | "legendFormat": "Free PCPU", 243 | "range": true, 244 | "refId": "C" 245 | }, 246 | { 247 | "datasource": { 248 | "type": "prometheus", 249 | "uid": "${DS_PROMETHEUS}" 250 | }, 251 | "editorMode": "code", 252 | "expr": "SUM(openstack_hypervisor_placement_allocated{resource=\"VCPU\"})", 253 | "hide": false, 254 | "legendFormat": "Allocated VCPU", 255 | "range": true, 256 | "refId": "B" 257 | }, 258 | { 259 | "datasource": { 260 | "type": "prometheus", 261 | "uid": "${DS_PROMETHEUS}" 262 | }, 263 | "editorMode": "code", 264 | "expr": "SUM(openstack_hypervisor_placement_allocated{resource=\"PCPU\"})", 265 | "hide": false, 266 | "legendFormat": "Allocated PCPU", 267 | "range": true, 268 | "refId": "D" 269 | } 270 | ], 271 | "title": "Total CPU", 272 | "type": "piechart" 273 | }, 274 | { 275 | "datasource": { 276 | "type": "prometheus", 277 | "uid": "${DS_PROMETHEUS}" 278 | }, 279 | "description": "", 280 | "fieldConfig": { 281 | "defaults": { 282 | "color": { 283 | "mode": "palette-classic" 284 | }, 285 | "custom": { 286 | "hideFrom": { 287 | "legend": false, 288 | "tooltip": false, 289 | "viz": false 290 | } 291 | }, 292 | "mappings": [], 293 | "unit": "decgbytes" 294 | }, 295 | "overrides": [] 296 | }, 297 | "gridPos": { 298 | "h": 10, 299 | "w": 7, 300 | "x": 17, 301 | "y": 1 302 | }, 303 | "id": 8, 304 | "options": { 305 | "legend": { 306 | "displayMode": "table", 307 | "placement": "bottom", 308 | "showLegend": true, 309 | "values": [ 310 | "percent", 311 | "value" 312 | ] 313 | }, 314 | "pieType": "pie", 315 | "reduceOptions": { 316 | "calcs": [ 317 | "lastNotNull" 318 | ], 319 | "fields": "", 320 | "values": false 321 | }, 322 | "tooltip": { 323 | "mode": "single", 324 | "sort": "none" 325 | } 326 | }, 327 | "targets": [ 328 | { 329 | "datasource": { 330 | "type": "prometheus", 331 | "uid": "${DS_PROMETHEUS}" 332 | }, 333 | "editorMode": "code", 334 | "expr": "SUM(openstack_hypervisor_placement_allocatable_capacity{resource=\"DISK_GB\"} - on(hypervisor) openstack_hypervisor_placement_allocated{resource=\"DISK_GB\"})", 335 | "hide": false, 336 | "legendFormat": "Free Local Disk", 337 | "range": true, 338 | "refId": "A" 339 | }, 340 | { 341 | "datasource": { 342 | "type": "prometheus", 343 | "uid": "${DS_PROMETHEUS}" 344 | }, 345 | "editorMode": "code", 346 | "expr": "SUM(openstack_hypervisor_placement_allocated{resource=\"DISK_GB\"})", 347 | "hide": false, 348 | "legendFormat": "Allocated Local Disk", 349 | "range": true, 350 | "refId": "D" 351 | } 352 | ], 353 | "title": "Total Local Disk", 354 | "type": "piechart" 355 | }, 356 | { 357 | "datasource": { 358 | "type": "prometheus", 359 | "uid": "${DS_PROMETHEUS}" 360 | }, 361 | "description": "", 362 | "fieldConfig": { 363 | "defaults": { 364 | "color": { 365 | "mode": "thresholds" 366 | }, 367 | "mappings": [], 368 | "thresholds": { 369 | "mode": "absolute", 370 | "steps": [ 371 | { 372 | "color": "semi-dark-yellow", 373 | "value": null 374 | }, 375 | { 376 | "color": "green", 377 | "value": 4 378 | } 379 | ] 380 | } 381 | }, 382 | "overrides": [] 383 | }, 384 | "gridPos": { 385 | "h": 10, 386 | "w": 24, 387 | "x": 0, 388 | "y": 11 389 | }, 390 | "id": 2, 391 | "options": { 392 | "displayMode": "basic", 393 | "minVizHeight": 10, 394 | "minVizWidth": 0, 395 | "orientation": "horizontal", 396 | "reduceOptions": { 397 | "calcs": [ 398 | "lastNotNull" 399 | ], 400 | "fields": "", 401 | "values": false 402 | }, 403 | "showUnfilled": true 404 | }, 405 | "pluginVersion": "9.1.2", 406 | "targets": [ 407 | { 408 | "datasource": { 409 | "type": "prometheus", 410 | "uid": "${DS_PROMETHEUS}" 411 | }, 412 | "editorMode": "builder", 413 | "expr": "openstack_free_capacity_by_flavor_total", 414 | "format": "time_series", 415 | "legendFormat": "{{flavor_name}}", 416 | "range": true, 417 | "refId": "A" 418 | } 419 | ], 420 | "title": "Free Capacity by Flavor", 421 | "type": "bargauge" 422 | }, 423 | { 424 | "collapsed": false, 425 | "gridPos": { 426 | "h": 1, 427 | "w": 24, 428 | "x": 0, 429 | "y": 21 430 | }, 431 | "id": 11, 432 | "panels": [], 433 | "title": "Project Usage", 434 | "type": "row" 435 | }, 436 | { 437 | "datasource": { 438 | "type": "prometheus", 439 | "uid": "${DS_PROMETHEUS}" 440 | }, 441 | "fieldConfig": { 442 | "defaults": { 443 | "color": { 444 | "mode": "palette-classic" 445 | }, 446 | "custom": { 447 | "axisCenteredZero": false, 448 | "axisColorMode": "text", 449 | "axisLabel": "", 450 | "axisPlacement": "auto", 451 | "barAlignment": 0, 452 | "drawStyle": "line", 453 | "fillOpacity": 23, 454 | "gradientMode": "none", 455 | "hideFrom": { 456 | "legend": false, 457 | "tooltip": false, 458 | "viz": false 459 | }, 460 | "lineInterpolation": "linear", 461 | "lineWidth": 1, 462 | "pointSize": 5, 463 | "scaleDistribution": { 464 | "type": "linear" 465 | }, 466 | "showPoints": "auto", 467 | "spanNulls": false, 468 | "stacking": { 469 | "group": "A", 470 | "mode": "none" 471 | }, 472 | "thresholdsStyle": { 473 | "mode": "off" 474 | } 475 | }, 476 | "mappings": [], 477 | "thresholds": { 478 | "mode": "absolute", 479 | "steps": [ 480 | { 481 | "color": "green", 482 | "value": null 483 | }, 484 | { 485 | "color": "red", 486 | "value": 80 487 | } 488 | ] 489 | }, 490 | "unit": "decmbytes" 491 | }, 492 | "overrides": [] 493 | }, 494 | "gridPos": { 495 | "h": 8, 496 | "w": 12, 497 | "x": 0, 498 | "y": 22 499 | }, 500 | "id": 5, 501 | "options": { 502 | "legend": { 503 | "calcs": [ 504 | "min", 505 | "max" 506 | ], 507 | "displayMode": "table", 508 | "placement": "bottom", 509 | "showLegend": true, 510 | "sortBy": "Max", 511 | "sortDesc": true 512 | }, 513 | "tooltip": { 514 | "mode": "single", 515 | "sort": "none" 516 | } 517 | }, 518 | "targets": [ 519 | { 520 | "datasource": { 521 | "type": "prometheus", 522 | "uid": "${DS_PROMETHEUS}" 523 | }, 524 | "editorMode": "code", 525 | "expr": "openstack_project_usage{placement_resource=\"MEMORY_MB\"}", 526 | "legendFormat": "{{project_name}}", 527 | "range": true, 528 | "refId": "A" 529 | } 530 | ], 531 | "title": "Memory Used by Project", 532 | "type": "timeseries" 533 | }, 534 | { 535 | "datasource": { 536 | "type": "prometheus", 537 | "uid": "${DS_PROMETHEUS}" 538 | }, 539 | "fieldConfig": { 540 | "defaults": { 541 | "color": { 542 | "mode": "palette-classic" 543 | }, 544 | "custom": { 545 | "axisCenteredZero": false, 546 | "axisColorMode": "text", 547 | "axisLabel": "", 548 | "axisPlacement": "auto", 549 | "barAlignment": 0, 550 | "drawStyle": "line", 551 | "fillOpacity": 23, 552 | "gradientMode": "none", 553 | "hideFrom": { 554 | "legend": false, 555 | "tooltip": false, 556 | "viz": false 557 | }, 558 | "lineInterpolation": "linear", 559 | "lineWidth": 1, 560 | "pointSize": 5, 561 | "scaleDistribution": { 562 | "type": "linear" 563 | }, 564 | "showPoints": "auto", 565 | "spanNulls": false, 566 | "stacking": { 567 | "group": "A", 568 | "mode": "none" 569 | }, 570 | "thresholdsStyle": { 571 | "mode": "off" 572 | } 573 | }, 574 | "mappings": [], 575 | "thresholds": { 576 | "mode": "absolute", 577 | "steps": [ 578 | { 579 | "color": "green", 580 | "value": null 581 | }, 582 | { 583 | "color": "red", 584 | "value": 80 585 | } 586 | ] 587 | }, 588 | "unit": "decmbytes" 589 | }, 590 | "overrides": [] 591 | }, 592 | "gridPos": { 593 | "h": 8, 594 | "w": 12, 595 | "x": 12, 596 | "y": 22 597 | }, 598 | "id": 16, 599 | "options": { 600 | "legend": { 601 | "calcs": [ 602 | "min", 603 | "max" 604 | ], 605 | "displayMode": "table", 606 | "placement": "bottom", 607 | "showLegend": true, 608 | "sortBy": "Max", 609 | "sortDesc": true 610 | }, 611 | "tooltip": { 612 | "mode": "single", 613 | "sort": "none" 614 | } 615 | }, 616 | "targets": [ 617 | { 618 | "datasource": { 619 | "type": "prometheus", 620 | "uid": "${DS_PROMETHEUS}" 621 | }, 622 | "editorMode": "code", 623 | "expr": "openstack_project_usage{placement_resource=\"VCPU\"}", 624 | "legendFormat": "VCPU {{project_name}}", 625 | "range": true, 626 | "refId": "A" 627 | }, 628 | { 629 | "datasource": { 630 | "type": "prometheus", 631 | "uid": "${DS_PROMETHEUS}" 632 | }, 633 | "editorMode": "code", 634 | "expr": "openstack_project_usage{placement_resource=\"PCPU\"}", 635 | "hide": false, 636 | "legendFormat": "PCPU {{project_name}}", 637 | "range": true, 638 | "refId": "B" 639 | } 640 | ], 641 | "title": "CPUs Used by Project", 642 | "type": "timeseries" 643 | }, 644 | { 645 | "collapsed": false, 646 | "gridPos": { 647 | "h": 1, 648 | "w": 24, 649 | "x": 0, 650 | "y": 30 651 | }, 652 | "id": 15, 653 | "panels": [], 654 | "title": "Per Hypervisor Free Capacity", 655 | "type": "row" 656 | }, 657 | { 658 | "datasource": { 659 | "type": "prometheus", 660 | "uid": "${DS_PROMETHEUS}" 661 | }, 662 | "description": "", 663 | "fieldConfig": { 664 | "defaults": { 665 | "color": { 666 | "mode": "palette-classic" 667 | }, 668 | "custom": { 669 | "axisCenteredZero": false, 670 | "axisColorMode": "text", 671 | "axisLabel": "", 672 | "axisPlacement": "auto", 673 | "barAlignment": 0, 674 | "drawStyle": "line", 675 | "fillOpacity": 29, 676 | "gradientMode": "none", 677 | "hideFrom": { 678 | "legend": false, 679 | "tooltip": false, 680 | "viz": false 681 | }, 682 | "lineInterpolation": "smooth", 683 | "lineWidth": 1, 684 | "pointSize": 5, 685 | "scaleDistribution": { 686 | "type": "linear" 687 | }, 688 | "showPoints": "auto", 689 | "spanNulls": false, 690 | "stacking": { 691 | "group": "A", 692 | "mode": "none" 693 | }, 694 | "thresholdsStyle": { 695 | "mode": "off" 696 | } 697 | }, 698 | "mappings": [], 699 | "thresholds": { 700 | "mode": "absolute", 701 | "steps": [ 702 | { 703 | "color": "green", 704 | "value": null 705 | }, 706 | { 707 | "color": "red", 708 | "value": 80 709 | } 710 | ] 711 | } 712 | }, 713 | "overrides": [] 714 | }, 715 | "gridPos": { 716 | "h": 13, 717 | "w": 12, 718 | "x": 0, 719 | "y": 31 720 | }, 721 | "id": 6, 722 | "options": { 723 | "legend": { 724 | "calcs": [ 725 | "min", 726 | "max", 727 | "mean" 728 | ], 729 | "displayMode": "table", 730 | "placement": "bottom", 731 | "showLegend": true, 732 | "sortBy": "Max", 733 | "sortDesc": true 734 | }, 735 | "tooltip": { 736 | "mode": "single", 737 | "sort": "none" 738 | } 739 | }, 740 | "pluginVersion": "9.1.2", 741 | "targets": [ 742 | { 743 | "datasource": { 744 | "type": "prometheus", 745 | "uid": "${DS_PROMETHEUS}" 746 | }, 747 | "editorMode": "code", 748 | "expr": "sum(openstack_free_capacity_hypervisor_by_flavor) by (flavor_name, az_aggregate)", 749 | "format": "time_series", 750 | "legendFormat": "{{az_aggregate}} : {{flavor_name}} ", 751 | "range": true, 752 | "refId": "A" 753 | } 754 | ], 755 | "title": "Free Capacity by Flavor and AZ", 756 | "type": "timeseries" 757 | }, 758 | { 759 | "datasource": { 760 | "type": "prometheus", 761 | "uid": "${DS_PROMETHEUS}" 762 | }, 763 | "fieldConfig": { 764 | "defaults": { 765 | "color": { 766 | "mode": "palette-classic" 767 | }, 768 | "custom": { 769 | "axisCenteredZero": false, 770 | "axisColorMode": "text", 771 | "axisLabel": "", 772 | "axisPlacement": "auto", 773 | "barAlignment": 0, 774 | "drawStyle": "line", 775 | "fillOpacity": 21, 776 | "gradientMode": "none", 777 | "hideFrom": { 778 | "legend": false, 779 | "tooltip": false, 780 | "viz": false 781 | }, 782 | "lineInterpolation": "linear", 783 | "lineWidth": 1, 784 | "pointSize": 5, 785 | "scaleDistribution": { 786 | "type": "linear" 787 | }, 788 | "showPoints": "auto", 789 | "spanNulls": false, 790 | "stacking": { 791 | "group": "A", 792 | "mode": "none" 793 | }, 794 | "thresholdsStyle": { 795 | "mode": "off" 796 | } 797 | }, 798 | "mappings": [], 799 | "thresholds": { 800 | "mode": "absolute", 801 | "steps": [ 802 | { 803 | "color": "green", 804 | "value": null 805 | }, 806 | { 807 | "color": "red", 808 | "value": 80 809 | } 810 | ] 811 | }, 812 | "unit": "decmbytes" 813 | }, 814 | "overrides": [] 815 | }, 816 | "gridPos": { 817 | "h": 13, 818 | "w": 12, 819 | "x": 12, 820 | "y": 31 821 | }, 822 | "id": 4, 823 | "options": { 824 | "legend": { 825 | "calcs": [ 826 | "min", 827 | "max" 828 | ], 829 | "displayMode": "table", 830 | "placement": "bottom", 831 | "showLegend": true, 832 | "sortBy": "Max", 833 | "sortDesc": true 834 | }, 835 | "tooltip": { 836 | "mode": "single", 837 | "sort": "none" 838 | } 839 | }, 840 | "targets": [ 841 | { 842 | "datasource": { 843 | "type": "prometheus", 844 | "uid": "${DS_PROMETHEUS}" 845 | }, 846 | "editorMode": "builder", 847 | "expr": "openstack_hypervisor_placement_allocatable_capacity{resource=\"MEMORY_MB\"} - on(hypervisor) openstack_hypervisor_placement_allocated{resource=\"MEMORY_MB\"}", 848 | "legendFormat": "{{hypervisor}}", 849 | "range": true, 850 | "refId": "A" 851 | } 852 | ], 853 | "title": "Memory Free by Hypervisor", 854 | "type": "timeseries" 855 | } 856 | ], 857 | "schemaVersion": 37, 858 | "style": "dark", 859 | "tags": [], 860 | "templating": { 861 | "list": [ 862 | { 863 | "current": { 864 | "selected": false, 865 | "text": "Prometheus", 866 | "value": "Prometheus" 867 | }, 868 | "description": "The prometheus datasource used for queries.", 869 | "hide": 0, 870 | "includeAll": false, 871 | "label": "datasource", 872 | "multi": false, 873 | "name": "DS_PROMETHEUS", 874 | "options": [], 875 | "query": "prometheus", 876 | "queryValue": "", 877 | "refresh": 1, 878 | "regex": "", 879 | "skipUrlSync": false, 880 | "type": "datasource" 881 | } 882 | ] 883 | }, 884 | "time": { 885 | "from": "now-1h", 886 | "to": "now" 887 | }, 888 | "timepicker": {}, 889 | "timezone": "", 890 | "title": "Cloud Metrics", 891 | "uid": "g__ksD67z", 892 | "version": 14, 893 | "weekStart": "" 894 | } 895 | -------------------------------------------------------------------------------- /grafana_project_dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "datasource", 8 | "uid": "grafana" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "target": { 15 | "limit": 100, 16 | "matchAny": false, 17 | "tags": [], 18 | "type": "dashboard" 19 | }, 20 | "type": "dashboard" 21 | } 22 | ] 23 | }, 24 | "editable": true, 25 | "fiscalYearStartMonth": 0, 26 | "graphTooltip": 0, 27 | "id": 83, 28 | "links": [], 29 | "liveNow": false, 30 | "panels": [ 31 | { 32 | "collapsed": false, 33 | "gridPos": { 34 | "h": 1, 35 | "w": 24, 36 | "x": 0, 37 | "y": 0 38 | }, 39 | "id": 11, 40 | "panels": [], 41 | "title": "Project Usage", 42 | "type": "row" 43 | }, 44 | { 45 | "datasource": { 46 | "type": "prometheus", 47 | "uid": "${datasource}" 48 | }, 49 | "description": "", 50 | "fieldConfig": { 51 | "defaults": { 52 | "color": { 53 | "mode": "thresholds" 54 | }, 55 | "mappings": [], 56 | "thresholds": { 57 | "mode": "absolute", 58 | "steps": [ 59 | { 60 | "color": "semi-dark-yellow", 61 | "value": null 62 | }, 63 | { 64 | "color": "green", 65 | "value": 1 66 | } 67 | ] 68 | } 69 | }, 70 | "overrides": [] 71 | }, 72 | "gridPos": { 73 | "h": 10, 74 | "w": 24, 75 | "x": 0, 76 | "y": 1 77 | }, 78 | "id": 18, 79 | "options": { 80 | "displayMode": "basic", 81 | "minVizHeight": 10, 82 | "minVizWidth": 0, 83 | "orientation": "horizontal", 84 | "reduceOptions": { 85 | "calcs": [ 86 | "lastNotNull" 87 | ], 88 | "fields": "", 89 | "values": false 90 | }, 91 | "showUnfilled": true 92 | }, 93 | "pluginVersion": "9.1.2", 94 | "targets": [ 95 | { 96 | "datasource": { 97 | "type": "prometheus", 98 | "uid": "${datasource}" 99 | }, 100 | "editorMode": "code", 101 | "expr": "max(openstack_project_usage{project_id=~\"${project_id}\"}) by (placement_resource)", 102 | "format": "time_series", 103 | "hide": false, 104 | "legendFormat": "{{flavor_name}}", 105 | "range": true, 106 | "refId": "A" 107 | } 108 | ], 109 | "title": "Max usage", 110 | "type": "bargauge" 111 | }, 112 | { 113 | "datasource": { 114 | "type": "prometheus", 115 | "uid": "${datasource}" 116 | }, 117 | "fieldConfig": { 118 | "defaults": { 119 | "color": { 120 | "mode": "palette-classic" 121 | }, 122 | "custom": { 123 | "axisCenteredZero": false, 124 | "axisColorMode": "text", 125 | "axisLabel": "", 126 | "axisPlacement": "auto", 127 | "barAlignment": 0, 128 | "drawStyle": "line", 129 | "fillOpacity": 23, 130 | "gradientMode": "none", 131 | "hideFrom": { 132 | "legend": false, 133 | "tooltip": false, 134 | "viz": false 135 | }, 136 | "lineInterpolation": "linear", 137 | "lineWidth": 1, 138 | "pointSize": 5, 139 | "scaleDistribution": { 140 | "type": "linear" 141 | }, 142 | "showPoints": "auto", 143 | "spanNulls": false, 144 | "stacking": { 145 | "group": "A", 146 | "mode": "none" 147 | }, 148 | "thresholdsStyle": { 149 | "mode": "off" 150 | } 151 | }, 152 | "mappings": [], 153 | "thresholds": { 154 | "mode": "absolute", 155 | "steps": [ 156 | { 157 | "color": "green", 158 | "value": null 159 | }, 160 | { 161 | "color": "red", 162 | "value": 80 163 | } 164 | ] 165 | }, 166 | "unit": "none" 167 | }, 168 | "overrides": [] 169 | }, 170 | "gridPos": { 171 | "h": 9, 172 | "w": 8, 173 | "x": 0, 174 | "y": 11 175 | }, 176 | "id": 5, 177 | "options": { 178 | "legend": { 179 | "calcs": [ 180 | "min", 181 | "max" 182 | ], 183 | "displayMode": "table", 184 | "placement": "bottom", 185 | "showLegend": true, 186 | "sortBy": "Max", 187 | "sortDesc": true 188 | }, 189 | "tooltip": { 190 | "mode": "single", 191 | "sort": "none" 192 | } 193 | }, 194 | "targets": [ 195 | { 196 | "datasource": { 197 | "type": "prometheus", 198 | "uid": "${datasource}" 199 | }, 200 | "editorMode": "code", 201 | "expr": "sum(openstack_project_usage{project_id=~\"${project_id}\"}) by (placement_resource)", 202 | "hide": false, 203 | "legendFormat": "{{placement_resource}}", 204 | "range": true, 205 | "refId": "A" 206 | } 207 | ], 208 | "title": "Resources per Project", 209 | "type": "timeseries" 210 | }, 211 | { 212 | "datasource": { 213 | "type": "prometheus", 214 | "uid": "${datasource}" 215 | }, 216 | "description": "", 217 | "fieldConfig": { 218 | "defaults": { 219 | "color": { 220 | "mode": "palette-classic" 221 | }, 222 | "custom": { 223 | "axisCenteredZero": false, 224 | "axisColorMode": "text", 225 | "axisLabel": "", 226 | "axisPlacement": "auto", 227 | "barAlignment": 0, 228 | "drawStyle": "line", 229 | "fillOpacity": 23, 230 | "gradientMode": "none", 231 | "hideFrom": { 232 | "legend": false, 233 | "tooltip": false, 234 | "viz": false 235 | }, 236 | "lineInterpolation": "linear", 237 | "lineWidth": 1, 238 | "pointSize": 5, 239 | "scaleDistribution": { 240 | "type": "linear" 241 | }, 242 | "showPoints": "auto", 243 | "spanNulls": false, 244 | "stacking": { 245 | "group": "A", 246 | "mode": "none" 247 | }, 248 | "thresholdsStyle": { 249 | "mode": "off" 250 | } 251 | }, 252 | "mappings": [], 253 | "thresholds": { 254 | "mode": "absolute", 255 | "steps": [ 256 | { 257 | "color": "green", 258 | "value": null 259 | }, 260 | { 261 | "color": "red", 262 | "value": 80 263 | } 264 | ] 265 | }, 266 | "unit": "none" 267 | }, 268 | "overrides": [] 269 | }, 270 | "gridPos": { 271 | "h": 9, 272 | "w": 8, 273 | "x": 8, 274 | "y": 11 275 | }, 276 | "id": 19, 277 | "options": { 278 | "legend": { 279 | "calcs": [ 280 | "min", 281 | "max" 282 | ], 283 | "displayMode": "table", 284 | "placement": "bottom", 285 | "showLegend": true, 286 | "sortBy": "Max", 287 | "sortDesc": true 288 | }, 289 | "tooltip": { 290 | "mode": "single", 291 | "sort": "none" 292 | } 293 | }, 294 | "targets": [ 295 | { 296 | "datasource": { 297 | "type": "prometheus", 298 | "uid": "${datasource}" 299 | }, 300 | "editorMode": "code", 301 | "expr": "openstack_project_quota{project_id=~\"${project_id}\"}", 302 | "hide": false, 303 | "legendFormat": "{{project_name}}:{{quota_resource}}", 304 | "range": true, 305 | "refId": "B" 306 | } 307 | ], 308 | "title": "Project quota", 309 | "type": "timeseries" 310 | }, 311 | { 312 | "datasource": { 313 | "type": "prometheus", 314 | "uid": "${datasource}" 315 | }, 316 | "fieldConfig": { 317 | "defaults": { 318 | "color": { 319 | "mode": "palette-classic" 320 | }, 321 | "custom": { 322 | "axisCenteredZero": false, 323 | "axisColorMode": "text", 324 | "axisLabel": "", 325 | "axisPlacement": "auto", 326 | "barAlignment": 0, 327 | "drawStyle": "line", 328 | "fillOpacity": 23, 329 | "gradientMode": "none", 330 | "hideFrom": { 331 | "legend": false, 332 | "tooltip": false, 333 | "viz": false 334 | }, 335 | "lineInterpolation": "linear", 336 | "lineWidth": 1, 337 | "pointSize": 5, 338 | "scaleDistribution": { 339 | "type": "linear" 340 | }, 341 | "showPoints": "auto", 342 | "spanNulls": false, 343 | "stacking": { 344 | "group": "A", 345 | "mode": "none" 346 | }, 347 | "thresholdsStyle": { 348 | "mode": "off" 349 | } 350 | }, 351 | "mappings": [], 352 | "thresholds": { 353 | "mode": "absolute", 354 | "steps": [ 355 | { 356 | "color": "green", 357 | "value": null 358 | }, 359 | { 360 | "color": "red", 361 | "value": 80 362 | } 363 | ] 364 | }, 365 | "unit": "none" 366 | }, 367 | "overrides": [] 368 | }, 369 | "gridPos": { 370 | "h": 9, 371 | "w": 8, 372 | "x": 16, 373 | "y": 11 374 | }, 375 | "id": 17, 376 | "options": { 377 | "legend": { 378 | "calcs": [ 379 | "min", 380 | "max" 381 | ], 382 | "displayMode": "table", 383 | "placement": "bottom", 384 | "showLegend": true, 385 | "sortBy": "Max", 386 | "sortDesc": true 387 | }, 388 | "tooltip": { 389 | "mode": "single", 390 | "sort": "none" 391 | } 392 | }, 393 | "targets": [ 394 | { 395 | "datasource": { 396 | "type": "prometheus", 397 | "uid": "${datasource}" 398 | }, 399 | "editorMode": "code", 400 | "expr": "count(count(libvirt_domain_info_meta{project_id=~\"${project_id}\"}) by (domain,flavor)) by (flavor)", 401 | "hide": false, 402 | "legendFormat": "__auto", 403 | "range": true, 404 | "refId": "A" 405 | } 406 | ], 407 | "title": "Servers by flavor", 408 | "type": "timeseries" 409 | }, 410 | { 411 | "datasource": { 412 | "type": "prometheus", 413 | "uid": "${datasource}" 414 | }, 415 | "fieldConfig": { 416 | "defaults": { 417 | "color": { 418 | "mode": "palette-classic" 419 | }, 420 | "custom": { 421 | "axisCenteredZero": false, 422 | "axisColorMode": "text", 423 | "axisLabel": "", 424 | "axisPlacement": "auto", 425 | "axisSoftMax": 1, 426 | "barAlignment": 0, 427 | "drawStyle": "line", 428 | "fillOpacity": 23, 429 | "gradientMode": "none", 430 | "hideFrom": { 431 | "legend": false, 432 | "tooltip": false, 433 | "viz": false 434 | }, 435 | "lineInterpolation": "linear", 436 | "lineWidth": 1, 437 | "pointSize": 5, 438 | "scaleDistribution": { 439 | "type": "linear" 440 | }, 441 | "showPoints": "auto", 442 | "spanNulls": false, 443 | "stacking": { 444 | "group": "A", 445 | "mode": "none" 446 | }, 447 | "thresholdsStyle": { 448 | "mode": "off" 449 | } 450 | }, 451 | "mappings": [], 452 | "thresholds": { 453 | "mode": "absolute", 454 | "steps": [ 455 | { 456 | "color": "green", 457 | "value": null 458 | }, 459 | { 460 | "color": "red", 461 | "value": 80 462 | } 463 | ] 464 | }, 465 | "unit": "percentunit" 466 | }, 467 | "overrides": [] 468 | }, 469 | "gridPos": { 470 | "h": 9, 471 | "w": 8, 472 | "x": 0, 473 | "y": 20 474 | }, 475 | "id": 20, 476 | "options": { 477 | "legend": { 478 | "calcs": [ 479 | "min", 480 | "max" 481 | ], 482 | "displayMode": "table", 483 | "placement": "bottom", 484 | "showLegend": true, 485 | "sortBy": "Max", 486 | "sortDesc": true 487 | }, 488 | "tooltip": { 489 | "mode": "single", 490 | "sort": "none" 491 | } 492 | }, 493 | "targets": [ 494 | { 495 | "datasource": { 496 | "type": "prometheus", 497 | "uid": "${datasource}" 498 | }, 499 | "editorMode": "code", 500 | "expr": "sum(irate(libvirt_domain_vcpu_time_seconds_total{}[5m]) / ignoring(instance,vcpu) group_left(domain) libvirt_domain_info_virtual_cpus{}) by (domain) * on(domain) group_left(instance_name,project_name,project_id) libvirt_domain_info_meta{project_id=~\"${project_id}\"}", 501 | "hide": false, 502 | "legendFormat": "{{instance_name}}", 503 | "range": true, 504 | "refId": "B" 505 | } 506 | ], 507 | "title": "CPU utilization per instance", 508 | "type": "timeseries" 509 | }, 510 | { 511 | "datasource": { 512 | "type": "prometheus", 513 | "uid": "${datasource}" 514 | }, 515 | "fieldConfig": { 516 | "defaults": { 517 | "color": { 518 | "mode": "palette-classic" 519 | }, 520 | "custom": { 521 | "axisCenteredZero": false, 522 | "axisColorMode": "text", 523 | "axisLabel": "", 524 | "axisPlacement": "auto", 525 | "barAlignment": 0, 526 | "drawStyle": "line", 527 | "fillOpacity": 23, 528 | "gradientMode": "none", 529 | "hideFrom": { 530 | "legend": false, 531 | "tooltip": false, 532 | "viz": false 533 | }, 534 | "lineInterpolation": "linear", 535 | "lineWidth": 1, 536 | "pointSize": 5, 537 | "scaleDistribution": { 538 | "type": "linear" 539 | }, 540 | "showPoints": "auto", 541 | "spanNulls": false, 542 | "stacking": { 543 | "group": "A", 544 | "mode": "none" 545 | }, 546 | "thresholdsStyle": { 547 | "mode": "off" 548 | } 549 | }, 550 | "mappings": [], 551 | "max": 100, 552 | "thresholds": { 553 | "mode": "absolute", 554 | "steps": [ 555 | { 556 | "color": "green", 557 | "value": null 558 | }, 559 | { 560 | "color": "red", 561 | "value": 80 562 | } 563 | ] 564 | }, 565 | "unit": "percent" 566 | }, 567 | "overrides": [] 568 | }, 569 | "gridPos": { 570 | "h": 9, 571 | "w": 8, 572 | "x": 8, 573 | "y": 20 574 | }, 575 | "id": 21, 576 | "options": { 577 | "legend": { 578 | "calcs": [ 579 | "min", 580 | "max" 581 | ], 582 | "displayMode": "table", 583 | "placement": "bottom", 584 | "showLegend": true, 585 | "sortBy": "Max", 586 | "sortDesc": true 587 | }, 588 | "tooltip": { 589 | "mode": "single", 590 | "sort": "none" 591 | } 592 | }, 593 | "targets": [ 594 | { 595 | "datasource": { 596 | "type": "prometheus", 597 | "uid": "${datasource}" 598 | }, 599 | "editorMode": "code", 600 | "expr": "libvirt_domain_memory_stats_used_percent * on(domain) group_left(instance_name,project_name,project_id) libvirt_domain_info_meta{project_id=~\"${project_id}\"}", 601 | "hide": false, 602 | "legendFormat": "{{instance_name}}", 603 | "range": true, 604 | "refId": "A" 605 | } 606 | ], 607 | "title": "Memory utilization per instance", 608 | "type": "timeseries" 609 | }, 610 | { 611 | "datasource": { 612 | "type": "prometheus", 613 | "uid": "${datasource}" 614 | }, 615 | "description": "", 616 | "fieldConfig": { 617 | "defaults": { 618 | "color": { 619 | "mode": "palette-classic" 620 | }, 621 | "custom": { 622 | "axisCenteredZero": true, 623 | "axisColorMode": "text", 624 | "axisLabel": "", 625 | "axisPlacement": "auto", 626 | "barAlignment": 0, 627 | "drawStyle": "line", 628 | "fillOpacity": 23, 629 | "gradientMode": "none", 630 | "hideFrom": { 631 | "legend": false, 632 | "tooltip": false, 633 | "viz": false 634 | }, 635 | "lineInterpolation": "linear", 636 | "lineWidth": 1, 637 | "pointSize": 5, 638 | "scaleDistribution": { 639 | "type": "linear" 640 | }, 641 | "showPoints": "auto", 642 | "spanNulls": false, 643 | "stacking": { 644 | "group": "A", 645 | "mode": "none" 646 | }, 647 | "thresholdsStyle": { 648 | "mode": "off" 649 | } 650 | }, 651 | "mappings": [], 652 | "max": 1, 653 | "min": -1, 654 | "thresholds": { 655 | "mode": "absolute", 656 | "steps": [ 657 | { 658 | "color": "green", 659 | "value": null 660 | }, 661 | { 662 | "color": "red", 663 | "value": 80 664 | } 665 | ] 666 | }, 667 | "unit": "percentunit" 668 | }, 669 | "overrides": [] 670 | }, 671 | "gridPos": { 672 | "h": 9, 673 | "w": 8, 674 | "x": 16, 675 | "y": 20 676 | }, 677 | "id": 22, 678 | "options": { 679 | "legend": { 680 | "calcs": [ 681 | "min", 682 | "max" 683 | ], 684 | "displayMode": "table", 685 | "placement": "bottom", 686 | "showLegend": true, 687 | "sortBy": "Max", 688 | "sortDesc": true 689 | }, 690 | "tooltip": { 691 | "mode": "single", 692 | "sort": "none" 693 | } 694 | }, 695 | "targets": [ 696 | { 697 | "datasource": { 698 | "type": "prometheus", 699 | "uid": "${datasource}" 700 | }, 701 | "editorMode": "code", 702 | "expr": "rate(libvirt_domain_block_stats_read_time_seconds_total[5m]) * on(domain) group_left(instance_name,project_name,project_id) libvirt_domain_info_meta{project_id=~\"${project_id}\"}", 703 | "hide": false, 704 | "legendFormat": "{{instance_name}} : read {{target_device}}", 705 | "range": true, 706 | "refId": "B" 707 | }, 708 | { 709 | "datasource": { 710 | "type": "prometheus", 711 | "uid": "${datasource}" 712 | }, 713 | "editorMode": "code", 714 | "expr": "rate(libvirt_domain_block_stats_write_time_seconds_total[5m]) * on(domain) group_left(instance_name,project_name,project_id) libvirt_domain_info_meta{project_id=~\"${project_id}\"} * -1", 715 | "hide": false, 716 | "legendFormat": "{{instance_name}} : write {{target_device}}", 717 | "range": true, 718 | "refId": "C" 719 | } 720 | ], 721 | "title": "Disk utilization per instance", 722 | "type": "timeseries" 723 | }, 724 | { 725 | "collapsed": false, 726 | "gridPos": { 727 | "h": 1, 728 | "w": 24, 729 | "x": 0, 730 | "y": 29 731 | }, 732 | "id": 15, 733 | "panels": [], 734 | "title": "Per Hypervisor Free Capacity", 735 | "type": "row" 736 | }, 737 | { 738 | "datasource": { 739 | "type": "prometheus", 740 | "uid": "${datasource}" 741 | }, 742 | "description": "", 743 | "fieldConfig": { 744 | "defaults": { 745 | "color": { 746 | "mode": "thresholds" 747 | }, 748 | "mappings": [], 749 | "thresholds": { 750 | "mode": "absolute", 751 | "steps": [ 752 | { 753 | "color": "semi-dark-yellow", 754 | "value": null 755 | }, 756 | { 757 | "color": "green", 758 | "value": 4 759 | } 760 | ] 761 | } 762 | }, 763 | "overrides": [] 764 | }, 765 | "gridPos": { 766 | "h": 10, 767 | "w": 24, 768 | "x": 0, 769 | "y": 30 770 | }, 771 | "id": 2, 772 | "options": { 773 | "displayMode": "basic", 774 | "minVizHeight": 10, 775 | "minVizWidth": 0, 776 | "orientation": "horizontal", 777 | "reduceOptions": { 778 | "calcs": [ 779 | "lastNotNull" 780 | ], 781 | "fields": "", 782 | "values": false 783 | }, 784 | "showUnfilled": true 785 | }, 786 | "pluginVersion": "9.1.2", 787 | "targets": [ 788 | { 789 | "datasource": { 790 | "type": "prometheus", 791 | "uid": "${datasource}" 792 | }, 793 | "editorMode": "builder", 794 | "expr": "openstack_free_capacity_by_flavor_total", 795 | "format": "time_series", 796 | "legendFormat": "{{flavor_name}}", 797 | "range": true, 798 | "refId": "A" 799 | } 800 | ], 801 | "title": "Free Capacity by Flavor", 802 | "type": "bargauge" 803 | } 804 | ], 805 | "schemaVersion": 37, 806 | "style": "dark", 807 | "tags": [ 808 | "capacity", 809 | "azimuth" 810 | ], 811 | "templating": { 812 | "list": [ 813 | { 814 | "current": { 815 | "selected": false, 816 | "text": "Prometheus", 817 | "value": "Prometheus" 818 | }, 819 | "description": "The prometheus datasource used for queries.", 820 | "hide": 0, 821 | "includeAll": false, 822 | "label": "datasource", 823 | "multi": false, 824 | "name": "datasource", 825 | "options": [], 826 | "query": "prometheus", 827 | "queryValue": "", 828 | "refresh": 1, 829 | "regex": "", 830 | "skipUrlSync": false, 831 | "type": "datasource" 832 | }, 833 | { 834 | "current": { 835 | "selected": true, 836 | "text": [ 837 | "All" 838 | ], 839 | "value": [ 840 | "$__all" 841 | ] 842 | }, 843 | "datasource": { 844 | "type": "prometheus", 845 | "uid": "${datasource}" 846 | }, 847 | "definition": "openstack_project_usage", 848 | "hide": 0, 849 | "includeAll": true, 850 | "label": "Project", 851 | "multi": true, 852 | "name": "project_id", 853 | "options": [], 854 | "query": { 855 | "query": "openstack_project_usage", 856 | "refId": "StandardVariableQuery" 857 | }, 858 | "refresh": 1, 859 | "regex": "/project_name=\"(?[^\"]+)|project_id=\"(?[^\"]+)/g", 860 | "skipUrlSync": false, 861 | "sort": 0, 862 | "type": "query" 863 | } 864 | ] 865 | }, 866 | "time": { 867 | "from": "now-3h", 868 | "to": "now" 869 | }, 870 | "timepicker": {}, 871 | "timezone": "", 872 | "title": "OpenStack Project Metrics", 873 | "uid": "mXiuBDe7z", 874 | "version": 11, 875 | "weekStart": "" 876 | } 877 | -------------------------------------------------------------------------------- /os_capacity/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackhpc/os-capacity/09f29d9303902766212dedda1879f2ee49deb56b/os_capacity/__init__.py -------------------------------------------------------------------------------- /os_capacity/prometheus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 3 | # not use this file except in compliance with the License. You may obtain 4 | # a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11 | # License for the specific language governing permissions and limitations 12 | # under the License. 13 | 14 | import collections 15 | import os 16 | import time 17 | import uuid 18 | 19 | import openstack 20 | import prometheus_client as prom_client 21 | from prometheus_client import core as prom_core 22 | 23 | RESOURCE_PROVIDER_AGGREGATE_CACHE = {} 24 | 25 | 26 | def get_capacity_per_flavor(placement_client, flavors): 27 | capacity_per_flavor = {} 28 | 29 | for flavor in flavors: 30 | max_per_host = get_max_per_host(placement_client, flavor) 31 | capacity_per_flavor[flavor.name] = max_per_host 32 | 33 | return capacity_per_flavor 34 | 35 | 36 | def get_placement_request(flavor): 37 | resources = {} 38 | required_traits = [] 39 | 40 | def add_defaults(resources, flavor, skip_vcpu=False): 41 | resources["MEMORY_MB"] = flavor.ram 42 | resources["DISK_GB"] = flavor.disk + flavor.ephemeral 43 | if not skip_vcpu: 44 | resources["VCPU"] = flavor.vcpus 45 | 46 | for key, value in flavor.extra_specs.items(): 47 | if "trait:" == key[:6]: 48 | if value == "required": 49 | required_traits.append(key[6:]) 50 | if "resources:" == key[:10]: 51 | count = int(value) 52 | resources[key[10:]] = count 53 | if "hw:cpu_policy" == key and value == "dedicated": 54 | resources["PCPU"] = flavor.vcpus 55 | add_defaults(resources, flavor, skip_vcpu=True) 56 | 57 | # if not baremetal and not PCPU 58 | # we should add the default vcpu ones 59 | if not resources: 60 | add_defaults(resources, flavor) 61 | 62 | return resources, required_traits 63 | 64 | 65 | def get_max_per_host(placement_client, flavor): 66 | resources, required_traits = get_placement_request(flavor) 67 | resource_str = ",".join( 68 | [key + ":" + str(value) for key, value in resources.items() if value] 69 | ) 70 | required_str = ",".join(required_traits) 71 | # TODO(johngarbut): remove disabled! 72 | if required_str: 73 | required_str += "," 74 | required_str += "!COMPUTE_STATUS_DISABLED" 75 | 76 | params = {"resources": resource_str} 77 | if not resource_str: 78 | raise Exception("we must have some resources here!") 79 | if required_str: 80 | params["required"] = required_str 81 | 82 | response = placement_client.get( 83 | "/allocation_candidates", 84 | params=params, 85 | headers={"OpenStack-API-Version": "placement 1.29"}, 86 | ) 87 | raw_data = response.json() 88 | count_per_rp = {} 89 | for rp_uuid, summary in raw_data.get("provider_summaries", {}).items(): 90 | # per resource, get max possible number of instances 91 | max_counts = [] 92 | for resource, amounts in summary["resources"].items(): 93 | requested = resources.get(resource, 0) 94 | if requested: 95 | free = amounts["capacity"] - amounts["used"] 96 | amount = int(free / requested) 97 | max_counts.append(amount) 98 | # available count is the min of the max counts 99 | if max_counts: 100 | count_per_rp[rp_uuid] = min(max_counts) 101 | if not count_per_rp: 102 | print(f"# WARNING - no candidates hosts for flavor: {flavor.name} {params}") 103 | return count_per_rp 104 | 105 | 106 | def get_resource_provider_info(compute_client, placement_client): 107 | # get host aggregates to look up things like az 108 | nova_aggregates = list(compute_client.aggregates()) 109 | 110 | azones = {} 111 | project_filters = {} 112 | for agg in nova_aggregates: 113 | az = agg.metadata.get("availability_zone") 114 | if az: 115 | azones[agg.uuid] = az 116 | 117 | projects = [] 118 | for key in agg.metadata.keys(): 119 | if key.startswith("filter_tenant_id"): 120 | projects.append(agg.metadata[key]) 121 | if projects: 122 | # TODO(johngarbutt): expose project id to aggregate names? 123 | project_filters[agg.uuid] = {"name": agg.name, "projects": projects} 124 | 125 | raw_rps = list(placement_client.resource_providers()) 126 | 127 | skip_aggregate_lookup = ( 128 | int(os.environ.get("OS_CAPACITY_SKIP_AGGREGATE_LOOKUP", "0")) == 1 129 | ) 130 | resource_providers = {} 131 | for raw_rp in raw_rps: 132 | rp = {"uuid": raw_rp.id} 133 | resource_providers[raw_rp.name] = rp 134 | 135 | if skip_aggregate_lookup: 136 | # skip checking every resource provider for their aggregates 137 | continue 138 | 139 | # TODO(johngarbutt): maybe check if cached aggregate still exists? 140 | aggregates = RESOURCE_PROVIDER_AGGREGATE_CACHE.get(raw_rp.id) 141 | if aggregates is None: 142 | response = placement_client.get( 143 | f"/resource_providers/{raw_rp.id}/aggregates", 144 | headers={"OpenStack-API-Version": "placement 1.19"}, 145 | ) 146 | response.raise_for_status() 147 | aggs = response.json() 148 | rp["aggregates"] = aggs["aggregates"] 149 | RESOURCE_PROVIDER_AGGREGATE_CACHE[raw_rp.id] = aggs["aggregates"] 150 | else: 151 | rp["aggregates"] = aggregates 152 | 153 | for agg_id in rp["aggregates"]: 154 | if agg_id in azones: 155 | rp["az"] = azones[agg_id] 156 | if agg_id in project_filters: 157 | # TODO(johngarbutt): loosing info here 158 | if "project_filter" in rp: 159 | rp["project_filter"] = "multiple" 160 | else: 161 | rp["project_filter"] = project_filters[agg_id]["name"] 162 | 163 | project_to_aggregate = collections.defaultdict(list) 164 | for filter_info in project_filters.values(): 165 | name = filter_info["name"] 166 | for project in filter_info["projects"]: 167 | project_to_aggregate[project] += [name] 168 | 169 | return resource_providers, project_to_aggregate 170 | 171 | 172 | def get_host_details(compute_client, placement_client): 173 | flavors = list(compute_client.flavors()) 174 | capacity_per_flavor = get_capacity_per_flavor(placement_client, flavors) 175 | 176 | # total capacity per flavor 177 | free_by_flavor_total = prom_core.GaugeMetricFamily( 178 | "openstack_free_capacity_by_flavor_total", 179 | "Free capacity if you fill the cloud full of each flavor", 180 | labels=["flavor_name", "public"], 181 | ) 182 | 183 | for flavor in flavors: 184 | counts = capacity_per_flavor.get(flavor.name, {}).values() 185 | total = 0 if not counts else sum(counts) 186 | free_by_flavor_total.add_metric([flavor.name, str(flavor.is_public)], total) 187 | 188 | # capacity per host 189 | free_by_flavor_hypervisor = prom_core.GaugeMetricFamily( 190 | "openstack_free_capacity_hypervisor_by_flavor", 191 | "Free capacity for each hypervisor if you fill " 192 | "remaining space full of each flavor", 193 | labels=["hypervisor", "flavor_name", "az_aggregate", "project_aggregate"], 194 | ) 195 | resource_providers, project_to_aggregate = get_resource_provider_info( 196 | compute_client, placement_client 197 | ) 198 | hostnames = sorted(resource_providers.keys()) 199 | for hostname in hostnames: 200 | rp = resource_providers[hostname] 201 | rp_id = rp["uuid"] 202 | free_space_found = False 203 | for flavor in flavors: 204 | flavor_name = flavor.name 205 | all_counts = capacity_per_flavor.get(flavor_name, {}) 206 | our_count = all_counts.get(rp_id, 0) 207 | if our_count == 0: 208 | continue 209 | az = rp.get("az", "") 210 | project_filter = rp.get("project_filter", "") 211 | free_by_flavor_hypervisor.add_metric( 212 | [hostname, flavor_name, az, project_filter], our_count 213 | ) 214 | free_space_found = True 215 | if not free_space_found: 216 | # TODO(johngarbutt) allocation candidates only returns some, 217 | # not all candidates! 218 | print(f"# WARNING - no free spaces found for {hostname}") 219 | 220 | project_filter_aggregates = prom_core.GaugeMetricFamily( 221 | "openstack_project_filter_aggregate", 222 | "Mapping of project_ids to aggregates in the host free capacity info.", 223 | labels=["project_id", "aggregate"], 224 | ) 225 | for project, names in project_to_aggregate.items(): 226 | for name in names: 227 | project_filter_aggregates.add_metric([project, name], 1) 228 | return resource_providers, [ 229 | free_by_flavor_total, 230 | free_by_flavor_hypervisor, 231 | project_filter_aggregates, 232 | ] 233 | 234 | 235 | def get_project_usage(indentity_client, placement_client, compute_client): 236 | projects = {proj.id: dict(name=proj.name) for proj in indentity_client.projects()} 237 | for project_id in projects.keys(): 238 | # TODO(johngarbutt) On Xena we should do consumer_type=INSTANCE using 1.38! 239 | response = placement_client.get( 240 | f"/usages?project_id={project_id}", 241 | headers={"OpenStack-API-Version": "placement 1.19"}, 242 | ) 243 | response.raise_for_status() 244 | usages = response.json() 245 | projects[project_id]["usages"] = usages["usages"] 246 | 247 | response = compute_client.get( 248 | f"/os-quota-sets/{project_id}", 249 | headers={"OpenStack-API-Version": "compute 2.20"}, 250 | ) 251 | response.raise_for_status() 252 | quotas = response.json().get("quota_set", {}) 253 | projects[project_id]["quotas"] = dict( 254 | CPUS=quotas.get("cores"), MEMORY_MB=quotas.get("ram") 255 | ) 256 | # print(json.dumps(projects, indent=2)) 257 | 258 | project_usage_guage = prom_core.GaugeMetricFamily( 259 | "openstack_project_usage", 260 | "Current placement allocations per project.", 261 | labels=["project_id", "project_name", "placement_resource"], 262 | ) 263 | project_quota_guage = prom_core.GaugeMetricFamily( 264 | "openstack_project_quota", 265 | "Current quota set to limit max resource allocations per project.", 266 | labels=["project_id", "project_name", "quota_resource"], 267 | ) 268 | for project_id, data in projects.items(): 269 | name = data["name"] 270 | project_usages = data["usages"] 271 | for resource, amount in project_usages.items(): 272 | project_usage_guage.add_metric([project_id, name, resource], amount) 273 | 274 | if not project_usages: 275 | # skip projects with zero usage? 276 | print(f"# WARNING no usage for project: {name} {project_id}") 277 | continue 278 | project_quotas = data["quotas"] 279 | for resource, amount in project_quotas.items(): 280 | project_quota_guage.add_metric([project_id, name, resource], amount) 281 | return [project_usage_guage, project_quota_guage] 282 | 283 | 284 | def get_host_usage(resource_providers, placement_client): 285 | usage_guage = prom_core.GaugeMetricFamily( 286 | "openstack_hypervisor_placement_allocated", 287 | "Currently allocated resource for each provider in placement.", 288 | labels=["hypervisor", "resource"], 289 | ) 290 | capacity_guage = prom_core.GaugeMetricFamily( 291 | "openstack_hypervisor_placement_allocatable_capacity", 292 | "The total allocatable resource in the placement inventory.", 293 | labels=["hypervisor", "resource"], 294 | ) 295 | for name, data in resource_providers.items(): 296 | rp_id = data["uuid"] 297 | response = placement_client.get( 298 | f"/resource_providers/{rp_id}/usages", 299 | headers={"OpenStack-API-Version": "placement 1.19"}, 300 | ) 301 | response.raise_for_status() 302 | rp_usages = response.json()["usages"] 303 | resource_providers[name]["usages"] = rp_usages 304 | 305 | for resource, amount in rp_usages.items(): 306 | usage_guage.add_metric([name, resource], amount) 307 | 308 | response = placement_client.get( 309 | f"/resource_providers/{rp_id}/inventories", 310 | headers={"OpenStack-API-Version": "placement 1.19"}, 311 | ) 312 | response.raise_for_status() 313 | inventories = response.json()["inventories"] 314 | resource_providers[name]["inventories"] = inventories 315 | 316 | for resource, data in inventories.items(): 317 | amount = int(data["total"] * data["allocation_ratio"]) - data["reserved"] 318 | capacity_guage.add_metric([name, resource], amount) 319 | # print(json.dumps(resource_providers, indent=2)) 320 | return [usage_guage, capacity_guage] 321 | 322 | 323 | class OpenStackCapacityCollector(object): 324 | def __init__(self): 325 | self.conn = openstack.connect() 326 | openstack.enable_logging(debug=False) 327 | print("got openstack connection") 328 | # for some reason this makes the logging work?! 329 | self.conn.compute.flavors() 330 | 331 | def collect(self): 332 | start_time = time.perf_counter() 333 | collect_id = uuid.uuid4().hex 334 | print(f"Collect started {collect_id}") 335 | guages = [] 336 | 337 | skip_project_usage = ( 338 | int(os.environ.get("OS_CAPACITY_SKIP_PROJECT_USAGE", "0")) == 1 339 | ) 340 | skip_host_usage = int(os.environ.get("OS_CAPACITY_SKIP_HOST_USAGE", "0")) == 1 341 | 342 | conn = openstack.connect() 343 | openstack.enable_logging(debug=False) 344 | try: 345 | resource_providers, host_guages = get_host_details( 346 | conn.compute, conn.placement 347 | ) 348 | guages += host_guages 349 | 350 | host_time = time.perf_counter() 351 | host_duration = host_time - start_time 352 | print( 353 | "1 of 3: host flavor capacity complete " 354 | f"for {collect_id} it took {host_duration} seconds" 355 | ) 356 | 357 | if not skip_project_usage: 358 | guages += get_project_usage(conn.identity, conn.placement, conn.compute) 359 | project_time = time.perf_counter() 360 | project_duration = project_time - host_time 361 | print( 362 | "2 of 3: project usage complete " 363 | f"for {collect_id} it took {project_duration} seconds" 364 | ) 365 | else: 366 | print("2 of 3: skipping project usage") 367 | 368 | if not skip_host_usage: 369 | guages += get_host_usage(resource_providers, conn.placement) 370 | host_usage_time = time.perf_counter() 371 | host_usage_duration = host_usage_time - project_time 372 | print( 373 | "3 of 3: host usage complete for " 374 | f"{collect_id} it took {host_usage_duration} seconds" 375 | ) 376 | else: 377 | print("3 of 3: skipping host usage") 378 | except Exception as e: 379 | print(f"error {e}") 380 | 381 | end_time = time.perf_counter() 382 | duration = end_time - start_time 383 | print(f"Collect complete {collect_id} it took {duration} seconds") 384 | return guages 385 | 386 | 387 | def main(): 388 | kwargs = { 389 | "port": int(os.environ.get("OS_CAPACITY_EXPORTER_PORT", 9000)), 390 | "addr": os.environ.get("OS_CAPACITY_EXPORTER_LISTEN_ADDRESS", "0.0.0.0"), 391 | } 392 | prom_client.start_http_server(**kwargs) 393 | 394 | prom_core.REGISTRY.register(OpenStackCapacityCollector()) 395 | # there must be a better way! 396 | while True: 397 | time.sleep(5000) 398 | 399 | 400 | if __name__ == "__main__": 401 | main() 402 | -------------------------------------------------------------------------------- /os_capacity/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackhpc/os-capacity/09f29d9303902766212dedda1879f2ee49deb56b/os_capacity/tests/__init__.py -------------------------------------------------------------------------------- /os_capacity/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackhpc/os-capacity/09f29d9303902766212dedda1879f2ee49deb56b/os_capacity/tests/unit/__init__.py -------------------------------------------------------------------------------- /os_capacity/tests/unit/test_prometheus.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import unittest 14 | 15 | from os_capacity import prometheus 16 | 17 | 18 | class FakeFlavor: 19 | def __init__(self, id, name, vcpus, ram, disk, ephemeral, extra_specs): 20 | self.id = id 21 | self.name = name 22 | self.vcpus = vcpus 23 | self.ram = ram 24 | self.disk = disk 25 | self.ephemeral = ephemeral 26 | self.extra_specs = extra_specs 27 | 28 | 29 | class TestFlavor(unittest.TestCase): 30 | def test_get_placement_request(self): 31 | flavor = FakeFlavor( 32 | "fake_id", "fake_name", 8, 2048, 30, 0, {"hw:cpu_policy": "dedicated"} 33 | ) 34 | resources, traits = prometheus.get_placement_request(flavor) 35 | 36 | self.assertEqual({"PCPU": 8, "MEMORY_MB": 2048, "DISK_GB": 30}, resources) 37 | self.assertEqual([], traits) 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | openstacksdk 2 | pbr 3 | prometheus-client 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = os_capacity 3 | summary = Deployment of Scientific OpenStack using OpenStack Kolla 4 | description-file = 5 | README.rst 6 | author = StackHPC 7 | author-email = johng@stackhpc.com 8 | url = https://github.com/stackhpc/os-capacity 9 | python-requires = >=3.9 10 | license = Apache-2 11 | 12 | [files] 13 | packages = 14 | os_capacity 15 | 16 | [entry_points] 17 | console_scripts= 18 | os_capacity = os_capacity.prometheus:main 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import setuptools 4 | 5 | setuptools.setup(setup_requires=["pbr"], pbr=True) 6 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | # The order of packages is significant, because pip processes them in the order 2 | # of appearance. Changing the order has an impact on the overall integration 3 | # process, which may cause wedges in the gate later. 4 | hacking>=3.0,<3.1 # Apache-2.0 5 | 6 | black 7 | coverage>=4.0,!=4.4 # Apache-2.0 8 | python-subunit>=0.0.18 # Apache-2.0/BSD 9 | oslotest>=1.10.0 # Apache-2.0 10 | stestr>=1.0.0 # Apache-2.0 11 | testtools>=1.4.0 # MIT 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 2.0 3 | envlist = py3,black,pep8 4 | skipsdist = True 5 | 6 | [testenv] 7 | usedevelop = True 8 | install_command = pip install {opts} {packages} 9 | setenv = 10 | VIRTUAL_ENV={envdir} 11 | PYTHONWARNINGS=default::DeprecationWarning 12 | deps = -r{toxinidir}/requirements.txt 13 | -r{toxinidir}/test-requirements.txt 14 | commands = stestr run {posargs} 15 | 16 | [testenv:cover] 17 | setenv = 18 | VIRTUAL_ENV={envdir} 19 | PYTHON=coverage run --source azimuth_caas_operator --parallel-mode 20 | commands = 21 | stestr run {posargs} 22 | coverage combine 23 | coverage html -d cover 24 | coverage xml -o cover/coverage.xml 25 | coverage report 26 | 27 | [flake8] 28 | # E123, E125 skipped as they are invalid PEP-8. 29 | show-source = True 30 | ignore = E123,E125 31 | builtins = _ 32 | exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build 33 | # match black 34 | max-line-length = 88 35 | 36 | [testenv:pep8] 37 | commands = 38 | black {tox_root} 39 | flake8 {posargs} 40 | allowlist_externals = black 41 | 42 | [testenv:black] 43 | commands = black {tox_root} --check 44 | allowlist_externals = black 45 | --------------------------------------------------------------------------------