netbox-plugin-prometheus-sd 1.2.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/PKG-INFO +12 -18
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/README.md +10 -15
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/netbox_prometheus_sd/__init__.py +3 -6
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/netbox_prometheus_sd/api/serializers.py +0 -5
- netbox_plugin_prometheus_sd-2.0.0/netbox_prometheus_sd/api/views.py +153 -0
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/netbox_prometheus_sd/filtersets.py +1 -6
- netbox_plugin_prometheus_sd-2.0.0/netbox_prometheus_sd/tests/test_api.py +176 -0
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/netbox_prometheus_sd/tests/test_serializers.py +59 -0
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/netbox_prometheus_sd/tests/utils.py +48 -11
- netbox_plugin_prometheus_sd-2.0.0/pyproject.toml +39 -0
- netbox_plugin_prometheus_sd-1.2.0/netbox_prometheus_sd/api/views.py +0 -107
- netbox_plugin_prometheus_sd-1.2.0/netbox_prometheus_sd/tests/test_api.py +0 -102
- netbox_plugin_prometheus_sd-1.2.0/pyproject.toml +0 -22
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/LICENSE +0 -0
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/netbox_prometheus_sd/api/__init__.py +0 -0
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/netbox_prometheus_sd/api/urls.py +0 -0
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/netbox_prometheus_sd/api/utils.py +0 -0
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/netbox_prometheus_sd/tests/__init__.py +0 -0
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/netbox_prometheus_sd/tests/test_filtersets.py +0 -0
- {netbox_plugin_prometheus_sd-1.2.0 → netbox_plugin_prometheus_sd-2.0.0}/netbox_prometheus_sd/tests/test_utils.py +0 -0
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: netbox-plugin-prometheus-sd
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: A Netbox plugin to provide Netbox entires to Prometheus HTTP service discovery
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Felix Peters
|
|
7
7
|
Author-email: felix.peters@breuninger.de
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -110,7 +109,7 @@ Netbox content is created by using Netbox docker initializers.
|
|
|
110
109
|
The demo data doesn't make sense, but they are good enough for demonstrating how to configure Prometheus
|
|
111
110
|
and get demo data to Prometheus service discovery.
|
|
112
111
|
|
|
113
|
-
Go to the `example` folder and run `docker-compose up`. Prometheus should get available on `http://localhost:9090`.
|
|
112
|
+
Go to the `example` folder and run `docker-compose up --build`. Prometheus should get available on `http://localhost:9090`.
|
|
114
113
|
|
|
115
114
|
Push some example devices and objects to Netbox using the initializers:
|
|
116
115
|
|
|
@@ -122,34 +121,29 @@ Netbox content should then be available in the service discovery tab.
|
|
|
122
121
|
|
|
123
122
|
## Development
|
|
124
123
|
|
|
125
|
-
We use Poetry for dependency management and invoke as task runner.
|
|
126
|
-
|
|
124
|
+
We use [Poetry](https://python-poetry.org/) for dependency management and [invoke](https://www.pyinvoke.org/) as task runner.
|
|
125
|
+
To test the plugin in an isolated environment, we use [testcontainers](https://testcontainers.com/?language=python)
|
|
126
|
+
which creates "throwaway, lightweight" Netbox Docker containers.
|
|
127
127
|
|
|
128
128
|
All code to run in docker is located under `develop`.
|
|
129
129
|
To start a virtual env managed by poetry run `poetry shell`.
|
|
130
130
|
All following commands are started inside this environment.
|
|
131
131
|
|
|
132
|
-
In order to run tests invoke the
|
|
132
|
+
In order to run tests invoke the tests:
|
|
133
133
|
|
|
134
134
|
```bash
|
|
135
|
-
# Build
|
|
136
|
-
invoke
|
|
137
|
-
|
|
138
|
-
# Execute all tests
|
|
139
|
-
invoke tests
|
|
135
|
+
# Build the containers and execute all tests
|
|
136
|
+
invoke test
|
|
140
137
|
```
|
|
141
138
|
|
|
142
139
|
Features should be covered by a unit test, but some times it's easier to develop on an running system.
|
|
143
140
|
|
|
144
141
|
```bash
|
|
145
|
-
# Start a
|
|
146
|
-
invoke
|
|
147
|
-
|
|
148
|
-
# Create an user named `admin`
|
|
149
|
-
invoke create-user
|
|
142
|
+
# Start a development environment
|
|
143
|
+
invoke build_dev
|
|
150
144
|
```
|
|
151
145
|
|
|
152
|
-
Visit http://localhost:8000 and log in with the
|
|
146
|
+
Visit http://localhost:8000 and log in with the default admin credentials.
|
|
153
147
|
You can now define Netbox entities and test around.
|
|
154
148
|
|
|
155
149
|
API endpoints for testing can be found at http://localhost:8000/api/plugins/prometheus-sd/
|
|
@@ -93,7 +93,7 @@ Netbox content is created by using Netbox docker initializers.
|
|
|
93
93
|
The demo data doesn't make sense, but they are good enough for demonstrating how to configure Prometheus
|
|
94
94
|
and get demo data to Prometheus service discovery.
|
|
95
95
|
|
|
96
|
-
Go to the `example` folder and run `docker-compose up`. Prometheus should get available on `http://localhost:9090`.
|
|
96
|
+
Go to the `example` folder and run `docker-compose up --build`. Prometheus should get available on `http://localhost:9090`.
|
|
97
97
|
|
|
98
98
|
Push some example devices and objects to Netbox using the initializers:
|
|
99
99
|
|
|
@@ -105,34 +105,29 @@ Netbox content should then be available in the service discovery tab.
|
|
|
105
105
|
|
|
106
106
|
## Development
|
|
107
107
|
|
|
108
|
-
We use Poetry for dependency management and invoke as task runner.
|
|
109
|
-
|
|
108
|
+
We use [Poetry](https://python-poetry.org/) for dependency management and [invoke](https://www.pyinvoke.org/) as task runner.
|
|
109
|
+
To test the plugin in an isolated environment, we use [testcontainers](https://testcontainers.com/?language=python)
|
|
110
|
+
which creates "throwaway, lightweight" Netbox Docker containers.
|
|
110
111
|
|
|
111
112
|
All code to run in docker is located under `develop`.
|
|
112
113
|
To start a virtual env managed by poetry run `poetry shell`.
|
|
113
114
|
All following commands are started inside this environment.
|
|
114
115
|
|
|
115
|
-
In order to run tests invoke the
|
|
116
|
+
In order to run tests invoke the tests:
|
|
116
117
|
|
|
117
118
|
```bash
|
|
118
|
-
# Build
|
|
119
|
-
invoke
|
|
120
|
-
|
|
121
|
-
# Execute all tests
|
|
122
|
-
invoke tests
|
|
119
|
+
# Build the containers and execute all tests
|
|
120
|
+
invoke test
|
|
123
121
|
```
|
|
124
122
|
|
|
125
123
|
Features should be covered by a unit test, but some times it's easier to develop on an running system.
|
|
126
124
|
|
|
127
125
|
```bash
|
|
128
|
-
# Start a
|
|
129
|
-
invoke
|
|
130
|
-
|
|
131
|
-
# Create an user named `admin`
|
|
132
|
-
invoke create-user
|
|
126
|
+
# Start a development environment
|
|
127
|
+
invoke build_dev
|
|
133
128
|
```
|
|
134
129
|
|
|
135
|
-
Visit http://localhost:8000 and log in with the
|
|
130
|
+
Visit http://localhost:8000 and log in with the default admin credentials.
|
|
136
131
|
You can now define Netbox entities and test around.
|
|
137
132
|
|
|
138
133
|
API endpoints for testing can be found at http://localhost:8000/api/plugins/prometheus-sd/
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
from netbox.plugins import PluginConfig
|
|
3
|
-
except ImportError:
|
|
4
|
-
# Fallback for old NetBox versions < 4.0
|
|
5
|
-
from extras.plugins import PluginConfig
|
|
1
|
+
from netbox.plugins import PluginConfig
|
|
6
2
|
|
|
7
3
|
# Placeholder for semantic release
|
|
8
|
-
__VERSION__ = "
|
|
4
|
+
__VERSION__ = "2.0.0"
|
|
9
5
|
|
|
10
6
|
|
|
11
7
|
class PrometheusSD(PluginConfig):
|
|
@@ -20,6 +16,7 @@ class PrometheusSD(PluginConfig):
|
|
|
20
16
|
base_url = "prometheus-sd"
|
|
21
17
|
required_settings = []
|
|
22
18
|
default_settings = {}
|
|
19
|
+
min_version = "4.0.0"
|
|
23
20
|
|
|
24
21
|
|
|
25
22
|
config = PrometheusSD
|
|
@@ -98,11 +98,6 @@ class PrometheusDeviceSerializer(serializers.ModelSerializer, PrometheusTargetsM
|
|
|
98
98
|
if hasattr(obj, "role") and obj.role is not None:
|
|
99
99
|
labels["role"] = obj.role.name
|
|
100
100
|
labels["role_slug"] = obj.role.slug
|
|
101
|
-
elif (
|
|
102
|
-
hasattr(obj, "device_role") and obj.device_role is not None
|
|
103
|
-
): # netbox <3.6.0
|
|
104
|
-
labels["role"] = obj.device_role.name
|
|
105
|
-
labels["role_slug"] = obj.device_role.slug
|
|
106
101
|
|
|
107
102
|
if hasattr(obj, "device_type") and obj.device_type is not None:
|
|
108
103
|
labels["device_type"] = obj.device_type.model
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from django.contrib.contenttypes.prefetch import GenericPrefetch
|
|
2
|
+
|
|
3
|
+
from ipam.models import IPAddress, Service
|
|
4
|
+
from virtualization.models import VirtualMachine
|
|
5
|
+
from dcim.models.devices import Device
|
|
6
|
+
|
|
7
|
+
from netbox.api.viewsets import BaseViewSet
|
|
8
|
+
from netbox.api.viewsets.mixins import CustomFieldsMixin
|
|
9
|
+
from rest_framework.mixins import ListModelMixin
|
|
10
|
+
|
|
11
|
+
from .utils import NETBOX_RELEASE_CURRENT, NETBOX_RELEASE_41
|
|
12
|
+
|
|
13
|
+
from ipam.filtersets import IPAddressFilterSet
|
|
14
|
+
from dcim.filtersets import DeviceFilterSet
|
|
15
|
+
from virtualization.filtersets import VirtualMachineFilterSet
|
|
16
|
+
|
|
17
|
+
from ..filtersets import ServiceFilterSet
|
|
18
|
+
from .serializers import (
|
|
19
|
+
PrometheusIPAddressSerializer,
|
|
20
|
+
PrometheusDeviceSerializer,
|
|
21
|
+
PrometheusVirtualMachineSerializer,
|
|
22
|
+
PrometheusServiceSerializer,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Netbox MosdelViewSet with list only
|
|
27
|
+
class NetboxPrometheusSDModelViewSet(CustomFieldsMixin, ListModelMixin, BaseViewSet):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Netbox 4.2 replaced the direct Cluster.site FK with a generic `scope` relation.
|
|
32
|
+
# It stays in prefetch_related either way: a GenericForeignKey cannot be JOINed
|
|
33
|
+
# via select_related.
|
|
34
|
+
if NETBOX_RELEASE_CURRENT > NETBOX_RELEASE_41:
|
|
35
|
+
CLUSTER_SCOPE = "cluster__scope"
|
|
36
|
+
else:
|
|
37
|
+
CLUSTER_SCOPE = "cluster__site"
|
|
38
|
+
|
|
39
|
+
# Netbox 4.3 replaced Service.device/Service.virtual_machine with a single
|
|
40
|
+
# generic `parent` relation, exposed via parent_object_type/parent_object_id.
|
|
41
|
+
# Note: hasattr(Service, "device") is NOT a usable check — Device and
|
|
42
|
+
# VirtualMachine declare `services = GenericRelation(..., related_query_name=...)`,
|
|
43
|
+
# which makes Service.device resolve to a read-only reverse accessor on every
|
|
44
|
+
# Netbox version, including 4.3+.
|
|
45
|
+
SERVICE_HAS_GENERIC_PARENT = hasattr(Service, "parent_object_type")
|
|
46
|
+
|
|
47
|
+
# Relations read off `obj.cluster` by utils.extract_cluster(), and off
|
|
48
|
+
# `obj.contacts` by utils.extract_contacts(). Shared by the device and VM
|
|
49
|
+
# querysets, which run the same extractors.
|
|
50
|
+
CLUSTER_RELATED = ("cluster", "cluster__group", "cluster__type")
|
|
51
|
+
CONTACTS_RELATED = ("contacts__contact", "contacts__role")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ServiceViewSet(NetboxPrometheusSDModelViewSet):
|
|
55
|
+
# utils.extract_parent() walks the service's parent device/VM and pulls its
|
|
56
|
+
# name, IPs, tenant, cluster and contacts, so the parent's own relations have
|
|
57
|
+
# to be fetched too or every service row re-queries them.
|
|
58
|
+
_parent_related = (
|
|
59
|
+
"tenant__group",
|
|
60
|
+
"primary_ip4",
|
|
61
|
+
"primary_ip6",
|
|
62
|
+
"site",
|
|
63
|
+
*CLUSTER_RELATED,
|
|
64
|
+
CLUSTER_SCOPE,
|
|
65
|
+
*CONTACTS_RELATED,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if SERVICE_HAS_GENERIC_PARENT: # Netbox >= 4.3
|
|
69
|
+
queryset = Service.objects.prefetch_related(
|
|
70
|
+
GenericPrefetch(
|
|
71
|
+
"parent",
|
|
72
|
+
[
|
|
73
|
+
Device.objects.prefetch_related(*_parent_related, "oob_ip"),
|
|
74
|
+
VirtualMachine.objects.prefetch_related(*_parent_related),
|
|
75
|
+
],
|
|
76
|
+
),
|
|
77
|
+
"ipaddresses",
|
|
78
|
+
"tags",
|
|
79
|
+
)
|
|
80
|
+
else: # Netbox < 4.3
|
|
81
|
+
queryset = Service.objects.prefetch_related(
|
|
82
|
+
*(f"device__{rel}" for rel in _parent_related),
|
|
83
|
+
"device__oob_ip",
|
|
84
|
+
*(f"virtual_machine__{rel}" for rel in _parent_related),
|
|
85
|
+
"device",
|
|
86
|
+
"virtual_machine",
|
|
87
|
+
"ipaddresses",
|
|
88
|
+
"tags",
|
|
89
|
+
)
|
|
90
|
+
filterset_class = ServiceFilterSet
|
|
91
|
+
serializer_class = PrometheusServiceSerializer
|
|
92
|
+
pagination_class = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class VirtualMachineViewSet(NetboxPrometheusSDModelViewSet):
|
|
96
|
+
queryset = (
|
|
97
|
+
VirtualMachine.objects.select_related(
|
|
98
|
+
"role",
|
|
99
|
+
"tenant__group",
|
|
100
|
+
"platform",
|
|
101
|
+
"primary_ip4",
|
|
102
|
+
"primary_ip6",
|
|
103
|
+
# utils.extract_cluster() falls through to obj.site for anything that
|
|
104
|
+
# has one, and VMs do on every supported Netbox release.
|
|
105
|
+
"site",
|
|
106
|
+
)
|
|
107
|
+
.prefetch_related(
|
|
108
|
+
CLUSTER_SCOPE,
|
|
109
|
+
*CLUSTER_RELATED,
|
|
110
|
+
*CONTACTS_RELATED,
|
|
111
|
+
"tags",
|
|
112
|
+
"services",
|
|
113
|
+
)
|
|
114
|
+
.annotate_config_context_data()
|
|
115
|
+
)
|
|
116
|
+
filterset_class = VirtualMachineFilterSet
|
|
117
|
+
serializer_class = PrometheusVirtualMachineSerializer
|
|
118
|
+
pagination_class = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class DeviceViewSet(NetboxPrometheusSDModelViewSet):
|
|
122
|
+
queryset = (
|
|
123
|
+
Device.objects.select_related(
|
|
124
|
+
"device_type",
|
|
125
|
+
"role",
|
|
126
|
+
"tenant__group",
|
|
127
|
+
"platform",
|
|
128
|
+
"site",
|
|
129
|
+
"location",
|
|
130
|
+
"rack",
|
|
131
|
+
"primary_ip4",
|
|
132
|
+
"primary_ip6",
|
|
133
|
+
"oob_ip",
|
|
134
|
+
)
|
|
135
|
+
.prefetch_related(
|
|
136
|
+
CLUSTER_SCOPE,
|
|
137
|
+
*CLUSTER_RELATED,
|
|
138
|
+
*CONTACTS_RELATED,
|
|
139
|
+
"tags",
|
|
140
|
+
"services",
|
|
141
|
+
)
|
|
142
|
+
.annotate_config_context_data()
|
|
143
|
+
)
|
|
144
|
+
filterset_class = DeviceFilterSet
|
|
145
|
+
serializer_class = PrometheusDeviceSerializer
|
|
146
|
+
pagination_class = None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class IPAddressViewSet(NetboxPrometheusSDModelViewSet):
|
|
150
|
+
queryset = IPAddress.objects.select_related("tenant__group").prefetch_related("tags")
|
|
151
|
+
serializer_class = PrometheusIPAddressSerializer
|
|
152
|
+
filterset_class = IPAddressFilterSet
|
|
153
|
+
pagination_class = None
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# Filtersets have been renamed, we support both
|
|
2
|
-
# https://github.com/netbox-community/netbox/commit/1024782b9e0abb48f6da65f8248741227d53dbed#diff-d9224204dab475bbe888868c02235b8ef10f07c9201c45c90804d395dc161c40
|
|
3
1
|
from django.db.models import Q
|
|
4
2
|
from django.utils.translation import gettext as _
|
|
5
3
|
|
|
@@ -9,10 +7,7 @@ from utilities.filters import (
|
|
|
9
7
|
NumericArrayFilter,
|
|
10
8
|
)
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
from ipam.filtersets import ServiceFilterSet as NetboxServiceFilterSet
|
|
14
|
-
except ImportError:
|
|
15
|
-
from ipam.filters import ServiceFilterSet as NetboxServiceFilterSet
|
|
10
|
+
from ipam.filtersets import ServiceFilterSet as NetboxServiceFilterSet
|
|
16
11
|
|
|
17
12
|
|
|
18
13
|
class ServiceFilterSet(NetboxServiceFilterSet):
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from users.models import ObjectPermission, User
|
|
4
|
+
from core.models import ObjectType
|
|
5
|
+
|
|
6
|
+
from django.db import connection
|
|
7
|
+
from django.test import TestCase
|
|
8
|
+
from django.test.utils import CaptureQueriesContext
|
|
9
|
+
|
|
10
|
+
from rest_framework.test import APIClient
|
|
11
|
+
from rest_framework import status
|
|
12
|
+
|
|
13
|
+
from . import utils
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthenticatedApiTestCase(TestCase):
|
|
17
|
+
"""Base class providing an API client authenticated with view permissions."""
|
|
18
|
+
|
|
19
|
+
def setUp(self):
|
|
20
|
+
|
|
21
|
+
self.client = APIClient()
|
|
22
|
+
|
|
23
|
+
# Create test user and view permissions
|
|
24
|
+
user = User.objects.create_user("username", "Pas$w0rd")
|
|
25
|
+
obj_perm = ObjectPermission(name="test", actions=["view"])
|
|
26
|
+
obj_perm.save()
|
|
27
|
+
obj_perm.users.add(user)
|
|
28
|
+
obj_perm.object_types.add(
|
|
29
|
+
ObjectType.objects.get(app_label="dcim", model="device")
|
|
30
|
+
)
|
|
31
|
+
obj_perm.object_types.add(
|
|
32
|
+
ObjectType.objects.get(app_label="ipam", model="ipaddress")
|
|
33
|
+
)
|
|
34
|
+
obj_perm.object_types.add(
|
|
35
|
+
ObjectType.objects.get(app_label="ipam", model="service")
|
|
36
|
+
)
|
|
37
|
+
obj_perm.object_types.add(
|
|
38
|
+
ObjectType.objects.get(app_label="virtualization", model="virtualmachine")
|
|
39
|
+
)
|
|
40
|
+
self.client.force_authenticate(user)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ApiEndpointTests(AuthenticatedApiTestCase):
|
|
44
|
+
"""Test cases for ensuring API endpoint is working properly."""
|
|
45
|
+
|
|
46
|
+
def test_endpoint_device(self):
|
|
47
|
+
"""Ensure device endpoint returns a valid response"""
|
|
48
|
+
|
|
49
|
+
for i in range(1, 61):
|
|
50
|
+
utils.build_device_full(f"api-test-{i}.example.com", i)
|
|
51
|
+
|
|
52
|
+
resp = self.client.get("/api/plugins/prometheus-sd/devices/")
|
|
53
|
+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
|
54
|
+
data = json.loads(resp.content)
|
|
55
|
+
|
|
56
|
+
self.assertIsNotNone(data[0]["targets"])
|
|
57
|
+
self.assertIsNotNone(data[0]["labels"])
|
|
58
|
+
self.assertEqual(len(data), 60)
|
|
59
|
+
|
|
60
|
+
# Serializer tests render the in-memory fixture object, so they cannot
|
|
61
|
+
# catch a fixture that sets an attribute without persisting it. Assert
|
|
62
|
+
# these here, where the objects come back out of the database.
|
|
63
|
+
labels = {d["labels"]["__meta_netbox_name"]: d["labels"] for d in data}
|
|
64
|
+
first = labels["api-test-1.example.com"]
|
|
65
|
+
self.assertEqual(first.get("__meta_netbox_rack_u_position"), "1.0")
|
|
66
|
+
self.assertEqual(first.get("__meta_netbox_contact_primary_name"), "Jane Doe")
|
|
67
|
+
self.assertEqual(first.get("__meta_netbox_contact_primary_role"), "On Call")
|
|
68
|
+
|
|
69
|
+
def test_endpoint_virtual_machine(self):
|
|
70
|
+
"""Ensure virtual machine endpoint returns a valid response"""
|
|
71
|
+
|
|
72
|
+
for i in range(1, 61):
|
|
73
|
+
utils.build_vm_full(f"api-test-vm-{i}.example.com", i)
|
|
74
|
+
|
|
75
|
+
resp = self.client.get("/api/plugins/prometheus-sd/virtual-machines/")
|
|
76
|
+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
|
77
|
+
data = json.loads(resp.content)
|
|
78
|
+
|
|
79
|
+
self.assertIsNotNone(data[0]["targets"])
|
|
80
|
+
self.assertIsNotNone(data[0]["labels"])
|
|
81
|
+
# Full vm contains two entry in the config context so we have to double the number of vm
|
|
82
|
+
self.assertEqual(len(data), 120)
|
|
83
|
+
|
|
84
|
+
labels = {d["labels"]["__meta_netbox_name"]: d["labels"] for d in data}
|
|
85
|
+
first = labels["api-test-vm-1.example.com"]
|
|
86
|
+
self.assertEqual(first.get("__meta_netbox_contact_primary_name"), "Jane Doe")
|
|
87
|
+
self.assertEqual(first.get("__meta_netbox_contact_primary_role"), "On Call")
|
|
88
|
+
|
|
89
|
+
def test_endpoint_ip_address(self):
|
|
90
|
+
"""Ensure ip address endpoint returns a valid response"""
|
|
91
|
+
|
|
92
|
+
for i in range(1, 61):
|
|
93
|
+
utils.build_full_ip(address=f"10.10.10.{i}/24")
|
|
94
|
+
|
|
95
|
+
resp = self.client.get("/api/plugins/prometheus-sd/ip-addresses/")
|
|
96
|
+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
|
97
|
+
data = json.loads(resp.content)
|
|
98
|
+
|
|
99
|
+
self.assertIsNotNone(data[0]["targets"])
|
|
100
|
+
self.assertIsNotNone(data[0]["labels"])
|
|
101
|
+
self.assertEqual(len(data), 60)
|
|
102
|
+
|
|
103
|
+
def test_endpoint_service(self):
|
|
104
|
+
"""Ensure service endpoint returns a valid response"""
|
|
105
|
+
|
|
106
|
+
for i in range(1, 61):
|
|
107
|
+
utils.build_vm_full(f"api-test-vm-{i}.example.com", i)
|
|
108
|
+
|
|
109
|
+
resp = self.client.get("/api/plugins/prometheus-sd/services/")
|
|
110
|
+
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
|
111
|
+
data = json.loads(resp.content)
|
|
112
|
+
|
|
113
|
+
self.assertIsNotNone(data[0]["targets"])
|
|
114
|
+
self.assertIsNotNone(data[0]["labels"])
|
|
115
|
+
self.assertEqual(len(data), 60)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ApiQueryCountTests(AuthenticatedApiTestCase):
|
|
119
|
+
"""Regression tests for the N+1 queries reported in issue #265.
|
|
120
|
+
|
|
121
|
+
These endpoints are unpaginated, so a relation that is read by a serializer
|
|
122
|
+
but not prefetched costs one query per row. Rather than assert an exact
|
|
123
|
+
query count (which legitimately shifts between Netbox releases), assert that
|
|
124
|
+
the count does not grow when the number of objects grows -- that is the
|
|
125
|
+
property an N+1 breaks.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def assertQueryCountDoesNotScale(self, url, build, first=5, second=15):
|
|
129
|
+
for i in range(1, first + 1):
|
|
130
|
+
build(i)
|
|
131
|
+
|
|
132
|
+
# Warm up caches (content types, permissions) that are populated on the
|
|
133
|
+
# first request and would otherwise be counted only in the small run.
|
|
134
|
+
self.assertEqual(self.client.get(url).status_code, status.HTTP_200_OK)
|
|
135
|
+
|
|
136
|
+
with CaptureQueriesContext(connection) as few_objects:
|
|
137
|
+
self.assertEqual(self.client.get(url).status_code, status.HTTP_200_OK)
|
|
138
|
+
|
|
139
|
+
for i in range(first + 1, second + 1):
|
|
140
|
+
build(i)
|
|
141
|
+
|
|
142
|
+
with CaptureQueriesContext(connection) as many_objects:
|
|
143
|
+
self.assertEqual(self.client.get(url).status_code, status.HTTP_200_OK)
|
|
144
|
+
|
|
145
|
+
self.assertEqual(
|
|
146
|
+
len(few_objects),
|
|
147
|
+
len(many_objects),
|
|
148
|
+
f"Query count for {url} grew from {len(few_objects)} queries with "
|
|
149
|
+
f"{first} objects to {len(many_objects)} with {second}. A relation "
|
|
150
|
+
f"used by the serializer is likely missing from the queryset in "
|
|
151
|
+
f"api/views.py.",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def test_device_endpoint_query_count_does_not_scale(self):
|
|
155
|
+
self.assertQueryCountDoesNotScale(
|
|
156
|
+
"/api/plugins/prometheus-sd/devices/",
|
|
157
|
+
lambda i: utils.build_device_full(f"query-count-{i}.example.com", i),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def test_virtual_machine_endpoint_query_count_does_not_scale(self):
|
|
161
|
+
self.assertQueryCountDoesNotScale(
|
|
162
|
+
"/api/plugins/prometheus-sd/virtual-machines/",
|
|
163
|
+
lambda i: utils.build_vm_full(f"query-count-vm-{i}.example.com", i),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def test_service_endpoint_query_count_does_not_scale(self):
|
|
167
|
+
self.assertQueryCountDoesNotScale(
|
|
168
|
+
"/api/plugins/prometheus-sd/services/",
|
|
169
|
+
lambda i: utils.build_vm_full(f"query-count-svc-{i}.example.com", i),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def test_ip_address_endpoint_query_count_does_not_scale(self):
|
|
173
|
+
self.assertQueryCountDoesNotScale(
|
|
174
|
+
"/api/plugins/prometheus-sd/ip-addresses/",
|
|
175
|
+
lambda i: utils.build_full_ip(address=f"10.10.20.{i}/24"),
|
|
176
|
+
)
|
|
@@ -198,6 +198,32 @@ class PrometheusVirtualMachineSerializerTests(TestCase):
|
|
|
198
198
|
data["labels"],
|
|
199
199
|
)
|
|
200
200
|
)
|
|
201
|
+
# Tags are compared as sets: the label is a joined list whose order
|
|
202
|
+
# is not guaranteed.
|
|
203
|
+
self.assertEqual(
|
|
204
|
+
set(data["labels"]["__meta_netbox_tags"].split(",")),
|
|
205
|
+
{"Tag1", "Tag 2"},
|
|
206
|
+
)
|
|
207
|
+
self.assertEqual(
|
|
208
|
+
set(data["labels"]["__meta_netbox_tag_slugs"].split(",")),
|
|
209
|
+
{"tag1", "tag-2"},
|
|
210
|
+
)
|
|
211
|
+
self.assertTrue(
|
|
212
|
+
utils.dictContainsSubset(
|
|
213
|
+
{"__meta_netbox_services": "ssh"}, data["labels"]
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
self.assertTrue(
|
|
217
|
+
utils.dictContainsSubset(
|
|
218
|
+
{
|
|
219
|
+
"__meta_netbox_contact_primary_name": "Jane Doe",
|
|
220
|
+
"__meta_netbox_contact_primary_email": "jane@example.com",
|
|
221
|
+
"__meta_netbox_contact_primary_comments": "Primary on-call",
|
|
222
|
+
"__meta_netbox_contact_primary_role": "On Call",
|
|
223
|
+
},
|
|
224
|
+
data["labels"],
|
|
225
|
+
)
|
|
226
|
+
)
|
|
201
227
|
|
|
202
228
|
|
|
203
229
|
class PrometheusDeviceSerializerTests(TestCase):
|
|
@@ -356,6 +382,29 @@ class PrometheusDeviceSerializerTests(TestCase):
|
|
|
356
382
|
{"__meta_netbox_rack_u_position": "1.0"}, data["labels"]
|
|
357
383
|
)
|
|
358
384
|
)
|
|
385
|
+
# Tags are compared as sets: the label is a joined list whose order is
|
|
386
|
+
# not guaranteed.
|
|
387
|
+
self.assertEqual(
|
|
388
|
+
set(data["labels"]["__meta_netbox_tags"].split(",")), {"Tag1", "Tag 2"}
|
|
389
|
+
)
|
|
390
|
+
self.assertEqual(
|
|
391
|
+
set(data["labels"]["__meta_netbox_tag_slugs"].split(",")),
|
|
392
|
+
{"tag1", "tag-2"},
|
|
393
|
+
)
|
|
394
|
+
self.assertTrue(
|
|
395
|
+
utils.dictContainsSubset({"__meta_netbox_services": "ssh"}, data["labels"])
|
|
396
|
+
)
|
|
397
|
+
self.assertTrue(
|
|
398
|
+
utils.dictContainsSubset(
|
|
399
|
+
{
|
|
400
|
+
"__meta_netbox_contact_primary_name": "Jane Doe",
|
|
401
|
+
"__meta_netbox_contact_primary_email": "jane@example.com",
|
|
402
|
+
"__meta_netbox_contact_primary_comments": "Primary on-call",
|
|
403
|
+
"__meta_netbox_contact_primary_role": "On Call",
|
|
404
|
+
},
|
|
405
|
+
data["labels"],
|
|
406
|
+
)
|
|
407
|
+
)
|
|
359
408
|
|
|
360
409
|
|
|
361
410
|
class PrometheusIPAddressSerializerTests(TestCase):
|
|
@@ -493,6 +542,11 @@ class PrometheusServiceSerializerTests(TestCase):
|
|
|
493
542
|
{"__meta_netbox_primary_ip6": "2001:db8:1701::2"}, data["labels"]
|
|
494
543
|
)
|
|
495
544
|
)
|
|
545
|
+
self.assertTrue(
|
|
546
|
+
utils.dictContainsSubset(
|
|
547
|
+
{"__meta_netbox_ipaddresses": "192.168.0.1"}, data["labels"]
|
|
548
|
+
)
|
|
549
|
+
)
|
|
496
550
|
|
|
497
551
|
def test_vm_service_full_to_target(self):
|
|
498
552
|
vm = utils.build_vm_full("vm-full-01.example.com")
|
|
@@ -576,3 +630,8 @@ class PrometheusServiceSerializerTests(TestCase):
|
|
|
576
630
|
{"__meta_netbox_primary_ip6": "2001:db8:1701::2"}, data["labels"]
|
|
577
631
|
)
|
|
578
632
|
)
|
|
633
|
+
self.assertTrue(
|
|
634
|
+
utils.dictContainsSubset(
|
|
635
|
+
{"__meta_netbox_ipaddresses": "192.168.0.1"}, data["labels"]
|
|
636
|
+
)
|
|
637
|
+
)
|
|
@@ -7,7 +7,13 @@ from dcim.models import Device, DeviceRole, Platform, Rack
|
|
|
7
7
|
from extras.models import ConfigContext, Tag
|
|
8
8
|
|
|
9
9
|
from ipam.models import IPAddress, Service
|
|
10
|
-
from tenancy.models import
|
|
10
|
+
from tenancy.models import (
|
|
11
|
+
Contact,
|
|
12
|
+
ContactAssignment,
|
|
13
|
+
ContactRole,
|
|
14
|
+
Tenant,
|
|
15
|
+
TenantGroup,
|
|
16
|
+
)
|
|
11
17
|
|
|
12
18
|
from virtualization.models import (
|
|
13
19
|
Cluster,
|
|
@@ -53,6 +59,18 @@ def build_tenant():
|
|
|
53
59
|
return Tenant.objects.get_or_create(name="Acme Corp.", slug="acme")[0]
|
|
54
60
|
|
|
55
61
|
|
|
62
|
+
def build_contact_for(obj):
|
|
63
|
+
"""Assign a contact to obj so extract_contacts() has something to render."""
|
|
64
|
+
contact = Contact.objects.get_or_create(
|
|
65
|
+
name="Jane Doe",
|
|
66
|
+
defaults={"email": "jane@example.com", "comments": "Primary on-call"},
|
|
67
|
+
)[0]
|
|
68
|
+
role = ContactRole.objects.get_or_create(name="On Call", slug="on-call")[0]
|
|
69
|
+
return ContactAssignment.objects.create(
|
|
70
|
+
object=obj, contact=contact, role=role, priority="primary"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
56
74
|
def build_custom_fields():
|
|
57
75
|
"""Build custom field definition with different kinds of custom values"""
|
|
58
76
|
return {
|
|
@@ -114,12 +132,29 @@ def build_vm_full(name, ip_octet=1):
|
|
|
114
132
|
vm.tags.add("Tag 2")
|
|
115
133
|
vm.save()
|
|
116
134
|
|
|
117
|
-
|
|
135
|
+
build_contact_for(vm)
|
|
136
|
+
build_service_for(vm, name="ssh", protocol="tcp", ports=[22])
|
|
118
137
|
return vm
|
|
119
138
|
|
|
120
139
|
|
|
140
|
+
def build_service_for(parent, **kwargs):
|
|
141
|
+
"""Create a service bound to parent, with the parent's primary IPv4 attached.
|
|
142
|
+
|
|
143
|
+
Netbox 4.3 replaced Service.device/Service.virtual_machine with a single
|
|
144
|
+
generic `parent` relation, which is read-only on older releases.
|
|
145
|
+
"""
|
|
146
|
+
try: # NetBox 4.3+
|
|
147
|
+
service = Service.objects.create(parent=parent, **kwargs)
|
|
148
|
+
except AttributeError: # NetBox <4.3
|
|
149
|
+
field = "device" if isinstance(parent, Device) else "virtual_machine"
|
|
150
|
+
service = Service.objects.create(**{field: parent}, **kwargs)
|
|
151
|
+
|
|
152
|
+
if parent.primary_ip4 is not None:
|
|
153
|
+
service.ipaddresses.set([parent.primary_ip4])
|
|
154
|
+
return service
|
|
155
|
+
|
|
156
|
+
|
|
121
157
|
def build_minimal_device(name):
|
|
122
|
-
role_attr = "role" if hasattr(Device, "role") else "device_role"
|
|
123
158
|
return Device.objects.get_or_create(
|
|
124
159
|
name=name,
|
|
125
160
|
device_type=DeviceType.objects.get_or_create(
|
|
@@ -130,11 +165,7 @@ def build_minimal_device(name):
|
|
|
130
165
|
)[0],
|
|
131
166
|
)[0],
|
|
132
167
|
site=Site.objects.get_or_create(name="Site", slug="site")[0],
|
|
133
|
-
|
|
134
|
-
role_attr: DeviceRole.objects.get_or_create(
|
|
135
|
-
name="Firewall", slug="firewall"
|
|
136
|
-
)[0],
|
|
137
|
-
},
|
|
168
|
+
role=DeviceRole.objects.get_or_create(name="Firewall", slug="firewall")[0],
|
|
138
169
|
)[0]
|
|
139
170
|
|
|
140
171
|
|
|
@@ -222,14 +253,20 @@ def build_device_full(name, ip_octet=1):
|
|
|
222
253
|
)[0]
|
|
223
254
|
device.oob_ip = IPAddress.objects.get_or_create(address=f"10.0.0.{ip_octet}/24")[0]
|
|
224
255
|
device.rack = Rack.objects.get_or_create(
|
|
225
|
-
name="R01B01",
|
|
256
|
+
name="R01B01",
|
|
257
|
+
site=Site.objects.get_or_create(name="Site", slug="site")[0],
|
|
258
|
+
defaults={"u_height": 100},
|
|
226
259
|
)[0]
|
|
227
260
|
device.site = Site.objects.get_or_create(name="Site", slug="site")[0]
|
|
261
|
+
# Rack position is unique per (rack, position, face), so it has to follow
|
|
262
|
+
# ip_octet like the addresses do -- otherwise callers building more than one
|
|
263
|
+
# device hit an IntegrityError.
|
|
264
|
+
device.position = float(ip_octet)
|
|
228
265
|
device.tags.add("Tag1")
|
|
229
266
|
device.tags.add("Tag 2")
|
|
230
267
|
device.save()
|
|
231
|
-
device
|
|
232
|
-
|
|
268
|
+
build_contact_for(device)
|
|
269
|
+
build_service_for(device, name="ssh", protocol="tcp", ports=[22])
|
|
233
270
|
return device
|
|
234
271
|
|
|
235
272
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "netbox-plugin-prometheus-sd"
|
|
3
|
+
version = "v2.0.0" # placeholder
|
|
4
|
+
description = "A Netbox plugin to provide Netbox entires to Prometheus HTTP service discovery"
|
|
5
|
+
authors = ["Felix Peters <felix.peters@breuninger.de>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
packages = [
|
|
10
|
+
{ include = "netbox_prometheus_sd" },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[tool.poetry.dependencies]
|
|
14
|
+
python = ">=3.10,<4.0"
|
|
15
|
+
|
|
16
|
+
[tool.poetry.group.dev.dependencies]
|
|
17
|
+
invoke = ">=2.2,<4.0"
|
|
18
|
+
docker = "^7.1.0"
|
|
19
|
+
testcontainers = {extras = ["postgres", "redis", "generic"], version = "^4.14.1"}
|
|
20
|
+
python-semantic-release = "^10.5.3"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["poetry-core>=1.0.0"]
|
|
24
|
+
build-backend = "poetry.core.masonry.api"
|
|
25
|
+
|
|
26
|
+
[tool.semantic_release]
|
|
27
|
+
commit_parser = "conventional"
|
|
28
|
+
version_toml = ["pyproject.toml:tool.poetry.version"]
|
|
29
|
+
version_variables = ["netbox_prometheus_sd/__init__.py:__VERSION__"]
|
|
30
|
+
build_command = "pip install poetry && poetry build"
|
|
31
|
+
|
|
32
|
+
[tool.semantic_release.branches.main]
|
|
33
|
+
match = "main"
|
|
34
|
+
|
|
35
|
+
[tool.semantic_release.branches.prerelease]
|
|
36
|
+
# Matches branches with the prefixes 'pre/'.
|
|
37
|
+
match = "^(pre)/.+"
|
|
38
|
+
prerelease = true
|
|
39
|
+
prerelease_token = "pre"
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
from ipam.models import IPAddress, Service
|
|
2
|
-
from virtualization.models import VirtualMachine
|
|
3
|
-
from dcim.models.devices import Device
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
try: # Netbox >= 3.5
|
|
7
|
-
from netbox.api.viewsets import BaseViewSet
|
|
8
|
-
from netbox.api.viewsets.mixins import CustomFieldsMixin
|
|
9
|
-
from rest_framework.mixins import ListModelMixin
|
|
10
|
-
|
|
11
|
-
# Netbox MosdelViewSet with list only
|
|
12
|
-
class NetboxPrometheusSDModelViewSet(
|
|
13
|
-
CustomFieldsMixin, ListModelMixin, BaseViewSet
|
|
14
|
-
):
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
except ImportError:
|
|
18
|
-
try: # 3.2 >= Netbox < 3.5
|
|
19
|
-
from netbox.api.viewsets import (
|
|
20
|
-
NetBoxModelViewSet as NetboxPrometheusSDModelViewSet,
|
|
21
|
-
)
|
|
22
|
-
except ImportError: # Netbox < 3.2
|
|
23
|
-
from extras.api.views import (
|
|
24
|
-
CustomFieldModelViewSet as NetboxPrometheusSDModelViewSet,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
from .utils import NETBOX_RELEASE_CURRENT, NETBOX_RELEASE_41
|
|
28
|
-
|
|
29
|
-
# Filtersets have been renamed, we support both
|
|
30
|
-
# https://github.com/netbox-community/netbox/commit/1024782b9e0abb48f6da65f8248741227d53dbed#diff-d9224204dab475bbe888868c02235b8ef10f07c9201c45c90804d395dc161c40
|
|
31
|
-
try:
|
|
32
|
-
from ipam.filtersets import IPAddressFilterSet
|
|
33
|
-
from dcim.filtersets import DeviceFilterSet
|
|
34
|
-
from virtualization.filtersets import VirtualMachineFilterSet
|
|
35
|
-
except ImportError:
|
|
36
|
-
from ipam.filters import IPAddressFilterSet
|
|
37
|
-
from dcim.filters import DeviceFilterSet
|
|
38
|
-
from virtualization.filters import VirtualMachineFilterSet
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
from ..filtersets import ServiceFilterSet
|
|
42
|
-
from .serializers import (
|
|
43
|
-
PrometheusIPAddressSerializer,
|
|
44
|
-
PrometheusDeviceSerializer,
|
|
45
|
-
PrometheusVirtualMachineSerializer,
|
|
46
|
-
PrometheusServiceSerializer,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class ServiceViewSet(NetboxPrometheusSDModelViewSet):
|
|
51
|
-
queryset = Service.objects.prefetch_related(
|
|
52
|
-
"device",
|
|
53
|
-
"virtual_machine",
|
|
54
|
-
"ipaddresses",
|
|
55
|
-
"tags",
|
|
56
|
-
)
|
|
57
|
-
filterset_class = ServiceFilterSet
|
|
58
|
-
serializer_class = PrometheusServiceSerializer
|
|
59
|
-
pagination_class = None
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class VirtualMachineViewSet(NetboxPrometheusSDModelViewSet):
|
|
63
|
-
if NETBOX_RELEASE_CURRENT > NETBOX_RELEASE_41:
|
|
64
|
-
cluster_scope = "cluster__scope"
|
|
65
|
-
else:
|
|
66
|
-
cluster_scope = "cluster__site"
|
|
67
|
-
queryset = VirtualMachine.objects.prefetch_related(
|
|
68
|
-
cluster_scope,
|
|
69
|
-
"role",
|
|
70
|
-
"tenant",
|
|
71
|
-
"platform",
|
|
72
|
-
"primary_ip4",
|
|
73
|
-
"primary_ip6",
|
|
74
|
-
"tags",
|
|
75
|
-
"services",
|
|
76
|
-
"contacts",
|
|
77
|
-
)
|
|
78
|
-
filterset_class = VirtualMachineFilterSet
|
|
79
|
-
serializer_class = PrometheusVirtualMachineSerializer
|
|
80
|
-
pagination_class = None
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class DeviceViewSet(NetboxPrometheusSDModelViewSet):
|
|
84
|
-
queryset = Device.objects.prefetch_related(
|
|
85
|
-
"device_type__manufacturer",
|
|
86
|
-
"role" if hasattr(Device, "role") else "device_role",
|
|
87
|
-
"tenant",
|
|
88
|
-
"platform",
|
|
89
|
-
"site",
|
|
90
|
-
"location",
|
|
91
|
-
"rack",
|
|
92
|
-
"parent_bay",
|
|
93
|
-
"virtual_chassis__master",
|
|
94
|
-
"primary_ip4__nat_outside",
|
|
95
|
-
"primary_ip6__nat_outside",
|
|
96
|
-
"tags",
|
|
97
|
-
)
|
|
98
|
-
filterset_class = DeviceFilterSet
|
|
99
|
-
serializer_class = PrometheusDeviceSerializer
|
|
100
|
-
pagination_class = None
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
class IPAddressViewSet(NetboxPrometheusSDModelViewSet):
|
|
104
|
-
queryset = IPAddress.objects.prefetch_related("tenant", "tags")
|
|
105
|
-
serializer_class = PrometheusIPAddressSerializer
|
|
106
|
-
filterset_class = IPAddressFilterSet
|
|
107
|
-
pagination_class = None
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
try:
|
|
4
|
-
from users.models import ObjectPermission
|
|
5
|
-
from users.models import User
|
|
6
|
-
from core.models import ObjectType
|
|
7
|
-
|
|
8
|
-
except ImportError:
|
|
9
|
-
# Fallback for old NetBox versions < 4.0
|
|
10
|
-
from django.contrib.contenttypes.models import ContentType as ObjectType
|
|
11
|
-
from django.contrib.auth.models import User
|
|
12
|
-
|
|
13
|
-
from django.test import TestCase
|
|
14
|
-
|
|
15
|
-
from rest_framework.test import APIClient
|
|
16
|
-
from rest_framework import status
|
|
17
|
-
|
|
18
|
-
from . import utils
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ApiEndpointTests(TestCase):
|
|
22
|
-
"""Test cases for ensuring API endpoint is working properly."""
|
|
23
|
-
|
|
24
|
-
def setUp(self):
|
|
25
|
-
|
|
26
|
-
self.client = APIClient()
|
|
27
|
-
|
|
28
|
-
# Create test user and view permissions
|
|
29
|
-
user = User.objects.create_user("username", "Pas$w0rd")
|
|
30
|
-
obj_perm = ObjectPermission(name="test", actions=["view"])
|
|
31
|
-
obj_perm.save()
|
|
32
|
-
obj_perm.users.add(user)
|
|
33
|
-
obj_perm.object_types.add(
|
|
34
|
-
ObjectType.objects.get(app_label="dcim", model="device")
|
|
35
|
-
)
|
|
36
|
-
obj_perm.object_types.add(
|
|
37
|
-
ObjectType.objects.get(app_label="ipam", model="ipaddress")
|
|
38
|
-
)
|
|
39
|
-
obj_perm.object_types.add(
|
|
40
|
-
ObjectType.objects.get(app_label="ipam", model="service")
|
|
41
|
-
)
|
|
42
|
-
obj_perm.object_types.add(
|
|
43
|
-
ObjectType.objects.get(app_label="virtualization", model="virtualmachine")
|
|
44
|
-
)
|
|
45
|
-
self.client.force_authenticate(user)
|
|
46
|
-
|
|
47
|
-
def test_endpoint_device(self):
|
|
48
|
-
"""Ensure device endpoint returns a valid response"""
|
|
49
|
-
|
|
50
|
-
for i in range(1, 61):
|
|
51
|
-
utils.build_device_full(f"api-test-{i}.example.com", i)
|
|
52
|
-
|
|
53
|
-
resp = self.client.get("/api/plugins/prometheus-sd/devices/")
|
|
54
|
-
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
|
55
|
-
data = json.loads(resp.content)
|
|
56
|
-
|
|
57
|
-
self.assertIsNotNone(data[0]["targets"])
|
|
58
|
-
self.assertIsNotNone(data[0]["labels"])
|
|
59
|
-
self.assertEqual(len(data), 60)
|
|
60
|
-
|
|
61
|
-
def test_endpoint_virtual_machine(self):
|
|
62
|
-
"""Ensure virtual machine endpoint returns a valid response"""
|
|
63
|
-
|
|
64
|
-
for i in range(1, 61):
|
|
65
|
-
utils.build_vm_full(f"api-test-vm-{i}.example.com", i)
|
|
66
|
-
|
|
67
|
-
resp = self.client.get("/api/plugins/prometheus-sd/virtual-machines/")
|
|
68
|
-
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
|
69
|
-
data = json.loads(resp.content)
|
|
70
|
-
|
|
71
|
-
self.assertIsNotNone(data[0]["targets"])
|
|
72
|
-
self.assertIsNotNone(data[0]["labels"])
|
|
73
|
-
# Full vm contains two entry in the config context so we have to double the number of vm
|
|
74
|
-
self.assertEqual(len(data), 120)
|
|
75
|
-
|
|
76
|
-
def test_endpoint_ip_address(self):
|
|
77
|
-
"""Ensure ip address endpoint returns a valid response"""
|
|
78
|
-
|
|
79
|
-
for i in range(1, 61):
|
|
80
|
-
utils.build_full_ip(address=f"10.10.10.{i}/24")
|
|
81
|
-
|
|
82
|
-
resp = self.client.get("/api/plugins/prometheus-sd/ip-addresses/")
|
|
83
|
-
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
|
84
|
-
data = json.loads(resp.content)
|
|
85
|
-
|
|
86
|
-
self.assertIsNotNone(data[0]["targets"])
|
|
87
|
-
self.assertIsNotNone(data[0]["labels"])
|
|
88
|
-
self.assertEqual(len(data), 60)
|
|
89
|
-
|
|
90
|
-
def test_endpoint_service(self):
|
|
91
|
-
"""Ensure service endpoint returns a valid response"""
|
|
92
|
-
|
|
93
|
-
for i in range(1, 61):
|
|
94
|
-
utils.build_vm_full(f"api-test-vm-{i}.example.com", i)
|
|
95
|
-
|
|
96
|
-
resp = self.client.get("/api/plugins/prometheus-sd/services/")
|
|
97
|
-
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
|
98
|
-
data = json.loads(resp.content)
|
|
99
|
-
|
|
100
|
-
self.assertIsNotNone(data[0]["targets"])
|
|
101
|
-
self.assertIsNotNone(data[0]["labels"])
|
|
102
|
-
self.assertEqual(len(data), 60)
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "netbox-plugin-prometheus-sd"
|
|
3
|
-
version = "v1.2.0" # placeholder
|
|
4
|
-
description = "A Netbox plugin to provide Netbox entires to Prometheus HTTP service discovery"
|
|
5
|
-
authors = ["Felix Peters <felix.peters@breuninger.de>"]
|
|
6
|
-
license = "MIT"
|
|
7
|
-
readme = "README.md"
|
|
8
|
-
|
|
9
|
-
packages = [
|
|
10
|
-
{ include = "netbox_prometheus_sd" },
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
[tool.poetry.dependencies]
|
|
14
|
-
python = ">=3.8.1,<4.0"
|
|
15
|
-
|
|
16
|
-
[tool.poetry.dev-dependencies]
|
|
17
|
-
invoke = "^2.2.0"
|
|
18
|
-
typed-ast = "^1.5.5"
|
|
19
|
-
|
|
20
|
-
[build-system]
|
|
21
|
-
requires = ["poetry-core>=1.0.0"]
|
|
22
|
-
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|