mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-19 03:42:25 -06:00
Merge branch 'develop' into fix/generic_prefetch_4.2
This commit is contained in:
@@ -9,6 +9,8 @@ urlpatterns = [
|
||||
# Account views
|
||||
path('profile/', views.ProfileView.as_view(), name='profile'),
|
||||
path('bookmarks/', views.BookmarkListView.as_view(), name='bookmarks'),
|
||||
path('notifications/', views.NotificationListView.as_view(), name='notifications'),
|
||||
path('subscriptions/', views.SubscriptionListView.as_view(), name='subscriptions'),
|
||||
path('preferences/', views.UserConfigView.as_view(), name='preferences'),
|
||||
path('password/', views.ChangePasswordView.as_view(), name='change_password'),
|
||||
path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'),
|
||||
|
||||
@@ -19,8 +19,10 @@ from django.views.generic import View
|
||||
from social_core.backends.utils import load_backends
|
||||
|
||||
from account.models import UserToken
|
||||
from extras.models import Bookmark, ObjectChange
|
||||
from extras.tables import BookmarkTable, ObjectChangeTable
|
||||
from core.models import ObjectChange
|
||||
from core.tables import ObjectChangeTable
|
||||
from extras.models import Bookmark
|
||||
from extras.tables import BookmarkTable, NotificationTable, SubscriptionTable
|
||||
from netbox.authentication import get_auth_backend_display, get_saml_idps
|
||||
from netbox.config import get_config
|
||||
from netbox.views import generic
|
||||
@@ -44,10 +46,20 @@ class LoginView(View):
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
def gen_auth_data(self, name, url, params):
|
||||
display_name, icon_name = get_auth_backend_display(name)
|
||||
display_name, icon_source = get_auth_backend_display(name)
|
||||
|
||||
icon_name = None
|
||||
icon_img = None
|
||||
if icon_source:
|
||||
if '://' in icon_source:
|
||||
icon_img = icon_source
|
||||
else:
|
||||
icon_name = icon_source
|
||||
|
||||
return {
|
||||
'display_name': display_name,
|
||||
'icon_name': icon_name,
|
||||
'icon_img': icon_img,
|
||||
'url': f'{url}?{urlencode(params)}',
|
||||
}
|
||||
|
||||
@@ -99,15 +111,21 @@ class LoginView(View):
|
||||
# Authenticate user
|
||||
auth_login(request, form.get_user())
|
||||
logger.info(f"User {request.user} successfully authenticated")
|
||||
messages.success(request, f"Logged in as {request.user}.")
|
||||
messages.success(request, _("Logged in as {user}.").format(user=request.user))
|
||||
|
||||
# Ensure the user has a UserConfig defined. (This should normally be handled by
|
||||
# create_userconfig() on user creation.)
|
||||
if not hasattr(request.user, 'config'):
|
||||
config = get_config()
|
||||
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save()
|
||||
request.user.config = get_config()
|
||||
UserConfig(user=request.user, data=request.user.config.DEFAULT_USER_PREFERENCES).save()
|
||||
|
||||
return self.redirect_to_next(request, logger)
|
||||
response = self.redirect_to_next(request, logger)
|
||||
|
||||
# Set the user's preferred language (if any)
|
||||
if language := request.user.config.get('locale.language'):
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
|
||||
|
||||
return response
|
||||
|
||||
else:
|
||||
logger.debug(f"Login form validation failed for username: {form['username'].value()}")
|
||||
@@ -143,11 +161,12 @@ class LogoutView(View):
|
||||
username = request.user
|
||||
auth_logout(request)
|
||||
logger.info(f"User {username} has logged out")
|
||||
messages.info(request, "You have logged out.")
|
||||
messages.info(request, _("You have logged out."))
|
||||
|
||||
# Delete session key cookie (if set) upon logout
|
||||
# Delete session key & language cookies (if set) upon logout
|
||||
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
|
||||
response.delete_cookie('session_key')
|
||||
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
|
||||
|
||||
return response
|
||||
|
||||
@@ -199,7 +218,7 @@ class UserConfigView(LoginRequiredMixin, View):
|
||||
|
||||
# Set/clear language cookie
|
||||
if language := form.cleaned_data['locale.language']:
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language)
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language, max_age=request.session.get_expiry_age())
|
||||
else:
|
||||
response.delete_cookie(settings.LANGUAGE_COOKIE_NAME)
|
||||
|
||||
@@ -217,7 +236,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
# LDAP users cannot change their password here
|
||||
if getattr(request.user, 'ldap_username', None):
|
||||
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
|
||||
messages.warning(request, _("LDAP-authenticated user credentials cannot be changed within NetBox."))
|
||||
return redirect('account:profile')
|
||||
|
||||
form = PasswordChangeForm(user=request.user)
|
||||
@@ -232,7 +251,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
update_session_auth_hash(request, form.user)
|
||||
messages.success(request, "Your password has been changed successfully.")
|
||||
messages.success(request, _("Your password has been changed successfully."))
|
||||
return redirect('account:profile')
|
||||
|
||||
return render(request, self.template_name, {
|
||||
@@ -258,6 +277,36 @@ class BookmarkListView(LoginRequiredMixin, generic.ObjectListView):
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Notifications & subscriptions
|
||||
#
|
||||
|
||||
class NotificationListView(LoginRequiredMixin, generic.ObjectListView):
|
||||
table = NotificationTable
|
||||
template_name = 'account/notifications.html'
|
||||
|
||||
def get_queryset(self, request):
|
||||
return request.user.notifications.all()
|
||||
|
||||
def get_extra_context(self, request):
|
||||
return {
|
||||
'active_tab': 'notifications',
|
||||
}
|
||||
|
||||
|
||||
class SubscriptionListView(LoginRequiredMixin, generic.ObjectListView):
|
||||
table = SubscriptionTable
|
||||
template_name = 'account/subscriptions.html'
|
||||
|
||||
def get_queryset(self, request):
|
||||
return request.user.subscriptions.all()
|
||||
|
||||
def get_extra_context(self, request):
|
||||
return {
|
||||
'active_tab': 'subscriptions',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# User views for token management
|
||||
#
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import *
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCircuitSerializer',
|
||||
'NestedCircuitTerminationSerializer',
|
||||
'NestedCircuitTypeSerializer',
|
||||
'NestedProviderNetworkSerializer',
|
||||
'NestedProviderSerializer',
|
||||
'NestedProviderAccountSerializer',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedProviderSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
#
|
||||
# Provider Accounts
|
||||
#
|
||||
|
||||
class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'url', 'display', 'name', 'account']
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
class NestedCircuitSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'url', 'display', 'cid']
|
||||
|
||||
|
||||
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = NestedCircuitSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'url', 'display', 'circuit', 'term_side', 'cable', '_occupied']
|
||||
@@ -1,3 +1,2 @@
|
||||
from .serializers_.providers import *
|
||||
from .serializers_.circuits import *
|
||||
from .nested_serializers import *
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import Circuit, CircuitTermination, CircuitType
|
||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
||||
from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import (
|
||||
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
|
||||
VirtualCircuitTermination, VirtualCircuitType,
|
||||
)
|
||||
from dcim.api.serializers_.device_components import InterfaceSerializer
|
||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||
from dcim.api.serializers_.sites import SiteSerializer
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
|
||||
from utilities.api import get_serializer_for_model
|
||||
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
||||
|
||||
__all__ = (
|
||||
'CircuitSerializer',
|
||||
'CircuitGroupAssignmentSerializer',
|
||||
'CircuitGroupSerializer',
|
||||
'CircuitTerminationSerializer',
|
||||
'CircuitTypeSerializer',
|
||||
'VirtualCircuitSerializer',
|
||||
'VirtualCircuitTerminationSerializer',
|
||||
'VirtualCircuitTypeSerializer',
|
||||
)
|
||||
|
||||
|
||||
class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
|
||||
# Related object counts
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
@@ -26,56 +37,184 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'circuit_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
||||
|
||||
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
site = SiteSerializer(nested=True, allow_null=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
|
||||
termination_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(
|
||||
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
),
|
||||
allow_null=True,
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
termination = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'description',
|
||||
'id', 'url', 'display_url', 'display', 'termination_type', 'termination_id', 'termination', 'port_speed',
|
||||
'upstream_speed', 'xconnect_id', 'description',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_termination(self, obj):
|
||||
if obj.termination_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.termination)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.termination, nested=True, context=context).data
|
||||
|
||||
|
||||
class CircuitGroupSerializer(NetBoxModelSerializer):
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
circuit_count = RelatedObjectCountField('assignments')
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count'
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentSerializer_(NetBoxModelSerializer):
|
||||
"""
|
||||
Base serializer for group assignments under CircuitSerializer.
|
||||
"""
|
||||
group = CircuitGroupSerializer(nested=True)
|
||||
priority = ChoiceField(choices=CircuitPriorityChoices, allow_blank=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'group', 'priority', 'tags', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'group', 'priority')
|
||||
|
||||
|
||||
class CircuitSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
type = CircuitTypeSerializer(nested=True)
|
||||
distance_unit = ChoiceField(choices=DistanceUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
assignments = CircuitGroupAssignmentSerializer_(nested=True, many=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date',
|
||||
'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
|
||||
'install_date', 'termination_date', 'commit_rate', 'description', 'distance', 'distance_unit',
|
||||
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'assignments',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'cid', 'description')
|
||||
brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
|
||||
|
||||
|
||||
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
site = SiteSerializer(nested=True, required=False, allow_null=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True)
|
||||
termination_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(
|
||||
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
),
|
||||
allow_null=True,
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
termination = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'termination_type', 'termination_id',
|
||||
'termination', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'mark_connected',
|
||||
'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'_occupied',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_termination(self, obj):
|
||||
if obj.termination_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.termination)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.termination, nested=True, context=context).data
|
||||
|
||||
|
||||
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
||||
member_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS)
|
||||
)
|
||||
member = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority', 'tags',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_member(self, obj):
|
||||
if obj.member_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.member)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.member, nested=True, context=context).data
|
||||
|
||||
|
||||
class VirtualCircuitTypeSerializer(NetBoxModelSerializer):
|
||||
|
||||
# Related object counts
|
||||
virtual_circuit_count = RelatedObjectCountField('virtual_circuits')
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'virtual_circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')
|
||||
|
||||
|
||||
class VirtualCircuitSerializer(NetBoxModelSerializer):
|
||||
provider_network = ProviderNetworkSerializer(nested=True)
|
||||
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
type = VirtualCircuitTypeSerializer(nested=True)
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'type', 'status',
|
||||
'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description')
|
||||
|
||||
|
||||
class VirtualCircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
virtual_circuit = VirtualCircuitSerializer(nested=True)
|
||||
role = ChoiceField(choices=VirtualCircuitTerminationRoleChoices, required=False)
|
||||
interface = InterfaceSerializer(nested=True)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'virtual_circuit', 'role', 'interface', 'description', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'virtual_circuit', 'role', 'interface', 'description')
|
||||
|
||||
13
netbox/circuits/api/serializers_/nested.py
Normal file
13
netbox/circuits/api/serializers_/nested.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from circuits.models import ProviderAccount
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
__all__ = (
|
||||
'NestedProviderAccountSerializer',
|
||||
)
|
||||
|
||||
|
||||
class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'url', 'display_url', 'display', 'name', 'account']
|
||||
@@ -5,7 +5,7 @@ from ipam.api.serializers_.asns import ASNSerializer
|
||||
from ipam.models import ASN
|
||||
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
from ..nested_serializers import *
|
||||
from .nested import NestedProviderAccountSerializer
|
||||
|
||||
__all__ = (
|
||||
'ProviderAccountSerializer',
|
||||
@@ -15,7 +15,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ProviderSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
accounts = SerializedPKRelatedField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
serializer=NestedProviderAccountSerializer,
|
||||
@@ -36,34 +35,32 @@ class ProviderSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'accounts', 'description', 'comments',
|
||||
'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
||||
|
||||
|
||||
class ProviderAccountSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='')
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'account', 'description')
|
||||
|
||||
|
||||
class ProviderNetworkSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
provider = ProviderSerializer(nested=True)
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
@@ -14,6 +14,13 @@ router.register('provider-networks', views.ProviderNetworkViewSet)
|
||||
router.register('circuit-types', views.CircuitTypeViewSet)
|
||||
router.register('circuits', views.CircuitViewSet)
|
||||
router.register('circuit-terminations', views.CircuitTerminationViewSet)
|
||||
router.register('circuit-groups', views.CircuitGroupViewSet)
|
||||
router.register('circuit-group-assignments', views.CircuitGroupAssignmentViewSet)
|
||||
|
||||
# Virtual circuits
|
||||
router.register('virtual-circuits', views.VirtualCircuitViewSet)
|
||||
router.register('virtual-circuit-types', views.VirtualCircuitTypeViewSet)
|
||||
router.register('virtual-circuit-terminations', views.VirtualCircuitTerminationViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -55,6 +55,26 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Circuit Groups
|
||||
#
|
||||
|
||||
class CircuitGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
serializer_class = serializers.CircuitGroupSerializer
|
||||
filterset_class = filtersets.CircuitGroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Circuit Group Assignments
|
||||
#
|
||||
|
||||
class CircuitGroupAssignmentViewSet(NetBoxModelViewSet):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
serializer_class = serializers.CircuitGroupAssignmentSerializer
|
||||
filterset_class = filtersets.CircuitGroupAssignmentFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Provider accounts
|
||||
#
|
||||
@@ -73,3 +93,33 @@ class ProviderNetworkViewSet(NetBoxModelViewSet):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
serializer_class = serializers.ProviderNetworkSerializer
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuit types
|
||||
#
|
||||
|
||||
class VirtualCircuitTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
serializer_class = serializers.VirtualCircuitTypeSerializer
|
||||
filterset_class = filtersets.VirtualCircuitTypeFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuits
|
||||
#
|
||||
|
||||
class VirtualCircuitViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
serializer_class = serializers.VirtualCircuitSerializer
|
||||
filterset_class = filtersets.VirtualCircuitFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuit terminations
|
||||
#
|
||||
|
||||
class VirtualCircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
serializer_class = serializers.VirtualCircuitTerminationSerializer
|
||||
filterset_class = filtersets.VirtualCircuitTerminationFilterSet
|
||||
|
||||
@@ -7,7 +7,7 @@ class CircuitsConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
from netbox.models.features import register_models
|
||||
from . import signals, search
|
||||
from . import signals, search # noqa: F401
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
@@ -38,6 +38,8 @@ class CircuitCommitRateChoices(ChoiceSet):
|
||||
(25000000, '25 Gbps'),
|
||||
(40000000, '40 Gbps'),
|
||||
(100000000, '100 Gbps'),
|
||||
(200000000, '200 Gbps'),
|
||||
(400000000, '400 Gbps'),
|
||||
(1544, 'T1 (1.544 Mbps)'),
|
||||
(2048, 'E1 (2.048 Mbps)'),
|
||||
]
|
||||
@@ -69,6 +71,40 @@ class CircuitTerminationPortSpeedChoices(ChoiceSet):
|
||||
(25000000, '25 Gbps'),
|
||||
(40000000, '40 Gbps'),
|
||||
(100000000, '100 Gbps'),
|
||||
(200000000, '200 Gbps'),
|
||||
(400000000, '400 Gbps'),
|
||||
(1544, 'T1 (1.544 Mbps)'),
|
||||
(2048, 'E1 (2.048 Mbps)'),
|
||||
]
|
||||
|
||||
|
||||
class CircuitPriorityChoices(ChoiceSet):
|
||||
key = 'CircuitGroupAssignment.priority'
|
||||
|
||||
PRIORITY_PRIMARY = 'primary'
|
||||
PRIORITY_SECONDARY = 'secondary'
|
||||
PRIORITY_TERTIARY = 'tertiary'
|
||||
PRIORITY_INACTIVE = 'inactive'
|
||||
|
||||
CHOICES = [
|
||||
(PRIORITY_PRIMARY, _('Primary')),
|
||||
(PRIORITY_SECONDARY, _('Secondary')),
|
||||
(PRIORITY_TERTIARY, _('Tertiary')),
|
||||
(PRIORITY_INACTIVE, _('Inactive')),
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuits
|
||||
#
|
||||
|
||||
class VirtualCircuitTerminationRoleChoices(ChoiceSet):
|
||||
ROLE_PEER = 'peer'
|
||||
ROLE_HUB = 'hub'
|
||||
ROLE_SPOKE = 'spoke'
|
||||
|
||||
CHOICES = [
|
||||
(ROLE_PEER, _('Peer'), 'green'),
|
||||
(ROLE_HUB, _('Hub'), 'blue'),
|
||||
(ROLE_SPOKE, _('Spoke'), 'orange'),
|
||||
]
|
||||
|
||||
12
netbox/circuits/constants.py
Normal file
12
netbox/circuits/constants.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
# models values for ContentTypes which may be CircuitTermination termination types
|
||||
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
|
||||
'region', 'sitegroup', 'site', 'location', 'providernetwork',
|
||||
)
|
||||
|
||||
CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS = Q(
|
||||
app_label='circuits',
|
||||
model__in=['circuit', 'virtualcircuit']
|
||||
)
|
||||
@@ -1,60 +1,68 @@
|
||||
import django_filters
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.filtersets import CabledObjectFilterSet
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from dcim.models import Interface, Location, Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterSet',
|
||||
'CircuitGroupAssignmentFilterSet',
|
||||
'CircuitGroupFilterSet',
|
||||
'CircuitTerminationFilterSet',
|
||||
'CircuitTypeFilterSet',
|
||||
'ProviderNetworkFilterSet',
|
||||
'ProviderAccountFilterSet',
|
||||
'ProviderFilterSet',
|
||||
'VirtualCircuitFilterSet',
|
||||
'VirtualCircuitTerminationFilterSet',
|
||||
'VirtualCircuitTypeFilterSet',
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
field_name='circuits__terminations___region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
field_name='circuits__terminations___region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
field_name='circuits__terminations___site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
field_name='circuits__terminations___site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site',
|
||||
field_name='circuits__terminations___site',
|
||||
queryset=Site.objects.all(),
|
||||
label=_('Site'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site__slug',
|
||||
field_name='circuits__terminations___site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
@@ -171,7 +179,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__provider_network',
|
||||
field_name='terminations___provider_network',
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
@@ -191,37 +199,37 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
field_name='terminations___region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
field_name='terminations___region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
field_name='terminations___site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
field_name='terminations___site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site',
|
||||
field_name='terminations___site',
|
||||
queryset=Site.objects.all(),
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site__slug',
|
||||
field_name='terminations___site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
@@ -237,7 +245,9 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate')
|
||||
fields = (
|
||||
'id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -261,26 +271,79 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
queryset=Circuit.objects.all(),
|
||||
label=_('Circuit'),
|
||||
)
|
||||
termination_type = ContentTypeFilter()
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='_site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='_site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
field_name='_site',
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site__slug',
|
||||
field_name='_site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='_location',
|
||||
lookup_expr='in',
|
||||
label=_('Location (ID)'),
|
||||
)
|
||||
location = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='_location',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Location (slug)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
field_name='_provider_network',
|
||||
label=_('ProviderNetwork (ID)'),
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider_id',
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuit__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = (
|
||||
'id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected',
|
||||
'pp_info', 'cable_end',
|
||||
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description',
|
||||
'mark_connected', 'pp_info', 'cable_end',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
@@ -292,3 +355,231 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
Q(pp_info__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
member_type = ContentTypeFilter()
|
||||
circuit = MultiValueCharFilter(
|
||||
method='filter_circuit',
|
||||
field_name='cid',
|
||||
label=_('Circuit (CID)'),
|
||||
)
|
||||
circuit_id = MultiValueNumberFilter(
|
||||
method='filter_circuit',
|
||||
field_name='pk',
|
||||
label=_('Circuit (ID)'),
|
||||
)
|
||||
virtual_circuit = MultiValueCharFilter(
|
||||
method='filter_virtual_circuit',
|
||||
field_name='cid',
|
||||
label=_('Virtual circuit (CID)'),
|
||||
)
|
||||
virtual_circuit_id = MultiValueNumberFilter(
|
||||
method='filter_virtual_circuit',
|
||||
field_name='pk',
|
||||
label=_('Virtual circuit (ID)'),
|
||||
)
|
||||
provider = MultiValueCharFilter(
|
||||
method='filter_provider',
|
||||
field_name='slug',
|
||||
label=_('Provider (name)'),
|
||||
)
|
||||
provider_id = MultiValueNumberFilter(
|
||||
method='filter_provider',
|
||||
field_name='pk',
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
label=_('Circuit group (ID)'),
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='group__slug',
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Circuit group (slug)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = ('id', 'member_id', 'priority')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(member__cid__icontains=value) |
|
||||
Q(group__name__icontains=value)
|
||||
)
|
||||
|
||||
def filter_circuit(self, queryset, name, value):
|
||||
circuits = Circuit.objects.filter(**{f'{name}__in': value})
|
||||
if not circuits.exists():
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(Circuit),
|
||||
member_id__in=circuits
|
||||
)
|
||||
)
|
||||
|
||||
def filter_virtual_circuit(self, queryset, name, value):
|
||||
virtual_circuits = VirtualCircuit.objects.filter(**{f'{name}__in': value})
|
||||
if not virtual_circuits.exists():
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(VirtualCircuit),
|
||||
member_id__in=virtual_circuits
|
||||
)
|
||||
)
|
||||
|
||||
def filter_provider(self, queryset, name, value):
|
||||
providers = Provider.objects.filter(**{f'{name}__in': value})
|
||||
if not providers.exists():
|
||||
return queryset.none()
|
||||
circuits = Circuit.objects.filter(provider__in=providers)
|
||||
virtual_circuits = VirtualCircuit.objects.filter(provider_network__provider__in=providers)
|
||||
return queryset.filter(
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(Circuit),
|
||||
member_id__in=circuits
|
||||
) |
|
||||
Q(
|
||||
member_type=ContentType.objects.get_for_model(VirtualCircuit),
|
||||
member_id__in=virtual_circuits
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_network__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account__account',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
label=_('Virtual circuit type (ID)'),
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='type__slug',
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Virtual circuit type (slug)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=CircuitStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = ('id', 'cid', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(cid__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
label=_('Virtual circuit'),
|
||||
)
|
||||
role = django_filters.MultipleChoiceFilter(
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
null_value=None
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_network__provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_network__provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
label=_('Provider account (ID)'),
|
||||
)
|
||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='virtual_circuit__provider_account__account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
to_field_name='account',
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
field_name='virtual_circuit__provider_network',
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Interface.objects.all(),
|
||||
field_name='interface',
|
||||
label=_('Interface (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = ('id', 'interface_id', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(virtual_circuit__cid__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
).distinct()
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
||||
from circuits.choices import (
|
||||
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices,
|
||||
)
|
||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms import add_blank_choice, get_field_value
|
||||
from utilities.forms.fields import (
|
||||
ColorField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
'CircuitGroupAssignmentBulkEditForm',
|
||||
'CircuitGroupBulkEditForm',
|
||||
'CircuitTerminationBulkEditForm',
|
||||
'CircuitTypeBulkEditForm',
|
||||
'ProviderBulkEditForm',
|
||||
'ProviderAccountBulkEditForm',
|
||||
'ProviderNetworkBulkEditForm',
|
||||
'VirtualCircuitBulkEditForm',
|
||||
'VirtualCircuitTerminationBulkEditForm',
|
||||
'VirtualCircuitTypeBulkEditForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -156,6 +172,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
options=CircuitCommitRateChoices
|
||||
)
|
||||
)
|
||||
distance = forms.DecimalField(
|
||||
label=_('Distance'),
|
||||
min_value=0,
|
||||
required=False
|
||||
)
|
||||
distance_unit = forms.ChoiceField(
|
||||
label=_('Distance unit'),
|
||||
choices=add_blank_choice(DistanceUnitChoices),
|
||||
required=False,
|
||||
initial=''
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=100,
|
||||
@@ -167,8 +194,187 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
|
||||
FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
|
||||
FieldSet('distance', 'distance_unit', name=_('Attributes')),
|
||||
FieldSet('tenant', name=_('Tenancy')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'tenant', 'commit_rate', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
termination_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
|
||||
required=False,
|
||||
label=_('Termination type')
|
||||
)
|
||||
termination = DynamicModelChoiceField(
|
||||
label=_('Termination'),
|
||||
queryset=Site.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
port_speed = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('Port speed (Kbps)'),
|
||||
)
|
||||
upstream_speed = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('Upstream speed (Kbps)'),
|
||||
)
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
)
|
||||
|
||||
model = CircuitTermination
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'description',
|
||||
'termination_type', 'termination',
|
||||
'mark_connected', name=_('Circuit Termination')
|
||||
),
|
||||
FieldSet('port_speed', 'upstream_speed', name=_('Termination Details')),
|
||||
)
|
||||
nullable_fields = ('description', 'termination')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if termination_type_id := get_field_value(self, 'termination_type'):
|
||||
try:
|
||||
termination_type = ContentType.objects.get(pk=termination_type_id)
|
||||
model = termination_type.model_class()
|
||||
self.fields['termination'].queryset = model.objects.all()
|
||||
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['termination'].disabled = False
|
||||
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
model = CircuitGroup
|
||||
nullable_fields = (
|
||||
'description', 'tenant',
|
||||
)
|
||||
|
||||
|
||||
class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
||||
member = DynamicModelChoiceField(
|
||||
label=_('Circuit'),
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False
|
||||
)
|
||||
priority = forms.ChoiceField(
|
||||
label=_('Priority'),
|
||||
choices=add_blank_choice(CircuitPriorityChoices),
|
||||
required=False
|
||||
)
|
||||
|
||||
model = CircuitGroupAssignment
|
||||
fieldsets = (
|
||||
FieldSet('member', 'priority'),
|
||||
)
|
||||
nullable_fields = ('priority',)
|
||||
|
||||
|
||||
class VirtualCircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
color = ColorField(
|
||||
label=_('Color'),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
model = VirtualCircuitType
|
||||
fieldsets = (
|
||||
FieldSet('color', 'description'),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
|
||||
|
||||
class VirtualCircuitBulkEditForm(NetBoxModelBulkEditForm):
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False
|
||||
)
|
||||
provider_account = DynamicModelChoiceField(
|
||||
label=_('Provider account'),
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
label=_('Type'),
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
required=False
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
label=_('Status'),
|
||||
choices=add_blank_choice(CircuitStatusChoices),
|
||||
required=False,
|
||||
initial=''
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=100,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = VirtualCircuit
|
||||
fieldsets = (
|
||||
FieldSet('provider_network', 'provider_account', 'status', 'description', name=_('Virtual circuit')),
|
||||
FieldSet('tenant', name=_('Tenancy')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'provider_account', 'tenant', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
role = forms.ChoiceField(
|
||||
label=_('Role'),
|
||||
choices=add_blank_choice(VirtualCircuitTerminationRoleChoices),
|
||||
required=False,
|
||||
initial=''
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
|
||||
model = VirtualCircuitTermination
|
||||
fieldsets = (
|
||||
FieldSet('role', 'description'),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
from django import forms
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.constants import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Interface
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
|
||||
|
||||
__all__ = (
|
||||
'CircuitImportForm',
|
||||
'CircuitGroupAssignmentImportForm',
|
||||
'CircuitGroupImportForm',
|
||||
'CircuitTerminationImportForm',
|
||||
'CircuitTerminationImportRelatedForm',
|
||||
'CircuitTypeImportForm',
|
||||
'ProviderImportForm',
|
||||
'ProviderAccountImportForm',
|
||||
'ProviderNetworkImportForm',
|
||||
'VirtualCircuitImportForm',
|
||||
'VirtualCircuitTerminationImportForm',
|
||||
'VirtualCircuitTerminationImportRelatedForm',
|
||||
'VirtualCircuitTypeImportForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -65,9 +74,6 @@ class CircuitTypeImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ('name', 'slug', 'color', 'description', 'tags')
|
||||
help_texts = {
|
||||
'color': mark_safe(_('RGB color in hexadecimal. Example:') + ' <code>00ff00</code>'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitImportForm(NetBoxModelImportForm):
|
||||
@@ -95,6 +101,12 @@ class CircuitImportForm(NetBoxModelImportForm):
|
||||
choices=CircuitStatusChoices,
|
||||
help_text=_('Operational status')
|
||||
)
|
||||
distance_unit = CSVChoiceField(
|
||||
label=_('Distance unit'),
|
||||
choices=DistanceUnitChoices,
|
||||
required=False,
|
||||
help_text=_('Distance unit')
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
@@ -107,27 +119,162 @@ class CircuitImportForm(NetBoxModelImportForm):
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
|
||||
'commit_rate', 'description', 'comments', 'tags'
|
||||
'commit_rate', 'distance', 'distance_unit', 'description', 'comments', 'tags'
|
||||
]
|
||||
|
||||
|
||||
class CircuitTerminationImportForm(forms.ModelForm):
|
||||
site = CSVModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
class BaseCircuitTerminationImportForm(forms.ModelForm):
|
||||
circuit = CSVModelChoiceField(
|
||||
label=_('Circuit'),
|
||||
queryset=Circuit.objects.all(),
|
||||
to_field_name='cid',
|
||||
)
|
||||
provider_network = CSVModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
term_side = CSVChoiceField(
|
||||
label=_('Termination'),
|
||||
choices=CircuitTerminationSideChoices,
|
||||
)
|
||||
termination_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
required=False,
|
||||
label=_('Termination type (app & model)')
|
||||
)
|
||||
|
||||
|
||||
class CircuitTerminationImportRelatedForm(BaseCircuitTerminationImportForm):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description'
|
||||
]
|
||||
labels = {
|
||||
'termination_id': _('Termination ID'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTerminationImportForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description',
|
||||
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description', 'tags'
|
||||
]
|
||||
labels = {
|
||||
'termination_id': _('Termination ID'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupImportForm(NetBoxModelImportForm):
|
||||
tenant = CSVModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned tenant')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = ('name', 'slug', 'description', 'tenant', 'tags')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
||||
member_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
|
||||
label=_('Circuit type (app & model)')
|
||||
)
|
||||
priority = CSVChoiceField(
|
||||
label=_('Priority'),
|
||||
choices=CircuitPriorityChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = ('member_type', 'member_id', 'group', 'priority')
|
||||
|
||||
|
||||
class VirtualCircuitTypeImportForm(NetBoxModelImportForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = ('name', 'slug', 'color', 'description', 'tags')
|
||||
|
||||
|
||||
class VirtualCircuitImportForm(NetBoxModelImportForm):
|
||||
provider_network = CSVModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('The network to which this virtual circuit belongs')
|
||||
)
|
||||
provider_account = CSVModelChoiceField(
|
||||
label=_('Provider account'),
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
to_field_name='account',
|
||||
help_text=_('Assigned provider account (if any)'),
|
||||
required=False
|
||||
)
|
||||
type = CSVModelChoiceField(
|
||||
label=_('Type'),
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Type of virtual circuit')
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
label=_('Status'),
|
||||
choices=CircuitStatusChoices,
|
||||
help_text=_('Operational status')
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned tenant')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = [
|
||||
'cid', 'provider_network', 'provider_account', 'type', 'status', 'tenant', 'description', 'comments',
|
||||
'tags',
|
||||
]
|
||||
|
||||
|
||||
class BaseVirtualCircuitTerminationImportForm(forms.ModelForm):
|
||||
virtual_circuit = CSVModelChoiceField(
|
||||
label=_('Virtual circuit'),
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
to_field_name='cid',
|
||||
)
|
||||
role = CSVChoiceField(
|
||||
label=_('Role'),
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
help_text=_('Operational role')
|
||||
)
|
||||
interface = CSVModelChoiceField(
|
||||
label=_('Interface'),
|
||||
queryset=Interface.objects.all(),
|
||||
to_field_name='pk',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationImportRelatedForm(BaseVirtualCircuitTerminationImportForm):
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = [
|
||||
'virtual_circuit', 'role', 'interface', 'description',
|
||||
]
|
||||
|
||||
|
||||
class VirtualCircuitTerminationImportForm(NetBoxModelImportForm, BaseVirtualCircuitTerminationImportForm):
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = [
|
||||
'virtual_circuit', 'role', 'interface', 'description', 'tags',
|
||||
]
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
||||
from circuits.choices import (
|
||||
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices,
|
||||
VirtualCircuitTerminationRoleChoices,
|
||||
)
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitFilterForm',
|
||||
'CircuitGroupAssignmentFilterForm',
|
||||
'CircuitGroupFilterForm',
|
||||
'CircuitTerminationFilterForm',
|
||||
'CircuitTypeFilterForm',
|
||||
'ProviderFilterForm',
|
||||
'ProviderAccountFilterForm',
|
||||
'ProviderNetworkFilterForm',
|
||||
'VirtualCircuitFilterForm',
|
||||
'VirtualCircuitTerminationFilterForm',
|
||||
'VirtualCircuitTypeFilterForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -111,7 +122,10 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', name=_('Attributes')),
|
||||
FieldSet(
|
||||
'type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit',
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
@@ -185,4 +199,195 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
||||
options=CircuitCommitRateChoices
|
||||
)
|
||||
)
|
||||
distance = forms.DecimalField(
|
||||
label=_('Distance'),
|
||||
required=False,
|
||||
)
|
||||
distance_unit = forms.ChoiceField(
|
||||
label=_('Distance unit'),
|
||||
choices=add_blank_choice(DistanceUnitChoices),
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitTermination
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('circuit_id', 'term_side', name=_('Circuit')),
|
||||
FieldSet('provider_id', name=_('Provider')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Termination')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
label=_('Location')
|
||||
)
|
||||
circuit_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False,
|
||||
label=_('Circuit')
|
||||
)
|
||||
term_side = forms.MultipleChoiceField(
|
||||
label=_('Term Side'),
|
||||
choices=CircuitTerminationSideChoices,
|
||||
required=False
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider network')
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = CircuitGroup
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitGroupAssignment
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'member_id', 'group_id', 'priority', name=_('Assignment')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
member_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False,
|
||||
label=_('Circuit')
|
||||
)
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Group')
|
||||
)
|
||||
priority = forms.MultipleChoiceField(
|
||||
label=_('Priority'),
|
||||
choices=CircuitPriorityChoices,
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualCircuitTypeFilterForm(NetBoxModelFilterSetForm):
|
||||
model = VirtualCircuitType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('color', name=_('Attributes')),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
color = ColorField(
|
||||
label=_('Color'),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
model = VirtualCircuit
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('type', 'status', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'provider_id', 'provider_network_id')
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
provider_account_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider account')
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider network')
|
||||
)
|
||||
type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
required=False,
|
||||
label=_('Type')
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
label=_('Status'),
|
||||
choices=CircuitStatusChoices,
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
model = VirtualCircuitTermination
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('virtual_circuit_id', 'role', name=_('Virtual circuit')),
|
||||
FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
|
||||
)
|
||||
virtual_circuit_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
required=False,
|
||||
label=_('Virtual circuit')
|
||||
)
|
||||
role = forms.MultipleChoiceField(
|
||||
label=_('Role'),
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
required=False
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider network')
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
|
||||
from circuits.choices import (
|
||||
CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices, VirtualCircuitTerminationRoleChoices,
|
||||
)
|
||||
from circuits.constants import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from dcim.models import Interface, Site
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import (
|
||||
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet, InlineFields
|
||||
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
|
||||
__all__ = (
|
||||
'CircuitForm',
|
||||
'CircuitGroupAssignmentForm',
|
||||
'CircuitGroupForm',
|
||||
'CircuitTerminationForm',
|
||||
'CircuitTypeForm',
|
||||
'ProviderForm',
|
||||
'ProviderAccountForm',
|
||||
'ProviderNetworkForm',
|
||||
'VirtualCircuitForm',
|
||||
'VirtualCircuitTerminationForm',
|
||||
'VirtualCircuitTypeForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -43,7 +58,9 @@ class ProviderForm(NetBoxModelForm):
|
||||
class ProviderAccountForm(NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
label=_('Provider'),
|
||||
queryset=Provider.objects.all()
|
||||
queryset=Provider.objects.all(),
|
||||
selector=True,
|
||||
quick_add=True
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
@@ -57,7 +74,9 @@ class ProviderAccountForm(NetBoxModelForm):
|
||||
class ProviderNetworkForm(NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
label=_('Provider'),
|
||||
queryset=Provider.objects.all()
|
||||
queryset=Provider.objects.all(),
|
||||
selector=True,
|
||||
quick_add=True
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
@@ -90,7 +109,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
label=_('Provider'),
|
||||
queryset=Provider.objects.all(),
|
||||
selector=True
|
||||
selector=True,
|
||||
quick_add=True
|
||||
)
|
||||
provider_account = DynamicModelChoiceField(
|
||||
label=_('Provider account'),
|
||||
@@ -101,12 +121,23 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
}
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all()
|
||||
queryset=CircuitType.objects.all(),
|
||||
quick_add=True
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
|
||||
FieldSet(
|
||||
'provider',
|
||||
'provider_account',
|
||||
'cid',
|
||||
'type',
|
||||
'status',
|
||||
InlineFields('distance', 'distance_unit', label=_('Distance')),
|
||||
'description',
|
||||
'tags',
|
||||
name=_('Circuit')
|
||||
),
|
||||
FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
@@ -115,7 +146,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description', 'tenant_group', 'tenant', 'comments', 'tags',
|
||||
'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'install_date': DatePicker(),
|
||||
@@ -132,26 +163,24 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
queryset=Circuit.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
termination_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
widget=HTMXSelect(),
|
||||
required=False,
|
||||
selector=True
|
||||
label=_('Termination type')
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
termination = DynamicModelChoiceField(
|
||||
label=_('Termination'),
|
||||
queryset=Site.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'circuit', 'term_side', 'description', 'tags',
|
||||
TabbedGroups(
|
||||
FieldSet('site', name=_('Site')),
|
||||
FieldSet('provider_network', name=_('Provider Network')),
|
||||
),
|
||||
'termination_type', 'termination',
|
||||
'mark_connected', name=_('Circuit Termination')
|
||||
),
|
||||
FieldSet('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', name=_('Termination Details')),
|
||||
@@ -160,7 +189,7 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed', 'upstream_speed',
|
||||
'circuit', 'term_side', 'termination_type', 'mark_connected', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
@@ -171,3 +200,189 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
options=CircuitTerminationPortSpeedChoices
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
if instance is not None and instance.termination:
|
||||
initial['termination'] = instance.termination
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if termination_type_id := get_field_value(self, 'termination_type'):
|
||||
try:
|
||||
termination_type = ContentType.objects.get(pk=termination_type_id)
|
||||
model = termination_type.model_class()
|
||||
self.fields['termination'].queryset = model.objects.all()
|
||||
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['termination'].disabled = False
|
||||
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
if self.instance and termination_type_id != self.instance.termination_type_id:
|
||||
self.initial['termination'] = None
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Assign the selected termination (if any)
|
||||
self.instance.termination = self.cleaned_data.get('termination')
|
||||
|
||||
|
||||
class CircuitGroupForm(TenancyForm, NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'description', 'tags', name=_('Circuit Group')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class CircuitGroupAssignmentForm(NetBoxModelForm):
|
||||
group = DynamicModelChoiceField(
|
||||
label=_('Group'),
|
||||
queryset=CircuitGroup.objects.all(),
|
||||
)
|
||||
member_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
|
||||
widget=HTMXSelect(),
|
||||
required=False,
|
||||
label=_('Circuit type')
|
||||
)
|
||||
member = DynamicModelChoiceField(
|
||||
label=_('Circuit'),
|
||||
queryset=Circuit.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('group', 'member_type', 'member', 'priority', 'tags', name=_('Group Assignment')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroupAssignment
|
||||
fields = [
|
||||
'group', 'member_type', 'priority', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
if instance is not None and instance.member:
|
||||
initial['member'] = instance.member
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if member_type_id := get_field_value(self, 'member_type'):
|
||||
try:
|
||||
model = ContentType.objects.get(pk=member_type_id).model_class()
|
||||
self.fields['member'].queryset = model.objects.all()
|
||||
self.fields['member'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['member'].disabled = False
|
||||
self.fields['member'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
if self.instance.pk and member_type_id != self.instance.member_type_id:
|
||||
self.initial['member'] = None
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Assign the selected circuit (if any)
|
||||
self.instance.member = self.cleaned_data.get('member')
|
||||
|
||||
|
||||
class VirtualCircuitTypeForm(NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'color', 'description', 'tags'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
provider_account = DynamicModelChoiceField(
|
||||
label=_('Provider account'),
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=VirtualCircuitType.objects.all(),
|
||||
quick_add=True
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'provider_network', 'provider_account', 'cid', 'type', 'status', 'description', 'tags',
|
||||
name=_('Virtual circuit'),
|
||||
),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuit
|
||||
fields = [
|
||||
'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant',
|
||||
'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class VirtualCircuitTerminationForm(NetBoxModelForm):
|
||||
virtual_circuit = DynamicModelChoiceField(
|
||||
label=_('Virtual circuit'),
|
||||
queryset=VirtualCircuit.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
role = forms.ChoiceField(
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
widget=HTMXSelect(),
|
||||
label=_('Role')
|
||||
)
|
||||
interface = DynamicModelChoiceField(
|
||||
label=_('Interface'),
|
||||
queryset=Interface.objects.all(),
|
||||
selector=True,
|
||||
query_params={
|
||||
'kind': 'virtual',
|
||||
'virtual_circuit_termination_id': 'null',
|
||||
},
|
||||
context={
|
||||
'parent': 'device',
|
||||
}
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('virtual_circuit', 'role', 'interface', 'description', 'tags'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitTermination
|
||||
fields = [
|
||||
'virtual_circuit', 'role', 'interface', 'description', 'tags',
|
||||
]
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from circuits import filtersets, models
|
||||
|
||||
from circuits import filtersets, models
|
||||
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationFilter',
|
||||
'CircuitFilter',
|
||||
'CircuitGroupAssignmentFilter',
|
||||
'CircuitGroupFilter',
|
||||
'CircuitTerminationFilter',
|
||||
'CircuitTypeFilter',
|
||||
'ProviderFilter',
|
||||
'ProviderAccountFilter',
|
||||
'ProviderNetworkFilter',
|
||||
'VirtualCircuitFilter',
|
||||
'VirtualCircuitTerminationFilter',
|
||||
'VirtualCircuitTypeFilter',
|
||||
)
|
||||
|
||||
|
||||
@@ -32,6 +36,18 @@ class CircuitTypeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitGroup, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitGroupFilterSet)
|
||||
class CircuitGroupFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True)
|
||||
@autotype_decorator(filtersets.CircuitGroupAssignmentFilterSet)
|
||||
class CircuitGroupAssignmentFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Provider, lookups=True)
|
||||
@autotype_decorator(filtersets.ProviderFilterSet)
|
||||
class ProviderFilter(BaseFilterMixin):
|
||||
@@ -48,3 +64,21 @@ class ProviderAccountFilter(BaseFilterMixin):
|
||||
@autotype_decorator(filtersets.ProviderNetworkFilterSet)
|
||||
class ProviderNetworkFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuitType, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualCircuitTypeFilterSet)
|
||||
class VirtualCircuitTypeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualCircuitFilterSet)
|
||||
class VirtualCircuitFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
|
||||
@autotype_decorator(filtersets.VirtualCircuitTerminationFilterSet)
|
||||
class VirtualCircuitTerminationFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
@@ -3,38 +3,40 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from circuits import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class CircuitsQuery:
|
||||
@strawberry.field
|
||||
def circuit(self, id: int) -> CircuitType:
|
||||
return models.Circuit.objects.get(pk=id)
|
||||
circuit: CircuitType = strawberry_django.field()
|
||||
circuit_list: List[CircuitType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_termination(self, id: int) -> CircuitTerminationType:
|
||||
return models.CircuitTermination.objects.get(pk=id)
|
||||
circuit_termination: CircuitTerminationType = strawberry_django.field()
|
||||
circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_type(self, id: int) -> CircuitTypeType:
|
||||
return models.CircuitType.objects.get(pk=id)
|
||||
circuit_type: CircuitTypeType = strawberry_django.field()
|
||||
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider(self, id: int) -> ProviderType:
|
||||
return models.Provider.objects.get(pk=id)
|
||||
circuit_group: CircuitGroupType = strawberry_django.field()
|
||||
circuit_group_list: List[CircuitGroupType] = strawberry_django.field()
|
||||
|
||||
circuit_group_assignment: CircuitGroupAssignmentType = strawberry_django.field()
|
||||
circuit_group_assignment_list: List[CircuitGroupAssignmentType] = strawberry_django.field()
|
||||
|
||||
provider: ProviderType = strawberry_django.field()
|
||||
provider_list: List[ProviderType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider_account(self, id: int) -> ProviderAccountType:
|
||||
return models.ProviderAccount.objects.get(pk=id)
|
||||
provider_account: ProviderAccountType = strawberry_django.field()
|
||||
provider_account_list: List[ProviderAccountType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider_network(self, id: int) -> ProviderNetworkType:
|
||||
return models.ProviderNetwork.objects.get(pk=id)
|
||||
provider_network: ProviderNetworkType = strawberry_django.field()
|
||||
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
|
||||
|
||||
virtual_circuit: VirtualCircuitType = strawberry_django.field()
|
||||
virtual_circuit_list: List[VirtualCircuitType] = strawberry_django.field()
|
||||
|
||||
virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field()
|
||||
virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field()
|
||||
|
||||
virtual_circuit_type: VirtualCircuitTypeType = strawberry_django.field()
|
||||
virtual_circuit_type_list: List[VirtualCircuitTypeType] = strawberry_django.field()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Annotated, List
|
||||
from typing import Annotated, List, Union
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
@@ -6,17 +6,22 @@ import strawberry_django
|
||||
from circuits import models
|
||||
from dcim.graphql.mixins import CabledObjectMixin
|
||||
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
|
||||
from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType
|
||||
from netbox.graphql.types import BaseObjectType, NetBoxObjectType, ObjectType, OrganizationalObjectType
|
||||
from tenancy.graphql.types import TenantType
|
||||
from .filters import *
|
||||
|
||||
__all__ = (
|
||||
'CircuitTerminationType',
|
||||
'CircuitType',
|
||||
'CircuitGroupAssignmentType',
|
||||
'CircuitGroupType',
|
||||
'CircuitTypeType',
|
||||
'ProviderType',
|
||||
'ProviderAccountType',
|
||||
'ProviderNetworkType',
|
||||
'VirtualCircuitTerminationType',
|
||||
'VirtualCircuitType',
|
||||
'VirtualCircuitTypeType',
|
||||
)
|
||||
|
||||
|
||||
@@ -57,13 +62,21 @@ class ProviderNetworkType(NetBoxObjectType):
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitTermination,
|
||||
fields='__all__',
|
||||
exclude=('termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'),
|
||||
filters=CircuitTerminationFilter
|
||||
)
|
||||
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
provider_network: Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')] | None
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
@strawberry_django.field
|
||||
def termination(self) -> Annotated[Union[
|
||||
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
|
||||
], strawberry.union("CircuitTerminationTerminationType")] | None:
|
||||
return self.termination
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
@@ -91,3 +104,71 @@ class CircuitType(NetBoxObjectType, ContactsMixin):
|
||||
tenant: TenantType | None
|
||||
|
||||
terminations: List[CircuitTerminationType]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitGroup,
|
||||
fields='__all__',
|
||||
filters=CircuitGroupFilter
|
||||
)
|
||||
class CircuitGroupType(OrganizationalObjectType):
|
||||
tenant: TenantType | None
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitGroupAssignment,
|
||||
exclude=('member_type', 'member_id'),
|
||||
filters=CircuitGroupAssignmentFilter
|
||||
)
|
||||
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
||||
|
||||
@strawberry_django.field
|
||||
def member(self) -> Annotated[Union[
|
||||
Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')],
|
||||
Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')],
|
||||
], strawberry.union("CircuitGroupAssignmentMemberType")] | None:
|
||||
return self.member
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuitType,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitTypeFilter
|
||||
)
|
||||
class VirtualCircuitTypeType(OrganizationalObjectType):
|
||||
color: str
|
||||
|
||||
virtual_circuits: List[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuitTermination,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitTerminationFilter
|
||||
)
|
||||
class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
|
||||
virtual_circuit: Annotated[
|
||||
"VirtualCircuitType",
|
||||
strawberry.lazy('circuits.graphql.types')
|
||||
] = strawberry_django.field(select_related=["virtual_circuit"])
|
||||
interface: Annotated[
|
||||
"InterfaceType",
|
||||
strawberry.lazy('dcim.graphql.types')
|
||||
] = strawberry_django.field(select_related=["interface"])
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualCircuit,
|
||||
fields='__all__',
|
||||
filters=VirtualCircuitFilter
|
||||
)
|
||||
class VirtualCircuitType(NetBoxObjectType):
|
||||
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
|
||||
provider_account: ProviderAccountType | None
|
||||
type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field(
|
||||
select_related=["type"]
|
||||
)
|
||||
tenant: TenantType | None
|
||||
|
||||
terminations: List[VirtualCircuitTerminationType]
|
||||
|
||||
@@ -5,11 +5,9 @@ import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
replaces = [
|
||||
('circuits', '0001_initial'),
|
||||
@@ -98,7 +96,12 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=100)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='networks', to='circuits.provider')),
|
||||
(
|
||||
'provider',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, related_name='networks', to='circuits.provider'
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('provider', 'name'),
|
||||
|
||||
@@ -4,7 +4,6 @@ import taggit.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0001_initial'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
@@ -58,32 +57,56 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_cable_peer_type',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='cable',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.cable'),
|
||||
field=models.ForeignKey(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.cable'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='circuit',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.circuit'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.circuit'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='provider_network',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='circuits.providernetwork'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
to='circuits.providernetwork',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.site'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.site',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='provider',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provider'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provider'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
@@ -93,26 +116,50 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.tenant'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='circuits',
|
||||
to='tenancy.tenant',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='termination_a',
|
||||
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
editable=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='circuits.circuittermination',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='termination_z',
|
||||
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
editable=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='circuits.circuittermination',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.circuittype'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.circuittype'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='providernetwork',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_provider_name'),
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider', 'name'), name='circuits_providernetwork_provider_name'
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='providernetwork',
|
||||
|
||||
@@ -5,7 +5,6 @@ import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [
|
||||
('circuits', '0003_extend_tag_support'),
|
||||
('circuits', '0004_rename_cable_peer'),
|
||||
@@ -14,7 +13,7 @@ class Migration(migrations.Migration):
|
||||
('circuits', '0034_created_datetimefield'),
|
||||
('circuits', '0035_provider_asns'),
|
||||
('circuits', '0036_circuit_termination_date_tags_custom_fields'),
|
||||
('circuits', '0037_new_cabling_models')
|
||||
('circuits', '0037_new_cabling_models'),
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
|
||||
@@ -6,13 +6,12 @@ import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [
|
||||
('circuits', '0038_cabling_cleanup'),
|
||||
('circuits', '0039_unique_constraints'),
|
||||
('circuits', '0040_provider_remove_deprecated_fields'),
|
||||
('circuits', '0041_standardize_description_comments'),
|
||||
('circuits', '0042_provideraccount')
|
||||
('circuits', '0042_provideraccount'),
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
@@ -51,11 +50,15 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuittermination',
|
||||
constraint=models.UniqueConstraint(fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'),
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='providernetwork',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'),
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'
|
||||
),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
@@ -84,12 +87,20 @@ class Migration(migrations.Migration):
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('account', models.CharField(max_length=100)),
|
||||
('name', models.CharField(blank=True, max_length=100)),
|
||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider')),
|
||||
(
|
||||
'provider',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider'
|
||||
),
|
||||
),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
@@ -98,11 +109,17 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='provideraccount',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('name', ''), _negated=True), fields=('provider', 'name'), name='circuits_provideraccount_unique_provider_name'),
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(('name', ''), _negated=True),
|
||||
fields=('provider', 'name'),
|
||||
name='circuits_provideraccount_unique_provider_name',
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='provideraccount',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'),
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'
|
||||
),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
@@ -111,7 +128,13 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='provider_account',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount'),
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='circuits',
|
||||
to='circuits.provideraccount',
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
@@ -120,6 +143,8 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuit',
|
||||
constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'),
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
87
netbox/circuits/migrations/0044_circuit_groups.py
Normal file
87
netbox/circuits/migrations/0044_circuit_groups.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0043_circuittype_color'),
|
||||
('extras', '0119_notifications'),
|
||||
('tenancy', '0015_contactassignment_rename_content_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CircuitGroup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
(
|
||||
'tenant',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='circuit_groups',
|
||||
to='tenancy.tenant',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Circuit group',
|
||||
'verbose_name_plural': 'Circuit group',
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CircuitGroupAssignment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('priority', models.CharField(blank=True, max_length=50)),
|
||||
(
|
||||
'circuit',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='assignments',
|
||||
to='circuits.circuit',
|
||||
),
|
||||
),
|
||||
(
|
||||
'group',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='assignments',
|
||||
to='circuits.circuitgroup',
|
||||
),
|
||||
),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Circuit group assignment',
|
||||
'verbose_name_plural': 'Circuit group assignments',
|
||||
'ordering': ('group', 'circuit', 'priority', 'pk'),
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuitgroupassignment',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('circuit', 'group'), name='circuits_circuitgroupassignment_unique_circuit_group'
|
||||
),
|
||||
),
|
||||
]
|
||||
27
netbox/circuits/migrations/0045_circuit_distance.py
Normal file
27
netbox/circuits/migrations/0045_circuit_distance.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.0.9 on 2024-09-26 22:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0044_circuit_groups'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='_abs_distance',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='distance',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='distance_unit',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
||||
39
netbox/circuits/migrations/0046_charfield_null_choices.py
Normal file
39
netbox/circuits/migrations/0046_charfield_null_choices.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_null_values(apps, schema_editor):
|
||||
"""
|
||||
Replace empty strings with null values.
|
||||
"""
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
|
||||
Circuit.objects.filter(distance_unit='').update(distance_unit=None)
|
||||
CircuitGroupAssignment.objects.filter(priority='').update(priority=None)
|
||||
CircuitTermination.objects.filter(cable_end='').update(cable_end=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0045_circuit_distance'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='distance_unit',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuitgroupassignment',
|
||||
name='priority',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuittermination',
|
||||
name='cable_end',
|
||||
field=models.CharField(blank=True, max_length=1, null=True),
|
||||
),
|
||||
migrations.RunPython(code=set_null_values, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def copy_site_assignments(apps, schema_editor):
|
||||
"""
|
||||
Copy site ForeignKey values to the Termination GFK.
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
Site = apps.get_model('dcim', 'Site')
|
||||
|
||||
CircuitTermination.objects.filter(site__isnull=False).update(
|
||||
termination_type=ContentType.objects.get_for_model(Site), termination_id=models.F('site_id')
|
||||
)
|
||||
|
||||
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
|
||||
CircuitTermination.objects.filter(provider_network__isnull=False).update(
|
||||
termination_type=ContentType.objects.get_for_model(ProviderNetwork),
|
||||
termination_id=models.F('provider_network_id'),
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0046_charfield_null_choices'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0193_poweroutlet_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='termination_id',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='termination_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
limit_choices_to=models.Q(
|
||||
('model__in', ('region', 'sitegroup', 'site', 'location', 'providernetwork'))
|
||||
),
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
),
|
||||
),
|
||||
# Copy over existing site assignments
|
||||
migrations.RunPython(code=copy_site_assignments, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
@@ -0,0 +1,84 @@
|
||||
# Generated by Django 5.0.9 on 2024-10-21 17:34
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def populate_denormalized_fields(apps, schema_editor):
|
||||
"""
|
||||
Copy site ForeignKey values to the Termination GFK.
|
||||
"""
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
|
||||
terminations = CircuitTermination.objects.filter(site__isnull=False).prefetch_related('site')
|
||||
for termination in terminations:
|
||||
termination._region_id = termination.site.region_id
|
||||
termination._site_group_id = termination.site.group_id
|
||||
termination._site_id = termination.site_id
|
||||
# Note: Location cannot be set prior to migration
|
||||
|
||||
CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0047_circuittermination__termination'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_location',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.location',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_region',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.region',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_site',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.site',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_site_group',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.sitegroup',
|
||||
),
|
||||
),
|
||||
# Populate denormalized FK values
|
||||
migrations.RunPython(code=populate_denormalized_fields, reverse_code=migrations.RunPython.noop),
|
||||
# Delete the site ForeignKey
|
||||
migrations.RemoveField(
|
||||
model_name='circuittermination',
|
||||
name='site',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='circuittermination',
|
||||
old_name='provider_network',
|
||||
new_name='_provider_network',
|
||||
),
|
||||
]
|
||||
21
netbox/circuits/migrations/0049_natural_ordering.py
Normal file
21
netbox/circuits/migrations/0049_natural_ordering.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0048_circuitterminations_cached_relations'),
|
||||
('dcim', '0197_natural_sort_collation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='provider',
|
||||
name='name',
|
||||
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='providernetwork',
|
||||
name='name',
|
||||
field=models.CharField(db_collation='natural_sort', max_length=100),
|
||||
),
|
||||
]
|
||||
147
netbox/circuits/migrations/0050_virtual_circuits.py
Normal file
147
netbox/circuits/migrations/0050_virtual_circuits.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.fields
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0049_natural_ordering'),
|
||||
('dcim', '0196_qinq_svlan'),
|
||||
('extras', '0122_charfield_null_choices'),
|
||||
('tenancy', '0016_charfield_null_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VirtualCircuitType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
encoder=utilities.json.CustomFieldJSONEncoder
|
||||
)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('color', utilities.fields.ColorField(blank=True, max_length=6)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'virtual circuit type',
|
||||
'verbose_name_plural': 'virtual circuit types',
|
||||
'ordering': ('name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VirtualCircuit',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('cid', models.CharField(max_length=100)),
|
||||
('status', models.CharField(default='active', max_length=50)),
|
||||
(
|
||||
'provider_account',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
to='circuits.provideraccount',
|
||||
),
|
||||
),
|
||||
(
|
||||
'provider_network',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
to='circuits.providernetwork',
|
||||
),
|
||||
),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
(
|
||||
'type',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
to='circuits.virtualcircuittype'
|
||||
)
|
||||
),
|
||||
(
|
||||
'tenant',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
to='tenancy.tenant',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'circuit',
|
||||
'verbose_name_plural': 'circuits',
|
||||
'ordering': ['provider_network', 'provider_account', 'cid'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VirtualCircuitTermination',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('role', models.CharField(default='peer', max_length=50)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
(
|
||||
'interface',
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='virtual_circuit_termination',
|
||||
to='dcim.interface',
|
||||
),
|
||||
),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
(
|
||||
'virtual_circuit',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='terminations',
|
||||
to='circuits.virtualcircuit',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'virtual circuit termination',
|
||||
'verbose_name_plural': 'virtual circuit terminations',
|
||||
'ordering': ['virtual_circuit', 'role', 'pk'],
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='virtualcircuit',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider_network', 'cid'), name='circuits_virtualcircuit_unique_provider_network_cid'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='virtualcircuit',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('provider_account', 'cid'), name='circuits_virtualcircuit_unique_provideraccount_cid'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,85 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_member_type(apps, schema_editor):
|
||||
"""
|
||||
Set member_type on any existing CircuitGroupAssignments to the content type for Circuit.
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
|
||||
|
||||
CircuitGroupAssignment.objects.update(
|
||||
member_type=ContentType.objects.get_for_model(Circuit)
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0050_virtual_circuits'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0122_charfield_null_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='circuitgroupassignment',
|
||||
name='circuits_circuitgroupassignment_unique_circuit_group',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='circuitgroupassignment',
|
||||
options={'ordering': ('group', 'member_type', 'member_id', 'priority', 'pk')},
|
||||
),
|
||||
|
||||
# Change member_id to an integer field for the member GFK
|
||||
migrations.RenameField(
|
||||
model_name='circuitgroupassignment',
|
||||
old_name='circuit',
|
||||
new_name='member_id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='circuitgroupassignment',
|
||||
name='member_id',
|
||||
field=models.PositiveBigIntegerField(),
|
||||
),
|
||||
|
||||
# Add content type pointer for the member GFK
|
||||
migrations.AddField(
|
||||
model_name='circuitgroupassignment',
|
||||
name='member_type',
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
blank=True,
|
||||
null=True
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
|
||||
# Populate member_type for any existing assignments
|
||||
migrations.RunPython(code=set_member_type, reverse_code=migrations.RunPython.noop),
|
||||
|
||||
# Disallow null values for member_type
|
||||
migrations.AlterField(
|
||||
model_name='circuitgroupassignment',
|
||||
name='member_type',
|
||||
field=models.ForeignKey(
|
||||
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype'
|
||||
),
|
||||
),
|
||||
|
||||
migrations.AddConstraint(
|
||||
model_name='circuitgroupassignment',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('member_type', 'member_id', 'group'),
|
||||
name='circuits_circuitgroupassignment_unique_member_group'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,2 +1,3 @@
|
||||
from .circuits import *
|
||||
from .providers import *
|
||||
from .virtual_circuits import *
|
||||
|
||||
23
netbox/circuits/models/base.py
Normal file
23
netbox/circuits/models/base.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.models import OrganizationalModel
|
||||
from utilities.fields import ColorField
|
||||
|
||||
__all__ = (
|
||||
'BaseCircuitType',
|
||||
)
|
||||
|
||||
|
||||
class BaseCircuitType(OrganizationalModel):
|
||||
"""
|
||||
Abstract base model to represent a type of physical or virtual circuit.
|
||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||
"Long Haul," "Metro," or "Out-of-Band".
|
||||
"""
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -1,41 +1,41 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.constants import *
|
||||
from dcim.models import CabledObjectModel
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ImageAttachmentsMixin, TagsMixin
|
||||
from utilities.fields import ColorField
|
||||
from netbox.models.mixins import DistanceMixin
|
||||
from netbox.models.features import (
|
||||
ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin,
|
||||
)
|
||||
from .base import BaseCircuitType
|
||||
|
||||
__all__ = (
|
||||
'Circuit',
|
||||
'CircuitGroup',
|
||||
'CircuitGroupAssignment',
|
||||
'CircuitTermination',
|
||||
'CircuitType',
|
||||
)
|
||||
|
||||
|
||||
class CircuitType(OrganizationalModel):
|
||||
class CircuitType(BaseCircuitType):
|
||||
"""
|
||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||
"Long Haul," "Metro," or "Out-of-Band".
|
||||
"""
|
||||
color = ColorField(
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuittype', args=[self.pk])
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('circuit type')
|
||||
verbose_name_plural = _('circuit types')
|
||||
|
||||
|
||||
class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel):
|
||||
"""
|
||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||
circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
|
||||
@@ -59,7 +59,7 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
type = models.ForeignKey(
|
||||
to='CircuitType',
|
||||
to='circuits.CircuitType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuits'
|
||||
)
|
||||
@@ -111,6 +111,13 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
group_assignments = GenericRelation(
|
||||
to='circuits.CircuitGroupAssignment',
|
||||
content_type_field='member_type',
|
||||
object_id_field='member_id',
|
||||
related_query_name='circuit'
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description',
|
||||
@@ -138,9 +145,6 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
def __str__(self):
|
||||
return self.cid
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
||||
def get_status_color(self):
|
||||
return CircuitStatusChoices.colors.get(self.status)
|
||||
|
||||
@@ -151,6 +155,80 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
||||
raise ValidationError({'provider_account': "The assigned account must belong to the assigned provider."})
|
||||
|
||||
|
||||
class CircuitGroup(OrganizationalModel):
|
||||
"""
|
||||
An administrative grouping of Circuits.
|
||||
"""
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_groups',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('circuit group')
|
||||
verbose_name_plural = _('circuit groups')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||
"""
|
||||
Assignment of a physical or virtual circuit to a CircuitGroup with an optional priority.
|
||||
"""
|
||||
member_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
limit_choices_to=CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='+'
|
||||
)
|
||||
member_id = models.PositiveBigIntegerField(
|
||||
verbose_name=_('member ID')
|
||||
)
|
||||
member = GenericForeignKey(
|
||||
ct_field='member_type',
|
||||
fk_field='member_id'
|
||||
)
|
||||
group = models.ForeignKey(
|
||||
to='circuits.CircuitGroup',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignments'
|
||||
)
|
||||
priority = models.CharField(
|
||||
verbose_name=_('priority'),
|
||||
max_length=50,
|
||||
choices=CircuitPriorityChoices,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
prerequisite_models = (
|
||||
'circuits.CircuitGroup',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('group', 'member_type', 'member_id', 'priority', 'pk')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('member_type', 'member_id', 'group'),
|
||||
name='%(app_label)s_%(class)s_unique_member_group'
|
||||
),
|
||||
)
|
||||
verbose_name = _('Circuit group assignment')
|
||||
verbose_name_plural = _('Circuit group assignments')
|
||||
|
||||
def __str__(self):
|
||||
if self.priority:
|
||||
return f"{self.group} ({self.get_priority_display()})"
|
||||
return str(self.group)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuitgroupassignment', args=[self.pk])
|
||||
|
||||
|
||||
class CircuitTermination(
|
||||
CustomFieldsMixin,
|
||||
CustomLinksMixin,
|
||||
@@ -166,22 +244,24 @@ class CircuitTermination(
|
||||
term_side = models.CharField(
|
||||
max_length=1,
|
||||
choices=CircuitTerminationSideChoices,
|
||||
verbose_name=_('termination')
|
||||
verbose_name=_('termination side')
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
termination_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
limit_choices_to=Q(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
provider_network = models.ForeignKey(
|
||||
to='circuits.ProviderNetwork',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
termination_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
termination = GenericForeignKey(
|
||||
ct_field='termination_type',
|
||||
fk_field='termination_id'
|
||||
)
|
||||
port_speed = models.PositiveIntegerField(
|
||||
verbose_name=_('port speed (Kbps)'),
|
||||
blank=True,
|
||||
@@ -212,6 +292,43 @@ class CircuitTermination(
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Cached associations to enable efficient filtering
|
||||
_provider_network = models.ForeignKey(
|
||||
to='circuits.ProviderNetwork',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_location = models.ForeignKey(
|
||||
to='dcim.Location',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_region = models.ForeignKey(
|
||||
to='dcim.Region',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_site_group = models.ForeignKey(
|
||||
to='dcim.SiteGroup',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['circuit', 'term_side']
|
||||
constraints = (
|
||||
@@ -227,16 +344,41 @@ class CircuitTermination(
|
||||
return f'{self.circuit}: Termination {self.term_side}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.circuit.get_absolute_url()
|
||||
return reverse('circuits:circuittermination', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Must define either site *or* provider network
|
||||
if self.site is None and self.provider_network is None:
|
||||
raise ValidationError(_("A circuit termination must attach to either a site or a provider network."))
|
||||
if self.site and self.provider_network:
|
||||
raise ValidationError(_("A circuit termination cannot attach to both a site and a provider network."))
|
||||
if self.termination is None:
|
||||
raise ValidationError(_("A circuit termination must attach to termination."))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Cache objects associated with the terminating object (for filtering)
|
||||
self.cache_related_objects()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def cache_related_objects(self):
|
||||
self._provider_network = self._region = self._site_group = self._site = self._location = None
|
||||
if self.termination_type:
|
||||
termination_type = self.termination_type.model_class()
|
||||
if termination_type == apps.get_model('dcim', 'region'):
|
||||
self._region = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'sitegroup'):
|
||||
self._site_group = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'site'):
|
||||
self._region = self.termination.region
|
||||
self._site_group = self.termination.group
|
||||
self._site = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'location'):
|
||||
self._region = self.termination.site.region
|
||||
self._site_group = self.termination.site.group
|
||||
self._site = self.termination.site
|
||||
self._location = self.termination
|
||||
elif termination_type == apps.get_model('circuits', 'providernetwork'):
|
||||
self._provider_network = self.termination
|
||||
cache_related_objects.alters_data = True
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
@@ -250,7 +392,7 @@ class CircuitTermination(
|
||||
def get_peer_termination(self):
|
||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||
try:
|
||||
return CircuitTermination.objects.prefetch_related('site').get(
|
||||
return CircuitTermination.objects.prefetch_related('termination').get(
|
||||
circuit=self.circuit,
|
||||
term_side=peer_side
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.models import PrimaryModel
|
||||
@@ -22,7 +21,8 @@ class Provider(ContactsMixin, PrimaryModel):
|
||||
verbose_name=_('name'),
|
||||
max_length=100,
|
||||
unique=True,
|
||||
help_text=_('Full name of the provider')
|
||||
help_text=_('Full name of the provider'),
|
||||
db_collation="natural_sort"
|
||||
)
|
||||
slug = models.SlugField(
|
||||
verbose_name=_('slug'),
|
||||
@@ -45,9 +45,6 @@ class Provider(ContactsMixin, PrimaryModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provider', args=[self.pk])
|
||||
|
||||
|
||||
class ProviderAccount(ContactsMixin, PrimaryModel):
|
||||
"""
|
||||
@@ -91,9 +88,6 @@ class ProviderAccount(ContactsMixin, PrimaryModel):
|
||||
return f'{self.account} ({self.name})'
|
||||
return f'{self.account}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provideraccount', args=[self.pk])
|
||||
|
||||
|
||||
class ProviderNetwork(PrimaryModel):
|
||||
"""
|
||||
@@ -102,7 +96,8 @@ class ProviderNetwork(PrimaryModel):
|
||||
"""
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=100
|
||||
max_length=100,
|
||||
db_collation="natural_sort"
|
||||
)
|
||||
provider = models.ForeignKey(
|
||||
to='circuits.Provider',
|
||||
@@ -128,6 +123,3 @@ class ProviderNetwork(PrimaryModel):
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:providernetwork', args=[self.pk])
|
||||
|
||||
191
netbox/circuits/models/virtual_circuits.py
Normal file
191
netbox/circuits/models/virtual_circuits.py
Normal file
@@ -0,0 +1,191 @@
|
||||
from functools import cached_property
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from netbox.models import ChangeLoggedModel, PrimaryModel
|
||||
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin
|
||||
from .base import BaseCircuitType
|
||||
|
||||
__all__ = (
|
||||
'VirtualCircuit',
|
||||
'VirtualCircuitTermination',
|
||||
'VirtualCircuitType',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitType(BaseCircuitType):
|
||||
"""
|
||||
Like physical circuits, virtual circuits can be organized by their functional role. For example, a user might wish
|
||||
to categorize virtual circuits by their technological nature or by product name.
|
||||
"""
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('virtual circuit type')
|
||||
verbose_name_plural = _('virtual circuit types')
|
||||
|
||||
|
||||
class VirtualCircuit(PrimaryModel):
|
||||
"""
|
||||
A virtual connection between two or more endpoints, delivered across one or more physical circuits.
|
||||
"""
|
||||
cid = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name=_('circuit ID'),
|
||||
help_text=_('Unique circuit ID')
|
||||
)
|
||||
provider_network = models.ForeignKey(
|
||||
to='circuits.ProviderNetwork',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='virtual_circuits'
|
||||
)
|
||||
provider_account = models.ForeignKey(
|
||||
to='circuits.ProviderAccount',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
type = models.ForeignKey(
|
||||
to='circuits.VirtualCircuitType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='virtual_circuits'
|
||||
)
|
||||
status = models.CharField(
|
||||
verbose_name=_('status'),
|
||||
max_length=50,
|
||||
choices=CircuitStatusChoices,
|
||||
default=CircuitStatusChoices.STATUS_ACTIVE
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='virtual_circuits',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
group_assignments = GenericRelation(
|
||||
to='circuits.CircuitGroupAssignment',
|
||||
content_type_field='member_type',
|
||||
object_id_field='member_id',
|
||||
related_query_name='virtual_circuit'
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'provider_network', 'provider_account', 'status', 'tenant', 'description',
|
||||
)
|
||||
prerequisite_models = (
|
||||
'circuits.ProviderNetwork',
|
||||
'circuits.VirtualCircuitType',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider_network', 'provider_account', 'cid']
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('provider_network', 'cid'),
|
||||
name='%(app_label)s_%(class)s_unique_provider_network_cid'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('provider_account', 'cid'),
|
||||
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
||||
),
|
||||
)
|
||||
verbose_name = _('virtual circuit')
|
||||
verbose_name_plural = _('virtual circuits')
|
||||
|
||||
def __str__(self):
|
||||
return self.cid
|
||||
|
||||
def get_status_color(self):
|
||||
return CircuitStatusChoices.colors.get(self.status)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.provider_account and self.provider_network.provider != self.provider_account.provider:
|
||||
raise ValidationError({
|
||||
'provider_account': "The assigned account must belong to the provider of the assigned network."
|
||||
})
|
||||
|
||||
@property
|
||||
def provider(self):
|
||||
return self.provider_network.provider
|
||||
|
||||
|
||||
class VirtualCircuitTermination(
|
||||
CustomFieldsMixin,
|
||||
CustomLinksMixin,
|
||||
TagsMixin,
|
||||
ChangeLoggedModel
|
||||
):
|
||||
virtual_circuit = models.ForeignKey(
|
||||
to='circuits.VirtualCircuit',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='terminations'
|
||||
)
|
||||
role = models.CharField(
|
||||
verbose_name=_('role'),
|
||||
max_length=50,
|
||||
choices=VirtualCircuitTerminationRoleChoices,
|
||||
default=VirtualCircuitTerminationRoleChoices.ROLE_PEER
|
||||
)
|
||||
interface = models.OneToOneField(
|
||||
to='dcim.Interface',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='virtual_circuit_termination'
|
||||
)
|
||||
description = models.CharField(
|
||||
verbose_name=_('description'),
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['virtual_circuit', 'role', 'pk']
|
||||
verbose_name = _('virtual circuit termination')
|
||||
verbose_name_plural = _('virtual circuit terminations')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.virtual_circuit}: {self.get_role_display()} termination'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:virtualcircuittermination', args=[self.pk])
|
||||
|
||||
def get_role_color(self):
|
||||
return VirtualCircuitTerminationRoleChoices.colors.get(self.role)
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
objectchange.related_object = self.virtual_circuit
|
||||
return objectchange
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
return self.virtual_circuit
|
||||
|
||||
@cached_property
|
||||
def peer_terminations(self):
|
||||
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_PEER:
|
||||
return self.virtual_circuit.terminations.exclude(pk=self.pk).filter(
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER
|
||||
)
|
||||
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_HUB:
|
||||
return self.virtual_circuit.terminations.filter(
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE
|
||||
)
|
||||
if self.role == VirtualCircuitTerminationRoleChoices.ROLE_SPOKE:
|
||||
return self.virtual_circuit.terminations.filter(
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.interface and not self.interface.is_virtual:
|
||||
raise ValidationError("Virtual circuits may be terminated only to virtual interfaces.")
|
||||
@@ -13,6 +13,17 @@ class CircuitIndex(SearchIndex):
|
||||
display_attrs = ('provider', 'provider_account', 'type', 'status', 'tenant', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class CircuitGroupIndex(SearchIndex):
|
||||
model = models.CircuitGroup
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@register_search
|
||||
class CircuitTerminationIndex(SearchIndex):
|
||||
model = models.CircuitTermination
|
||||
@@ -48,6 +59,7 @@ class ProviderIndex(SearchIndex):
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@register_search
|
||||
class ProviderAccountIndex(SearchIndex):
|
||||
model = models.ProviderAccount
|
||||
fields = (
|
||||
@@ -68,3 +80,34 @@ class ProviderNetworkIndex(SearchIndex):
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('provider', 'service_id', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class VirtualCircuitIndex(SearchIndex):
|
||||
model = models.VirtualCircuit
|
||||
fields = (
|
||||
('cid', 100),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('provider', 'provider_network', 'provider_account', 'status', 'tenant', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class VirtualCircuitTerminationIndex(SearchIndex):
|
||||
model = models.VirtualCircuitTermination
|
||||
fields = (
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('virtual_circuit', 'role', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class VirtualCircuitTypeIndex(SearchIndex):
|
||||
model = models.VirtualCircuitType
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .circuits import *
|
||||
from .columns import *
|
||||
from .providers import *
|
||||
from .virtual_circuits import *
|
||||
|
||||
@@ -9,16 +9,17 @@ from netbox.tables import NetBoxTable, columns
|
||||
from .columns import CommitRateColumn
|
||||
|
||||
__all__ = (
|
||||
'CircuitGroupAssignmentTable',
|
||||
'CircuitGroupTable',
|
||||
'CircuitTable',
|
||||
'CircuitTerminationTable',
|
||||
'CircuitTypeTable',
|
||||
)
|
||||
|
||||
|
||||
CIRCUITTERMINATION_LINK = """
|
||||
{% if value.site %}
|
||||
<a href="{{ value.site.get_absolute_url }}">{{ value.site }}</a>
|
||||
{% elif value.provider_network %}
|
||||
<a href="{{ value.provider_network.get_absolute_url }}">{{ value.provider_network }}</a>
|
||||
{% if value.termination %}
|
||||
<a href="{{ value.termination.get_absolute_url }}">{{ value.termination }}</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -41,9 +42,10 @@ class CircuitTypeTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitType
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions',
|
||||
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated',
|
||||
'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
|
||||
|
||||
|
||||
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
@@ -59,32 +61,159 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
linkify=True,
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
type = tables.Column(
|
||||
verbose_name=_('Type'),
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_a = tables.TemplateColumn(
|
||||
termination_a = columns.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
orderable=False,
|
||||
verbose_name=_('Side A')
|
||||
)
|
||||
termination_z = tables.TemplateColumn(
|
||||
termination_z = columns.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
orderable=False,
|
||||
verbose_name=_('Side Z')
|
||||
)
|
||||
commit_rate = CommitRateColumn(
|
||||
verbose_name=_('Commit Rate')
|
||||
)
|
||||
distance = columns.DistanceColumn()
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
verbose_name=_('Comments')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuit_list'
|
||||
)
|
||||
assignments = columns.ManyToManyColumn(
|
||||
verbose_name=_('Assignments'),
|
||||
linkify_item=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Circuit
|
||||
fields = (
|
||||
'pk', 'id', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'tenant_group',
|
||||
'termination_a', 'termination_z', 'install_date', 'termination_date', 'commit_rate', 'description',
|
||||
'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||
'comments', 'contacts', 'tags', 'created', 'last_updated', 'assignments',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||
)
|
||||
|
||||
|
||||
class CircuitTerminationTable(NetBoxTable):
|
||||
circuit = tables.Column(
|
||||
verbose_name=_('Circuit'),
|
||||
linkify=True
|
||||
)
|
||||
provider = tables.Column(
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True,
|
||||
accessor='circuit.provider'
|
||||
)
|
||||
term_side = tables.Column(
|
||||
verbose_name=_('Side')
|
||||
)
|
||||
termination_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Termination Type'),
|
||||
)
|
||||
termination = tables.Column(
|
||||
verbose_name=_('Termination Point'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
# Termination types
|
||||
site = tables.Column(
|
||||
verbose_name=_('Site'),
|
||||
linkify=True,
|
||||
accessor='_site'
|
||||
)
|
||||
site_group = tables.Column(
|
||||
verbose_name=_('Site Group'),
|
||||
linkify=True,
|
||||
accessor='_sitegroup'
|
||||
)
|
||||
region = tables.Column(
|
||||
verbose_name=_('Region'),
|
||||
linkify=True,
|
||||
accessor='_region'
|
||||
)
|
||||
location = tables.Column(
|
||||
verbose_name=_('Location'),
|
||||
linkify=True,
|
||||
accessor='_location'
|
||||
)
|
||||
provider_network = tables.Column(
|
||||
verbose_name=_('Provider Network'),
|
||||
linkify=True,
|
||||
accessor='_provider_network'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitTermination
|
||||
fields = (
|
||||
'pk', 'id', 'circuit', 'provider', 'term_side', 'termination_type', 'termination', 'site_group', 'region',
|
||||
'site', 'location', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
'description', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'circuit', 'provider', 'term_side', 'termination_type', 'termination', 'description',
|
||||
)
|
||||
|
||||
|
||||
class CircuitGroupTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
circuit_group_assignment_count = columns.LinkedCountColumn(
|
||||
viewname='circuits:circuitgroupassignment_list',
|
||||
url_params={'group_id': 'pk'},
|
||||
verbose_name=_('Circuits')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuitgroup_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitGroup
|
||||
fields = (
|
||||
'pk', 'name', 'description', 'circuit_group_assignment_count', 'tags',
|
||||
'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'description', 'circuit_group_assignment_count')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTable(NetBoxTable):
|
||||
group = tables.Column(
|
||||
verbose_name=_('Group'),
|
||||
linkify=True
|
||||
)
|
||||
provider = tables.Column(
|
||||
accessor='member__provider',
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True
|
||||
)
|
||||
member_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Type')
|
||||
)
|
||||
member = tables.Column(
|
||||
verbose_name=_('Circuit'),
|
||||
linkify=True
|
||||
)
|
||||
priority = tables.Column(
|
||||
verbose_name=_('Priority'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:circuitgroupassignment_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitGroupAssignment
|
||||
fields = (
|
||||
'pk', 'id', 'group', 'provider', 'member_type', 'member', 'priority', 'created', 'last_updated', 'actions',
|
||||
'tags',
|
||||
)
|
||||
default_columns = ('pk', 'group', 'provider', 'member_type', 'member', 'priority')
|
||||
|
||||
@@ -25,7 +25,7 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
||||
account_count = columns.LinkedCountColumn(
|
||||
accessor=tables.A('accounts__count'),
|
||||
viewname='circuits:provideraccount_list',
|
||||
url_params={'account_id': 'pk'},
|
||||
url_params={'provider_id': 'pk'},
|
||||
verbose_name=_('Account Count')
|
||||
)
|
||||
asns = columns.ManyToManyColumn(
|
||||
|
||||
124
netbox/circuits/tables/virtual_circuits.py
Normal file
124
netbox/circuits/tables/virtual_circuits.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.models import *
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
__all__ = (
|
||||
'VirtualCircuitTable',
|
||||
'VirtualCircuitTerminationTable',
|
||||
'VirtualCircuitTypeTable',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTypeTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Name'),
|
||||
)
|
||||
color = columns.ColorColumn()
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:virtualcircuittype_list'
|
||||
)
|
||||
virtual_circuit_count = columns.LinkedCountColumn(
|
||||
viewname='circuits:virtualcircuit_list',
|
||||
url_params={'type_id': 'pk'},
|
||||
verbose_name=_('Circuits')
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VirtualCircuitType
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'virtual_circuit_count', 'color', 'description', 'slug', 'tags', 'created',
|
||||
'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'virtual_circuit_count', 'color', 'description')
|
||||
|
||||
|
||||
class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
cid = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Circuit ID')
|
||||
)
|
||||
provider = tables.Column(
|
||||
accessor=tables.A('provider_network__provider'),
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True
|
||||
)
|
||||
provider_network = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Provider network')
|
||||
)
|
||||
provider_account = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
type = tables.Column(
|
||||
verbose_name=_('Type'),
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_count = columns.LinkedCountColumn(
|
||||
viewname='circuits:virtualcircuittermination_list',
|
||||
url_params={'virtual_circuit_id': 'pk'},
|
||||
verbose_name=_('Terminations')
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:virtualcircuit_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VirtualCircuit
|
||||
fields = (
|
||||
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
|
||||
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
|
||||
'termination_count', 'description',
|
||||
)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationTable(NetBoxTable):
|
||||
virtual_circuit = tables.Column(
|
||||
verbose_name=_('Virtual circuit'),
|
||||
linkify=True
|
||||
)
|
||||
provider = tables.Column(
|
||||
accessor=tables.A('virtual_circuit__provider_network__provider'),
|
||||
verbose_name=_('Provider'),
|
||||
linkify=True
|
||||
)
|
||||
provider_network = tables.Column(
|
||||
accessor=tables.A('virtual_circuit__provider_network'),
|
||||
linkify=True,
|
||||
verbose_name=_('Provider network')
|
||||
)
|
||||
provider_account = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
role = columns.ChoiceFieldColumn()
|
||||
device = tables.Column(
|
||||
accessor=tables.A('interface__device'),
|
||||
linkify=True,
|
||||
verbose_name=_('Device')
|
||||
)
|
||||
interface = tables.Column(
|
||||
verbose_name=_('Interface'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = VirtualCircuitTermination
|
||||
fields = (
|
||||
'pk', 'id', 'virtual_circuit', 'provider', 'provider_network', 'provider_account', 'role', 'interfaces',
|
||||
'description', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'virtual_circuit', 'role', 'device', 'interface', 'description',
|
||||
)
|
||||
@@ -2,7 +2,8 @@ from django.urls import reverse
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
|
||||
from ipam.models import ASN, RIR
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
|
||||
@@ -92,10 +93,11 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Circuit
|
||||
brief_fields = ['cid', 'description', 'display', 'id', 'url']
|
||||
brief_fields = ['cid', 'description', 'display', 'id', 'provider', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
user_permissions = ('circuits.view_provider', 'circuits.view_circuittype')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -119,9 +121,15 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
CircuitType.objects.bulk_create(circuit_types)
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
|
||||
Circuit(
|
||||
cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]
|
||||
),
|
||||
Circuit(
|
||||
cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]
|
||||
),
|
||||
Circuit(
|
||||
cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]
|
||||
),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
@@ -150,6 +158,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitTermination
|
||||
brief_fields = ['_occupied', 'cable', 'circuit', 'description', 'display', 'id', 'term_side', 'url']
|
||||
user_permissions = ('circuits.view_circuit', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -179,10 +188,10 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, provider_network=provider_networks[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, provider_network=provider_networks[1]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, termination=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, termination=provider_networks[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, termination=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, termination=provider_networks[1]),
|
||||
)
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
@@ -190,13 +199,15 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_A,
|
||||
'site': sites[0].pk,
|
||||
'termination_type': 'dcim.site',
|
||||
'termination_id': sites[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_Z,
|
||||
'provider_network': provider_networks[0].pk,
|
||||
'termination_type': 'circuits.providernetwork',
|
||||
'termination_id': provider_networks[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
]
|
||||
@@ -206,9 +217,42 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitGroup
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
circuit_groups = (
|
||||
CircuitGroup(name="Circuit Group 1", slug='circuit-group-1'),
|
||||
CircuitGroup(name="Circuit Group 2", slug='circuit-group-2'),
|
||||
CircuitGroup(name="Circuit Group 3", slug='circuit-group-3'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Circuit Group 4',
|
||||
'slug': 'circuit-group-4',
|
||||
},
|
||||
{
|
||||
'name': 'Circuit Group 5',
|
||||
'slug': 'circuit-group-5',
|
||||
},
|
||||
{
|
||||
'name': 'Circuit Group 6',
|
||||
'slug': 'circuit-group-6',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderAccount
|
||||
brief_fields = ['account', 'description', 'display', 'id', 'name', 'url']
|
||||
user_permissions = ('circuits.view_provider',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -249,9 +293,85 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
||||
model = CircuitGroupAssignment
|
||||
brief_fields = ['display', 'group', 'id', 'member', 'member_id', 'member_type', 'priority', 'url']
|
||||
bulk_update_data = {
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
}
|
||||
user_permissions = ('circuits.view_circuit', 'circuits.view_circuitgroup')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
|
||||
CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
|
||||
CircuitGroup(name='Circuit Group 5', slug='circuit-group-5'),
|
||||
CircuitGroup(name='Circuit Group 6', slug='circuit-group-6'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 4', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 5', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 6', provider=provider, type=circuittype),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
member=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
member=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
member=circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
)
|
||||
CircuitGroupAssignment.objects.bulk_create(assignments)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'group': circuit_groups[3].pk,
|
||||
'member_type': 'circuits.circuit',
|
||||
'member_id': circuits[3].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_PRIMARY,
|
||||
},
|
||||
{
|
||||
'group': circuit_groups[4].pk,
|
||||
'member_type': 'circuits.circuit',
|
||||
'member_id': circuits[4].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_SECONDARY,
|
||||
},
|
||||
{
|
||||
'group': circuit_groups[5].pk,
|
||||
'member_type': 'circuits.circuit',
|
||||
'member_id': circuits[5].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_TERTIARY,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderNetwork
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||
user_permissions = ('circuits.view_provider', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -287,3 +407,290 @@ class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||
'provider': providers[1].pk,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class VirtualCircuitTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualCircuitType
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url', 'virtual_circuit_count']
|
||||
create_data = (
|
||||
{
|
||||
'name': 'Virtual Circuit Type 4',
|
||||
'slug': 'virtual-circuit-type-4',
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Circuit Type 5',
|
||||
'slug': 'virtual-circuit-type-5',
|
||||
},
|
||||
{
|
||||
'name': 'Virtual Circuit Type 6',
|
||||
'slug': 'virtual-circuit-type-6',
|
||||
},
|
||||
)
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
virtual_circuit_types = (
|
||||
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
|
||||
)
|
||||
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
|
||||
|
||||
|
||||
class VirtualCircuitTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualCircuit
|
||||
brief_fields = ['cid', 'description', 'display', 'id', 'provider_network', 'url']
|
||||
bulk_update_data = {
|
||||
'status': 'planned',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||
virtual_circuit_type = VirtualCircuitType.objects.create(
|
||||
name='Virtual Circuit Type 1',
|
||||
slug='virtual-circuit-type-1'
|
||||
)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
type=virtual_circuit_type,
|
||||
cid='Virtual Circuit 1'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
type=virtual_circuit_type,
|
||||
cid='Virtual Circuit 2'
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
type=virtual_circuit_type,
|
||||
cid='Virtual Circuit 3'
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'cid': 'Virtual Circuit 4',
|
||||
'provider_network': provider_network.pk,
|
||||
'provider_account': provider_account.pk,
|
||||
'type': virtual_circuit_type.pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
{
|
||||
'cid': 'Virtual Circuit 5',
|
||||
'provider_network': provider_network.pk,
|
||||
'provider_account': provider_account.pk,
|
||||
'type': virtual_circuit_type.pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
{
|
||||
'cid': 'Virtual Circuit 6',
|
||||
'provider_network': provider_network.pk,
|
||||
'provider_account': provider_account.pk,
|
||||
'type': virtual_circuit_type.pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class VirtualCircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = VirtualCircuitTermination
|
||||
brief_fields = ['description', 'display', 'id', 'interface', 'role', 'url', 'virtual_circuit']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
|
||||
devices = (
|
||||
Device(site=site, name='hub', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke1', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke2', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke3', device_type=device_type, role=device_role),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
physical_interfaces = (
|
||||
Interface(device=devices[0], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[1], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[2], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[3], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
)
|
||||
Interface.objects.bulk_create(physical_interfaces)
|
||||
|
||||
virtual_interfaces = (
|
||||
# Point-to-point VCs
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.2',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.3',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[1],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[2],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[3],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
|
||||
# Hub and spoke VCs
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
)
|
||||
Interface.objects.bulk_create(virtual_interfaces)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||
virtual_circuit_type = VirtualCircuitType.objects.create(
|
||||
name='Virtual Circuit Type 1',
|
||||
slug='virtual-circuit-type-1'
|
||||
)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 4',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
virtual_circuit_terminations = (
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[0]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[3]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[1]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[4]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[2]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[5]
|
||||
),
|
||||
)
|
||||
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'virtual_circuit': virtual_circuits[3].pk,
|
||||
'role': VirtualCircuitTerminationRoleChoices.ROLE_HUB,
|
||||
'interface': virtual_interfaces[6].pk
|
||||
},
|
||||
{
|
||||
'virtual_circuit': virtual_circuits[3].pk,
|
||||
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||
'interface': virtual_interfaces[7].pk
|
||||
},
|
||||
{
|
||||
'virtual_circuit': virtual_circuits[3].pk,
|
||||
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||
'interface': virtual_interfaces[8].pk
|
||||
},
|
||||
{
|
||||
'virtual_circuit': virtual_circuits[3].pk,
|
||||
'role': VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||
'interface': virtual_interfaces[9].pk
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3,8 +3,10 @@ from django.test import TestCase
|
||||
from circuits.choices import *
|
||||
from circuits.filtersets import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Cable, Region, Site, SiteGroup
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Region, Site, SiteGroup
|
||||
from ipam.models import ASN, RIR
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
|
||||
@@ -69,10 +71,12 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
CircuitTermination.objects.bulk_create((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
|
||||
))
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[0], term_side='A'),
|
||||
)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
@@ -222,24 +226,93 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
|
||||
Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
|
||||
Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
|
||||
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
|
||||
Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE),
|
||||
Circuit(
|
||||
provider=providers[0],
|
||||
provider_account=provider_accounts[0],
|
||||
tenant=tenants[0],
|
||||
type=circuit_types[0],
|
||||
cid='Test Circuit 1',
|
||||
install_date='2020-01-01',
|
||||
termination_date='2021-01-01',
|
||||
commit_rate=1000,
|
||||
status=CircuitStatusChoices.STATUS_ACTIVE,
|
||||
description='foobar1',
|
||||
distance=10,
|
||||
distance_unit=DistanceUnitChoices.UNIT_FOOT,
|
||||
),
|
||||
Circuit(
|
||||
provider=providers[0],
|
||||
provider_account=provider_accounts[0],
|
||||
tenant=tenants[0],
|
||||
type=circuit_types[0],
|
||||
cid='Test Circuit 2',
|
||||
install_date='2020-01-02',
|
||||
termination_date='2021-01-02',
|
||||
commit_rate=2000,
|
||||
status=CircuitStatusChoices.STATUS_ACTIVE,
|
||||
description='foobar2',
|
||||
distance=20,
|
||||
distance_unit=DistanceUnitChoices.UNIT_METER,
|
||||
),
|
||||
Circuit(
|
||||
provider=providers[0],
|
||||
provider_account=provider_accounts[1],
|
||||
tenant=tenants[1],
|
||||
type=circuit_types[0],
|
||||
cid='Test Circuit 3',
|
||||
install_date='2020-01-03',
|
||||
termination_date='2021-01-03',
|
||||
commit_rate=3000,
|
||||
status=CircuitStatusChoices.STATUS_PLANNED,
|
||||
distance=30,
|
||||
distance_unit=DistanceUnitChoices.UNIT_METER,
|
||||
),
|
||||
Circuit(
|
||||
provider=providers[1],
|
||||
provider_account=provider_accounts[1],
|
||||
tenant=tenants[1],
|
||||
type=circuit_types[1],
|
||||
cid='Test Circuit 4',
|
||||
install_date='2020-01-04',
|
||||
termination_date='2021-01-04',
|
||||
commit_rate=4000,
|
||||
status=CircuitStatusChoices.STATUS_PLANNED,
|
||||
),
|
||||
Circuit(
|
||||
provider=providers[1],
|
||||
provider_account=provider_accounts[2],
|
||||
tenant=tenants[2],
|
||||
type=circuit_types[1],
|
||||
cid='Test Circuit 5',
|
||||
install_date='2020-01-05',
|
||||
termination_date='2021-01-05',
|
||||
commit_rate=5000,
|
||||
status=CircuitStatusChoices.STATUS_OFFLINE,
|
||||
),
|
||||
Circuit(
|
||||
provider=providers[1],
|
||||
provider_account=provider_accounts[2],
|
||||
tenant=tenants[2],
|
||||
type=circuit_types[1],
|
||||
cid='Test Circuit 6',
|
||||
install_date='2020-01-06',
|
||||
termination_date='2021-01-06',
|
||||
commit_rate=6000,
|
||||
status=CircuitStatusChoices.STATUS_OFFLINE,
|
||||
),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = ((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[2], termination=sites[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], termination=provider_networks[2], term_side='A'),
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
@@ -289,6 +362,14 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_distance(self):
|
||||
params = {'distance': [10, 20]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_distance_unit(self):
|
||||
params = {'distance_unit': DistanceUnitChoices.UNIT_FOOT}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -351,40 +432,89 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_networks = (
|
||||
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
|
||||
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
|
||||
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
|
||||
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 2'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 3'),
|
||||
Circuit(provider=providers[1], type=circuit_types[0], cid='Circuit 2'),
|
||||
Circuit(provider=providers[2], type=circuit_types[0], cid='Circuit 3'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 4'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 5'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 6'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 7'),
|
||||
Circuit(provider=providers[1], type=circuit_types[0], cid='Circuit 5'),
|
||||
Circuit(provider=providers[2], type=circuit_types[0], cid='Circuit 6'),
|
||||
Circuit(provider=providers[2], type=circuit_types[0], cid='Circuit 7'),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = ((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC', description='foobar1'),
|
||||
CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF', description='foobar2'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
|
||||
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[6], provider_network=provider_networks[0], term_side='A', mark_connected=True),
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
circuit_terminations = (
|
||||
CircuitTermination(
|
||||
circuit=circuits[0],
|
||||
termination=sites[0],
|
||||
term_side='A',
|
||||
port_speed=1000,
|
||||
upstream_speed=1000,
|
||||
xconnect_id='ABC',
|
||||
description='foobar1',
|
||||
),
|
||||
CircuitTermination(
|
||||
circuit=circuits[0],
|
||||
termination=sites[1],
|
||||
term_side='Z',
|
||||
port_speed=1000,
|
||||
upstream_speed=1000,
|
||||
xconnect_id='DEF',
|
||||
description='foobar2',
|
||||
),
|
||||
CircuitTermination(
|
||||
circuit=circuits[1],
|
||||
termination=sites[1],
|
||||
term_side='A',
|
||||
port_speed=2000,
|
||||
upstream_speed=2000,
|
||||
xconnect_id='GHI',
|
||||
),
|
||||
CircuitTermination(
|
||||
circuit=circuits[1],
|
||||
termination=sites[2],
|
||||
term_side='Z',
|
||||
port_speed=2000,
|
||||
upstream_speed=2000,
|
||||
xconnect_id='JKL',
|
||||
),
|
||||
CircuitTermination(
|
||||
circuit=circuits[2],
|
||||
termination=sites[2],
|
||||
term_side='A',
|
||||
port_speed=3000,
|
||||
upstream_speed=3000,
|
||||
xconnect_id='MNO',
|
||||
),
|
||||
CircuitTermination(
|
||||
circuit=circuits[2],
|
||||
termination=sites[0],
|
||||
term_side='Z',
|
||||
port_speed=3000,
|
||||
upstream_speed=3000,
|
||||
xconnect_id='PQR',
|
||||
),
|
||||
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], termination=provider_networks[2], term_side='A'),
|
||||
CircuitTermination(
|
||||
circuit=circuits[6], termination=provider_networks[0], term_side='A', mark_connected=True
|
||||
),
|
||||
)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
Cable(a_terminations=[circuit_terminations[0]], b_terminations=[circuit_terminations[1]]).save()
|
||||
|
||||
@@ -413,10 +543,17 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_circuit_id(self):
|
||||
circuits = Circuit.objects.all()[:2]
|
||||
circuits = Circuit.objects.filter(cid__in=['Circuit 1', 'Circuit 2'])
|
||||
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_provider(self):
|
||||
providers = Provider.objects.all()[:2]
|
||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -442,6 +579,185 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
|
||||
|
||||
|
||||
class CircuitGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
filterset = CircuitGroupFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
tenant_groups = (
|
||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
|
||||
)
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
|
||||
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
|
||||
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
CircuitGroup.objects.bulk_create((
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1', description='foobar1', tenant=tenants[0]),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2', description='foobar2', tenant=tenants[1]),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3', tenant=tenants[1]),
|
||||
))
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Circuit Group 1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['circuit-group-1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant(self):
|
||||
tenants = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_tenant_group(self):
|
||||
tenant_groups = TenantGroup.objects.all()[:2]
|
||||
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
filterset = CircuitGroupAssignmentFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
providers = Provider.objects.bulk_create((
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
))
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=providers[0], type=circuit_type),
|
||||
Circuit(cid='Circuit 2', provider=providers[1], type=circuit_type),
|
||||
Circuit(cid='Circuit 3', provider=providers[2], type=circuit_type),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
provider_networks = (
|
||||
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
|
||||
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
virtual_circuit_type = VirtualCircuitType.objects.create(
|
||||
name='Virtual Circuit Type 1',
|
||||
slug='virtual-circuit-type-1'
|
||||
)
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[1],
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[2],
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
member=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
member=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
member=circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
member=virtual_circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
member=virtual_circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
member=virtual_circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
)
|
||||
CircuitGroupAssignment.objects.bulk_create(assignments)
|
||||
|
||||
def test_group(self):
|
||||
groups = CircuitGroup.objects.all()[:2]
|
||||
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_circuit(self):
|
||||
circuits = Circuit.objects.all()[:2]
|
||||
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'circuit': [circuits[0].cid, circuits[1].cid]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_virtual_circuit(self):
|
||||
virtual_circuits = VirtualCircuit.objects.all()[:2]
|
||||
params = {'virtual_circuit_id': [virtual_circuits[0].pk, virtual_circuits[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'virtual_circuit': [virtual_circuits[0].cid, virtual_circuits[1].cid]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider(self):
|
||||
providers = Provider.objects.all()[:2]
|
||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = ProviderNetworkFilterSet
|
||||
@@ -526,3 +842,347 @@ class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class VirtualCircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
filterset = VirtualCircuitTypeFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
VirtualCircuitType.objects.bulk_create((
|
||||
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1', description='foobar1'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2', description='foobar2'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
|
||||
))
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Virtual Circuit Type 1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['virtual-circuit-type-1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class VirtualCircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
filterset = VirtualCircuitFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
tenant_groups = (
|
||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
|
||||
)
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
tenants = (
|
||||
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
|
||||
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
|
||||
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='A'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[1], account='B'),
|
||||
ProviderAccount(name='Provider Account 3', provider=providers[2], account='C'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
provider_networks = (
|
||||
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
|
||||
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
virtual_circuit_types = (
|
||||
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 3', slug='virtual-circuit-type-3'),
|
||||
)
|
||||
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
|
||||
|
||||
virutal_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
tenant=tenants[0],
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_types[0],
|
||||
status=CircuitStatusChoices.STATUS_PLANNED,
|
||||
description='virtualcircuit1',
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[1],
|
||||
provider_account=provider_accounts[1],
|
||||
tenant=tenants[1],
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_types[1],
|
||||
status=CircuitStatusChoices.STATUS_ACTIVE,
|
||||
description='virtualcircuit2',
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[2],
|
||||
provider_account=provider_accounts[2],
|
||||
tenant=tenants[2],
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_types[2],
|
||||
status=CircuitStatusChoices.STATUS_DEPROVISIONING,
|
||||
description='virtualcircuit3',
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virutal_circuits)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'virtualcircuit1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_cid(self):
|
||||
params = {'cid': ['Virtual Circuit 1', 'Virtual Circuit 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider(self):
|
||||
providers = Provider.objects.all()[:2]
|
||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider_account(self):
|
||||
provider_accounts = ProviderAccount.objects.all()[:2]
|
||||
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider_network(self):
|
||||
provider_networks = ProviderNetwork.objects.all()[:2]
|
||||
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_type(self):
|
||||
virtual_circuit_types = VirtualCircuitType.objects.all()[:2]
|
||||
params = {'type_id': [virtual_circuit_types[0].pk, virtual_circuit_types[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'type': [virtual_circuit_types[0].slug, virtual_circuit_types[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['virtualcircuit1', 'virtualcircuit2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant(self):
|
||||
tenants = Tenant.objects.all()[:2]
|
||||
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_tenant_group(self):
|
||||
tenant_groups = TenantGroup.objects.all()[:2]
|
||||
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
filterset = VirtualCircuitTerminationFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
|
||||
devices = (
|
||||
Device(site=site, name='Device 1', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='Device 2', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='Device 3', device_type=device_type, role=device_role),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
virtual_interfaces = (
|
||||
# Device 1
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.1',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.2',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
# Device 2
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.1',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.2',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
# Device 3
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.1',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.2',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
)
|
||||
Interface.objects.bulk_create(virtual_interfaces)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
provider_networks = (
|
||||
ProviderNetwork(provider=providers[0], name='Provider Network 1'),
|
||||
ProviderNetwork(provider=providers[1], name='Provider Network 2'),
|
||||
ProviderNetwork(provider=providers[2], name='Provider Network 3'),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
provider_accounts = (
|
||||
ProviderAccount(provider=providers[0], account='Provider Account 1'),
|
||||
ProviderAccount(provider=providers[1], account='Provider Account 2'),
|
||||
ProviderAccount(provider=providers[2], account='Provider Account 3'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
virtual_circuit_type = VirtualCircuitType.objects.create(
|
||||
name='Virtual Circuit Type 1',
|
||||
slug='virtual-circuit-type-1'
|
||||
)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[1],
|
||||
provider_account=provider_accounts[1],
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[2],
|
||||
provider_account=provider_accounts[2],
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
virtual_circuit_terminations = (
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB,
|
||||
interface=virtual_interfaces[0],
|
||||
description='termination1'
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE,
|
||||
interface=virtual_interfaces[3],
|
||||
description='termination2'
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[1],
|
||||
description='termination3'
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[4],
|
||||
description='termination4'
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[2],
|
||||
description='termination5'
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[5],
|
||||
description='termination6'
|
||||
),
|
||||
)
|
||||
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'termination1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['termination1', 'termination2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_virtual_circuit_id(self):
|
||||
virtual_circuits = VirtualCircuit.objects.filter()[:2]
|
||||
params = {'virtual_circuit_id': [virtual_circuits[0].pk, virtual_circuits[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_provider(self):
|
||||
providers = Provider.objects.all()[:2]
|
||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_provider_network(self):
|
||||
provider_networks = ProviderNetwork.objects.all()[:2]
|
||||
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_provider_account(self):
|
||||
provider_accounts = ProviderAccount.objects.all()[:2]
|
||||
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'provider_account': [provider_accounts[0].account, provider_accounts[1].account]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_interface(self):
|
||||
interfaces = Interface.objects.all()[:2]
|
||||
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import datetime
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Cable, Interface, Site
|
||||
from core.models import ObjectType
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from dcim.models import Cable, Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
|
||||
from ipam.models import ASN, RIR
|
||||
from netbox.choices import ImportFormatChoices
|
||||
from users.models import ObjectPermission
|
||||
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
||||
|
||||
|
||||
@@ -115,6 +120,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
Site.objects.create(name='Site 1', slug='site-1')
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
@@ -135,9 +141,15 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
CircuitType.objects.bulk_create(circuittypes)
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
|
||||
Circuit(
|
||||
cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]
|
||||
),
|
||||
Circuit(
|
||||
cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]
|
||||
),
|
||||
Circuit(
|
||||
cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]
|
||||
),
|
||||
)
|
||||
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
@@ -167,7 +179,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
f"id,cid,description,status",
|
||||
"id,cid,description,status",
|
||||
f"{circuits[0].pk},Circuit 7,New description7,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
f"{circuits[1].pk},Circuit 8,New description8,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
f"{circuits[2].pk},Circuit 9,New description9,{CircuitStatusChoices.STATUS_DECOMMISSIONED}",
|
||||
@@ -184,6 +196,55 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||
def test_bulk_import_objects_with_terminations(self):
|
||||
site = Site.objects.first()
|
||||
json_data = f"""
|
||||
[
|
||||
{{
|
||||
"cid": "Circuit 7",
|
||||
"provider": "Provider 1",
|
||||
"type": "Circuit Type 1",
|
||||
"status": "active",
|
||||
"description": "Testing Import",
|
||||
"terminations": [
|
||||
{{
|
||||
"term_side": "A",
|
||||
"termination_type": "dcim.site",
|
||||
"termination_id": "{site.pk}"
|
||||
}},
|
||||
{{
|
||||
"term_side": "Z",
|
||||
"termination_type": "dcim.site",
|
||||
"termination_id": "{site.pk}"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
"""
|
||||
|
||||
initial_count = self._get_queryset().count()
|
||||
data = {
|
||||
'data': json_data,
|
||||
'format': ImportFormatChoices.JSON,
|
||||
}
|
||||
|
||||
# Assign model-level permission
|
||||
obj_perm = ObjectPermission(
|
||||
name='Test permission',
|
||||
actions=['add']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
|
||||
|
||||
# Try GET with model-level permission
|
||||
self.assertHttpStatus(self.client.get(self._get_url('bulk_import')), 200)
|
||||
|
||||
# Test POST with permission
|
||||
self.assertHttpStatus(self.client.post(self._get_url('bulk_import'), data), 302)
|
||||
self.assertEqual(self._get_queryset().count(), initial_count + 1)
|
||||
|
||||
|
||||
class ProviderAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = ProviderAccount
|
||||
@@ -287,10 +348,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class CircuitTerminationTestCase(
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
):
|
||||
class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = CircuitTermination
|
||||
|
||||
@classmethod
|
||||
@@ -313,17 +371,38 @@ class CircuitTerminationTestCase(
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], term_side='A', site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side='Z', site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='A', site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='Z', site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[0], term_side='A', termination=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side='Z', termination=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='A', termination=sites[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='Z', termination=sites[1]),
|
||||
)
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
cls.form_data = {
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': 'A',
|
||||
'site': sites[2].pk,
|
||||
'termination_type': ContentType.objects.get_for_model(Site).pk,
|
||||
'termination': sites[2].pk,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
site = sites[0].pk
|
||||
cls.csv_data = (
|
||||
"circuit,term_side,termination_type,termination_id,description",
|
||||
f"Circuit 3,A,dcim.site,{site},Foo",
|
||||
f"Circuit 3,Z,dcim.site,{site},Bar",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,port_speed,description",
|
||||
f"{circuit_terminations[0].pk},100,New description7",
|
||||
f"{circuit_terminations[1].pk},200,New description8",
|
||||
f"{circuit_terminations[2].pk},300,New description9",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'port_speed': 400,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@@ -340,3 +419,519 @@ class CircuitTerminationTestCase(
|
||||
|
||||
response = self.client.get(reverse('circuits:circuittermination_trace', kwargs={'pk': circuittermination.pk}))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class CircuitGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = CircuitGroup
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Circuit Group X',
|
||||
'slug': 'circuit-group-x',
|
||||
'description': 'A new Circuit Group',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug",
|
||||
"Circuit Group 4,circuit-group-4",
|
||||
"Circuit Group 5,circuit-group-5",
|
||||
"Circuit Group 6,circuit-group-6",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,description",
|
||||
f"{circuit_groups[0].pk},Circuit Group 7,New description7",
|
||||
f"{circuit_groups[1].pk},Circuit Group 8,New description8",
|
||||
f"{circuit_groups[2].pk},Circuit Group 9,New description9",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'Foo',
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupAssignmentTestCase(
|
||||
ViewTestCases.CreateObjectViewTestCase,
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
ViewTestCases.ListObjectsViewTestCase,
|
||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
||||
ViewTestCases.BulkImportObjectsViewTestCase,
|
||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||
):
|
||||
model = CircuitGroupAssignment
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
circuit_groups = (
|
||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
|
||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
|
||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
|
||||
CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
|
||||
)
|
||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
|
||||
Circuit(cid='Circuit 4', provider=provider, type=circuittype),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
assignments = (
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[0],
|
||||
member=circuits[0],
|
||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[1],
|
||||
member=circuits[1],
|
||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||
),
|
||||
CircuitGroupAssignment(
|
||||
group=circuit_groups[2],
|
||||
member=circuits[2],
|
||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||
),
|
||||
)
|
||||
CircuitGroupAssignment.objects.bulk_create(assignments)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'group': circuit_groups[3].pk,
|
||||
'member_type': ContentType.objects.get_for_model(Circuit).pk,
|
||||
'member': circuits[3].pk,
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"member_type,member_id,group,priority",
|
||||
f"circuits.circuit,{circuits[0].pk},{circuit_groups[3].pk},primary",
|
||||
f"circuits.circuit,{circuits[1].pk},{circuit_groups[3].pk},secondary",
|
||||
f"circuits.circuit,{circuits[2].pk},{circuit_groups[3].pk},tertiary",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,priority",
|
||||
f"{assignments[0].pk},inactive",
|
||||
f"{assignments[1].pk},inactive",
|
||||
f"{assignments[2].pk},inactive",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||
}
|
||||
|
||||
|
||||
class VirtualCircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = VirtualCircuitType
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
virtual_circuit_types = (
|
||||
VirtualCircuitType(name='Virtual Circuit Type 1', slug='circuit-type-1'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 2', slug='circuit-type-2'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 3', slug='circuit-type-3'),
|
||||
)
|
||||
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Virtual Circuit Type X',
|
||||
'slug': 'virtual-circuit-type-x',
|
||||
'description': 'A new virtual circuit type',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug",
|
||||
"Virtual Circuit Type 4,circuit-type-4",
|
||||
"Virtual Circuit Type 5,circuit-type-5",
|
||||
"Virtual Circuit Type 6,circuit-type-6",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,description",
|
||||
f"{virtual_circuit_types[0].pk},Virtual Circuit Type 7,New description7",
|
||||
f"{virtual_circuit_types[1].pk},Virtual Circuit Type 8,New description8",
|
||||
f"{virtual_circuit_types[2].pk},Virtual Circuit Type 9,New description9",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'Foo',
|
||||
}
|
||||
|
||||
|
||||
class VirtualCircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualCircuit
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.add_permissions(
|
||||
'circuits.add_virtualcircuittermination',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_networks = (
|
||||
ProviderNetwork(provider=provider, name='Provider Network 1'),
|
||||
ProviderNetwork(provider=provider, name='Provider Network 2'),
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
provider_accounts = (
|
||||
ProviderAccount(provider=provider, account='Provider Account 1'),
|
||||
ProviderAccount(provider=provider, account='Provider Account 2'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
virtual_circuit_types = (
|
||||
VirtualCircuitType(name='Virtual Circuit Type 1', slug='virtual-circuit-type-1'),
|
||||
VirtualCircuitType(name='Virtual Circuit Type 2', slug='virtual-circuit-type-2'),
|
||||
)
|
||||
VirtualCircuitType.objects.bulk_create(virtual_circuit_types)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_types[0]
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_types[0]
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_networks[0],
|
||||
provider_account=provider_accounts[0],
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_types[0]
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
device = create_test_device('Device 1')
|
||||
interfaces = (
|
||||
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'cid': 'Virtual Circuit X',
|
||||
'provider_network': provider_networks[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': virtual_circuit_types[1].pk,
|
||||
'status': CircuitStatusChoices.STATUS_PLANNED,
|
||||
'description': 'A new virtual circuit',
|
||||
'comments': 'Some comments',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"cid,provider_network,provider_account,type,status",
|
||||
(
|
||||
f"Virtual Circuit 4,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
|
||||
f"{CircuitStatusChoices.STATUS_PLANNED}"
|
||||
),
|
||||
(
|
||||
f"Virtual Circuit 5,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
|
||||
f"{CircuitStatusChoices.STATUS_PLANNED}"
|
||||
),
|
||||
(
|
||||
f"Virtual Circuit 6,Provider Network 1,Provider Account 1,{virtual_circuit_types[0].name},"
|
||||
f"{CircuitStatusChoices.STATUS_PLANNED}"
|
||||
),
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,cid,description,type,status",
|
||||
(
|
||||
f"{virtual_circuits[0].pk},Virtual Circuit A,New description,{virtual_circuit_types[1].name},"
|
||||
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
|
||||
),
|
||||
(
|
||||
f"{virtual_circuits[1].pk},Virtual Circuit B,New description,{virtual_circuit_types[1].name},"
|
||||
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
|
||||
),
|
||||
(
|
||||
f"{virtual_circuits[2].pk},Virtual Circuit C,New description,{virtual_circuit_types[1].name},"
|
||||
f"{CircuitStatusChoices.STATUS_DECOMMISSIONED}"
|
||||
),
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'provider_network': provider_networks[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': virtual_circuit_types[1].pk,
|
||||
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
|
||||
'description': 'New description',
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||
def test_bulk_import_objects_with_terminations(self):
|
||||
interfaces = Interface.objects.filter(type=InterfaceTypeChoices.TYPE_VIRTUAL)
|
||||
json_data = f"""
|
||||
[
|
||||
{{
|
||||
"cid": "Virtual Circuit 7",
|
||||
"provider_network": "Provider Network 1",
|
||||
"type": "Virtual Circuit Type 1",
|
||||
"status": "active",
|
||||
"terminations": [
|
||||
{{
|
||||
"role": "hub",
|
||||
"interface": {interfaces[0].pk}
|
||||
}},
|
||||
{{
|
||||
"role": "spoke",
|
||||
"interface": {interfaces[1].pk}
|
||||
}},
|
||||
{{
|
||||
"role": "spoke",
|
||||
"interface": {interfaces[2].pk}
|
||||
}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
"""
|
||||
|
||||
initial_count = self._get_queryset().count()
|
||||
data = {
|
||||
'data': json_data,
|
||||
'format': ImportFormatChoices.JSON,
|
||||
}
|
||||
|
||||
# Assign model-level permission
|
||||
obj_perm = ObjectPermission(
|
||||
name='Test permission',
|
||||
actions=['add']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
|
||||
|
||||
# Try GET with model-level permission
|
||||
self.assertHttpStatus(self.client.get(self._get_url('bulk_import')), 200)
|
||||
|
||||
# Test POST with permission
|
||||
self.assertHttpStatus(self.client.post(self._get_url('bulk_import'), data), 302)
|
||||
self.assertEqual(self._get_queryset().count(), initial_count + 1)
|
||||
|
||||
|
||||
class VirtualCircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualCircuitTermination
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
|
||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
|
||||
devices = (
|
||||
Device(site=site, name='hub', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke1', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke2', device_type=device_type, role=device_role),
|
||||
Device(site=site, name='spoke3', device_type=device_type, role=device_role),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
physical_interfaces = (
|
||||
Interface(device=devices[0], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[1], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[2], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
Interface(device=devices[3], name='eth0', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
|
||||
)
|
||||
Interface.objects.bulk_create(physical_interfaces)
|
||||
|
||||
virtual_interfaces = (
|
||||
# Point-to-point VCs
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.2',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.3',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[1],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[2],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='eth0.1',
|
||||
parent=physical_interfaces[3],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
|
||||
# Hub and spoke VCs
|
||||
Interface(
|
||||
device=devices[0],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[1],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[2],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
Interface(
|
||||
device=devices[3],
|
||||
name='eth0.9',
|
||||
parent=physical_interfaces[0],
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL
|
||||
),
|
||||
)
|
||||
Interface.objects.bulk_create(virtual_interfaces)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(provider=provider, name='Provider Network 1')
|
||||
provider_account = ProviderAccount.objects.create(provider=provider, account='Provider Account 1')
|
||||
virtual_circuit_type = VirtualCircuitType.objects.create(
|
||||
name='Virtual Circuit Type 1',
|
||||
slug='virtual-circuit-type-1'
|
||||
)
|
||||
|
||||
virtual_circuits = (
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 1',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 2',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 3',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
VirtualCircuit(
|
||||
provider_network=provider_network,
|
||||
provider_account=provider_account,
|
||||
cid='Virtual Circuit 4',
|
||||
type=virtual_circuit_type
|
||||
),
|
||||
)
|
||||
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||
|
||||
virtual_circuit_terminations = (
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[0]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[0],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[3]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[1]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[1],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[4]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[2]
|
||||
),
|
||||
VirtualCircuitTermination(
|
||||
virtual_circuit=virtual_circuits[2],
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=virtual_interfaces[5]
|
||||
),
|
||||
)
|
||||
VirtualCircuitTermination.objects.bulk_create(virtual_circuit_terminations)
|
||||
|
||||
cls.form_data = {
|
||||
'virtual_circuit': virtual_circuits[3].pk,
|
||||
'role': VirtualCircuitTerminationRoleChoices.ROLE_HUB,
|
||||
'interface': virtual_interfaces[6].pk
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"virtual_circuit,role,interface,description",
|
||||
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_HUB},{virtual_interfaces[6].pk},Hub",
|
||||
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[7].pk},Spoke 1",
|
||||
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[8].pk},Spoke 2",
|
||||
f"Virtual Circuit 4,{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},{virtual_interfaces[9].pk},Spoke 3",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,role,description",
|
||||
f"{virtual_circuit_terminations[0].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
|
||||
f"{virtual_circuit_terminations[1].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
|
||||
f"{virtual_circuit_terminations[2].pk},{VirtualCircuitTerminationRoleChoices.ROLE_SPOKE},New description",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@@ -5,50 +5,71 @@ from . import views
|
||||
|
||||
app_name = 'circuits'
|
||||
urlpatterns = [
|
||||
|
||||
# Providers
|
||||
path('providers/', views.ProviderListView.as_view(), name='provider_list'),
|
||||
path('providers/add/', views.ProviderEditView.as_view(), name='provider_add'),
|
||||
path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
||||
path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
path('providers/', include(get_model_urls('circuits', 'provider', detail=False))),
|
||||
path('providers/<int:pk>/', include(get_model_urls('circuits', 'provider'))),
|
||||
|
||||
# Provider accounts
|
||||
path('provider-accounts/', views.ProviderAccountListView.as_view(), name='provideraccount_list'),
|
||||
path('provider-accounts/add/', views.ProviderAccountEditView.as_view(), name='provideraccount_add'),
|
||||
path('provider-accounts/import/', views.ProviderAccountBulkImportView.as_view(), name='provideraccount_import'),
|
||||
path('provider-accounts/edit/', views.ProviderAccountBulkEditView.as_view(), name='provideraccount_bulk_edit'),
|
||||
path('provider-accounts/delete/', views.ProviderAccountBulkDeleteView.as_view(), name='provideraccount_bulk_delete'),
|
||||
path('provider-accounts/', include(get_model_urls('circuits', 'provideraccount', detail=False))),
|
||||
path('provider-accounts/<int:pk>/', include(get_model_urls('circuits', 'provideraccount'))),
|
||||
|
||||
# Provider networks
|
||||
path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'),
|
||||
path('provider-networks/add/', views.ProviderNetworkEditView.as_view(), name='providernetwork_add'),
|
||||
path('provider-networks/import/', views.ProviderNetworkBulkImportView.as_view(), name='providernetwork_import'),
|
||||
path('provider-networks/edit/', views.ProviderNetworkBulkEditView.as_view(), name='providernetwork_bulk_edit'),
|
||||
path('provider-networks/delete/', views.ProviderNetworkBulkDeleteView.as_view(), name='providernetwork_bulk_delete'),
|
||||
path('provider-networks/', include(get_model_urls('circuits', 'providernetwork', detail=False))),
|
||||
path('provider-networks/<int:pk>/', include(get_model_urls('circuits', 'providernetwork'))),
|
||||
|
||||
# Circuit types
|
||||
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
|
||||
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
|
||||
path('circuit-types/edit/', views.CircuitTypeBulkEditView.as_view(), name='circuittype_bulk_edit'),
|
||||
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
||||
path('circuit-types/', include(get_model_urls('circuits', 'circuittype', detail=False))),
|
||||
path('circuit-types/<int:pk>/', include(get_model_urls('circuits', 'circuittype'))),
|
||||
|
||||
# Circuits
|
||||
path('circuits/', views.CircuitListView.as_view(), name='circuit_list'),
|
||||
path('circuits/add/', views.CircuitEditView.as_view(), name='circuit_add'),
|
||||
path('circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
||||
path('circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
||||
path('circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
||||
path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'),
|
||||
path('circuits/', include(get_model_urls('circuits', 'circuit', detail=False))),
|
||||
path(
|
||||
'circuits/<int:pk>/terminations/swap/',
|
||||
views.CircuitSwapTerminations.as_view(),
|
||||
name='circuit_terminations_swap'
|
||||
),
|
||||
path('circuits/<int:pk>/', include(get_model_urls('circuits', 'circuit'))),
|
||||
|
||||
# Circuit terminations
|
||||
path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
|
||||
path('circuit-terminations/', include(get_model_urls('circuits', 'circuittermination', detail=False))),
|
||||
path('circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'circuittermination'))),
|
||||
|
||||
path('circuit-groups/', include(get_model_urls('circuits', 'circuitgroup', detail=False))),
|
||||
path('circuit-groups/<int:pk>/', include(get_model_urls('circuits', 'circuitgroup'))),
|
||||
|
||||
path('circuit-group-assignments/', include(get_model_urls('circuits', 'circuitgroupassignment', detail=False))),
|
||||
path('circuit-group-assignments/<int:pk>/', include(get_model_urls('circuits', 'circuitgroupassignment'))),
|
||||
|
||||
# Virtual circuits
|
||||
path('virtual-circuits/', views.VirtualCircuitListView.as_view(), name='virtualcircuit_list'),
|
||||
path('virtual-circuits/add/', views.VirtualCircuitEditView.as_view(), name='virtualcircuit_add'),
|
||||
path('virtual-circuits/import/', views.VirtualCircuitBulkImportView.as_view(), name='virtualcircuit_bulk_import'),
|
||||
path('virtual-circuits/edit/', views.VirtualCircuitBulkEditView.as_view(), name='virtualcircuit_bulk_edit'),
|
||||
path('virtual-circuits/delete/', views.VirtualCircuitBulkDeleteView.as_view(), name='virtualcircuit_bulk_delete'),
|
||||
path('virtual-circuits/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuit'))),
|
||||
|
||||
path('virtual-circuit-types/', include(get_model_urls('circuits', 'virtualcircuittype', detail=False))),
|
||||
path('virtual-circuit-types/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuittype'))),
|
||||
|
||||
# Virtual circuit terminations
|
||||
path(
|
||||
'virtual-circuit-terminations/',
|
||||
views.VirtualCircuitTerminationListView.as_view(),
|
||||
name='virtualcircuittermination_list',
|
||||
),
|
||||
path(
|
||||
'virtual-circuit-terminations/add/',
|
||||
views.VirtualCircuitTerminationEditView.as_view(),
|
||||
name='virtualcircuittermination_add',
|
||||
),
|
||||
path(
|
||||
'virtual-circuit-terminations/import/',
|
||||
views.VirtualCircuitTerminationBulkImportView.as_view(),
|
||||
name='virtualcircuittermination_bulk_import',
|
||||
),
|
||||
path(
|
||||
'virtual-circuit-terminations/edit/',
|
||||
views.VirtualCircuitTerminationBulkEditView.as_view(),
|
||||
name='virtualcircuittermination_bulk_edit',
|
||||
),
|
||||
path(
|
||||
'virtual-circuit-terminations/delete/',
|
||||
views.VirtualCircuitTerminationBulkDeleteView.as_view(),
|
||||
name='virtualcircuittermination_bulk_delete',
|
||||
),
|
||||
path('virtual-circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'virtualcircuittermination'))),
|
||||
]
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.views import PathTraceView
|
||||
from netbox.views import generic
|
||||
from tenancy.views import ObjectContactsView
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.query import count_related
|
||||
from utilities.views import register_model_view
|
||||
from utilities.views import GetRelatedModelsMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
|
||||
@@ -16,6 +17,7 @@ from .models import *
|
||||
# Providers
|
||||
#
|
||||
|
||||
@register_model_view(Provider, 'list', path='', detail=False)
|
||||
class ProviderListView(generic.ObjectListView):
|
||||
queryset = Provider.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider')
|
||||
@@ -26,20 +28,16 @@ class ProviderListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(Provider)
|
||||
class ProviderView(generic.ObjectView):
|
||||
class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Provider.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(ProviderAccount.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(Provider, 'add', detail=False)
|
||||
@register_model_view(Provider, 'edit')
|
||||
class ProviderEditView(generic.ObjectEditView):
|
||||
queryset = Provider.objects.all()
|
||||
@@ -51,11 +49,13 @@ class ProviderDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Provider.objects.all()
|
||||
|
||||
|
||||
@register_model_view(Provider, 'bulk_import', detail=False)
|
||||
class ProviderBulkImportView(generic.BulkImportView):
|
||||
queryset = Provider.objects.all()
|
||||
model_form = forms.ProviderImportForm
|
||||
|
||||
|
||||
@register_model_view(Provider, 'bulk_edit', path='edit', detail=False)
|
||||
class ProviderBulkEditView(generic.BulkEditView):
|
||||
queryset = Provider.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider')
|
||||
@@ -65,6 +65,7 @@ class ProviderBulkEditView(generic.BulkEditView):
|
||||
form = forms.ProviderBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(Provider, 'bulk_delete', path='delete', detail=False)
|
||||
class ProviderBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Provider.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider')
|
||||
@@ -82,6 +83,7 @@ class ProviderContactsView(ObjectContactsView):
|
||||
# ProviderAccounts
|
||||
#
|
||||
|
||||
@register_model_view(ProviderAccount, 'list', path='', detail=False)
|
||||
class ProviderAccountListView(generic.ObjectListView):
|
||||
queryset = ProviderAccount.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider_account')
|
||||
@@ -92,19 +94,16 @@ class ProviderAccountListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount)
|
||||
class ProviderAccountView(generic.ObjectView):
|
||||
class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(provider_account=instance), 'provider_account_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount, 'add', detail=False)
|
||||
@register_model_view(ProviderAccount, 'edit')
|
||||
class ProviderAccountEditView(generic.ObjectEditView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
@@ -116,12 +115,14 @@ class ProviderAccountDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount, 'bulk_import', detail=False)
|
||||
class ProviderAccountBulkImportView(generic.BulkImportView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
model_form = forms.ProviderAccountImportForm
|
||||
table = tables.ProviderAccountTable
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount, 'bulk_edit', path='edit', detail=False)
|
||||
class ProviderAccountBulkEditView(generic.BulkEditView):
|
||||
queryset = ProviderAccount.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider_account')
|
||||
@@ -131,6 +132,7 @@ class ProviderAccountBulkEditView(generic.BulkEditView):
|
||||
form = forms.ProviderAccountBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount, 'bulk_delete', path='delete', detail=False)
|
||||
class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ProviderAccount.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider_account')
|
||||
@@ -148,6 +150,7 @@ class ProviderAccountContactsView(ObjectContactsView):
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
@register_model_view(ProviderNetwork, 'list', path='', detail=False)
|
||||
class ProviderNetworkListView(generic.ObjectListView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = filtersets.ProviderNetworkFilterSet
|
||||
@@ -156,22 +159,25 @@ class ProviderNetworkListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(ProviderNetwork)
|
||||
class ProviderNetworkView(generic.ObjectView):
|
||||
class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
|
||||
'provider_network_id',
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
instance,
|
||||
extra=(
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___provider_network=instance),
|
||||
'provider_network_id',
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ProviderNetwork, 'add', detail=False)
|
||||
@register_model_view(ProviderNetwork, 'edit')
|
||||
class ProviderNetworkEditView(generic.ObjectEditView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
@@ -183,11 +189,13 @@ class ProviderNetworkDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
|
||||
|
||||
@register_model_view(ProviderNetwork, 'bulk_import', detail=False)
|
||||
class ProviderNetworkBulkImportView(generic.BulkImportView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
model_form = forms.ProviderNetworkImportForm
|
||||
|
||||
|
||||
@register_model_view(ProviderNetwork, 'bulk_edit', path='edit', detail=False)
|
||||
class ProviderNetworkBulkEditView(generic.BulkEditView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = filtersets.ProviderNetworkFilterSet
|
||||
@@ -195,6 +203,7 @@ class ProviderNetworkBulkEditView(generic.BulkEditView):
|
||||
form = forms.ProviderNetworkBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(ProviderNetwork, 'bulk_delete', path='delete', detail=False)
|
||||
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = filtersets.ProviderNetworkFilterSet
|
||||
@@ -205,6 +214,7 @@ class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
|
||||
# Circuit Types
|
||||
#
|
||||
|
||||
@register_model_view(CircuitType, 'list', path='', detail=False)
|
||||
class CircuitTypeListView(generic.ObjectListView):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
@@ -215,19 +225,16 @@ class CircuitTypeListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(CircuitType)
|
||||
class CircuitTypeView(generic.ObjectView):
|
||||
class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = CircuitType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(CircuitType, 'add', detail=False)
|
||||
@register_model_view(CircuitType, 'edit')
|
||||
class CircuitTypeEditView(generic.ObjectEditView):
|
||||
queryset = CircuitType.objects.all()
|
||||
@@ -239,11 +246,13 @@ class CircuitTypeDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CircuitType.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitType, 'bulk_import', detail=False)
|
||||
class CircuitTypeBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitType.objects.all()
|
||||
model_form = forms.CircuitTypeImportForm
|
||||
|
||||
|
||||
@register_model_view(CircuitType, 'bulk_edit', path='edit', detail=False)
|
||||
class CircuitTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
@@ -253,6 +262,7 @@ class CircuitTypeBulkEditView(generic.BulkEditView):
|
||||
form = forms.CircuitTypeBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(CircuitType, 'bulk_delete', path='delete', detail=False)
|
||||
class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
@@ -265,10 +275,10 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
# Circuits
|
||||
#
|
||||
|
||||
@register_model_view(Circuit, 'list', path='', detail=False)
|
||||
class CircuitListView(generic.ObjectListView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'tenant__group', 'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
'tenant__group', 'termination_a__termination', 'termination_z__termination',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
filterset_form = forms.CircuitFilterForm
|
||||
@@ -280,6 +290,7 @@ class CircuitView(generic.ObjectView):
|
||||
queryset = Circuit.objects.all()
|
||||
|
||||
|
||||
@register_model_view(Circuit, 'add', detail=False)
|
||||
@register_model_view(Circuit, 'edit')
|
||||
class CircuitEditView(generic.ObjectEditView):
|
||||
queryset = Circuit.objects.all()
|
||||
@@ -291,6 +302,7 @@ class CircuitDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Circuit.objects.all()
|
||||
|
||||
|
||||
@register_model_view(Circuit, 'bulk_import', detail=False)
|
||||
class CircuitBulkImportView(generic.BulkImportView):
|
||||
queryset = Circuit.objects.all()
|
||||
model_form = forms.CircuitImportForm
|
||||
@@ -298,7 +310,7 @@ class CircuitBulkImportView(generic.BulkImportView):
|
||||
'circuits.add_circuittermination',
|
||||
]
|
||||
related_object_forms = {
|
||||
'terminations': forms.CircuitTerminationImportForm,
|
||||
'terminations': forms.CircuitTerminationImportRelatedForm,
|
||||
}
|
||||
|
||||
def prep_related_object_data(self, parent, data):
|
||||
@@ -306,20 +318,20 @@ class CircuitBulkImportView(generic.BulkImportView):
|
||||
return data
|
||||
|
||||
|
||||
@register_model_view(Circuit, 'bulk_edit', path='edit', detail=False)
|
||||
class CircuitBulkEditView(generic.BulkEditView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
'tenant__group', 'termination_a__termination', 'termination_z__termination',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
form = forms.CircuitBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(Circuit, 'bulk_delete', path='delete', detail=False)
|
||||
class CircuitBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
'tenant__group', 'termination_a__termination', 'termination_z__termination',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
@@ -337,7 +349,9 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
|
||||
# Circuit must have at least one termination to swap
|
||||
if not circuit.termination_a and not circuit.termination_z:
|
||||
messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
|
||||
messages.error(request, _(
|
||||
"No terminations have been defined for circuit {circuit}."
|
||||
).format(circuit=circuit))
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
return render(request, 'circuits/circuit_terminations_swap.html', {
|
||||
@@ -385,7 +399,7 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
circuit.termination_z = None
|
||||
circuit.save()
|
||||
|
||||
messages.success(request, f"Swapped terminations for circuit {circuit}.")
|
||||
messages.success(request, _("Swapped terminations for circuit {circuit}.").format(circuit=circuit))
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
return render(request, 'circuits/circuit_terminations_swap.html', {
|
||||
@@ -408,6 +422,20 @@ class CircuitContactsView(ObjectContactsView):
|
||||
# Circuit terminations
|
||||
#
|
||||
|
||||
@register_model_view(CircuitTermination, 'list', path='', detail=False)
|
||||
class CircuitTerminationListView(generic.ObjectListView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
filterset = filtersets.CircuitTerminationFilterSet
|
||||
filterset_form = forms.CircuitTerminationFilterForm
|
||||
table = tables.CircuitTerminationTable
|
||||
|
||||
|
||||
@register_model_view(CircuitTermination)
|
||||
class CircuitTerminationView(generic.ObjectView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitTermination, 'add', detail=False)
|
||||
@register_model_view(CircuitTermination, 'edit')
|
||||
class CircuitTerminationEditView(generic.ObjectEditView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
@@ -419,5 +447,300 @@ class CircuitTerminationDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitTermination, 'bulk_import', detail=False)
|
||||
class CircuitTerminationBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
model_form = forms.CircuitTerminationImportForm
|
||||
|
||||
|
||||
@register_model_view(CircuitTermination, 'bulk_edit', path='edit', detail=False)
|
||||
class CircuitTerminationBulkEditView(generic.BulkEditView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
filterset = filtersets.CircuitTerminationFilterSet
|
||||
table = tables.CircuitTerminationTable
|
||||
form = forms.CircuitTerminationBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(CircuitTermination, 'bulk_delete', path='delete', detail=False)
|
||||
class CircuitTerminationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
filterset = filtersets.CircuitTerminationFilterSet
|
||||
table = tables.CircuitTerminationTable
|
||||
|
||||
|
||||
# Trace view
|
||||
register_model_view(CircuitTermination, 'trace', kwargs={'model': CircuitTermination})(PathTraceView)
|
||||
|
||||
|
||||
#
|
||||
# Circuit Groups
|
||||
#
|
||||
|
||||
@register_model_view(CircuitGroup, 'list', path='', detail=False)
|
||||
class CircuitGroupListView(generic.ObjectListView):
|
||||
queryset = CircuitGroup.objects.annotate(
|
||||
circuit_group_assignment_count=count_related(CircuitGroupAssignment, 'group')
|
||||
)
|
||||
filterset = filtersets.CircuitGroupFilterSet
|
||||
filterset_form = forms.CircuitGroupFilterForm
|
||||
table = tables.CircuitGroupTable
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup)
|
||||
class CircuitGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup, 'add', detail=False)
|
||||
@register_model_view(CircuitGroup, 'edit')
|
||||
class CircuitGroupEditView(generic.ObjectEditView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
form = forms.CircuitGroupForm
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup, 'delete')
|
||||
class CircuitGroupDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup, 'bulk_import', detail=False)
|
||||
class CircuitGroupBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
model_form = forms.CircuitGroupImportForm
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup, 'bulk_edit', path='edit', detail=False)
|
||||
class CircuitGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
filterset = filtersets.CircuitGroupFilterSet
|
||||
table = tables.CircuitGroupTable
|
||||
form = forms.CircuitGroupBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(CircuitGroup, 'bulk_delete', path='delete', detail=False)
|
||||
class CircuitGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
filterset = filtersets.CircuitGroupFilterSet
|
||||
table = tables.CircuitGroupTable
|
||||
|
||||
|
||||
#
|
||||
# Circuit Groups
|
||||
#
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, 'list', path='', detail=False)
|
||||
class CircuitGroupAssignmentListView(generic.ObjectListView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
||||
filterset_form = forms.CircuitGroupAssignmentFilterForm
|
||||
table = tables.CircuitGroupAssignmentTable
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment)
|
||||
class CircuitGroupAssignmentView(generic.ObjectView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, 'add', detail=False)
|
||||
@register_model_view(CircuitGroupAssignment, 'edit')
|
||||
class CircuitGroupAssignmentEditView(generic.ObjectEditView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
form = forms.CircuitGroupAssignmentForm
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, 'delete')
|
||||
class CircuitGroupAssignmentDeleteView(generic.ObjectDeleteView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, 'bulk_import', detail=False)
|
||||
class CircuitGroupAssignmentBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
model_form = forms.CircuitGroupAssignmentImportForm
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, 'bulk_edit', path='edit', detail=False)
|
||||
class CircuitGroupAssignmentBulkEditView(generic.BulkEditView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
||||
table = tables.CircuitGroupAssignmentTable
|
||||
form = forms.CircuitGroupAssignmentBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, 'bulk_delete', path='delete', detail=False)
|
||||
class CircuitGroupAssignmentBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
filterset = filtersets.CircuitGroupAssignmentFilterSet
|
||||
table = tables.CircuitGroupAssignmentTable
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuit Types
|
||||
#
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'list', path='', detail=False)
|
||||
class VirtualCircuitTypeListView(generic.ObjectListView):
|
||||
queryset = VirtualCircuitType.objects.annotate(
|
||||
virtual_circuit_count=count_related(VirtualCircuit, 'type')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitTypeFilterSet
|
||||
filterset_form = forms.VirtualCircuitTypeFilterForm
|
||||
table = tables.VirtualCircuitTypeTable
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType)
|
||||
class VirtualCircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'add', detail=False)
|
||||
@register_model_view(VirtualCircuitType, 'edit')
|
||||
class VirtualCircuitTypeEditView(generic.ObjectEditView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
form = forms.VirtualCircuitTypeForm
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'delete')
|
||||
class VirtualCircuitTypeDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'bulk_import', detail=False)
|
||||
class VirtualCircuitTypeBulkImportView(generic.BulkImportView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
model_form = forms.VirtualCircuitTypeImportForm
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'bulk_edit', path='edit', detail=False)
|
||||
class VirtualCircuitTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = VirtualCircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitTypeFilterSet
|
||||
table = tables.VirtualCircuitTypeTable
|
||||
form = forms.VirtualCircuitTypeBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitType, 'bulk_delete', path='delete', detail=False)
|
||||
class VirtualCircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualCircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitTypeFilterSet
|
||||
table = tables.VirtualCircuitTypeTable
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuits
|
||||
#
|
||||
|
||||
class VirtualCircuitListView(generic.ObjectListView):
|
||||
queryset = VirtualCircuit.objects.annotate(
|
||||
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitFilterSet
|
||||
filterset_form = forms.VirtualCircuitFilterForm
|
||||
table = tables.VirtualCircuitTable
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuit)
|
||||
class VirtualCircuitView(generic.ObjectView):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuit, 'edit')
|
||||
class VirtualCircuitEditView(generic.ObjectEditView):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
form = forms.VirtualCircuitForm
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuit, 'delete')
|
||||
class VirtualCircuitDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
|
||||
|
||||
class VirtualCircuitBulkImportView(generic.BulkImportView):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
model_form = forms.VirtualCircuitImportForm
|
||||
additional_permissions = [
|
||||
'circuits.add_virtualcircuittermination',
|
||||
]
|
||||
related_object_forms = {
|
||||
'terminations': forms.VirtualCircuitTerminationImportRelatedForm,
|
||||
}
|
||||
|
||||
def prep_related_object_data(self, parent, data):
|
||||
data.update({'virtual_circuit': parent})
|
||||
return data
|
||||
|
||||
|
||||
class VirtualCircuitBulkEditView(generic.BulkEditView):
|
||||
queryset = VirtualCircuit.objects.annotate(
|
||||
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitFilterSet
|
||||
table = tables.VirtualCircuitTable
|
||||
form = forms.VirtualCircuitBulkEditForm
|
||||
|
||||
|
||||
class VirtualCircuitBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualCircuit.objects.annotate(
|
||||
termination_count=count_related(VirtualCircuitTermination, 'virtual_circuit')
|
||||
)
|
||||
filterset = filtersets.VirtualCircuitFilterSet
|
||||
table = tables.VirtualCircuitTable
|
||||
|
||||
|
||||
#
|
||||
# Virtual circuit terminations
|
||||
#
|
||||
|
||||
class VirtualCircuitTerminationListView(generic.ObjectListView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
||||
filterset_form = forms.VirtualCircuitTerminationFilterForm
|
||||
table = tables.VirtualCircuitTerminationTable
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitTermination)
|
||||
class VirtualCircuitTerminationView(generic.ObjectView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitTermination, 'edit')
|
||||
class VirtualCircuitTerminationEditView(generic.ObjectEditView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
form = forms.VirtualCircuitTerminationForm
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitTermination, 'delete')
|
||||
class VirtualCircuitTerminationDeleteView(generic.ObjectDeleteView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
|
||||
|
||||
class VirtualCircuitTerminationBulkImportView(generic.BulkImportView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
model_form = forms.VirtualCircuitTerminationImportForm
|
||||
|
||||
|
||||
class VirtualCircuitTerminationBulkEditView(generic.BulkEditView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
||||
table = tables.VirtualCircuitTerminationTable
|
||||
form = forms.VirtualCircuitTerminationBulkEditForm
|
||||
|
||||
|
||||
class VirtualCircuitTerminationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
filterset = filtersets.VirtualCircuitTerminationFilterSet
|
||||
table = tables.VirtualCircuitTerminationTable
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import JobStatusChoices
|
||||
from core.models import *
|
||||
from netbox.api.fields import ChoiceField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from users.api.serializers import UserSerializer
|
||||
|
||||
__all__ = (
|
||||
'NestedDataFileSerializer',
|
||||
'NestedDataSourceSerializer',
|
||||
'NestedJobSerializer',
|
||||
)
|
||||
|
||||
|
||||
class NestedDataSourceSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:datasource-detail')
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedDataFileSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:datafile-detail')
|
||||
|
||||
class Meta:
|
||||
model = DataFile
|
||||
fields = ['id', 'url', 'display', 'path']
|
||||
|
||||
|
||||
class NestedJobSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
status = ChoiceField(choices=JobStatusChoices)
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = ['url', 'created', 'completed', 'user', 'status']
|
||||
@@ -8,10 +8,8 @@ from drf_spectacular.plumbing import (
|
||||
build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import serializers
|
||||
from rest_framework.relations import ManyRelatedField
|
||||
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
from netbox.api.fields import ChoiceField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
# see netbox.api.routers.NetBoxRouter
|
||||
@@ -37,7 +35,10 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
|
||||
|
||||
elif direction == "response":
|
||||
value = build_cf
|
||||
label = {**build_basic_type(OpenApiTypes.STR), "enum": list(OrderedDict.fromkeys(self.target.choices.values()))}
|
||||
label = {
|
||||
**build_basic_type(OpenApiTypes.STR),
|
||||
"enum": list(OrderedDict.fromkeys(self.target.choices.values()))
|
||||
}
|
||||
|
||||
return build_object_type(
|
||||
properties={
|
||||
@@ -126,9 +127,18 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
|
||||
return response_serializers
|
||||
|
||||
def _get_serializer_name(self, serializer, direction, bypass_extensions=False) -> str:
|
||||
name = super()._get_serializer_name(serializer, direction, bypass_extensions)
|
||||
|
||||
# If this serializer is nested, prepend its name with "Brief"
|
||||
if getattr(serializer, 'nested', False):
|
||||
name = f'Brief{name}'
|
||||
|
||||
return name
|
||||
|
||||
def get_serializer_ref_name(self, serializer):
|
||||
# from drf-yasg.utils
|
||||
"""Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
|
||||
"""Get serializer's ref_name
|
||||
:param serializer: Serializer instance
|
||||
:return: Serializer's ``ref_name`` or ``None`` for inline serializer
|
||||
:rtype: str or None
|
||||
@@ -137,8 +147,6 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
serializer_name = type(serializer).__name__
|
||||
if hasattr(serializer_meta, 'ref_name'):
|
||||
ref_name = serializer_meta.ref_name
|
||||
elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
|
||||
ref_name = None
|
||||
else:
|
||||
ref_name = serializer_name
|
||||
if ref_name.endswith('Serializer'):
|
||||
@@ -150,6 +158,9 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
fields = {} if hasattr(serializer, 'child') else serializer.fields
|
||||
remove_fields = []
|
||||
|
||||
# If you get a failure here for "AttributeError: 'cached_property' object has no attribute 'items'"
|
||||
# it is probably because you are using a viewsets.ViewSet for the API View and are defining a
|
||||
# serializer_class. You will also need to define a get_serializer() method like for GenericAPIView.
|
||||
for child_name, child in fields.items():
|
||||
# read_only fields don't need to be in writable (write only) serializers
|
||||
if 'read_only' in dir(child) and child.read_only:
|
||||
@@ -255,3 +266,14 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
if '{id}' in self.path:
|
||||
return f"{self.method.capitalize()} a {model_name} object."
|
||||
return f"{self.method.capitalize()} a list of {model_name} objects."
|
||||
|
||||
|
||||
class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
|
||||
target_class = 'netbox.api.fields.SerializedPKRelatedField'
|
||||
|
||||
def map_serializer_field(self, auto_schema, direction):
|
||||
if direction == "response":
|
||||
component = auto_schema.resolve_serializer(self.target.serializer, direction)
|
||||
return component.ref if component else None
|
||||
else:
|
||||
return build_basic_type(OpenApiTypes.INT)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .serializers_.change_logging import *
|
||||
from .serializers_.data import *
|
||||
from .serializers_.jobs import *
|
||||
from .nested_serializers import *
|
||||
from .serializers_.tasks import *
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from extras.choices import *
|
||||
from extras.models import ObjectChange
|
||||
from core.choices import *
|
||||
from core.models import ObjectChange
|
||||
from netbox.api.exceptions import SerializerNotFound
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
from netbox.api.serializers import BaseModelSerializer
|
||||
@@ -15,7 +15,6 @@ __all__ = (
|
||||
|
||||
|
||||
class ObjectChangeSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
@@ -30,12 +29,22 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
||||
changed_object = serializers.SerializerMethodField(
|
||||
read_only=True
|
||||
)
|
||||
prechange_data = serializers.JSONField(
|
||||
source='prechange_data_clean',
|
||||
read_only=True,
|
||||
allow_null=True
|
||||
)
|
||||
postchange_data = serializers.JSONField(
|
||||
source='postchange_data_clean',
|
||||
read_only=True,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type',
|
||||
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||
'id', 'url', 'display_url', 'display', 'time', 'user', 'user_name', 'request_id', 'action',
|
||||
'changed_object_type', 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import DataFile, DataSource
|
||||
from netbox.api.fields import ChoiceField, RelatedObjectCountField
|
||||
@@ -13,9 +11,6 @@ __all__ = (
|
||||
|
||||
|
||||
class DataSourceSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datasource-detail'
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=get_data_backend_choices()
|
||||
)
|
||||
@@ -30,16 +25,14 @@ class DataSourceSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
|
||||
'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description',
|
||||
'parameters', 'ignore_rules', 'comments', 'custom_fields', 'created', 'last_updated', 'last_synced',
|
||||
'file_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
class DataFileSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:datafile-detail'
|
||||
)
|
||||
source = DataSourceSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
@@ -48,6 +41,6 @@ class DataFileSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = DataFile
|
||||
fields = [
|
||||
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||
'id', 'url', 'display_url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'path')
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.choices import *
|
||||
from core.models import Job
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
@@ -12,7 +10,6 @@ __all__ = (
|
||||
|
||||
|
||||
class JobSerializer(BaseModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
@@ -25,7 +22,7 @@ class JobSerializer(BaseModelSerializer):
|
||||
class Meta:
|
||||
model = Job
|
||||
fields = [
|
||||
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
|
||||
'started', 'completed', 'user', 'data', 'error', 'job_id',
|
||||
'id', 'url', 'display_url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled',
|
||||
'interval', 'started', 'completed', 'user', 'data', 'error', 'job_id',
|
||||
]
|
||||
brief_fields = ('url', 'created', 'completed', 'user', 'status')
|
||||
|
||||
87
netbox/core/api/serializers_/tasks.py
Normal file
87
netbox/core/api/serializers_/tasks.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
__all__ = (
|
||||
'BackgroundTaskSerializer',
|
||||
'BackgroundQueueSerializer',
|
||||
'BackgroundWorkerSerializer',
|
||||
)
|
||||
|
||||
|
||||
class BackgroundTaskSerializer(serializers.Serializer):
|
||||
id = serializers.CharField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:rqtask-detail',
|
||||
lookup_field='id',
|
||||
lookup_url_kwarg='pk'
|
||||
)
|
||||
description = serializers.CharField()
|
||||
origin = serializers.CharField()
|
||||
func_name = serializers.CharField()
|
||||
args = serializers.ListField(child=serializers.CharField())
|
||||
kwargs = serializers.DictField()
|
||||
result = serializers.CharField()
|
||||
timeout = serializers.IntegerField()
|
||||
result_ttl = serializers.IntegerField()
|
||||
created_at = serializers.DateTimeField()
|
||||
enqueued_at = serializers.DateTimeField()
|
||||
started_at = serializers.DateTimeField()
|
||||
ended_at = serializers.DateTimeField()
|
||||
worker_name = serializers.CharField()
|
||||
position = serializers.SerializerMethodField()
|
||||
status = serializers.SerializerMethodField()
|
||||
meta = serializers.DictField()
|
||||
last_heartbeat = serializers.CharField()
|
||||
|
||||
is_finished = serializers.BooleanField()
|
||||
is_queued = serializers.BooleanField()
|
||||
is_failed = serializers.BooleanField()
|
||||
is_started = serializers.BooleanField()
|
||||
is_deferred = serializers.BooleanField()
|
||||
is_canceled = serializers.BooleanField()
|
||||
is_scheduled = serializers.BooleanField()
|
||||
is_stopped = serializers.BooleanField()
|
||||
|
||||
def get_position(self, obj) -> int:
|
||||
return obj.get_position()
|
||||
|
||||
def get_status(self, obj) -> str:
|
||||
return obj.get_status()
|
||||
|
||||
|
||||
class BackgroundQueueSerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
url = serializers.SerializerMethodField()
|
||||
jobs = serializers.IntegerField()
|
||||
oldest_job_timestamp = serializers.CharField()
|
||||
index = serializers.IntegerField()
|
||||
scheduler_pid = serializers.CharField()
|
||||
workers = serializers.IntegerField()
|
||||
finished_jobs = serializers.IntegerField()
|
||||
started_jobs = serializers.IntegerField()
|
||||
deferred_jobs = serializers.IntegerField()
|
||||
failed_jobs = serializers.IntegerField()
|
||||
scheduled_jobs = serializers.IntegerField()
|
||||
|
||||
def get_url(self, obj):
|
||||
return reverse('core-api:rqqueue-detail', args=[obj['name']], request=self.context.get("request"))
|
||||
|
||||
|
||||
class BackgroundWorkerSerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name='core-api:rqworker-detail',
|
||||
lookup_field='name'
|
||||
)
|
||||
state = serializers.SerializerMethodField()
|
||||
birth_date = serializers.DateTimeField()
|
||||
queue_names = serializers.ListField(
|
||||
child=serializers.CharField()
|
||||
)
|
||||
pid = serializers.CharField()
|
||||
successful_job_count = serializers.IntegerField()
|
||||
failed_job_count = serializers.IntegerField()
|
||||
total_working_time = serializers.IntegerField()
|
||||
|
||||
def get_state(self, obj):
|
||||
return obj.get_state()
|
||||
@@ -1,16 +1,17 @@
|
||||
from netbox.api.routers import NetBoxRouter
|
||||
from . import views
|
||||
|
||||
app_name = 'core-api'
|
||||
|
||||
router = NetBoxRouter()
|
||||
router.APIRootView = views.CoreRootView
|
||||
|
||||
# Data sources
|
||||
router.register('data-sources', views.DataSourceViewSet)
|
||||
router.register('data-files', views.DataFileViewSet)
|
||||
|
||||
# Jobs
|
||||
router.register('jobs', views.JobViewSet)
|
||||
router.register('object-changes', views.ObjectChangeViewSet)
|
||||
router.register('background-queues', views.BackgroundQueueViewSet, basename='rqqueue')
|
||||
router.register('background-workers', views.BackgroundWorkerViewSet, basename='rqworker')
|
||||
router.register('background-tasks', views.BackgroundTaskViewSet, basename='rqtask')
|
||||
|
||||
app_name = 'core-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
@@ -7,8 +10,20 @@ from rest_framework.routers import APIRootView
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from core import filtersets
|
||||
from core.choices import DataSourceStatusChoices
|
||||
from core.jobs import SyncDataSourceJob
|
||||
from core.models import *
|
||||
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs, requeue_rq_job, stop_rq_job
|
||||
from django_rq.queues import get_redis_connection
|
||||
from django_rq.utils import get_statistics
|
||||
from django_rq.settings import QUEUES_LIST
|
||||
from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.pagination import LimitOffsetListPagination
|
||||
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rq.job import Job as RQ_Job
|
||||
from rq.worker import Worker
|
||||
from . import serializers
|
||||
|
||||
|
||||
@@ -35,7 +50,11 @@ class DataSourceViewSet(NetBoxModelViewSet):
|
||||
if not request.user.has_perm('core.sync_datasource', obj=datasource):
|
||||
raise PermissionDenied(_("This user does not have permission to synchronize this data source."))
|
||||
|
||||
datasource.enqueue_sync_job(request)
|
||||
# Enqueue the sync job & update the DataSource's status
|
||||
SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
|
||||
datasource.status = DataSourceStatusChoices.QUEUED
|
||||
DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
|
||||
|
||||
serializer = serializers.DataSourceSerializer(datasource, context={'request': request})
|
||||
|
||||
return Response(serializer.data)
|
||||
@@ -54,3 +73,162 @@ class JobViewSet(ReadOnlyModelViewSet):
|
||||
queryset = Job.objects.all()
|
||||
serializer_class = serializers.JobSerializer
|
||||
filterset_class = filtersets.JobFilterSet
|
||||
|
||||
|
||||
class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
Retrieve a list of recent changes.
|
||||
"""
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ObjectChange.objects.valid_models()
|
||||
serializer_class = serializers.ObjectChangeSerializer
|
||||
filterset_class = filtersets.ObjectChangeFilterSet
|
||||
|
||||
|
||||
class BaseRQViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
Base class for RQ view sets. Provides a list() method. Subclasses must implement get_data().
|
||||
"""
|
||||
permission_classes = [IsAdminUser]
|
||||
serializer_class = None
|
||||
|
||||
def get_data(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||
def list(self, request):
|
||||
data = self.get_data()
|
||||
paginator = LimitOffsetListPagination()
|
||||
data = paginator.paginate_list(data, request)
|
||||
|
||||
serializer = self.serializer_class(data, many=True, context={'request': request})
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""
|
||||
Return the serializer instance that should be used for validating and
|
||||
deserializing input, and for serializing output.
|
||||
"""
|
||||
serializer_class = self.get_serializer_class()
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
return serializer_class(*args, **kwargs)
|
||||
|
||||
|
||||
class BackgroundQueueViewSet(BaseRQViewSet):
|
||||
"""
|
||||
Retrieve a list of RQ Queues.
|
||||
Note: Queue names are not URL safe so not returning a detail view.
|
||||
"""
|
||||
serializer_class = serializers.BackgroundQueueSerializer
|
||||
lookup_field = 'name'
|
||||
lookup_value_regex = r'[\w.@+-]+'
|
||||
|
||||
def get_view_name(self):
|
||||
return "Background Queues"
|
||||
|
||||
def get_data(self):
|
||||
return get_statistics(run_maintenance_tasks=True)["queues"]
|
||||
|
||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||
def retrieve(self, request, name):
|
||||
data = self.get_data()
|
||||
if not data:
|
||||
raise Http404
|
||||
|
||||
for queue in data:
|
||||
if queue['name'] == name:
|
||||
serializer = self.serializer_class(queue, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
raise Http404
|
||||
|
||||
|
||||
class BackgroundWorkerViewSet(BaseRQViewSet):
|
||||
"""
|
||||
Retrieve a list of RQ Workers.
|
||||
"""
|
||||
serializer_class = serializers.BackgroundWorkerSerializer
|
||||
lookup_field = 'name'
|
||||
|
||||
def get_view_name(self):
|
||||
return "Background Workers"
|
||||
|
||||
def get_data(self):
|
||||
config = QUEUES_LIST[0]
|
||||
return Worker.all(get_redis_connection(config['connection_config']))
|
||||
|
||||
def retrieve(self, request, name):
|
||||
# all the RQ queues should use the same connection
|
||||
config = QUEUES_LIST[0]
|
||||
workers = Worker.all(get_redis_connection(config['connection_config']))
|
||||
worker = next((item for item in workers if item.name == name), None)
|
||||
if not worker:
|
||||
raise Http404
|
||||
|
||||
serializer = serializers.BackgroundWorkerSerializer(worker, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class BackgroundTaskViewSet(BaseRQViewSet):
|
||||
"""
|
||||
Retrieve a list of RQ Tasks.
|
||||
"""
|
||||
serializer_class = serializers.BackgroundTaskSerializer
|
||||
|
||||
def get_view_name(self):
|
||||
return "Background Tasks"
|
||||
|
||||
def get_data(self):
|
||||
return get_rq_jobs()
|
||||
|
||||
def get_task_from_id(self, task_id):
|
||||
config = QUEUES_LIST[0]
|
||||
task = RQ_Job.fetch(task_id, connection=get_redis_connection(config['connection_config']))
|
||||
if not task:
|
||||
raise Http404
|
||||
|
||||
return task
|
||||
|
||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||
def retrieve(self, request, pk):
|
||||
"""
|
||||
Retrieve the details of the specified RQ Task.
|
||||
"""
|
||||
task = self.get_task_from_id(pk)
|
||||
serializer = self.serializer_class(task, context={'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=["POST"], detail=True)
|
||||
def delete(self, request, pk):
|
||||
"""
|
||||
Delete the specified RQ Task.
|
||||
"""
|
||||
delete_rq_job(pk)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@action(methods=["POST"], detail=True)
|
||||
def requeue(self, request, pk):
|
||||
"""
|
||||
Requeues the specified RQ Task.
|
||||
"""
|
||||
requeue_rq_job(pk)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@action(methods=["POST"], detail=True)
|
||||
def enqueue(self, request, pk):
|
||||
"""
|
||||
Enqueues the specified RQ Task.
|
||||
"""
|
||||
enqueue_rq_job(pk)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@action(methods=["POST"], detail=True)
|
||||
def stop(self, request, pk):
|
||||
"""
|
||||
Stops the specified RQ Task.
|
||||
"""
|
||||
stopped_jobs = stop_rq_job(pk)
|
||||
if len(stopped_jobs) == 1:
|
||||
return HttpResponse(status=200)
|
||||
else:
|
||||
return HttpResponse(status=204)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.migrations.operations import AlterModelOptions
|
||||
|
||||
@@ -16,9 +18,14 @@ class CoreConfig(AppConfig):
|
||||
name = "core"
|
||||
|
||||
def ready(self):
|
||||
from core.api import schema # noqa
|
||||
from core.api import schema # noqa: F401
|
||||
from netbox.models.features import register_models
|
||||
from . import data_backends, search
|
||||
from . import data_backends, events, search # noqa: F401
|
||||
from netbox import context_managers # noqa: F401
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
# Clear Redis cache on startup in development mode
|
||||
if settings.DEBUG:
|
||||
cache.clear()
|
||||
|
||||
@@ -59,8 +59,45 @@ class JobStatusChoices(ChoiceSet):
|
||||
(STATUS_FAILED, _('Failed'), 'red'),
|
||||
)
|
||||
|
||||
ENQUEUED_STATE_CHOICES = (
|
||||
STATUS_PENDING,
|
||||
STATUS_SCHEDULED,
|
||||
STATUS_RUNNING,
|
||||
)
|
||||
|
||||
TERMINAL_STATE_CHOICES = (
|
||||
STATUS_COMPLETED,
|
||||
STATUS_ERRORED,
|
||||
STATUS_FAILED,
|
||||
)
|
||||
|
||||
|
||||
class JobIntervalChoices(ChoiceSet):
|
||||
INTERVAL_MINUTELY = 1
|
||||
INTERVAL_HOURLY = 60
|
||||
INTERVAL_DAILY = 60 * 24
|
||||
INTERVAL_WEEKLY = 60 * 24 * 7
|
||||
|
||||
CHOICES = (
|
||||
(INTERVAL_MINUTELY, _('Minutely')),
|
||||
(INTERVAL_HOURLY, _('Hourly')),
|
||||
(INTERVAL_DAILY, _('Daily')),
|
||||
(INTERVAL_WEEKLY, _('Weekly')),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ObjectChanges
|
||||
#
|
||||
|
||||
class ObjectChangeActionChoices(ChoiceSet):
|
||||
|
||||
ACTION_CREATE = 'create'
|
||||
ACTION_UPDATE = 'update'
|
||||
ACTION_DELETE = 'delete'
|
||||
|
||||
CHOICES = (
|
||||
(ACTION_CREATE, _('Created'), 'green'),
|
||||
(ACTION_UPDATE, _('Updated'), 'blue'),
|
||||
(ACTION_DELETE, _('Deleted'), 'red'),
|
||||
)
|
||||
|
||||
@@ -8,10 +8,13 @@ from urllib.parse import urlparse
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.data_backends import DataBackend
|
||||
from netbox.utils import register_data_backend
|
||||
from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS
|
||||
from utilities.socks import ProxyPoolManager
|
||||
from .exceptions import SyncError
|
||||
|
||||
__all__ = (
|
||||
@@ -31,7 +34,7 @@ class LocalBackend(DataBackend):
|
||||
|
||||
@contextmanager
|
||||
def fetch(self):
|
||||
logger.debug(f"Data source type is local; skipping fetch")
|
||||
logger.debug("Data source type is local; skipping fetch")
|
||||
local_path = urlparse(self.url).path # Strip file:// scheme
|
||||
|
||||
yield local_path
|
||||
@@ -67,11 +70,18 @@ class GitBackend(DataBackend):
|
||||
|
||||
# Initialize backend config
|
||||
config = ConfigDict()
|
||||
self.use_socks = False
|
||||
|
||||
# Apply HTTP proxy (if configured)
|
||||
if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'):
|
||||
if proxy := settings.HTTP_PROXIES.get(self.url_scheme):
|
||||
config.set("http", "proxy", proxy)
|
||||
if settings.HTTP_PROXIES:
|
||||
if proxy := settings.HTTP_PROXIES.get(self.url_scheme, None):
|
||||
if urlparse(proxy).scheme not in HTTP_PROXY_SUPPORTED_SCHEMAS:
|
||||
raise ImproperlyConfigured(f"Unsupported Git DataSource proxy scheme: {urlparse(proxy).scheme}")
|
||||
|
||||
if self.url_scheme in ('http', 'https'):
|
||||
config.set("http", "proxy", proxy)
|
||||
if urlparse(proxy).scheme in HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS:
|
||||
self.use_socks = True
|
||||
|
||||
return config
|
||||
|
||||
@@ -84,11 +94,13 @@ class GitBackend(DataBackend):
|
||||
clone_args = {
|
||||
"branch": self.params.get('branch'),
|
||||
"config": self.config,
|
||||
"depth": 1,
|
||||
"errstream": porcelain.NoneStream(),
|
||||
"quiet": True,
|
||||
}
|
||||
|
||||
# check if using socks for proxy - if so need to use custom pool_manager
|
||||
if self.use_socks:
|
||||
clone_args['pool_manager'] = ProxyPoolManager(settings.HTTP_PROXIES.get(self.url_scheme))
|
||||
|
||||
if self.url_scheme in ('http', 'https'):
|
||||
if self.params.get('username'):
|
||||
clone_args.update(
|
||||
@@ -97,6 +109,9 @@ class GitBackend(DataBackend):
|
||||
"password": self.params.get('password'),
|
||||
}
|
||||
)
|
||||
if self.url_scheme:
|
||||
clone_args["quiet"] = True
|
||||
clone_args["depth"] = 1
|
||||
|
||||
logger.debug(f"Cloning git repo: {self.url}")
|
||||
try:
|
||||
|
||||
33
netbox/core/events.py
Normal file
33
netbox/core/events.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.events import EventType, EVENT_TYPE_KIND_DANGER, EVENT_TYPE_KIND_SUCCESS, EVENT_TYPE_KIND_WARNING
|
||||
|
||||
__all__ = (
|
||||
'JOB_COMPLETED',
|
||||
'JOB_ERRORED',
|
||||
'JOB_FAILED',
|
||||
'JOB_STARTED',
|
||||
'OBJECT_CREATED',
|
||||
'OBJECT_DELETED',
|
||||
'OBJECT_UPDATED',
|
||||
)
|
||||
|
||||
# Object events
|
||||
OBJECT_CREATED = 'object_created'
|
||||
OBJECT_UPDATED = 'object_updated'
|
||||
OBJECT_DELETED = 'object_deleted'
|
||||
|
||||
# Job events
|
||||
JOB_STARTED = 'job_started'
|
||||
JOB_COMPLETED = 'job_completed'
|
||||
JOB_FAILED = 'job_failed'
|
||||
JOB_ERRORED = 'job_errored'
|
||||
|
||||
# Register core events
|
||||
EventType(OBJECT_CREATED, _('Object created')).register()
|
||||
EventType(OBJECT_UPDATED, _('Object updated')).register()
|
||||
EventType(OBJECT_DELETED, _('Object deleted'), destructive=True).register()
|
||||
EventType(JOB_STARTED, _('Job started')).register()
|
||||
EventType(JOB_COMPLETED, _('Job completed'), kind=EVENT_TYPE_KIND_SUCCESS).register()
|
||||
EventType(JOB_FAILED, _('Job failed'), kind=EVENT_TYPE_KIND_WARNING).register()
|
||||
EventType(JOB_ERRORED, _('Job errored'), kind=EVENT_TYPE_KIND_DANGER).register()
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@@ -5,6 +6,8 @@ import django_filters
|
||||
|
||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from users.models import User
|
||||
from utilities.filters import ContentTypeFilter
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@@ -13,6 +16,7 @@ __all__ = (
|
||||
'DataFileFilterSet',
|
||||
'DataSourceFilterSet',
|
||||
'JobFilterSet',
|
||||
'ObjectChangeFilterSet',
|
||||
)
|
||||
|
||||
|
||||
@@ -126,6 +130,43 @@ class JobFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
changed_object_type = ContentTypeFilter()
|
||||
changed_object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ContentType.objects.all()
|
||||
)
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label=_('User (ID)'),
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label=_('User name'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = (
|
||||
'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id',
|
||||
'related_object_type', 'related_object_id', 'object_repr',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(user_name__icontains=value) |
|
||||
Q(object_repr__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevisionFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.choices import *
|
||||
@@ -7,8 +6,11 @@ from core.models import *
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
from netbox.forms.mixins import SavedFiltersMixin
|
||||
from netbox.utils import get_data_backend_choices
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
|
||||
from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField
|
||||
from users.models import User
|
||||
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
|
||||
from utilities.forms.fields import (
|
||||
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DateTimePicker
|
||||
|
||||
@@ -17,6 +19,7 @@ __all__ = (
|
||||
'DataFileFilterForm',
|
||||
'DataSourceFilterForm',
|
||||
'JobFilterForm',
|
||||
'ObjectChangeFilterForm',
|
||||
)
|
||||
|
||||
|
||||
@@ -118,12 +121,46 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
user = DynamicModelMultipleChoiceField(
|
||||
queryset=get_user_model().objects.all(),
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
label=_('User')
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = ObjectChange
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('time_before', 'time_after', name=_('Time')),
|
||||
FieldSet('action', 'user_id', 'changed_object_type_id', name=_('Attributes')),
|
||||
)
|
||||
time_after = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('After'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
time_before = forms.DateTimeField(
|
||||
required=False,
|
||||
label=_('Before'),
|
||||
widget=DateTimePicker()
|
||||
)
|
||||
action = forms.ChoiceField(
|
||||
label=_('Action'),
|
||||
choices=add_blank_choice(ObjectChangeActionChoices),
|
||||
required=False
|
||||
)
|
||||
user_id = DynamicModelMultipleChoiceField(
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
label=_('User')
|
||||
)
|
||||
changed_object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.with_feature('change_logging'),
|
||||
required=False,
|
||||
label=_('Object Type'),
|
||||
)
|
||||
|
||||
|
||||
class ConfigRevisionFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
|
||||
@@ -6,6 +6,7 @@ from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
|
||||
__all__ = (
|
||||
'DataFileFilter',
|
||||
'DataSourceFilter',
|
||||
'ObjectChangeFilter',
|
||||
)
|
||||
|
||||
|
||||
@@ -19,3 +20,9 @@ class DataFileFilter(BaseFilterMixin):
|
||||
@autotype_decorator(filtersets.DataSourceFilterSet)
|
||||
class DataSourceFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ObjectChange, lookups=True)
|
||||
@autotype_decorator(filtersets.ObjectChangeFilterSet)
|
||||
class ObjectChangeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
24
netbox/core/graphql/mixins.py
Normal file
24
netbox/core/graphql/mixins.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from typing import Annotated, List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from core.models import ObjectChange
|
||||
|
||||
__all__ = (
|
||||
'ChangelogMixin',
|
||||
)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class ChangelogMixin:
|
||||
|
||||
@strawberry_django.field
|
||||
def changelog(self, info) -> List[Annotated["ObjectChangeType", strawberry.lazy('.types')]]: # noqa: F821
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
object_changes = ObjectChange.objects.filter(
|
||||
changed_object_type=content_type,
|
||||
changed_object_id=self.pk
|
||||
)
|
||||
return object_changes.restrict(info.context.request.user, 'view')
|
||||
@@ -3,18 +3,13 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from core import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class CoreQuery:
|
||||
@strawberry.field
|
||||
def data_file(self, id: int) -> DataFileType:
|
||||
return models.DataFile.objects.get(pk=id)
|
||||
data_file: DataFileType = strawberry_django.field()
|
||||
data_file_list: List[DataFileType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def data_source(self, id: int) -> DataSourceType:
|
||||
return models.DataSource.objects.get(pk=id)
|
||||
data_source: DataSourceType = strawberry_django.field()
|
||||
data_source_list: List[DataSourceType] = strawberry_django.field()
|
||||
|
||||
@@ -10,6 +10,7 @@ from .filters import *
|
||||
__all__ = (
|
||||
'DataFileType',
|
||||
'DataSourceType',
|
||||
'ObjectChangeType',
|
||||
)
|
||||
|
||||
|
||||
@@ -30,3 +31,12 @@ class DataFileType(BaseObjectType):
|
||||
class DataSourceType(NetBoxObjectType):
|
||||
|
||||
datafiles: List[Annotated["DataFileType", strawberry.lazy('core.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.ObjectChange,
|
||||
fields='__all__',
|
||||
filters=ObjectChangeFilter
|
||||
)
|
||||
class ObjectChangeType(BaseObjectType):
|
||||
pass
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import logging
|
||||
|
||||
from netbox.jobs import JobRunner
|
||||
from netbox.search.backends import search_backend
|
||||
from .choices import *
|
||||
from .choices import DataSourceStatusChoices
|
||||
from .exceptions import SyncError
|
||||
from .models import DataSource
|
||||
from rq.timeouts import JobTimeoutException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sync_datasource(job, *args, **kwargs):
|
||||
class SyncDataSourceJob(JobRunner):
|
||||
"""
|
||||
Call sync() on a DataSource.
|
||||
"""
|
||||
datasource = DataSource.objects.get(pk=job.object_id)
|
||||
|
||||
try:
|
||||
job.start()
|
||||
datasource.sync()
|
||||
class Meta:
|
||||
name = 'Synchronization'
|
||||
|
||||
# Update the search cache for DataFiles belonging to this source
|
||||
search_backend.cache(datasource.datafiles.iterator())
|
||||
def run(self, *args, **kwargs):
|
||||
datasource = DataSource.objects.get(pk=self.job.object_id)
|
||||
|
||||
job.terminate()
|
||||
try:
|
||||
datasource.sync()
|
||||
|
||||
except Exception as e:
|
||||
job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
|
||||
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
|
||||
if type(e) in (SyncError, JobTimeoutException):
|
||||
logging.error(e)
|
||||
else:
|
||||
# Update the search cache for DataFiles belonging to this source
|
||||
search_backend.cache(datasource.datafiles.iterator())
|
||||
|
||||
except Exception as e:
|
||||
DataSource.objects.filter(pk=datasource.pk).update(status=DataSourceStatusChoices.FAILED)
|
||||
if type(e) is SyncError:
|
||||
logging.error(e)
|
||||
raise e
|
||||
|
||||
@@ -5,12 +5,16 @@ import sys
|
||||
from django import get_version
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from core.models import ObjectType
|
||||
from users.models import User
|
||||
|
||||
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
|
||||
EXCLUDE_MODELS = (
|
||||
'extras.branch',
|
||||
'extras.stagedchange',
|
||||
)
|
||||
|
||||
BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||
### Python {python} | Django {django} | NetBox {netbox}
|
||||
@@ -18,7 +22,7 @@ BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||
node=platform.node(),
|
||||
python=platform.python_version(),
|
||||
django=get_version(),
|
||||
netbox=settings.VERSION
|
||||
netbox=settings.RELEASE.name
|
||||
)
|
||||
|
||||
|
||||
@@ -44,12 +48,16 @@ class Command(BaseCommand):
|
||||
|
||||
# Gather Django models and constants from each app
|
||||
for app in APPS:
|
||||
self.django_models[app] = []
|
||||
models = []
|
||||
|
||||
# Load models from each app
|
||||
for model in apps.get_app_config(app).get_models():
|
||||
namespace[model.__name__] = model
|
||||
self.django_models[app].append(model.__name__)
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
if f'{app_label}.{model_name}' not in EXCLUDE_MODELS:
|
||||
namespace[model.__name__] = model
|
||||
models.append(model.__name__)
|
||||
self.django_models[app] = sorted(models)
|
||||
|
||||
# Constants
|
||||
try:
|
||||
@@ -61,7 +69,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Additional objects to include
|
||||
namespace['ObjectType'] = ObjectType
|
||||
namespace['User'] = get_user_model()
|
||||
namespace['User'] = User
|
||||
|
||||
# Load convenience commands
|
||||
namespace.update({
|
||||
|
||||
@@ -2,6 +2,8 @@ import logging
|
||||
|
||||
from django_rq.management.commands.rqworker import Command as _Command
|
||||
|
||||
from netbox.registry import registry
|
||||
|
||||
|
||||
DEFAULT_QUEUES = ('high', 'default', 'low')
|
||||
|
||||
@@ -14,6 +16,15 @@ class Command(_Command):
|
||||
of only the 'default' queue).
|
||||
"""
|
||||
def handle(self, *args, **options):
|
||||
# Setup system jobs.
|
||||
for job, kwargs in registry['system_jobs'].items():
|
||||
try:
|
||||
interval = kwargs['interval']
|
||||
except KeyError:
|
||||
raise TypeError("System job must specify an interval (in minutes).")
|
||||
logger.debug(f"Scheduling system job {job.name} (interval={interval})")
|
||||
job.enqueue_once(**kwargs)
|
||||
|
||||
# Run the worker with scheduler functionality
|
||||
options['with_scheduler'] = True
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class Command(BaseCommand):
|
||||
if invalid_names := set(options['name']) - found_names:
|
||||
raise CommandError(f"Invalid data source names: {', '.join(invalid_names)}")
|
||||
else:
|
||||
raise CommandError(f"Must specify at least one data source, or set --all.")
|
||||
raise CommandError("Must specify at least one data source, or set --all.")
|
||||
|
||||
if len(options['name']) > 1:
|
||||
self.stdout.write(f"Syncing {len(datasources)} data sources.")
|
||||
@@ -43,4 +43,4 @@ class Command(BaseCommand):
|
||||
raise e
|
||||
|
||||
if len(options['name']) > 1:
|
||||
self.stdout.write(f"Finished.")
|
||||
self.stdout.write("Finished.")
|
||||
|
||||
@@ -8,13 +8,12 @@ import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [
|
||||
('core', '0001_initial'),
|
||||
('core', '0002_managedfile'),
|
||||
('core', '0003_job'),
|
||||
('core', '0004_replicate_jobresults'),
|
||||
('core', '0005_job_created_auto_now')
|
||||
('core', '0005_job_created_auto_now'),
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
@@ -30,7 +29,10 @@ class Migration(migrations.Migration):
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
(
|
||||
'custom_field_data',
|
||||
models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
|
||||
),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
@@ -55,9 +57,28 @@ class Migration(migrations.Migration):
|
||||
('last_updated', models.DateTimeField(editable=False)),
|
||||
('path', models.CharField(editable=False, max_length=1000)),
|
||||
('size', models.PositiveIntegerField(editable=False)),
|
||||
('hash', models.CharField(editable=False, max_length=64, validators=[django.core.validators.RegexValidator(message='Length must be 64 hexadecimal characters.', regex='^[0-9a-f]{64}$')])),
|
||||
(
|
||||
'hash',
|
||||
models.CharField(
|
||||
editable=False,
|
||||
max_length=64,
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
message='Length must be 64 hexadecimal characters.', regex='^[0-9a-f]{64}$'
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
('data', models.BinaryField()),
|
||||
('source', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='datafiles', to='core.datasource')),
|
||||
(
|
||||
'source',
|
||||
models.ForeignKey(
|
||||
editable=False,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='datafiles',
|
||||
to='core.datasource',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ('source', 'path'),
|
||||
@@ -76,8 +97,18 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('object_id', models.PositiveBigIntegerField()),
|
||||
('datafile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='core.datafile')),
|
||||
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
|
||||
(
|
||||
'datafile',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name='+', to='core.datafile'
|
||||
),
|
||||
),
|
||||
(
|
||||
'object_type',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype'
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'indexes': [models.Index(fields=['object_type', 'object_id'], name='core_autosy_object__c17bac_idx')],
|
||||
@@ -97,8 +128,26 @@ class Migration(migrations.Migration):
|
||||
('last_updated', models.DateTimeField(blank=True, editable=False, null=True)),
|
||||
('file_root', models.CharField(max_length=1000)),
|
||||
('file_path', models.FilePathField(editable=False)),
|
||||
('data_file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='core.datafile')),
|
||||
('data_source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.datasource')),
|
||||
(
|
||||
'data_file',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='core.datafile',
|
||||
),
|
||||
),
|
||||
(
|
||||
'data_source',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='core.datasource',
|
||||
),
|
||||
),
|
||||
('auto_sync_enabled', models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
@@ -108,7 +157,9 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='managedfile',
|
||||
constraint=models.UniqueConstraint(fields=('file_root', 'file_path'), name='core_managedfile_unique_root_path'),
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('file_root', 'file_path'), name='core_managedfile_unique_root_path'
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Job',
|
||||
@@ -118,14 +169,33 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=200)),
|
||||
('created', models.DateTimeField()),
|
||||
('scheduled', models.DateTimeField(blank=True, null=True)),
|
||||
('interval', models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)])),
|
||||
(
|
||||
'interval',
|
||||
models.PositiveIntegerField(
|
||||
blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]
|
||||
),
|
||||
),
|
||||
('started', models.DateTimeField(blank=True, null=True)),
|
||||
('completed', models.DateTimeField(blank=True, null=True)),
|
||||
('status', models.CharField(default='pending', max_length=30)),
|
||||
('data', models.JSONField(blank=True, null=True)),
|
||||
('job_id', models.UUIDField(unique=True)),
|
||||
('object_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
'object_type',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='contenttypes.contenttype'
|
||||
),
|
||||
),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created'],
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_job_created_auto_now'),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_datasource_type_remove_choices'),
|
||||
]
|
||||
|
||||
@@ -3,7 +3,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0007_job_add_error_field'),
|
||||
@@ -12,8 +11,7 @@ class Migration(migrations.Migration):
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ObjectType',
|
||||
fields=[
|
||||
],
|
||||
fields=[],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
|
||||
@@ -2,7 +2,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_contenttype_proxy'),
|
||||
]
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_configrevision'),
|
||||
]
|
||||
|
||||
78
netbox/core/migrations/0011_move_objectchange.py
Normal file
78
netbox/core/migrations/0011_move_objectchange.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0010_gfk_indexes'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.CreateModel(
|
||||
name='ObjectChange',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('time', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('user_name', models.CharField(editable=False, max_length=150)),
|
||||
('request_id', models.UUIDField(db_index=True, editable=False)),
|
||||
('action', models.CharField(max_length=50)),
|
||||
('changed_object_id', models.PositiveBigIntegerField()),
|
||||
('related_object_id', models.PositiveBigIntegerField(blank=True, null=True)),
|
||||
('object_repr', models.CharField(editable=False, max_length=200)),
|
||||
('prechange_data', models.JSONField(blank=True, editable=False, null=True)),
|
||||
('postchange_data', models.JSONField(blank=True, editable=False, null=True)),
|
||||
(
|
||||
'changed_object_type',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
),
|
||||
),
|
||||
(
|
||||
'related_object_type',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
),
|
||||
),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='changes',
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'object change',
|
||||
'verbose_name_plural': 'object changes',
|
||||
'ordering': ['-time'],
|
||||
'indexes': [
|
||||
models.Index(
|
||||
fields=['changed_object_type', 'changed_object_id'],
|
||||
name='core_object_changed_c227ce_idx',
|
||||
),
|
||||
models.Index(
|
||||
fields=['related_object_type', 'related_object_id'],
|
||||
name='core_object_related_3375d6_idx',
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
# Table has been renamed from 'extras' app
|
||||
database_operations=[],
|
||||
),
|
||||
]
|
||||
23
netbox/core/migrations/0012_job_object_type_optional.py
Normal file
23
netbox/core/migrations/0012_job_object_type_optional.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0011_move_objectchange'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='job',
|
||||
name='object_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='jobs',
|
||||
to='contenttypes.contenttype',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
from .config import *
|
||||
from .contenttypes import *
|
||||
from .change_logging import *
|
||||
from .config import *
|
||||
from .data import *
|
||||
from .files import *
|
||||
from .jobs import *
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
from functools import cached_property
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from mptt.models import MPTTModel
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from ..querysets import ObjectChangeQuerySet
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from core.querysets import ObjectChangeQuerySet
|
||||
from netbox.models.features import ChangeLoggingMixin
|
||||
from utilities.data import shallow_compare_dict
|
||||
from .contenttypes import ObjectType
|
||||
|
||||
__all__ = (
|
||||
'ObjectChange',
|
||||
@@ -131,11 +136,76 @@ class ObjectChange(models.Model):
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:objectchange', args=[self.pk])
|
||||
return reverse('core:objectchange', args=[self.pk])
|
||||
|
||||
def get_action_color(self):
|
||||
return ObjectChangeActionChoices.colors.get(self.action)
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def has_changes(self):
|
||||
return self.prechange_data != self.postchange_data
|
||||
|
||||
@cached_property
|
||||
def diff_exclude_fields(self):
|
||||
"""
|
||||
Return a set of attributes which should be ignored when calculating a diff
|
||||
between the pre- and post-change data. (For instance, it would not make
|
||||
sense to compare the "last updated" times as these are expected to differ.)
|
||||
"""
|
||||
model = self.changed_object_type.model_class()
|
||||
attrs = set()
|
||||
|
||||
# Exclude auto-populated change tracking fields
|
||||
if issubclass(model, ChangeLoggingMixin):
|
||||
attrs.update({'created', 'last_updated'})
|
||||
|
||||
# Exclude MPTT-internal fields
|
||||
if issubclass(model, MPTTModel):
|
||||
attrs.update({'level', 'lft', 'rght', 'tree_id'})
|
||||
|
||||
return attrs
|
||||
|
||||
def get_clean_data(self, prefix):
|
||||
"""
|
||||
Return only the pre-/post-change attributes which are relevant for calculating a diff.
|
||||
"""
|
||||
ret = {}
|
||||
change_data = getattr(self, f'{prefix}_data') or {}
|
||||
for k, v in change_data.items():
|
||||
if k not in self.diff_exclude_fields and not k.startswith('_'):
|
||||
ret[k] = v
|
||||
return ret
|
||||
|
||||
@cached_property
|
||||
def prechange_data_clean(self):
|
||||
return self.get_clean_data('prechange')
|
||||
|
||||
@cached_property
|
||||
def postchange_data_clean(self):
|
||||
return self.get_clean_data('postchange')
|
||||
|
||||
def diff(self):
|
||||
"""
|
||||
Return a dictionary of pre- and post-change values for attributes which have changed.
|
||||
"""
|
||||
prechange_data = self.prechange_data_clean
|
||||
postchange_data = self.postchange_data_clean
|
||||
|
||||
# Determine which attributes have changed
|
||||
if self.action == ObjectChangeActionChoices.ACTION_CREATE:
|
||||
changed_attrs = sorted(postchange_data.keys())
|
||||
elif self.action == ObjectChangeActionChoices.ACTION_DELETE:
|
||||
changed_attrs = sorted(prechange_data.keys())
|
||||
else:
|
||||
# TODO: Support deep (recursive) comparison
|
||||
changed_data = shallow_compare_dict(prechange_data, postchange_data)
|
||||
changed_attrs = sorted(changed_data.keys())
|
||||
|
||||
return {
|
||||
'pre': {
|
||||
k: prechange_data.get(k) for k in changed_attrs
|
||||
},
|
||||
'post': {
|
||||
k: postchange_data.get(k) for k in changed_attrs
|
||||
},
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import yaml
|
||||
from fnmatch import fnmatchcase
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import yaml
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -12,7 +12,6 @@ from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
@@ -22,8 +21,6 @@ from netbox.registry import registry
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from ..choices import *
|
||||
from ..exceptions import SyncError
|
||||
from ..signals import post_sync, pre_sync
|
||||
from .jobs import Job
|
||||
|
||||
__all__ = (
|
||||
'AutoSyncRecord',
|
||||
@@ -87,9 +84,6 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
def __str__(self):
|
||||
return f'{self.name}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('core:datasource', args=[self.pk])
|
||||
|
||||
@property
|
||||
def docs_url(self):
|
||||
return f'{settings.STATIC_URL}docs/models/{self._meta.app_label}/{self._meta.model_name}/'
|
||||
@@ -128,7 +122,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
# Ensure URL scheme matches selected type
|
||||
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
|
||||
raise ValidationError({
|
||||
'source_url': f"URLs for local sources must start with file:// (or specify no scheme)"
|
||||
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
|
||||
})
|
||||
|
||||
def to_objectchange(self, action):
|
||||
@@ -153,21 +147,6 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
|
||||
return objectchange
|
||||
|
||||
def enqueue_sync_job(self, request):
|
||||
"""
|
||||
Enqueue a background job to synchronize the DataSource by calling sync().
|
||||
"""
|
||||
# Set the status to "syncing"
|
||||
self.status = DataSourceStatusChoices.QUEUED
|
||||
DataSource.objects.filter(pk=self.pk).update(status=self.status)
|
||||
|
||||
# Enqueue a sync job
|
||||
return Job.enqueue(
|
||||
import_string('core.jobs.sync_datasource'),
|
||||
instance=self,
|
||||
user=request.user
|
||||
)
|
||||
|
||||
def get_backend(self):
|
||||
backend_params = self.parameters or {}
|
||||
return self.backend_class(self.source_url, **backend_params)
|
||||
@@ -176,6 +155,8 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
"""
|
||||
Create/update/delete child DataFiles as necessary to synchronize with the remote source.
|
||||
"""
|
||||
from core.signals import post_sync, pre_sync
|
||||
|
||||
if self.status == DataSourceStatusChoices.SYNCING:
|
||||
raise SyncError(_("Cannot initiate sync; syncing already in progress."))
|
||||
|
||||
@@ -217,7 +198,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
logger.debug(f"Updated {updated_count} files")
|
||||
|
||||
# Bulk delete deleted files
|
||||
deleted_count, _ = DataFile.objects.filter(pk__in=deleted_file_ids).delete()
|
||||
deleted_count, __ = DataFile.objects.filter(pk__in=deleted_file_ids).delete()
|
||||
logger.debug(f"Deleted {deleted_count} files")
|
||||
|
||||
# Walk the local replication to find new files
|
||||
|
||||
@@ -93,9 +93,14 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
||||
self.file_path = os.path.basename(self.data_path)
|
||||
|
||||
# Ensure that the file root and path make a unique pair
|
||||
if self._meta.model.objects.filter(file_root=self.file_root, file_path=self.file_path).exclude(pk=self.pk).exists():
|
||||
if self._meta.model.objects.filter(
|
||||
file_root=self.file_root, file_path=self.file_path
|
||||
).exclude(pk=self.pk).exists():
|
||||
raise ValidationError(
|
||||
f"A {self._meta.verbose_name.lower()} with this file path already exists ({self.file_root}/{self.file_path}).")
|
||||
_("A {model} with this file path already exists ({path}).").format(
|
||||
model=self._meta.verbose_name.lower(),
|
||||
path=f"{self.file_root}/{self.file_path}"
|
||||
))
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# Delete file from disk
|
||||
|
||||
@@ -9,13 +9,11 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from rq.exceptions import InvalidJobOperation
|
||||
|
||||
from core.choices import JobStatusChoices
|
||||
from core.models import ObjectType
|
||||
from core.signals import job_end, job_start
|
||||
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
||||
from netbox.config import get_config
|
||||
from netbox.constants import RQ_QUEUE_DEFAULT
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.rqworker import get_queue_for_model
|
||||
|
||||
@@ -32,6 +30,8 @@ class Job(models.Model):
|
||||
to='contenttypes.ContentType',
|
||||
related_name='jobs',
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
object_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
@@ -117,10 +117,11 @@ class Job(models.Model):
|
||||
|
||||
def get_absolute_url(self):
|
||||
# TODO: Employ dynamic registration
|
||||
if self.object_type.model == 'reportmodule':
|
||||
return reverse(f'extras:report_result', kwargs={'job_pk': self.pk})
|
||||
if self.object_type.model == 'scriptmodule':
|
||||
return reverse(f'extras:script_result', kwargs={'job_pk': self.pk})
|
||||
if self.object_type:
|
||||
if self.object_type.model == 'reportmodule':
|
||||
return reverse('extras:report_result', kwargs={'job_pk': self.pk})
|
||||
elif self.object_type.model == 'scriptmodule':
|
||||
return reverse('extras:script_result', kwargs={'job_pk': self.pk})
|
||||
return reverse('core:job', args=[self.pk])
|
||||
|
||||
def get_status_color(self):
|
||||
@@ -130,7 +131,7 @@ class Job(models.Model):
|
||||
super().clean()
|
||||
|
||||
# Validate the assigned object type
|
||||
if self.object_type not in ObjectType.objects.with_feature('jobs'):
|
||||
if self.object_type and self.object_type not in ObjectType.objects.with_feature('jobs'):
|
||||
raise ValidationError(
|
||||
_("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type)
|
||||
)
|
||||
@@ -153,12 +154,16 @@ class Job(models.Model):
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
rq_queue_name = get_config().QUEUE_MAPPINGS.get(self.object_type.model, RQ_QUEUE_DEFAULT)
|
||||
rq_queue_name = get_queue_for_model(self.object_type.model if self.object_type else None)
|
||||
queue = django_rq.get_queue(rq_queue_name)
|
||||
job = queue.fetch_job(str(self.job_id))
|
||||
|
||||
if job:
|
||||
job.cancel()
|
||||
try:
|
||||
job.cancel()
|
||||
except InvalidJobOperation:
|
||||
# Job may raise this exception from get_status() if missing from Redis
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
@@ -198,25 +203,44 @@ class Job(models.Model):
|
||||
job_end.send(self)
|
||||
|
||||
@classmethod
|
||||
def enqueue(cls, func, instance, name='', user=None, schedule_at=None, interval=None, **kwargs):
|
||||
def enqueue(
|
||||
cls,
|
||||
func,
|
||||
instance=None,
|
||||
name='',
|
||||
user=None,
|
||||
schedule_at=None,
|
||||
interval=None,
|
||||
immediate=False,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Create a Job instance and enqueue a job using the given callable
|
||||
|
||||
Args:
|
||||
func: The callable object to be enqueued for execution
|
||||
instance: The NetBox object to which this job pertains
|
||||
instance: The NetBox object to which this job pertains (optional)
|
||||
name: Name for the job (optional)
|
||||
user: The user responsible for running the job
|
||||
schedule_at: Schedule the job to be executed at the passed date and time
|
||||
interval: Recurrence interval (in minutes)
|
||||
immediate: Run the job immediately without scheduling it in the background. Should be used for interactive
|
||||
management commands only.
|
||||
"""
|
||||
object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False)
|
||||
rq_queue_name = get_queue_for_model(object_type.model)
|
||||
if schedule_at and immediate:
|
||||
raise ValueError(_("enqueue() cannot be called with values for both schedule_at and immediate."))
|
||||
|
||||
if instance:
|
||||
object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False)
|
||||
object_id = instance.pk
|
||||
else:
|
||||
object_type = object_id = None
|
||||
rq_queue_name = get_queue_for_model(object_type.model if object_type else None)
|
||||
queue = django_rq.get_queue(rq_queue_name)
|
||||
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
|
||||
job = Job.objects.create(
|
||||
job = Job(
|
||||
object_type=object_type,
|
||||
object_id=instance.pk,
|
||||
object_id=object_id,
|
||||
name=name,
|
||||
status=status,
|
||||
scheduled=schedule_at,
|
||||
@@ -224,9 +248,19 @@ class Job(models.Model):
|
||||
user=user,
|
||||
job_id=uuid.uuid4()
|
||||
)
|
||||
job.full_clean()
|
||||
job.save()
|
||||
|
||||
if schedule_at:
|
||||
# Run the job immediately, rather than enqueuing it as a background task. Note that this is a synchronous
|
||||
# (blocking) operation, and execution will pause until the job completes.
|
||||
if immediate:
|
||||
func(job_id=str(job.job_id), job=job, **kwargs)
|
||||
|
||||
# Schedule the job to run at a specific date & time.
|
||||
elif schedule_at:
|
||||
queue.enqueue_at(schedule_at, func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
|
||||
# Schedule the job to run asynchronously at this first available opportunity.
|
||||
else:
|
||||
queue.enqueue(func, job_id=str(job.job_id), job=job, **kwargs)
|
||||
|
||||
|
||||
216
netbox/core/plugins.py
Normal file
216
netbox/core/plugins.py
Normal file
@@ -0,0 +1,216 @@
|
||||
import datetime
|
||||
import importlib
|
||||
import importlib.util
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
from netbox.plugins import PluginConfig
|
||||
from utilities.datetime import datetime_from_timestamp
|
||||
|
||||
USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}'
|
||||
CACHE_KEY_CATALOG_FEED = 'plugins-catalog-feed'
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginAuthor:
|
||||
"""
|
||||
Identifying information for the author of a plugin.
|
||||
"""
|
||||
name: str
|
||||
org_id: str = ''
|
||||
url: str = ''
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginVersion:
|
||||
"""
|
||||
Details for a specific versioned release of a plugin.
|
||||
"""
|
||||
date: datetime.datetime = None
|
||||
version: str = ''
|
||||
netbox_min_version: str = ''
|
||||
netbox_max_version: str = ''
|
||||
has_model: bool = False
|
||||
is_certified: bool = False
|
||||
is_feature: bool = False
|
||||
is_integration: bool = False
|
||||
is_netboxlabs_supported: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class Plugin:
|
||||
"""
|
||||
The representation of a NetBox plugin in the catalog API.
|
||||
"""
|
||||
id: str = ''
|
||||
status: str = ''
|
||||
title_short: str = ''
|
||||
title_long: str = ''
|
||||
tag_line: str = ''
|
||||
description_short: str = ''
|
||||
slug: str = ''
|
||||
author: Optional[PluginAuthor] = None
|
||||
created_at: datetime.datetime = None
|
||||
updated_at: datetime.datetime = None
|
||||
license_type: str = ''
|
||||
homepage_url: str = ''
|
||||
package_name_pypi: str = ''
|
||||
config_name: str = ''
|
||||
is_certified: bool = False
|
||||
release_latest: PluginVersion = field(default_factory=PluginVersion)
|
||||
release_recent_history: list[PluginVersion] = field(default_factory=list)
|
||||
is_local: bool = False # extra field for locally installed plugins
|
||||
is_installed: bool = False
|
||||
installed_version: str = ''
|
||||
|
||||
|
||||
def get_local_plugins(plugins=None):
|
||||
"""
|
||||
Return a dictionary of all locally-installed plugins, mapped by name.
|
||||
"""
|
||||
plugins = plugins or {}
|
||||
local_plugins = {}
|
||||
|
||||
# Gather all locally-installed plugins
|
||||
for plugin_name in settings.PLUGINS:
|
||||
plugin = importlib.import_module(plugin_name)
|
||||
plugin_config: PluginConfig = plugin.config
|
||||
|
||||
local_plugins[plugin_config.name] = Plugin(
|
||||
config_name=plugin_config.name,
|
||||
title_short=plugin_config.verbose_name,
|
||||
title_long=plugin_config.verbose_name,
|
||||
tag_line=plugin_config.description,
|
||||
description_short=plugin_config.description,
|
||||
is_local=True,
|
||||
is_installed=True,
|
||||
installed_version=plugin_config.version,
|
||||
)
|
||||
|
||||
# Update catalog entries for local plugins, or add them to the list if not listed
|
||||
for k, v in local_plugins.items():
|
||||
if k in plugins:
|
||||
plugins[k].is_local = True
|
||||
plugins[k].is_installed = True
|
||||
plugins[k].installed_version = v.installed_version
|
||||
else:
|
||||
plugins[k] = v
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
def get_catalog_plugins():
|
||||
"""
|
||||
Return a dictionary of all entries in the plugins catalog, mapped by name.
|
||||
"""
|
||||
session = requests.Session()
|
||||
|
||||
# Disable catalog fetching for isolated deployments
|
||||
if settings.ISOLATED_DEPLOYMENT:
|
||||
return {}
|
||||
|
||||
def get_pages():
|
||||
# TODO: pagination is currently broken in API
|
||||
payload = {'page': '1', 'per_page': '50'}
|
||||
first_page = session.get(
|
||||
settings.PLUGIN_CATALOG_URL,
|
||||
headers={'User-Agent': USER_AGENT_STRING},
|
||||
proxies=settings.HTTP_PROXIES,
|
||||
timeout=3,
|
||||
params=payload
|
||||
).json()
|
||||
yield first_page
|
||||
num_pages = first_page['metadata']['pagination']['last_page']
|
||||
|
||||
for page in range(2, num_pages + 1):
|
||||
payload['page'] = page
|
||||
next_page = session.get(
|
||||
settings.PLUGIN_CATALOG_URL,
|
||||
headers={'User-Agent': USER_AGENT_STRING},
|
||||
proxies=settings.HTTP_PROXIES,
|
||||
timeout=3,
|
||||
params=payload
|
||||
).json()
|
||||
yield next_page
|
||||
|
||||
def make_plugin_dict():
|
||||
plugins = {}
|
||||
|
||||
for page in get_pages():
|
||||
for data in page['data']:
|
||||
|
||||
# Populate releases
|
||||
releases = []
|
||||
for version in data['release_recent_history']:
|
||||
releases.append(
|
||||
PluginVersion(
|
||||
date=datetime_from_timestamp(version['date']),
|
||||
version=version['version'],
|
||||
netbox_min_version=version['netbox_min_version'],
|
||||
netbox_max_version=version['netbox_max_version'],
|
||||
has_model=version['has_model'],
|
||||
is_certified=version['is_certified'],
|
||||
is_feature=version['is_feature'],
|
||||
is_integration=version['is_integration'],
|
||||
is_netboxlabs_supported=version['is_netboxlabs_supported'],
|
||||
)
|
||||
)
|
||||
releases = sorted(releases, key=lambda x: x.date, reverse=True)
|
||||
latest_release = PluginVersion(
|
||||
date=datetime_from_timestamp(data['release_latest']['date']),
|
||||
version=data['release_latest']['version'],
|
||||
netbox_min_version=data['release_latest']['netbox_min_version'],
|
||||
netbox_max_version=data['release_latest']['netbox_max_version'],
|
||||
has_model=data['release_latest']['has_model'],
|
||||
is_certified=data['release_latest']['is_certified'],
|
||||
is_feature=data['release_latest']['is_feature'],
|
||||
is_integration=data['release_latest']['is_integration'],
|
||||
is_netboxlabs_supported=data['release_latest']['is_netboxlabs_supported'],
|
||||
)
|
||||
|
||||
# Populate author (if any)
|
||||
if data['author']:
|
||||
author = PluginAuthor(
|
||||
name=data['author']['name'],
|
||||
org_id=data['author']['org_id'],
|
||||
url=data['author']['url'],
|
||||
)
|
||||
else:
|
||||
author = None
|
||||
|
||||
# Populate plugin data
|
||||
plugins[data['config_name']] = Plugin(
|
||||
id=data['id'],
|
||||
status=data['status'],
|
||||
title_short=data['title_short'],
|
||||
title_long=data['title_long'],
|
||||
tag_line=data['tag_line'],
|
||||
description_short=data['description_short'],
|
||||
slug=data['slug'],
|
||||
author=author,
|
||||
created_at=datetime_from_timestamp(data['created_at']),
|
||||
updated_at=datetime_from_timestamp(data['updated_at']),
|
||||
license_type=data['license_type'],
|
||||
homepage_url=data['homepage_url'],
|
||||
package_name_pypi=data['package_name_pypi'],
|
||||
config_name=data['config_name'],
|
||||
is_certified=data['is_certified'],
|
||||
release_latest=latest_release,
|
||||
release_recent_history=releases,
|
||||
)
|
||||
|
||||
return plugins
|
||||
|
||||
catalog_plugins = cache.get(CACHE_KEY_CATALOG_FEED, default={})
|
||||
if not catalog_plugins:
|
||||
try:
|
||||
catalog_plugins = make_plugin_dict()
|
||||
cache.set(CACHE_KEY_CATALOG_FEED, catalog_plugins, 3600)
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
return catalog_plugins
|
||||
26
netbox/core/querysets.py
Normal file
26
netbox/core/querysets.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.utils import ProgrammingError
|
||||
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
__all__ = (
|
||||
'ObjectChangeQuerySet',
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeQuerySet(RestrictedQuerySet):
|
||||
|
||||
def valid_models(self):
|
||||
# Exclude any change records which refer to an instance of a model that's no longer installed. This
|
||||
# can happen when a plugin is removed but its data remains in the database, for example.
|
||||
try:
|
||||
content_types = ContentType.objects.get_for_models(*apps.get_models()).values()
|
||||
except ProgrammingError:
|
||||
# Handle the case where the database schema has not yet been initialized
|
||||
content_types = ContentType.objects.none()
|
||||
|
||||
content_type_ids = set(
|
||||
ct.pk for ct in content_types
|
||||
)
|
||||
return self.filter(changed_object_type_id__in=content_type_ids)
|
||||
@@ -1,9 +1,26 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import Signal, receiver
|
||||
import logging
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.fields.reverse_related import ManyToManyRel
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
||||
from django.dispatch import receiver, Signal
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
||||
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from core.events import *
|
||||
from core.models import ObjectChange
|
||||
from extras.events import enqueue_event
|
||||
from extras.utils import run_validators
|
||||
from netbox.config import get_config
|
||||
from netbox.context import current_request, events_queue
|
||||
from netbox.models.features import ChangeLoggingMixin
|
||||
from utilities.exceptions import AbortRequest
|
||||
from .models import ConfigRevision
|
||||
|
||||
__all__ = (
|
||||
'clear_events',
|
||||
'job_end',
|
||||
'job_start',
|
||||
'post_sync',
|
||||
@@ -18,6 +35,152 @@ job_end = Signal()
|
||||
pre_sync = Signal()
|
||||
post_sync = Signal()
|
||||
|
||||
# Event signals
|
||||
clear_events = Signal()
|
||||
|
||||
|
||||
#
|
||||
# Change logging & event handling
|
||||
#
|
||||
|
||||
@receiver((post_save, m2m_changed))
|
||||
def handle_changed_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Fires when an object is created or updated.
|
||||
"""
|
||||
m2m_changed = False
|
||||
|
||||
if not hasattr(instance, 'to_objectchange'):
|
||||
return
|
||||
|
||||
# Get the current request, or bail if not set
|
||||
request = current_request.get()
|
||||
if request is None:
|
||||
return
|
||||
|
||||
# Determine the type of change being made
|
||||
if kwargs.get('created'):
|
||||
event_type = OBJECT_CREATED
|
||||
elif 'created' in kwargs:
|
||||
event_type = OBJECT_UPDATED
|
||||
elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']:
|
||||
# m2m_changed with objects added or removed
|
||||
m2m_changed = True
|
||||
event_type = OBJECT_UPDATED
|
||||
else:
|
||||
return
|
||||
|
||||
# Create/update an ObjectChange record for this change
|
||||
action = {
|
||||
OBJECT_CREATED: ObjectChangeActionChoices.ACTION_CREATE,
|
||||
OBJECT_UPDATED: ObjectChangeActionChoices.ACTION_UPDATE,
|
||||
OBJECT_DELETED: ObjectChangeActionChoices.ACTION_DELETE,
|
||||
}[event_type]
|
||||
objectchange = instance.to_objectchange(action)
|
||||
# If this is a many-to-many field change, check for a previous ObjectChange instance recorded
|
||||
# for this object by this request and update it
|
||||
if m2m_changed and (
|
||||
prev_change := ObjectChange.objects.filter(
|
||||
changed_object_type=ContentType.objects.get_for_model(instance),
|
||||
changed_object_id=instance.pk,
|
||||
request_id=request.id
|
||||
).first()
|
||||
):
|
||||
prev_change.postchange_data = objectchange.postchange_data
|
||||
prev_change.save()
|
||||
elif objectchange and objectchange.has_changes:
|
||||
objectchange.user = request.user
|
||||
objectchange.request_id = request.id
|
||||
objectchange.save()
|
||||
|
||||
# Ensure that we're working with fresh M2M assignments
|
||||
if m2m_changed:
|
||||
instance.refresh_from_db()
|
||||
|
||||
# Enqueue the object for event processing
|
||||
queue = events_queue.get()
|
||||
enqueue_event(queue, instance, request.user, request.id, event_type)
|
||||
events_queue.set(queue)
|
||||
|
||||
# Increment metric counters
|
||||
if event_type == OBJECT_CREATED:
|
||||
model_inserts.labels(instance._meta.model_name).inc()
|
||||
elif event_type == OBJECT_UPDATED:
|
||||
model_updates.labels(instance._meta.model_name).inc()
|
||||
|
||||
|
||||
@receiver(pre_delete)
|
||||
def handle_deleted_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Fires when an object is deleted.
|
||||
"""
|
||||
# Run any deletion protection rules for the object. Note that this must occur prior
|
||||
# to queueing any events for the object being deleted, in case a validation error is
|
||||
# raised, causing the deletion to fail.
|
||||
model_name = f'{sender._meta.app_label}.{sender._meta.model_name}'
|
||||
validators = get_config().PROTECTION_RULES.get(model_name, [])
|
||||
try:
|
||||
run_validators(instance, validators)
|
||||
except ValidationError as e:
|
||||
raise AbortRequest(
|
||||
_("Deletion is prevented by a protection rule: {message}").format(message=e)
|
||||
)
|
||||
|
||||
# Get the current request, or bail if not set
|
||||
request = current_request.get()
|
||||
if request is None:
|
||||
return
|
||||
|
||||
# Record an ObjectChange if applicable
|
||||
if hasattr(instance, 'to_objectchange'):
|
||||
if hasattr(instance, 'snapshot') and not getattr(instance, '_prechange_snapshot', None):
|
||||
instance.snapshot()
|
||||
objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
|
||||
objectchange.user = request.user
|
||||
objectchange.request_id = request.id
|
||||
objectchange.save()
|
||||
|
||||
# Django does not automatically send an m2m_changed signal for the reverse direction of a
|
||||
# many-to-many relationship (see https://code.djangoproject.com/ticket/17688), so we need to
|
||||
# trigger one manually. We do this by checking for any reverse M2M relationships on the
|
||||
# instance being deleted, and explicitly call .remove() on the remote M2M field to delete
|
||||
# the association. This triggers an m2m_changed signal with the `post_remove` action type
|
||||
# for the forward direction of the relationship, ensuring that the change is recorded.
|
||||
for relation in instance._meta.related_objects:
|
||||
if type(relation) is not ManyToManyRel:
|
||||
continue
|
||||
related_model = relation.related_model
|
||||
related_field_name = relation.remote_field.name
|
||||
if not issubclass(related_model, ChangeLoggingMixin):
|
||||
# We only care about triggering the m2m_changed signal for models which support
|
||||
# change logging
|
||||
continue
|
||||
for obj in related_model.objects.filter(**{related_field_name: instance.pk}):
|
||||
obj.snapshot() # Ensure the change record includes the "before" state
|
||||
getattr(obj, related_field_name).remove(instance)
|
||||
|
||||
# Enqueue the object for event processing
|
||||
queue = events_queue.get()
|
||||
enqueue_event(queue, instance, request.user, request.id, OBJECT_DELETED)
|
||||
events_queue.set(queue)
|
||||
|
||||
# Increment metric counters
|
||||
model_deletes.labels(instance._meta.model_name).inc()
|
||||
|
||||
|
||||
@receiver(clear_events)
|
||||
def clear_events_queue(sender, **kwargs):
|
||||
"""
|
||||
Delete any queued events (e.g. because of an aborted bulk transaction)
|
||||
"""
|
||||
logger = logging.getLogger('events')
|
||||
logger.info(f"Clearing {len(events_queue.get())} queued events ({sender})")
|
||||
events_queue.set({})
|
||||
|
||||
|
||||
#
|
||||
# DataSource handlers
|
||||
#
|
||||
|
||||
@receiver(post_sync)
|
||||
def auto_sync(instance, **kwargs):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .change_logging import *
|
||||
from .config import *
|
||||
from .data import *
|
||||
from .jobs import *
|
||||
|
||||
53
netbox/core/tables/change_logging.py
Normal file
53
netbox/core/tables/change_logging.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ObjectChange
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from .template_code import *
|
||||
|
||||
__all__ = (
|
||||
'ObjectChangeTable',
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeTable(NetBoxTable):
|
||||
time = columns.DateTimeColumn(
|
||||
verbose_name=_('Time'),
|
||||
timespec='minutes',
|
||||
linkify=True
|
||||
)
|
||||
user_name = tables.Column(
|
||||
verbose_name=_('Username')
|
||||
)
|
||||
full_name = tables.TemplateColumn(
|
||||
accessor=tables.A('user'),
|
||||
template_code=OBJECTCHANGE_FULL_NAME,
|
||||
verbose_name=_('Full Name'),
|
||||
orderable=False
|
||||
)
|
||||
action = columns.ChoiceFieldColumn(
|
||||
verbose_name=_('Action'),
|
||||
)
|
||||
changed_object_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Type')
|
||||
)
|
||||
object_repr = tables.TemplateColumn(
|
||||
accessor=tables.A('changed_object'),
|
||||
template_code=OBJECTCHANGE_OBJECT,
|
||||
verbose_name=_('Object'),
|
||||
orderable=False
|
||||
)
|
||||
request_id = tables.TemplateColumn(
|
||||
template_code=OBJECTCHANGE_REQUEST_ID,
|
||||
verbose_name=_('Request ID')
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=()
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = ObjectChange
|
||||
fields = (
|
||||
'pk', 'time', 'user_name', 'full_name', 'action', 'changed_object_type', 'object_repr', 'request_id',
|
||||
'actions',
|
||||
)
|
||||
@@ -19,6 +19,7 @@ REVISION_BUTTONS = """
|
||||
class ConfigRevisionTable(NetBoxTable):
|
||||
is_active = columns.BooleanColumn(
|
||||
verbose_name=_('Is Active'),
|
||||
false_mark=None
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
actions=('delete',),
|
||||
|
||||
@@ -1,39 +1,84 @@
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.tables import BaseTable
|
||||
|
||||
from netbox.tables import BaseTable, columns
|
||||
|
||||
__all__ = (
|
||||
'PluginTable',
|
||||
'CatalogPluginTable',
|
||||
'PluginVersionTable',
|
||||
)
|
||||
|
||||
|
||||
class PluginTable(BaseTable):
|
||||
name = tables.Column(
|
||||
accessor=tables.A('verbose_name'),
|
||||
verbose_name=_('Name')
|
||||
)
|
||||
class PluginVersionTable(BaseTable):
|
||||
version = tables.Column(
|
||||
verbose_name=_('Version')
|
||||
)
|
||||
package = tables.Column(
|
||||
accessor=tables.A('name'),
|
||||
verbose_name=_('Package')
|
||||
last_updated = columns.DateTimeColumn(
|
||||
accessor=tables.A('date'),
|
||||
timespec='minutes',
|
||||
verbose_name=_('Last Updated')
|
||||
)
|
||||
author = tables.Column(
|
||||
verbose_name=_('Author')
|
||||
min_version = tables.Column(
|
||||
accessor=tables.A('netbox_min_version'),
|
||||
verbose_name=_('Minimum NetBox Version')
|
||||
)
|
||||
author_email = tables.Column(
|
||||
verbose_name=_('Author Email')
|
||||
)
|
||||
description = tables.Column(
|
||||
verbose_name=_('Description')
|
||||
max_version = tables.Column(
|
||||
accessor=tables.A('netbox_max_version'),
|
||||
verbose_name=_('Maximum NetBox Version')
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
empty_text = _('No plugins found')
|
||||
empty_text = _('No plugin data found')
|
||||
fields = (
|
||||
'name', 'version', 'package', 'author', 'author_email', 'description',
|
||||
'version', 'last_updated', 'min_version', 'max_version',
|
||||
)
|
||||
default_columns = (
|
||||
'name', 'version', 'package', 'description',
|
||||
'version', 'last_updated', 'min_version', 'max_version',
|
||||
)
|
||||
orderable = False
|
||||
|
||||
|
||||
class CatalogPluginTable(BaseTable):
|
||||
title_long = tables.Column(
|
||||
linkify=('core:plugin', [tables.A('config_name')]),
|
||||
verbose_name=_('Name')
|
||||
)
|
||||
author = tables.Column(
|
||||
accessor=tables.A('author__name'),
|
||||
verbose_name=_('Author')
|
||||
)
|
||||
is_local = columns.BooleanColumn(
|
||||
verbose_name=_('Local')
|
||||
)
|
||||
is_installed = columns.BooleanColumn(
|
||||
verbose_name=_('Installed')
|
||||
)
|
||||
is_certified = columns.BooleanColumn(
|
||||
verbose_name=_('Certified')
|
||||
)
|
||||
created_at = columns.DateTimeColumn(
|
||||
verbose_name=_('Published')
|
||||
)
|
||||
updated_at = columns.DateTimeColumn(
|
||||
verbose_name=_('Updated')
|
||||
)
|
||||
installed_version = tables.Column(
|
||||
verbose_name=_('Installed Version')
|
||||
)
|
||||
latest_version = tables.Column(
|
||||
accessor=tables.A('release_latest__version'),
|
||||
verbose_name=_('Latest Version')
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
empty_text = _('No plugin data found')
|
||||
fields = (
|
||||
'title_long', 'author', 'is_local', 'is_installed', 'is_certified', 'created_at', 'updated_at',
|
||||
'installed_version', 'latest_version',
|
||||
)
|
||||
default_columns = (
|
||||
'title_long', 'author', 'is_local', 'is_installed', 'is_certified', 'installed_version', 'latest_version',
|
||||
)
|
||||
# List installed plugins first, then certified plugins, then
|
||||
# everything else (with each tranche ordered alphabetically)
|
||||
order_by = ('-is_installed', '-is_certified', 'name')
|
||||
|
||||
16
netbox/core/tables/template_code.py
Normal file
16
netbox/core/tables/template_code.py
Normal file
@@ -0,0 +1,16 @@
|
||||
OBJECTCHANGE_FULL_NAME = """
|
||||
{% load helpers %}
|
||||
{{ value.get_full_name|placeholder }}
|
||||
"""
|
||||
|
||||
OBJECTCHANGE_OBJECT = """
|
||||
{% if value and value.get_absolute_url %}
|
||||
<a href="{{ value.get_absolute_url }}">{{ record.object_repr }}</a>
|
||||
{% else %}
|
||||
{{ record.object_repr }}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
OBJECTCHANGE_REQUEST_ID = """
|
||||
<a href="{% url 'core:objectchange_list' %}?request_id={{ value }}">{{ value }}</a>
|
||||
"""
|
||||
@@ -1,7 +1,14 @@
|
||||
import uuid
|
||||
|
||||
from django_rq import get_queue
|
||||
from django_rq.workers import get_worker
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from rq.job import Job as RQ_Job, JobStatus
|
||||
from rq.registry import FailedJobRegistry, StartedJobRegistry
|
||||
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
from users.models import Token, User
|
||||
from utilities.testing import APITestCase, APIViewTestCases, TestCase
|
||||
from ..models import *
|
||||
|
||||
|
||||
@@ -57,6 +64,7 @@ class DataFileTest(
|
||||
):
|
||||
model = DataFile
|
||||
brief_fields = ['display', 'id', 'path', 'url']
|
||||
user_permissions = ('core.view_datasource', )
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -90,3 +98,164 @@ class DataFileTest(
|
||||
),
|
||||
)
|
||||
DataFile.objects.bulk_create(data_files)
|
||||
|
||||
|
||||
class BackgroundTaskTestCase(TestCase):
|
||||
user_permissions = ()
|
||||
|
||||
@staticmethod
|
||||
def dummy_job_default():
|
||||
return "Job finished"
|
||||
|
||||
@staticmethod
|
||||
def dummy_job_failing():
|
||||
raise Exception("Job failed")
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a user and token for API calls.
|
||||
"""
|
||||
# Create the test user and assign permissions
|
||||
self.user = User.objects.create_user(username='testuser')
|
||||
self.user.is_staff = True
|
||||
self.user.is_active = True
|
||||
self.user.save()
|
||||
self.token = Token.objects.create(user=self.user)
|
||||
self.header = {'HTTP_AUTHORIZATION': f'Token {self.token.key}'}
|
||||
|
||||
# Clear all queues prior to running each test
|
||||
get_queue('default').connection.flushall()
|
||||
get_queue('high').connection.flushall()
|
||||
get_queue('low').connection.flushall()
|
||||
|
||||
def test_background_queue_list(self):
|
||||
url = reverse('core-api:rqqueue-list')
|
||||
|
||||
# Attempt to load view without permission
|
||||
self.user.is_staff = False
|
||||
self.user.save()
|
||||
response = self.client.get(url, **self.header)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# Load view with permission
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
response = self.client.get(url, **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('default', str(response.content))
|
||||
self.assertIn('high', str(response.content))
|
||||
self.assertIn('low', str(response.content))
|
||||
|
||||
def test_background_queue(self):
|
||||
response = self.client.get(reverse('core-api:rqqueue-detail', args=['default']), **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('default', str(response.content))
|
||||
self.assertIn('oldest_job_timestamp', str(response.content))
|
||||
self.assertIn('scheduled_jobs', str(response.content))
|
||||
|
||||
def test_background_task_list(self):
|
||||
queue = get_queue('default')
|
||||
queue.enqueue(self.dummy_job_default)
|
||||
|
||||
response = self.client.get(reverse('core-api:rqtask-list'), **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('origin', str(response.content))
|
||||
self.assertIn('core.tests.test_api.BackgroundTaskTestCase.dummy_job_default()', str(response.content))
|
||||
|
||||
def test_background_task(self):
|
||||
queue = get_queue('default')
|
||||
job = queue.enqueue(self.dummy_job_default)
|
||||
|
||||
response = self.client.get(reverse('core-api:rqtask-detail', args=[job.id]), **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(str(job.id), str(response.content))
|
||||
self.assertIn('origin', str(response.content))
|
||||
self.assertIn('meta', str(response.content))
|
||||
self.assertIn('kwargs', str(response.content))
|
||||
|
||||
def test_background_task_delete(self):
|
||||
queue = get_queue('default')
|
||||
job = queue.enqueue(self.dummy_job_default)
|
||||
|
||||
response = self.client.post(reverse('core-api:rqtask-delete', args=[job.id]), **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(RQ_Job.exists(job.id, connection=queue.connection))
|
||||
queue = get_queue('default')
|
||||
self.assertNotIn(job.id, queue.job_ids)
|
||||
|
||||
def test_background_task_requeue(self):
|
||||
queue = get_queue('default')
|
||||
|
||||
# Enqueue & run a job that will fail
|
||||
job = queue.enqueue(self.dummy_job_failing)
|
||||
worker = get_worker('default')
|
||||
worker.work(burst=True)
|
||||
self.assertTrue(job.is_failed)
|
||||
|
||||
# Re-enqueue the failed job and check that its status has been reset
|
||||
response = self.client.post(reverse('core-api:rqtask-requeue', args=[job.id]), **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
job = RQ_Job.fetch(job.id, queue.connection)
|
||||
self.assertFalse(job.is_failed)
|
||||
|
||||
def test_background_task_enqueue(self):
|
||||
queue = get_queue('default')
|
||||
|
||||
# Enqueue some jobs that each depends on its predecessor
|
||||
job = previous_job = None
|
||||
for _ in range(0, 3):
|
||||
job = queue.enqueue(self.dummy_job_default, depends_on=previous_job)
|
||||
previous_job = job
|
||||
|
||||
# Check that the last job to be enqueued has a status of deferred
|
||||
self.assertIsNotNone(job)
|
||||
self.assertEqual(job.get_status(), JobStatus.DEFERRED)
|
||||
self.assertIsNone(job.enqueued_at)
|
||||
|
||||
# Force-enqueue the deferred job
|
||||
response = self.client.post(reverse('core-api:rqtask-enqueue', args=[job.id]), **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that job's status is updated correctly
|
||||
job = queue.fetch_job(job.id)
|
||||
self.assertEqual(job.get_status(), JobStatus.QUEUED)
|
||||
self.assertIsNotNone(job.enqueued_at)
|
||||
|
||||
def test_background_task_stop(self):
|
||||
queue = get_queue('default')
|
||||
|
||||
worker = get_worker('default')
|
||||
job = queue.enqueue(self.dummy_job_default)
|
||||
worker.prepare_job_execution(job)
|
||||
|
||||
self.assertEqual(job.get_status(), JobStatus.STARTED)
|
||||
response = self.client.post(reverse('core-api:rqtask-stop', args=[job.id]), **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
|
||||
started_job_registry = StartedJobRegistry(queue.name, connection=queue.connection)
|
||||
self.assertEqual(len(started_job_registry), 0)
|
||||
|
||||
canceled_job_registry = FailedJobRegistry(queue.name, connection=queue.connection)
|
||||
self.assertEqual(len(canceled_job_registry), 1)
|
||||
self.assertIn(job.id, canceled_job_registry)
|
||||
|
||||
def test_worker_list(self):
|
||||
worker1 = get_worker('default', name=uuid.uuid4().hex)
|
||||
worker1.register_birth()
|
||||
|
||||
worker2 = get_worker('high')
|
||||
worker2.register_birth()
|
||||
|
||||
response = self.client.get(reverse('core-api:rqworker-list'), **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(str(worker1.name), str(response.content))
|
||||
|
||||
def test_worker(self):
|
||||
worker1 = get_worker('default', name=uuid.uuid4().hex)
|
||||
worker1.register_birth()
|
||||
|
||||
response = self.client.get(reverse('core-api:rqworker-detail', args=[worker1.name]), **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(str(worker1.name), str(response.content))
|
||||
self.assertIn('birth_date', str(response.content))
|
||||
self.assertIn('total_working_time', str(response.content))
|
||||
|
||||
@@ -3,11 +3,12 @@ from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from core.models import ObjectType
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from core.models import ObjectChange, ObjectType
|
||||
from dcim.choices import SiteStatusChoices
|
||||
from dcim.models import Site
|
||||
from extras.choices import *
|
||||
from extras.models import CustomField, CustomFieldChoiceSet, ObjectChange, Tag
|
||||
from extras.models import CustomField, CustomFieldChoiceSet, Tag
|
||||
from utilities.testing import APITestCase
|
||||
from utilities.testing.utils import create_tags, post_data
|
||||
from utilities.testing.views import ModelViewTestCase
|
||||
@@ -1,7 +1,13 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
|
||||
from dcim.models import Site
|
||||
from ipam.models import IPAddress
|
||||
from users.models import User
|
||||
from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests
|
||||
from ..choices import *
|
||||
from ..filtersets import *
|
||||
from ..models import *
|
||||
@@ -132,3 +138,99 @@ class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
'a78168c7c97115bafd96450ed03ea43acec495094c5caa28f0d02e20e3a76cc2',
|
||||
]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = ObjectChange.objects.all()
|
||||
filterset = ObjectChangeFilterSet
|
||||
ignore_fields = ('prechange_data', 'postchange_data')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
users = (
|
||||
User(username='user1'),
|
||||
User(username='user2'),
|
||||
User(username='user3'),
|
||||
)
|
||||
User.objects.bulk_create(users)
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
ipaddress = IPAddress.objects.create(address='192.0.2.1/24')
|
||||
|
||||
object_changes = (
|
||||
ObjectChange(
|
||||
user=users[0],
|
||||
user_name=users[0].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_CREATE,
|
||||
changed_object=site,
|
||||
object_repr=str(site),
|
||||
postchange_data={'name': site.name, 'slug': site.slug}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[0],
|
||||
user_name=users[0].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_UPDATE,
|
||||
changed_object=site,
|
||||
object_repr=str(site),
|
||||
postchange_data={'name': site.name, 'slug': site.slug}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[1],
|
||||
user_name=users[1].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_DELETE,
|
||||
changed_object=site,
|
||||
object_repr=str(site),
|
||||
postchange_data={'name': site.name, 'slug': site.slug}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[1],
|
||||
user_name=users[1].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_CREATE,
|
||||
changed_object=ipaddress,
|
||||
object_repr=str(ipaddress),
|
||||
postchange_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[2],
|
||||
user_name=users[2].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_UPDATE,
|
||||
changed_object=ipaddress,
|
||||
object_repr=str(ipaddress),
|
||||
postchange_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
),
|
||||
ObjectChange(
|
||||
user=users[2],
|
||||
user_name=users[2].username,
|
||||
request_id=uuid.uuid4(),
|
||||
action=ObjectChangeActionChoices.ACTION_DELETE,
|
||||
changed_object=ipaddress,
|
||||
object_repr=str(ipaddress),
|
||||
postchange_data={'address': ipaddress.address, 'status': ipaddress.status}
|
||||
),
|
||||
)
|
||||
ObjectChange.objects.bulk_create(object_changes)
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'Site 1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_user(self):
|
||||
params = {'user_id': User.objects.filter(username__in=['user1', 'user2']).values_list('pk', flat=True)}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'user': ['user1', 'user2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_user_name(self):
|
||||
params = {'user_name': ['user1', 'user2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_changed_object_type(self):
|
||||
params = {'changed_object_type': 'dcim.site'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
params = {'changed_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from core.models import DataSource
|
||||
from extras.choices import ObjectChangeActionChoices
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import logging
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
@@ -10,8 +10,11 @@ from django_rq.workers import get_worker
|
||||
from rq.job import Job as RQ_Job, JobStatus
|
||||
from rq.registry import DeferredJobRegistry, FailedJobRegistry, FinishedJobRegistry, StartedJobRegistry
|
||||
|
||||
from core.choices import ObjectChangeActionChoices
|
||||
from core.models import *
|
||||
from dcim.models import Site
|
||||
from users.models import User
|
||||
from utilities.testing import TestCase, ViewTestCases, create_tags
|
||||
from ..models import *
|
||||
|
||||
|
||||
class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
@@ -99,6 +102,43 @@ class DataFileTestCase(
|
||||
DataFile.objects.bulk_create(data_files)
|
||||
|
||||
|
||||
# TODO: Convert to StandardTestCases.Views
|
||||
class ObjectChangeTestCase(TestCase):
|
||||
user_permissions = (
|
||||
'core.view_objectchange',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
site = Site(name='Site 1', slug='site-1')
|
||||
site.save()
|
||||
|
||||
# Create three ObjectChanges
|
||||
user = User.objects.create_user(username='testuser2')
|
||||
for i in range(1, 4):
|
||||
oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE)
|
||||
oc.user = user
|
||||
oc.request_id = uuid.uuid4()
|
||||
oc.save()
|
||||
|
||||
def test_objectchange_list(self):
|
||||
|
||||
url = reverse('core:objectchange_list')
|
||||
params = {
|
||||
"user": User.objects.first().pk,
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
def test_objectchange(self):
|
||||
|
||||
objectchange = ObjectChange.objects.first()
|
||||
response = self.client.get(objectchange.get_absolute_url())
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
|
||||
class BackgroundTaskTestCase(TestCase):
|
||||
user_permissions = ()
|
||||
|
||||
@@ -268,6 +308,7 @@ class BackgroundTaskTestCase(TestCase):
|
||||
worker = get_worker('default')
|
||||
job = queue.enqueue(self.dummy_job_default)
|
||||
worker.prepare_job_execution(job)
|
||||
worker.prepare_execution(job)
|
||||
|
||||
self.assertEqual(job.get_status(), JobStatus.STARTED)
|
||||
|
||||
@@ -305,3 +346,32 @@ class BackgroundTaskTestCase(TestCase):
|
||||
self.assertIn(str(worker1.name), str(response.content))
|
||||
self.assertIn('Birth', str(response.content))
|
||||
self.assertIn('Total working time', str(response.content))
|
||||
|
||||
|
||||
class SystemTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
def test_system_view_default(self):
|
||||
# Test UI render
|
||||
response = self.client.get(reverse('core:system'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test export
|
||||
response = self.client.get(f"{reverse('core:system')}?export=true")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_system_view_with_config_revision(self):
|
||||
ConfigRevision.objects.create()
|
||||
|
||||
# Test UI render
|
||||
response = self.client.get(reverse('core:system'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test export
|
||||
response = self.client.get(f"{reverse('core:system')}?export=true")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -6,43 +6,50 @@ from . import views
|
||||
app_name = 'core'
|
||||
urlpatterns = (
|
||||
|
||||
# Data sources
|
||||
path('data-sources/', views.DataSourceListView.as_view(), name='datasource_list'),
|
||||
path('data-sources/add/', views.DataSourceEditView.as_view(), name='datasource_add'),
|
||||
path('data-sources/import/', views.DataSourceBulkImportView.as_view(), name='datasource_import'),
|
||||
path('data-sources/edit/', views.DataSourceBulkEditView.as_view(), name='datasource_bulk_edit'),
|
||||
path('data-sources/delete/', views.DataSourceBulkDeleteView.as_view(), name='datasource_bulk_delete'),
|
||||
path('data-sources/', include(get_model_urls('core', 'datasource', detail=False))),
|
||||
path('data-sources/<int:pk>/', include(get_model_urls('core', 'datasource'))),
|
||||
|
||||
# Data files
|
||||
path('data-files/', views.DataFileListView.as_view(), name='datafile_list'),
|
||||
path('data-files/delete/', views.DataFileBulkDeleteView.as_view(), name='datafile_bulk_delete'),
|
||||
path('data-files/', include(get_model_urls('core', 'datafile', detail=False))),
|
||||
path('data-files/<int:pk>/', include(get_model_urls('core', 'datafile'))),
|
||||
|
||||
# Job results
|
||||
path('jobs/', views.JobListView.as_view(), name='job_list'),
|
||||
path('jobs/delete/', views.JobBulkDeleteView.as_view(), name='job_bulk_delete'),
|
||||
path('jobs/<int:pk>/', views.JobView.as_view(), name='job'),
|
||||
path('jobs/<int:pk>/delete/', views.JobDeleteView.as_view(), name='job_delete'),
|
||||
path('jobs/', include(get_model_urls('core', 'job', detail=False))),
|
||||
path('jobs/<int:pk>/', include(get_model_urls('core', 'job'))),
|
||||
|
||||
path('changelog/', include(get_model_urls('core', 'objectchange', detail=False))),
|
||||
path('changelog/<int:pk>/', include(get_model_urls('core', 'objectchange'))),
|
||||
|
||||
# Background Tasks
|
||||
path('background-queues/', views.BackgroundQueueListView.as_view(), name='background_queue_list'),
|
||||
path('background-queues/<int:queue_index>/<str:status>/', views.BackgroundTaskListView.as_view(), name='background_task_list'),
|
||||
path(
|
||||
'background-queues/<int:queue_index>/<str:status>/',
|
||||
views.BackgroundTaskListView.as_view(),
|
||||
name='background_task_list'
|
||||
),
|
||||
path('background-tasks/<str:job_id>/', views.BackgroundTaskView.as_view(), name='background_task'),
|
||||
path('background-tasks/<str:job_id>/delete/', views.BackgroundTaskDeleteView.as_view(), name='background_task_delete'),
|
||||
path('background-tasks/<str:job_id>/requeue/', views.BackgroundTaskRequeueView.as_view(), name='background_task_requeue'),
|
||||
path('background-tasks/<str:job_id>/enqueue/', views.BackgroundTaskEnqueueView.as_view(), name='background_task_enqueue'),
|
||||
path(
|
||||
'background-tasks/<str:job_id>/delete/',
|
||||
views.BackgroundTaskDeleteView.as_view(),
|
||||
name='background_task_delete'
|
||||
),
|
||||
path(
|
||||
'background-tasks/<str:job_id>/requeue/',
|
||||
views.BackgroundTaskRequeueView.as_view(),
|
||||
name='background_task_requeue'
|
||||
),
|
||||
path(
|
||||
'background-tasks/<str:job_id>/enqueue/',
|
||||
views.BackgroundTaskEnqueueView.as_view(),
|
||||
name='background_task_enqueue'
|
||||
),
|
||||
path('background-tasks/<str:job_id>/stop/', views.BackgroundTaskStopView.as_view(), name='background_task_stop'),
|
||||
path('background-workers/<int:queue_index>/', views.WorkerListView.as_view(), name='worker_list'),
|
||||
path('background-workers/<str:key>/', views.WorkerView.as_view(), name='worker'),
|
||||
|
||||
# Config revisions
|
||||
path('config-revisions/', views.ConfigRevisionListView.as_view(), name='configrevision_list'),
|
||||
path('config-revisions/add/', views.ConfigRevisionEditView.as_view(), name='configrevision_add'),
|
||||
path('config-revisions/delete/', views.ConfigRevisionBulkDeleteView.as_view(), name='configrevision_bulk_delete'),
|
||||
path('config-revisions/<int:pk>/restore/', views.ConfigRevisionRestoreView.as_view(), name='configrevision_restore'),
|
||||
path('config-revisions/', include(get_model_urls('core', 'configrevision', detail=False))),
|
||||
path('config-revisions/<int:pk>/', include(get_model_urls('core', 'configrevision'))),
|
||||
|
||||
# System
|
||||
path('system/', views.SystemView.as_view(), name='system'),
|
||||
|
||||
path('plugins/', views.PluginListView.as_view(), name='plugin_list'),
|
||||
path('plugins/<str:name>/', views.PluginView.as_view(), name='plugin'),
|
||||
)
|
||||
|
||||
155
netbox/core/utils.py
Normal file
155
netbox/core/utils.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_rq.queues import get_queue, get_queue_by_index, get_redis_connection
|
||||
from django_rq.settings import QUEUES_MAP, QUEUES_LIST
|
||||
from django_rq.utils import get_jobs, stop_jobs
|
||||
from rq import requeue_job
|
||||
from rq.exceptions import NoSuchJobError
|
||||
from rq.job import Job as RQ_Job, JobStatus as RQJobStatus
|
||||
from rq.registry import (
|
||||
DeferredJobRegistry,
|
||||
FailedJobRegistry,
|
||||
FinishedJobRegistry,
|
||||
ScheduledJobRegistry,
|
||||
StartedJobRegistry,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
'delete_rq_job',
|
||||
'enqueue_rq_job',
|
||||
'get_rq_jobs',
|
||||
'get_rq_jobs_from_status',
|
||||
'requeue_rq_job',
|
||||
'stop_rq_job',
|
||||
)
|
||||
|
||||
|
||||
def get_rq_jobs():
|
||||
"""
|
||||
Return a list of all RQ jobs.
|
||||
"""
|
||||
jobs = set()
|
||||
|
||||
for queue in QUEUES_LIST:
|
||||
queue = get_queue(queue['name'])
|
||||
jobs.update(queue.get_jobs())
|
||||
|
||||
return list(jobs)
|
||||
|
||||
|
||||
def get_rq_jobs_from_status(queue, status):
|
||||
"""
|
||||
Return the RQ jobs with the given status.
|
||||
"""
|
||||
jobs = []
|
||||
|
||||
try:
|
||||
registry_cls = {
|
||||
RQJobStatus.STARTED: StartedJobRegistry,
|
||||
RQJobStatus.DEFERRED: DeferredJobRegistry,
|
||||
RQJobStatus.FINISHED: FinishedJobRegistry,
|
||||
RQJobStatus.FAILED: FailedJobRegistry,
|
||||
RQJobStatus.SCHEDULED: ScheduledJobRegistry,
|
||||
}[status]
|
||||
except KeyError:
|
||||
raise Http404
|
||||
registry = registry_cls(queue.name, queue.connection)
|
||||
|
||||
job_ids = registry.get_job_ids()
|
||||
if status != RQJobStatus.DEFERRED:
|
||||
jobs = get_jobs(queue, job_ids, registry)
|
||||
else:
|
||||
# Deferred jobs require special handling
|
||||
for job_id in job_ids:
|
||||
try:
|
||||
jobs.append(RQ_Job.fetch(job_id, connection=queue.connection, serializer=queue.serializer))
|
||||
except NoSuchJobError:
|
||||
pass
|
||||
|
||||
if jobs and status == RQJobStatus.SCHEDULED:
|
||||
for job in jobs:
|
||||
job.scheduled_at = registry.get_scheduled_time(job)
|
||||
|
||||
return jobs
|
||||
|
||||
|
||||
def delete_rq_job(job_id):
|
||||
"""
|
||||
Delete the specified RQ job.
|
||||
"""
|
||||
config = QUEUES_LIST[0]
|
||||
try:
|
||||
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
|
||||
except NoSuchJobError:
|
||||
raise Http404(_("Job {job_id} not found").format(job_id=job_id))
|
||||
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
|
||||
# Remove job id from queue and delete the actual job
|
||||
queue.connection.lrem(queue.key, 0, job.id)
|
||||
job.delete()
|
||||
|
||||
|
||||
def requeue_rq_job(job_id):
|
||||
"""
|
||||
Requeue the specified RQ job.
|
||||
"""
|
||||
config = QUEUES_LIST[0]
|
||||
try:
|
||||
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
|
||||
except NoSuchJobError:
|
||||
raise Http404(_("Job {id} not found.").format(id=job_id))
|
||||
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
|
||||
requeue_job(job_id, connection=queue.connection, serializer=queue.serializer)
|
||||
|
||||
|
||||
def enqueue_rq_job(job_id):
|
||||
"""
|
||||
Enqueue the specified RQ job.
|
||||
"""
|
||||
config = QUEUES_LIST[0]
|
||||
try:
|
||||
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
|
||||
except NoSuchJobError:
|
||||
raise Http404(_("Job {id} not found.").format(id=job_id))
|
||||
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
|
||||
try:
|
||||
# _enqueue_job is new in RQ 1.14, this is used to enqueue
|
||||
# job regardless of its dependencies
|
||||
queue._enqueue_job(job)
|
||||
except AttributeError:
|
||||
queue.enqueue_job(job)
|
||||
|
||||
# Remove job from correct registry if needed
|
||||
if job.get_status() == RQJobStatus.DEFERRED:
|
||||
registry = DeferredJobRegistry(queue.name, queue.connection)
|
||||
registry.remove(job)
|
||||
elif job.get_status() == RQJobStatus.FINISHED:
|
||||
registry = FinishedJobRegistry(queue.name, queue.connection)
|
||||
registry.remove(job)
|
||||
elif job.get_status() == RQJobStatus.SCHEDULED:
|
||||
registry = ScheduledJobRegistry(queue.name, queue.connection)
|
||||
registry.remove(job)
|
||||
|
||||
|
||||
def stop_rq_job(job_id):
|
||||
"""
|
||||
Stop the specified RQ job.
|
||||
"""
|
||||
config = QUEUES_LIST[0]
|
||||
try:
|
||||
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
|
||||
except NoSuchJobError:
|
||||
raise Http404(_("Job {job_id} not found").format(job_id=job_id))
|
||||
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
|
||||
return stop_jobs(queue, job_id)[0]
|
||||
@@ -2,7 +2,6 @@ import json
|
||||
import platform
|
||||
|
||||
from django import __version__ as DJANGO_VERSION
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
@@ -15,32 +14,36 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import View
|
||||
from django_rq.queues import get_connection, get_queue_by_index, get_redis_connection
|
||||
from django_rq.settings import QUEUES_MAP, QUEUES_LIST
|
||||
from django_rq.utils import get_jobs, get_statistics, stop_jobs
|
||||
from rq import requeue_job
|
||||
from django_rq.utils import get_statistics
|
||||
from rq.exceptions import NoSuchJobError
|
||||
from rq.job import Job as RQ_Job, JobStatus as RQJobStatus
|
||||
from rq.registry import (
|
||||
DeferredJobRegistry, FailedJobRegistry, FinishedJobRegistry, ScheduledJobRegistry, StartedJobRegistry,
|
||||
)
|
||||
from rq.worker import Worker
|
||||
from rq.worker_registration import clean_worker_registry
|
||||
|
||||
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
|
||||
from netbox.config import get_config, PARAMS
|
||||
from netbox.views import generic
|
||||
from netbox.views.generic.base import BaseObjectView
|
||||
from netbox.views.generic.mixins import TableMixin
|
||||
from utilities.data import shallow_compare_dict
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.json import ConfigJSONEncoder
|
||||
from utilities.query import count_related
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import DataSourceStatusChoices
|
||||
from .jobs import SyncDataSourceJob
|
||||
from .models import *
|
||||
from .plugins import get_catalog_plugins, get_local_plugins
|
||||
from .tables import CatalogPluginTable, PluginVersionTable
|
||||
|
||||
|
||||
#
|
||||
# Data sources
|
||||
#
|
||||
|
||||
@register_model_view(DataSource, 'list', path='', detail=False)
|
||||
class DataSourceListView(generic.ObjectListView):
|
||||
queryset = DataSource.objects.annotate(
|
||||
file_count=count_related(DataFile, 'source')
|
||||
@@ -51,16 +54,12 @@ class DataSourceListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(DataSource)
|
||||
class DataSourceView(generic.ObjectView):
|
||||
class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = DataSource.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(DataFile.objects.restrict(request.user, 'view').filter(source=instance), 'source_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@@ -78,12 +77,20 @@ class DataSourceSyncView(BaseObjectView):
|
||||
|
||||
def post(self, request, pk):
|
||||
datasource = get_object_or_404(self.queryset, pk=pk)
|
||||
job = datasource.enqueue_sync_job(request)
|
||||
|
||||
messages.success(request, f"Queued job #{job.pk} to sync {datasource}")
|
||||
# Enqueue the sync job & update the DataSource's status
|
||||
job = SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
|
||||
datasource.status = DataSourceStatusChoices.QUEUED
|
||||
DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
_("Queued job #{id} to sync {datasource}").format(id=job.pk, datasource=datasource)
|
||||
)
|
||||
return redirect(datasource.get_absolute_url())
|
||||
|
||||
|
||||
@register_model_view(DataSource, 'add', detail=False)
|
||||
@register_model_view(DataSource, 'edit')
|
||||
class DataSourceEditView(generic.ObjectEditView):
|
||||
queryset = DataSource.objects.all()
|
||||
@@ -95,11 +102,13 @@ class DataSourceDeleteView(generic.ObjectDeleteView):
|
||||
queryset = DataSource.objects.all()
|
||||
|
||||
|
||||
@register_model_view(DataSource, 'bulk_import', detail=False)
|
||||
class DataSourceBulkImportView(generic.BulkImportView):
|
||||
queryset = DataSource.objects.all()
|
||||
model_form = forms.DataSourceImportForm
|
||||
|
||||
|
||||
@register_model_view(DataSource, 'bulk_edit', path='edit', detail=False)
|
||||
class DataSourceBulkEditView(generic.BulkEditView):
|
||||
queryset = DataSource.objects.annotate(
|
||||
count_files=count_related(DataFile, 'source')
|
||||
@@ -109,6 +118,7 @@ class DataSourceBulkEditView(generic.BulkEditView):
|
||||
form = forms.DataSourceBulkEditForm
|
||||
|
||||
|
||||
@register_model_view(DataSource, 'bulk_delete', path='delete', detail=False)
|
||||
class DataSourceBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DataSource.objects.annotate(
|
||||
count_files=count_related(DataFile, 'source')
|
||||
@@ -121,6 +131,7 @@ class DataSourceBulkDeleteView(generic.BulkDeleteView):
|
||||
# Data files
|
||||
#
|
||||
|
||||
@register_model_view(DataFile, 'list', path='', detail=False)
|
||||
class DataFileListView(generic.ObjectListView):
|
||||
queryset = DataFile.objects.defer('data')
|
||||
filterset = filtersets.DataFileFilterSet
|
||||
@@ -141,6 +152,7 @@ class DataFileDeleteView(generic.ObjectDeleteView):
|
||||
queryset = DataFile.objects.all()
|
||||
|
||||
|
||||
@register_model_view(DataFile, 'bulk_delete', path='delete', detail=False)
|
||||
class DataFileBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DataFile.objects.defer('data')
|
||||
filterset = filtersets.DataFileFilterSet
|
||||
@@ -151,6 +163,7 @@ class DataFileBulkDeleteView(generic.BulkDeleteView):
|
||||
# Jobs
|
||||
#
|
||||
|
||||
@register_model_view(Job, 'list', path='', detail=False)
|
||||
class JobListView(generic.ObjectListView):
|
||||
queryset = Job.objects.all()
|
||||
filterset = filtersets.JobFilterSet
|
||||
@@ -162,24 +175,98 @@ class JobListView(generic.ObjectListView):
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(Job)
|
||||
class JobView(generic.ObjectView):
|
||||
queryset = Job.objects.all()
|
||||
|
||||
|
||||
@register_model_view(Job, 'delete')
|
||||
class JobDeleteView(generic.ObjectDeleteView):
|
||||
queryset = Job.objects.all()
|
||||
|
||||
|
||||
@register_model_view(Job, 'bulk_delete', path='delete', detail=False)
|
||||
class JobBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Job.objects.all()
|
||||
filterset = filtersets.JobFilterSet
|
||||
table = tables.JobTable
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
@register_model_view(ObjectChange, 'list', path='', detail=False)
|
||||
class ObjectChangeListView(generic.ObjectListView):
|
||||
queryset = ObjectChange.objects.valid_models()
|
||||
filterset = filtersets.ObjectChangeFilterSet
|
||||
filterset_form = forms.ObjectChangeFilterForm
|
||||
table = tables.ObjectChangeTable
|
||||
template_name = 'core/objectchange_list.html'
|
||||
actions = {
|
||||
'export': {'view'},
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ObjectChange)
|
||||
class ObjectChangeView(generic.ObjectView):
|
||||
queryset = ObjectChange.objects.valid_models()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_changes = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
|
||||
request_id=instance.request_id
|
||||
).exclude(
|
||||
pk=instance.pk
|
||||
)
|
||||
related_changes_table = tables.ObjectChangeTable(
|
||||
data=related_changes[:50],
|
||||
orderable=False
|
||||
)
|
||||
|
||||
objectchanges = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
|
||||
changed_object_type=instance.changed_object_type,
|
||||
changed_object_id=instance.changed_object_id,
|
||||
)
|
||||
|
||||
next_change = objectchanges.filter(time__gt=instance.time).order_by('time').first()
|
||||
prev_change = objectchanges.filter(time__lt=instance.time).order_by('-time').first()
|
||||
|
||||
if not instance.prechange_data and instance.action in ['update', 'delete'] and prev_change:
|
||||
non_atomic_change = True
|
||||
prechange_data = prev_change.postchange_data_clean
|
||||
else:
|
||||
non_atomic_change = False
|
||||
prechange_data = instance.prechange_data_clean
|
||||
|
||||
if prechange_data and instance.postchange_data:
|
||||
diff_added = shallow_compare_dict(
|
||||
prechange_data or dict(),
|
||||
instance.postchange_data_clean or dict(),
|
||||
exclude=['last_updated'],
|
||||
)
|
||||
diff_removed = {
|
||||
x: prechange_data.get(x) for x in diff_added
|
||||
} if prechange_data else {}
|
||||
else:
|
||||
diff_added = None
|
||||
diff_removed = None
|
||||
|
||||
return {
|
||||
'diff_added': diff_added,
|
||||
'diff_removed': diff_removed,
|
||||
'next_change': next_change,
|
||||
'prev_change': prev_change,
|
||||
'related_changes_table': related_changes_table,
|
||||
'related_changes_count': related_changes.count(),
|
||||
'non_atomic_change': non_atomic_change
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Config Revisions
|
||||
#
|
||||
|
||||
@register_model_view(ConfigRevision, 'list', path='', detail=False)
|
||||
class ConfigRevisionListView(generic.ObjectListView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
filterset = filtersets.ConfigRevisionFilterSet
|
||||
@@ -192,6 +279,7 @@ class ConfigRevisionView(generic.ObjectView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
|
||||
|
||||
@register_model_view(ConfigRevision, 'add', detail=False)
|
||||
class ConfigRevisionEditView(generic.ObjectEditView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
form = forms.ConfigRevisionForm
|
||||
@@ -202,12 +290,14 @@ class ConfigRevisionDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
|
||||
|
||||
@register_model_view(ConfigRevision, 'bulk_delete', path='delete', detail=False)
|
||||
class ConfigRevisionBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
filterset = filtersets.ConfigRevisionFilterSet
|
||||
table = tables.ConfigRevisionTable
|
||||
|
||||
|
||||
@register_model_view(ConfigRevision, 'restore')
|
||||
class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
|
||||
|
||||
def get_required_permission(self):
|
||||
@@ -224,7 +314,7 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
|
||||
for param in PARAMS:
|
||||
params.append((
|
||||
param.name,
|
||||
current_config.data.get(param.name, None),
|
||||
current_config.data.get(param.name, None) if current_config else None,
|
||||
candidate_config.data.get(param.name, None)
|
||||
))
|
||||
|
||||
@@ -239,7 +329,7 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
|
||||
|
||||
candidate_config = get_object_or_404(ConfigRevision, pk=pk)
|
||||
candidate_config.activate()
|
||||
messages.success(request, f"Restored configuration revision #{pk}")
|
||||
messages.success(request, _("Restored configuration revision #{id}").format(id=pk))
|
||||
|
||||
return redirect(candidate_config.get_absolute_url())
|
||||
|
||||
@@ -270,41 +360,12 @@ class BackgroundTaskListView(TableMixin, BaseRQView):
|
||||
table = tables.BackgroundTaskTable
|
||||
|
||||
def get_table_data(self, request, queue, status):
|
||||
jobs = []
|
||||
|
||||
# Call get_jobs() to returned queued tasks
|
||||
if status == RQJobStatus.QUEUED:
|
||||
return queue.get_jobs()
|
||||
|
||||
# For other statuses, determine the registry to list (or raise a 404 for invalid statuses)
|
||||
try:
|
||||
registry_cls = {
|
||||
RQJobStatus.STARTED: StartedJobRegistry,
|
||||
RQJobStatus.DEFERRED: DeferredJobRegistry,
|
||||
RQJobStatus.FINISHED: FinishedJobRegistry,
|
||||
RQJobStatus.FAILED: FailedJobRegistry,
|
||||
RQJobStatus.SCHEDULED: ScheduledJobRegistry,
|
||||
}[status]
|
||||
except KeyError:
|
||||
raise Http404
|
||||
registry = registry_cls(queue.name, queue.connection)
|
||||
|
||||
job_ids = registry.get_job_ids()
|
||||
if status != RQJobStatus.DEFERRED:
|
||||
jobs = get_jobs(queue, job_ids, registry)
|
||||
else:
|
||||
# Deferred jobs require special handling
|
||||
for job_id in job_ids:
|
||||
try:
|
||||
jobs.append(RQ_Job.fetch(job_id, connection=queue.connection, serializer=queue.serializer))
|
||||
except NoSuchJobError:
|
||||
pass
|
||||
|
||||
if jobs and status == RQJobStatus.SCHEDULED:
|
||||
for job in jobs:
|
||||
job.scheduled_at = registry.get_scheduled_time(job)
|
||||
|
||||
return jobs
|
||||
return get_rq_jobs_from_status(queue, status)
|
||||
|
||||
def get(self, request, queue_index, status):
|
||||
queue = get_queue_by_index(queue_index)
|
||||
@@ -370,22 +431,10 @@ class BackgroundTaskDeleteView(BaseRQView):
|
||||
form = ConfirmationForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
# all the RQ queues should use the same connection
|
||||
config = QUEUES_LIST[0]
|
||||
try:
|
||||
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
|
||||
except NoSuchJobError:
|
||||
raise Http404(_("Job {job_id} not found").format(job_id=job_id))
|
||||
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
|
||||
# Remove job id from queue and delete the actual job
|
||||
queue.connection.lrem(queue.key, 0, job.id)
|
||||
job.delete()
|
||||
messages.success(request, f'Deleted job {job_id}')
|
||||
delete_rq_job(job_id)
|
||||
messages.success(request, _('Job {id} has been deleted.').format(id=job_id))
|
||||
else:
|
||||
messages.error(request, f'Error deleting job: {form.errors[0]}')
|
||||
messages.error(request, _('Error deleting job {id}: {error}').format(id=job_id, error=form.errors[0]))
|
||||
|
||||
return redirect(reverse('core:background_queue_list'))
|
||||
|
||||
@@ -393,18 +442,8 @@ class BackgroundTaskDeleteView(BaseRQView):
|
||||
class BackgroundTaskRequeueView(BaseRQView):
|
||||
|
||||
def get(self, request, job_id):
|
||||
# all the RQ queues should use the same connection
|
||||
config = QUEUES_LIST[0]
|
||||
try:
|
||||
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
|
||||
except NoSuchJobError:
|
||||
raise Http404(_("Job {job_id} not found").format(job_id=job_id))
|
||||
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
|
||||
requeue_job(job_id, connection=queue.connection, serializer=queue.serializer)
|
||||
messages.success(request, f'You have successfully requeued: {job_id}')
|
||||
requeue_rq_job(job_id)
|
||||
messages.success(request, _('Job {id} has been re-enqueued.').format(id=job_id))
|
||||
return redirect(reverse('core:background_task', args=[job_id]))
|
||||
|
||||
|
||||
@@ -412,55 +451,19 @@ class BackgroundTaskEnqueueView(BaseRQView):
|
||||
|
||||
def get(self, request, job_id):
|
||||
# all the RQ queues should use the same connection
|
||||
config = QUEUES_LIST[0]
|
||||
try:
|
||||
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
|
||||
except NoSuchJobError:
|
||||
raise Http404(_("Job {job_id} not found").format(job_id=job_id))
|
||||
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
|
||||
try:
|
||||
# _enqueue_job is new in RQ 1.14, this is used to enqueue
|
||||
# job regardless of its dependencies
|
||||
queue._enqueue_job(job)
|
||||
except AttributeError:
|
||||
queue.enqueue_job(job)
|
||||
|
||||
# Remove job from correct registry if needed
|
||||
if job.get_status() == RQJobStatus.DEFERRED:
|
||||
registry = DeferredJobRegistry(queue.name, queue.connection)
|
||||
registry.remove(job)
|
||||
elif job.get_status() == RQJobStatus.FINISHED:
|
||||
registry = FinishedJobRegistry(queue.name, queue.connection)
|
||||
registry.remove(job)
|
||||
elif job.get_status() == RQJobStatus.SCHEDULED:
|
||||
registry = ScheduledJobRegistry(queue.name, queue.connection)
|
||||
registry.remove(job)
|
||||
|
||||
messages.success(request, f'You have successfully enqueued: {job_id}')
|
||||
enqueue_rq_job(job_id)
|
||||
messages.success(request, _('Job {id} has been enqueued.').format(id=job_id))
|
||||
return redirect(reverse('core:background_task', args=[job_id]))
|
||||
|
||||
|
||||
class BackgroundTaskStopView(BaseRQView):
|
||||
|
||||
def get(self, request, job_id):
|
||||
# all the RQ queues should use the same connection
|
||||
config = QUEUES_LIST[0]
|
||||
try:
|
||||
job = RQ_Job.fetch(job_id, connection=get_redis_connection(config['connection_config']),)
|
||||
except NoSuchJobError:
|
||||
raise Http404(_("Job {job_id} not found").format(job_id=job_id))
|
||||
|
||||
queue_index = QUEUES_MAP[job.origin]
|
||||
queue = get_queue_by_index(queue_index)
|
||||
|
||||
stopped, _ = stop_jobs(queue, job_id)
|
||||
if len(stopped) == 1:
|
||||
messages.success(request, f'You have successfully stopped {job_id}')
|
||||
stopped_jobs = stop_rq_job(job_id)
|
||||
if len(stopped_jobs) == 1:
|
||||
messages.success(request, _('Job {id} has been stopped.').format(id=job_id))
|
||||
else:
|
||||
messages.error(request, f'Failed to stop {job_id}')
|
||||
messages.error(request, _('Failed to stop job {id}').format(id=job_id))
|
||||
|
||||
return redirect(reverse('core:background_task', args=[job_id]))
|
||||
|
||||
@@ -515,7 +518,7 @@ class WorkerView(BaseRQView):
|
||||
|
||||
|
||||
#
|
||||
# Plugins
|
||||
# System
|
||||
#
|
||||
|
||||
class SystemView(UserPassesTestMixin, View):
|
||||
@@ -539,7 +542,7 @@ class SystemView(UserPassesTestMixin, View):
|
||||
except (ProgrammingError, IndexError):
|
||||
pass
|
||||
stats = {
|
||||
'netbox_version': settings.VERSION,
|
||||
'netbox_release': settings.RELEASE,
|
||||
'django_version': DJANGO_VERSION,
|
||||
'python_version': platform.python_version(),
|
||||
'postgresql_version': psql_version,
|
||||
@@ -548,39 +551,93 @@ class SystemView(UserPassesTestMixin, View):
|
||||
'rq_worker_count': Worker.count(get_connection('default')),
|
||||
}
|
||||
|
||||
# Plugins
|
||||
plugins = [
|
||||
# Look up app config by package name
|
||||
apps.get_app_config(plugin.rsplit('.', 1)[-1]) for plugin in settings.PLUGINS
|
||||
]
|
||||
|
||||
# Configuration
|
||||
try:
|
||||
config = ConfigRevision.objects.get(pk=cache.get('config_version'))
|
||||
except ConfigRevision.DoesNotExist:
|
||||
# Fall back to using the active config data if no record is found
|
||||
config = ConfigRevision(data=get_config().defaults)
|
||||
config = get_config()
|
||||
|
||||
# Raw data export
|
||||
if 'export' in request.GET:
|
||||
stats['netbox_release'] = stats['netbox_release'].asdict()
|
||||
params = [param.name for param in PARAMS]
|
||||
data = {
|
||||
**stats,
|
||||
'plugins': {
|
||||
plugin.name: plugin.version for plugin in plugins
|
||||
},
|
||||
'plugins': settings.PLUGINS,
|
||||
'config': {
|
||||
k: config.data[k] for k in sorted(config.data)
|
||||
k: getattr(config, k) for k in sorted(params)
|
||||
},
|
||||
}
|
||||
response = HttpResponse(json.dumps(data, indent=4), content_type='text/json')
|
||||
response = HttpResponse(json.dumps(data, cls=ConfigJSONEncoder, indent=4), content_type='text/json')
|
||||
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
|
||||
return response
|
||||
|
||||
plugins_table = tables.PluginTable(plugins, orderable=False)
|
||||
plugins_table.configure(request)
|
||||
# Serialize any CustomValidator classes
|
||||
if hasattr(config, 'CUSTOM_VALIDATORS') and config.CUSTOM_VALIDATORS:
|
||||
config.CUSTOM_VALIDATORS = json.dumps(config.CUSTOM_VALIDATORS, cls=ConfigJSONEncoder, indent=4)
|
||||
|
||||
return render(request, 'core/system.html', {
|
||||
'stats': stats,
|
||||
'plugins_table': plugins_table,
|
||||
'config': config,
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# Plugins
|
||||
#
|
||||
|
||||
class BasePluginView(UserPassesTestMixin, View):
|
||||
CACHE_KEY_CATALOG_ERROR = 'plugins-catalog-error'
|
||||
|
||||
def test_func(self):
|
||||
return self.request.user.is_staff
|
||||
|
||||
def get_cached_plugins(self, request):
|
||||
catalog_plugins = {}
|
||||
catalog_plugins_error = cache.get(self.CACHE_KEY_CATALOG_ERROR, default=False)
|
||||
if not catalog_plugins_error:
|
||||
catalog_plugins = get_catalog_plugins()
|
||||
if not catalog_plugins:
|
||||
# Cache for 5 minutes to avoid spamming connection
|
||||
cache.set(self.CACHE_KEY_CATALOG_ERROR, True, 300)
|
||||
messages.warning(request, _("Plugins catalog could not be loaded"))
|
||||
|
||||
return get_local_plugins(catalog_plugins)
|
||||
|
||||
|
||||
class PluginListView(BasePluginView):
|
||||
|
||||
def get(self, request):
|
||||
q = request.GET.get('q', None)
|
||||
|
||||
plugins = self.get_cached_plugins(request).values()
|
||||
if q:
|
||||
plugins = [obj for obj in plugins if q.casefold() in obj.title_short.casefold()]
|
||||
|
||||
table = CatalogPluginTable(plugins, user=request.user)
|
||||
table.configure(request)
|
||||
|
||||
# If this is an HTMX request, return only the rendered table HTML
|
||||
if htmx_partial(request):
|
||||
return render(request, 'htmx/table.html', {
|
||||
'table': table,
|
||||
})
|
||||
|
||||
return render(request, 'core/plugin_list.html', {
|
||||
'table': table,
|
||||
})
|
||||
|
||||
|
||||
class PluginView(BasePluginView):
|
||||
|
||||
def get(self, request, name):
|
||||
|
||||
plugins = self.get_cached_plugins(request)
|
||||
if name not in plugins:
|
||||
raise Http404(_("Plugin {name} not found").format(name=name))
|
||||
plugin = plugins[name]
|
||||
|
||||
table = PluginVersionTable(plugin.release_recent_history, user=request.user)
|
||||
table.configure(request)
|
||||
|
||||
return render(request, 'core/plugin.html', {
|
||||
'plugin': plugin,
|
||||
'table': table,
|
||||
})
|
||||
|
||||
@@ -1,501 +0,0 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim import models
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCableSerializer',
|
||||
'NestedConsolePortSerializer',
|
||||
'NestedConsolePortTemplateSerializer',
|
||||
'NestedConsoleServerPortSerializer',
|
||||
'NestedConsoleServerPortTemplateSerializer',
|
||||
'NestedDeviceBaySerializer',
|
||||
'NestedDeviceBayTemplateSerializer',
|
||||
'NestedDeviceRoleSerializer',
|
||||
'NestedDeviceSerializer',
|
||||
'NestedDeviceTypeSerializer',
|
||||
'NestedFrontPortSerializer',
|
||||
'NestedFrontPortTemplateSerializer',
|
||||
'NestedInterfaceSerializer',
|
||||
'NestedInterfaceTemplateSerializer',
|
||||
'NestedInventoryItemSerializer',
|
||||
'NestedInventoryItemRoleSerializer',
|
||||
'NestedInventoryItemTemplateSerializer',
|
||||
'NestedManufacturerSerializer',
|
||||
'NestedModuleBaySerializer',
|
||||
'NestedModuleBayTemplateSerializer',
|
||||
'NestedModuleSerializer',
|
||||
'NestedModuleTypeSerializer',
|
||||
'NestedPlatformSerializer',
|
||||
'NestedPowerFeedSerializer',
|
||||
'NestedPowerOutletSerializer',
|
||||
'NestedPowerOutletTemplateSerializer',
|
||||
'NestedPowerPanelSerializer',
|
||||
'NestedPowerPortSerializer',
|
||||
'NestedPowerPortTemplateSerializer',
|
||||
'NestedLocationSerializer',
|
||||
'NestedRackReservationSerializer',
|
||||
'NestedRackRoleSerializer',
|
||||
'NestedRackSerializer',
|
||||
'NestedRearPortSerializer',
|
||||
'NestedRearPortTemplateSerializer',
|
||||
'NestedRegionSerializer',
|
||||
'NestedSiteSerializer',
|
||||
'NestedSiteGroupSerializer',
|
||||
'NestedVirtualChassisSerializer',
|
||||
'NestedVirtualDeviceContextSerializer',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Regions/sites
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('site_count',),
|
||||
)
|
||||
class NestedRegionSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Region
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('site_count',),
|
||||
)
|
||||
class NestedSiteGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.SiteGroup
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
class NestedSiteSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Site
|
||||
fields = ['id', 'url', 'display', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('rack_count',),
|
||||
)
|
||||
class NestedLocationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Location
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('rack_count',),
|
||||
)
|
||||
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||
rack_count = RelatedObjectCountField('racks')
|
||||
|
||||
class Meta:
|
||||
model = models.RackRole
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('device_count',),
|
||||
)
|
||||
class NestedRackSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
|
||||
class Meta:
|
||||
model = models.Rack
|
||||
fields = ['id', 'url', 'display', 'name', 'device_count']
|
||||
|
||||
|
||||
class NestedRackReservationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
|
||||
user = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.RackReservation
|
||||
fields = ['id', 'url', 'display', 'user', 'units']
|
||||
|
||||
def get_user(self, obj):
|
||||
return obj.user.username
|
||||
|
||||
|
||||
#
|
||||
# Device/module types
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('devicetype_count',),
|
||||
)
|
||||
class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||
devicetype_count = RelatedObjectCountField('device_types')
|
||||
|
||||
class Meta:
|
||||
model = models.Manufacturer
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'devicetype_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('device_count',),
|
||||
)
|
||||
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||
device_count = RelatedObjectCountField('instances')
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceType
|
||||
fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'device_count']
|
||||
|
||||
|
||||
class NestedModuleTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleType
|
||||
fields = ['id', 'url', 'display', 'manufacturer', 'model']
|
||||
|
||||
|
||||
#
|
||||
# Component templates
|
||||
#
|
||||
|
||||
class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePortTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPortTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPortTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutletTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.InterfaceTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.RearPortTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.FrontPortTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedModuleBayTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleBayTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBayTemplate
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItemTemplate
|
||||
fields = ['id', 'url', 'display', 'name', '_depth']
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('device_count', 'virtualmachine_count'),
|
||||
)
|
||||
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceRole
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('device_count', 'virtualmachine_count'),
|
||||
)
|
||||
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||
|
||||
class Meta:
|
||||
model = models.Platform
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
|
||||
|
||||
class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Device
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleBay
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Module
|
||||
fields = ['id', 'url', 'display', 'serial']
|
||||
|
||||
|
||||
class NestedModuleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
module_bay = ModuleNestedModuleBaySerializer(read_only=True)
|
||||
module_type = NestedModuleTypeSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Module
|
||||
fields = ['id', 'url', 'display', 'device', 'module_bay', 'module_type']
|
||||
|
||||
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPort
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePort
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutlet
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPort
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Interface
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.RearPort
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.FrontPort
|
||||
fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedModuleBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||
installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleBay
|
||||
fields = ['id', 'url', 'display', 'installed_module', 'name']
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceBay
|
||||
fields = ['id', 'url', 'display', 'device', 'name']
|
||||
|
||||
|
||||
class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
_depth = serializers.IntegerField(source='level', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItem
|
||||
fields = ['id', 'url', 'display', 'device', 'name', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('inventoryitem_count',),
|
||||
)
|
||||
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
||||
inventoryitem_count = RelatedObjectCountField('inventory_items')
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItemRole
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'inventoryitem_count']
|
||||
|
||||
|
||||
#
|
||||
# Cables
|
||||
#
|
||||
|
||||
class NestedCableSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||
|
||||
class Meta:
|
||||
model = models.Cable
|
||||
fields = ['id', 'url', 'display', 'label']
|
||||
|
||||
|
||||
#
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('member_count',),
|
||||
)
|
||||
class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer()
|
||||
member_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualChassis
|
||||
fields = ['id', 'url', 'display', 'name', 'master', 'member_count']
|
||||
|
||||
|
||||
#
|
||||
# Power panels/feeds
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('powerfeed_count',),
|
||||
)
|
||||
class NestedPowerPanelSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||
powerfeed_count = RelatedObjectCountField('powerfeeds')
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPanel
|
||||
fields = ['id', 'url', 'display', 'name', 'powerfeed_count']
|
||||
|
||||
|
||||
class NestedPowerFeedSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PowerFeed
|
||||
fields = ['id', 'url', 'display', 'name', 'cable', '_occupied']
|
||||
|
||||
|
||||
class NestedVirtualDeviceContextSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
|
||||
class Meta:
|
||||
model = models.VirtualDeviceContext
|
||||
fields = ['id', 'url', 'display', 'name', 'identifier', 'device']
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user