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

Initial work on custom scripts (#3415)

This commit is contained in:
Jeremy Stretch
2019-08-09 12:33:33 -04:00
parent f18c3be745
commit a25a27f31f
11 changed files with 376 additions and 4 deletions

2
.gitignore vendored
View File

@ -3,6 +3,8 @@
/netbox/netbox/ldap_config.py
/netbox/reports/*
!/netbox/reports/__init__.py
/netbox/scripts/*
!/netbox/scripts/__init__.py
/netbox/static
.idea
/*.sh

View File

@ -380,3 +380,18 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
widget=ContentTypeSelect(),
label='Object Type'
)
#
# Scripts
#
class ScriptForm(BootstrapMixin, forms.Form):
def __init__(self, vars, *args, **kwargs):
super().__init__(*args, **kwargs)
# Dynamically populate fields for variables
for name, var in vars:
self.fields[name] = var.as_field()

143
netbox/extras/scripts.py Normal file
View File

@ -0,0 +1,143 @@
from collections import OrderedDict
import inspect
import pkgutil
from django import forms
from django.conf import settings
from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_SUCCESS, LOG_WARNING
from .forms import ScriptForm
#
# Script variables
#
class ScriptVariable:
form_field = forms.CharField
def __init__(self, label='', description=''):
# Default field attributes
if not hasattr(self, 'field_attrs'):
self.field_attrs = {}
if label:
self.field_attrs['label'] = label
if description:
self.field_attrs['help_text'] = description
def as_field(self):
"""
Render the variable as a Django form field.
"""
return self.form_field(**self.field_attrs)
class StringVar(ScriptVariable):
pass
class IntegerVar(ScriptVariable):
form_field = forms.IntegerField
class BooleanVar(ScriptVariable):
form_field = forms.BooleanField
field_attrs = {
'required': False
}
class ObjectVar(ScriptVariable):
form_field = forms.ModelChoiceField
def __init__(self, queryset, *args, **kwargs):
super().__init__(*args, **kwargs)
self.field_attrs['queryset'] = queryset
class Script:
"""
Custom scripts inherit this object.
"""
def __init__(self):
# Initiate the log
self.log = []
# Grab some info about the script
self.filename = inspect.getfile(self.__class__)
self.source = inspect.getsource(self.__class__)
def __str__(self):
if hasattr(self, 'name'):
return self.name
return self.__class__.__name__
def _get_vars(self):
# TODO: This should preserve var ordering
return inspect.getmembers(self, is_variable)
def run(self, context):
raise NotImplementedError("The script must define a run() method.")
def as_form(self, data=None):
"""
Return a Django form suitable for populating the context data required to run this Script.
"""
vars = self._get_vars()
form = ScriptForm(vars, data)
return form
# Logging
def log_debug(self, message):
self.log.append((LOG_DEFAULT, message))
def log_success(self, message):
self.log.append((LOG_SUCCESS, message))
def log_info(self, message):
self.log.append((LOG_INFO, message))
def log_warning(self, message):
self.log.append((LOG_WARNING, message))
def log_failure(self, message):
self.log.append((LOG_FAILURE, message))
#
# Functions
#
def is_script(obj):
"""
Returns True if the object is a Script.
"""
return obj in Script.__subclasses__()
def is_variable(obj):
"""
Returns True if the object is a ScriptVariable.
"""
return isinstance(obj, ScriptVariable)
def get_scripts():
scripts = OrderedDict()
# Iterate through all modules within the reports path. These are the user-created files in which reports are
# defined.
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
module = importer.find_module(module_name).load_module(module_name)
module_scripts = OrderedDict()
for name, cls in inspect.getmembers(module, is_script):
module_scripts[name] = cls
scripts[module_name] = module_scripts
return scripts

View File

@ -0,0 +1,37 @@
from django import template
from extras.constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_SUCCESS, LOG_WARNING
register = template.Library()
@register.inclusion_tag('extras/templatetags/log_level.html')
def log_level(level):
"""
Display a label indicating a syslog severity (e.g. info, warning, etc.).
"""
levels = {
LOG_DEFAULT: {
'name': 'Default',
'class': 'default'
},
LOG_SUCCESS: {
'name': 'Success',
'class': 'success',
},
LOG_INFO: {
'name': 'Info',
'class': 'info'
},
LOG_WARNING: {
'name': 'Warning',
'class': 'warning'
},
LOG_FAILURE: {
'name': 'Failure',
'class': 'danger'
}
}
return levels[level]

View File

@ -28,13 +28,17 @@ urlpatterns = [
path(r'image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
path(r'image-attachments/<int:pk>/delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
# Change logging
path(r'changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
path(r'changelog/<int:pk>/', views.ObjectChangeView.as_view(), name='objectchange'),
# Reports
path(r'reports/', views.ReportListView.as_view(), name='report_list'),
path(r'reports/<str:name>/', views.ReportView.as_view(), name='report'),
path(r'reports/<str:name>/run/', views.ReportRunView.as_view(), name='report_run'),
# Change logging
path(r'changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
path(r'changelog/<int:pk>/', views.ObjectChangeView.as_view(), name='objectchange'),
# Scripts
path(r'scripts/', views.ScriptListView.as_view(), name='script_list'),
path(r'scripts/<str:module>/<str:name>/', views.ScriptView.as_view(), name='script'),
]

View File

@ -1,8 +1,9 @@
from django import template
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import Count, Q
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
@ -20,6 +21,7 @@ from .forms import (
)
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem
from .reports import get_report, get_reports
from .scripts import get_scripts
from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable
@ -355,3 +357,53 @@ class ReportRunView(PermissionRequiredMixin, View):
messages.success(request, mark_safe(msg))
return redirect('extras:report', name=report.full_name)
#
# Scripts
#
class ScriptListView(LoginRequiredMixin, View):
def get(self, request):
return render(request, 'extras/script_list.html', {
'scripts': get_scripts(),
})
class ScriptView(LoginRequiredMixin, View):
def _get_script(self, module, name):
scripts = get_scripts()
try:
return scripts[module][name]()
except KeyError:
raise Http404
def get(self, request, module, name):
script = self._get_script(module, name)
form = script.as_form()
return render(request, 'extras/script.html', {
'module': module,
'script': script,
'form': form,
})
def post(self, request, module, name):
script = self._get_script(module, name)
form = script.as_form(request.POST)
if form.is_valid():
with transaction.atomic():
script.run(form.cleaned_data)
return render(request, 'extras/script.html', {
'module': module,
'script': script,
'form': form,
})

View File

@ -85,6 +85,7 @@ NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')

View File

View File

@ -0,0 +1,77 @@
{% extends '_base.html' %}
{% load helpers %}
{% load form_helpers %}
{% load log_levels %}
{% block title %}{{ script }}{% endblock %}
{% block content %}
<div class="row noprint">
<div class="col-md-12">
<ol class="breadcrumb">
<li><a href="{% url 'extras:script_list' %}">Scripts</a></li>
<li><a href="{% url 'extras:script_list' %}#module.{{ module }}">{{ module|bettertitle }}</a></li>
<li>{{ script }}</li>
</ol>
</div>
</div>
<h1>{{ script }}</h1>
<p>{{ script.description }}</p>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#run" role="tab" data-toggle="tab" class="active">Run</a>
</li>
<li role="presentation">
<a href="#source" role="tab" data-toggle="tab">Source</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="run">
{% if script.log %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Script Output</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<th>Line</th>
<th>Level</th>
<th>Message</th>
</tr>
{% for level, message in script.log %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{% log_level level %}</td>
<td>{{ message }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-8 col-md-offset-2">
<form action="" method="post">
{% csrf_token %}
{% if form %}
{% render_form form %}
{% else %}
<p>This script does not require any input to run.</p>
{% endif %}
<div class="pull-right">
<button type="submit" name="_run" class="btn btn-primary"><i class="fa fa-play"></i> Run Script</button>
<a href="{% url 'extras:script_list' %}" class="btn btn-default">Cancel</a>
</div>
</form>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="source">
<strong>{{ script.filename }}</strong>
<pre>{{ script.source }}</pre>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends '_base.html' %}
{% load helpers %}
{% block content %}
<h1>{% block title %}Scripts{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% if scripts %}
{% for module, module_scripts in scripts.items %}
<h3><a name="module.{{ module }}"></a>{{ module|bettertitle }}</h3>
<table class="table table-hover table-headings reports">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
{% for class_name, script in module_scripts.items %}
<tr>
<td>
<a href="{% url 'extras:script' module=module name=class_name %}" name="script.{{ class_name }}"><strong>{{ script }}</strong></a>
</td>
<td>{{ script.description }}</td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
{% else %}
<div class="alert alert-info">
<p><strong>No scripts found.</strong></p>
<p>Reports should be saved to <code>{{ settings.SCRIPTS_ROOT }}</code>. (This path can be changed by setting <code>SCRIPTS_ROOT</code> in NetBox's configuration.)</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1 @@
<label class="label label-{{ class }}">{{ name }}</label>