mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 04:56:29 -06:00
Merge pull request #4315 from netbox-community/4195-application-logging
Closes #4195: Application logging
This commit is contained in:
commit
2bd3f1fcc3
@ -183,6 +183,14 @@ LOGGING = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Available Loggers
|
||||||
|
|
||||||
|
* `netbox.auth.*` - Authentication events
|
||||||
|
* `netbox.api.views.*` - Views which handle business logic for the REST API
|
||||||
|
* `netbox.reports.*` - Report execution (`module.name`)
|
||||||
|
* `netbox.scripts.*` - Custom script execution (`module.name`)
|
||||||
|
* `netbox.views.*` - Views which handle business logic for the web UI
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## LOGIN_REQUIRED
|
## LOGIN_REQUIRED
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
# v2.8.0 (FUTURE)
|
# v2.8.0 (FUTURE)
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
|
||||||
|
* [#4195](https://github.com/netbox-community/netbox/issues/4195) - Enabled application logging (see [logging configuration](../configuration/optional-settings.md#logging))
|
||||||
|
|
||||||
## API Changes
|
## API Changes
|
||||||
|
|
||||||
* dcim.Rack: The `/api/dcim/racks/<pk>/units/` endpoint has been replaced with `/api/dcim/racks/<pk>/elevation/`.
|
* dcim.Rack: The `/api/dcim/racks/<pk>/units/` endpoint has been replaced with `/api/dcim/racks/<pk>/elevation/`.
|
||||||
|
@ -63,6 +63,7 @@ nav:
|
|||||||
- Release Checklist: 'development/release-checklist.md'
|
- Release Checklist: 'development/release-checklist.md'
|
||||||
- Squashing Migrations: 'development/squashing-migrations.md'
|
- Squashing Migrations: 'development/squashing-migrations.md'
|
||||||
- Release Notes:
|
- Release Notes:
|
||||||
|
- Version 2.8: 'release-notes/version-2.8.md'
|
||||||
- Version 2.7: 'release-notes/version-2.7.md'
|
- Version 2.7: 'release-notes/version-2.7.md'
|
||||||
- Version 2.6: 'release-notes/version-2.6.md'
|
- Version 2.6: 'release-notes/version-2.6.md'
|
||||||
- Version 2.5: 'release-notes/version-2.5.md'
|
- Version 2.5: 'release-notes/version-2.5.md'
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
|
import logging
|
||||||
import pkgutil
|
import pkgutil
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
@ -91,6 +92,8 @@ class Report(object):
|
|||||||
self.active_test = None
|
self.active_test = None
|
||||||
self.failed = False
|
self.failed = False
|
||||||
|
|
||||||
|
self.logger = logging.getLogger(f"netbox.reports.{self.module}.{self.name}")
|
||||||
|
|
||||||
# Compile test methods and initialize results skeleton
|
# Compile test methods and initialize results skeleton
|
||||||
test_methods = []
|
test_methods = []
|
||||||
for method in dir(self):
|
for method in dir(self):
|
||||||
@ -138,6 +141,7 @@ class Report(object):
|
|||||||
Log a message which is not associated with a particular object.
|
Log a message which is not associated with a particular object.
|
||||||
"""
|
"""
|
||||||
self._log(None, message, level=LOG_DEFAULT)
|
self._log(None, message, level=LOG_DEFAULT)
|
||||||
|
self.logger.info(message)
|
||||||
|
|
||||||
def log_success(self, obj, message=None):
|
def log_success(self, obj, message=None):
|
||||||
"""
|
"""
|
||||||
@ -146,6 +150,7 @@ class Report(object):
|
|||||||
if message:
|
if message:
|
||||||
self._log(obj, message, level=LOG_SUCCESS)
|
self._log(obj, message, level=LOG_SUCCESS)
|
||||||
self._results[self.active_test]['success'] += 1
|
self._results[self.active_test]['success'] += 1
|
||||||
|
self.logger.info(f"Success | {obj}: {message}")
|
||||||
|
|
||||||
def log_info(self, obj, message):
|
def log_info(self, obj, message):
|
||||||
"""
|
"""
|
||||||
@ -153,6 +158,7 @@ class Report(object):
|
|||||||
"""
|
"""
|
||||||
self._log(obj, message, level=LOG_INFO)
|
self._log(obj, message, level=LOG_INFO)
|
||||||
self._results[self.active_test]['info'] += 1
|
self._results[self.active_test]['info'] += 1
|
||||||
|
self.logger.info(f"Info | {obj}: {message}")
|
||||||
|
|
||||||
def log_warning(self, obj, message):
|
def log_warning(self, obj, message):
|
||||||
"""
|
"""
|
||||||
@ -160,6 +166,7 @@ class Report(object):
|
|||||||
"""
|
"""
|
||||||
self._log(obj, message, level=LOG_WARNING)
|
self._log(obj, message, level=LOG_WARNING)
|
||||||
self._results[self.active_test]['warning'] += 1
|
self._results[self.active_test]['warning'] += 1
|
||||||
|
self.logger.info(f"Warning | {obj}: {message}")
|
||||||
|
|
||||||
def log_failure(self, obj, message):
|
def log_failure(self, obj, message):
|
||||||
"""
|
"""
|
||||||
@ -167,12 +174,15 @@ class Report(object):
|
|||||||
"""
|
"""
|
||||||
self._log(obj, message, level=LOG_FAILURE)
|
self._log(obj, message, level=LOG_FAILURE)
|
||||||
self._results[self.active_test]['failure'] += 1
|
self._results[self.active_test]['failure'] += 1
|
||||||
|
self.logger.info(f"Failure | {obj}: {message}")
|
||||||
self.failed = True
|
self.failed = True
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
Run the report and return its results. Each test method will be executed in order.
|
Run the report and return its results. Each test method will be executed in order.
|
||||||
"""
|
"""
|
||||||
|
self.logger.info(f"Running report")
|
||||||
|
|
||||||
for method_name in self.test_methods:
|
for method_name in self.test_methods:
|
||||||
self.active_test = method_name
|
self.active_test = method_name
|
||||||
test_method = getattr(self, method_name)
|
test_method = getattr(self, method_name)
|
||||||
@ -184,6 +194,11 @@ class Report(object):
|
|||||||
result.save()
|
result.save()
|
||||||
self.result = result
|
self.result = result
|
||||||
|
|
||||||
|
if self.failed:
|
||||||
|
self.logger.warning("Report failed")
|
||||||
|
else:
|
||||||
|
self.logger.info("Report completed successfully")
|
||||||
|
|
||||||
# Perform any post-run tasks
|
# Perform any post-run tasks
|
||||||
self.post_run()
|
self.post_run()
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import time
|
import time
|
||||||
@ -254,6 +255,7 @@ class BaseScript:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
# Initiate the log
|
# Initiate the log
|
||||||
|
self.logger = logging.getLogger(f"netbox.scripts.{self.module()}.{self.__class__.__name__}")
|
||||||
self.log = []
|
self.log = []
|
||||||
|
|
||||||
# Declare the placeholder for the current request
|
# Declare the placeholder for the current request
|
||||||
@ -301,18 +303,23 @@ class BaseScript:
|
|||||||
# Logging
|
# Logging
|
||||||
|
|
||||||
def log_debug(self, message):
|
def log_debug(self, message):
|
||||||
|
self.logger.log(logging.DEBUG, message)
|
||||||
self.log.append((LOG_DEFAULT, message))
|
self.log.append((LOG_DEFAULT, message))
|
||||||
|
|
||||||
def log_success(self, message):
|
def log_success(self, message):
|
||||||
|
self.logger.log(logging.INFO, message) # No syslog equivalent for SUCCESS
|
||||||
self.log.append((LOG_SUCCESS, message))
|
self.log.append((LOG_SUCCESS, message))
|
||||||
|
|
||||||
def log_info(self, message):
|
def log_info(self, message):
|
||||||
|
self.logger.log(logging.INFO, message)
|
||||||
self.log.append((LOG_INFO, message))
|
self.log.append((LOG_INFO, message))
|
||||||
|
|
||||||
def log_warning(self, message):
|
def log_warning(self, message):
|
||||||
|
self.logger.log(logging.WARNING, message)
|
||||||
self.log.append((LOG_WARNING, message))
|
self.log.append((LOG_WARNING, message))
|
||||||
|
|
||||||
def log_failure(self, message):
|
def log_failure(self, message):
|
||||||
|
self.logger.log(logging.ERROR, message)
|
||||||
self.log.append((LOG_FAILURE, message))
|
self.log.append((LOG_FAILURE, message))
|
||||||
|
|
||||||
# Convenience functions
|
# Convenience functions
|
||||||
@ -375,6 +382,10 @@ def run_script(script, data, request, commit=True):
|
|||||||
start_time = None
|
start_time = None
|
||||||
end_time = None
|
end_time = None
|
||||||
|
|
||||||
|
script_name = script.__class__.__name__
|
||||||
|
logger = logging.getLogger(f"netbox.scripts.{script.module()}.{script_name}")
|
||||||
|
logger.info(f"Running script (commit={commit})")
|
||||||
|
|
||||||
# Add files to form data
|
# Add files to form data
|
||||||
files = request.FILES
|
files = request.FILES
|
||||||
for field_name, fileobj in files.items():
|
for field_name, fileobj in files.items():
|
||||||
@ -404,6 +415,7 @@ def run_script(script, data, request, commit=True):
|
|||||||
script.log_failure(
|
script.log_failure(
|
||||||
"An exception occurred: `{}: {}`\n```\n{}\n```".format(type(e).__name__, e, stacktrace)
|
"An exception occurred: `{}: {}`\n```\n{}\n```".format(type(e).__name__, e, stacktrace)
|
||||||
)
|
)
|
||||||
|
logger.error(f"Exception raised during script execution: {e}")
|
||||||
commit = False
|
commit = False
|
||||||
finally:
|
finally:
|
||||||
if not commit:
|
if not commit:
|
||||||
@ -416,6 +428,7 @@ def run_script(script, data, request, commit=True):
|
|||||||
# Calculate execution time
|
# Calculate execution time
|
||||||
if end_time is not None:
|
if end_time is not None:
|
||||||
execution_time = end_time - start_time
|
execution_time = end_time - start_time
|
||||||
|
logger.info(f"Script completed in {execution_time:.4f} seconds")
|
||||||
else:
|
else:
|
||||||
execution_time = None
|
execution_time = None
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
|
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
|
||||||
@ -24,6 +26,9 @@ from .models import Token
|
|||||||
#
|
#
|
||||||
|
|
||||||
class LoginView(View):
|
class LoginView(View):
|
||||||
|
"""
|
||||||
|
Perform user authentication via the web UI.
|
||||||
|
"""
|
||||||
template_name = 'login.html'
|
template_name = 'login.html'
|
||||||
|
|
||||||
@method_decorator(sensitive_post_parameters('password'))
|
@method_decorator(sensitive_post_parameters('password'))
|
||||||
@ -38,36 +43,51 @@ class LoginView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
logger = logging.getLogger('netbox.auth.login')
|
||||||
form = LoginForm(request, data=request.POST)
|
form = LoginForm(request, data=request.POST)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
logger.debug("Login form validation was successful")
|
||||||
|
|
||||||
# Determine where to direct user after successful login
|
# Determine where to direct user after successful login
|
||||||
redirect_to = request.POST.get('next', '')
|
redirect_to = request.POST.get('next')
|
||||||
if not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
|
if redirect_to and not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
|
||||||
|
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_to}")
|
||||||
redirect_to = reverse('home')
|
redirect_to = reverse('home')
|
||||||
|
|
||||||
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
|
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
|
||||||
# last_login time upon authentication.
|
# last_login time upon authentication.
|
||||||
if settings.MAINTENANCE_MODE:
|
if settings.MAINTENANCE_MODE:
|
||||||
|
logger.warning("Maintenance mode enabled: disabling update of most recent login time")
|
||||||
user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
|
user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
|
||||||
|
|
||||||
# Authenticate user
|
# Authenticate user
|
||||||
auth_login(request, form.get_user())
|
auth_login(request, form.get_user())
|
||||||
|
logger.info(f"User {request.user} successfully authenticated")
|
||||||
messages.info(request, "Logged in as {}.".format(request.user))
|
messages.info(request, "Logged in as {}.".format(request.user))
|
||||||
|
|
||||||
|
logger.debug(f"Redirecting user to {redirect_to}")
|
||||||
return HttpResponseRedirect(redirect_to)
|
return HttpResponseRedirect(redirect_to)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("Login form validation failed")
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class LogoutView(View):
|
class LogoutView(View):
|
||||||
|
"""
|
||||||
|
Deauthenticate a web user.
|
||||||
|
"""
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
logger = logging.getLogger('netbox.auth.logout')
|
||||||
|
|
||||||
# Log out the user
|
# Log out the user
|
||||||
|
username = request.user
|
||||||
auth_logout(request)
|
auth_logout(request)
|
||||||
|
logger.info(f"User {username} has logged out")
|
||||||
messages.info(request, "You have logged out.")
|
messages.info(request, "You have logged out.")
|
||||||
|
|
||||||
# Delete session key cookie (if set) upon logout
|
# Delete session key cookie (if set) upon logout
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
@ -303,25 +304,35 @@ class ModelViewSet(_ModelViewSet):
|
|||||||
return super().get_serializer(*args, **kwargs)
|
return super().get_serializer(*args, **kwargs)
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
|
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||||
|
|
||||||
# If 'brief' has been passed as a query param, find and return the nested serializer for this model, if one
|
# If 'brief' has been passed as a query param, find and return the nested serializer for this model, if one
|
||||||
# exists
|
# exists
|
||||||
request = self.get_serializer_context()['request']
|
request = self.get_serializer_context()['request']
|
||||||
if request.query_params.get('brief', False):
|
if request.query_params.get('brief'):
|
||||||
|
logger.debug("Request is for 'brief' format; initializing nested serializer")
|
||||||
try:
|
try:
|
||||||
return get_serializer_for_model(self.queryset.model, prefix='Nested')
|
serializer = get_serializer_for_model(self.queryset.model, prefix='Nested')
|
||||||
|
logger.debug(f"Using serializer {serializer}")
|
||||||
|
return serializer
|
||||||
except SerializerNotFound:
|
except SerializerNotFound:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fall back to the hard-coded serializer class
|
# Fall back to the hard-coded serializer class
|
||||||
|
logger.debug(f"Using serializer {self.serializer_class}")
|
||||||
return self.serializer_class
|
return self.serializer_class
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
except ProtectedError as e:
|
except ProtectedError as e:
|
||||||
models = ['{} ({})'.format(o, o._meta) for o in e.protected_objects.all()]
|
models = [
|
||||||
|
'{} ({})'.format(o, o._meta) for o in e.protected_objects.all()
|
||||||
|
]
|
||||||
msg = 'Unable to delete object. The following dependent objects were found: {}'.format(', '.join(models))
|
msg = 'Unable to delete object. The following dependent objects were found: {}'.format(', '.join(models))
|
||||||
|
logger.warning(msg)
|
||||||
return self.finalize_response(
|
return self.finalize_response(
|
||||||
request,
|
request,
|
||||||
Response({'detail': msg}, status=409),
|
Response({'detail': msg}, status=409),
|
||||||
@ -341,6 +352,26 @@ class ModelViewSet(_ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
return super().retrieve(*args, **kwargs)
|
return super().retrieve(*args, **kwargs)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Logging
|
||||||
|
#
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
model = serializer.child.Meta.model if hasattr(serializer, 'many') else serializer.Meta.model
|
||||||
|
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||||
|
logger.info(f"Creating new {model._meta.verbose_name}")
|
||||||
|
return super().perform_create(serializer)
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||||
|
logger.info(f"Updating {serializer.instance} (PK: {serializer.instance.pk})")
|
||||||
|
return super().perform_update(serializer)
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
||||||
|
logger.info(f"Deleting {instance} (PK: {instance.pk})")
|
||||||
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
class FieldChoicesViewSet(ViewSet):
|
class FieldChoicesViewSet(ViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
@ -219,35 +220,36 @@ class ObjectEditView(GetReturnURLMixin, View):
|
|||||||
# given some parameter from the request URL.
|
# given some parameter from the request URL.
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
|
||||||
|
|
||||||
obj = self.get_object(kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
obj = self.alter_obj(obj, request, args, kwargs)
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
# Parse initial data manually to avoid setting field values as lists
|
# Parse initial data manually to avoid setting field values as lists
|
||||||
initial_data = {k: request.GET[k] for k in request.GET}
|
initial_data = {k: request.GET[k] for k in request.GET}
|
||||||
form = self.model_form(instance=obj, initial=initial_data)
|
form = self.model_form(instance=self.obj, initial=initial_data)
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'obj': obj,
|
'obj': self.obj,
|
||||||
'obj_type': self.model._meta.verbose_name,
|
'obj_type': self.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': self.get_return_url(request, obj),
|
'return_url': self.get_return_url(request, self.obj),
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
logger = logging.getLogger('netbox.views.ObjectEditView')
|
||||||
obj = self.get_object(kwargs)
|
form = self.model_form(request.POST, request.FILES, instance=self.obj)
|
||||||
obj = self.alter_obj(obj, request, args, kwargs)
|
|
||||||
form = self.model_form(request.POST, request.FILES, instance=obj)
|
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
obj_created = not form.instance.pk
|
logger.debug("Form validation was successful")
|
||||||
obj = form.save()
|
|
||||||
|
|
||||||
|
obj = form.save()
|
||||||
msg = '{} {}'.format(
|
msg = '{} {}'.format(
|
||||||
'Created' if obj_created else 'Modified',
|
'Created' if not form.instance.pk else 'Modified',
|
||||||
self.model._meta.verbose_name
|
self.model._meta.verbose_name
|
||||||
)
|
)
|
||||||
|
logger.info(f"{msg} {obj} (PK: {obj.pk})")
|
||||||
if hasattr(obj, 'get_absolute_url'):
|
if hasattr(obj, 'get_absolute_url'):
|
||||||
msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
|
msg = '{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
|
||||||
else:
|
else:
|
||||||
@ -269,11 +271,14 @@ class ObjectEditView(GetReturnURLMixin, View):
|
|||||||
else:
|
else:
|
||||||
return redirect(self.get_return_url(request, obj))
|
return redirect(self.get_return_url(request, obj))
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("Form validation failed")
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'obj': obj,
|
'obj': self.obj,
|
||||||
'obj_type': self.model._meta.verbose_name,
|
'obj_type': self.model._meta.verbose_name,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': self.get_return_url(request, obj),
|
'return_url': self.get_return_url(request, self.obj),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -295,7 +300,6 @@ class ObjectDeleteView(GetReturnURLMixin, View):
|
|||||||
return get_object_or_404(self.model, pk=kwargs['pk'])
|
return get_object_or_404(self.model, pk=kwargs['pk'])
|
||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
|
|
||||||
obj = self.get_object(kwargs)
|
obj = self.get_object(kwargs)
|
||||||
form = ConfirmationForm(initial=request.GET)
|
form = ConfirmationForm(initial=request.GET)
|
||||||
|
|
||||||
@ -307,18 +311,22 @@ class ObjectDeleteView(GetReturnURLMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, **kwargs):
|
def post(self, request, **kwargs):
|
||||||
|
logger = logging.getLogger('netbox.views.ObjectDeleteView')
|
||||||
obj = self.get_object(kwargs)
|
obj = self.get_object(kwargs)
|
||||||
form = ConfirmationForm(request.POST)
|
form = ConfirmationForm(request.POST)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
logger.debug("Form validation was successful")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
obj.delete()
|
obj.delete()
|
||||||
except ProtectedError as e:
|
except ProtectedError as e:
|
||||||
|
logger.info("Caught ProtectedError while attempting to delete object")
|
||||||
handle_protectederror(obj, request, e)
|
handle_protectederror(obj, request, e)
|
||||||
return redirect(obj.get_absolute_url())
|
return redirect(obj.get_absolute_url())
|
||||||
|
|
||||||
msg = 'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
|
msg = 'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
|
||||||
|
logger.info(msg)
|
||||||
messages.success(request, msg)
|
messages.success(request, msg)
|
||||||
|
|
||||||
return_url = form.cleaned_data.get('return_url')
|
return_url = form.cleaned_data.get('return_url')
|
||||||
@ -327,6 +335,9 @@ class ObjectDeleteView(GetReturnURLMixin, View):
|
|||||||
else:
|
else:
|
||||||
return redirect(self.get_return_url(request, obj))
|
return redirect(self.get_return_url(request, obj))
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("Form validation failed")
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'obj': obj,
|
'obj': obj,
|
||||||
'form': form,
|
'form': form,
|
||||||
@ -350,7 +361,6 @@ class BulkCreateView(GetReturnURLMixin, View):
|
|||||||
template_name = None
|
template_name = None
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
||||||
# Set initial values for visible form fields from query args
|
# Set initial values for visible form fields from query args
|
||||||
initial = {}
|
initial = {}
|
||||||
for field in getattr(self.model_form._meta, 'fields', []):
|
for field in getattr(self.model_form._meta, 'fields', []):
|
||||||
@ -368,13 +378,13 @@ class BulkCreateView(GetReturnURLMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
logger = logging.getLogger('netbox.views.BulkCreateView')
|
||||||
model = self.model_form._meta.model
|
model = self.model_form._meta.model
|
||||||
form = self.form(request.POST)
|
form = self.form(request.POST)
|
||||||
model_form = self.model_form(request.POST)
|
model_form = self.model_form(request.POST)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
logger.debug("Form validation was successful")
|
||||||
pattern = form.cleaned_data['pattern']
|
pattern = form.cleaned_data['pattern']
|
||||||
new_objs = []
|
new_objs = []
|
||||||
|
|
||||||
@ -392,6 +402,7 @@ class BulkCreateView(GetReturnURLMixin, View):
|
|||||||
# Validate each new object independently.
|
# Validate each new object independently.
|
||||||
if model_form.is_valid():
|
if model_form.is_valid():
|
||||||
obj = model_form.save()
|
obj = model_form.save()
|
||||||
|
logger.debug(f"Created {obj} (PK: {obj.pk})")
|
||||||
new_objs.append(obj)
|
new_objs.append(obj)
|
||||||
else:
|
else:
|
||||||
# Copy any errors on the pattern target field to the pattern form.
|
# Copy any errors on the pattern target field to the pattern form.
|
||||||
@ -403,6 +414,7 @@ class BulkCreateView(GetReturnURLMixin, View):
|
|||||||
|
|
||||||
# If we make it to this point, validation has succeeded on all new objects.
|
# If we make it to this point, validation has succeeded on all new objects.
|
||||||
msg = "Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
|
msg = "Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
|
||||||
|
logger.info(msg)
|
||||||
messages.success(request, msg)
|
messages.success(request, msg)
|
||||||
|
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
@ -412,6 +424,9 @@ class BulkCreateView(GetReturnURLMixin, View):
|
|||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("Form validation failed")
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
'model_form': model_form,
|
'model_form': model_form,
|
||||||
@ -430,7 +445,6 @@ class ObjectImportView(GetReturnURLMixin, View):
|
|||||||
template_name = 'utilities/obj_import.html'
|
template_name = 'utilities/obj_import.html'
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
||||||
form = ImportForm()
|
form = ImportForm()
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
@ -440,9 +454,11 @@ class ObjectImportView(GetReturnURLMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
logger = logging.getLogger('netbox.views.ObjectImportView')
|
||||||
form = ImportForm(request.POST)
|
form = ImportForm(request.POST)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
logger.debug("Import form validation was successful")
|
||||||
|
|
||||||
# Initialize model form
|
# Initialize model form
|
||||||
data = form.cleaned_data['data']
|
data = form.cleaned_data['data']
|
||||||
@ -463,9 +479,11 @@ class ObjectImportView(GetReturnURLMixin, View):
|
|||||||
|
|
||||||
# Save the primary object
|
# Save the primary object
|
||||||
obj = model_form.save()
|
obj = model_form.save()
|
||||||
|
logger.debug(f"Created {obj} (PK: {obj.pk})")
|
||||||
|
|
||||||
# Iterate through the related object forms (if any), validating and saving each instance.
|
# Iterate through the related object forms (if any), validating and saving each instance.
|
||||||
for field_name, related_object_form in self.related_object_forms.items():
|
for field_name, related_object_form in self.related_object_forms.items():
|
||||||
|
logger.debug("Processing form for related objects: {related_object_form}")
|
||||||
|
|
||||||
for i, rel_obj_data in enumerate(data.get(field_name, list())):
|
for i, rel_obj_data in enumerate(data.get(field_name, list())):
|
||||||
|
|
||||||
@ -489,7 +507,7 @@ class ObjectImportView(GetReturnURLMixin, View):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if not model_form.errors:
|
if not model_form.errors:
|
||||||
|
logger.info(f"Import object {obj} (PK: {obj.pk})")
|
||||||
messages.success(request, mark_safe('Imported object: <a href="{}">{}</a>'.format(
|
messages.success(request, mark_safe('Imported object: <a href="{}">{}</a>'.format(
|
||||||
obj.get_absolute_url(), obj
|
obj.get_absolute_url(), obj
|
||||||
)))
|
)))
|
||||||
@ -504,6 +522,7 @@ class ObjectImportView(GetReturnURLMixin, View):
|
|||||||
return redirect(self.get_return_url(request, obj))
|
return redirect(self.get_return_url(request, obj))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
logger.debug("Model form validation failed")
|
||||||
|
|
||||||
# Replicate model form errors for display
|
# Replicate model form errors for display
|
||||||
for field, errors in model_form.errors.items():
|
for field, errors in model_form.errors.items():
|
||||||
@ -513,6 +532,9 @@ class ObjectImportView(GetReturnURLMixin, View):
|
|||||||
else:
|
else:
|
||||||
form.add_error(None, "{}: {}".format(field, err))
|
form.add_error(None, "{}: {}".format(field, err))
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("Import form validation failed")
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
'obj_type': self.model._meta.verbose_name,
|
'obj_type': self.model._meta.verbose_name,
|
||||||
@ -560,14 +582,14 @@ class BulkImportView(GetReturnURLMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
logger = logging.getLogger('netbox.views.BulkImportView')
|
||||||
new_objs = []
|
new_objs = []
|
||||||
form = self._import_form(request.POST)
|
form = self._import_form(request.POST)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
logger.debug("Form validation was successful")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
# Iterate through CSV data and bind each row to a new model form instance.
|
# Iterate through CSV data and bind each row to a new model form instance.
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for row, data in enumerate(form.cleaned_data['csv'], start=1):
|
for row, data in enumerate(form.cleaned_data['csv'], start=1):
|
||||||
@ -585,6 +607,7 @@ class BulkImportView(GetReturnURLMixin, View):
|
|||||||
|
|
||||||
if new_objs:
|
if new_objs:
|
||||||
msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
|
msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
|
||||||
|
logger.info(msg)
|
||||||
messages.success(request, msg)
|
messages.success(request, msg)
|
||||||
|
|
||||||
return render(request, "import_success.html", {
|
return render(request, "import_success.html", {
|
||||||
@ -595,6 +618,9 @@ class BulkImportView(GetReturnURLMixin, View):
|
|||||||
except ValidationError:
|
except ValidationError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("Form validation failed")
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
'fields': self.model_form().fields,
|
'fields': self.model_form().fields,
|
||||||
@ -623,7 +649,7 @@ class BulkEditView(GetReturnURLMixin, View):
|
|||||||
return redirect(self.get_return_url(request))
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
def post(self, request, **kwargs):
|
def post(self, request, **kwargs):
|
||||||
|
logger = logging.getLogger('netbox.views.BulkEditView')
|
||||||
model = self.queryset.model
|
model = self.queryset.model
|
||||||
|
|
||||||
# If we are editing *all* objects in the queryset, replace the PK list with all matched objects.
|
# If we are editing *all* objects in the queryset, replace the PK list with all matched objects.
|
||||||
@ -636,8 +662,9 @@ class BulkEditView(GetReturnURLMixin, View):
|
|||||||
|
|
||||||
if '_apply' in request.POST:
|
if '_apply' in request.POST:
|
||||||
form = self.form(model, request.POST)
|
form = self.form(model, request.POST)
|
||||||
if form.is_valid():
|
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
logger.debug("Form validation was successful")
|
||||||
custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
|
custom_fields = form.custom_fields if hasattr(form, 'custom_fields') else []
|
||||||
standard_fields = [
|
standard_fields = [
|
||||||
field for field in form.fields if field not in custom_fields + ['pk']
|
field for field in form.fields if field not in custom_fields + ['pk']
|
||||||
@ -677,6 +704,7 @@ class BulkEditView(GetReturnURLMixin, View):
|
|||||||
|
|
||||||
obj.full_clean()
|
obj.full_clean()
|
||||||
obj.save()
|
obj.save()
|
||||||
|
logger.debug(f"Saved {obj} (PK: {obj.pk})")
|
||||||
|
|
||||||
# Update custom fields
|
# Update custom fields
|
||||||
obj_type = ContentType.objects.get_for_model(model)
|
obj_type = ContentType.objects.get_for_model(model)
|
||||||
@ -697,6 +725,7 @@ class BulkEditView(GetReturnURLMixin, View):
|
|||||||
)
|
)
|
||||||
cfv.value = form.cleaned_data[name]
|
cfv.value = form.cleaned_data[name]
|
||||||
cfv.save()
|
cfv.save()
|
||||||
|
logger.debug(f"Saved custom fields for {obj} (PK: {obj.pk})")
|
||||||
|
|
||||||
# Add/remove tags
|
# Add/remove tags
|
||||||
if form.cleaned_data.get('add_tags', None):
|
if form.cleaned_data.get('add_tags', None):
|
||||||
@ -708,6 +737,7 @@ class BulkEditView(GetReturnURLMixin, View):
|
|||||||
|
|
||||||
if updated_count:
|
if updated_count:
|
||||||
msg = 'Updated {} {}'.format(updated_count, model._meta.verbose_name_plural)
|
msg = 'Updated {} {}'.format(updated_count, model._meta.verbose_name_plural)
|
||||||
|
logger.info(msg)
|
||||||
messages.success(self.request, msg)
|
messages.success(self.request, msg)
|
||||||
|
|
||||||
return redirect(self.get_return_url(request))
|
return redirect(self.get_return_url(request))
|
||||||
@ -715,6 +745,9 @@ class BulkEditView(GetReturnURLMixin, View):
|
|||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
messages.error(self.request, "{} failed validation: {}".format(obj, e))
|
messages.error(self.request, "{} failed validation: {}".format(obj, e))
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("Form validation failed")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Include the PK list as initial data for the form
|
# Include the PK list as initial data for the form
|
||||||
initial_data = {'pk': pk_list}
|
initial_data = {'pk': pk_list}
|
||||||
@ -761,7 +794,7 @@ class BulkDeleteView(GetReturnURLMixin, View):
|
|||||||
return redirect(self.get_return_url(request))
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
def post(self, request, **kwargs):
|
def post(self, request, **kwargs):
|
||||||
|
logger = logging.getLogger('netbox.views.BulkDeleteView')
|
||||||
model = self.queryset.model
|
model = self.queryset.model
|
||||||
|
|
||||||
# Are we deleting *all* objects in the queryset or just a selected subset?
|
# Are we deleting *all* objects in the queryset or just a selected subset?
|
||||||
@ -778,19 +811,25 @@ class BulkDeleteView(GetReturnURLMixin, View):
|
|||||||
if '_confirm' in request.POST:
|
if '_confirm' in request.POST:
|
||||||
form = form_cls(request.POST)
|
form = form_cls(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
logger.debug("Form validation was successful")
|
||||||
|
|
||||||
# Delete objects
|
# Delete objects
|
||||||
queryset = model.objects.filter(pk__in=pk_list)
|
queryset = model.objects.filter(pk__in=pk_list)
|
||||||
try:
|
try:
|
||||||
deleted_count = queryset.delete()[1][model._meta.label]
|
deleted_count = queryset.delete()[1][model._meta.label]
|
||||||
except ProtectedError as e:
|
except ProtectedError as e:
|
||||||
|
logger.info("Caught ProtectedError while attempting to delete objects")
|
||||||
handle_protectederror(list(queryset), request, e)
|
handle_protectederror(list(queryset), request, e)
|
||||||
return redirect(self.get_return_url(request))
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
msg = 'Deleted {} {}'.format(deleted_count, model._meta.verbose_name_plural)
|
msg = 'Deleted {} {}'.format(deleted_count, model._meta.verbose_name_plural)
|
||||||
|
logger.info(msg)
|
||||||
messages.success(request, msg)
|
messages.success(request, msg)
|
||||||
return redirect(self.get_return_url(request))
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("Form validation failed")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
form = form_cls(initial={
|
form = form_cls(initial={
|
||||||
'pk': pk_list,
|
'pk': pk_list,
|
||||||
@ -814,12 +853,12 @@ class BulkDeleteView(GetReturnURLMixin, View):
|
|||||||
"""
|
"""
|
||||||
Provide a standard bulk delete form if none has been specified for the view
|
Provide a standard bulk delete form if none has been specified for the view
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class BulkDeleteForm(ConfirmationForm):
|
class BulkDeleteForm(ConfirmationForm):
|
||||||
pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
|
pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput)
|
||||||
|
|
||||||
if self.form:
|
if self.form:
|
||||||
return self.form
|
return self.form
|
||||||
|
|
||||||
return BulkDeleteForm
|
return BulkDeleteForm
|
||||||
|
|
||||||
|
|
||||||
@ -908,7 +947,7 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
|
|||||||
template_name = 'utilities/obj_bulk_add_component.html'
|
template_name = 'utilities/obj_bulk_add_component.html'
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
logger = logging.getLogger('netbox.views.BulkComponentCreateView')
|
||||||
parent_model_name = self.parent_model._meta.verbose_name_plural
|
parent_model_name = self.parent_model._meta.verbose_name_plural
|
||||||
model_name = self.model._meta.verbose_name_plural
|
model_name = self.model._meta.verbose_name_plural
|
||||||
|
|
||||||
@ -926,10 +965,13 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
|
|||||||
|
|
||||||
if '_create' in request.POST:
|
if '_create' in request.POST:
|
||||||
form = self.form(request.POST)
|
form = self.form(request.POST)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
logger.debug("Form validation was successful")
|
||||||
|
|
||||||
new_components = []
|
new_components = []
|
||||||
data = deepcopy(form.cleaned_data)
|
data = deepcopy(form.cleaned_data)
|
||||||
|
|
||||||
for obj in data['pk']:
|
for obj in data['pk']:
|
||||||
|
|
||||||
names = data['name_pattern']
|
names = data['name_pattern']
|
||||||
@ -949,15 +991,20 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
|
|||||||
|
|
||||||
if not form.errors:
|
if not form.errors:
|
||||||
self.model.objects.bulk_create(new_components)
|
self.model.objects.bulk_create(new_components)
|
||||||
|
msg = "Added {} {} to {} {}.".format(
|
||||||
messages.success(request, "Added {} {} to {} {}.".format(
|
|
||||||
len(new_components),
|
len(new_components),
|
||||||
model_name,
|
model_name,
|
||||||
len(form.cleaned_data['pk']),
|
len(form.cleaned_data['pk']),
|
||||||
parent_model_name
|
parent_model_name
|
||||||
))
|
)
|
||||||
|
logger.info(msg)
|
||||||
|
messages.success(request, msg)
|
||||||
|
|
||||||
return redirect(self.get_return_url(request))
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("Form validation failed")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
form = self.form(initial={'pk': pk_list})
|
form = self.form(initial={'pk': pk_list})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user