Skip to content

Commit ba7fab3

Browse files
committed
Initial work on #11890
1 parent 9c5f416 commit ba7fab3

File tree

15 files changed

+310
-5
lines changed

15 files changed

+310
-5
lines changed

netbox/core/choices.py

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ class DataSourceTypeChoices(ChoiceSet):
2020

2121

2222
class DataSourceStatusChoices(ChoiceSet):
23-
2423
NEW = 'new'
2524
QUEUED = 'queued'
2625
SYNCING = 'syncing'

netbox/core/forms/model_forms.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from core.models import *
66
from netbox.forms import NetBoxModelForm
77
from netbox.registry import registry
8-
from utilities.forms import CommentField, get_field_value
8+
from utilities.forms import BootstrapMixin, CommentField, get_field_value
99

1010
__all__ = (
1111
'DataSourceForm',
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Generated by Django 4.1.7 on 2023-03-23 17:35
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('core', '0001_initial'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='ManagedFile',
16+
fields=[
17+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
18+
('data_path', models.CharField(blank=True, editable=False, max_length=1000)),
19+
('data_synced', models.DateTimeField(blank=True, editable=False, null=True)),
20+
('created', models.DateTimeField(auto_now_add=True)),
21+
('last_updated', models.DateTimeField(blank=True, editable=False, null=True)),
22+
('file_root', models.CharField(max_length=1000)),
23+
('file_path', models.FilePathField(editable=False)),
24+
('data_file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile')),
25+
('data_source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource')),
26+
],
27+
options={
28+
'ordering': ('file_root', 'file_path'),
29+
},
30+
),
31+
migrations.AddIndex(
32+
model_name='managedfile',
33+
index=models.Index(fields=['file_root', 'file_path'], name='core_managedfile_root_path'),
34+
),
35+
migrations.AddConstraint(
36+
model_name='managedfile',
37+
constraint=models.UniqueConstraint(fields=('file_root', 'file_path'), name='core_managedfile_unique_root_path'),
38+
),
39+
]

netbox/core/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .data import *
2+
from .files import *

netbox/core/models/data.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from django.utils.module_loading import import_string
1515
from django.utils.translation import gettext as _
1616

17-
from extras.models import JobResult
1817
from netbox.models import PrimaryModel
1918
from netbox.registry import registry
2019
from utilities.files import sha256_hash
@@ -113,6 +112,8 @@ def enqueue_sync_job(self, request):
113112
"""
114113
Enqueue a background job to synchronize the DataSource by calling sync().
115114
"""
115+
from extras.models import JobResult
116+
116117
# Set the status to "syncing"
117118
self.status = DataSourceStatusChoices.QUEUED
118119
DataSource.objects.filter(pk=self.pk).update(status=self.status)

netbox/core/models/files.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import logging
2+
import os
3+
from importlib.machinery import FileFinder
4+
from pkgutil import ModuleInfo, get_importer
5+
6+
from django.conf import settings
7+
from django.db import models
8+
from django.urls import reverse
9+
from django.utils.translation import gettext as _
10+
11+
from netbox.models.features import SyncedDataMixin
12+
from utilities.querysets import RestrictedQuerySet
13+
14+
__all__ = (
15+
'ManagedFile',
16+
)
17+
18+
logger = logging.getLogger('netbox.core.files')
19+
20+
ROOT_PATH_CHOICES = (
21+
('scripts', 'Scripts Root'),
22+
('reports', 'Reports Root'),
23+
)
24+
25+
26+
class ManagedFile(SyncedDataMixin, models.Model):
27+
"""
28+
Database representation for a file on disk.
29+
"""
30+
created = models.DateTimeField(
31+
auto_now_add=True
32+
)
33+
last_updated = models.DateTimeField(
34+
editable=False,
35+
blank=True,
36+
null=True
37+
)
38+
file_root = models.CharField(
39+
max_length=1000,
40+
choices=ROOT_PATH_CHOICES
41+
)
42+
file_path = models.FilePathField(
43+
editable=False,
44+
help_text=_("File path relative to the designated root path")
45+
)
46+
47+
objects = RestrictedQuerySet.as_manager()
48+
49+
class Meta:
50+
ordering = ('file_root', 'file_path')
51+
constraints = (
52+
models.UniqueConstraint(
53+
fields=('file_root', 'file_path'),
54+
name='%(app_label)s_%(class)s_unique_root_path'
55+
),
56+
)
57+
indexes = [
58+
models.Index(fields=('file_root', 'file_path'), name='core_managedfile_root_path'),
59+
]
60+
61+
def __str__(self):
62+
return f'{self.get_file_root_display()}: {self.file_path}'
63+
64+
def get_absolute_url(self):
65+
return reverse('core:managedfile', args=[self.pk])
66+
67+
@property
68+
def full_path(self):
69+
return os.path.join(self._resolve_root_path(), self.file_path)
70+
71+
def _resolve_root_path(self):
72+
return {
73+
'scripts': settings.SCRIPTS_ROOT,
74+
'reports': settings.REPORTS_ROOT,
75+
}[self.file_root]
76+
77+
def get_module_info(self):
78+
return ModuleInfo(
79+
module_finder=get_importer(self.file_root),
80+
name=self.file_path.split('.py')[0],
81+
ispkg=False
82+
)

