1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #12906: Make boto3 & dulwich libraries optional (#13324)

* Initial work on #12906

* Catch import errors during backend init

* Tweak error message

* Update requirements & add note to docs
This commit is contained in:
Jeremy Stretch
2023-08-01 11:13:35 -04:00
committed by GitHub
parent 43e6308d90
commit c1ca8d5d8d
7 changed files with 91 additions and 49 deletions

View File

@ -2,10 +2,6 @@
# https://github.com/mozilla/bleach/blob/main/CHANGES # https://github.com/mozilla/bleach/blob/main/CHANGES
bleach bleach
# Python client for Amazon AWS API
# https://github.com/boto/boto3/blob/develop/CHANGELOG.rst
boto3
# The Python web framework on which NetBox is built # The Python web framework on which NetBox is built
# https://docs.djangoproject.com/en/stable/releases/ # https://docs.djangoproject.com/en/stable/releases/
Django<5.0 Django<5.0
@ -74,10 +70,6 @@ drf-spectacular
# https://github.com/tfranzel/drf-spectacular-sidecar # https://github.com/tfranzel/drf-spectacular-sidecar
drf-spectacular-sidecar drf-spectacular-sidecar
# Git client for file sync
# https://github.com/jelmer/dulwich/releases
dulwich
# RSS feed parser # RSS feed parser
# https://github.com/kurtmckee/feedparser/blob/develop/CHANGELOG.rst # https://github.com/kurtmckee/feedparser/blob/develop/CHANGELOG.rst
feedparser feedparser

View File

