mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 19:08:38 -06:00
18423 scripts upload to django-storages
This commit is contained in:
parent
d202b3529d
commit
289dcffb95
@ -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
|
||||||
|
@ -351,17 +351,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):
|
||||||
"""
|
"""
|
||||||
|
@ -4,6 +4,7 @@ import os
|
|||||||
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 _
|
||||||
|
|
||||||
@ -19,6 +20,8 @@ logger = logging.getLogger('netbox.core.files')
|
|||||||
|
|
||||||
|
|
||||||
class ManagedFile(SyncedDataMixin, models.Model):
|
class ManagedFile(SyncedDataMixin, models.Model):
|
||||||
|
storage = None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Database representation for a file on disk. This class is typically wrapped by a proxy class (e.g. ScriptModule)
|
Database representation for a file on disk. This class is typically wrapped by a proxy class (e.g. ScriptModule)
|
||||||
to provide additional functionality.
|
to provide additional functionality.
|
||||||
@ -84,7 +87,25 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
|||||||
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.get_storage()
|
||||||
|
if storage.exists(path) and not overwrite:
|
||||||
|
raise FileExistsError()
|
||||||
|
|
||||||
|
with storage.open(path, 'wb+') as new_file:
|
||||||
|
new_file.write(self.data)
|
||||||
|
|
||||||
|
def get_storage(self):
|
||||||
|
if self.storage is None:
|
||||||
|
self.storage = storages.create_storage(storages.backends["scripts"])
|
||||||
|
|
||||||
|
return self.storage
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
@ -104,8 +125,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.get_storage()
|
||||||
try:
|
try:
|
||||||
os.remove(self.full_path)
|
storage.delete(self.full_path)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
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 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 +58,20 @@ 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"])
|
||||||
|
|
||||||
|
self.instance.file_path = self.cleaned_data['upload_file'].name
|
||||||
|
data = self.cleaned_data['upload_file']
|
||||||
|
storage.save(self.instance.name, data)
|
||||||
|
|
||||||
|
# need to skip ManagedFileForm save method
|
||||||
|
return super(ManagedFileForm, self).save(*args, **kwargs)
|
||||||
|
@ -1,11 +1,39 @@
|
|||||||
|
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):
|
||||||
|
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__)
|
||||||
|
|
||||||
|
|
||||||
|
def load_module(module_name, filename):
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, filename)
|
||||||
|
if spec is None:
|
||||||
|
raise ModuleNotFoundError(f"Could not find module: {module_name}")
|
||||||
|
loader = CustomStoragesLoader(filename)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[module_name] = module
|
||||||
|
loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
class PythonModuleMixin:
|
class PythonModuleMixin:
|
||||||
|
|
||||||
def get_jobs(self, name):
|
def get_jobs(self, name):
|
||||||
@ -33,6 +61,8 @@ class PythonModuleMixin:
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
def get_module(self):
|
def get_module(self):
|
||||||
loader = SourceFileLoader(self.python_name, self.full_path)
|
# loader = SourceFileLoader(self.python_name, self.full_path)
|
||||||
module = loader.load_module()
|
# module = loader.load_module()
|
||||||
|
# module = load_module(self.python_name, self.full_path)
|
||||||
|
module = load_module(self.python_name, self.name)
|
||||||
return module
|
return module
|
||||||
|
@ -369,6 +369,7 @@ class BaseScript:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self):
|
def source(self):
|
||||||
|
breakpoint()
|
||||||
return inspect.getsource(self.__class__)
|
return inspect.getsource(self.__class__)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -601,5 +602,6 @@ def is_variable(obj):
|
|||||||
|
|
||||||
def get_module_and_script(module_name, script_name):
|
def get_module_and_script(module_name, script_name):
|
||||||
module = ScriptModule.objects.get(file_path=f'{module_name}.py')
|
module = ScriptModule.objects.get(file_path=f'{module_name}.py')
|
||||||
|
breakpoint()
|
||||||
script = module.scripts.get(name=script_name)
|
script = module.scripts.get(name=script_name)
|
||||||
return module, script
|
return module, script
|
||||||
|
10
netbox/extras/storage.py
Normal file
10
netbox/extras/storage.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptFileSystemStorage(FileSystemStorage):
|
||||||
|
|
||||||
|
@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
|
||||||
|
@ -15,6 +15,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
|
||||||
|
|
||||||
@ -175,6 +176,7 @@ 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', {})
|
||||||
|
STORAGES = getattr(configuration, 'STORAGES', None)
|
||||||
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)
|
||||||
|
|
||||||
@ -223,60 +225,58 @@ DATABASES = {
|
|||||||
# Storage backend
|
# Storage backend
|
||||||
#
|
#
|
||||||
|
|
||||||
# Default STORAGES for Django
|
if STORAGE_BACKEND is not None:
|
||||||
STORAGES = {
|
warnings.warn(
|
||||||
"default": {
|
"STORAGE_BACKEND is deprecated, use the new STORAGES setting instead."
|
||||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
)
|
||||||
},
|
|
||||||
"staticfiles": {
|
|
||||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if STORAGE_BACKEND is not None and STORAGES is not None:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"STORAGE_BACKEND and STORAGES are both set, remove the deprecated STORAGE_BACKEND setting."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Default STORAGES for Django
|
||||||
|
if STORAGES is None:
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"BACKEND": "extras.storage.ScriptFileSystemStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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(
|
||||||
|
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
|
||||||
|
|
||||||
# django-storage-swift
|
# Load all SWIFT_* settings from the user configuration
|
||||||
elif STORAGE_BACKEND == 'swift.storage.SwiftStorage':
|
for param, value in STORAGE_CONFIG.items():
|
||||||
try:
|
if param.startswith('SWIFT_'):
|
||||||
import swift.utils # noqa: F401
|
globals()[param] = value
|
||||||
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."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user