Merge v2.5 work

This commit is contained in:
Jeremy Stretch
2018-12-07 10:51:28 -05:00
396 changed files with 8663 additions and 6511 deletions

View File

@@ -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)

View File

@@ -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:

View 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']

View File

@@ -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']

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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']

View File

@@ -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',
]
#

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
import code
import platform
import sys

View File

@@ -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!")

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.core.management.base import BaseCommand
from django.utils import timezone

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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',
),
]

View 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'),
),
]

View File

@@ -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 ''

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.db.models import Q, QuerySet

View File

@@ -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:

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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',

View File

@@ -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)

View File

@@ -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):

View File

@@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.conf.urls import url
from extras import views

View File

@@ -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)

View File

@@ -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

View File

@@ -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