netbox-loki 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,116 @@
1
+ """
2
+ NetBox Loki plugin.
3
+
4
+ Display recent Loki logs in Device and VirtualMachine detail pages.
5
+ """
6
+
7
+ import logging
8
+
9
+ from netbox.plugins import PluginConfig
10
+
11
+ __version__ = "1.2.0"
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class LokiConfig(PluginConfig):
17
+ """Plugin configuration for NetBox Loki integration."""
18
+
19
+ name = "netbox_loki"
20
+ verbose_name = "NetBox Loki Logs"
21
+ description = "Display recent Loki logs in device, VM, and endpoint detail pages"
22
+ version = __version__
23
+ author = "-----"
24
+ author_email = "---@gmail.com"
25
+ base_url = "loki"
26
+ min_version = "4.0.0"
27
+ max_version = "5.99"
28
+
29
+ # Required settings - plugin won't load without these
30
+ required_settings = []
31
+
32
+ # Default configuration values
33
+ default_settings = {
34
+ "loki_url": "http://localhost:3100",
35
+ "loki_external_url": "",
36
+ "loki_tenant": "docker",
37
+ "loki_username": "",
38
+ "loki_password": "",
39
+ "loki_bearer_token": "",
40
+ "loki_job": "syslog",
41
+ "device_label": "routerboard",
42
+ "stream_selector": "",
43
+ "log_limit": 100,
44
+ "time_range": 3600,
45
+ "timeout": 10,
46
+ "cache_timeout": 60,
47
+ "verify_tls": True,
48
+ "use_regex_matching": True,
49
+ "fallback_to_ip": True,
50
+ }
51
+
52
+ def ready(self):
53
+ """Register endpoint view if netbox_endpoints is available."""
54
+ super().ready()
55
+ from . import widgets # noqa: F401
56
+
57
+ self._register_endpoint_views()
58
+
59
+ def _register_endpoint_views(self):
60
+ """Register Loki Logs tab for Endpoints if plugin is installed."""
61
+ import sys
62
+
63
+ # Quick check if netbox_endpoints is available
64
+ if "netbox_endpoints" not in sys.modules:
65
+ try:
66
+ import importlib.util
67
+
68
+ if importlib.util.find_spec("netbox_endpoints") is None:
69
+ logger.debug("netbox_endpoints not installed, skipping endpoint view registration")
70
+ return
71
+ except Exception:
72
+ logger.debug("netbox_endpoints not available, skipping endpoint view registration")
73
+ return
74
+
75
+ try:
76
+ from django.shortcuts import render
77
+ from netbox.views import generic
78
+ from netbox_endpoints.models import Endpoint
79
+ from utilities.views import ViewTab, register_model_view
80
+
81
+ @register_model_view(Endpoint, name="loki_logs", path="logs")
82
+ class EndpointLokiLogsView(generic.ObjectView):
83
+ """Display Loki logs for an Endpoint with async loading."""
84
+
85
+ queryset = Endpoint.objects.all()
86
+ template_name = "netbox_loki/endpoint_logs_tab.html"
87
+
88
+ tab = ViewTab(
89
+ label="Loki",
90
+ weight=9004,
91
+ permission="netbox_endpoints.view_endpoint",
92
+ hide_if_empty=False,
93
+ )
94
+
95
+ def get(self, request, pk):
96
+ endpoint = Endpoint.objects.get(pk=pk)
97
+ time_range = request.GET.get("range", "")
98
+ return render(
99
+ request,
100
+ self.template_name,
101
+ {
102
+ "object": endpoint,
103
+ "tab": self.tab,
104
+ "loading": True,
105
+ "time_range_param": time_range,
106
+ },
107
+ )
108
+
109
+ logger.info("Registered Loki Logs tab for Endpoint model")
110
+ except ImportError:
111
+ logger.debug("netbox_endpoints not installed, skipping endpoint view registration")
112
+ except Exception as e:
113
+ logger.warning(f"Could not register endpoint views: {e}")
114
+
115
+
116
+ config = LokiConfig
netbox_loki/forms.py ADDED
@@ -0,0 +1,174 @@
1
+ """
2
+ Forms for NetBox Loki plugin settings.
3
+ """
4
+
5
+ from django import forms
6
+
7
+
8
+ class LokiSettingsForm(forms.Form):
9
+ """Form for displaying Loki plugin settings."""
10
+
11
+ loki_url = forms.URLField(
12
+ label="Loki URL",
13
+ help_text="Base URL for the Loki HTTP API, for example: http://loki:3100",
14
+ required=True,
15
+ widget=forms.URLInput(
16
+ attrs={
17
+ "class": "form-control",
18
+ "placeholder": "http://loki:3100",
19
+ }
20
+ ),
21
+ )
22
+
23
+ loki_external_url = forms.URLField(
24
+ label="External URL",
25
+ help_text="Optional browser URL for opening Loki or Grafana from the NetBox UI.",
26
+ required=False,
27
+ widget=forms.URLInput(
28
+ attrs={
29
+ "class": "form-control",
30
+ "placeholder": "https://grafana.example.com/explore",
31
+ }
32
+ ),
33
+ )
34
+
35
+ loki_tenant = forms.CharField(
36
+ label="Tenant Header",
37
+ help_text="Optional value for the X-Scope-OrgID header when Loki multi-tenancy is enabled.",
38
+ required=False,
39
+ initial="docker",
40
+ widget=forms.TextInput(
41
+ attrs={
42
+ "class": "form-control",
43
+ "placeholder": "docker",
44
+ }
45
+ ),
46
+ )
47
+
48
+ loki_username = forms.CharField(
49
+ label="Username",
50
+ help_text="Optional basic-auth username used by a reverse proxy or managed Loki.",
51
+ required=False,
52
+ widget=forms.TextInput(attrs={"class": "form-control"}),
53
+ )
54
+
55
+ loki_password = forms.CharField(
56
+ label="Password",
57
+ help_text="Optional basic-auth password.",
58
+ required=False,
59
+ widget=forms.PasswordInput(attrs={"class": "form-control"}, render_value=True),
60
+ )
61
+
62
+ loki_bearer_token = forms.CharField(
63
+ label="Bearer Token",
64
+ help_text="Optional bearer token if your Loki endpoint is protected by token auth.",
65
+ required=False,
66
+ widget=forms.PasswordInput(attrs={"class": "form-control"}, render_value=True),
67
+ )
68
+
69
+ loki_job = forms.CharField(
70
+ label="Job Label Value",
71
+ help_text="Optional value for the standard Loki label `job`, for example: `syslog`.",
72
+ required=False,
73
+ initial="syslog",
74
+ widget=forms.TextInput(
75
+ attrs={
76
+ "class": "form-control",
77
+ "placeholder": "syslog",
78
+ }
79
+ ),
80
+ )
81
+
82
+ device_label = forms.CharField(
83
+ label="Device Label",
84
+ help_text="Loki label that stores the device or VM name, for example: `routerboard`, `host`, or `hostname`.",
85
+ required=True,
86
+ initial="routerboard",
87
+ widget=forms.TextInput(
88
+ attrs={
89
+ "class": "form-control",
90
+ "placeholder": "routerboard",
91
+ }
92
+ ),
93
+ )
94
+
95
+ stream_selector = forms.CharField(
96
+ label="Extra Stream Selector",
97
+ help_text='Optional extra Loki label selector fragment without braces, for example: `site="kyiv",env="prod"`.',
98
+ required=False,
99
+ widget=forms.TextInput(
100
+ attrs={
101
+ "class": "form-control",
102
+ "placeholder": 'site="kyiv",env="prod"',
103
+ }
104
+ ),
105
+ )
106
+
107
+ log_limit = forms.IntegerField(
108
+ label="Log Limit",
109
+ help_text="Maximum number of log lines to display per request.",
110
+ required=False,
111
+ initial=100,
112
+ min_value=10,
113
+ max_value=500,
114
+ widget=forms.NumberInput(attrs={"class": "form-control"}),
115
+ )
116
+
117
+ time_range = forms.ChoiceField(
118
+ label="Default Time Range",
119
+ help_text="Default time range for log queries.",
120
+ choices=[
121
+ (300, "5 minutes"),
122
+ (900, "15 minutes"),
123
+ (3600, "1 hour"),
124
+ (14400, "4 hours"),
125
+ (86400, "24 hours"),
126
+ (604800, "7 days"),
127
+ ],
128
+ initial=3600,
129
+ widget=forms.Select(attrs={"class": "form-control"}),
130
+ )
131
+
132
+ timeout = forms.IntegerField(
133
+ label="API Timeout",
134
+ help_text="Timeout for Loki API requests in seconds.",
135
+ required=False,
136
+ initial=10,
137
+ min_value=5,
138
+ max_value=60,
139
+ widget=forms.NumberInput(attrs={"class": "form-control"}),
140
+ )
141
+
142
+ cache_timeout = forms.IntegerField(
143
+ label="Cache Timeout",
144
+ help_text="How long to cache API responses in seconds.",
145
+ required=False,
146
+ initial=60,
147
+ min_value=0,
148
+ max_value=300,
149
+ widget=forms.NumberInput(attrs={"class": "form-control"}),
150
+ )
151
+
152
+ verify_tls = forms.BooleanField(
153
+ label="Verify TLS Certificates",
154
+ help_text="Disable only if your Loki endpoint uses a self-signed certificate.",
155
+ required=False,
156
+ initial=True,
157
+ widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
158
+ )
159
+
160
+ use_regex_matching = forms.BooleanField(
161
+ label="Allow Shortname/FQDN Matching",
162
+ help_text="Match `router1` against both `router1` and `router1.example.com` in Loki labels.",
163
+ required=False,
164
+ initial=True,
165
+ widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
166
+ )
167
+
168
+ fallback_to_ip = forms.BooleanField(
169
+ label="Fallback To Primary IP",
170
+ help_text="If hostname lookup returns no logs, try the primary IP from NetBox.",
171
+ required=False,
172
+ initial=True,
173
+ widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
174
+ )
@@ -0,0 +1,377 @@
1
+ """
2
+ Loki API client.
3
+
4
+ The module name stays the same to preserve import compatibility with the
5
+ existing plugin package layout.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import re
12
+ from datetime import datetime, timedelta, timezone
13
+ from typing import Any
14
+
15
+ import requests
16
+ from django.conf import settings
17
+ from django.core.cache import cache
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def _read_netbox_setting(config: dict[str, Any], plugin_key: str, setting_names: tuple[str, ...], default: Any) -> Any:
23
+ """Read a plugin setting from PLUGINS_CONFIG first, then from top-level Django settings."""
24
+ if plugin_key in config:
25
+ return config[plugin_key]
26
+
27
+ for setting_name in setting_names:
28
+ if hasattr(settings, setting_name):
29
+ return getattr(settings, setting_name)
30
+
31
+ return default
32
+
33
+
34
+ class LokiClient:
35
+ """Client for querying Grafana Loki."""
36
+
37
+ def __init__(self) -> None:
38
+ self.config = settings.PLUGINS_CONFIG.get("netbox_loki", {})
39
+ self.base_url = _read_netbox_setting(
40
+ self.config,
41
+ "loki_url",
42
+ ("NETBOX_LOKI_LOKI_URL", "LOKI_URL"),
43
+ "http://localhost:3100",
44
+ ).rstrip("/")
45
+ self.timeout = _read_netbox_setting(self.config, "timeout", ("NETBOX_LOKI_TIMEOUT",), 10)
46
+ self.cache_timeout = _read_netbox_setting(self.config, "cache_timeout", ("NETBOX_LOKI_CACHE_TIMEOUT",), 60)
47
+ self.verify_tls = _read_netbox_setting(self.config, "verify_tls", ("NETBOX_LOKI_VERIFY_TLS",), True)
48
+
49
+ def _get_headers(self) -> dict[str, str]:
50
+ headers = {
51
+ "Accept": "application/json",
52
+ "X-Requested-By": "NetBox-Loki-Plugin",
53
+ }
54
+ tenant = str(
55
+ _read_netbox_setting(
56
+ self.config,
57
+ "loki_tenant",
58
+ ("NETBOX_LOKI_TENANT", "LOKI_TENANT", "LOKI_X_SCOPE_ORGID"),
59
+ "docker",
60
+ )
61
+ ).strip()
62
+ bearer = str(
63
+ _read_netbox_setting(
64
+ self.config,
65
+ "loki_bearer_token",
66
+ ("NETBOX_LOKI_BEARER_TOKEN", "LOKI_BEARER_TOKEN"),
67
+ "",
68
+ )
69
+ ).strip()
70
+ if tenant:
71
+ headers["X-Scope-OrgID"] = tenant
72
+ if bearer:
73
+ headers["Authorization"] = f"Bearer {bearer}"
74
+ return headers
75
+
76
+ def _get_auth(self) -> tuple[str, str] | None:
77
+ username = str(
78
+ _read_netbox_setting(
79
+ self.config,
80
+ "loki_username",
81
+ ("NETBOX_LOKI_USERNAME", "LOKI_USERNAME"),
82
+ "",
83
+ )
84
+ ).strip()
85
+ password = _read_netbox_setting(
86
+ self.config,
87
+ "loki_password",
88
+ ("NETBOX_LOKI_PASSWORD", "LOKI_PASSWORD"),
89
+ "",
90
+ )
91
+ if username:
92
+ return (username, password)
93
+ return None
94
+
95
+ def _request(
96
+ self,
97
+ *,
98
+ endpoint: str,
99
+ params: dict[str, Any],
100
+ ) -> dict[str, Any]:
101
+ response = requests.get(
102
+ f"{self.base_url}{endpoint}",
103
+ params=params,
104
+ headers=self._get_headers(),
105
+ auth=self._get_auth(),
106
+ timeout=self.timeout,
107
+ verify=self.verify_tls,
108
+ )
109
+ response.raise_for_status()
110
+ return response.json()
111
+
112
+ @staticmethod
113
+ def _ns_timestamp(value: datetime) -> str:
114
+ return str(int(value.timestamp() * 1_000_000_000))
115
+
116
+ @staticmethod
117
+ def _format_timestamp(raw_value: str) -> str:
118
+ try:
119
+ as_int = int(raw_value)
120
+ return datetime.fromtimestamp(as_int / 1_000_000_000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
121
+ except (TypeError, ValueError, OSError):
122
+ return raw_value
123
+
124
+ def _default_selector_parts(self) -> list[str]:
125
+ parts: list[str] = []
126
+
127
+ job = self.config.get("loki_job", "").strip()
128
+ if job:
129
+ parts.append(f'job="{self._escape_label_value(job)}"')
130
+
131
+ extra_selector = self.config.get("stream_selector", "").strip()
132
+ if extra_selector:
133
+ parts.append(extra_selector)
134
+
135
+ return parts
136
+
137
+ @staticmethod
138
+ def _escape_label_value(value: str) -> str:
139
+ return value.replace("\\", "\\\\").replace('"', '\\"')
140
+
141
+ def _build_value_matcher(self, value: str) -> tuple[str, str]:
142
+ if self.config.get("use_regex_matching", True):
143
+ if "." in value:
144
+ return "=~", self._escape_label_value(f"^{re.escape(value)}$")
145
+ return "=~", self._escape_label_value(f"^{re.escape(value)}(?:\\..+)?$")
146
+ return "=", self._escape_label_value(value)
147
+
148
+ def _build_selector(self, *, label_name: str | None = None, value: str | None = None) -> str:
149
+ selector_parts = self._default_selector_parts()
150
+ effective_label = label_name or self.config.get("device_label", "routerboard")
151
+
152
+ if value:
153
+ operator, matcher = self._build_value_matcher(value)
154
+ selector_parts.append(f'{effective_label}{operator}"{matcher}"')
155
+
156
+ return "{" + ",".join(selector_parts) + "}"
157
+
158
+ def search_logs(
159
+ self,
160
+ value: str | None = None,
161
+ *,
162
+ time_range: int | None = None,
163
+ limit: int | None = None,
164
+ label_name: str | None = None,
165
+ ) -> dict[str, Any]:
166
+ time_range = int(time_range or self.config.get("time_range", 3600))
167
+ limit = int(limit or self.config.get("log_limit", 100))
168
+ selector = self._build_selector(label_name=label_name, value=value)
169
+
170
+ cache_key = f"loki_logs::{selector}::{time_range}::{limit}"
171
+ cached = cache.get(cache_key)
172
+ if cached is not None:
173
+ return cached
174
+
175
+ end = datetime.now(timezone.utc)
176
+ start = end - timedelta(seconds=time_range)
177
+ query = selector
178
+
179
+ try:
180
+ payload = self._request(
181
+ endpoint="/loki/api/v1/query_range",
182
+ params={
183
+ "query": query,
184
+ "start": self._ns_timestamp(start),
185
+ "end": self._ns_timestamp(end),
186
+ "limit": limit,
187
+ "direction": "backward",
188
+ },
189
+ )
190
+ result = self._parse_log_response(payload, query=query, time_range=time_range)
191
+ cache.set(cache_key, result, self.cache_timeout)
192
+ return result
193
+ except requests.RequestException as exc:
194
+ logger.warning("Loki request failed for query %s: %s", query, exc)
195
+ return {"error": str(exc), "messages": [], "query": query, "time_range": time_range}
196
+
197
+ def _parse_log_response(self, payload: dict[str, Any], *, query: str, time_range: int) -> dict[str, Any]:
198
+ messages: list[dict[str, Any]] = []
199
+ data = payload.get("data", {})
200
+
201
+ for stream in data.get("result", []):
202
+ labels = stream.get("stream", {})
203
+ for timestamp, line in stream.get("values", []):
204
+ messages.append(
205
+ {
206
+ "message": {
207
+ "timestamp": timestamp,
208
+ "timestamp_display": self._format_timestamp(timestamp),
209
+ "message": line,
210
+ "source": self._derive_source(labels),
211
+ "job": labels.get("job", ""),
212
+ "severity": self._derive_severity(labels, line),
213
+ "labels": labels,
214
+ }
215
+ }
216
+ )
217
+
218
+ messages.sort(key=lambda item: item["message"]["timestamp"], reverse=True)
219
+
220
+ return {
221
+ "messages": messages,
222
+ "total_results": len(messages),
223
+ "query": query,
224
+ "time_range": time_range,
225
+ }
226
+
227
+ def _derive_source(self, labels: dict[str, str]) -> str:
228
+ configured_label = self.config.get("device_label", "routerboard")
229
+ for key in (configured_label, "host", "hostname", "instance", "source"):
230
+ value = labels.get(key)
231
+ if value:
232
+ return value
233
+ return ""
234
+
235
+ @staticmethod
236
+ def _derive_severity(labels: dict[str, str], line: str) -> str:
237
+ label_severity = labels.get("severity") or labels.get("level")
238
+ if label_severity:
239
+ return str(label_severity)
240
+
241
+ match = re.search(r":(?P<severity>[A-Za-z][A-Za-z0-9_-]{1,31}):", line)
242
+ if match:
243
+ raw = match.group("severity")
244
+ return raw[:1].upper() + raw[1:]
245
+
246
+ return ""
247
+
248
+ @staticmethod
249
+ def _primary_ip_value(primary_ip: Any) -> str | None:
250
+ if not primary_ip:
251
+ return None
252
+ address = getattr(primary_ip, "address", None)
253
+ if not address:
254
+ return None
255
+ return str(address).split("/")[0]
256
+
257
+ def _search_with_fallback(
258
+ self,
259
+ *,
260
+ name: str,
261
+ time_range: int | None,
262
+ fallback_ip: str | None = None,
263
+ label_name: str | None = None,
264
+ ) -> dict[str, Any]:
265
+ result = self.search_logs(name, time_range=time_range, label_name=label_name)
266
+ result["search_type"] = "name"
267
+
268
+ if result.get("messages") or not self.config.get("fallback_to_ip", True) or not fallback_ip:
269
+ return result
270
+
271
+ fallback = self.search_logs(fallback_ip, time_range=time_range, label_name=label_name)
272
+ fallback["search_type"] = "ip"
273
+ return fallback
274
+
275
+ def get_logs_for_device(self, device: Any, *, time_range: int | None = None) -> dict[str, Any]:
276
+ hostname = device.virtual_chassis.name if getattr(device, "virtual_chassis", None) else device.name
277
+ fallback_ip = self._primary_ip_value(getattr(device, "primary_ip", None))
278
+ result = self._search_with_fallback(
279
+ name=hostname,
280
+ time_range=time_range,
281
+ fallback_ip=fallback_ip,
282
+ )
283
+ result["device_name"] = device.name
284
+ return result
285
+
286
+ def get_logs_for_vm(self, vm: Any, *, time_range: int | None = None) -> dict[str, Any]:
287
+ fallback_ip = self._primary_ip_value(getattr(vm, "primary_ip", None))
288
+ result = self._search_with_fallback(
289
+ name=vm.name,
290
+ time_range=time_range,
291
+ fallback_ip=fallback_ip,
292
+ )
293
+ result["vm_name"] = vm.name
294
+ return result
295
+
296
+ def get_logs_for_endpoint(self, endpoint: Any, *, time_range: int | None = None) -> dict[str, Any]:
297
+ search_value = endpoint.name or str(endpoint.mac_address)
298
+ fallback_ip = self._primary_ip_value(getattr(endpoint, "primary_ip", None))
299
+ result = self._search_with_fallback(
300
+ name=search_value,
301
+ time_range=time_range,
302
+ fallback_ip=fallback_ip,
303
+ )
304
+ result["endpoint_name"] = search_value
305
+ return result
306
+
307
+ def _instant_scalar_query(self, query: str) -> int:
308
+ payload = self._request(
309
+ endpoint="/loki/api/v1/query",
310
+ params={
311
+ "query": query,
312
+ "time": self._ns_timestamp(datetime.now(timezone.utc)),
313
+ },
314
+ )
315
+ result = payload.get("data", {}).get("result", [])
316
+ if not result:
317
+ return 0
318
+
319
+ value = result[0].get("value", [None, "0"])
320
+ try:
321
+ return int(float(value[1]))
322
+ except (TypeError, ValueError, IndexError):
323
+ return 0
324
+
325
+ @staticmethod
326
+ def _duration_expr(time_range: int) -> str:
327
+ if time_range % 86400 == 0:
328
+ return f"{time_range // 86400}d"
329
+ if time_range % 3600 == 0:
330
+ return f"{time_range // 3600}h"
331
+ if time_range % 60 == 0:
332
+ return f"{time_range // 60}m"
333
+ return f"{time_range}s"
334
+
335
+ def get_log_summary(self, time_range: int = 3600, cache_timeout: int = 120) -> dict[str, Any]:
336
+ cache_key = f"loki_log_summary::{time_range}"
337
+ cached = cache.get(cache_key)
338
+ if cached is not None:
339
+ cached["cached"] = True
340
+ return cached
341
+
342
+ selector = self._build_selector()
343
+ range_expr = self._duration_expr(time_range)
344
+
345
+ try:
346
+ total = self._instant_scalar_query(f"sum(count_over_time({selector}[{range_expr}]))")
347
+ errors = self._instant_scalar_query(
348
+ f'sum(count_over_time({selector} |~ "(?i)error|err|critical|fatal"[{range_expr}]))'
349
+ )
350
+ warnings = self._instant_scalar_query(
351
+ f'sum(count_over_time({selector} |~ "(?i)warn|warning"[{range_expr}]))'
352
+ )
353
+ except requests.RequestException as exc:
354
+ logger.warning("Loki summary query failed: %s", exc)
355
+ return {"error": str(exc)}
356
+
357
+ summary = {
358
+ "total": total,
359
+ "errors": errors,
360
+ "warnings": warnings,
361
+ "cached": False,
362
+ }
363
+ cache.set(cache_key, summary, cache_timeout)
364
+ return summary
365
+
366
+
367
+ LokiClient = LokiClient
368
+
369
+ _client: LokiClient | None = None
370
+
371
+
372
+ def get_client() -> LokiClient:
373
+ """Get or create the Loki client singleton."""
374
+ global _client
375
+ if _client is None:
376
+ _client = LokiClient()
377
+ return _client
@@ -0,0 +1,20 @@
1
+ from django.db import migrations
2
+
3
+
4
+ class Migration(migrations.Migration):
5
+
6
+ initial = True
7
+
8
+ dependencies = []
9
+
10
+ operations = [
11
+ migrations.CreateModel(
12
+ name="GraylogPermission",
13
+ fields=[],
14
+ options={
15
+ "managed": False,
16
+ "default_permissions": (),
17
+ "permissions": (("configure_graylog", "Can configure Graylog plugin settings"),),
18
+ },
19
+ ),
20
+ ]
@@ -0,0 +1,15 @@
1
+ from django.db import migrations
2
+
3
+
4
+ class Migration(migrations.Migration):
5
+
6
+ dependencies = [
7
+ ("netbox_graylog", "0001_initial"),
8
+ ]
9
+
10
+ operations = [
11
+ migrations.RenameModel(
12
+ old_name="GraylogPermission",
13
+ new_name="Graylog",
14
+ ),
15
+ ]