mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-28 19:36:26 -06:00
Merge branch 'feature' into 17170-contact-groups
This commit is contained in:
commit
51f9f4ca86
@ -1,6 +1,6 @@
|
|||||||
# 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.2
|
Django==5.2.*
|
||||||
|
|
||||||
# Django middleware which permits cross-domain API requests
|
# Django middleware which permits cross-domain API requests
|
||||||
# https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst
|
# https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst
|
||||||
@ -42,6 +42,10 @@ django-rich
|
|||||||
# https://github.com/rq/django-rq/blob/master/CHANGELOG.md
|
# https://github.com/rq/django-rq/blob/master/CHANGELOG.md
|
||||||
django-rq
|
django-rq
|
||||||
|
|
||||||
|
# Provides a variety of storage backends
|
||||||
|
# https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst
|
||||||
|
django-storages
|
||||||
|
|
||||||
# Abstraction models for rendering and paginating HTML tables
|
# Abstraction models for rendering and paginating HTML tables
|
||||||
# https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md
|
# https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md
|
||||||
django-tables2
|
django-tables2
|
||||||
|
@ -54,7 +54,7 @@ pg_dump --username netbox --password --host localhost -s netbox > netbox_schema.
|
|||||||
By default, NetBox stores uploaded files (such as image attachments) in its media directory. To fully replicate an instance of NetBox, you'll need to copy both the database and the media files.
|
By default, NetBox stores uploaded files (such as image attachments) in its media directory. To fully replicate an instance of NetBox, you'll need to copy both the database and the media files.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
These operations are not necessary if your installation is utilizing a [remote storage backend](../configuration/system.md#storage_backend).
|
These operations are not necessary if your installation is utilizing a [remote storage backend](../configuration/system.md#storages).
|
||||||
|
|
||||||
### Archive the Media Directory
|
### Archive the Media Directory
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ ALLOWED_HOSTS = ['*']
|
|||||||
|
|
||||||
## DATABASE
|
## DATABASE
|
||||||
|
|
||||||
NetBox requires access to a PostgreSQL 13 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
|
NetBox requires access to a PostgreSQL 14 or later database service to store data. This service can run locally on the NetBox server or on a remote system. The following parameters must be defined within the `DATABASE` dictionary:
|
||||||
|
|
||||||
* `NAME` - Database name
|
* `NAME` - Database name
|
||||||
* `USER` - PostgreSQL username
|
* `USER` - PostgreSQL username
|
||||||
|
@ -196,23 +196,46 @@ The dotted path to the desired search backend class. `CachedValueSearchBackend`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## STORAGE_BACKEND
|
## STORAGES
|
||||||
|
|
||||||
Default: None (local storage)
|
The backend storage engine for handling uploaded files such as [image attachments](../models/extras/imageattachment.md) and [custom scripts](../customization/custom-scripts.md). NetBox integrates with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) and [`django-storage-swift`](https://github.com/dennisv/django-storage-swift) libraries, which provide backends for several popular file storage services. If not configured, local filesystem storage will be used.
|
||||||
|
|
||||||
The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) and [`django-storage-swift`](https://github.com/dennisv/django-storage-swift) packages, which provide backends for several popular file storage services. If not configured, local filesystem storage will be used.
|
By default, the following configuration is used:
|
||||||
|
|
||||||
The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting.
|
```python
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"BACKEND": "extras.storage.ScriptFileSystemStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
Within the `STORAGES` dictionary, `"default"` is used for image uploads, "staticfiles" is for static files and `"scripts"` is used for custom scripts.
|
||||||
|
|
||||||
## STORAGE_CONFIG
|
If using a remote storage like S3, define the config as `STORAGES[key]["OPTIONS"]` for each storage item as needed. For example:
|
||||||
|
|
||||||
Default: Empty
|
```python
|
||||||
|
STORAGES = {
|
||||||
|
"scripts": {
|
||||||
|
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
|
||||||
|
"OPTIONS": {
|
||||||
|
'access_key': 'access key',
|
||||||
|
'secret_key': 'secret key',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the documentation for your selected backend ([`django-storages`](https://django-storages.readthedocs.io/en/stable/) or [`django-storage-swift`](https://github.com/dennisv/django-storage-swift)) for more detail.
|
The specific configuration settings for each storage backend can be found in the [django-storages documentation](https://django-storages.readthedocs.io/en/latest/index.html).
|
||||||
|
|
||||||
If `STORAGE_BACKEND` is not defined, this setting will be ignored.
|
!!! note
|
||||||
|
Any keys defined in the `STORAGES` configuration parameter replace those in the default configuration. It is only necessary to define keys within the `STORAGES` for the specific backend(s) you wish to configure.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -140,6 +140,8 @@ The Script class provides two convenience methods for reading data from files:
|
|||||||
|
|
||||||
These two methods will load data in YAML or JSON format, respectively, from files within the local path (i.e. `SCRIPTS_ROOT`).
|
These two methods will load data in YAML or JSON format, respectively, from files within the local path (i.e. `SCRIPTS_ROOT`).
|
||||||
|
|
||||||
|
**Note:** These convenience methods are deprecated and will be removed in NetBox v4.4. These only work if running scripts within the local path, they will not work if using a storage other than ScriptFileSystemStorage.
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
The Script object provides a set of convenient functions for recording messages at different severity levels:
|
The Script object provides a set of convenient functions for recording messages at different severity levels:
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md).
|
This section entails the installation and configuration of a local PostgreSQL database. If you already have a PostgreSQL database service in place, skip to [the next section](2-redis.md).
|
||||||
|
|
||||||
!!! warning "PostgreSQL 13 or later required"
|
!!! warning "PostgreSQL 14 or later required"
|
||||||
NetBox requires PostgreSQL 13 or later. Please note that MySQL and other relational databases are **not** supported.
|
NetBox requires PostgreSQL 14 or later. Please note that MySQL and other relational databases are **not** supported.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ This section entails the installation and configuration of a local PostgreSQL da
|
|||||||
sudo systemctl enable --now postgresql
|
sudo systemctl enable --now postgresql
|
||||||
```
|
```
|
||||||
|
|
||||||
Before continuing, verify that you have installed PostgreSQL 13 or later:
|
Before continuing, verify that you have installed PostgreSQL 14 or later:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
psql -V
|
psql -V
|
||||||
|
@ -207,7 +207,7 @@ All Python packages required by NetBox are listed in `requirements.txt` and will
|
|||||||
|
|
||||||
### Remote File Storage
|
### Remote File Storage
|
||||||
|
|
||||||
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storage_backend) in `configuration.py`.
|
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storages) in `configuration.py`.
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt"
|
sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt"
|
||||||
|
@ -21,7 +21,7 @@ The following sections detail how to set up a new instance of NetBox:
|
|||||||
| Dependency | Supported Versions |
|
| Dependency | Supported Versions |
|
||||||
|------------|--------------------|
|
|------------|--------------------|
|
||||||
| Python | 3.10, 3.11, 3.12 |
|
| Python | 3.10, 3.11, 3.12 |
|
||||||
| PostgreSQL | 13+ |
|
| PostgreSQL | 14+ |
|
||||||
| Redis | 4.0+ |
|
| Redis | 4.0+ |
|
||||||
|
|
||||||
Below is a simplified overview of the NetBox application stack for reference:
|
Below is a simplified overview of the NetBox application stack for reference:
|
||||||
|
@ -20,7 +20,7 @@ NetBox requires the following dependencies:
|
|||||||
| Dependency | Supported Versions |
|
| Dependency | Supported Versions |
|
||||||
|------------|--------------------|
|
|------------|--------------------|
|
||||||
| Python | 3.10, 3.11, 3.12 |
|
| Python | 3.10, 3.11, 3.12 |
|
||||||
| PostgreSQL | 13+ |
|
| PostgreSQL | 14+ |
|
||||||
| Redis | 4.0+ |
|
| Redis | 4.0+ |
|
||||||
|
|
||||||
## 3. Install the Latest Release
|
## 3. Install the Latest Release
|
||||||
|
@ -79,5 +79,5 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
|
|||||||
| HTTP service | nginx or Apache |
|
| HTTP service | nginx or Apache |
|
||||||
| WSGI service | gunicorn or uWSGI |
|
| WSGI service | gunicorn or uWSGI |
|
||||||
| Application | Django/Python |
|
| Application | Django/Python |
|
||||||
| Database | PostgreSQL 13+ |
|
| Database | PostgreSQL 14+ |
|
||||||
| Task queuing | Redis/django-rq |
|
| Task queuing | Redis/django-rq |
|
||||||
|
@ -28,12 +28,7 @@ plugins:
|
|||||||
- mkdocstrings:
|
- mkdocstrings:
|
||||||
handlers:
|
handlers:
|
||||||
python:
|
python:
|
||||||
setup_commands:
|
paths: ["netbox"]
|
||||||
- import os
|
|
||||||
- import django
|
|
||||||
- os.chdir('netbox/')
|
|
||||||
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
|
|
||||||
- django.setup()
|
|
||||||
options:
|
options:
|
||||||
heading_level: 3
|
heading_level: 3
|
||||||
members_order: source
|
members_order: source
|
||||||
|
@ -357,17 +357,6 @@ class DataFile(models.Model):
|
|||||||
|
|
||||||
return is_modified
|
return is_modified
|
||||||
|
|
||||||
def write_to_disk(self, path, overwrite=False):
|
|
||||||
"""
|
|
||||||
Write the object's data to disk at the specified path
|
|
||||||
"""
|
|
||||||
# Check whether file already exists
|
|
||||||
if os.path.isfile(path) and not overwrite:
|
|
||||||
raise FileExistsError()
|
|
||||||
|
|
||||||
with open(path, 'wb+') as new_file:
|
|
||||||
new_file.write(self.data)
|
|
||||||
|
|
||||||
|
|
||||||
class AutoSyncRecord(models.Model):
|
class AutoSyncRecord(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.core.files.storage import storages
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from ..choices import ManagedFileRootPathChoices
|
from ..choices import ManagedFileRootPathChoices
|
||||||
|
from extras.storage import ScriptFileSystemStorage
|
||||||
from netbox.models.features import SyncedDataMixin
|
from netbox.models.features import SyncedDataMixin
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
|
||||||
@ -76,15 +79,35 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
|||||||
return os.path.join(self._resolve_root_path(), self.file_path)
|
return os.path.join(self._resolve_root_path(), self.file_path)
|
||||||
|
|
||||||
def _resolve_root_path(self):
|
def _resolve_root_path(self):
|
||||||
return {
|
storage = self.storage
|
||||||
'scripts': settings.SCRIPTS_ROOT,
|
if isinstance(storage, ScriptFileSystemStorage):
|
||||||
'reports': settings.REPORTS_ROOT,
|
return {
|
||||||
}[self.file_root]
|
'scripts': settings.SCRIPTS_ROOT,
|
||||||
|
'reports': settings.REPORTS_ROOT,
|
||||||
|
}[self.file_root]
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
def sync_data(self):
|
def sync_data(self):
|
||||||
if self.data_file:
|
if self.data_file:
|
||||||
self.file_path = os.path.basename(self.data_path)
|
self.file_path = os.path.basename(self.data_path)
|
||||||
self.data_file.write_to_disk(self.full_path, overwrite=True)
|
self._write_to_disk(self.full_path, overwrite=True)
|
||||||
|
|
||||||
|
def _write_to_disk(self, path, overwrite=False):
|
||||||
|
"""
|
||||||
|
Write the object's data to disk at the specified path
|
||||||
|
"""
|
||||||
|
# Check whether file already exists
|
||||||
|
storage = self.storage
|
||||||
|
if storage.exists(path) and not overwrite:
|
||||||
|
raise FileExistsError()
|
||||||
|
|
||||||
|
with storage.open(path, 'wb+') as new_file:
|
||||||
|
new_file.write(self.data)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def storage(self):
|
||||||
|
return storages.create_storage(storages.backends["scripts"])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
@ -104,8 +127,9 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
|||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
# Delete file from disk
|
# Delete file from disk
|
||||||
|
storage = self.storage
|
||||||
try:
|
try:
|
||||||
os.remove(self.full_path)
|
storage.delete(self.full_path)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import storages
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from core.forms import ManagedFileForm
|
||||||
from extras.choices import DurationChoices
|
from extras.choices import DurationChoices
|
||||||
|
from extras.storage import ScriptFileSystemStorage
|
||||||
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
|
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
|
||||||
from utilities.datetime import local_now
|
from utilities.datetime import local_now
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'ScriptFileForm',
|
||||||
'ScriptForm',
|
'ScriptForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -55,3 +62,26 @@ class ScriptForm(forms.Form):
|
|||||||
self.cleaned_data['_schedule_at'] = local_now()
|
self.cleaned_data['_schedule_at'] = local_now()
|
||||||
|
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptFileForm(ManagedFileForm):
|
||||||
|
"""
|
||||||
|
ManagedFileForm with a custom save method to use django-storages.
|
||||||
|
"""
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# If a file was uploaded, save it to disk
|
||||||
|
if self.cleaned_data['upload_file']:
|
||||||
|
storage = storages.create_storage(storages.backends["scripts"])
|
||||||
|
|
||||||
|
filename = self.cleaned_data['upload_file'].name
|
||||||
|
if isinstance(storage, ScriptFileSystemStorage):
|
||||||
|
full_path = os.path.join(settings.SCRIPTS_ROOT, filename)
|
||||||
|
else:
|
||||||
|
full_path = filename
|
||||||
|
|
||||||
|
self.instance.file_path = full_path
|
||||||
|
data = self.cleaned_data['upload_file']
|
||||||
|
storage.save(filename, data)
|
||||||
|
|
||||||
|
# need to skip ManagedFileForm save method
|
||||||
|
return super(ManagedFileForm, self).save(*args, **kwargs)
|
||||||
|
@ -1,11 +1,31 @@
|
|||||||
|
import importlib.abc
|
||||||
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
from importlib.machinery import SourceFileLoader
|
import sys
|
||||||
|
from django.core.files.storage import storages
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'PythonModuleMixin',
|
'PythonModuleMixin',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomStoragesLoader(importlib.abc.Loader):
|
||||||
|
"""
|
||||||
|
Custom loader for exec_module to use django-storages instead of the file system.
|
||||||
|
"""
|
||||||
|
def __init__(self, filename):
|
||||||
|
self.filename = filename
|
||||||
|
|
||||||
|
def create_module(self, spec):
|
||||||
|
return None # Use default module creation
|
||||||
|
|
||||||
|
def exec_module(self, module):
|
||||||
|
storage = storages.create_storage(storages.backends["scripts"])
|
||||||
|
with storage.open(self.filename, 'rb') as f:
|
||||||
|
code = f.read()
|
||||||
|
exec(code, module.__dict__)
|
||||||
|
|
||||||
|
|
||||||
class PythonModuleMixin:
|
class PythonModuleMixin:
|
||||||
|
|
||||||
def get_jobs(self, name):
|
def get_jobs(self, name):
|
||||||
@ -33,6 +53,16 @@ class PythonModuleMixin:
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
def get_module(self):
|
def get_module(self):
|
||||||
loader = SourceFileLoader(self.python_name, self.full_path)
|
"""
|
||||||
module = loader.load_module()
|
Load the module using importlib, but use a custom loader to use django-storages
|
||||||
|
instead of the file system.
|
||||||
|
"""
|
||||||
|
spec = importlib.util.spec_from_file_location(self.python_name, self.name)
|
||||||
|
if spec is None:
|
||||||
|
raise ModuleNotFoundError(f"Could not find module: {self.python_name}")
|
||||||
|
loader = CustomStoragesLoader(self.name)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[self.python_name] = module
|
||||||
|
loader.exec_module(module)
|
||||||
|
|
||||||
return module
|
return module
|
||||||
|
@ -2,10 +2,12 @@ import inspect
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import storages
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import classproperty
|
from django.utils.functional import classproperty
|
||||||
@ -367,9 +369,46 @@ class BaseScript:
|
|||||||
def filename(self):
|
def filename(self):
|
||||||
return inspect.getfile(self.__class__)
|
return inspect.getfile(self.__class__)
|
||||||
|
|
||||||
|
def findsource(self, object):
|
||||||
|
storage = storages.create_storage(storages.backends["scripts"])
|
||||||
|
with storage.open(os.path.basename(self.filename), 'r') as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
# Break the source code into lines
|
||||||
|
lines = [line + '\n' for line in data.splitlines()]
|
||||||
|
|
||||||
|
# Find the class definition
|
||||||
|
name = object.__name__
|
||||||
|
pat = re.compile(r'^(\s*)class\s*' + name + r'\b')
|
||||||
|
# use the class definition with the least indentation
|
||||||
|
candidates = []
|
||||||
|
for i in range(len(lines)):
|
||||||
|
match = pat.match(lines[i])
|
||||||
|
if match:
|
||||||
|
if lines[i][0] == 'c':
|
||||||
|
return lines, i
|
||||||
|
|
||||||
|
candidates.append((match.group(1), i))
|
||||||
|
if not candidates:
|
||||||
|
raise OSError('could not find class definition')
|
||||||
|
|
||||||
|
# Sort the candidates by whitespace, and by line number
|
||||||
|
candidates.sort()
|
||||||
|
return lines, candidates[0][1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self):
|
def source(self):
|
||||||
return inspect.getsource(self.__class__)
|
# Can't use inspect.getsource() as it uses os to get the file
|
||||||
|
# inspect uses ast, but that is overkill for this as we only do
|
||||||
|
# classes.
|
||||||
|
object = self.__class__
|
||||||
|
|
||||||
|
try:
|
||||||
|
lines, lnum = self.findsource(object)
|
||||||
|
lines = inspect.getblock(lines[lnum:])
|
||||||
|
return ''.join(lines)
|
||||||
|
except OSError:
|
||||||
|
return ''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_vars(cls):
|
def _get_vars(cls):
|
||||||
@ -524,7 +563,12 @@ class BaseScript:
|
|||||||
def load_yaml(self, filename):
|
def load_yaml(self, filename):
|
||||||
"""
|
"""
|
||||||
Return data from a YAML file
|
Return data from a YAML file
|
||||||
|
TODO: DEPRECATED: Remove this method in v4.4
|
||||||
"""
|
"""
|
||||||
|
self._log(
|
||||||
|
_("load_yaml is deprecated and will be removed in v4.4"),
|
||||||
|
level=LogLevelChoices.LOG_WARNING
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
from yaml import CLoader as Loader
|
from yaml import CLoader as Loader
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -539,7 +583,12 @@ class BaseScript:
|
|||||||
def load_json(self, filename):
|
def load_json(self, filename):
|
||||||
"""
|
"""
|
||||||
Return data from a JSON file
|
Return data from a JSON file
|
||||||
|
TODO: DEPRECATED: Remove this method in v4.4
|
||||||
"""
|
"""
|
||||||
|
self._log(
|
||||||
|
_("load_json is deprecated and will be removed in v4.4"),
|
||||||
|
level=LogLevelChoices.LOG_WARNING
|
||||||
|
)
|
||||||
file_path = os.path.join(settings.SCRIPTS_ROOT, filename)
|
file_path = os.path.join(settings.SCRIPTS_ROOT, filename)
|
||||||
with open(file_path, 'r') as datafile:
|
with open(file_path, 'r') as datafile:
|
||||||
data = json.load(datafile)
|
data = json.load(datafile)
|
||||||
@ -555,7 +604,6 @@ class BaseScript:
|
|||||||
Run the report and save its results. Each test method will be executed in order.
|
Run the report and save its results. Each test method will be executed in order.
|
||||||
"""
|
"""
|
||||||
self.logger.info("Running report")
|
self.logger.info("Running report")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for test_name in self.tests:
|
for test_name in self.tests:
|
||||||
self._current_test = test_name
|
self._current_test = test_name
|
||||||
|
14
netbox/extras/storage.py
Normal file
14
netbox/extras/storage.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptFileSystemStorage(FileSystemStorage):
|
||||||
|
"""
|
||||||
|
Custom storage for scripts - for django-storages as the default one will
|
||||||
|
go off media-root and raise security errors as the scripts can be outside
|
||||||
|
the media-root directory.
|
||||||
|
"""
|
||||||
|
@cached_property
|
||||||
|
def base_location(self):
|
||||||
|
return settings.SCRIPTS_ROOT
|
@ -12,7 +12,6 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from core.choices import ManagedFileRootPathChoices
|
from core.choices import ManagedFileRootPathChoices
|
||||||
from core.forms import ManagedFileForm
|
|
||||||
from core.models import Job
|
from core.models import Job
|
||||||
from core.tables import JobTable
|
from core.tables import JobTable
|
||||||
from dcim.models import Device, DeviceRole, Platform
|
from dcim.models import Device, DeviceRole, Platform
|
||||||
@ -1163,7 +1162,7 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View):
|
|||||||
@register_model_view(ScriptModule, 'edit')
|
@register_model_view(ScriptModule, 'edit')
|
||||||
class ScriptModuleCreateView(generic.ObjectEditView):
|
class ScriptModuleCreateView(generic.ObjectEditView):
|
||||||
queryset = ScriptModule.objects.all()
|
queryset = ScriptModule.objects.all()
|
||||||
form = ManagedFileForm
|
form = forms.ScriptFileForm
|
||||||
|
|
||||||
def alter_object(self, obj, *args, **kwargs):
|
def alter_object(self, obj, *args, **kwargs):
|
||||||
obj.file_root = ManagedFileRootPathChoices.SCRIPTS
|
obj.file_root = ManagedFileRootPathChoices.SCRIPTS
|
||||||
|
@ -98,18 +98,23 @@ class RemoteUserMiddleware(RemoteUserMiddleware_):
|
|||||||
"""
|
"""
|
||||||
Custom implementation of Django's RemoteUserMiddleware which allows for a user-configurable HTTP header name.
|
Custom implementation of Django's RemoteUserMiddleware which allows for a user-configurable HTTP header name.
|
||||||
"""
|
"""
|
||||||
|
async_capable = False
|
||||||
force_logout_if_no_header = False
|
force_logout_if_no_header = False
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
if get_response is None:
|
||||||
|
raise ValueError("get_response must be provided.")
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def header(self):
|
def header(self):
|
||||||
return settings.REMOTE_AUTH_HEADER
|
return settings.REMOTE_AUTH_HEADER
|
||||||
|
|
||||||
def process_request(self, request):
|
def __call__(self, request):
|
||||||
logger = logging.getLogger(
|
logger = logging.getLogger('netbox.authentication.RemoteUserMiddleware')
|
||||||
'netbox.authentication.RemoteUserMiddleware')
|
|
||||||
# Bypass middleware if remote authentication is not enabled
|
# Bypass middleware if remote authentication is not enabled
|
||||||
if not settings.REMOTE_AUTH_ENABLED:
|
if not settings.REMOTE_AUTH_ENABLED:
|
||||||
return
|
return self.get_response(request)
|
||||||
# AuthenticationMiddleware is required so that request.user exists.
|
# AuthenticationMiddleware is required so that request.user exists.
|
||||||
if not hasattr(request, 'user'):
|
if not hasattr(request, 'user'):
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
@ -126,13 +131,13 @@ class RemoteUserMiddleware(RemoteUserMiddleware_):
|
|||||||
# AnonymousUser by the AuthenticationMiddleware).
|
# AnonymousUser by the AuthenticationMiddleware).
|
||||||
if self.force_logout_if_no_header and request.user.is_authenticated:
|
if self.force_logout_if_no_header and request.user.is_authenticated:
|
||||||
self._remove_invalid_user(request)
|
self._remove_invalid_user(request)
|
||||||
return
|
return self.get_response(request)
|
||||||
# If the user is already authenticated and that user is the user we are
|
# If the user is already authenticated and that user is the user we are
|
||||||
# getting passed in the headers, then the correct user is already
|
# getting passed in the headers, then the correct user is already
|
||||||
# persisted in the session and we don't need to continue.
|
# persisted in the session and we don't need to continue.
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
if request.user.get_username() == self.clean_username(username, request):
|
if request.user.get_username() == self.clean_username(username, request):
|
||||||
return
|
return self.get_response(request)
|
||||||
else:
|
else:
|
||||||
# An authenticated user is associated with the request, but
|
# An authenticated user is associated with the request, but
|
||||||
# it does not match the authorized user in the header.
|
# it does not match the authorized user in the header.
|
||||||
@ -162,6 +167,8 @@ class RemoteUserMiddleware(RemoteUserMiddleware_):
|
|||||||
request.user = user
|
request.user = user
|
||||||
auth.login(request, user)
|
auth.login(request, user)
|
||||||
|
|
||||||
|
return self.get_response(request)
|
||||||
|
|
||||||
def _get_groups(self, request):
|
def _get_groups(self, request):
|
||||||
logger = logging.getLogger(
|
logger = logging.getLogger(
|
||||||
'netbox.authentication.RemoteUserMiddleware')
|
'netbox.authentication.RemoteUserMiddleware')
|
||||||
|
@ -17,6 +17,7 @@ from netbox.config import PARAMS as CONFIG_PARAMS
|
|||||||
from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
|
import storages.utils # type: ignore
|
||||||
from utilities.release import load_release_data
|
from utilities.release import load_release_data
|
||||||
from utilities.string import trailing_slash
|
from utilities.string import trailing_slash
|
||||||
|
|
||||||
@ -177,7 +178,8 @@ SESSION_COOKIE_PATH = CSRF_COOKIE_PATH
|
|||||||
SESSION_COOKIE_SECURE = getattr(configuration, 'SESSION_COOKIE_SECURE', False)
|
SESSION_COOKIE_SECURE = getattr(configuration, 'SESSION_COOKIE_SECURE', False)
|
||||||
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
||||||
STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
|
STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
|
||||||
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
|
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', None)
|
||||||
|
STORAGES = getattr(configuration, 'STORAGES', {})
|
||||||
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
||||||
TRANSLATION_ENABLED = getattr(configuration, 'TRANSLATION_ENABLED', True)
|
TRANSLATION_ENABLED = getattr(configuration, 'TRANSLATION_ENABLED', True)
|
||||||
|
|
||||||
@ -234,61 +236,64 @@ DATABASES = {
|
|||||||
# Storage backend
|
# Storage backend
|
||||||
#
|
#
|
||||||
|
|
||||||
|
if STORAGE_BACKEND is not None:
|
||||||
|
if not STORAGES:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"STORAGE_BACKEND and STORAGES are both set, remove the deprecated STORAGE_BACKEND setting."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
warnings.warn(
|
||||||
|
"STORAGE_BACKEND is deprecated, use the new STORAGES setting instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
if STORAGE_CONFIG is not None:
|
||||||
|
warnings.warn(
|
||||||
|
"STORAGE_CONFIG is deprecated, use the new STORAGES setting instead."
|
||||||
|
)
|
||||||
|
|
||||||
# Default STORAGES for Django
|
# Default STORAGES for Django
|
||||||
STORAGES = {
|
DEFAULT_STORAGES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
},
|
},
|
||||||
"staticfiles": {
|
"staticfiles": {
|
||||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"BACKEND": "extras.storage.ScriptFileSystemStorage",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
STORAGES = DEFAULT_STORAGES | STORAGES
|
||||||
|
|
||||||
|
# TODO: This code is deprecated and needs to be removed in the future
|
||||||
if STORAGE_BACKEND is not None:
|
if STORAGE_BACKEND is not None:
|
||||||
STORAGES['default']['BACKEND'] = STORAGE_BACKEND
|
STORAGES['default']['BACKEND'] = STORAGE_BACKEND
|
||||||
|
|
||||||
# django-storages
|
# Monkey-patch django-storages to fetch settings from STORAGE_CONFIG
|
||||||
if STORAGE_BACKEND.startswith('storages.'):
|
if STORAGE_CONFIG is not None:
|
||||||
try:
|
def _setting(name, default=None):
|
||||||
import storages.utils # type: ignore
|
if name in STORAGE_CONFIG:
|
||||||
except ModuleNotFoundError as e:
|
return STORAGE_CONFIG[name]
|
||||||
if getattr(e, 'name') == 'storages':
|
return globals().get(name, default)
|
||||||
raise ImproperlyConfigured(
|
storages.utils.setting = _setting
|
||||||
f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storages is not present. It can be "
|
|
||||||
f"installed by running 'pip install django-storages'."
|
|
||||||
)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# Monkey-patch django-storages to fetch settings from STORAGE_CONFIG
|
# django-storage-swift
|
||||||
def _setting(name, default=None):
|
if STORAGE_BACKEND == 'swift.storage.SwiftStorage':
|
||||||
if name in STORAGE_CONFIG:
|
try:
|
||||||
return STORAGE_CONFIG[name]
|
import swift.utils # noqa: F401
|
||||||
return globals().get(name, default)
|
except ModuleNotFoundError as e:
|
||||||
storages.utils.setting = _setting
|
if getattr(e, 'name') == 'swift':
|
||||||
|
raise ImproperlyConfigured(
|
||||||
# django-storage-swift
|
f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storage-swift is not present. "
|
||||||
elif STORAGE_BACKEND == 'swift.storage.SwiftStorage':
|
"It can be installed by running 'pip install django-storage-swift'."
|
||||||
try:
|
)
|
||||||
import swift.utils # noqa: F401
|
raise e
|
||||||
except ModuleNotFoundError as e:
|
|
||||||
if getattr(e, 'name') == 'swift':
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storage-swift is not present. "
|
|
||||||
"It can be installed by running 'pip install django-storage-swift'."
|
|
||||||
)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# Load all SWIFT_* settings from the user configuration
|
|
||||||
for param, value in STORAGE_CONFIG.items():
|
|
||||||
if param.startswith('SWIFT_'):
|
|
||||||
globals()[param] = value
|
|
||||||
|
|
||||||
if STORAGE_CONFIG and STORAGE_BACKEND is None:
|
|
||||||
warnings.warn(
|
|
||||||
"STORAGE_CONFIG has been set in configuration.py but STORAGE_BACKEND is not defined. STORAGE_CONFIG will be "
|
|
||||||
"ignored."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Load all SWIFT_* settings from the user configuration
|
||||||
|
for param, value in STORAGE_CONFIG.items():
|
||||||
|
if param.startswith('SWIFT_'):
|
||||||
|
globals()[param] = value
|
||||||
|
# TODO: End of deprecated code
|
||||||
|
|
||||||
#
|
#
|
||||||
# Redis
|
# Redis
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Django==5.1.5
|
Django==5.2b1
|
||||||
django-cors-headers==4.6.0
|
django-cors-headers==4.6.0
|
||||||
django-debug-toolbar==5.0.1
|
django-debug-toolbar==5.0.1
|
||||||
django-filter==24.3
|
django-filter==24.3
|
||||||
@ -10,6 +10,7 @@ django-prometheus==2.3.1
|
|||||||
django-redis==5.4.0
|
django-redis==5.4.0
|
||||||
django-rich==1.13.0
|
django-rich==1.13.0
|
||||||
django-rq==3.0
|
django-rq==3.0
|
||||||
|
django-storages==1.14.4
|
||||||
django-taggit==6.1.0
|
django-taggit==6.1.0
|
||||||
django-tables2==2.7.5
|
django-tables2==2.7.5
|
||||||
django-timezone-field==7.1
|
django-timezone-field==7.1
|
||||||
@ -20,8 +21,8 @@ feedparser==6.0.11
|
|||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
Jinja2==3.1.5
|
Jinja2==3.1.5
|
||||||
Markdown==3.7
|
Markdown==3.7
|
||||||
mkdocs-material==9.6.2
|
mkdocs-material==9.6.7
|
||||||
mkdocstrings[python-legacy]==0.27.0
|
mkdocstrings[python]==0.28.2
|
||||||
netaddr==1.3.0
|
netaddr==1.3.0
|
||||||
nh3==0.2.20
|
nh3==0.2.20
|
||||||
Pillow==11.1.0
|
Pillow==11.1.0
|
||||||
|
Loading…
Reference in New Issue
Block a user