├── .github ├── CODEOWNERS └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .pydocstyle.ini ├── .pylintrc ├── .travis.yml ├── .yamllint.yml ├── FAQ.md ├── LICENSE ├── README.md ├── development ├── Dockerfile ├── configuration.py ├── dev.env ├── docker-compose.yml └── example_reports │ └── users.py ├── netbox_grafana_dashboard.json ├── netbox_grafana_dashboard.png ├── netbox_metrics_ext ├── __init__.py ├── api │ ├── __init__.py │ ├── urls.py │ └── views.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── rqworker_metrics.py ├── metrics.py └── tests │ ├── __init__.py │ ├── test_endpoints.py │ └── test_registry.py ├── poetry.lock ├── pyproject.toml └── tasks.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default owners for all files in this repository 2 | * @dgarros @nniehoff -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Report a reproducible bug in the current release of ntc-netbox-plugin-metrics-ext 4 | --- 5 | 6 | ### Environment 7 | * Python version: 8 | * NetBox version: 9 | * ntc-netbox-plugin-metrics-ext version: 10 | 11 | 15 | ### Steps to Reproduce 16 | 1. 17 | 2. 18 | 3. 19 | 20 | 21 | ### Expected Behavior 22 | 23 | 24 | 25 | ### Observed Behavior -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature Request 3 | about: Propose a new feature or enhancement 4 | --- 5 | 6 | ### Environment 7 | * Python version: 8 | * NetBox version: 9 | * ntc-netbox-plugin-metrics-ext version: 10 | 11 | 14 | ### Proposed Functionality 15 | 16 | 21 | ### Use Case 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | __pycache__ 3 | *.swp 4 | dist 5 | -------------------------------------------------------------------------------- /.pydocstyle.ini: -------------------------------------------------------------------------------- 1 | [pydocstyle] 2 | convention = google 3 | inherit = false 4 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | # Include the pylint_django plugin to avoid spurious warnings about Django patterns 3 | load-plugins=pylint_django 4 | 5 | # Don't cache data for later comparisons 6 | persistent=no 7 | 8 | [BASIC] 9 | # Don't require docstrings for inner Meta classes (or private classes starting with _) 10 | no-docstring-rgx=^(_|Meta$) 11 | 12 | [MISCELLANEOUS] 13 | # Don't currently throw warnings for TODO comments - we should eventually remove this 14 | notes=FIXME,XXX, 15 | 16 | [MESSAGE CONTROL] 17 | # Disabled due to contention with Black: bad-continuation, line-too-long 18 | # Disabled due to noise: too-few-public-methods, too-many-ancestors, too-many-instance-attributes 19 | disable=bad-continuation, 20 | line-too-long, 21 | too-few-public-methods, 22 | too-many-ancestors, 23 | too-many-instance-attributes, 24 | duplicate-code 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: python 3 | python: 4 | - '3.6' 5 | - '3.7' 6 | - '3.8' 7 | env: 8 | # Each version of NetBox listed here must have a corresponding directory/configuration file 9 | # under development/netbox_/configuration.py 10 | matrix: 11 | - NETBOX_VER=v2.8.9 12 | - NETBOX_VER=v2.9.11 13 | - NETBOX_VER=v2.10.4 14 | 15 | services: 16 | - docker 17 | # -------------------------------------------------------------------------- 18 | # Tests 19 | # -------------------------------------------------------------------------- 20 | before_script: 21 | - pip install invoke docker-compose 22 | - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py > /tmp/get-poetry.py 23 | - python /tmp/get-poetry.py -y --version 1.0.2 24 | - source $HOME/.poetry/env 25 | 26 | script: 27 | - invoke build --python-ver $TRAVIS_PYTHON_VERSION 28 | - invoke tests --python-ver $TRAVIS_PYTHON_VERSION 29 | # -------------------------------------------------------------------------- 30 | # Deploy 31 | # -------------------------------------------------------------------------- 32 | deploy: 33 | provider: script 34 | script: poetry config pypi-token.pypi $PYPI_TOKEN && poetry publish --build 35 | skip_cleanup: true 36 | on: 37 | tags: true 38 | branch: master 39 | condition: $NETBOX_VER = master 40 | python: 3.7 41 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | truthy: disable 6 | brackets: { min-spaces-inside: 0, max-spaces-inside: 1 } 7 | braces: { min-spaces-inside: 0, max-spaces-inside: 1 } 8 | line-length: { allow-non-breakable-inline-mappings: true, allow-non-breakable-words: true, max: 180 } 9 | comments-indentation: disable 10 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## NetBox already expose a metrics endpoint, why do I need another one ? 4 | 5 | > System metrics and application level metrics are complementary with each other 6 | > - **SYSTEM Metrics** are very useful to instrument code, track ephemeral information and get a better visibility into what is happening. (Example of metrics: nbr of requests, requests per second, nbr of exceptions, response time, etc ...) The idea is that if we have multiple NetBox instances running behind a load balancer each one will produce a different set of metrics and the monitoring system needs to collect these metrics from all running instances and aggregate them in a dashboard. NetBox exposes some system metrics by default at `localhost/metrics`. 7 | > - **APPLICATION Metrics** are at a higher level and represent information that is the same across all instances of an application running behind a load balancer. if I have 3 instances of NetBox running, there is no point to ask each of them how many Device objects I have in the database, since they will always return the same information. In this case, the goal is to expose only 1 endpoint that can be served by any running instance. 8 | 9 | ## Do I need an API token to access the application metrics endpoint ? 10 | 11 | > No, currently no authentication is required (or possible). 12 | 13 | ## I don't see the plugin in the API documentation, is it expected ? 14 | 15 | > Yes, this is expected. This API endpoint is not published in the swagger documentation because it's not a REST compatible endpoint. 16 | 17 | ## Does this plugin support NetBox 2.9 ? 18 | 19 | > Yes -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache Software License 2.0 2 | 3 | Copyright (c) 2020, Network to Code, LLC 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ntc-netbox-plugin-metrics-ext 2 | 3 | A plugin for [NetBox](https://github.com/netbox-community/netbox) to expose additional metrics information. 4 | 5 | The plugin is composed of multiple features that can be used independantly: 6 | - Application Metrics Endpoint: prometheus endpoint at `/api/plugins/metrics-ext/app-metrics` 7 | - RQ Worker Metrics Command: Add prometheus endpoint on each RQ worker 8 | 9 | # Application Metrics Endpoint 10 | 11 | NetBox already exposes some information via a Prometheus endpoint but the information currently available are mostly at the system level and not at the application level. 12 | - **SYSTEM Metrics** are very useful to instrument code, track ephemeral information and get a better visibility into what is happening. (Example of metrics: nbr of requests, requests per second, nbr of exceptions, response time, etc ...) The idea is that when multiple instances of NetBox are running behind a load balancer each one will produce a different set of metrics and the monitoring system needs to collect these metrics from all running instances and aggregate them in a dashboard. NetBox exposes some system metrics at `localhost/metrics` [NetBox DOC](https://netbox.readthedocs.io/en/stable/additional-features/prometheus-metrics/). 13 | - **APPLICATION Metrics** are at a higher level and represent information that is the same across all instances of an application running behind a load balancer. If I have 3 instances of NetBox running, there is no point to ask each of them how many Device objects I have in the database, since they will always return the same information. In this case, the goal is to expose only 1 endpoint that can be served by any running instance. 14 | 15 | System metrics and application level metrics are complementary with each other 16 | 17 | Currently the plugin exposes these simple metrics by default: 18 | - RQ Queues stats 19 | - Reports stats 20 | - Models count (configurable via configuration.py) 21 | 22 | ## Add your own metrics 23 | 24 | This plugin supports some options to generate and publish your own application metrics behind the same endpoint. 25 | 26 | ### Option 1 - Register function(s) via configuration.py. 27 | 28 | It's possible to create your own function to generate some metrics and register it to the plugin in the configuration.py. 29 | Here is an example where the custom function are centralized in a `metrics.py` file, located next to the main `configuration.py`. 30 | 31 | ```python 32 | # metrics.py 33 | from prometheus_client.core import GaugeMetricFamily 34 | 35 | def metric_prefix_utilization(): 36 | """Report prefix utilization as a metric per container.""" 37 | from ipam.models import Prefix # pylint: disable=import-outside-toplevel 38 | 39 | containers = Prefix.objects.filter(status="container").all() 40 | g = GaugeMetricFamily( 41 | "netbox_prefix_utilization", "percentage of utilization per container prefix", labels=["prefix", "role", "site"] 42 | ) 43 | 44 | for container in containers: 45 | 46 | site = "none" 47 | role = "none" 48 | if container.role: 49 | role = container.role.slug 50 | 51 | if container.site: 52 | site = container.site.slug 53 | 54 | g.add_metric( 55 | [str(container.prefix), site, role], container.get_utilization(), 56 | ) 57 | 58 | yield g 59 | ``` 60 | The new function can be imported in the `configuration.py` file and registered with the plugin. 61 | ```python 62 | # configuration.py 63 | from netbox.metrics import metric_prefix_utilization 64 | PLUGINS_CONFIG = { 65 | "netbox_metrics_ext": { 66 | "app_metrics": { 67 | "extras": [ 68 | metric_prefix_utilization 69 | ] 70 | } 71 | } 72 | }, 73 | ``` 74 | 75 | ### Option 2 - Registry for third party plugins 76 | 77 | Any plugin can include its own metrics to improve the visibility and/or the troubleshooting of the plugin itself. 78 | Third party plugins can register their own function(s) using the `ready()` function as part of their PluginConfig class. 79 | 80 | ```python 81 | # my_plugin/__init__.py 82 | from netbox_metrics_ext import register_metric_func 83 | from netbox.metrics import metric_circuit_bandwidth 84 | 85 | class MyPluginConfig(PluginConfig): 86 | name = "netbox_myplugin" 87 | verbose_name = "Demo Plugin " 88 | # [ ... ] 89 | def ready(self): 90 | super().ready() 91 | register_metric_func(metric_circuit_bandwidth) 92 | ``` 93 | 94 | ### Option 3 - NOT AVAILABLE YET - Metrics directory 95 | 96 | In the future it will be possible to add metrics by adding them in a predefined directory, similar to reports and scripts. 97 | 98 | ## Parameters 99 | 100 | The behavior of the app_metrics feature can be controlled with the following list of settings (under `netbox_metrics_ext > app_metrics`): 101 | - `reports` boolean (default True), publish stats about the reports (success, warning, info, failure) 102 | - `queues` boolean (default True), publish stats about RQ Worker (nbr of worker, nbr and type of job in the different queues) 103 | - `models` nested dict, publish the count for a given object (Nbr Device, Nbr IP etc.. ). The first level must be the name of the module in lowercase (dcim, ipam etc..), the second level must be the name of the object (usually starting with a uppercase) 104 | ```python 105 | { 106 | "dcim": {"Site": True, "Rack": True, "Device": True,}, 107 | "ipam": {"IPAddress": True, "Prefix": True} 108 | } 109 | ``` 110 | ## Usage 111 | 112 | Configure your Prometheus server to collect the application metrics at `/api/plugins/metrics-ext/app-metrics/` 113 | 114 | ```yaml 115 | # Sample prometheus configuration 116 | scrape_configs: 117 | - job_name: 'netbox_app' 118 | scrape_interval: 60s 119 | metrics_path: /api/plugins/metrics-ext/app-metrics 120 | static_configs: 121 | - targets: ['netbox'] 122 | ``` 123 | 124 | # RQ Worker Metrics Endpoint 125 | 126 | This plugin add a new django management command `rqworker_metrics` that is behaving identically to the default `rqworker` command except that this command also exposes a prometheus endpoint (default port 8001). 127 | 128 | With this endpoint it become possible to instrument the tasks running asyncronously in the worker. 129 | 130 | ## Usage 131 | 132 | The new command needs to be executed on the worker as a replacement for the default `rqworker` 133 | ``` 134 | python manage.py rqworker_metrics 135 | ``` 136 | 137 | The port used to expose the prometheus endpoint can be configured for each worker in CLI. 138 | ``` 139 | python manage.py rqworker_metrics --prom-port 8002 140 | ``` 141 | 142 | Since the rq-worker is based on a fork model, for this feature to work it''s required to use prometheus in multi processes mode. 143 | To enable this mode the environment variable `prometheus_multiproc_dir` must be define and point at a valid directory. 144 | 145 | # Installation 146 | 147 | The plugin is available as a Python package in pypi and can be installed with pip 148 | ```shell 149 | pip install ntc-netbox-plugin-metrics-ext 150 | ``` 151 | 152 | > The plugin is compatible with NetBox 2.8.1 and higher 153 | 154 | To ensure Application Metrics Plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `ntc-netbox-plugin-metrics-ext` package: 155 | 156 | ```no-highlight 157 | # echo ntc-netbox-plugin-metrics-ext >> local_requirements.txt 158 | ``` 159 | 160 | Once installed, the plugin needs to be enabled in your `configuration.py` 161 | ```python 162 | # In your configuration.py 163 | PLUGINS = ["netbox_metrics_ext"] 164 | 165 | # PLUGINS_CONFIG = { 166 | # "netbox_metrics_ext": { 167 | # "app_metrics": { 168 | # "models": { 169 | # "dcim": {"Site": True, "Rack": True, "Device": True,}, 170 | # "ipam": {"IPAddress": True, "Prefix": True}, 171 | # }, 172 | # "reports": True, 173 | # "queues": True, 174 | # } 175 | # } 176 | # } 177 | # } 178 | ``` 179 | 180 | ## Included Grafana Dashboard 181 | 182 | Included within this plugin is a Grafana dashboard which will work with the example configuration above. To install this dashboard import the JSON from [Grafana Dashboard](netbox_grafana_dashboard.json) into Grafana. 183 | 184 | ![Netbox Grafana Dashboard](netbox_grafana_dashboard.png) 185 | 186 | # Contributing 187 | 188 | Pull requests are welcomed and automatically built and tested against multiple version of Python and multiple version of NetBox through TravisCI. 189 | 190 | The project is packaged with a light development environment based on `docker-compose` to help with the local development of the project and to run the tests within TravisCI. 191 | 192 | The project is following Network to Code software development guideline and is leveraging: 193 | - Black, Pylint, Bandit and pydocstyle for Python linting and formatting. 194 | - Django unit test to ensure the plugin is working properly. 195 | 196 | ### CLI Helper Commands 197 | 198 | The project is coming with a CLI helper based on [invoke](http://www.pyinvoke.org/) to help setup the development environment. The commands are listed below in 3 categories `dev environment`, `utility` and `testing`. 199 | 200 | Each command can be executed with `invoke `. All commands support the arguments `--netbox-ver` and `--python-ver` if you want to manually define the version of Python and NetBox to use. Each command also has its own help `invoke --help` 201 | 202 | #### Local dev environment 203 | ``` 204 | build Build all docker images. 205 | debug Start NetBox and its dependencies in debug mode. 206 | destroy Destroy all containers and volumes. 207 | start Start NetBox and its dependencies in detached mode. 208 | stop Stop NetBox and its dependencies. 209 | ``` 210 | 211 | #### Utility 212 | ``` 213 | cli Launch a bash shell inside the running NetBox container. 214 | create-user Create a new user in django (default: admin), will prompt for password. 215 | makemigrations Run Make Migration in Django. 216 | nbshell Launch a nbshell session. 217 | ``` 218 | #### Testing 219 | 220 | ``` 221 | tests Run all tests for this plugin. 222 | pylint Run pylint code analysis. 223 | pydocstyle Run pydocstyle to validate docstring formatting adheres to NTC defined standards. 224 | bandit Run bandit to validate basic static code security analysis. 225 | black Run black to check that Python files adhere to its style standards. 226 | unittest Run Django unit tests for the plugin. 227 | ``` 228 | 229 | ## Questions 230 | 231 | For any questions or comments, please check the [FAQ](FAQ.md) first and feel free to swing by the [Network to Code slack channel](https://networktocode.slack.com/) (channel #networktocode). 232 | Sign up [here](http://slack.networktocode.com/) 233 | 234 | ## Default Metrics for the application metrics endpoint 235 | 236 | By Default the plugin will generate the following metrics 237 | ``` 238 | # HELP netbox_queue_stats Per RQ queue and job status statistics 239 | # TYPE netbox_queue_stats gauge 240 | netbox_queue_stats{name="check_releases",status="finished"} 0.0 241 | netbox_queue_stats{name="check_releases",status="started"} 0.0 242 | netbox_queue_stats{name="check_releases",status="deferred"} 0.0 243 | netbox_queue_stats{name="check_releases",status="failed"} 0.0 244 | netbox_queue_stats{name="check_releases",status="scheduled"} 0.0 245 | netbox_queue_stats{name="default",status="finished"} 0.0 246 | netbox_queue_stats{name="default",status="started"} 0.0 247 | netbox_queue_stats{name="default",status="deferred"} 0.0 248 | netbox_queue_stats{name="default",status="failed"} 0.0 249 | netbox_queue_stats{name="default",status="scheduled"} 0.0 250 | # HELP netbox_report_stats Per report statistics 251 | # TYPE netbox_report_stats gauge 252 | netbox_report_stats{name="test_hostname",status="success"} 13.0 253 | netbox_report_stats{name="test_hostname",status="warning"} 0.0 254 | netbox_report_stats{name="test_hostname",status="failure"} 0.0 255 | netbox_report_stats{name="test_hostname",status="info"} 0.0 256 | # HELP netbox_model_count Per NetBox Model count 257 | # TYPE netbox_model_count gauge 258 | netbox_model_count{app="dcim",name="Site"} 24.0 259 | netbox_model_count{app="dcim",name="Rack"} 24.0 260 | netbox_model_count{app="dcim",name="Device"} 46.0 261 | netbox_model_count{app="ipam",name="IPAddress"} 58.0 262 | netbox_model_count{app="ipam",name="Prefix"} 18.0 263 | # HELP netbox_app_metrics_processing_ms Time in ms to generate the app metrics endpoint 264 | # TYPE netbox_app_metrics_processing_ms gauge 265 | netbox_app_metrics_processing_ms 19.90485 266 | ``` -------------------------------------------------------------------------------- /development/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | ARG python_ver=3.7 3 | FROM python:${python_ver} 4 | 5 | ENV PYTHONUNBUFFERED 1 6 | 7 | RUN mkdir /prom_cache 8 | ENV prometheus_multiproc_dir /prom_cache 9 | 10 | RUN mkdir -p /opt 11 | 12 | RUN pip install --upgrade pip\ 13 | && pip install poetry 14 | 15 | # ------------------------------------------------------------------------------------- 16 | # Install NetBox 17 | # ------------------------------------------------------------------------------------- 18 | # Remove redis==3.4.1 from the requirements.txt file as a workaround to #4910 19 | # https://github.com/netbox-community/netbox/issues/4910, required for version 2.8.8 and earlier 20 | ARG netbox_ver=${NETBOX_VER} 21 | RUN git clone --single-branch --branch ${netbox_ver} https://github.com/netbox-community/netbox.git /opt/netbox/ && \ 22 | cd /opt/netbox/ && \ 23 | pip install -r /opt/netbox/requirements.txt 24 | 25 | # Make the django-debug-toolbar always visible when DEBUG is enabled, 26 | # except when we're running Django unit-tests. 27 | RUN echo "import sys" >> /opt/netbox/netbox/netbox/settings.py && \ 28 | echo "TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'" >> /opt/netbox/netbox/netbox/settings.py && \ 29 | echo "DEBUG_TOOLBAR_CONFIG = {'SHOW_TOOLBAR_CALLBACK': lambda _: DEBUG and not TESTING }" >> /opt/netbox/netbox/netbox/settings.py 30 | 31 | # ------------------------------------------------------------------------------------- 32 | # Install Netbox Plugin 33 | # ------------------------------------------------------------------------------------- 34 | RUN mkdir -p /source 35 | WORKDIR /source 36 | COPY . /source 37 | RUN poetry config virtualenvs.create false \ 38 | && poetry install --no-interaction --no-ansi 39 | 40 | WORKDIR /opt/netbox/netbox/ 41 | -------------------------------------------------------------------------------- /development/configuration.py: -------------------------------------------------------------------------------- 1 | """NetBox configuration.""" 2 | import os 3 | from distutils.util import strtobool 4 | from django.core.exceptions import ImproperlyConfigured 5 | from .settings import VERSION # pylint: disable=relative-beyond-top-level 6 | 7 | 8 | # Enforce required configuration parameters 9 | for key in [ 10 | "ALLOWED_HOSTS", 11 | "POSTGRES_DB", 12 | "POSTGRES_USER", 13 | "POSTGRES_HOST", 14 | "POSTGRES_PASSWORD", 15 | "REDIS_HOST", 16 | "REDIS_PASSWORD", 17 | "SECRET_KEY", 18 | ]: 19 | if not os.environ.get(key): 20 | raise ImproperlyConfigured(f"Required environment variable {key} is missing.") 21 | 22 | 23 | def is_truthy(arg): 24 | """Convert "truthy" strings into Booleans. 25 | 26 | Examples: 27 | >>> is_truthy('yes') 28 | True 29 | Args: 30 | arg (str): Truthy string (True values are y, yes, t, true, on and 1; false values are n, no, 31 | f, false, off and 0. Raises ValueError if val is anything else. 32 | """ 33 | if isinstance(arg, bool): 34 | return arg 35 | return bool(strtobool(arg)) 36 | 37 | 38 | # For reference see http://netbox.readthedocs.io/en/latest/configuration/mandatory-settings/ 39 | # Based on https://github.com/digitalocean/netbox/blob/develop/netbox/netbox/configuration.example.py 40 | 41 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 42 | 43 | ######################### 44 | # # 45 | # Required settings # 46 | # # 47 | ######################### 48 | 49 | # This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write 50 | # access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. 51 | # 52 | # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] 53 | ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(" ") 54 | 55 | # PostgreSQL database configuration. 56 | DATABASE = { 57 | "NAME": os.environ["POSTGRES_DB"], # Database name 58 | "USER": os.environ["POSTGRES_USER"], # PostgreSQL username 59 | "PASSWORD": os.environ["POSTGRES_PASSWORD"], 60 | # PostgreSQL password 61 | "HOST": os.environ["POSTGRES_HOST"], # Database server 62 | "PORT": 5432 if not os.environ.get("POSTGRES_PORT", False) else int(os.environ["POSTGRES_PORT"]), # Database port 63 | } 64 | 65 | # This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. 66 | # For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and 67 | # symbols. NetBox will not run without this defined. For more information, see 68 | # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECRET_KEY 69 | SECRET_KEY = os.environ["SECRET_KEY"] 70 | 71 | # Redis database settings. The Redis database is used for caching and background processing such as webhooks 72 | # Seperate sections for webhooks and caching allow for connecting to seperate Redis instances/datbases if desired. 73 | # Full connection details are required in both sections, even if they are the same. 74 | REDIS = { 75 | "caching": { 76 | "HOST": os.environ["REDIS_HOST"], 77 | "PORT": int(os.environ.get("REDIS_PORT", 6379)), 78 | "PASSWORD": os.environ["REDIS_PASSWORD"], 79 | "DATABASE": 1, 80 | "SSL": is_truthy(os.environ.get("REDIS_SSL", False)), 81 | }, 82 | "tasks": { 83 | "HOST": os.environ["REDIS_HOST"], 84 | "PORT": int(os.environ.get("REDIS_PORT", 6379)), 85 | "PASSWORD": os.environ["REDIS_PASSWORD"], 86 | "DATABASE": 0, 87 | "SSL": is_truthy(os.environ.get("REDIS_SSL", False)), 88 | }, 89 | } 90 | 91 | if VERSION.startswith("2.8."): 92 | # NetBox 2.8.x Specific Settings 93 | REDIS["caching"]["DEFAULT_TIMEOUT"] = 300 94 | REDIS["tasks"]["DEFAULT_TIMEOUT"] = 300 95 | elif VERSION.startswith("2.9.") or VERSION.startswith("2.10."): 96 | RQ_DEFAULT_TIMEOUT = 300 97 | else: 98 | raise ImproperlyConfigured(f"Version {VERSION} of NetBox is unsupported at this time.") 99 | 100 | ######################### 101 | # # 102 | # Optional settings # 103 | # # 104 | ######################### 105 | 106 | # Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of 107 | # application errors (assuming correct email settings are provided). 108 | ADMINS = [ 109 | # ['John Doe', 'jdoe@example.com'], 110 | ] 111 | 112 | # Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same 113 | # content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. 114 | BANNER_TOP = os.environ.get("BANNER_TOP", "") 115 | BANNER_BOTTOM = os.environ.get("BANNER_BOTTOM", "") 116 | 117 | # Text to include on the login page above the login form. HTML is allowed. 118 | BANNER_LOGIN = os.environ.get("BANNER_LOGIN", "") 119 | 120 | # Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set: 121 | # BASE_PATH = 'netbox/' 122 | BASE_PATH = os.environ.get("BASE_PATH", "") 123 | 124 | # Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90) 125 | CHANGELOG_RETENTION = int(os.environ.get("CHANGELOG_RETENTION", 0)) 126 | 127 | # API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be 128 | # allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or 129 | # CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers 130 | CORS_ORIGIN_ALLOW_ALL = is_truthy(os.environ.get("CORS_ORIGIN_ALLOW_ALL", False)) 131 | CORS_ORIGIN_WHITELIST = [] 132 | CORS_ORIGIN_REGEX_WHITELIST = [] 133 | 134 | # Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal 135 | # sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging 136 | # on a production system. 137 | DEBUG = is_truthy(os.environ.get("DEBUG", False)) 138 | DEVELOPER = is_truthy(os.environ.get("DEVELOPER", False)) 139 | 140 | # Email settings 141 | EMAIL = { 142 | "SERVER": os.environ.get("EMAIL_SERVER", "localhost"), 143 | "PORT": int(os.environ.get("EMAIL_PORT", 25)), 144 | "USERNAME": os.environ.get("EMAIL_USERNAME", ""), 145 | "PASSWORD": os.environ.get("EMAIL_PASSWORD", ""), 146 | "TIMEOUT": int(os.environ.get("EMAIL_TIMEOUT", 10)), # seconds 147 | "FROM_EMAIL": os.environ.get("EMAIL_FROM", ""), 148 | } 149 | 150 | # Enforcement of unique IP space can be toggled on a per-VRF basis. 151 | # To enforce unique IP space within the global table (all prefixes and IP addresses not assigned to a VRF), 152 | # set ENFORCE_GLOBAL_UNIQUE to True. 153 | ENFORCE_GLOBAL_UNIQUE = is_truthy(os.environ.get("ENFORCE_GLOBAL_UNIQUE", False)) 154 | 155 | # HTTP proxies NetBox should use when sending outbound HTTP requests (e.g. for webhooks). 156 | # HTTP_PROXIES = { 157 | # "http": "http://192.0.2.1:3128", 158 | # "https": "http://192.0.2.1:1080", 159 | # } 160 | 161 | # IP addresses recognized as internal to the system. The debugging toolbar will be available only to clients accessing 162 | # NetBox from an internal IP. 163 | INTERNAL_IPS = ("127.0.0.1", "::1") 164 | 165 | LOG_LEVEL = os.environ.get("LOG_LEVEL", "DEBUG" if DEBUG else "INFO") 166 | 167 | # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: 168 | # https://docs.djangoproject.com/en/1.11/topics/logging/ 169 | LOGGING = { 170 | "version": 1, 171 | "disable_existing_loggers": False, 172 | "formatters": { 173 | "verbose": { 174 | "format": "{asctime} {levelname} {message} - {name} - {module} - {pathname}:{lineno}", 175 | "datefmt": "%H:%M:%S", 176 | "style": "{", 177 | }, 178 | }, 179 | "handlers": {"console": {"level": "DEBUG", "class": "rq.utils.ColorizingStreamHandler", "formatter": "verbose"}}, 180 | "root": {"handlers": ["console"], "level": LOG_LEVEL}, 181 | } 182 | 183 | # Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users 184 | # are permitted to access most data in NetBox (excluding secrets) but not make any changes. 185 | LOGIN_REQUIRED = is_truthy(os.environ.get("LOGIN_REQUIRED", False)) 186 | 187 | # Setting this to True will display a "maintenance mode" banner at the top of every page. 188 | MAINTENANCE_MODE = is_truthy(os.environ.get("MAINTENANCE_MODE", False)) 189 | 190 | # An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g. 191 | # "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request 192 | # all objects by specifying "?limit=0". 193 | MAX_PAGE_SIZE = int(os.environ.get("MAX_PAGE_SIZE", 1000)) 194 | 195 | # The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that 196 | # the default value of this setting is derived from the installed location. 197 | MEDIA_ROOT = os.environ.get("MEDIA_ROOT", os.path.join(BASE_DIR, "media")) 198 | 199 | # Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics' 200 | METRICS_ENABLED = True 201 | 202 | NAPALM_USERNAME = os.environ.get("NAPALM_USERNAME", "") 203 | NAPALM_PASSWORD = os.environ.get("NAPALM_PASSWORD", "") 204 | 205 | # NAPALM timeout (in seconds). (Default: 30) 206 | NAPALM_TIMEOUT = int(os.environ.get("NAPALM_TIMEOUT", 30)) 207 | 208 | # NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must 209 | # be provided as a dictionary. 210 | NAPALM_ARGS = { 211 | "secret": NAPALM_PASSWORD, 212 | # Include any additional args here 213 | } 214 | 215 | # Determine how many objects to display per page within a list. (Default: 50) 216 | PAGINATE_COUNT = int(os.environ.get("PAGINATE_COUNT", 50)) 217 | 218 | # Enable installed plugins. Add the name of each plugin to the list. 219 | PLUGINS = ["netbox_metrics_ext"] 220 | 221 | # Plugins configuration settings. These settings are used by various plugins that the user may have installed. 222 | # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. 223 | PLUGINS_CONFIG = {} 224 | 225 | # When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to 226 | # prefer IPv4 instead. 227 | PREFER_IPV4 = is_truthy(os.environ.get("PREFER_IPV4", False)) 228 | 229 | # Remote authentication support 230 | REMOTE_AUTH_ENABLED = False 231 | REMOTE_AUTH_HEADER = "HTTP_REMOTE_USER" 232 | REMOTE_AUTH_AUTO_CREATE_USER = True 233 | REMOTE_AUTH_DEFAULT_GROUPS = [] 234 | 235 | if VERSION.startswith("2.8."): 236 | # NetBox 2.8.x Specific Settings 237 | REMOTE_AUTH_BACKEND = "utilities.auth_backends.RemoteUserBackend" 238 | REMOTE_AUTH_DEFAULT_PERMISSIONS = [] 239 | elif VERSION.startswith("2.9.") or VERSION.startswith("2.10."): 240 | REMOTE_AUTH_BACKEND = "netbox.authentication.RemoteUserBackend" 241 | REMOTE_AUTH_DEFAULT_PERMISSIONS = {} 242 | else: 243 | raise ImproperlyConfigured(f"Version {VERSION} of NetBox is unsupported at this time.") 244 | 245 | # This determines how often the GitHub API is called to check the latest release of NetBox. Must be at least 1 hour. 246 | RELEASE_CHECK_TIMEOUT = 24 * 3600 247 | 248 | # This repository is used to check whether there is a new release of NetBox available. Set to None to disable the 249 | # version check or use the URL below to check for release in the official NetBox repository. 250 | RELEASE_CHECK_URL = None 251 | # RELEASE_CHECK_URL = 'https://api.github.com/repos/netbox-community/netbox/releases' 252 | 253 | SESSION_FILE_PATH = None 254 | 255 | # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of 256 | # this setting is derived from the installed location. 257 | REPORTS_ROOT = os.environ.get("REPORTS_ROOT", os.path.join(BASE_DIR, "reports")) 258 | 259 | # The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of 260 | # this setting is derived from the installed location. 261 | SCRIPTS_ROOT = os.environ.get("SCRIPTS_ROOT", os.path.join(BASE_DIR, "scripts")) 262 | 263 | # Time zone (default: UTC) 264 | TIME_ZONE = os.environ.get("TIME_ZONE", "UTC") 265 | 266 | # Date/time formatting. See the following link for supported formats: 267 | # https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date 268 | DATE_FORMAT = os.environ.get("DATE_FORMAT", "N j, Y") 269 | SHORT_DATE_FORMAT = os.environ.get("SHORT_DATE_FORMAT", "Y-m-d") 270 | TIME_FORMAT = os.environ.get("TIME_FORMAT", "g:i a") 271 | SHORT_TIME_FORMAT = os.environ.get("SHORT_TIME_FORMAT", "H:i:s") 272 | DATETIME_FORMAT = os.environ.get("DATETIME_FORMAT", "N j, Y g:i a") 273 | SHORT_DATETIME_FORMAT = os.environ.get("SHORT_DATETIME_FORMAT", "Y-m-d H:i") 274 | -------------------------------------------------------------------------------- /development/dev.env: -------------------------------------------------------------------------------- 1 | ALLOWED_HOSTS=* 2 | BANNER_TOP="Metrics Ext plugin dev" 3 | CHANGELOG_RETENTION=0 4 | DEBUG=True 5 | DEVELOPER=True 6 | EMAIL_FROM=netbox@example.com 7 | EMAIL_PASSWORD= 8 | EMAIL_PORT=25 9 | EMAIL_SERVER=localhost 10 | EMAIL_TIMEOUT=5 11 | EMAIL_USERNAME=netbox 12 | MAX_PAGE_SIZE=0 13 | METRICS_ENABLED=True 14 | NAPALM_TIMEOUT=5 15 | POSTGRES_DB=netbox 16 | POSTGRES_HOST=postgres 17 | POSTGRES_PASSWORD=notverysecurepwd 18 | POSTGRES_USER=netbox 19 | REDIS_HOST=redis 20 | REDIS_PASSWORD=notverysecurepwd 21 | REDIS_PORT=6379 22 | # REDIS_SSL=True 23 | # Uncomment REDIS_SSL if using SSL 24 | SECRET_KEY=r8OwDznj!!dci#P9ghmRfdu1Ysxm0AiPeDCQhKE+N_rClfWNj 25 | SUPERUSER_API_TOKEN=0123456789abcdef0123456789abcdef01234567 26 | SUPERUSER_EMAIL=admin@example.com 27 | SUPERUSER_NAME=admin 28 | SUPERUSER_PASSWORD=admin -------------------------------------------------------------------------------- /development/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | services: 4 | netbox: 5 | build: 6 | context: ../ 7 | dockerfile: development/Dockerfile 8 | image: "ntc-netbox-plugin-metrics-ext/netbox:${NETBOX_VER}-py${PYTHON_VER}" 9 | command: > 10 | sh -c "python manage.py migrate && 11 | python manage.py runserver 0.0.0.0:8000" 12 | ports: 13 | - "8000:8000" 14 | depends_on: 15 | - postgres 16 | - redis 17 | env_file: 18 | - ./dev.env 19 | volumes: 20 | - ./configuration.py:/opt/netbox/netbox/netbox/configuration.py 21 | - ./example_reports:/opt/netbox/netbox/reports 22 | - ../netbox_metrics_ext:/source/netbox_metrics_ext 23 | tty: true 24 | worker: 25 | build: 26 | context: ../ 27 | dockerfile: development/Dockerfile 28 | image: "ntc-netbox-plugin-metrics-ext/netbox:${NETBOX_VER}-py${PYTHON_VER}" 29 | command: sh -c "python manage.py rqworker" 30 | depends_on: 31 | - netbox 32 | env_file: 33 | - ./dev.env 34 | volumes: 35 | - ./configuration.py:/opt/netbox/netbox/netbox/configuration.py 36 | - ./example_reports:/opt/netbox/netbox/reports 37 | - ../netbox_metrics_ext:/source/netbox_metrics_ext 38 | tty: true 39 | postgres: 40 | image: postgres:10 41 | env_file: dev.env 42 | volumes: 43 | - pgdata_netbox_metrics_ext:/var/lib/postgresql/data 44 | redis: 45 | image: redis:5-alpine 46 | command: 47 | - sh 48 | - -c # this is to evaluate the $REDIS_PASSWORD from the env 49 | - redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose 50 | env_file: ./dev.env 51 | volumes: 52 | pgdata_netbox_metrics_ext: 53 | -------------------------------------------------------------------------------- /development/example_reports/users.py: -------------------------------------------------------------------------------- 1 | """This script contains an example of report about users.""" 2 | from django.contrib.auth import get_user_model 3 | from extras.reports import Report 4 | 5 | User = get_user_model() 6 | 7 | 8 | class CheckUser(Report): 9 | """Report for Users.""" 10 | 11 | def test_is_uppercase(self): 12 | """Check that every user has his/her name in lowercase.""" 13 | for user in User.objects.all(): 14 | if user.username != user.username.lower(): 15 | self.log_failure(user, f"{user.username} is not in lowercase") 16 | else: 17 | self.log_success(user, f"{user.username} is in lowercase") 18 | -------------------------------------------------------------------------------- /netbox_grafana_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 | "__requires": [ 13 | { 14 | "type": "panel", 15 | "id": "bargauge", 16 | "name": "Bar gauge", 17 | "version": "" 18 | }, 19 | { 20 | "type": "grafana", 21 | "id": "grafana", 22 | "name": "Grafana", 23 | "version": "7.1.1" 24 | }, 25 | { 26 | "type": "panel", 27 | "id": "graph", 28 | "name": "Graph", 29 | "version": "" 30 | }, 31 | { 32 | "type": "datasource", 33 | "id": "prometheus", 34 | "name": "Prometheus", 35 | "version": "1.0.0" 36 | }, 37 | { 38 | "type": "panel", 39 | "id": "stat", 40 | "name": "Stat", 41 | "version": "" 42 | } 43 | ], 44 | "annotations": { 45 | "list": [ 46 | { 47 | "builtIn": 1, 48 | "datasource": "-- Grafana --", 49 | "enable": true, 50 | "hide": true, 51 | "iconColor": "rgba(0, 211, 255, 1)", 52 | "limit": 100, 53 | "name": "Annotations & Alerts", 54 | "showIn": 0, 55 | "type": "dashboard" 56 | } 57 | ] 58 | }, 59 | "description": "Netbox Application Dashboard", 60 | "editable": true, 61 | "gnetId": null, 62 | "graphTooltip": 0, 63 | "id": null, 64 | "links": [], 65 | "panels": [ 66 | { 67 | "datasource": "${DS_PROMETHEUS}", 68 | "description": "", 69 | "fieldConfig": { 70 | "defaults": { 71 | "custom": {}, 72 | "mappings": [], 73 | "thresholds": { 74 | "mode": "absolute", 75 | "steps": [ 76 | { 77 | "color": "green", 78 | "value": null 79 | } 80 | ] 81 | } 82 | }, 83 | "overrides": [] 84 | }, 85 | "gridPos": { 86 | "h": 6, 87 | "w": 4, 88 | "x": 0, 89 | "y": 0 90 | }, 91 | "id": 3, 92 | "options": { 93 | "colorMode": "value", 94 | "graphMode": "area", 95 | "justifyMode": "auto", 96 | "orientation": "auto", 97 | "reduceOptions": { 98 | "calcs": [ 99 | "mean" 100 | ], 101 | "fields": "", 102 | "values": false 103 | }, 104 | "textMode": "auto" 105 | }, 106 | "pluginVersion": "7.1.1", 107 | "targets": [ 108 | { 109 | "expr": "netbox_model_count{name=\"Site\"}", 110 | "interval": "", 111 | "legendFormat": "", 112 | "refId": "A" 113 | } 114 | ], 115 | "timeFrom": null, 116 | "timeShift": null, 117 | "title": "Sites", 118 | "transparent": true, 119 | "type": "stat" 120 | }, 121 | { 122 | "datasource": "${DS_PROMETHEUS}", 123 | "description": "", 124 | "fieldConfig": { 125 | "defaults": { 126 | "custom": {}, 127 | "mappings": [], 128 | "thresholds": { 129 | "mode": "absolute", 130 | "steps": [ 131 | { 132 | "color": "green", 133 | "value": null 134 | } 135 | ] 136 | } 137 | }, 138 | "overrides": [] 139 | }, 140 | "gridPos": { 141 | "h": 6, 142 | "w": 4, 143 | "x": 4, 144 | "y": 0 145 | }, 146 | "id": 2, 147 | "options": { 148 | "colorMode": "value", 149 | "graphMode": "area", 150 | "justifyMode": "auto", 151 | "orientation": "auto", 152 | "reduceOptions": { 153 | "calcs": [ 154 | "mean" 155 | ], 156 | "fields": "", 157 | "values": false 158 | }, 159 | "textMode": "auto" 160 | }, 161 | "pluginVersion": "7.1.1", 162 | "targets": [ 163 | { 164 | "expr": "netbox_model_count{name=\"Device\"}", 165 | "interval": "", 166 | "legendFormat": "", 167 | "refId": "A" 168 | } 169 | ], 170 | "timeFrom": null, 171 | "timeShift": null, 172 | "title": "Devices", 173 | "transparent": true, 174 | "type": "stat" 175 | }, 176 | { 177 | "datasource": "${DS_PROMETHEUS}", 178 | "description": "", 179 | "fieldConfig": { 180 | "defaults": { 181 | "custom": {}, 182 | "mappings": [], 183 | "thresholds": { 184 | "mode": "absolute", 185 | "steps": [ 186 | { 187 | "color": "green", 188 | "value": null 189 | } 190 | ] 191 | } 192 | }, 193 | "overrides": [] 194 | }, 195 | "gridPos": { 196 | "h": 6, 197 | "w": 4, 198 | "x": 8, 199 | "y": 0 200 | }, 201 | "id": 4, 202 | "options": { 203 | "colorMode": "value", 204 | "graphMode": "area", 205 | "justifyMode": "auto", 206 | "orientation": "auto", 207 | "reduceOptions": { 208 | "calcs": [ 209 | "mean" 210 | ], 211 | "fields": "", 212 | "values": false 213 | }, 214 | "textMode": "auto" 215 | }, 216 | "pluginVersion": "7.1.1", 217 | "targets": [ 218 | { 219 | "expr": "netbox_model_count{name=\"Rack\"}", 220 | "interval": "", 221 | "legendFormat": "", 222 | "refId": "A" 223 | } 224 | ], 225 | "timeFrom": null, 226 | "timeShift": null, 227 | "title": "Racks", 228 | "transparent": true, 229 | "type": "stat" 230 | }, 231 | { 232 | "datasource": "${DS_PROMETHEUS}", 233 | "description": "", 234 | "fieldConfig": { 235 | "defaults": { 236 | "custom": {}, 237 | "mappings": [], 238 | "thresholds": { 239 | "mode": "absolute", 240 | "steps": [ 241 | { 242 | "color": "green", 243 | "value": null 244 | } 245 | ] 246 | } 247 | }, 248 | "overrides": [] 249 | }, 250 | "gridPos": { 251 | "h": 6, 252 | "w": 4, 253 | "x": 12, 254 | "y": 0 255 | }, 256 | "id": 5, 257 | "options": { 258 | "colorMode": "value", 259 | "graphMode": "area", 260 | "justifyMode": "auto", 261 | "orientation": "auto", 262 | "reduceOptions": { 263 | "calcs": [ 264 | "mean" 265 | ], 266 | "fields": "", 267 | "values": false 268 | }, 269 | "textMode": "auto" 270 | }, 271 | "pluginVersion": "7.1.1", 272 | "targets": [ 273 | { 274 | "expr": "netbox_model_count{name=\"Prefix\"}", 275 | "interval": "", 276 | "legendFormat": "", 277 | "refId": "A" 278 | } 279 | ], 280 | "timeFrom": null, 281 | "timeShift": null, 282 | "title": "Prefixes", 283 | "transparent": true, 284 | "type": "stat" 285 | }, 286 | { 287 | "datasource": "${DS_PROMETHEUS}", 288 | "description": "", 289 | "fieldConfig": { 290 | "defaults": { 291 | "custom": {}, 292 | "mappings": [], 293 | "thresholds": { 294 | "mode": "absolute", 295 | "steps": [ 296 | { 297 | "color": "green", 298 | "value": null 299 | } 300 | ] 301 | } 302 | }, 303 | "overrides": [] 304 | }, 305 | "gridPos": { 306 | "h": 6, 307 | "w": 4, 308 | "x": 16, 309 | "y": 0 310 | }, 311 | "id": 6, 312 | "options": { 313 | "colorMode": "value", 314 | "graphMode": "area", 315 | "justifyMode": "auto", 316 | "orientation": "auto", 317 | "reduceOptions": { 318 | "calcs": [ 319 | "mean" 320 | ], 321 | "fields": "", 322 | "values": false 323 | }, 324 | "textMode": "auto" 325 | }, 326 | "pluginVersion": "7.1.1", 327 | "targets": [ 328 | { 329 | "expr": "netbox_model_count{name=\"IPAddress\"}", 330 | "interval": "", 331 | "legendFormat": "", 332 | "refId": "A" 333 | } 334 | ], 335 | "timeFrom": null, 336 | "timeShift": null, 337 | "title": "IP Addresses", 338 | "transparent": true, 339 | "type": "stat" 340 | }, 341 | { 342 | "datasource": "${DS_PROMETHEUS}", 343 | "description": "", 344 | "fieldConfig": { 345 | "defaults": { 346 | "custom": {}, 347 | "mappings": [], 348 | "thresholds": { 349 | "mode": "absolute", 350 | "steps": [ 351 | { 352 | "color": "green", 353 | "value": null 354 | } 355 | ] 356 | }, 357 | "unit": "clockms" 358 | }, 359 | "overrides": [] 360 | }, 361 | "gridPos": { 362 | "h": 6, 363 | "w": 4, 364 | "x": 20, 365 | "y": 0 366 | }, 367 | "id": 11, 368 | "options": { 369 | "colorMode": "value", 370 | "graphMode": "area", 371 | "justifyMode": "auto", 372 | "orientation": "auto", 373 | "reduceOptions": { 374 | "calcs": [ 375 | "last" 376 | ], 377 | "fields": "", 378 | "values": false 379 | }, 380 | "textMode": "value" 381 | }, 382 | "pluginVersion": "7.1.1", 383 | "repeat": null, 384 | "targets": [ 385 | { 386 | "expr": "netbox_app_metrics_processing_ms", 387 | "interval": "", 388 | "legendFormat": "", 389 | "refId": "A" 390 | } 391 | ], 392 | "timeFrom": null, 393 | "timeShift": null, 394 | "title": "Processing Time", 395 | "transparent": true, 396 | "type": "stat" 397 | }, 398 | { 399 | "datasource": "${DS_PROMETHEUS}", 400 | "fieldConfig": { 401 | "defaults": { 402 | "custom": {}, 403 | "mappings": [], 404 | "thresholds": { 405 | "mode": "absolute", 406 | "steps": [ 407 | { 408 | "color": "green", 409 | "value": null 410 | } 411 | ] 412 | } 413 | }, 414 | "overrides": [ 415 | { 416 | "matcher": { 417 | "id": "byName", 418 | "options": "failure" 419 | }, 420 | "properties": [ 421 | { 422 | "id": "thresholds", 423 | "value": { 424 | "mode": "absolute", 425 | "steps": [ 426 | { 427 | "color": "red", 428 | "value": null 429 | } 430 | ] 431 | } 432 | } 433 | ] 434 | }, 435 | { 436 | "matcher": { 437 | "id": "byName", 438 | "options": "info" 439 | }, 440 | "properties": [ 441 | { 442 | "id": "thresholds", 443 | "value": { 444 | "mode": "absolute", 445 | "steps": [ 446 | { 447 | "color": "blue", 448 | "value": null 449 | } 450 | ] 451 | } 452 | } 453 | ] 454 | }, 455 | { 456 | "matcher": { 457 | "id": "byName", 458 | "options": "warning" 459 | }, 460 | "properties": [ 461 | { 462 | "id": "thresholds", 463 | "value": { 464 | "mode": "absolute", 465 | "steps": [ 466 | { 467 | "color": "yellow", 468 | "value": null 469 | } 470 | ] 471 | } 472 | } 473 | ] 474 | } 475 | ] 476 | }, 477 | "gridPos": { 478 | "h": 9, 479 | "w": 12, 480 | "x": 0, 481 | "y": 6 482 | }, 483 | "id": 8, 484 | "options": { 485 | "displayMode": "gradient", 486 | "orientation": "auto", 487 | "reduceOptions": { 488 | "calcs": [ 489 | "last" 490 | ], 491 | "fields": "", 492 | "values": false 493 | }, 494 | "showUnfilled": true 495 | }, 496 | "pluginVersion": "7.1.1", 497 | "targets": [ 498 | { 499 | "expr": "sum by(status) (netbox_report_stats)", 500 | "interval": "", 501 | "legendFormat": "{{status}}", 502 | "refId": "A" 503 | } 504 | ], 505 | "timeFrom": null, 506 | "timeShift": null, 507 | "title": "Report Summary", 508 | "transparent": true, 509 | "type": "bargauge" 510 | }, 511 | { 512 | "datasource": "${DS_PROMETHEUS}", 513 | "fieldConfig": { 514 | "defaults": { 515 | "custom": { 516 | "align": null 517 | }, 518 | "mappings": [], 519 | "thresholds": { 520 | "mode": "absolute", 521 | "steps": [ 522 | { 523 | "color": "green", 524 | "value": null 525 | } 526 | ] 527 | } 528 | }, 529 | "overrides": [ 530 | { 531 | "matcher": { 532 | "id": "byName", 533 | "options": "failed" 534 | }, 535 | "properties": [ 536 | { 537 | "id": "thresholds", 538 | "value": { 539 | "mode": "absolute", 540 | "steps": [ 541 | { 542 | "color": "red", 543 | "value": null 544 | } 545 | ] 546 | } 547 | } 548 | ] 549 | }, 550 | { 551 | "matcher": { 552 | "id": "byName", 553 | "options": "started" 554 | }, 555 | "properties": [ 556 | { 557 | "id": "thresholds", 558 | "value": { 559 | "mode": "absolute", 560 | "steps": [ 561 | { 562 | "color": "orange", 563 | "value": null 564 | } 565 | ] 566 | } 567 | } 568 | ] 569 | }, 570 | { 571 | "matcher": { 572 | "id": "byName", 573 | "options": "scheduled" 574 | }, 575 | "properties": [ 576 | { 577 | "id": "thresholds", 578 | "value": { 579 | "mode": "absolute", 580 | "steps": [ 581 | { 582 | "color": "yellow", 583 | "value": null 584 | } 585 | ] 586 | } 587 | } 588 | ] 589 | }, 590 | { 591 | "matcher": { 592 | "id": "byName", 593 | "options": "deferred" 594 | }, 595 | "properties": [ 596 | { 597 | "id": "thresholds", 598 | "value": { 599 | "mode": "absolute", 600 | "steps": [ 601 | { 602 | "color": "purple", 603 | "value": null 604 | } 605 | ] 606 | } 607 | } 608 | ] 609 | } 610 | ] 611 | }, 612 | "gridPos": { 613 | "h": 9, 614 | "w": 12, 615 | "x": 12, 616 | "y": 6 617 | }, 618 | "id": 10, 619 | "links": [], 620 | "options": { 621 | "displayMode": "gradient", 622 | "orientation": "auto", 623 | "reduceOptions": { 624 | "calcs": [ 625 | "last" 626 | ], 627 | "fields": "", 628 | "values": false 629 | }, 630 | "showUnfilled": true 631 | }, 632 | "pluginVersion": "7.1.1", 633 | "targets": [ 634 | { 635 | "expr": "netbox_queue_number_jobs{name=\"default\"}", 636 | "format": "time_series", 637 | "instant": false, 638 | "interval": "", 639 | "intervalFactor": 1, 640 | "legendFormat": "{{status}}", 641 | "refId": "A" 642 | } 643 | ], 644 | "timeFrom": null, 645 | "timeShift": null, 646 | "title": "Job Results", 647 | "transparent": true, 648 | "type": "bargauge" 649 | }, 650 | { 651 | "aliasColors": {}, 652 | "bars": false, 653 | "dashLength": 10, 654 | "dashes": false, 655 | "datasource": "${DS_PROMETHEUS}", 656 | "fieldConfig": { 657 | "defaults": { 658 | "custom": {}, 659 | "mappings": [], 660 | "thresholds": { 661 | "mode": "absolute", 662 | "steps": [ 663 | { 664 | "color": "green", 665 | "value": null 666 | } 667 | ] 668 | } 669 | }, 670 | "overrides": [] 671 | }, 672 | "fill": 1, 673 | "fillGradient": 0, 674 | "gridPos": { 675 | "h": 9, 676 | "w": 12, 677 | "x": 0, 678 | "y": 15 679 | }, 680 | "hiddenSeries": false, 681 | "id": 12, 682 | "legend": { 683 | "alignAsTable": false, 684 | "avg": false, 685 | "current": true, 686 | "hideEmpty": false, 687 | "hideZero": false, 688 | "max": false, 689 | "min": false, 690 | "rightSide": false, 691 | "show": true, 692 | "total": false, 693 | "values": true 694 | }, 695 | "lines": true, 696 | "linewidth": 1, 697 | "nullPointMode": "null", 698 | "percentage": false, 699 | "pluginVersion": "7.1.1", 700 | "pointradius": 2, 701 | "points": false, 702 | "renderer": "flot", 703 | "seriesOverrides": [ 704 | { 705 | "alias": "failure", 706 | "color": "#F2495C", 707 | "fillGradient": 7 708 | }, 709 | { 710 | "alias": "info", 711 | "color": "#5794F2", 712 | "fillGradient": 7 713 | }, 714 | { 715 | "alias": "warning", 716 | "color": "#FADE2A", 717 | "fillGradient": 7 718 | }, 719 | { 720 | "alias": "success", 721 | "color": "#73BF69", 722 | "fillGradient": 7 723 | } 724 | ], 725 | "spaceLength": 10, 726 | "stack": false, 727 | "steppedLine": false, 728 | "targets": [ 729 | { 730 | "expr": "netbox_report_stats\n", 731 | "interval": "", 732 | "legendFormat": "{{status}}", 733 | "refId": "A" 734 | } 735 | ], 736 | "thresholds": [], 737 | "timeFrom": null, 738 | "timeRegions": [], 739 | "timeShift": null, 740 | "title": "Report Summary", 741 | "tooltip": { 742 | "shared": true, 743 | "sort": 0, 744 | "value_type": "individual" 745 | }, 746 | "transparent": true, 747 | "type": "graph", 748 | "xaxis": { 749 | "buckets": null, 750 | "mode": "time", 751 | "name": null, 752 | "show": true, 753 | "values": [] 754 | }, 755 | "yaxes": [ 756 | { 757 | "format": "short", 758 | "label": "", 759 | "logBase": 1, 760 | "max": null, 761 | "min": null, 762 | "show": true 763 | }, 764 | { 765 | "format": "short", 766 | "label": null, 767 | "logBase": 1, 768 | "max": null, 769 | "min": null, 770 | "show": true 771 | } 772 | ], 773 | "yaxis": { 774 | "align": false, 775 | "alignLevel": null 776 | } 777 | }, 778 | { 779 | "aliasColors": {}, 780 | "bars": false, 781 | "dashLength": 10, 782 | "dashes": false, 783 | "datasource": "${DS_PROMETHEUS}", 784 | "fieldConfig": { 785 | "defaults": { 786 | "custom": { 787 | "align": null 788 | }, 789 | "mappings": [], 790 | "thresholds": { 791 | "mode": "absolute", 792 | "steps": [ 793 | { 794 | "color": "green", 795 | "value": null 796 | } 797 | ] 798 | } 799 | }, 800 | "overrides": [ 801 | { 802 | "matcher": { 803 | "id": "byName", 804 | "options": "failed" 805 | }, 806 | "properties": [ 807 | { 808 | "id": "thresholds", 809 | "value": { 810 | "mode": "absolute", 811 | "steps": [ 812 | { 813 | "color": "red", 814 | "value": null 815 | } 816 | ] 817 | } 818 | } 819 | ] 820 | }, 821 | { 822 | "matcher": { 823 | "id": "byName", 824 | "options": "started" 825 | }, 826 | "properties": [ 827 | { 828 | "id": "thresholds", 829 | "value": { 830 | "mode": "absolute", 831 | "steps": [ 832 | { 833 | "color": "orange", 834 | "value": null 835 | } 836 | ] 837 | } 838 | } 839 | ] 840 | }, 841 | { 842 | "matcher": { 843 | "id": "byName", 844 | "options": "scheduled" 845 | }, 846 | "properties": [ 847 | { 848 | "id": "thresholds", 849 | "value": { 850 | "mode": "absolute", 851 | "steps": [ 852 | { 853 | "color": "yellow", 854 | "value": null 855 | } 856 | ] 857 | } 858 | } 859 | ] 860 | }, 861 | { 862 | "matcher": { 863 | "id": "byName", 864 | "options": "deferred" 865 | }, 866 | "properties": [ 867 | { 868 | "id": "thresholds", 869 | "value": { 870 | "mode": "absolute", 871 | "steps": [ 872 | { 873 | "color": "purple", 874 | "value": null 875 | } 876 | ] 877 | } 878 | } 879 | ] 880 | } 881 | ] 882 | }, 883 | "fill": 1, 884 | "fillGradient": 7, 885 | "gridPos": { 886 | "h": 9, 887 | "w": 12, 888 | "x": 12, 889 | "y": 15 890 | }, 891 | "hiddenSeries": false, 892 | "id": 13, 893 | "legend": { 894 | "avg": false, 895 | "current": true, 896 | "max": false, 897 | "min": false, 898 | "rightSide": false, 899 | "show": true, 900 | "total": false, 901 | "values": true 902 | }, 903 | "lines": true, 904 | "linewidth": 1, 905 | "links": [], 906 | "nullPointMode": "null", 907 | "percentage": false, 908 | "pluginVersion": "7.1.1", 909 | "pointradius": 2, 910 | "points": false, 911 | "renderer": "flot", 912 | "seriesOverrides": [ 913 | { 914 | "alias": "deferred", 915 | "color": "#B877D9" 916 | }, 917 | { 918 | "alias": "failed", 919 | "color": "#F2495C" 920 | }, 921 | { 922 | "alias": "finished", 923 | "color": "#73BF69" 924 | }, 925 | { 926 | "alias": "scheduled", 927 | "color": "#FADE2A" 928 | }, 929 | { 930 | "alias": "started", 931 | "color": "#FF9830" 932 | } 933 | ], 934 | "spaceLength": 10, 935 | "stack": false, 936 | "steppedLine": false, 937 | "targets": [ 938 | { 939 | "expr": "netbox_queue_number_jobs{name=\"default\"}", 940 | "format": "time_series", 941 | "instant": false, 942 | "interval": "", 943 | "intervalFactor": 1, 944 | "legendFormat": "{{status}}", 945 | "refId": "A" 946 | } 947 | ], 948 | "thresholds": [], 949 | "timeFrom": null, 950 | "timeRegions": [], 951 | "timeShift": null, 952 | "title": "Job Results", 953 | "tooltip": { 954 | "shared": true, 955 | "sort": 0, 956 | "value_type": "individual" 957 | }, 958 | "transparent": true, 959 | "type": "graph", 960 | "xaxis": { 961 | "buckets": null, 962 | "mode": "time", 963 | "name": null, 964 | "show": true, 965 | "values": [] 966 | }, 967 | "yaxes": [ 968 | { 969 | "format": "short", 970 | "label": null, 971 | "logBase": 1, 972 | "max": null, 973 | "min": null, 974 | "show": true 975 | }, 976 | { 977 | "format": "short", 978 | "label": null, 979 | "logBase": 1, 980 | "max": null, 981 | "min": null, 982 | "show": true 983 | } 984 | ], 985 | "yaxis": { 986 | "align": false, 987 | "alignLevel": null 988 | } 989 | } 990 | ], 991 | "refresh": "5s", 992 | "schemaVersion": 26, 993 | "style": "dark", 994 | "tags": [ 995 | "netbox", 996 | "app_metrics" 997 | ], 998 | "templating": { 999 | "list": [] 1000 | }, 1001 | "time": { 1002 | "from": "now-5m", 1003 | "to": "now" 1004 | }, 1005 | "timepicker": { 1006 | "hidden": false, 1007 | "refresh_intervals": [ 1008 | "10s", 1009 | "30s", 1010 | "1m", 1011 | "5m", 1012 | "15m", 1013 | "30m", 1014 | "1h", 1015 | "2h", 1016 | "1d" 1017 | ] 1018 | }, 1019 | "timezone": "", 1020 | "title": "Netbox Application Metrics", 1021 | "uid": "CS1NJwSGk", 1022 | "version": 1 1023 | } -------------------------------------------------------------------------------- /netbox_grafana_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/networktocode/ntc-netbox-plugin-metrics-ext/b6b67894ab24708dec127800c6b4098b75e0db1a/netbox_grafana_dashboard.png -------------------------------------------------------------------------------- /netbox_metrics_ext/__init__.py: -------------------------------------------------------------------------------- 1 | """Plugin declaration for netbox_metrics_ext.""" 2 | 3 | __version__ = "1.0.0" 4 | 5 | from extras.plugins import PluginConfig 6 | 7 | # Registry of functions that can generate additional application metrics 8 | # All functions in the registry should take no argument and return an Iterator (or list) of prometheus Metric Object 9 | # The Registry can be populated from the configuration file or using register_metric_func() 10 | __REGISTRY__ = [] 11 | 12 | 13 | def register_metric_func(func): 14 | """Register an additional function to generate application metrics. 15 | 16 | Args: 17 | func: python function, taking no argument that return a list of Prometheus Metric Object 18 | """ 19 | if not callable(func): 20 | raise TypeError( 21 | f"Trying to register a {type(func)} into the application metric registry, only function (callable) are supporter" 22 | ) 23 | 24 | __REGISTRY__.append(func) 25 | 26 | 27 | class MetricsExtConfig(PluginConfig): 28 | """Plugin configuration for the netbox_metrics_ext plugin.""" 29 | 30 | name = "netbox_metrics_ext" 31 | verbose_name = "Metrics & Monitoring Extension Plugin" 32 | version = __version__ 33 | author = "Network to Code, LLC" 34 | description = "Plugin to improve the instrumentation of NetBox and expose additional metrics (Application Metrics, RQ Worker)." 35 | base_url = "metrics-ext" 36 | required_settings = [] 37 | min_version = "2.8.1" 38 | default_settings = { 39 | "app_metrics": { 40 | "models": { 41 | "dcim": {"Site": True, "Rack": True, "Device": True,}, 42 | "ipam": {"IPAddress": True, "Prefix": True}, 43 | }, 44 | "reports": True, 45 | "queues": True, 46 | } 47 | } 48 | caching_config = {} 49 | 50 | 51 | config = MetricsExtConfig # pylint:disable=invalid-name 52 | -------------------------------------------------------------------------------- /netbox_metrics_ext/api/__init__.py: -------------------------------------------------------------------------------- 1 | """REST API module for netbox_metrics_ext plugin.""" 2 | -------------------------------------------------------------------------------- /netbox_metrics_ext/api/urls.py: -------------------------------------------------------------------------------- 1 | """Django URL patterns for netbox_metrics_ext plugin.""" 2 | 3 | from django.urls import path 4 | from . import views 5 | 6 | urlpatterns = [ 7 | path("app-metrics", views.ExportToDjangoView, name="netbox_metrics_ext_app_view"), 8 | ] 9 | -------------------------------------------------------------------------------- /netbox_metrics_ext/api/views.py: -------------------------------------------------------------------------------- 1 | """Django views for Prometheus metric collection and reporting.""" 2 | import time 3 | import logging 4 | 5 | from django.conf import settings 6 | from django.http import HttpResponse 7 | 8 | import prometheus_client 9 | from prometheus_client.core import CollectorRegistry, GaugeMetricFamily 10 | 11 | from netbox_metrics_ext import __REGISTRY__ 12 | from netbox_metrics_ext.metrics import collect_extras_metric, metric_reports, metric_models, metric_rq 13 | 14 | logger = logging.getLogger(__name__) 15 | PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["netbox_metrics_ext"]["app_metrics"] 16 | 17 | 18 | class CustomCollector: 19 | """Collector class for collecting plugin and extras metrics.""" 20 | 21 | def collect(self): # pylint: disable=no-self-use 22 | """Collect metrics for all plugins and extras.""" 23 | start = time.time() 24 | if "queues" in PLUGIN_SETTINGS and PLUGIN_SETTINGS["queues"]: 25 | for metric in metric_rq(): 26 | yield metric 27 | 28 | if "reports" in PLUGIN_SETTINGS and PLUGIN_SETTINGS["reports"]: 29 | for metric in metric_reports(): 30 | yield metric 31 | 32 | if "models" in PLUGIN_SETTINGS: 33 | for metric in metric_models(PLUGIN_SETTINGS["models"]): 34 | yield metric 35 | 36 | # -------------------------------------------------------------- 37 | # Extras Function defined in configuration.py or the Regristry 38 | # # -------------------------------------------------------------- 39 | if "extras" in PLUGIN_SETTINGS: 40 | for metric in collect_extras_metric(PLUGIN_SETTINGS["extras"]): 41 | yield metric 42 | 43 | for metric in collect_extras_metric(__REGISTRY__): 44 | yield metric 45 | 46 | gauge = GaugeMetricFamily("netbox_app_metrics_processing_ms", "Time in ms to generate the app metrics endpoint") 47 | duration = time.time() - start 48 | gauge.add_metric([], format(duration * 1000, ".5f")) 49 | yield gauge 50 | 51 | 52 | registry = CollectorRegistry() 53 | collector = CustomCollector() 54 | registry.register(collector) 55 | 56 | 57 | def ExportToDjangoView(request): # pylint: disable=invalid-name 58 | """Exports /api/plugins/metrics-ext/app-metrics as a Django view.""" 59 | metrics_page = prometheus_client.generate_latest(registry) 60 | return HttpResponse(metrics_page, content_type=prometheus_client.CONTENT_TYPE_LATEST) 61 | -------------------------------------------------------------------------------- /netbox_metrics_ext/management/__init__.py: -------------------------------------------------------------------------------- 1 | """Additional Django management capabilities added by netbox_metrics_ext plugin.""" 2 | -------------------------------------------------------------------------------- /netbox_metrics_ext/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | """Additional Django management commands added by netbox_metrics_ext plugin.""" 2 | -------------------------------------------------------------------------------- /netbox_metrics_ext/management/commands/rqworker_metrics.py: -------------------------------------------------------------------------------- 1 | """Implementation of the Django management "rqworker_prom" command.""" 2 | import sys 3 | from os import environ 4 | 5 | from django_rq.management.commands.rqworker import Command as DjangoRqCommand 6 | from prometheus_client import start_http_server, CollectorRegistry, multiprocess 7 | 8 | 9 | class Command(DjangoRqCommand): 10 | """Inherit from the default DjangoRqCommand to start a prometheus endpoint with the worker.""" 11 | 12 | def add_arguments(self, parser): 13 | """Add an additional argument to define the port number for prometheus.""" 14 | parser.add_argument( 15 | "--prom-port", 16 | action="store", 17 | type=int, 18 | default=8001, 19 | dest="prom_port", 20 | help="Port for the prometheus endpoint", 21 | ) 22 | super().add_arguments(parser) 23 | 24 | def handle(self, *args, **options): 25 | """Handler for the rqworker_metrics command.""" 26 | if environ.get("prometheus_multiproc_dir") is None: 27 | sys.exit( 28 | "The mandatory environ variable 'prometheus_multiproc_dir' is not defined, " 29 | "please configure it or use the default 'rqworker' command instead." 30 | ) 31 | 32 | registry = CollectorRegistry() 33 | multiprocess.MultiProcessCollector(registry) 34 | prom_port = options.get("prom_port") 35 | start_http_server(prom_port, registry=registry) 36 | super().handle(*args, **options) 37 | -------------------------------------------------------------------------------- /netbox_metrics_ext/metrics.py: -------------------------------------------------------------------------------- 1 | """Metrics libraries for the netbox_metrics_ext plugin.""" 2 | import logging 3 | import importlib 4 | from collections.abc import Iterable 5 | from packaging import version 6 | from django.conf import settings 7 | 8 | from prometheus_client.core import Metric, GaugeMetricFamily 9 | 10 | from django_rq.utils import get_statistics 11 | 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | netbox_version = version.parse(settings.VERSION) 16 | 17 | 18 | def metric_rq(): 19 | """Return stats about RQ Worker in Prometheus Metric format. 20 | 21 | Return: 22 | Iterator[GaugeMetricFamily] 23 | netbox_queue_number_jobs: Nbr Job per RQ queue and status 24 | netbox_queue_number_workers: Nbr worker per queue 25 | """ 26 | queue_stats = get_statistics() 27 | 28 | job = GaugeMetricFamily( 29 | "netbox_queue_number_jobs", "Number of Job per RQ queue and status", labels=["name", "status"] 30 | ) 31 | worker = GaugeMetricFamily("netbox_queue_number_workers", "Number of worker per queue", labels=["name"]) 32 | 33 | if "queues" in queue_stats: 34 | for queue in queue_stats["queues"]: 35 | for status in ["finished", "started", "deferred", "failed", "scheduled"]: 36 | if f"{status}_jobs" not in queue.keys(): 37 | continue 38 | job.add_metric([queue["name"], status], queue[f"{status}_jobs"]) 39 | 40 | if "workers" in queue.keys(): 41 | worker.add_metric([queue["name"]], queue["workers"]) 42 | 43 | yield job 44 | yield worker 45 | 46 | 47 | def metric_reports(): 48 | """Return Reports results in Prometheus Metric format. 49 | 50 | Return: 51 | Iterator[GaugeMetricFamily] 52 | netbox_report_stats: with report module, name and status as labels 53 | """ 54 | if netbox_version.major >= 2 and netbox_version.minor >= 9: 55 | from django.contrib.contenttypes.models import ContentType # pylint: disable=import-outside-toplevel 56 | from extras.models import Report, JobResult # pylint: disable=import-outside-toplevel,no-name-in-module 57 | 58 | report_results = JobResult.objects.filter(obj_type=ContentType.objects.get_for_model(Report)) 59 | 60 | else: 61 | from extras.models import ReportResult # pylint: disable=import-outside-toplevel,no-name-in-module 62 | 63 | report_results = ReportResult.objects.all() 64 | 65 | gauge = GaugeMetricFamily("netbox_report_stats", "Per report statistics", labels=["module", "name", "status"]) 66 | for result in report_results: 67 | if not result.data: 68 | continue 69 | 70 | for report_name, stats in result.data.items(): 71 | for status in ["success", "warning", "failure", "info"]: 72 | gauge.add_metric([result.name, report_name, status], stats[status]) 73 | yield gauge 74 | 75 | 76 | def metric_models(params): 77 | """Return Models count in Prometheus Metric format. 78 | 79 | Args: 80 | params (dict): list of models to return organized per application 81 | 82 | Return: 83 | Iterator[GaugeMetricFamily] 84 | netbox_model_count: with model name and application name as labels 85 | """ 86 | gauge = GaugeMetricFamily("netbox_model_count", "Per NetBox Model count", labels=["app", "name"]) 87 | for app, _ in params.items(): 88 | for model, _ in params[app].items(): 89 | try: 90 | models = importlib.import_module(f"{app}.models") 91 | model_class = getattr(models, model) 92 | gauge.add_metric([app, model], model_class.objects.count()) 93 | except ModuleNotFoundError: 94 | logger.warning("Unable to find the python library %s.models", app) 95 | except AttributeError: 96 | logger.warning("Unable to load the module %s from the python library %s.models", model, app) 97 | 98 | yield gauge 99 | 100 | 101 | def collect_extras_metric(funcs): 102 | """Collect Third party functions to generate additional Metrics. 103 | 104 | Args: 105 | funcs (list): list of functions to execute 106 | 107 | Return: 108 | List[GaugeMetricFamily] 109 | netbox_model_count: with model name and application name as labels 110 | """ 111 | for func in funcs: 112 | if not callable(func): 113 | logger.warning("Extra metric is not a function, skipping ... ") 114 | continue 115 | 116 | results = func() 117 | 118 | if not isinstance(results, Iterable): 119 | logger.warning("Extra metric didn't return a list, skipping ... ") 120 | continue 121 | 122 | for metric in results: 123 | if not Metric in type(metric).__bases__: 124 | logger.warning("Extra metric didn't return a Metric object, skipping ... ") 125 | continue 126 | yield metric 127 | -------------------------------------------------------------------------------- /netbox_metrics_ext/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests for netbox_metrics_ext plugin.""" 2 | -------------------------------------------------------------------------------- /netbox_metrics_ext/tests/test_endpoints.py: -------------------------------------------------------------------------------- 1 | """Test cases for netbox_metrics_ext views.""" 2 | 3 | from django.test import TestCase 4 | from django.urls import reverse 5 | from rest_framework.test import APIClient 6 | from rest_framework import status 7 | 8 | 9 | class AppMetricEndpointTests(TestCase): 10 | """Test cases for ensuring application metric endpoint is working properly.""" 11 | 12 | def setUp(self): 13 | """Basic setup to create API client for test case.""" 14 | self.app_metric_url = reverse("plugins-api:netbox_metrics_ext-api:netbox_metrics_ext_app_view") 15 | self.client = APIClient() 16 | 17 | def test_endpoint(self): 18 | """Ensure the endpoint is working properly and is not protected by authentication.""" 19 | resp = self.client.get(self.app_metric_url) 20 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 21 | -------------------------------------------------------------------------------- /netbox_metrics_ext/tests/test_registry.py: -------------------------------------------------------------------------------- 1 | """Test cases for netbox_metrics_ext app metric function registry.""" 2 | from django.test import TestCase 3 | from netbox_metrics_ext import register_metric_func, __REGISTRY__ 4 | 5 | 6 | class RegistryTests(TestCase): 7 | """Test cases for ensuring the registry is working properly.""" 8 | 9 | def test_register_metric_func(self): 10 | """Ensure the function to add functions to the registry is working properly.""" 11 | 12 | def myfunction(): 13 | """Dummy metric function.""" 14 | 15 | self.assertRaises(TypeError, register_metric_func, "test") 16 | self.assertRaises(TypeError, register_metric_func, dict(test="test")) 17 | self.assertRaises(TypeError, register_metric_func, [1, 2, 3]) 18 | 19 | register_metric_func(myfunction) 20 | self.assertEqual(__REGISTRY__[-1], myfunction) 21 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 4 | name = "appdirs" 5 | optional = false 6 | python-versions = "*" 7 | version = "1.4.4" 8 | 9 | [[package]] 10 | category = "dev" 11 | description = "An abstract syntax tree for Python with inference support." 12 | name = "astroid" 13 | optional = false 14 | python-versions = ">=3.5" 15 | version = "2.4.2" 16 | 17 | [package.dependencies] 18 | lazy-object-proxy = ">=1.4.0,<1.5.0" 19 | six = ">=1.12,<2.0" 20 | wrapt = ">=1.11,<2.0" 21 | 22 | [package.dependencies.typed-ast] 23 | python = "<3.8" 24 | version = ">=1.4.0,<1.5" 25 | 26 | [[package]] 27 | category = "dev" 28 | description = "Classes Without Boilerplate" 29 | name = "attrs" 30 | optional = false 31 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 32 | version = "19.3.0" 33 | 34 | [package.extras] 35 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 36 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 37 | docs = ["sphinx", "zope.interface"] 38 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 39 | 40 | [[package]] 41 | category = "dev" 42 | description = "Security oriented static analyser for python code." 43 | name = "bandit" 44 | optional = false 45 | python-versions = "*" 46 | version = "1.6.2" 47 | 48 | [package.dependencies] 49 | GitPython = ">=1.0.1" 50 | PyYAML = ">=3.13" 51 | colorama = ">=0.3.9" 52 | six = ">=1.10.0" 53 | stevedore = ">=1.20.0" 54 | 55 | [[package]] 56 | category = "dev" 57 | description = "The uncompromising code formatter." 58 | name = "black" 59 | optional = false 60 | python-versions = ">=3.6" 61 | version = "19.10b0" 62 | 63 | [package.dependencies] 64 | appdirs = "*" 65 | attrs = ">=18.1.0" 66 | click = ">=6.5" 67 | pathspec = ">=0.6,<1" 68 | regex = "*" 69 | toml = ">=0.9.4" 70 | typed-ast = ">=1.4.0" 71 | 72 | [package.extras] 73 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 74 | 75 | [[package]] 76 | category = "dev" 77 | description = "Composable command line interface toolkit" 78 | name = "click" 79 | optional = false 80 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 81 | version = "7.1.2" 82 | 83 | [[package]] 84 | category = "dev" 85 | description = "Cross-platform colored terminal text." 86 | marker = "platform_system == \"Windows\" or sys_platform == \"win32\"" 87 | name = "colorama" 88 | optional = false 89 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 90 | version = "0.4.3" 91 | 92 | [[package]] 93 | category = "dev" 94 | description = "Git Object Database" 95 | name = "gitdb" 96 | optional = false 97 | python-versions = ">=3.4" 98 | version = "4.0.5" 99 | 100 | [package.dependencies] 101 | smmap = ">=3.0.1,<4" 102 | 103 | [[package]] 104 | category = "dev" 105 | description = "Python Git Library" 106 | name = "gitpython" 107 | optional = false 108 | python-versions = ">=3.4" 109 | version = "3.1.7" 110 | 111 | [package.dependencies] 112 | gitdb = ">=4.0.1,<5" 113 | 114 | [[package]] 115 | category = "dev" 116 | description = "Read metadata from Python packages" 117 | marker = "python_version < \"3.8\"" 118 | name = "importlib-metadata" 119 | optional = false 120 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 121 | version = "1.7.0" 122 | 123 | [package.dependencies] 124 | zipp = ">=0.5" 125 | 126 | [package.extras] 127 | docs = ["sphinx", "rst.linker"] 128 | testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] 129 | 130 | [[package]] 131 | category = "main" 132 | description = "Pythonic task execution" 133 | name = "invoke" 134 | optional = false 135 | python-versions = "*" 136 | version = "1.4.1" 137 | 138 | [[package]] 139 | category = "dev" 140 | description = "A Python utility / library to sort Python imports." 141 | name = "isort" 142 | optional = false 143 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 144 | version = "4.3.21" 145 | 146 | [package.extras] 147 | pipfile = ["pipreqs", "requirementslib"] 148 | pyproject = ["toml"] 149 | requirements = ["pipreqs", "pip-api"] 150 | xdg_home = ["appdirs (>=1.4.0)"] 151 | 152 | [[package]] 153 | category = "dev" 154 | description = "A fast and thorough lazy object proxy." 155 | name = "lazy-object-proxy" 156 | optional = false 157 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 158 | version = "1.4.3" 159 | 160 | [[package]] 161 | category = "dev" 162 | description = "McCabe checker, plugin for flake8" 163 | name = "mccabe" 164 | optional = false 165 | python-versions = "*" 166 | version = "0.6.1" 167 | 168 | [[package]] 169 | category = "dev" 170 | description = "Utility library for gitignore style pattern matching of file paths." 171 | name = "pathspec" 172 | optional = false 173 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 174 | version = "0.8.0" 175 | 176 | [[package]] 177 | category = "dev" 178 | description = "Python Build Reasonableness" 179 | name = "pbr" 180 | optional = false 181 | python-versions = "*" 182 | version = "5.4.5" 183 | 184 | [[package]] 185 | category = "dev" 186 | description = "Python docstring style checker" 187 | name = "pydocstyle" 188 | optional = false 189 | python-versions = ">=3.5" 190 | version = "5.0.2" 191 | 192 | [package.dependencies] 193 | snowballstemmer = "*" 194 | 195 | [[package]] 196 | category = "dev" 197 | description = "python code static checker" 198 | name = "pylint" 199 | optional = false 200 | python-versions = ">=3.5.*" 201 | version = "2.5.3" 202 | 203 | [package.dependencies] 204 | astroid = ">=2.4.0,<=2.5" 205 | colorama = "*" 206 | isort = ">=4.2.5,<5" 207 | mccabe = ">=0.6,<0.7" 208 | toml = ">=0.7.1" 209 | 210 | [[package]] 211 | category = "dev" 212 | description = "A Pylint plugin to help Pylint understand the Django web framework" 213 | name = "pylint-django" 214 | optional = false 215 | python-versions = "*" 216 | version = "2.2.0" 217 | 218 | [package.dependencies] 219 | pylint = ">=2.0" 220 | pylint-plugin-utils = ">=0.5" 221 | 222 | [package.extras] 223 | for_tests = ["coverage", "django-tables2", "factory-boy", "pytest"] 224 | with_django = ["django"] 225 | 226 | [[package]] 227 | category = "dev" 228 | description = "Utilities and helpers for writing Pylint plugins" 229 | name = "pylint-plugin-utils" 230 | optional = false 231 | python-versions = "*" 232 | version = "0.6" 233 | 234 | [package.dependencies] 235 | pylint = ">=1.7" 236 | 237 | [[package]] 238 | category = "dev" 239 | description = "YAML parser and emitter for Python" 240 | name = "pyyaml" 241 | optional = false 242 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 243 | version = "5.3.1" 244 | 245 | [[package]] 246 | category = "dev" 247 | description = "Alternative regular expression module, to replace re." 248 | name = "regex" 249 | optional = false 250 | python-versions = "*" 251 | version = "2020.7.14" 252 | 253 | [[package]] 254 | category = "dev" 255 | description = "Python 2 and 3 compatibility utilities" 256 | name = "six" 257 | optional = false 258 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 259 | version = "1.15.0" 260 | 261 | [[package]] 262 | category = "dev" 263 | description = "A pure Python implementation of a sliding window memory map manager" 264 | name = "smmap" 265 | optional = false 266 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 267 | version = "3.0.4" 268 | 269 | [[package]] 270 | category = "dev" 271 | description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." 272 | name = "snowballstemmer" 273 | optional = false 274 | python-versions = "*" 275 | version = "2.0.0" 276 | 277 | [[package]] 278 | category = "dev" 279 | description = "Manage dynamic plugins for Python applications" 280 | name = "stevedore" 281 | optional = false 282 | python-versions = ">=3.6" 283 | version = "3.2.0" 284 | 285 | [package.dependencies] 286 | pbr = ">=2.0.0,<2.1.0 || >2.1.0" 287 | 288 | [package.dependencies.importlib-metadata] 289 | python = "<3.8" 290 | version = ">=1.7.0" 291 | 292 | [[package]] 293 | category = "dev" 294 | description = "Python Library for Tom's Obvious, Minimal Language" 295 | name = "toml" 296 | optional = false 297 | python-versions = "*" 298 | version = "0.10.1" 299 | 300 | [[package]] 301 | category = "dev" 302 | description = "a fork of Python 2 and 3 ast modules with type comment support" 303 | name = "typed-ast" 304 | optional = false 305 | python-versions = "*" 306 | version = "1.4.1" 307 | 308 | [[package]] 309 | category = "dev" 310 | description = "Module for decorators, wrappers and monkey patching." 311 | name = "wrapt" 312 | optional = false 313 | python-versions = "*" 314 | version = "1.12.1" 315 | 316 | [[package]] 317 | category = "dev" 318 | description = "A linter for YAML files." 319 | name = "yamllint" 320 | optional = false 321 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 322 | version = "1.24.2" 323 | 324 | [package.dependencies] 325 | pathspec = ">=0.5.3" 326 | pyyaml = "*" 327 | 328 | [[package]] 329 | category = "dev" 330 | description = "Backport of pathlib-compatible object wrapper for zip files" 331 | marker = "python_version < \"3.8\"" 332 | name = "zipp" 333 | optional = false 334 | python-versions = ">=3.6" 335 | version = "3.1.0" 336 | 337 | [package.extras] 338 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 339 | testing = ["jaraco.itertools", "func-timeout"] 340 | 341 | [metadata] 342 | content-hash = "83d9ceeaf9fcbf0adb2b0135ca02eb7c76f428983ac0616fc5861333aa62f48f" 343 | python-versions = "^3.6" 344 | 345 | [metadata.files] 346 | appdirs = [ 347 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 348 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 349 | ] 350 | astroid = [ 351 | {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, 352 | {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, 353 | ] 354 | attrs = [ 355 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 356 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 357 | ] 358 | bandit = [ 359 | {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, 360 | {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, 361 | ] 362 | black = [ 363 | {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, 364 | {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, 365 | ] 366 | click = [ 367 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 368 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 369 | ] 370 | colorama = [ 371 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 372 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 373 | ] 374 | gitdb = [ 375 | {file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"}, 376 | {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"}, 377 | ] 378 | gitpython = [ 379 | {file = "GitPython-3.1.7-py3-none-any.whl", hash = "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"}, 380 | {file = "GitPython-3.1.7.tar.gz", hash = "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858"}, 381 | ] 382 | importlib-metadata = [ 383 | {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, 384 | {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, 385 | ] 386 | invoke = [ 387 | {file = "invoke-1.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"}, 388 | {file = "invoke-1.4.1-py3-none-any.whl", hash = "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132"}, 389 | {file = "invoke-1.4.1.tar.gz", hash = "sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d"}, 390 | ] 391 | isort = [ 392 | {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, 393 | {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, 394 | ] 395 | lazy-object-proxy = [ 396 | {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, 397 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, 398 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, 399 | {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, 400 | {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, 401 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, 402 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, 403 | {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, 404 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, 405 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, 406 | {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, 407 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, 408 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, 409 | {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, 410 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, 411 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, 412 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, 413 | {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, 414 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, 415 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, 416 | {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, 417 | ] 418 | mccabe = [ 419 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 420 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 421 | ] 422 | pathspec = [ 423 | {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, 424 | {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, 425 | ] 426 | pbr = [ 427 | {file = "pbr-5.4.5-py2.py3-none-any.whl", hash = "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"}, 428 | {file = "pbr-5.4.5.tar.gz", hash = "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c"}, 429 | ] 430 | pydocstyle = [ 431 | {file = "pydocstyle-5.0.2-py3-none-any.whl", hash = "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586"}, 432 | {file = "pydocstyle-5.0.2.tar.gz", hash = "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"}, 433 | ] 434 | pylint = [ 435 | {file = "pylint-2.5.3-py3-none-any.whl", hash = "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"}, 436 | {file = "pylint-2.5.3.tar.gz", hash = "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc"}, 437 | ] 438 | pylint-django = [ 439 | {file = "pylint-django-2.2.0.tar.gz", hash = "sha256:20e4d5f3987e96d29ce51ef24f13187f0d23f37a0558b6eed9b5571487ba3f4c"}, 440 | {file = "pylint_django-2.2.0-py3-none-any.whl", hash = "sha256:d47f278f2ef9244decc006a7412d0ea6bebe1594e6b5402703febbac036ba401"}, 441 | ] 442 | pylint-plugin-utils = [ 443 | {file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"}, 444 | {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, 445 | ] 446 | pyyaml = [ 447 | {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, 448 | {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, 449 | {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, 450 | {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, 451 | {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, 452 | {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, 453 | {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, 454 | {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, 455 | {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, 456 | {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, 457 | {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, 458 | ] 459 | regex = [ 460 | {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, 461 | {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, 462 | {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, 463 | {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, 464 | {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, 465 | {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, 466 | {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, 467 | {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, 468 | {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, 469 | {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, 470 | {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, 471 | {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, 472 | {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, 473 | {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, 474 | {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, 475 | {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, 476 | {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, 477 | {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, 478 | {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, 479 | {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, 480 | {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, 481 | ] 482 | six = [ 483 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 484 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 485 | ] 486 | smmap = [ 487 | {file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"}, 488 | {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"}, 489 | ] 490 | snowballstemmer = [ 491 | {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, 492 | {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, 493 | ] 494 | stevedore = [ 495 | {file = "stevedore-3.2.0-py3-none-any.whl", hash = "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"}, 496 | {file = "stevedore-3.2.0.tar.gz", hash = "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5"}, 497 | ] 498 | toml = [ 499 | {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, 500 | {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, 501 | ] 502 | typed-ast = [ 503 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 504 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 505 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 506 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 507 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 508 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 509 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 510 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 511 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 512 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 513 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 514 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 515 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 516 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 517 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 518 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 519 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 520 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 521 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 522 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 523 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 524 | ] 525 | wrapt = [ 526 | {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, 527 | ] 528 | yamllint = [ 529 | {file = "yamllint-1.24.2-py2.py3-none-any.whl", hash = "sha256:ad3b0d30317dca005d7af99ff27248d459cae2d931a2ff06a134b67bcd405b30"}, 530 | {file = "yamllint-1.24.2.tar.gz", hash = "sha256:40b68de6bacdccec1585dbd54072731b10da7fc2f9cfd96517a71f066208b61f"}, 531 | ] 532 | zipp = [ 533 | {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, 534 | {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, 535 | ] 536 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ntc-netbox-plugin-metrics-ext" 3 | version = "1.0.0" 4 | description = "Plugin to improve the instrumentation of NetBox and expose additional metrics (Application Metrics, RQ Worker)." 5 | authors = ["Network to Code, LLC "] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | homepage = "https://github.com/networktocode/ntc-netbox-plugin-metrics-ext" 9 | repository = "https://github.com/networktocode/ntc-netbox-plugin-metrics-ext" 10 | keywords = ["netbox", "netbox-plugin"] 11 | include = [ 12 | "LICENSE", 13 | "README.md", 14 | ] 15 | packages = [ 16 | { include = "netbox_metrics_ext" }, 17 | ] 18 | 19 | [tool.poetry.dependencies] 20 | python = "^3.6" 21 | invoke = "^1.4.1" 22 | 23 | [tool.poetry.dev-dependencies] 24 | black = "^19.10b0" 25 | yamllint = "^1.23.0" 26 | bandit = "^1.6.2" 27 | pylint = "^2.5.2" 28 | pylint-django = "^2.0.15" 29 | pydocstyle = "^5.0.2" 30 | 31 | [tool.black] 32 | line-length = 120 33 | target-version = ['py36'] 34 | include = '\.pyi?$' 35 | exclude = ''' 36 | ( 37 | /( 38 | \.eggs # exclude a few common directories in the 39 | | \.git # root of the project 40 | | \.hg 41 | | \.mypy_cache 42 | | \.tox 43 | | \.venv 44 | | _build 45 | | buck-out 46 | | build 47 | | dist 48 | )/ 49 | | settings.py # This is where you define files that should not be stylized by black 50 | # the root of the project 51 | ) 52 | ''' 53 | [build-system] 54 | requires = ["poetry>=0.12"] 55 | build-backend = "poetry.masonry.api" 56 | 57 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | """Tasks for use with Invoke.""" 2 | 3 | import os 4 | from invoke import task 5 | 6 | PYTHON_VER = os.getenv("PYTHON_VER", "3.7") 7 | NETBOX_VER = os.getenv("NETBOX_VER", "master") 8 | 9 | # Name of the docker image/container 10 | NAME = os.getenv("IMAGE_NAME", "ntc-netbox-plugin-metrics-ext") 11 | PWD = os.getcwd() 12 | 13 | COMPOSE_FILE = "development/docker-compose.yml" 14 | BUILD_NAME = "netbox_metrics_ext" 15 | 16 | 17 | # ------------------------------------------------------------------------------ 18 | # BUILD 19 | # ------------------------------------------------------------------------------ 20 | @task 21 | def build(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 22 | """Build all docker images. 23 | 24 | Args: 25 | context (obj): Used to run specific commands 26 | netbox_ver (str): NetBox version to use to build the container 27 | python_ver (str): Will use the Python version docker image to build from 28 | """ 29 | context.run( 30 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} build --build-arg netbox_ver={netbox_ver} --build-arg python_ver={python_ver}", 31 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 32 | ) 33 | 34 | 35 | # ------------------------------------------------------------------------------ 36 | # START / STOP / DEBUG 37 | # ------------------------------------------------------------------------------ 38 | @task 39 | def debug(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 40 | """Start NetBox and its dependencies in debug mode. 41 | 42 | Args: 43 | context (obj): Used to run specific commands 44 | netbox_ver (str): NetBox version to use to build the container 45 | python_ver (str): Will use the Python version docker image to build from 46 | """ 47 | print("Starting Netbox .. ") 48 | context.run( 49 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} up", 50 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 51 | ) 52 | 53 | 54 | @task 55 | def start(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 56 | """Start NetBox and its dependencies in detached mode. 57 | 58 | Args: 59 | context (obj): Used to run specific commands 60 | netbox_ver (str): NetBox version to use to build the container 61 | python_ver (str): Will use the Python version docker image to build from 62 | """ 63 | print("Starting Netbox in detached mode.. ") 64 | context.run( 65 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} up -d", 66 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 67 | ) 68 | 69 | 70 | @task 71 | def stop(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 72 | """Stop NetBox and its dependencies. 73 | 74 | Args: 75 | context (obj): Used to run specific commands 76 | netbox_ver (str): NetBox version to use to build the container 77 | python_ver (str): Will use the Python version docker image to build from 78 | """ 79 | print("Stopping Netbox .. ") 80 | context.run( 81 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} down", 82 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 83 | ) 84 | 85 | 86 | @task 87 | def destroy(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 88 | """Destroy all containers and volumes. 89 | 90 | Args: 91 | context (obj): Used to run specific commands 92 | netbox_ver (str): NetBox version to use to build the container 93 | python_ver (str): Will use the Python version docker image to build from 94 | """ 95 | context.run( 96 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} down", 97 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 98 | ) 99 | context.run( 100 | f"docker volume rm -f {BUILD_NAME}_pgdata_netbox_metrics_ext", 101 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 102 | ) 103 | 104 | 105 | # ------------------------------------------------------------------------------ 106 | # ACTIONS 107 | # ------------------------------------------------------------------------------ 108 | @task 109 | def nbshell(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 110 | """Launch a nbshell session. 111 | 112 | Args: 113 | context (obj): Used to run specific commands 114 | netbox_ver (str): NetBox version to use to build the container 115 | python_ver (str): Will use the Python version docker image to build from 116 | """ 117 | context.run( 118 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox python manage.py nbshell", 119 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 120 | pty=True, 121 | ) 122 | 123 | 124 | @task 125 | def cli(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 126 | """Launch a bash shell inside the running NetBox container. 127 | 128 | Args: 129 | context (obj): Used to run specific commands 130 | netbox_ver (str): NetBox version to use to build the container 131 | python_ver (str): Will use the Python version docker image to build from 132 | """ 133 | context.run( 134 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox bash", 135 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 136 | pty=True, 137 | ) 138 | 139 | 140 | @task 141 | def create_user(context, user="admin", netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 142 | """Create a new user in django (default: admin), will prompt for password. 143 | 144 | Args: 145 | context (obj): Used to run specific commands 146 | user (str): name of the superuser to create 147 | netbox_ver (str): NetBox version to use to build the container 148 | python_ver (str): Will use the Python version docker image to build from 149 | """ 150 | context.run( 151 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox python manage.py createsuperuser --username {user}", 152 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 153 | pty=True, 154 | ) 155 | 156 | 157 | @task 158 | def makemigrations(context, name="", netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 159 | """Run Make Migration in Django. 160 | 161 | Args: 162 | context (obj): Used to run specific commands 163 | name (str): Name of the migration to be created 164 | netbox_ver (str): NetBox version to use to build the container 165 | python_ver (str): Will use the Python version docker image to build from 166 | """ 167 | context.run( 168 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} up -d postgres", 169 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 170 | ) 171 | 172 | if name: 173 | context.run( 174 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox python manage.py makemigrations --name {name}", 175 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 176 | ) 177 | else: 178 | context.run( 179 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox python manage.py makemigrations", 180 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 181 | ) 182 | 183 | context.run( 184 | f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} down", 185 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 186 | ) 187 | 188 | 189 | # ------------------------------------------------------------------------------ 190 | # TESTS / LINTING 191 | # ------------------------------------------------------------------------------ 192 | @task 193 | def unittest(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 194 | """Run Django unit tests for the plugin. 195 | 196 | Args: 197 | context (obj): Used to run specific commands 198 | netbox_ver (str): NetBox version to use to build the container 199 | python_ver (str): Will use the Python version docker image to build from 200 | """ 201 | docker = f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox" 202 | context.run( 203 | f'{docker} sh -c "python manage.py test netbox_metrics_ext"', 204 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 205 | pty=True, 206 | ) 207 | 208 | 209 | @task 210 | def pylint(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 211 | """Run pylint code analysis. 212 | 213 | Args: 214 | context (obj): Used to run specific commands 215 | netbox_ver (str): NetBox version to use to build the container 216 | python_ver (str): Will use the Python version docker image to build from 217 | """ 218 | docker = f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox" 219 | # We exclude the /migrations/ directory since it is autogenerated code 220 | context.run( 221 | f"{docker} sh -c \"cd /source && find . -name '*.py' -not -path '*/migrations/*' | " 222 | 'PYTHONPATH=/opt/netbox/netbox xargs pylint"', 223 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 224 | pty=True, 225 | ) 226 | 227 | 228 | @task 229 | def black(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 230 | """Run black to check that Python files adhere to its style standards. 231 | 232 | Args: 233 | context (obj): Used to run specific commands 234 | netbox_ver (str): NetBox version to use to build the container 235 | python_ver (str): Will use the Python version docker image to build from 236 | """ 237 | docker = f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox" 238 | context.run( 239 | f'{docker} sh -c "cd /source && black --check --diff ."', 240 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 241 | pty=True, 242 | ) 243 | 244 | 245 | @task 246 | def pydocstyle(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 247 | """Run pydocstyle to validate docstring formatting adheres to NTC defined standards. 248 | 249 | Args: 250 | context (obj): Used to run specific commands 251 | netbox_ver (str): NetBox version to use to build the container 252 | python_ver (str): Will use the Python version docker image to build from 253 | """ 254 | docker = f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox" 255 | # We exclude the /migrations/ directory since it is autogenerated code 256 | context.run( 257 | f"{docker} sh -c \"cd /source && find . -name '*.py' -not -path '*/migrations/*' | xargs pydocstyle\"", 258 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 259 | pty=True, 260 | ) 261 | 262 | 263 | @task 264 | def bandit(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 265 | """Run bandit to validate basic static code security analysis. 266 | 267 | Args: 268 | context (obj): Used to run specific commands 269 | netbox_ver (str): NetBox version to use to build the container 270 | python_ver (str): Will use the Python version docker image to build from 271 | """ 272 | docker = f"docker-compose -f {COMPOSE_FILE} -p {BUILD_NAME} run netbox" 273 | context.run( 274 | f'{docker} sh -c "cd /source && bandit --recursive ./"', 275 | env={"NETBOX_VER": netbox_ver, "PYTHON_VER": python_ver}, 276 | pty=True, 277 | ) 278 | 279 | 280 | @task 281 | def tests(context, netbox_ver=NETBOX_VER, python_ver=PYTHON_VER): 282 | """Run all tests for this plugin. 283 | 284 | Args: 285 | context (obj): Used to run specific commands 286 | netbox_ver (str): NetBox version to use to build the container 287 | python_ver (str): Will use the Python version docker image to build from 288 | """ 289 | # Sorted loosely from fastest to slowest 290 | print("Running black...") 291 | black(context, netbox_ver=netbox_ver, python_ver=python_ver) 292 | print("Running bandit...") 293 | bandit(context, netbox_ver=netbox_ver, python_ver=python_ver) 294 | print("Running pydocstyle...") 295 | pydocstyle(context, netbox_ver=netbox_ver, python_ver=python_ver) 296 | print("Running pylint...") 297 | pylint(context, netbox_ver=netbox_ver, python_ver=python_ver) 298 | print("Running unit tests...") 299 | unittest(context, netbox_ver=netbox_ver, python_ver=python_ver) 300 | # print("Running yamllint...") 301 | # yamllint(context, NAME, python_ver) 302 | 303 | print("All tests have passed!") 304 | --------------------------------------------------------------------------------