netbox/core/tables/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .data import *
2+
from .files import *

netbox/core/tables/files.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import django_tables2 as tables
2+
3+
from core.models import *
4+
from netbox.tables import NetBoxTable, columns
5+
6+
__all__ = (
7+
'ManagedFileTable',
8+
)
9+
10+
11+
class ManagedFileTable(NetBoxTable):
12+
file_path = tables.Column(
13+
linkify=True
14+
)
15+
last_updated = columns.DateTimeColumn()
16+
actions = columns.ActionsColumn(
17+
actions=('delete',)
18+
)
19+
20+
class Meta(NetBoxTable.Meta):
21+
model = ManagedFile
22+
fields = (
23+
'pk', 'id', 'file_root', 'file_path', 'last_updated', 'size', 'hash',
24+
)
25+
default_columns = ('pk', 'file_root', 'file_path', 'last_updated')

netbox/core/urls.py

+7
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,11 @@
1919
path('data-files/delete/', views.DataFileBulkDeleteView.as_view(), name='datafile_bulk_delete'),
2020
path('data-files/<int:pk>/', include(get_model_urls('core', 'datafile'))),
2121

22+
# Managed files
23+
path('files/', views.ManagedFileListView.as_view(), name='managedfile_list'),
24+
# path('files/add/', views.ManagedFileEditView.as_view(), name='managedfile_add'),
25+
# path('files/edit/', views.ManagedFileBulkEditView.as_view(), name='managedfile_bulk_edit'),
26+
# path('files/delete/', views.ManagedFileBulkDeleteView.as_view(), name='managedfile_bulk_delete'),
27+
path('files/<int:pk>/', include(get_model_urls('core', 'managedfile'))),
28+
2229
)

netbox/core/views.py

+40
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,43 @@ class DataFileBulkDeleteView(generic.BulkDeleteView):
120120
queryset = DataFile.objects.defer('data')
121121
filterset = filtersets.DataFileFilterSet
122122
table = tables.DataFileTable
123+
124+
125+
#
126+
# Managed files
127+
#
128+
129+
class ManagedFileListView(generic.ObjectListView):
130+
queryset = ManagedFile.objects.all()
131+
# filterset = filtersets.ManagedFileFilterSet
132+
# filterset_form = forms.ManagedFileFilterForm
133+
table = tables.ManagedFileTable
134+
135+
136+
@register_model_view(ManagedFile)
137+
class ManagedFileView(generic.ObjectView):
138+
queryset = ManagedFile.objects.all()
139+
140+
141+
# @register_model_view(ManagedFile, 'edit')
142+
# class ManagedFileEditView(generic.ObjectEditView):
143+
# queryset = ManagedFile.objects.all()
144+
# form = forms.ManagedFileForm
145+
146+
147+
@register_model_view(ManagedFile, 'delete')
148+
class ManagedFileDeleteView(generic.ObjectDeleteView):
149+
queryset = ManagedFile.objects.all()
150+
151+
152+
# class ManagedFileBulkEditView(generic.BulkEditView):
153+
# queryset = ManagedFile.objects.all()
154+
# # filterset = filtersets.ManagedFileFilterSet
155+
# table = tables.ManagedFileTable
156+
# form = forms.ManagedFileBulkEditForm
157+
158+
159+
# class ManagedFileBulkDeleteView(generic.BulkDeleteView):
160+
# queryset = ManagedFile.objects.all()
161+
# # filterset = filtersets.ManagedFileFilterSet
162+
# table = tables.ManagedFileTable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import pkgutil
2+
3+
from django.conf import settings
4+
from django.db import migrations
5+
6+
7+
def create_files(cls, root_name, path):
8+
9+
modules = list(pkgutil.iter_modules([path]))
10+
filenames = [f'{m.name}.py' for m in modules]
11+
12+
managed_files = [
13+
cls(
14+
file_root=root_name,
15+
file_path=filename
16+
) for filename in filenames
17+
]
18+
cls.objects.bulk_create(managed_files)
19+
20+
21+
def replicate_scripts(apps, schema_editor):
22+
ManagedFile = apps.get_model('core', 'ManagedFile')
23+
create_files(ManagedFile, 'scripts', settings.SCRIPTS_ROOT)
24+
25+
26+
def replicate_reports(apps, schema_editor):
27+
ManagedFile = apps.get_model('core', 'ManagedFile')
28+
create_files(ManagedFile, 'reports', settings.REPORTS_ROOT)
29+
30+
31+
class Migration(migrations.Migration):
32+
33+
dependencies = [
34+
('core', '0002_managedfile'),
35+
('extras', '0090_objectchange_index_request_id'),
36+
]
37+
38+
operations = [
39+
migrations.RunPython(
40+
code=replicate_scripts,
41+
reverse_code=migrations.RunPython.noop
42+
),
43+
migrations.RunPython(
44+
code=replicate_reports,
45+
reverse_code=migrations.RunPython.noop
46+
),
47+
]

