Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 3 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ at [https://netboxlabs.com/blog/introducing-diode-streamlining-data-ingestion-in
|:--------------:|:--------------:|
| >= 3.7.2 | 0.1.0 |
| >= 4.1.0 | 0.4.0 |
| >= 4.2.3 | 1.0.0 |

## Installation

Expand Down Expand Up @@ -46,32 +47,18 @@ Also in your `configuration.py` file, in order to customise the plugin settings,
```python
PLUGINS_CONFIG = {
"netbox_diode_plugin": {
# Auto-provision users for Diode plugin
"auto_provision_users": False,

# Diode gRPC target for communication with Diode server
"diode_target_override": "grpc://localhost:8080/diode",

# User allowed for Diode to NetBox communication
"diode_to_netbox_username": "diode-to-netbox",

# User allowed for NetBox to Diode communication
"netbox_to_diode_username": "netbox-to-diode",

# User allowed for data ingestion
"diode_username": "diode-ingestion",
# Username associated with changes applied via plugin
"diode_username": "diode",
},
}
```

Note: Once you customise usernames with PLUGINS_CONFIG during first installation, you should not change or remove them
later on. Doing so will cause the plugin to stop working properly.

`auto_provision_users` is a boolean flag (default: `False`) that determines whether the plugin should automatically
create the users during
migration. If set to `False`, you will need to provision Diode users with their API keys manually via the plugin's setup
page in the NetBox UI.

Restart NetBox services to load the plugin:

```
Expand All @@ -89,28 +76,6 @@ cd /opt/netbox
source venv/bin/activate
```

Three API keys will be needed (these are random 40 character long alphanumeric strings). They can be generated and set
to the appropriate environment variables with the following commands:

```shell
# API key for the Diode service to interact with NetBox
export DIODE_TO_NETBOX_API_KEY=$(head -c20 </dev/urandom|xxd -p); env | grep DIODE_TO_NETBOX_API_KEY
# API key for the NetBox service to interact with Diode
export NETBOX_TO_DIODE_API_KEY=$(head -c20 </dev/urandom|xxd -p); env | grep NETBOX_TO_DIODE_API_KEY
# API key for Diode SDKs to ingest data into Diode
export DIODE_API_KEY=$(head -c20 </dev/urandom|xxd -p); env | grep DIODE_API_KEY
```

**Note:** store these API key strings in a safe place as they will be needed later to configure the Diode server.

If you don't set these environment variables, the plugin will generate random API keys for you either during the
migration process (with `auto_provision_users` set to `True`) or when you manually create the users in the plugin's
setup page in the NetBox UI.

It's important to note that environment variables with API keys should be populated in the Diode server's
environment variables (see [docs](https://github.com/netboxlabs/diode/tree/develop/diode-server#running-the-diode-server))
as well to ensure proper communication between the Diode SDK, Diode server and the NetBox plugin.

Run migrations to create all necessary resources:

```shell
Expand Down
5 changes: 1 addition & 4 deletions docker/netbox/env/netbox.env
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,5 @@ SUPERUSER_NAME=admin
SUPERUSER_PASSWORD=admin
WEBHOOKS_ENABLED=true
RELOAD_NETBOX_ON_DIODE_PLUGIN_CHANGE=false
DIODE_TO_NETBOX_API_KEY=1368dbad13e418d5a443d93cf255edde03a2a754
NETBOX_TO_DIODE_API_KEY=1e99338b8cab5fc637bc55f390bda1446f619c42
DIODE_API_KEY=5a52c45ee8231156cb620d193b0291912dd15433
BASE_PATH=netbox/
DEBUG=True
DEBUG=False
4 changes: 3 additions & 1 deletion docker/netbox/plugins_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

PLUGINS_CONFIG = {
"netbox_diode_plugin": {
"auto_provision_users": True,
# Diode gRPC target for communication with Diode server
"diode_target_override": "grpc://host.docker.internal:8080/diode",

# Username associated with changes applied via plugin
"diode_username": "diode",
}
}
2 changes: 1 addition & 1 deletion netbox-plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: 0.1
package_name: netboxlabs-diode-netbox-plugin
compatibility:
- release: 0.7.0
- release: 1.0.0
netbox_min: 4.2.3
netbox_max: 4.2.3
- release: 0.6.0
Expand Down
13 changes: 4 additions & 9 deletions netbox_diode_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# Copyright 2024 NetBox Labs Inc
# Copyright 2025 NetBox Labs, Inc.
"""Diode NetBox Plugin."""

from netbox.plugins import PluginConfig
Expand All @@ -17,16 +17,11 @@ class NetBoxDiodePluginConfig(PluginConfig):
base_url = "diode"
min_version = "4.2.3"
default_settings = {
# Auto-provision users for Diode plugin
"auto_provision_users": False,
# Default Diode gRPC target for communication with Diode server
"diode_target": "grpc://localhost:8080/diode",
# User allowed for Diode to NetBox communication
"diode_to_netbox_username": "diode-to-netbox",
# User allowed for NetBox to Diode communication
"netbox_to_diode_username": "netbox-to-diode",
# User allowed for data ingestion
"diode_username": "diode-ingestion",

# Default username associated with changes applied via plugin
"diode_username": "diode",
}


Expand Down
2 changes: 1 addition & 1 deletion netbox_diode_plugin/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env python
# Copyright 2024 NetBox Labs Inc
# Copyright 2025 NetBox Labs, Inc.
"""Diode NetBox Plugin - API."""
4 changes: 1 addition & 3 deletions netbox_diode_plugin/api/applier.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
#!/usr/bin/env python
# Copyright 2024 NetBox Labs Inc
# Copyright 2025 NetBox Labs, Inc.
"""Diode NetBox Plugin - API - Applier."""


import logging

from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.utils import IntegrityError
Expand Down
82 changes: 82 additions & 0 deletions netbox_diode_plugin/api/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python
# Copyright 2025 NetBox Labs, Inc.
"""Diode NetBox Plugin - API Authentication."""

import hashlib
import logging
from types import SimpleNamespace

import requests
from django.core.cache import cache
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed

from netbox_diode_plugin.plugin_config import (
get_diode_auth_introspect_url,
get_diode_user,
)

logger = logging.getLogger("netbox.diode_data")


class DiodeOAuth2Authentication(BaseAuthentication):
"""Diode OAuth2 Client Credentials Authentication."""

def authenticate(self, request):
"""Authenticate the request and return the user info."""
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return None

token = auth_header[7:].strip()

diode_user = self._introspect_token(token)
if not diode_user:
raise AuthenticationFailed("Invalid token")

request.user = diode_user.user
request.token_scopes = diode_user.token_scopes
request.token_data = diode_user.token_data

return (diode_user.user, None)

def _introspect_token(self, token: str):
"""Introspect the token and return the client info."""
hash_token = hashlib.sha256(token.encode()).hexdigest()
cache_key = f"diode:oauth2:introspect:{hash_token}"
cached_user = cache.get(cache_key)
if cached_user:
return cached_user

introspect_url = get_diode_auth_introspect_url()

if not introspect_url:
logger.error("Diode Auth introspect URL is not configured")
return None

try:
response = requests.post(
introspect_url, headers={"Authorization": f"Bearer {token}"}, timeout=5
)
response.raise_for_status()
data = response.json()
except Exception as e:
logger.error(f"Diode Auth token introspection failed: {e}")
return None

if data.get("active"):
diode_user = SimpleNamespace(
user=get_diode_user(),
token_scopes=data.get("scope", "").split(),
token_data=data,
)

expires_in = (
data.get("exp") - data.get("iat")
if "exp" in data and "iat" in data
else 300
)
cache.set(cache_key, diode_user, timeout=expires_in)
return diode_user

return None
4 changes: 0 additions & 4 deletions netbox_diode_plugin/api/differ.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@

import copy
import datetime
import decimal
import logging

import netaddr
from django.contrib.contenttypes.models import ContentType
from django.db.backends.postgresql.psycopg_any import NumericRange
from netaddr.eui import EUI
from rest_framework import serializers
from utilities.data import shallow_compare_dict

Expand Down
7 changes: 2 additions & 5 deletions netbox_diode_plugin/api/matcher.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
#!/usr/bin/env python
# Copyright 2024 NetBox Labs Inc
# Copyright 2025 NetBox Labs, Inc.
"""Diode NetBox Plugin - API - Object matching utilities."""

import copy
import logging
from dataclasses import dataclass
from functools import cache, lru_cache
from typing import Type

import netaddr
from core.models import ObjectType as NetBoxType
from django.conf import settings
from django.contrib.contenttypes.fields import ContentType
from django.core.exceptions import FieldDoesNotExist
from django.db import models
Expand All @@ -20,7 +17,7 @@
from django.db.models.query_utils import Q
from extras.models.customfields import CustomField

from .common import _TRACE, AutoSlug, UnresolvedReference
from .common import _TRACE, UnresolvedReference
from .plugin_utils import content_type_id, get_object_type, get_object_type_model

logger = logging.getLogger(__name__)
Expand Down
36 changes: 21 additions & 15 deletions netbox_diode_plugin/api/permissions.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
#!/usr/bin/env python
# Copyright 2024 NetBox Labs Inc
# Copyright 2025 NetBox Labs, Inc.
"""Diode NetBox Plugin - API Permissions."""

from rest_framework.permissions import SAFE_METHODS, BasePermission
from rest_framework.permissions import BasePermission

SCOPE_NETBOX_READ = "netbox:read"
SCOPE_NETBOX_WRITE = "netbox:write"

class IsDiodeReader(BasePermission):
"""Custom permission to allow users that has permission "netbox_diode_plugin.view_objectstate" to view the object type."""

class IsAuthenticated(BasePermission):
"""Check if the request is authenticated."""

def has_permission(self, request, view):
"""Check if the request is in SAFE_METHODS and user has netbox_diode_plugin.view_diode permission."""
return request.method in SAFE_METHODS and request.user.has_perm(
"netbox_diode_plugin.view_diode"
)
"""Check if the request is authenticated."""
return bool(getattr(request.user, "is_authenticated", False))


class IsDiodeWriter(BasePermission):
"""Custom permission to allow users that has permission "netbox_diode_plugin.add_diode" and POST requests."""
def require_scopes(*required_scopes):
"""Require one or more OAuth2 token scopes to access a view."""

def has_permission(self, request, view):
"""Check if the request is in POST and user has netbox_diode_plugin.add_diode permission."""
return request.method in ["POST"] and request.user.has_perm(
"netbox_diode_plugin.add_diode"
)
class ScopedPermission(BasePermission):
"""Check if the request has the required scopes."""

def has_permission(self, request, view):
"""Check if the request has the required scopes."""
scopes = getattr(request, "token_scopes", [])
return all(scope in scopes for scope in required_scopes)

ScopedPermission.__name__ = f"RequireScopes_{'_'.join(required_scopes)}"
return ScopedPermission
6 changes: 3 additions & 3 deletions netbox_diode_plugin/api/plugin_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
# Generated code. DO NOT EDIT.
# Timestamp: 2025-04-13 16:50:25Z

from dataclasses import dataclass
import datetime
import decimal
from functools import lru_cache
import logging
from dataclasses import dataclass
from functools import lru_cache
from typing import Type

import netaddr
from core.models import ObjectType as NetBoxType
from django.contrib.contenttypes.models import ContentType
from django.db import models
import netaddr
from rest_framework.exceptions import ValidationError

logger = logging.getLogger(__name__)
Expand Down
2 changes: 1 addition & 1 deletion netbox_diode_plugin/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# Copyright 2024 NetBox Labs Inc
# Copyright 2025 NetBox Labs, Inc.
"""Diode NetBox Plugin - Serializers."""

from netbox.api.serializers import NetBoxModelSerializer
Expand Down
2 changes: 1 addition & 1 deletion netbox_diode_plugin/api/transformer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# Copyright 2024 NetBox Labs Inc
# Copyright 2025 NetBox Labs, Inc.
"""Diode NetBox Plugin - API - Object resolution for diffing."""

import copy
Expand Down
2 changes: 1 addition & 1 deletion netbox_diode_plugin/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# Copyright 2024 NetBox Labs Inc
# Copyright 2025 NetBox Labs, Inc.
"""Diode NetBox Plugin - API URLs."""

from django.urls import include, path
Expand Down
Loading