@ -12,6 +12,10 @@ To enable remote data synchronization, the NetBox administrator first designates
(Local disk paths are considered "remote" in this context as they exist outside NetBox's database. These paths could also be mapped to external network shares.) (Local disk paths are considered "remote" in this context as they exist outside NetBox's database. These paths could also be mapped to external network shares.)
!!! info
Data backends which connect to external sources typically require the installation of one or more supporting Python libraries. The Git backend requires the [`dulwich`](https://www.dulwich.io/) package, and the S3 backend requires the [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) package. These must be installed within NetBox's environment to enable these backends.
Each type of remote source has its own configuration parameters. For instance, a git source will ask the user to specify a branch and authentication credentials. Once the source has been created, a synchronization job is run to automatically replicate remote files in the local database. Each type of remote source has its own configuration parameters. For instance, a git source will ask the user to specify a branch and authentication credentials. Once the source has been created, a synchronization job is run to automatically replicate remote files in the local database.
The following NetBox models can be associated with replicated data files: The following NetBox models can be associated with replicated data files:

View File

@ -211,6 +211,22 @@ By default, NetBox will use the local filesystem to store uploaded files. To use
sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt" sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt"
``` ```
### Remote Data Sources
NetBox supports integration with several remote data sources via configurable backends. Each of these requires the installation of one or more additional libraries.
* Amazon S3: [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html)
* Git: [`dulwich`](https://www.dulwich.io/)
For example, to enable the Amazon S3 backend, add `boto3` to your local requirements file:
```no-highlight
sudo sh -c "echo 'boto3' >> /opt/netbox/local_requirements.txt"
```
!!! info
These packages were previously required in NetBox v3.5 but now are optional.
## Run the Upgrade Script ## Run the Upgrade Script
Once NetBox has been configured, we're ready to proceed with the actual installation. We'll run the packaged upgrade script (`upgrade.sh`) to perform the following actions: Once NetBox has been configured, we're ready to proceed with the actual installation. We'll run the packaged upgrade script (`upgrade.sh`) to perform the following actions:

View File

@ -6,13 +6,9 @@ from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
import boto3
from botocore.config import Config as Boto3Config
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from dulwich import porcelain
from dulwich.config import ConfigDict
from netbox.registry import registry from netbox.registry import registry
from .choices import DataSourceTypeChoices from .choices import DataSourceTypeChoices
@ -43,9 +39,20 @@ class DataBackend:
parameters = {} parameters = {}
sensitive_parameters = [] sensitive_parameters = []
# Prevent Django's template engine from calling the backend
# class when referenced via DataSource.backend_class
do_not_call_in_templates = True
def __init__(self, url, **kwargs): def __init__(self, url, **kwargs):
self.url = url self.url = url
self.params = kwargs self.params = kwargs
self.config = self.init_config()
def init_config(self):
"""
Hook to initialize the instance's configuration.
"""
return
@property @property
def url_scheme(self): def url_scheme(self):
@ -58,6 +65,7 @@ class DataBackend:
@register_backend(DataSourceTypeChoices.LOCAL) @register_backend(DataSourceTypeChoices.LOCAL)
class LocalBackend(DataBackend): class LocalBackend(DataBackend):
@contextmanager @contextmanager
def fetch(self): def fetch(self):
logger.debug(f"Data source type is local; skipping fetch") logger.debug(f"Data source type is local; skipping fetch")
@ -89,14 +97,28 @@ class GitBackend(DataBackend):
} }
sensitive_parameters = ['password'] sensitive_parameters = ['password']
def init_config(self):
from dulwich.config import ConfigDict
# Initialize backend config
config = ConfigDict()
# Apply HTTP proxy (if configured)
if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'):
if proxy := settings.HTTP_PROXIES.get(self.url_scheme):
config.set("http", "proxy", proxy)
return config
@contextmanager @contextmanager
def fetch(self): def fetch(self):
from dulwich import porcelain
local_path = tempfile.TemporaryDirectory() local_path = tempfile.TemporaryDirectory()
config = ConfigDict()
clone_args = { clone_args = {
"branch": self.params.get('branch'), "branch": self.params.get('branch'),
"config": config, "config": self.config,
"depth": 1, "depth": 1,
"errstream": porcelain.NoneStream(), "errstream": porcelain.NoneStream(),
"quiet": True, "quiet": True,
@ -110,10 +132,6 @@ class GitBackend(DataBackend):
} }
) )
if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'):
if proxy := settings.HTTP_PROXIES.get(self.url_scheme):
config.set("http", "proxy", proxy)
logger.debug(f"Cloning git repo: {self.url}") logger.debug(f"Cloning git repo: {self.url}")
try: try:
porcelain.clone(self.url, local_path.name, **clone_args) porcelain.clone(self.url, local_path.name, **clone_args)
@ -141,15 +159,20 @@ class S3Backend(DataBackend):
REGION_REGEX = r's3\.([a-z0-9-]+)\.amazonaws\.com' REGION_REGEX = r's3\.([a-z0-9-]+)\.amazonaws\.com'
@contextmanager def init_config(self):
def fetch(self): from botocore.config import Config as Boto3Config
local_path = tempfile.TemporaryDirectory()
# Build the S3 configuration # Initialize backend config
s3_config = Boto3Config( return Boto3Config(
proxies=settings.HTTP_PROXIES, proxies=settings.HTTP_PROXIES,
) )
@contextmanager
def fetch(self):
import boto3
local_path = tempfile.TemporaryDirectory()
# Initialize the S3 resource and bucket # Initialize the S3 resource and bucket
aws_access_key_id = self.params.get('aws_access_key_id') aws_access_key_id = self.params.get('aws_access_key_id')
aws_secret_access_key = self.params.get('aws_secret_access_key') aws_secret_access_key = self.params.get('aws_secret_access_key')
@ -158,7 +181,7 @@ class S3Backend(DataBackend):
region_name=self._region_name, region_name=self._region_name,
aws_access_key_id=aws_access_key_id, aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key, aws_secret_access_key=aws_secret_access_key,
config=s3_config config=self.config
) )
bucket = s3.Bucket(self._bucket_name) bucket = s3.Bucket(self._bucket_name)

View File

