mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-22 13:22:24 -06:00
Merge v2.5 work
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from netbox.admin import admin_site
|
||||
from utilities.forms import LaxURLField
|
||||
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction, Webhook
|
||||
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, Webhook
|
||||
|
||||
|
||||
def order_content_types(field):
|
||||
@@ -31,7 +28,7 @@ class WebhookForm(forms.ModelForm):
|
||||
exclude = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(WebhookForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
order_content_types(self.fields['obj_type'])
|
||||
|
||||
@@ -59,7 +56,7 @@ class CustomFieldForm(forms.ModelForm):
|
||||
exclude = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomFieldForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
order_content_types(self.fields['obj_type'])
|
||||
|
||||
@@ -99,7 +96,7 @@ class ExportTemplateForm(forms.ModelForm):
|
||||
exclude = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ExportTemplateForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Format ContentType choices
|
||||
order_content_types(self.fields['content_type'])
|
||||
@@ -122,16 +119,3 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
#
|
||||
|
||||
@admin.register(UserAction, site=admin_site)
|
||||
class UserActionAdmin(admin.ModelAdmin):
|
||||
actions = None
|
||||
list_display = ['user', 'action', 'content_type', 'object_id', '_message']
|
||||
|
||||
def _message(self, obj):
|
||||
return mark_safe(obj.message)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -107,7 +105,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
custom_fields[cfv.field.name] = cfv.value
|
||||
instance.custom_fields = custom_fields
|
||||
|
||||
super(CustomFieldModelSerializer, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.instance is not None:
|
||||
|
||||
@@ -139,7 +137,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
instance = super(CustomFieldModelSerializer, self).create(validated_data)
|
||||
instance = super().create(validated_data)
|
||||
|
||||
# Save custom fields
|
||||
if custom_fields is not None:
|
||||
@@ -154,7 +152,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
instance = super(CustomFieldModelSerializer, self).update(instance, validated_data)
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
# Save custom fields
|
||||
if custom_fields is not None:
|
||||
|
||||
23
netbox/extras/api/nested_serializers.py
Normal file
23
netbox/extras/api/nested_serializers.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from extras.models import ReportResult
|
||||
|
||||
__all__ = [
|
||||
'NestedReportResultSerializer',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Reports
|
||||
#
|
||||
|
||||
class NestedReportResultSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='extras-api:report-detail',
|
||||
lookup_field='report',
|
||||
lookup_url_kwarg='pk'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ReportResult
|
||||
fields = ['url', 'created', 'user', 'failed']
|
||||
@@ -1,24 +1,23 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from rest_framework import serializers
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.api.serializers import (
|
||||
from dcim.api.nested_serializers import (
|
||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
|
||||
NestedRegionSerializer, NestedSiteSerializer,
|
||||
)
|
||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
|
||||
from extras.models import (
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, UserAction,
|
||||
)
|
||||
from extras.constants import *
|
||||
from tenancy.api.serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from extras.models import (
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||
)
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from utilities.api import (
|
||||
ChoiceField, ContentTypeField, get_serializer_for_model, SerializedPKRelatedField, ValidatedModelSerializer,
|
||||
)
|
||||
from .nested_serializers import *
|
||||
|
||||
|
||||
#
|
||||
@@ -109,7 +108,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
)
|
||||
|
||||
# Enforce model validation
|
||||
super(ImageAttachmentSerializer, self).validate(data)
|
||||
super().validate(data)
|
||||
|
||||
return data
|
||||
|
||||
@@ -189,18 +188,6 @@ class ReportResultSerializer(serializers.ModelSerializer):
|
||||
fields = ['created', 'user', 'failed', 'data']
|
||||
|
||||
|
||||
class NestedReportResultSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='extras-api:report-detail',
|
||||
lookup_field='report',
|
||||
lookup_url_kwarg='pk'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ReportResult
|
||||
fields = ['url', 'created', 'user', 'failed']
|
||||
|
||||
|
||||
class ReportSerializer(serializers.Serializer):
|
||||
module = serializers.CharField(max_length=255)
|
||||
name = serializers.CharField(max_length=255)
|
||||
@@ -240,16 +227,3 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
|
||||
context = {'request': self.context['request']}
|
||||
data = serializer(obj.changed_object, context=context).data
|
||||
return data
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
#
|
||||
|
||||
class UserActionSerializer(serializers.ModelSerializer):
|
||||
user = NestedUserSerializer()
|
||||
action = ChoiceField(choices=ACTION_CHOICES)
|
||||
|
||||
class Meta:
|
||||
model = UserAction
|
||||
fields = ['id', 'time', 'user', 'action', 'message']
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
@@ -17,7 +15,7 @@ router = routers.DefaultRouter()
|
||||
router.APIRootView = ExtrasRootView
|
||||
|
||||
# Field choices
|
||||
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, base_name='field-choice')
|
||||
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# Graphs
|
||||
router.register(r'graphs', views.GraphViewSet)
|
||||
@@ -38,13 +36,10 @@ router.register(r'image-attachments', views.ImageAttachmentViewSet)
|
||||
router.register(r'config-contexts', views.ConfigContextViewSet)
|
||||
|
||||
# Reports
|
||||
router.register(r'reports', views.ReportViewSet, base_name='report')
|
||||
router.register(r'reports', views.ReportViewSet, basename='report')
|
||||
|
||||
# Change logging
|
||||
router.register(r'object-changes', views.ObjectChangeViewSet)
|
||||
|
||||
# Recent activity
|
||||
router.register(r'recent-activity', views.RecentActivityViewSet)
|
||||
|
||||
app_name = 'extras-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Count
|
||||
from django.http import Http404, HttpResponse
|
||||
@@ -13,7 +11,6 @@ from taggit.models import Tag
|
||||
from extras import filters
|
||||
from extras.models import (
|
||||
ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||
UserAction,
|
||||
)
|
||||
from extras.reports import get_report, get_reports
|
||||
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
||||
@@ -53,7 +50,7 @@ class CustomFieldModelViewSet(ModelViewSet):
|
||||
custom_field_choices[cfc.id] = cfc.value
|
||||
custom_field_choices = custom_field_choices
|
||||
|
||||
context = super(CustomFieldModelViewSet, self).get_serializer_context()
|
||||
context = super().get_serializer_context()
|
||||
context.update({
|
||||
'custom_fields': custom_fields,
|
||||
'custom_field_choices': custom_field_choices,
|
||||
@@ -62,7 +59,7 @@ class CustomFieldModelViewSet(ModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
# Prefetch custom field values
|
||||
return super(CustomFieldModelViewSet, self).get_queryset().prefetch_related('custom_field_values__field')
|
||||
return super().get_queryset().prefetch_related('custom_field_values__field')
|
||||
|
||||
|
||||
#
|
||||
@@ -72,7 +69,7 @@ class CustomFieldModelViewSet(ModelViewSet):
|
||||
class GraphViewSet(ModelViewSet):
|
||||
queryset = Graph.objects.all()
|
||||
serializer_class = serializers.GraphSerializer
|
||||
filter_class = filters.GraphFilter
|
||||
filterset_class = filters.GraphFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -82,7 +79,7 @@ class GraphViewSet(ModelViewSet):
|
||||
class ExportTemplateViewSet(ModelViewSet):
|
||||
queryset = ExportTemplate.objects.all()
|
||||
serializer_class = serializers.ExportTemplateSerializer
|
||||
filter_class = filters.ExportTemplateFilter
|
||||
filterset_class = filters.ExportTemplateFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -92,7 +89,7 @@ class ExportTemplateViewSet(ModelViewSet):
|
||||
class TopologyMapViewSet(ModelViewSet):
|
||||
queryset = TopologyMap.objects.select_related('site')
|
||||
serializer_class = serializers.TopologyMapSerializer
|
||||
filter_class = filters.TopologyMapFilter
|
||||
filterset_class = filters.TopologyMapFilter
|
||||
|
||||
@action(detail=True)
|
||||
def render(self, request, pk):
|
||||
@@ -121,7 +118,7 @@ class TopologyMapViewSet(ModelViewSet):
|
||||
class TagViewSet(ModelViewSet):
|
||||
queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items'))
|
||||
serializer_class = serializers.TagSerializer
|
||||
filter_class = filters.TagFilter
|
||||
filterset_class = filters.TagFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -142,7 +139,7 @@ class ConfigContextViewSet(ModelViewSet):
|
||||
'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
||||
)
|
||||
serializer_class = serializers.ConfigContextSerializer
|
||||
filter_class = filters.ConfigContextFilter
|
||||
filterset_class = filters.ConfigContextFilter
|
||||
|
||||
|
||||
#
|
||||
@@ -231,17 +228,4 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
queryset = ObjectChange.objects.select_related('user')
|
||||
serializer_class = serializers.ObjectChangeSerializer
|
||||
filter_class = filters.ObjectChangeFilter
|
||||
|
||||
|
||||
#
|
||||
# User activity
|
||||
#
|
||||
|
||||
class RecentActivityViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
DEPRECATED: List all UserActions to provide a log of recent activity.
|
||||
"""
|
||||
queryset = UserAction.objects.all()
|
||||
serializer_class = serializers.UserActionSerializer
|
||||
filter_class = filters.UserActionFilter
|
||||
filterset_class = filters.ObjectChangeFilter
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
class ExtrasConfig(AppConfig):
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
# Models which support custom fields
|
||||
CUSTOMFIELD_MODELS = (
|
||||
@@ -51,7 +49,7 @@ GRAPH_TYPE_CHOICES = (
|
||||
EXPORTTEMPLATE_MODELS = [
|
||||
'provider', 'circuit', # Circuits
|
||||
'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', # DCIM
|
||||
'consoleport', 'powerport', 'interfaceconnection', 'virtualchassis', # DCIM
|
||||
'consoleport', 'powerport', 'interface', 'cable', 'virtualchassis', # DCIM
|
||||
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
|
||||
'secret', # Secrets
|
||||
'tenant', # Tenancy
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from taggit.models import Tag
|
||||
@@ -9,7 +6,7 @@ from taggit.models import Tag
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
|
||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap
|
||||
|
||||
|
||||
class CustomFieldFilter(django_filters.Filter):
|
||||
@@ -20,12 +17,12 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
def __init__(self, custom_field, *args, **kwargs):
|
||||
self.cf_type = custom_field.type
|
||||
self.filter_logic = custom_field.filter_logic
|
||||
super(CustomFieldFilter, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, queryset, value):
|
||||
|
||||
# Skip filter on empty value
|
||||
if not value.strip():
|
||||
if value is None or not value.strip():
|
||||
return queryset
|
||||
|
||||
# Selection fields get special treatment (values must be integers)
|
||||
@@ -66,12 +63,12 @@ class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomFieldFilterSet, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||
custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)
|
||||
for cf in custom_fields:
|
||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(name=cf.name, custom_field=cf)
|
||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
|
||||
|
||||
|
||||
class GraphFilter(django_filters.FilterSet):
|
||||
@@ -109,12 +106,12 @@ class TagFilter(django_filters.FilterSet):
|
||||
|
||||
class TopologyMapFilter(django_filters.FilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
field_name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site__slug',
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@@ -131,67 +128,67 @@ class ConfigContextFilter(django_filters.FilterSet):
|
||||
label='Search',
|
||||
)
|
||||
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='regions',
|
||||
field_name='regions',
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
)
|
||||
region = django_filters.ModelMultipleChoiceFilter(
|
||||
name='regions__slug',
|
||||
field_name='regions__slug',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='sites',
|
||||
field_name='sites',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='sites__slug',
|
||||
field_name='sites__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='roles',
|
||||
field_name='roles',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='roles__slug',
|
||||
field_name='roles__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platforms',
|
||||
field_name='platforms',
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform',
|
||||
)
|
||||
platform = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platforms__slug',
|
||||
field_name='platforms__slug',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
tenant_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant_groups',
|
||||
field_name='tenant_groups',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
label='Tenant group',
|
||||
)
|
||||
tenant_group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant_groups__slug',
|
||||
field_name='tenant_groups__slug',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant group (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenants',
|
||||
field_name='tenants',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenants__slug',
|
||||
field_name='tenants__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
@@ -229,15 +226,3 @@ class ObjectChangeFilter(django_filters.FilterSet):
|
||||
Q(user_name__icontains=value) |
|
||||
Q(object_repr__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class UserActionFilter(django_filters.FilterSet):
|
||||
username = django_filters.ModelMultipleChoiceFilter(
|
||||
name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = UserAction
|
||||
fields = ['user']
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
@@ -104,7 +102,7 @@ class CustomFieldForm(forms.ModelForm):
|
||||
self.custom_fields = []
|
||||
self.obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||
|
||||
super(CustomFieldForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Add all applicable CustomFields to the form
|
||||
custom_fields = []
|
||||
@@ -140,7 +138,7 @@ class CustomFieldForm(forms.ModelForm):
|
||||
cfv.save()
|
||||
|
||||
def save(self, commit=True):
|
||||
obj = super(CustomFieldForm, self).save(commit)
|
||||
obj = super().save(commit)
|
||||
|
||||
# Handle custom fields the same way we do M2M fields
|
||||
if commit:
|
||||
@@ -154,7 +152,7 @@ class CustomFieldForm(forms.ModelForm):
|
||||
class CustomFieldBulkEditForm(BulkEditForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomFieldBulkEditForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.custom_fields = []
|
||||
self.obj_type = ContentType.objects.get_for_model(self.model)
|
||||
@@ -177,7 +175,7 @@ class CustomFieldFilterForm(forms.Form):
|
||||
|
||||
self.obj_type = ContentType.objects.get_for_model(self.model)
|
||||
|
||||
super(CustomFieldFilterForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Add all applicable CustomFields to the form
|
||||
custom_fields = get_custom_fields_for_model(self.obj_type, filterable_only=True).items()
|
||||
@@ -195,13 +193,15 @@ class TagForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['name', 'slug']
|
||||
fields = [
|
||||
'name', 'slug',
|
||||
]
|
||||
|
||||
|
||||
class AddRemoveTagsForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AddRemoveTagsForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Add add/remove tags fields
|
||||
self.fields['add_tags'] = TagField(required=False)
|
||||
@@ -210,7 +210,10 @@ class AddRemoveTagsForm(forms.Form):
|
||||
|
||||
class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
model = Tag
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -251,7 +254,9 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
nullable_fields = [
|
||||
'description',
|
||||
]
|
||||
|
||||
|
||||
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
@@ -293,7 +298,9 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = ['name', 'image']
|
||||
fields = [
|
||||
'name', 'image',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import code
|
||||
import platform
|
||||
import sys
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from getpass import getpass
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from ncclient.transport.errors import AuthenticationError
|
||||
from paramiko import AuthenticationException
|
||||
|
||||
from dcim.models import DEVICE_STATUS_ACTIVE, Device, InventoryItem, Site
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Update inventory information for specified devices"
|
||||
username = settings.NAPALM_USERNAME
|
||||
password = settings.NAPALM_PASSWORD
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('-u', '--username', dest='username', help="Specify the username to use")
|
||||
parser.add_argument('-p', '--password', action='store_true', default=False, help="Prompt for password to use")
|
||||
parser.add_argument('-s', '--site', dest='site', action='append',
|
||||
help="Filter devices by site (include argument once per site)")
|
||||
parser.add_argument('-n', '--name', dest='name', help="Filter devices by name (regular expression)")
|
||||
parser.add_argument('--full', action='store_true', default=False, help="For inventory update for all devices")
|
||||
parser.add_argument('--fake', action='store_true', default=False, help="Do not actually update database")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
def create_inventory_items(inventory_items, parent=None):
|
||||
for item in inventory_items:
|
||||
i = InventoryItem(device=device, parent=parent, name=item['name'], part_id=item['part_id'],
|
||||
serial=item['serial'], discovered=True)
|
||||
i.save()
|
||||
create_inventory_items(item.get('items', []), parent=i)
|
||||
|
||||
# Credentials
|
||||
if options['username']:
|
||||
self.username = options['username']
|
||||
if options['password']:
|
||||
self.password = getpass("Password: ")
|
||||
|
||||
# Attempt to inventory only active devices
|
||||
device_list = Device.objects.filter(status=DEVICE_STATUS_ACTIVE)
|
||||
|
||||
# --site: Include only devices belonging to specified site(s)
|
||||
if options['site']:
|
||||
sites = Site.objects.filter(slug__in=options['site'])
|
||||
if sites:
|
||||
site_names = [s.name for s in sites]
|
||||
self.stdout.write("Running inventory for these sites: {}".format(', '.join(site_names)))
|
||||
else:
|
||||
raise CommandError("One or more sites specified but none found.")
|
||||
device_list = device_list.filter(site__in=sites)
|
||||
|
||||
# --name: Filter devices by name matching a regex
|
||||
if options['name']:
|
||||
device_list = device_list.filter(name__iregex=options['name'])
|
||||
|
||||
# --full: Gather inventory data for *all* devices
|
||||
if options['full']:
|
||||
self.stdout.write("WARNING: Running inventory for all devices! Prior data will be overwritten. (--full)")
|
||||
|
||||
# --fake: Gathering data but not updating the database
|
||||
if options['fake']:
|
||||
self.stdout.write("WARNING: Inventory data will not be saved! (--fake)")
|
||||
|
||||
device_count = device_list.count()
|
||||
self.stdout.write("** Found {} devices...".format(device_count))
|
||||
|
||||
for i, device in enumerate(device_list, start=1):
|
||||
|
||||
self.stdout.write("[{}/{}] {}: ".format(i, device_count, device.name), ending='')
|
||||
|
||||
# Skip inactive devices
|
||||
if not device.status:
|
||||
self.stdout.write("Skipped (not active)")
|
||||
continue
|
||||
|
||||
# Skip devices without primary_ip set
|
||||
if not device.primary_ip:
|
||||
self.stdout.write("Skipped (no primary IP set)")
|
||||
continue
|
||||
|
||||
# Skip devices which have already been inventoried if not doing a full update
|
||||
if device.serial and not options['full']:
|
||||
self.stdout.write("Skipped (Serial: {})".format(device.serial))
|
||||
continue
|
||||
|
||||
RPC = device.get_rpc_client()
|
||||
if not RPC:
|
||||
self.stdout.write("Skipped (no RPC client available for platform {})".format(device.platform))
|
||||
continue
|
||||
|
||||
# Connect to device and retrieve inventory info
|
||||
try:
|
||||
with RPC(device, self.username, self.password) as rpc_client:
|
||||
inventory = rpc_client.get_inventory()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except (AuthenticationError, AuthenticationException):
|
||||
self.stdout.write("Authentication error!")
|
||||
continue
|
||||
except Exception as e:
|
||||
self.stdout.write("Error: {}".format(e))
|
||||
continue
|
||||
|
||||
if options['verbosity'] > 1:
|
||||
self.stdout.write("")
|
||||
self.stdout.write("\tSerial: {}".format(inventory['chassis']['serial']))
|
||||
self.stdout.write("\tDescription: {}".format(inventory['chassis']['description']))
|
||||
for item in inventory['items']:
|
||||
self.stdout.write("\tItem: {} / {} ({})".format(item['name'], item['part_id'],
|
||||
item['serial']))
|
||||
else:
|
||||
self.stdout.write("{} ({})".format(inventory['chassis']['description'], inventory['chassis']['serial']))
|
||||
|
||||
if not options['fake']:
|
||||
with transaction.atomic():
|
||||
# Update device serial
|
||||
if device.serial != inventory['chassis']['serial']:
|
||||
device.serial = inventory['chassis']['serial']
|
||||
device.save()
|
||||
InventoryItem.objects.filter(device=device, discovered=True).delete()
|
||||
create_inventory_items(inventory.get('items', []))
|
||||
|
||||
self.stdout.write("Finished!")
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import timedelta
|
||||
import random
|
||||
import threading
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
@@ -16,7 +14,6 @@ from .constants import (
|
||||
)
|
||||
from .models import ObjectChange
|
||||
|
||||
|
||||
_thread_locals = threading.local()
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-06-22 18:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.14 on 2018-07-31 02:19
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-08-23 20:33
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-09-27 20:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-11-03 18:33
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-04-04 19:45
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-04-04 19:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import extras.models
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import extras.models
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-09-26 21:25
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-02-15 16:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-02-21 19:48
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.12 on 2018-05-30 17:55
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.12 on 2018-06-22 18:13
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
|
||||
24
netbox/extras/migrations/0015_remove_useraction.py
Normal file
24
netbox/extras/migrations/0015_remove_useraction.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 2.0.8 on 2018-08-14 16:10
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0014_configcontexts'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='useraction',
|
||||
name='content_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='useraction',
|
||||
name='user',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='UserAction',
|
||||
),
|
||||
]
|
||||
19
netbox/extras/migrations/0016_exporttemplate_add_cable.py
Normal file
19
netbox/extras/migrations/0016_exporttemplate_add_cable.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.1.3 on 2018-11-07 20:46
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0015_remove_useraction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='exporttemplate',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interface', 'cable', 'virtualchassis', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
|
||||
@@ -10,12 +8,10 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models import F, Q
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CONNECTED
|
||||
from utilities.utils import deepmerge, foreground_color
|
||||
@@ -27,7 +23,6 @@ from .querysets import ConfigContextQuerySet
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Webhook(models.Model):
|
||||
"""
|
||||
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
|
||||
@@ -136,7 +131,6 @@ class CustomFieldModel(models.Model):
|
||||
return OrderedDict([(field, None) for field in fields])
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CustomField(models.Model):
|
||||
obj_type = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
@@ -227,7 +221,6 @@ class CustomField(models.Model):
|
||||
return serialized_value
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CustomFieldValue(models.Model):
|
||||
field = models.ForeignKey(
|
||||
to='extras.CustomField',
|
||||
@@ -268,10 +261,9 @@ class CustomFieldValue(models.Model):
|
||||
if self.pk and self.value is None:
|
||||
self.delete()
|
||||
else:
|
||||
super(CustomFieldValue, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CustomFieldChoice(models.Model):
|
||||
field = models.ForeignKey(
|
||||
to='extras.CustomField',
|
||||
@@ -301,7 +293,7 @@ class CustomFieldChoice(models.Model):
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
|
||||
pk = self.pk
|
||||
super(CustomFieldChoice, self).delete(using, keep_parents)
|
||||
super().delete(using, keep_parents)
|
||||
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
|
||||
|
||||
|
||||
@@ -309,7 +301,6 @@ class CustomFieldChoice(models.Model):
|
||||
# Graphs
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Graph(models.Model):
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=GRAPH_TYPE_CHOICES
|
||||
@@ -351,7 +342,6 @@ class Graph(models.Model):
|
||||
# Export templates
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ExportTemplate(models.Model):
|
||||
content_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
@@ -410,7 +400,6 @@ class ExportTemplate(models.Model):
|
||||
# Topology maps
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class TopologyMap(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
@@ -515,18 +504,22 @@ class TopologyMap(models.Model):
|
||||
def add_network_connections(self, devices):
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.models import InterfaceConnection
|
||||
from dcim.models import Interface
|
||||
|
||||
# Add all interface connections to the graph
|
||||
connections = InterfaceConnection.objects.filter(
|
||||
interface_a__device__in=devices, interface_b__device__in=devices
|
||||
connected_interfaces = Interface.objects.select_related(
|
||||
'_connected_interface__device'
|
||||
).filter(
|
||||
Q(device__in=devices) | Q(_connected_interface__device__in=devices),
|
||||
_connected_interface__isnull=False,
|
||||
pk__lt=F('_connected_interface')
|
||||
)
|
||||
for c in connections:
|
||||
style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
|
||||
for interface in connected_interfaces:
|
||||
style = 'solid' if interface.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(interface.device.name, interface.connected_endpoint.device.name, style=style)
|
||||
|
||||
# Add all circuits to the graph
|
||||
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
|
||||
for termination in CircuitTermination.objects.filter(term_side='A', connected_endpoint__device__in=devices):
|
||||
peer_termination = termination.get_peer_termination()
|
||||
if (peer_termination is not None and peer_termination.interface is not None and
|
||||
peer_termination.interface.device in devices):
|
||||
@@ -537,20 +530,18 @@ class TopologyMap(models.Model):
|
||||
from dcim.models import ConsolePort
|
||||
|
||||
# Add all console connections to the graph
|
||||
console_ports = ConsolePort.objects.filter(device__in=devices, cs_port__device__in=devices)
|
||||
for cp in console_ports:
|
||||
for cp in ConsolePort.objects.filter(device__in=devices, connected_endpoint__device__in=devices):
|
||||
style = 'solid' if cp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(cp.cs_port.device.name, cp.device.name, style=style)
|
||||
self.graph.edge(cp.connected_endpoint.device.name, cp.device.name, style=style)
|
||||
|
||||
def add_power_connections(self, devices):
|
||||
|
||||
from dcim.models import PowerPort
|
||||
|
||||
# Add all power connections to the graph
|
||||
power_ports = PowerPort.objects.filter(device__in=devices, power_outlet__device__in=devices)
|
||||
for pp in power_ports:
|
||||
for pp in PowerPort.objects.filter(device__in=devices, connected_endpoint__device__in=devices):
|
||||
style = 'solid' if pp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(pp.power_outlet.device.name, pp.device.name, style=style)
|
||||
self.graph.edge(pp.connected_endpoint.device.name, pp.device.name, style=style)
|
||||
|
||||
|
||||
#
|
||||
@@ -571,7 +562,6 @@ def image_upload(instance, filename):
|
||||
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ImageAttachment(models.Model):
|
||||
"""
|
||||
An uploaded image which is associated with an object.
|
||||
@@ -613,7 +603,7 @@ class ImageAttachment(models.Model):
|
||||
|
||||
_name = self.image.name
|
||||
|
||||
super(ImageAttachment, self).delete(*args, **kwargs)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
# Delete file from disk
|
||||
self.image.delete(save=False)
|
||||
@@ -769,7 +759,6 @@ class ReportResult(models.Model):
|
||||
# Change logging
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ObjectChange(models.Model):
|
||||
"""
|
||||
Record a change to an object and the user account associated with that change. A change record may optionally
|
||||
@@ -852,7 +841,7 @@ class ObjectChange(models.Model):
|
||||
self.user_name = self.user.username
|
||||
self.object_repr = str(self.changed_object)
|
||||
|
||||
return super(ObjectChange, self).save(*args, **kwargs)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:objectchange', args=[self.pk])
|
||||
@@ -871,101 +860,3 @@ class ObjectChange(models.Model):
|
||||
self.object_repr,
|
||||
self.object_data,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
#
|
||||
|
||||
class UserActionManager(models.Manager):
|
||||
|
||||
# Actions affecting a single object
|
||||
def log_action(self, user, obj, action, message):
|
||||
self.model.objects.create(
|
||||
content_type=ContentType.objects.get_for_model(obj),
|
||||
object_id=obj.pk,
|
||||
user=user,
|
||||
action=action,
|
||||
message=message,
|
||||
)
|
||||
|
||||
def log_create(self, user, obj, message=''):
|
||||
self.log_action(user, obj, ACTION_CREATE, message)
|
||||
|
||||
def log_edit(self, user, obj, message=''):
|
||||
self.log_action(user, obj, ACTION_EDIT, message)
|
||||
|
||||
def log_delete(self, user, obj, message=''):
|
||||
self.log_action(user, obj, ACTION_DELETE, message)
|
||||
|
||||
# Actions affecting multiple objects
|
||||
def log_bulk_action(self, user, content_type, action, message):
|
||||
self.model.objects.create(
|
||||
content_type=content_type,
|
||||
user=user,
|
||||
action=action,
|
||||
message=message,
|
||||
)
|
||||
|
||||
def log_import(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_IMPORT, message)
|
||||
|
||||
def log_bulk_create(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_BULK_CREATE, message)
|
||||
|
||||
def log_bulk_edit(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_BULK_EDIT, message)
|
||||
|
||||
def log_bulk_delete(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message)
|
||||
|
||||
|
||||
# TODO: Remove UserAction, which has been replaced by ObjectChange.
|
||||
@python_2_unicode_compatible
|
||||
class UserAction(models.Model):
|
||||
"""
|
||||
DEPRECATED: A record of an action (add, edit, or delete) performed on an object by a User.
|
||||
"""
|
||||
time = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
editable=False
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
to=User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='actions'
|
||||
)
|
||||
content_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
object_id = models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
action = models.PositiveSmallIntegerField(
|
||||
choices=ACTION_CHOICES
|
||||
)
|
||||
message = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = UserActionManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['-time']
|
||||
|
||||
def __str__(self):
|
||||
if self.message:
|
||||
return '{} {}'.format(self.user, self.message)
|
||||
return '{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
||||
|
||||
def icon(self):
|
||||
if self.action in [ACTION_CREATE, ACTION_BULK_CREATE, ACTION_IMPORT]:
|
||||
return mark_safe('<i class="glyphicon glyphicon-plus text-success"></i>')
|
||||
elif self.action in [ACTION_EDIT, ACTION_BULK_EDIT]:
|
||||
return mark_safe('<i class="glyphicon glyphicon-pencil text-warning"></i>')
|
||||
elif self.action in [ACTION_DELETE, ACTION_BULK_DELETE]:
|
||||
return mark_safe('<i class="glyphicon glyphicon-remove text-danger"></i>')
|
||||
else:
|
||||
return ''
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import Q, QuerySet
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
@@ -26,22 +23,12 @@ def get_report(module_name, report_name):
|
||||
"""
|
||||
file_path = '{}/{}.py'.format(settings.REPORTS_ROOT, module_name)
|
||||
|
||||
# Python 3.5+
|
||||
if sys.version_info >= (3, 5):
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
# Python 2.7
|
||||
else:
|
||||
import imp
|
||||
try:
|
||||
module = imp.load_source(module_name, file_path)
|
||||
except IOError:
|
||||
return None
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
report = getattr(module, report_name, None)
|
||||
if report is None:
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
import paramiko
|
||||
import xmltodict
|
||||
from ncclient import manager
|
||||
|
||||
CONNECT_TIMEOUT = 5 # seconds
|
||||
|
||||
|
||||
class RPCClient(object):
|
||||
|
||||
def __init__(self, device, username='', password=''):
|
||||
self.username = username
|
||||
self.password = password
|
||||
try:
|
||||
self.host = str(device.primary_ip.address.ip)
|
||||
except AttributeError:
|
||||
raise Exception("Specified device ({}) does not have a primary IP defined.".format(device))
|
||||
|
||||
def get_inventory(self):
|
||||
"""
|
||||
Returns a dictionary representing the device chassis and installed inventory items.
|
||||
|
||||
{
|
||||
'chassis': {
|
||||
'serial': <str>,
|
||||
'description': <str>,
|
||||
}
|
||||
'items': [
|
||||
{
|
||||
'name': <str>,
|
||||
'part_id': <str>,
|
||||
'serial': <str>,
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
raise NotImplementedError("Feature not implemented for this platform.")
|
||||
|
||||
|
||||
class SSHClient(RPCClient):
|
||||
def __enter__(self):
|
||||
|
||||
self.ssh = paramiko.SSHClient()
|
||||
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
self.ssh.connect(
|
||||
self.host,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
timeout=CONNECT_TIMEOUT,
|
||||
allow_agent=False,
|
||||
look_for_keys=False,
|
||||
)
|
||||
except paramiko.AuthenticationException:
|
||||
# Try default credentials if the configured creds don't work
|
||||
try:
|
||||
default_creds = self.default_credentials
|
||||
if default_creds.get('username') and default_creds.get('password'):
|
||||
self.ssh.connect(
|
||||
self.host,
|
||||
username=default_creds['username'],
|
||||
password=default_creds['password'],
|
||||
timeout=CONNECT_TIMEOUT,
|
||||
allow_agent=False,
|
||||
look_for_keys=False,
|
||||
)
|
||||
else:
|
||||
raise ValueError('default_credentials are incomplete.')
|
||||
except AttributeError:
|
||||
raise paramiko.AuthenticationException
|
||||
|
||||
self.session = self.ssh.invoke_shell()
|
||||
self.session.recv(1000)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.ssh.close()
|
||||
|
||||
def _send(self, cmd, pause=1):
|
||||
self.session.send('{}\n'.format(cmd))
|
||||
data = ''
|
||||
time.sleep(pause)
|
||||
while self.session.recv_ready():
|
||||
data += self.session.recv(4096).decode()
|
||||
if not data:
|
||||
break
|
||||
return data
|
||||
|
||||
|
||||
class JunosNC(RPCClient):
|
||||
"""
|
||||
NETCONF client for Juniper Junos devices
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
# Initiate a connection to the device
|
||||
self.manager = manager.connect(host=self.host, username=self.username, password=self.password,
|
||||
hostkey_verify=False, timeout=CONNECT_TIMEOUT)
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
|
||||
# Close the connection to the device
|
||||
self.manager.close_session()
|
||||
|
||||
def get_inventory(self):
|
||||
|
||||
def glean_items(node, depth=0):
|
||||
items = []
|
||||
items_list = node.get('chassis{}-module'.format('-sub' * depth), [])
|
||||
# Junos like to return single children directly instead of as a single-item list
|
||||
if hasattr(items_list, 'items'):
|
||||
items_list = [items_list]
|
||||
for item in items_list:
|
||||
m = {
|
||||
'name': item['name'],
|
||||
'part_id': item.get('model-number') or item.get('part-number', ''),
|
||||
'serial': item.get('serial-number', ''),
|
||||
}
|
||||
child_items = glean_items(item, depth + 1)
|
||||
if child_items:
|
||||
m['items'] = child_items
|
||||
items.append(m)
|
||||
return items
|
||||
|
||||
rpc_reply = self.manager.dispatch('get-chassis-inventory')
|
||||
inventory_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['chassis-inventory']['chassis']
|
||||
|
||||
result = dict()
|
||||
|
||||
# Gather chassis data
|
||||
result['chassis'] = {
|
||||
'serial': inventory_raw['serial-number'],
|
||||
'description': inventory_raw['description'],
|
||||
}
|
||||
|
||||
# Gather inventory items
|
||||
result['items'] = glean_items(inventory_raw)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class IOSSSH(SSHClient):
|
||||
"""
|
||||
SSH client for Cisco IOS devices
|
||||
"""
|
||||
|
||||
def get_inventory(self):
|
||||
def version():
|
||||
|
||||
def parse(cmd_out, rex):
|
||||
for i in cmd_out:
|
||||
match = re.search(rex, i)
|
||||
if match:
|
||||
return match.groups()[0]
|
||||
|
||||
sh_ver = self._send('show version').split('\r\n')
|
||||
return {
|
||||
'serial': parse(sh_ver, r'Processor board ID ([^\s]+)'),
|
||||
'description': parse(sh_ver, r'cisco ([^\s]+)')
|
||||
}
|
||||
|
||||
def items(chassis_serial=None):
|
||||
cmd = self._send('show inventory').split('\r\n\r\n')
|
||||
for i in cmd:
|
||||
i_fmt = i.replace('\r\n', ' ')
|
||||
try:
|
||||
m_name = re.search(r'NAME: "([^"]+)"', i_fmt).group(1)
|
||||
m_pid = re.search(r'PID: ([^\s]+)', i_fmt).group(1)
|
||||
m_serial = re.search(r'SN: ([^\s]+)', i_fmt).group(1)
|
||||
# Omit built-in items and those with no PID
|
||||
if m_serial != chassis_serial and m_pid.lower() != 'unspecified':
|
||||
yield {
|
||||
'name': m_name,
|
||||
'part_id': m_pid,
|
||||
'serial': m_serial,
|
||||
}
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
self._send('term length 0')
|
||||
sh_version = version()
|
||||
|
||||
return {
|
||||
'chassis': sh_version,
|
||||
'items': list(items(chassis_serial=sh_version.get('serial')))
|
||||
}
|
||||
|
||||
|
||||
class OpengearSSH(SSHClient):
|
||||
"""
|
||||
SSH client for Opengear devices
|
||||
"""
|
||||
default_credentials = {
|
||||
'username': 'root',
|
||||
'password': 'default',
|
||||
}
|
||||
|
||||
def get_inventory(self):
|
||||
|
||||
try:
|
||||
stdin, stdout, stderr = self.ssh.exec_command("showserial")
|
||||
serial = stdout.readlines()[0].strip()
|
||||
except Exception:
|
||||
raise RuntimeError("Failed to glean chassis serial from device.")
|
||||
# Older models don't provide serial info
|
||||
if serial == "No serial number information available":
|
||||
serial = ''
|
||||
|
||||
try:
|
||||
stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model")
|
||||
description = stdout.readlines()[0].split(' ', 1)[1].strip()
|
||||
except Exception:
|
||||
raise RuntimeError("Failed to glean chassis description from device.")
|
||||
|
||||
return {
|
||||
'chassis': {
|
||||
'serial': serial,
|
||||
'description': description,
|
||||
},
|
||||
'items': [],
|
||||
}
|
||||
|
||||
|
||||
# For mapping platform -> NC client
|
||||
RPC_CLIENTS = {
|
||||
'juniper-junos': JunosNC,
|
||||
'cisco-ios': IOSSSH,
|
||||
'opengear': OpengearSSH,
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
from taggit.models import Tag, TaggedItem
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
@@ -16,7 +14,7 @@ class GraphTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(GraphTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.graph1 = Graph.objects.create(
|
||||
type=GRAPH_TYPE_SITE, name='Test Graph 1', source='http://example.com/graphs.py?site={{ obj.name }}&foo=1'
|
||||
@@ -120,7 +118,7 @@ class ExportTemplateTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ExportTemplateTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.content_type = ContentType.objects.get_for_model(Device)
|
||||
self.exporttemplate1 = ExportTemplate.objects.create(
|
||||
@@ -227,7 +225,7 @@ class TagTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(TagTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.tag1 = Tag.objects.create(name='Test Tag 1', slug='test-tag-1')
|
||||
self.tag2 = Tag.objects.create(name='Test Tag 2', slug='test-tag-2')
|
||||
@@ -318,7 +316,7 @@ class ConfigContextTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(ConfigContextTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
self.configcontext1 = ConfigContext.objects.create(
|
||||
name='Test Config Context 1',
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import date
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -103,7 +101,7 @@ class CustomFieldAPITest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(CustomFieldAPITest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Site)
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
@@ -14,7 +12,7 @@ class TaggedItemTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(TaggedItemTest, self).setUp()
|
||||
super().setUp()
|
||||
|
||||
def test_create_tagged_item(self):
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from extras import views
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -58,7 +56,7 @@ class TagView(View):
|
||||
# Generate a table of all items tagged with this Tag
|
||||
items_table = TaggedItemTable(tagged_items)
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(items_table)
|
||||
|
||||
@@ -3,8 +3,8 @@ import datetime
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from extras.models import Webhook
|
||||
from extras.constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||
from extras.models import Webhook
|
||||
from utilities.api import get_serializer_for_model
|
||||
from .constants import WEBHOOK_MODELS
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import requests
|
||||
import json
|
||||
|
||||
import requests
|
||||
from django_rq import job
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
|
||||
Reference in New Issue
Block a user