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
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
# https://docs.djangoproject.com/en/stable/releases/
Django<5.0
@ -74,10 +70,6 @@ drf-spectacular
# https://github.com/tfranzel/drf-spectacular-sidecar
drf-spectacular-sidecar
# Git client for file sync
# https://github.com/jelmer/dulwich/releases
dulwich
# RSS feed parser
# https://github.com/kurtmckee/feedparser/blob/develop/CHANGELOG.rst
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.)
!!! 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.
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"
```
### 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
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 urllib.parse import urlparse
import boto3
from botocore.config import Config as Boto3Config
from django import forms
from django.conf import settings
from django.utils.translation import gettext as _
from dulwich import porcelain
from dulwich.config import ConfigDict
from netbox.registry import registry
from .choices import DataSourceTypeChoices
@ -43,9 +39,20 @@ class DataBackend:
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):
self.url = url
self.params = kwargs
self.config = self.init_config()
def init_config(self):
"""
Hook to initialize the instance's configuration.
"""
return
@property
def url_scheme(self):
@ -58,6 +65,7 @@ class DataBackend:
@register_backend(DataSourceTypeChoices.LOCAL)
class LocalBackend(DataBackend):
@contextmanager
def fetch(self):
logger.debug(f"Data source type is local; skipping fetch")
@ -89,14 +97,28 @@ class GitBackend(DataBackend):
}
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
def fetch(self):
from dulwich import porcelain
local_path = tempfile.TemporaryDirectory()
config = ConfigDict()
clone_args = {
"branch": self.params.get('branch'),
"config": config,
"config": self.config,
"depth": 1,
"errstream": porcelain.NoneStream(),
"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}")
try:
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'
@contextmanager
def fetch(self):
local_path = tempfile.TemporaryDirectory()
def init_config(self):
from botocore.config import Config as Boto3Config
# Build the S3 configuration
s3_config = Boto3Config(
# Initialize backend config
return Boto3Config(
proxies=settings.HTTP_PROXIES,
)
@contextmanager
def fetch(self):
import boto3
local_path = tempfile.TemporaryDirectory()
# Initialize the S3 resource and bucket
aws_access_key_id = self.params.get('aws_access_key_id')
aws_secret_access_key = self.params.get('aws_secret_access_key')
@ -158,7 +181,7 @@ class S3Backend(DataBackend):
region_name=self._region_name,
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
config=s3_config
config=self.config
)
bucket = s3.Bucket(self._bucket_name)

View File

@ -104,6 +104,10 @@ class DataSource(JobsMixin, PrimaryModel):
def url_scheme(self):
return urlparse(self.source_url).scheme.lower()
@property
def backend_class(self):
return registry['data_backends'].get(self.type)
@property
def is_local(self):
return self.type == DataSourceTypeChoices.LOCAL
@ -139,17 +143,15 @@ class DataSource(JobsMixin, PrimaryModel):
)
def get_backend(self):
backend_cls = registry['data_backends'].get(self.type)
backend_params = self.parameters or {}
return backend_cls(self.source_url, **backend_params)
return self.backend_class(self.source_url, **backend_params)
def sync(self):
"""
Create/update/delete child DataFiles as necessary to synchronize with the remote source.
"""
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
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)
# 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:
logger.debug(f'Syncing files from source root {local_path}')

View File

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

View File

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