@ -104,6 +104,10 @@ class DataSource(JobsMixin, PrimaryModel):
def url_scheme(self): def url_scheme(self):
return urlparse(self.source_url).scheme.lower() return urlparse(self.source_url).scheme.lower()
@property
def backend_class(self):
return registry['data_backends'].get(self.type)
@property @property
def is_local(self): def is_local(self):
return self.type == DataSourceTypeChoices.LOCAL return self.type == DataSourceTypeChoices.LOCAL
@ -139,17 +143,15 @@ class DataSource(JobsMixin, PrimaryModel):
) )
def get_backend(self): def get_backend(self):
backend_cls = registry['data_backends'].get(self.type)
backend_params = self.parameters or {} backend_params = self.parameters or {}
return self.backend_class(self.source_url, **backend_params)
return backend_cls(self.source_url, **backend_params)
def sync(self): def sync(self):
""" """
Create/update/delete child DataFiles as necessary to synchronize with the remote source. Create/update/delete child DataFiles as necessary to synchronize with the remote source.
""" """
if self.status == DataSourceStatusChoices.SYNCING: if self.status == DataSourceStatusChoices.SYNCING:
raise SyncError(f"Cannot initiate sync; syncing already in progress.") raise SyncError("Cannot initiate sync; syncing already in progress.")
# Emit the pre_sync signal # Emit the pre_sync signal
pre_sync.send(sender=self.__class__, instance=self) pre_sync.send(sender=self.__class__, instance=self)
@ -158,7 +160,12 @@ class DataSource(JobsMixin, PrimaryModel):
DataSource.objects.filter(pk=self.pk).update(status=self.status) DataSource.objects.filter(pk=self.pk).update(status=self.status)
# Replicate source data locally # Replicate source data locally
backend = self.get_backend() try:
backend = self.get_backend()
except ModuleNotFoundError as e:
raise SyncError(
f"There was an error initializing the backend. A dependency needs to be installed: {e}"
)
with backend.fetch() as local_path: with backend.fetch() as local_path:
logger.debug(f'Syncing files from source root {local_path}') logger.debug(f'Syncing files from source root {local_path}')

View File

@ -85,24 +85,26 @@
<div class="card"> <div class="card">
<h5 class="card-header">{% trans "Backend" %}</h5> <h5 class="card-header">{% trans "Backend" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> {% with backend=object.backend_class %}
{% for name, field in object.get_backend.parameters.items %} <table class="table table-hover attr-table">
<tr> {% for name, field in backend.parameters.items %}
<th scope="row">{{ field.label }}</th> <tr>
{% if name in object.get_backend.sensitive_parameters and not perms.core.change_datasource %} <th scope="row">{{ field.label }}</th>
<td>********</td> {% if name in backend.sensitive_parameters and not perms.core.change_datasource %}
{% else %} <td>********</td>
<td>{{ object.parameters|get_key:name|placeholder }}</td> {% else %}
{% endif %} <td>{{ object.parameters|get_key:name|placeholder }}</td>
</tr> {% endif %}
{% empty %} </tr>
<tr> {% empty %}
<td colspan="2" class="text-muted"> <tr>
{% trans "No parameters defined" %} <td colspan="2" class="text-muted">
</td> {% trans "No parameters defined" %}
</tr> </td>
{% endfor %} </tr>
</table> {% endfor %}
</table>
{% endwith %}
</div> </div>
</div> </div>
{% include 'inc/panels/related_objects.html' %} {% include 'inc/panels/related_objects.html' %}

View File

@ -1,5 +1,4 @@
bleach==6.0.0 bleach==6.0.0
boto3==1.28.14
django-cors-headers==4.2.0 django-cors-headers==4.2.0
django-debug-toolbar==4.1.0 django-debug-toolbar==4.1.0
django-filter==23.2 django-filter==23.2
@ -16,7 +15,6 @@ django-timezone-field==5.1
djangorestframework==3.14.0 djangorestframework==3.14.0
drf-spectacular==0.26.4 drf-spectacular==0.26.4
drf-spectacular-sidecar==2023.7.1 drf-spectacular-sidecar==2023.7.1
dulwich==0.21.5
feedparser==6.0.10 feedparser==6.0.10
graphene-django==3.0.0 graphene-django==3.0.0
gunicorn==21.2.0 gunicorn==21.2.0