netbox/extras/reports.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.utils import timezone
99
from django_rq import job
1010

11+
from core.models import ManagedFile
1112
from .choices import JobResultStatusChoices, LogLevelChoices
1213
from .models import JobResult
1314

@@ -53,7 +54,9 @@ def get_reports():
5354

5455
# Iterate through all modules within the reports path. These are the user-created files in which reports are
5556
# defined.
56-
for importer, module_name, _ in pkgutil.iter_modules([settings.REPORTS_ROOT]):
57+
# modules = pkgutil.iter_modules([settings.REPORTS_ROOT])
58+
modules = [mf.get_module_info() for mf in ManagedFile.objects.filter(file_root='reports')]
59+
for importer, module_name, _ in modules:
5760
module = importer.find_module(module_name).load_module(module_name)
5861
report_order = getattr(module, "report_order", ())
5962
ordered_reports = [cls() for cls in report_order if is_report(cls)]

netbox/extras/scripts.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from django.db import transaction
1616
from django.utils.functional import classproperty
1717

18+
from core.models import ManagedFile
1819
from extras.api.serializers import ScriptOutputSerializer
1920
from extras.choices import JobResultStatusChoices, LogLevelChoices
2021
from extras.models import JobResult
@@ -531,7 +532,8 @@ def get_scripts(use_names=False):
531532

532533
# Get all modules within the scripts path. These are the user-created files in which scripts are
533534
# defined.
534-
modules = list(pkgutil.iter_modules([settings.SCRIPTS_ROOT]))
535+
# modules = list(pkgutil.iter_modules([settings.SCRIPTS_ROOT]))
536+
modules = [mf.get_module_info() for mf in ManagedFile.objects.filter(file_root='scripts')]
535537
modules_bases = set([name.split(".")[0] for _, name, _ in modules])
536538

537539
# Deleting from sys.modules needs to done behind a lock to prevent race conditions where a module is

netbox/netbox/navigation/menu.py

+1
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@
313313
get_model_item('extras', 'tag', 'Tags'),
314314
get_model_item('extras', 'configcontext', _('Config Contexts'), actions=['add']),
315315
get_model_item('extras', 'configtemplate', _('Config Templates'), actions=['add']),
316+
get_model_item('core', 'managedfile', _('Managed Files'), actions=()),
316317
),
317318
),
318319
),
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{% extends 'generic/object.html' %}
2+
{% load buttons %}
3+
{% load custom_links %}
4+
{% load helpers %}
5+
{% load perms %}
6+
{% load plugins %}
7+
8+
{% block controls %}
9+
<div class="controls">
10+
<div class="control-group">
11+
{% plugin_buttons object %}
12+
</div>
13+
{% if request.user|can_delete:object %}
14+
{% delete_button object %}
15+
{% endif %}
16+
<div class="control-group">
17+
{% custom_links object %}
18+
</div>
19+
</div>
20+
{% endblock controls %}
21+
22+
{% block content %}
23+
<div class="row mb-3">
24+
<div class="col">
25+
<div class="card">
26+
<h5 class="card-header">Managed File</h5>
27+
<div class="card-body">
28+
<table class="table table-hover attr-table">
29+
<tr>
30+
<th scope="row">Root</th>
31+
<td>{{ object.get_file_root_display }}</td>
32+
</tr>
33+
<tr>
34+
<th scope="row">Path</th>
35+
<td>
36+
<span class="font-monospace" id="datafile_path">{{ object.file_path }}</span>
37+
<a class="btn btn-sm btn-primary copy-token" data-clipboard-target="#datafile_path" title="Copy to clipboard">
38+
<i class="mdi mdi-content-copy"></i>
39+
</a>
40+
</td>
41+
</tr>
42+
<tr>
43+
<th scope="row">Last Updated</th>
44+
<td>{{ object.last_updated }}</td>
45+
</tr>
46+
</table>
47+
</div>
48+
</div>
49+
{% plugin_left_page object %}
50+
</div>
51+
</div>
52+
<div class="row mb-3">
53+
<div class="col col-md-12">
54+
{% plugin_full_width_page object %}
55+
</div>
56+
</div>
57+
{% endblock %}

0 commit comments

Comments
 (0)