mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-18 19:32:24 -06:00
Merge branch 'develop' into develop-2.1
This commit is contained in:
@@ -6,7 +6,7 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import ModelValidationMixin
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
@@ -45,7 +45,7 @@ class WritableProviderSerializer(CustomFieldModelSerializer):
|
||||
# Circuit types
|
||||
#
|
||||
|
||||
class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class CircuitTypeSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
@@ -111,7 +111,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
|
||||
@@ -170,6 +170,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
commit_rate = forms.IntegerField(required=False, min_value=0, label='Commit rate (Kbps)')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -13,22 +13,6 @@ from utilities.models import CreatedUpdatedModel
|
||||
from .constants import *
|
||||
|
||||
|
||||
def humanize_speed(speed):
|
||||
"""
|
||||
Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps')
|
||||
"""
|
||||
if speed >= 1000000000 and speed % 1000000000 == 0:
|
||||
return '{} Tbps'.format(speed / 1000000000)
|
||||
elif speed >= 1000000 and speed % 1000000 == 0:
|
||||
return '{} Gbps'.format(speed / 1000000)
|
||||
elif speed >= 1000 and speed % 1000 == 0:
|
||||
return '{} Mbps'.format(speed / 1000)
|
||||
elif speed >= 1000:
|
||||
return '{} Mbps'.format(float(speed) / 1000)
|
||||
else:
|
||||
return '{} Kbps'.format(speed)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Provider(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
@@ -139,10 +123,6 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
def termination_z(self):
|
||||
return self._get_termination('Z')
|
||||
|
||||
def commit_rate_human(self):
|
||||
return '' if not self.commit_rate else humanize_speed(self.commit_rate)
|
||||
commit_rate_human.admin_order_field = 'commit_rate'
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CircuitTermination(models.Model):
|
||||
@@ -173,11 +153,3 @@ class CircuitTermination(models.Model):
|
||||
return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side)
|
||||
except CircuitTermination.DoesNotExist:
|
||||
return None
|
||||
|
||||
def port_speed_human(self):
|
||||
return humanize_speed(self.port_speed)
|
||||
port_speed_human.admin_order_field = 'port_speed'
|
||||
|
||||
def upstream_speed_human(self):
|
||||
return '' if not self.upstream_speed else humanize_speed(self.upstream_speed)
|
||||
upstream_speed_human.admin_order_field = 'upstream_speed'
|
||||
|
||||
@@ -15,7 +15,7 @@ from dcim.models import (
|
||||
)
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
|
||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
@@ -38,7 +38,7 @@ class RegionSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'name', 'slug', 'parent']
|
||||
|
||||
|
||||
class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableRegionSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
@@ -100,7 +100,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableRackGroupSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
@@ -111,7 +111,7 @@ class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSeriali
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class RackRoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
@@ -216,7 +216,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
|
||||
|
||||
|
||||
class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableRackReservationSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
@@ -227,7 +227,7 @@ class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelS
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class ManufacturerSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
@@ -292,7 +292,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
@@ -311,7 +311,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
@@ -330,7 +330,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
@@ -349,7 +349,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
@@ -369,7 +369,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
|
||||
|
||||
|
||||
class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
@@ -388,7 +388,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
@@ -399,7 +399,7 @@ class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.Mode
|
||||
# Device roles
|
||||
#
|
||||
|
||||
class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class DeviceRoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
@@ -418,7 +418,7 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
|
||||
# Platforms
|
||||
#
|
||||
|
||||
class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class PlatformSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
@@ -473,14 +473,10 @@ class DeviceSerializer(CustomFieldModelSerializer):
|
||||
device_bay = obj.parent_bay
|
||||
except DeviceBay.DoesNotExist:
|
||||
return None
|
||||
return {
|
||||
'id': device_bay.device.pk,
|
||||
'name': device_bay.device.name,
|
||||
'device_bay': {
|
||||
'id': device_bay.pk,
|
||||
'name': device_bay.name,
|
||||
}
|
||||
}
|
||||
context = {'request': self.context['request']}
|
||||
data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
|
||||
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
|
||||
return data
|
||||
|
||||
|
||||
class WritableDeviceSerializer(CustomFieldModelSerializer):
|
||||
@@ -520,7 +516,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ['connected_console']
|
||||
|
||||
|
||||
class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableConsoleServerPortSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
@@ -540,7 +536,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
|
||||
|
||||
|
||||
class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableConsolePortSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
@@ -560,7 +556,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ['connected_port']
|
||||
|
||||
|
||||
class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritablePowerOutletSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
@@ -580,7 +576,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
|
||||
|
||||
|
||||
class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritablePowerPortSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
@@ -668,7 +664,7 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableInterfaceSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
@@ -690,7 +686,15 @@ class DeviceBaySerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device', 'name', 'installed_device']
|
||||
|
||||
|
||||
class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class NestedDeviceBaySerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class WritableDeviceBaySerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
@@ -713,7 +717,7 @@ class InventoryItemSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableInventoryItemSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
@@ -745,7 +749,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'connection_status']
|
||||
|
||||
|
||||
class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
|
||||
@@ -3,7 +3,6 @@ from collections import OrderedDict
|
||||
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
|
||||
|
||||
@@ -21,7 +20,7 @@ from dcim import filters
|
||||
from extras.api.serializers import RenderedGraphSerializer
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from utilities.api import ServiceUnavailable, WritableSerializerMixin
|
||||
from utilities.api import IsAuthenticatedOrLoginNotRequired, ServiceUnavailable, WritableSerializerMixin
|
||||
from .exceptions import MissingFilterException
|
||||
from . import serializers
|
||||
|
||||
@@ -272,15 +271,17 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
ip_address = str(device.primary_ip.address.ip)
|
||||
d = driver(
|
||||
hostname=ip_address,
|
||||
username=settings.NETBOX_USERNAME,
|
||||
password=settings.NETBOX_PASSWORD
|
||||
username=settings.NAPALM_USERNAME,
|
||||
password=settings.NAPALM_PASSWORD,
|
||||
timeout=settings.NAPALM_TIMEOUT,
|
||||
optional_args=settings.NAPALM_ARGS
|
||||
)
|
||||
try:
|
||||
d.open()
|
||||
for method in napalm_methods:
|
||||
response[method] = getattr(d, method)()
|
||||
except Exception as e:
|
||||
raise ServiceUnavailable("Error connecting to the device: {}".format(e))
|
||||
raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
|
||||
|
||||
d.close()
|
||||
return Response(response)
|
||||
@@ -385,7 +386,7 @@ class ConnectedDeviceViewSet(ViewSet):
|
||||
* `peer-device`: The name of the peer device
|
||||
* `peer-interface`: The name of the peer interface
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
|
||||
def get_view_name(self):
|
||||
return "Connected Device Locator"
|
||||
|
||||
@@ -58,6 +58,7 @@ IFACE_FF_1GE_FIXED = 1000
|
||||
IFACE_FF_1GE_GBIC = 1050
|
||||
IFACE_FF_1GE_SFP = 1100
|
||||
IFACE_FF_10GE_FIXED = 1150
|
||||
IFACE_FF_10GE_CX4 = 1170
|
||||
IFACE_FF_10GE_SFP_PLUS = 1200
|
||||
IFACE_FF_10GE_XFP = 1300
|
||||
IFACE_FF_10GE_XENPAK = 1310
|
||||
@@ -106,6 +107,7 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
|
||||
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
|
||||
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
|
||||
[IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_filters
|
||||
from netaddr import EUI
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
@@ -8,7 +9,7 @@ from django.db.models import Q
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||
@@ -113,6 +114,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
facility_id = NullableCharFieldFilter()
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -156,7 +158,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['facility_id', 'type', 'width', 'u_height', 'desc_units']
|
||||
fields = ['type', 'width', 'u_height', 'desc_units']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -271,6 +273,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
name='device_type_id',
|
||||
label='Device type (ID)',
|
||||
)
|
||||
|
||||
@@ -383,6 +386,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
name = NullableCharFieldFilter()
|
||||
asset_tag = NullableCharFieldFilter()
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
@@ -439,7 +444,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['name', 'serial', 'asset_tag']
|
||||
fields = ['serial']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -457,7 +462,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
return queryset.filter(interfaces__mac_address=value).distinct()
|
||||
mac = EUI(value.strip())
|
||||
return queryset.filter(interfaces__mac_address=mac).distinct()
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
@@ -569,7 +575,8 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
return queryset.filter(mac_address=value)
|
||||
mac = EUI(value.strip())
|
||||
return queryset.filter(mac_address=mac)
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
@@ -596,10 +603,11 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
asset_tag = NullableCharFieldFilter()
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
|
||||
fields = ['name', 'part_id', 'serial', 'discovered']
|
||||
|
||||
|
||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
|
||||
@@ -12,7 +12,7 @@ from ipam.models import IPAddress
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||
APISelect, ArrayFieldSelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField,
|
||||
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||
FilterTreeNodeMultipleChoiceField,
|
||||
@@ -28,12 +28,6 @@ from .models import (
|
||||
)
|
||||
|
||||
|
||||
FORM_STATUS_CHOICES = [
|
||||
['', '---------'],
|
||||
]
|
||||
|
||||
FORM_STATUS_CHOICES += STATUS_CHOICES
|
||||
|
||||
DEVICE_BY_PK_RE = '{\d+\}'
|
||||
|
||||
|
||||
@@ -728,13 +722,28 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
# Compile list of choices for primary IPv4 and IPv6 addresses
|
||||
for family in [4, 6]:
|
||||
ip_choices = []
|
||||
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
|
||||
ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
|
||||
.select_related('nat_inside__interface')
|
||||
ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
||||
self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
|
||||
ip_choices = [(None, '---------')]
|
||||
# Collect interface IPs
|
||||
interface_ips = IPAddress.objects.select_related('interface').filter(
|
||||
family=family, interface__device=self.instance
|
||||
)
|
||||
if interface_ips:
|
||||
ip_choices.append(
|
||||
('Interface IPs', [
|
||||
(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
|
||||
])
|
||||
)
|
||||
# Collect NAT IPs
|
||||
nat_ips = IPAddress.objects.select_related('nat_inside').filter(
|
||||
family=family, nat_inside__interface__device=self.instance
|
||||
)
|
||||
if nat_ips:
|
||||
ip_choices.append(
|
||||
('NAT IPs', [
|
||||
(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips
|
||||
])
|
||||
)
|
||||
self.fields['primary_ip{}'.format(family)].choices = ip_choices
|
||||
|
||||
# If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
|
||||
# can be flipped from one face to another.
|
||||
@@ -934,7 +943,7 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
|
||||
status = forms.ChoiceField(choices=add_blank_choice(STATUS_CHOICES), required=False, initial='')
|
||||
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
||||
|
||||
class Meta:
|
||||
|
||||
25
netbox/dcim/migrations/0042_interface_ff_10ge_cx4.py
Normal file
25
netbox/dcim/migrations/0042_interface_ff_10ge_cx4.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-29 21:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0041_napalm_integration'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||
),
|
||||
]
|
||||
70
netbox/dcim/migrations/0043_device_component_name_lengths.py
Normal file
70
netbox/dcim/migrations/0043_device_component_name_lengths.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-29 21:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0042_interface_ff_10ge_cx4'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='devicebaytemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlet',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerport',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='powerporttemplate',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -13,6 +13,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
@@ -379,6 +380,16 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
return list(reversed(available_units))
|
||||
|
||||
def get_reserved_units(self):
|
||||
"""
|
||||
Return a dictionary mapping all reserved units within the rack to their reservation.
|
||||
"""
|
||||
reserved_units = {}
|
||||
for r in self.reservations.all():
|
||||
for u in r.units:
|
||||
reserved_units[u] = r
|
||||
return reserved_units
|
||||
|
||||
def get_0u_devices(self):
|
||||
return self.devices.filter(position=0)
|
||||
|
||||
@@ -616,7 +627,7 @@ class ConsolePortTemplate(models.Model):
|
||||
A template for a ConsolePort to be created for a new Device.
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -632,7 +643,7 @@ class ConsoleServerPortTemplate(models.Model):
|
||||
A template for a ConsoleServerPort to be created for a new Device.
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -648,7 +659,7 @@ class PowerPortTemplate(models.Model):
|
||||
A template for a PowerPort to be created for a new Device.
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -664,7 +675,7 @@ class PowerOutletTemplate(models.Model):
|
||||
A template for a PowerOutlet to be created for a new Device.
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -684,15 +695,16 @@ class InterfaceQuerySet(models.QuerySet):
|
||||
To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
|
||||
slot, subslot, position, channel, and virtual circuit:
|
||||
|
||||
{type}{slot}/{subslot}/{position}:{channel}.{vc}
|
||||
{type}{slot}/{subslot}/{position}/{subposition}:{channel}.{vc}
|
||||
|
||||
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
|
||||
be parsed as follows:
|
||||
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet1/2/3
|
||||
would be parsed as follows:
|
||||
|
||||
name = 'GigabitEthernet'
|
||||
slot = None
|
||||
subslot = 0
|
||||
position = 1
|
||||
slot = 1
|
||||
subslot = 2
|
||||
position = 3
|
||||
subposition = 0
|
||||
channel = None
|
||||
vc = 0
|
||||
|
||||
@@ -701,17 +713,35 @@ class InterfaceQuerySet(models.QuerySet):
|
||||
"""
|
||||
sql_col = '{}.name'.format(self.model._meta.db_table)
|
||||
ordering = {
|
||||
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
|
||||
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
|
||||
IFACE_ORDERING_POSITION: (
|
||||
'_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_type', '_id', 'name',
|
||||
),
|
||||
IFACE_ORDERING_NAME: (
|
||||
'_type', '_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_id', 'name',
|
||||
),
|
||||
}[method]
|
||||
return self.extra(select={
|
||||
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
||||
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_channel': "COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col),
|
||||
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
|
||||
}).order_by(*ordering)
|
||||
|
||||
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
|
||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
|
||||
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)\/') AS integer)"
|
||||
SUBSLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/)([0-9]+)') AS integer)"
|
||||
POSITION_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer)"
|
||||
SUBPOSITION_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer)"
|
||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
|
||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
|
||||
|
||||
fields = {
|
||||
'_type': RawSQL(TYPE_RE.format(sql_col), []),
|
||||
'_id': RawSQL(ID_RE.format(sql_col), []),
|
||||
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
|
||||
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
|
||||
'_position': RawSQL(POSITION_RE.format(sql_col), []),
|
||||
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
|
||||
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
|
||||
'_vc': RawSQL(VC_RE.format(sql_col), []),
|
||||
}
|
||||
|
||||
return self.annotate(**fields).order_by(*ordering)
|
||||
|
||||
def connectable(self):
|
||||
"""
|
||||
@@ -727,7 +757,7 @@ class InterfaceTemplate(models.Model):
|
||||
A template for a physical data interface on a new Device.
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=64)
|
||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
||||
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
||||
|
||||
@@ -747,7 +777,7 @@ class DeviceBayTemplate(models.Model):
|
||||
A template for a DeviceBay to be created for a new parent Device.
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='device_bay_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
@@ -932,6 +962,30 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
except DeviceType.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Validate primary IPv4 address
|
||||
if self.primary_ip4 and (
|
||||
self.primary_ip4.interface is None or
|
||||
self.primary_ip4.interface.device != self
|
||||
) and (
|
||||
self.primary_ip4.nat_inside.interface is None or
|
||||
self.primary_ip4.nat_inside.interface.device != self
|
||||
):
|
||||
raise ValidationError({
|
||||
'primary_ip4': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip4),
|
||||
})
|
||||
|
||||
# Validate primary IPv6 address
|
||||
if self.primary_ip6 and (
|
||||
self.primary_ip6.interface is None or
|
||||
self.primary_ip6.interface.device != self
|
||||
) and (
|
||||
self.primary_ip6.nat_inside.interface is None or
|
||||
self.primary_ip6.nat_inside.interface.device != self
|
||||
):
|
||||
raise ValidationError({
|
||||
'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip6),
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
is_new = not bool(self.pk)
|
||||
@@ -1042,7 +1096,7 @@ class ConsolePort(models.Model):
|
||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=50)
|
||||
cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL,
|
||||
verbose_name='Console server port', blank=True, null=True)
|
||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
||||
@@ -1092,7 +1146,7 @@ class ConsoleServerPort(models.Model):
|
||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
objects = ConsoleServerPortManager()
|
||||
|
||||
@@ -1113,7 +1167,7 @@ class PowerPort(models.Model):
|
||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=50)
|
||||
power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL,
|
||||
blank=True, null=True)
|
||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
||||
@@ -1157,7 +1211,7 @@ class PowerOutlet(models.Model):
|
||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
objects = PowerOutletManager()
|
||||
|
||||
@@ -1187,7 +1241,7 @@ class Interface(models.Model):
|
||||
blank=True,
|
||||
verbose_name='Parent LAG'
|
||||
)
|
||||
name = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=64)
|
||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
||||
enabled = models.BooleanField(default=True)
|
||||
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
||||
|
||||
@@ -98,3 +98,112 @@ class RackTestCase(TestCase):
|
||||
face=None,
|
||||
)
|
||||
self.assertTrue(pdu)
|
||||
|
||||
|
||||
class InterfaceTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.site = Site.objects.create(
|
||||
name='TestSite1',
|
||||
slug='my-test-site'
|
||||
)
|
||||
self.rack = Rack.objects.create(
|
||||
name='TestRack1',
|
||||
facility_id='A101',
|
||||
site=self.site,
|
||||
u_height=42
|
||||
)
|
||||
self.manufacturer = Manufacturer.objects.create(
|
||||
name='Acme',
|
||||
slug='acme'
|
||||
)
|
||||
|
||||
self.device_type = DeviceType.objects.create(
|
||||
manufacturer=self.manufacturer,
|
||||
model='FrameForwarder 2048',
|
||||
slug='ff2048'
|
||||
)
|
||||
self.role = DeviceRole.objects.create(
|
||||
name='Switch',
|
||||
slug='switch',
|
||||
)
|
||||
|
||||
def test_interface_order_natural(self):
|
||||
device1 = Device.objects.create(
|
||||
name='TestSwitch1',
|
||||
device_type=self.device_type,
|
||||
device_role=self.role,
|
||||
site=self.site,
|
||||
rack=self.rack,
|
||||
position=10,
|
||||
face=RACK_FACE_REAR,
|
||||
)
|
||||
interface1 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/3/1'
|
||||
)
|
||||
interface2 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/5/1'
|
||||
)
|
||||
interface3 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/4'
|
||||
)
|
||||
interface4 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/3/2/4'
|
||||
)
|
||||
interface5 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Ethernet1/3/2/1'
|
||||
)
|
||||
interface6 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='Loopback1'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
list(Interface.objects.all().order_naturally()),
|
||||
[interface1, interface5, interface4, interface3, interface2, interface6]
|
||||
)
|
||||
|
||||
def test_interface_order_natural_subinterfaces(self):
|
||||
device1 = Device.objects.create(
|
||||
name='TestSwitch1',
|
||||
device_type=self.device_type,
|
||||
device_role=self.role,
|
||||
site=self.site,
|
||||
rack=self.rack,
|
||||
position=10,
|
||||
face=RACK_FACE_REAR,
|
||||
)
|
||||
interface1 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/3'
|
||||
)
|
||||
interface2 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/2.2'
|
||||
)
|
||||
interface3 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/0.120'
|
||||
)
|
||||
interface4 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/0'
|
||||
)
|
||||
interface5 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0/0/1.117'
|
||||
)
|
||||
interface6 = Interface.objects.create(
|
||||
device=device1,
|
||||
name='GigabitEthernet0'
|
||||
)
|
||||
self.assertEqual(
|
||||
list(Interface.objects.all().order_naturally()),
|
||||
[interface4, interface3, interface5, interface2, interface1, interface6]
|
||||
)
|
||||
|
||||
@@ -431,15 +431,10 @@ class RackView(View):
|
||||
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
|
||||
|
||||
reservations = RackReservation.objects.filter(rack=rack)
|
||||
reserved_units = {}
|
||||
for r in reservations:
|
||||
for u in r.units:
|
||||
reserved_units[u] = r
|
||||
|
||||
return render(request, 'dcim/rack.html', {
|
||||
'rack': rack,
|
||||
'reservations': reservations,
|
||||
'reserved_units': reserved_units,
|
||||
'nonracked_devices': nonracked_devices,
|
||||
'next_rack': next_rack,
|
||||
'prev_rack': prev_rack,
|
||||
@@ -969,7 +964,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
interfaces = Interface.objects.order_naturally(
|
||||
device.device_type.interface_ordering
|
||||
).filter(
|
||||
).connectable().filter(
|
||||
device=device
|
||||
).select_related(
|
||||
'connected_as_a', 'connected_as_b'
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.db import transaction
|
||||
from extras.models import (
|
||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
|
||||
)
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
@@ -28,34 +29,47 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
|
||||
for field_name, value in data.items():
|
||||
|
||||
cf = custom_fields[field_name]
|
||||
try:
|
||||
cf = custom_fields[field_name]
|
||||
except KeyError:
|
||||
raise ValidationError(
|
||||
"Invalid custom field for {} objects: {}".format(content_type, field_name)
|
||||
)
|
||||
|
||||
# Validate custom field name
|
||||
if field_name not in custom_fields:
|
||||
raise ValidationError("Invalid custom field for {} objects: {}".format(content_type, field_name))
|
||||
# Data validation
|
||||
if value not in [None, '']:
|
||||
|
||||
# Validate boolean
|
||||
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
raise ValidationError("Invalid value for boolean field {}: {}".format(field_name, value))
|
||||
# Validate boolean
|
||||
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
raise ValidationError(
|
||||
"Invalid value for boolean field {}: {}".format(field_name, value)
|
||||
)
|
||||
|
||||
# Validate date
|
||||
if cf.type == CF_TYPE_DATE:
|
||||
try:
|
||||
datetime.strptime(value, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise ValidationError("Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(
|
||||
field_name, value
|
||||
))
|
||||
# Validate date
|
||||
if cf.type == CF_TYPE_DATE:
|
||||
try:
|
||||
datetime.strptime(value, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
"Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(field_name, value)
|
||||
)
|
||||
|
||||
# Validate selected choice
|
||||
if cf.type == CF_TYPE_SELECT:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
raise ValidationError("{}: Choice selections must be passed as integers.".format(field_name))
|
||||
valid_choices = [c.pk for c in cf.choices.all()]
|
||||
if value not in valid_choices:
|
||||
raise ValidationError("Invalid choice for field {}: {}".format(field_name, value))
|
||||
# Validate selected choice
|
||||
if cf.type == CF_TYPE_SELECT:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
"{}: Choice selections must be passed as integers.".format(field_name)
|
||||
)
|
||||
valid_choices = [c.pk for c in cf.choices.all()]
|
||||
if value not in valid_choices:
|
||||
raise ValidationError(
|
||||
"Invalid choice for field {}: {}".format(field_name, value)
|
||||
)
|
||||
|
||||
elif cf.required:
|
||||
raise ValidationError("Required field {} cannot be empty.".format(field_name))
|
||||
|
||||
# Check for missing required fields
|
||||
missing_fields = []
|
||||
@@ -68,7 +82,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class CustomFieldModelSerializer(serializers.ModelSerializer):
|
||||
class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
"""
|
||||
Extends ModelSerializer to render any CustomFields and their values associated with an object.
|
||||
"""
|
||||
@@ -111,16 +125,6 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
|
||||
defaults={'serialized_value': custom_field.serialize_value(value)},
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Enforce model validation (see utilities.api.ModelValidationMixin)
|
||||
"""
|
||||
model_data = data.copy()
|
||||
model_data.pop('custom_fields', None)
|
||||
instance = self.Meta.model(**model_data)
|
||||
instance.clean()
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
|
||||
custom_fields = validated_data.pop('custom_fields', None)
|
||||
|
||||
@@ -10,7 +10,7 @@ from extras.models import (
|
||||
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
|
||||
)
|
||||
from users.api.serializers import NestedUserSerializer
|
||||
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ModelValidationMixin
|
||||
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
@@ -104,7 +104,7 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
|
||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||
|
||||
|
||||
class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
content_type = ContentTypeFieldSerializer()
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -37,19 +37,29 @@ class Command(BaseCommand):
|
||||
def get_namespace(self):
|
||||
namespace = {}
|
||||
|
||||
# Gather Django models from each app
|
||||
# Gather Django models and constants from each app
|
||||
for app in APPS:
|
||||
self.django_models[app] = []
|
||||
|
||||
# Models
|
||||
app_models = sys.modules['{}.models'.format(app)]
|
||||
for name in dir(app_models):
|
||||
model = getattr(app_models, name)
|
||||
try:
|
||||
if issubclass(model, Model):
|
||||
if issubclass(model, Model) and model._meta.app_label == app:
|
||||
namespace[name] = model
|
||||
self.django_models[app].append(name)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Constants
|
||||
try:
|
||||
app_constants = sys.modules['{}.constants'.format(app)]
|
||||
for name in dir(app_constants):
|
||||
namespace[name] = getattr(app_constants, name)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Load convenience commands
|
||||
namespace.update({
|
||||
'lsmodels': self._lsmodels,
|
||||
|
||||
@@ -13,8 +13,8 @@ from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Update inventory information for specified devices"
|
||||
username = settings.NETBOX_USERNAME
|
||||
password = settings.NETBOX_PASSWORD
|
||||
username = settings.NAPALM_USERNAME
|
||||
password = settings.NAPALM_PASSWORD
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('-u', '--username', dest='username', help="Specify the username to use")
|
||||
|
||||
@@ -285,7 +285,7 @@ class TopologyMap(models.Model):
|
||||
|
||||
# Add each device to the graph
|
||||
devices = []
|
||||
for query in device_set.split(';'): # Split regexes on semicolons
|
||||
for query in device_set.strip(';').split(';'): # Split regexes on semicolons
|
||||
devices += Device.objects.filter(name__regex=query).select_related('device_role')
|
||||
for d in devices:
|
||||
bg_color = '#{}'.format(d.device_role.color)
|
||||
|
||||
@@ -11,7 +11,7 @@ from ipam.models import (
|
||||
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
||||
)
|
||||
from tenancy.api.serializers import NestedTenantSerializer
|
||||
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
|
||||
from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
@@ -45,7 +45,7 @@ class WritableVRFSerializer(CustomFieldModelSerializer):
|
||||
# Roles
|
||||
#
|
||||
|
||||
class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class RoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
@@ -64,7 +64,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
|
||||
# RIRs
|
||||
#
|
||||
|
||||
class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class RIRSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
@@ -303,7 +303,7 @@ class ServiceSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
|
||||
|
||||
|
||||
# TODO: Figure out how to use ModelValidationMixin with ManyToManyFields. Calling clean() yields a ValueError.
|
||||
# TODO: Figure out how to use model validation with ManyToManyFields. Calling clean() yields a ValueError.
|
||||
class WritableServiceSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -98,7 +98,7 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
# Create the new IP address
|
||||
data = request.data.copy()
|
||||
data['address'] = '{}/{}'.format(ipaddress, prefix.prefix.prefixlen)
|
||||
data['vrf'] = prefix.vrf
|
||||
data['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
||||
serializer = serializers.WritableIPAddressSerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@@ -115,7 +115,11 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
limit = min(limit, settings.MAX_PAGE_SIZE)
|
||||
|
||||
# Calculate available IPs within the prefix
|
||||
ip_list = list(prefix.get_available_ips())[:limit]
|
||||
ip_list = []
|
||||
for index, ip in enumerate(prefix.get_available_ips(), start=1):
|
||||
ip_list.append(ip)
|
||||
if index == limit:
|
||||
break
|
||||
serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={
|
||||
'request': request,
|
||||
'prefix': prefix.prefix,
|
||||
|
||||
@@ -25,11 +25,8 @@ IP_FAMILY_CHOICES = [
|
||||
(6, 'IPv6'),
|
||||
]
|
||||
|
||||
PREFIX_MASK_LENGTH_CHOICES = [
|
||||
('', '---------'),
|
||||
] + [(i, i) for i in range(1, 128)]
|
||||
|
||||
IPADDRESS_MASK_LENGTH_CHOICES = PREFIX_MASK_LENGTH_CHOICES + [(128, 128)]
|
||||
PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 128)])
|
||||
IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 129)])
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.3 on 2017-08-03 19:37
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0017_ipaddress_roles'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='service',
|
||||
unique_together=set([]),
|
||||
),
|
||||
]
|
||||
@@ -512,6 +512,16 @@ class VLANGroup(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||
|
||||
def get_next_available_vid(self):
|
||||
"""
|
||||
Return the first available VLAN ID (1-4094) in the group.
|
||||
"""
|
||||
vids = [vlan['vid'] for vlan in self.vlans.order_by('vid').values('vid')]
|
||||
for i in range(1, 4095):
|
||||
if i not in vids:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
@@ -600,7 +610,6 @@ class Service(CreatedUpdatedModel):
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'protocol', 'port']
|
||||
unique_together = ['device', 'protocol', 'port']
|
||||
|
||||
def __str__(self):
|
||||
return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
|
||||
|
||||
@@ -34,7 +34,7 @@ RIR_ACTIONS = """
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% if record.pk %}{% utilization_graph value %}{% else %}—{% endif %}
|
||||
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}—{% endif %}
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
@@ -45,9 +45,9 @@ ROLE_ACTIONS = """
|
||||
|
||||
PREFIX_LINK = """
|
||||
{% if record.has_children %}
|
||||
<span style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
|
||||
<span class="text-nowrap" style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
|
||||
{% else %}
|
||||
<span style="padding-left: {{ record.depth }}9px">
|
||||
<span class="text-nowrap" style="padding-left: {{ record.depth }}9px">
|
||||
{% endif %}
|
||||
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if parent.vrf %}&vrf={{ parent.vrf.pk }}{% endif %}{% if parent.site %}&site={{ parent.site.pk }}{% endif %}{% endif %}">{{ record.prefix }}</a>
|
||||
</span>
|
||||
@@ -80,7 +80,6 @@ IPADDRESS_LINK = """
|
||||
IPADDRESS_DEVICE = """
|
||||
{% if record.interface %}
|
||||
<a href="{{ record.interface.device.get_absolute_url }}">{{ record.interface.device }}</a>
|
||||
({{ record.interface.name }})
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
@@ -121,6 +120,13 @@ VLAN_ROLE_LINK = """
|
||||
"""
|
||||
|
||||
VLANGROUP_ACTIONS = """
|
||||
{% with next_vid=record.get_next_available_vid %}
|
||||
{% if next_vid and perms.ipam.add_vlan %}
|
||||
<a href="{% url 'ipam:vlan_add' %}?site={{ record.site_id }}&group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if perms.ipam.change_vlangroup %}
|
||||
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
@@ -204,10 +210,10 @@ class AggregateTable(BaseTable):
|
||||
|
||||
class AggregateDetailTable(AggregateTable):
|
||||
child_count = tables.Column(verbose_name='Prefixes')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(AggregateTable.Meta):
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description')
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
|
||||
|
||||
|
||||
#
|
||||
@@ -250,10 +256,10 @@ class PrefixTable(BaseTable):
|
||||
|
||||
|
||||
class PrefixDetailTable(PrefixTable):
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False)
|
||||
|
||||
class Meta(PrefixTable.Meta):
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -16,6 +16,7 @@ from utilities.views import (
|
||||
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
from . import filters, forms, tables
|
||||
from .constants import IPADDRESS_ROLE_ANYCAST
|
||||
from .models import (
|
||||
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
|
||||
Service, VLAN, VLANGroup, VRF,
|
||||
@@ -285,11 +286,12 @@ class AggregateListView(ObjectListView):
|
||||
ipv4_total = 0
|
||||
ipv6_total = 0
|
||||
|
||||
for a in self.queryset:
|
||||
if a.prefix.version == 4:
|
||||
ipv4_total += a.prefix.size
|
||||
elif a.prefix.version == 6:
|
||||
ipv6_total += a.prefix.size / 2 ** 64
|
||||
for aggregate in self.queryset:
|
||||
if aggregate.prefix.version == 6:
|
||||
# Report equivalent /64s for IPv6 to keep things sane
|
||||
ipv6_total += int(aggregate.prefix.size / 2 ** 64)
|
||||
else:
|
||||
ipv4_total += aggregate.prefix.size
|
||||
|
||||
return {
|
||||
'ipv4_total': ipv4_total,
|
||||
@@ -313,7 +315,7 @@ class AggregateView(View):
|
||||
)
|
||||
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
|
||||
|
||||
prefix_table = tables.PrefixTable(child_prefixes)
|
||||
prefix_table = tables.PrefixDetailTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
prefix_table.base_columns['pk'].visible = True
|
||||
|
||||
@@ -472,11 +474,11 @@ class PrefixView(View):
|
||||
child_prefixes = Prefix.objects.filter(
|
||||
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
|
||||
).select_related(
|
||||
'site', 'role'
|
||||
'site', 'vlan', 'role',
|
||||
).annotate_depth(limit=0)
|
||||
if child_prefixes:
|
||||
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
||||
child_prefix_table = tables.PrefixTable(child_prefixes)
|
||||
child_prefix_table = tables.PrefixDetailTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
child_prefix_table.base_columns['pk'].visible = True
|
||||
|
||||
@@ -624,6 +626,9 @@ class IPAddressView(View):
|
||||
).select_related(
|
||||
'interface__device', 'nat_inside'
|
||||
)
|
||||
# Exclude anycast IPs if this IP is anycast
|
||||
if ipaddress.role == IPADDRESS_ROLE_ANYCAST:
|
||||
duplicate_ips = duplicate_ips.exclude(role=IPADDRESS_ROLE_ANYCAST)
|
||||
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
|
||||
|
||||
# Related IP table
|
||||
|
||||
@@ -60,8 +60,8 @@ BASE_PATH = os.environ.get('BASE_PATH', '')
|
||||
MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False)
|
||||
|
||||
# Credentials that NetBox will use to access live devices.
|
||||
NETBOX_USERNAME = os.environ.get('NETBOX_USERNAME', '')
|
||||
NETBOX_PASSWORD = os.environ.get('NETBOX_PASSWORD', '')
|
||||
NAPALM_USERNAME = os.environ.get('NAPALM_USERNAME', '')
|
||||
NAPALM_PASSWORD = os.environ.get('NAPALM_PASSWORD', '')
|
||||
|
||||
# Determine how many objects to display per page within a list. (Default: 50)
|
||||
PAGINATE_COUNT = os.environ.get('PAGINATE_COUNT', 50)
|
||||
|
||||
@@ -38,11 +38,14 @@ ADMINS = [
|
||||
# ['John Doe', 'jdoe@example.com'],
|
||||
]
|
||||
|
||||
# Optionally display a persistent banner at the top and/or bottom of every page. To display the same content in both
|
||||
# banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
||||
# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same
|
||||
# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
|
||||
BANNER_TOP = ''
|
||||
BANNER_BOTTOM = ''
|
||||
|
||||
# Text to include on the login page above the login form. HTML is allowed.
|
||||
BANNER_LOGIN = ''
|
||||
|
||||
# Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
|
||||
# BASE_PATH = 'netbox/'
|
||||
BASE_PATH = ''
|
||||
@@ -93,9 +96,20 @@ MAINTENANCE_MODE = False
|
||||
# all objects by specifying "?limit=0".
|
||||
MAX_PAGE_SIZE = 1000
|
||||
|
||||
# Credentials that NetBox will use to access live devices (future use).
|
||||
NETBOX_USERNAME = ''
|
||||
NETBOX_PASSWORD = ''
|
||||
# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that
|
||||
# the default value of this setting is derived from the installed location.
|
||||
# MEDIA_ROOT = '/opt/netbox/netbox/media'
|
||||
|
||||
# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
|
||||
NAPALM_USERNAME = ''
|
||||
NAPALM_PASSWORD = ''
|
||||
|
||||
# NAPALM timeout (in seconds). (Default: 30)
|
||||
NAPALM_TIMEOUT = 30
|
||||
|
||||
# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
|
||||
# be provided as a dictionary.
|
||||
NAPALM_ARGS = {}
|
||||
|
||||
# Determine how many objects to display per page within a list. (Default: 50)
|
||||
PAGINATE_COUNT = 50
|
||||
|
||||
@@ -35,7 +35,7 @@ OBJ_TYPE_CHOICES = (
|
||||
|
||||
class SearchForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
label='Query', widget=forms.TextInput(attrs={'style': 'width: 350px'})
|
||||
label='Search', widget=forms.TextInput(attrs={'style': 'width: 350px'})
|
||||
)
|
||||
obj_type = forms.ChoiceField(
|
||||
choices=OBJ_TYPE_CHOICES, required=False, label='Type'
|
||||
|
||||
@@ -13,7 +13,9 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.1.0-dev'
|
||||
VERSION = '2.1.6-dev'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Import required configuration parameters
|
||||
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
|
||||
@@ -27,8 +29,9 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
|
||||
# Import optional configuration parameters
|
||||
ADMINS = getattr(configuration, 'ADMINS', [])
|
||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', '')
|
||||
BANNER_LOGIN = getattr(configuration, 'BANNER_LOGIN', '')
|
||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', '')
|
||||
BASE_PATH = getattr(configuration, 'BASE_PATH', '')
|
||||
if BASE_PATH:
|
||||
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
|
||||
@@ -44,10 +47,15 @@ LOGGING = getattr(configuration, 'LOGGING', {})
|
||||
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
|
||||
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
|
||||
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
|
||||
MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media')).rstrip('/')
|
||||
NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
|
||||
NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
|
||||
NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30)
|
||||
NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
|
||||
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '') # Deprecated
|
||||
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '') # Deprecated
|
||||
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
|
||||
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
||||
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
|
||||
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
|
||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||
@@ -56,6 +64,19 @@ TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
||||
|
||||
# Check for deprecated configuration parameters
|
||||
config_logger = logging.getLogger('configuration')
|
||||
config_logger.addHandler(logging.StreamHandler())
|
||||
config_logger.setLevel(logging.WARNING)
|
||||
if NETBOX_USERNAME:
|
||||
config_logger.warning('NETBOX_USERNAME is deprecated and will be removed in v2.2. Please use NAPALM_USERNAME instead.')
|
||||
if not NAPALM_USERNAME:
|
||||
NAPALM_USERNAME = NETBOX_USERNAME
|
||||
if NETBOX_PASSWORD:
|
||||
config_logger.warning('NETBOX_PASSWORD is deprecated and will be removed in v2.2. Please use NAPALM_PASSWORD instead.')
|
||||
if not NAPALM_PASSWORD:
|
||||
NAPALM_PASSWORD = NETBOX_PASSWORD
|
||||
|
||||
# Attempt to import LDAP configuration if it has been defined
|
||||
LDAP_IGNORE_CERT_ERRORS = False
|
||||
try:
|
||||
@@ -78,17 +99,15 @@ if LDAP_CONFIGURED:
|
||||
if LDAP_IGNORE_CERT_ERRORS:
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||
# Enable logging for django_auth_ldap
|
||||
logger = logging.getLogger('django_auth_ldap')
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
logger.setLevel(logging.DEBUG)
|
||||
ldap_logger = logging.getLogger('django_auth_ldap')
|
||||
ldap_logger.addHandler(logging.StreamHandler())
|
||||
ldap_logger.setLevel(logging.DEBUG)
|
||||
except ImportError:
|
||||
raise ImproperlyConfigured(
|
||||
"LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove "
|
||||
"netbox/ldap_config.py to disable LDAP."
|
||||
)
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Database
|
||||
configuration.DATABASE.update({'ENGINE': 'django.db.backends.postgresql'})
|
||||
DATABASES = {
|
||||
@@ -184,7 +203,6 @@ STATICFILES_DIRS = (
|
||||
)
|
||||
|
||||
# Media
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
MEDIA_URL = '/{}media/'.format(BASE_PATH)
|
||||
|
||||
# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
|
||||
@@ -204,6 +222,7 @@ SECRETS_MIN_PUBKEY_SIZE = 2048
|
||||
# Django REST framework (API)
|
||||
REST_FRAMEWORK_VERSION = VERSION[0:3] # Use major.minor as API version
|
||||
REST_FRAMEWORK = {
|
||||
'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'utilities.api.TokenAuthentication',
|
||||
@@ -215,10 +234,14 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'utilities.api.TokenPermissions',
|
||||
),
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'utilities.api.FormlessBrowsableAPIRenderer',
|
||||
),
|
||||
'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
|
||||
'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION],
|
||||
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
|
||||
'PAGE_SIZE': PAGINATE_COUNT,
|
||||
'VIEW_NAME_FUNCTION': 'utilities.api.get_view_name',
|
||||
}
|
||||
|
||||
# Django debug toolbar
|
||||
|
||||
@@ -81,6 +81,11 @@ footer p {
|
||||
}
|
||||
}
|
||||
|
||||
/* Navigation menu */
|
||||
li.subnav > a {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
label {
|
||||
font-weight: normal;
|
||||
@@ -324,13 +329,14 @@ li.occupied + li.available {
|
||||
}
|
||||
|
||||
/* Devices */
|
||||
table.component-list tr.ipaddress td {
|
||||
background-color: #eeffff;
|
||||
padding-bottom: 4px;
|
||||
padding-top: 4px;
|
||||
table.component-list td.subtable {
|
||||
padding: 0;
|
||||
padding-left: 16px;
|
||||
}
|
||||
table.component-list tr.ipaddress:hover td {
|
||||
background-color: #e6f7f7;
|
||||
table.component-list td.subtable td {
|
||||
border: none;
|
||||
padding-bottom: 6px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
/* AJAX loader */
|
||||
|
||||
@@ -43,7 +43,7 @@ $(document).ready(function() {
|
||||
success: function (response, status) {
|
||||
if (response.plaintext) {
|
||||
console.log("Secret retrieved successfully");
|
||||
$('#secret_' + secret_id).html(response.plaintext);
|
||||
$('#secret_' + secret_id).text(response.plaintext);
|
||||
$('button.unlock-secret[secret-id=' + secret_id + ']').hide();
|
||||
$('button.lock-secret[secret-id=' + secret_id + ']').show();
|
||||
} else {
|
||||
|
||||
@@ -5,14 +5,14 @@ from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from dcim.api.serializers import NestedDeviceSerializer
|
||||
from secrets.models import Secret, SecretRole
|
||||
from utilities.api import ModelValidationMixin
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
# SecretRoles
|
||||
#
|
||||
|
||||
class SecretRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class SecretRoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
|
||||
@@ -31,201 +31,199 @@
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/sites/,/dcim/regions/,/tenancy/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li>
|
||||
<li><a href="{% url 'dcim:site_list' %}"><strong>Sites</strong></a></li>
|
||||
{% if perms.dcim.add_site %}
|
||||
<li><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Site</a></li>
|
||||
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus"></i> Add a Site</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download"></i> Import Sites</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'dcim:region_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Regions</a></li>
|
||||
<li><a href="{% url 'dcim:region_list' %}"><strong>Regions</strong></a></li>
|
||||
{% if perms.dcim.add_region %}
|
||||
<li><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Region</a></li>
|
||||
<li><a href="{% url 'dcim:region_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Regions</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus"></i> Add a Region</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:region_import' %}"><i class="fa fa-download"></i> Import Regions</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li>
|
||||
<li><a href="{% url 'tenancy:tenant_list' %}"><strong>Tenants</strong></a></li>
|
||||
{% if perms.tenancy.add_tenant %}
|
||||
<li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>
|
||||
<li><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Tenants</a></li>
|
||||
<li class="subnav"><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus"></i> Add a Tenant</a></li>
|
||||
<li class="subnav"><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download"></i> Import Tenants</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'tenancy:tenantgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenant Groups</a></li>
|
||||
<li><a href="{% url 'tenancy:tenantgroup_list' %}"><strong>Tenant Groups</strong></a></li>
|
||||
{% if perms.tenancy.add_tenantgroup %}
|
||||
<li><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant Group</a></li>
|
||||
<li class="subnav"><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus"></i> Add a Tenant Group</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/rack' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
|
||||
<li><a href="{% url 'dcim:rack_elevation_list' %}"><i class="fa fa-bars" aria-hidden="true"></i> Rack Elevations</a></li>
|
||||
<li><a href="{% url 'dcim:rack_list' %}"><strong>Racks</strong></a></li>
|
||||
{% if perms.dcim.add_rack %}
|
||||
<li><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack</a></li>
|
||||
<li><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Racks</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus"></i> Add a Rack</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download"></i> Import Racks</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:rack_elevation_list' %}"><strong>Rack Elevations</strong></a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Groups</a></li>
|
||||
<li><a href="{% url 'dcim:rackgroup_list' %}"><strong>Rack Groups</strong></a></li>
|
||||
{% if perms.dcim.add_rackgroup %}
|
||||
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
|
||||
<li><a href="{% url 'dcim:rackgroup_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Rack Groups</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus"></i> Add a Rack Group</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:rackgroup_import' %}"><i class="fa fa-download"></i> Import Rack Groups</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'dcim:rackrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Roles</a></li>
|
||||
<li><a href="{% url 'dcim:rackrole_list' %}"><strong>Rack Roles</strong></a></li>
|
||||
{% if perms.dcim.add_rackrole %}
|
||||
<li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus"></i> Add a Rack Role</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'dcim:rackreservation_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Reservations</a></li>
|
||||
<li><a href="{% url 'dcim:rackreservation_list' %}"><strong>Rack Reservations</strong></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
|
||||
<li><a href="{% url 'dcim:device_list' %}"><strong>Devices</strong></a></li>
|
||||
{% if perms.dcim.add_device %}
|
||||
<li><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device</a></li>
|
||||
<li><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Devices</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus"></i> Add a Device</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download"></i> Import Devices</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_device or perms.ipam.add_devicetype %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:devicetype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Types</a></li>
|
||||
<li><a href="{% url 'dcim:devicetype_list' %}"><strong>Device Types</strong></a></li>
|
||||
{% if perms.dcim.add_devicetype %}
|
||||
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Type</a></li>
|
||||
<li><a href="{% url 'dcim:devicetype_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Device Types</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus"></i> Add a Device Type</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:devicetype_import' %}"><i class="fa fa-download"></i> Import Device Types</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Roles</a></li>
|
||||
<li><a href="{% url 'dcim:devicerole_list' %}"><strong>Device Roles</strong></a></li>
|
||||
{% if perms.dcim.add_devicerole %}
|
||||
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Role</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus"></i> Add a Device Role</a></li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_devicerole or perms.dcim.add_manufacturer %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:manufacturer_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Manufacturers</a></li>
|
||||
<li><a href="{% url 'dcim:manufacturer_list' %}"><strong>Manufacturers</strong></a></li>
|
||||
{% if perms.dcim.add_manufacturer %}
|
||||
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
|
||||
<li><a href="{% url 'dcim:manufacturer_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Manufacturers</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus"></i> Add a Manufacturer</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:manufacturer_import' %}"><i class="fa fa-download"></i> Import Manufacturers</a></li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_manufacturer or perms.dcim.add_platform %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:platform_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Platforms</a></li>
|
||||
<li><a href="{% url 'dcim:platform_list' %}"><strong>Platforms</strong></a></li>
|
||||
{% if perms.dcim.add_platform %}
|
||||
<li><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Platform</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus"></i> Add a Platform</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/console-connections/,/dcim/power-connections/,/dcim/interface-connections/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li>
|
||||
<li><a href="{% url 'dcim:console_connections_list' %}"><strong>Console Connections</strong></a></li>
|
||||
{% if perms.dcim.change_consoleport %}
|
||||
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Console Connections</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download"></i> Import Console Connections</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.change_consoleport or perms.ipam.change_powerport %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:power_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Power Connections</a></li>
|
||||
<li><a href="{% url 'dcim:power_connections_list' %}"><strong>Power Connections</strong></a></li>
|
||||
{% if perms.dcim.change_powerport %}
|
||||
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Power Connections</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download"></i> Import Power Connections</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.change_powerport or perms.ipam.add_interfaceconnection %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:interface_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Interface Connections</a></li>
|
||||
<li><a href="{% url 'dcim:interface_connections_list' %}"><strong>Interface Connections</strong></a></li>
|
||||
{% if perms.dcim.add_interfaceconnection %}
|
||||
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Interface Connections</a></li>
|
||||
<li class="subnav"><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download"></i> Import Interface Connections</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/ipam/' and not request.path|contains:'/ipam/vlan' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li>
|
||||
<li><a href="{% url 'ipam:ipaddress_list' %}"><strong>IP Addresses</strong></a></li>
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<li><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an IP</a></li>
|
||||
<li><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import IPs</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus"></i> Add an IP</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download"></i> Import IPs</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_ipaddress or perms.ipam.add_prefix %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'ipam:prefix_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefixes</a></li>
|
||||
<li><a href="{% url 'ipam:prefix_list' %}"><strong>Prefixes</strong></a></li>
|
||||
{% if perms.ipam.add_prefix %}
|
||||
<li><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Prefix</a></li>
|
||||
<li><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Prefixes</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus"></i> Add a Prefix</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download"></i> Import Prefixes</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_prefix or perms.ipam.add_aggregate %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'ipam:aggregate_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Aggregates</a></li>
|
||||
<li><a href="{% url 'ipam:aggregate_list' %}"><strong>Aggregates</strong></a></li>
|
||||
{% if perms.ipam.add_aggregate %}
|
||||
<li><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
|
||||
<li><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Aggregates</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus"></i> Add an Aggregate</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download"></i> Import Aggregates</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_aggregate or perms.ipam.add_vrf %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'ipam:vrf_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VRFs</a></li>
|
||||
<li><a href="{% url 'ipam:vrf_list' %}"><strong>VRFs</strong></a></li>
|
||||
{% if perms.ipam.add_vrf %}
|
||||
<li><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VRF</a></li>
|
||||
<li><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VRFs</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus"></i> Add a VRF</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download"></i> Import VRFs</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'ipam:rir_list' %}"><i class="fa fa-search" aria-hidden="true"></i> RIRs</a></li>
|
||||
<li><a href="{% url 'ipam:rir_list' %}"><strong>RIRs</strong></a></li>
|
||||
{% if perms.ipam.add_rir %}
|
||||
<li><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a RIR</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus"></i> Add a RIR</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_rir or perms.ipam.add_role %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix Roles</a></li>
|
||||
<li><a href="{% url 'ipam:role_list' %}"><strong>Prefix Roles</strong></a></li>
|
||||
{% if perms.ipam.add_role %}
|
||||
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus"></i> Add a Role</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/ipam/vlan' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
|
||||
<li><a href="{% url 'ipam:vlan_list' %}"><strong>VLANs</strong></a></li>
|
||||
{% if perms.ipam.add_vlan %}
|
||||
<li><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN</a></li>
|
||||
<li><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VLANs</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus"></i> Add a VLAN</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download"></i> Import VLANs</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Groups</a></li>
|
||||
<li><a href="{% url 'ipam:vlangroup_list' %}"><strong>VLAN Groups</strong></a></li>
|
||||
{% if perms.ipam.add_vlangroup %}
|
||||
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus"></i> Add a VLAN Group</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Roles</a></li>
|
||||
<li><a href="{% url 'ipam:role_list' %}"><strong>VLAN Roles</strong></a></li>
|
||||
{% if perms.ipam.add_role %}
|
||||
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
|
||||
<li class="subnav"><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus"></i> Add a Role</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/circuits/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li>
|
||||
{% if perms.circuits.add_provider %}
|
||||
<li><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Provider</a></li>
|
||||
<li><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Providers</a></li>
|
||||
{% endif %}
|
||||
{% if perms.circuits.add_circuit or perms.circuits.add_provider %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'circuits:circuit_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuits</a></li>
|
||||
<li><a href="{% url 'circuits:circuit_list' %}"><strong>Circuits</strong></a></li>
|
||||
{% if perms.circuits.add_circuit %}
|
||||
<li><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit</a></li>
|
||||
<li><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Circuits</a></li>
|
||||
<li class="subnav"><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus"></i> Add a Circuit</a></li>
|
||||
<li class="subnav"><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download"></i> Import Circuits</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuit Types</a></li>
|
||||
<li><a href="{% url 'circuits:provider_list' %}"><strong>Providers</strong></a></li>
|
||||
{% if perms.circuits.add_provider %}
|
||||
<li class="subnav"><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus"></i> Add a Provider</a></li>
|
||||
<li class="subnav"><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download"></i> Import Providers</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'circuits:circuittype_list' %}"><strong>Circuit Types</strong></a></li>
|
||||
{% if perms.circuits.add_circuittype %}
|
||||
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
|
||||
<li class="subnav"><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus"></i> Add a Circuit Type</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
@@ -233,14 +231,14 @@
|
||||
<li class="dropdown{% if request.path|contains:'/secrets/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li>
|
||||
<li><a href="{% url 'secrets:secret_list' %}"><strong>Secrets</strong></a></li>
|
||||
{% if perms.secrets.add_secret %}
|
||||
<li><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Secrets</a></li>
|
||||
<li class="subnav"><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download"></i> Import Secrets</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secret Roles</a></li>
|
||||
<li><a href="{% url 'secrets:secretrole_list' %}"><strong>Secret Roles</strong></a></li>
|
||||
{% if perms.secrets.add_secretrole %}
|
||||
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
|
||||
<li class="subnav"><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus"></i> Add a Secret Role</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
@@ -254,16 +252,16 @@
|
||||
{{ request.user|truncatechars:"30" }} <span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li>
|
||||
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user"></i> Profile</a></li>
|
||||
{% if request.user.is_staff %}
|
||||
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs" aria-hidden="true"></i> Admin</a></li>
|
||||
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs"></i> Admin</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> Log out</a></li>
|
||||
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out"></i> Log out</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
|
||||
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in"></i> Log in</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" id="navbar_search" role="search">
|
||||
@@ -271,7 +269,7 @@
|
||||
<input type="text" name="q" class="form-control" placeholder="Search">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-search" aria-hidden="true"></i>
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -294,7 +292,7 @@
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissable" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span>×</span>
|
||||
</button>
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
@@ -39,7 +37,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
|
||||
<h1>{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=circuit %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@@ -90,7 +88,7 @@
|
||||
<td>Commit Rate</td>
|
||||
<td>
|
||||
{% if circuit.commit_rate %}
|
||||
{{ circuit.commit_rate_human }}
|
||||
{{ circuit.commit_rate|humanize_speed }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -9,7 +9,16 @@
|
||||
{% render_field form.cid %}
|
||||
{% render_field form.type %}
|
||||
{% render_field form.install_date %}
|
||||
{% render_field form.commit_rate %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_commit_rate">{{ form.commit_rate.label }}</label>
|
||||
<div class="col-md-9">
|
||||
<div class="input-group">
|
||||
{{ form.commit_rate }}
|
||||
{% include 'circuits/inc/speed_widget.html' with target_field='commit_rate' %}
|
||||
</div>
|
||||
<span class="help-block">{{ form.commit_rate.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,3 +44,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
$("a.set_speed").click(function(e) {
|
||||
e.preventDefault();
|
||||
$("#id_" + $(this).attr("target")).val($(this).attr("data"));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Circuits{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.circuits.add_circuit %}
|
||||
@@ -17,7 +15,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='circuits' %}
|
||||
</div>
|
||||
<h1>Circuits</h1>
|
||||
<h1>{% block title %}Circuits{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
{% load staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}
|
||||
Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
@@ -14,7 +10,7 @@
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h3>Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}</h3>
|
||||
<h3>{% block title %}Circuit {{ obj.circuit }} - {{ form.term_side.value }} Side{% endblock %}</h3>
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
@@ -53,8 +49,26 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Termination Details</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.port_speed %}
|
||||
{% render_field form.upstream_speed %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required" for="id_port_speed">{{ form.port_speed.label }}</label>
|
||||
<div class="col-md-9">
|
||||
<div class="input-group">
|
||||
{{ form.port_speed }}
|
||||
{% include 'circuits/inc/speed_widget.html' with target_field='port_speed' %}
|
||||
</div>
|
||||
<span class="help-block">{{ form.port_speed.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_upstream_speed">{{ form.upstream_speed.label }}</label>
|
||||
<div class="col-md-9">
|
||||
<div class="input-group">
|
||||
{{ form.upstream_speed }}
|
||||
{% include 'circuits/inc/speed_widget.html' with target_field='upstream_speed' %}
|
||||
</div>
|
||||
<span class="help-block">{{ form.upstream_speed.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.xconnect_id %}
|
||||
{% render_field form.pp_info %}
|
||||
</div>
|
||||
@@ -76,4 +90,10 @@
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/livesearch.js' %}?v{{ settings.VERSION }}"></script>
|
||||
<script type="text/javascript">
|
||||
$("a.set_speed").click(function(e) {
|
||||
e.preventDefault();
|
||||
$("#id_" + $(this).attr("target")).val($(this).attr("data"));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Circuit Types{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.circuits.add_circuittype %}
|
||||
@@ -12,7 +10,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>Circuit Types</h1>
|
||||
<h1>{% block title %}Circuit Types{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='circuits:circuittype_bulk_delete' %}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-right">
|
||||
@@ -49,10 +51,10 @@
|
||||
<td>Speed</td>
|
||||
<td>
|
||||
{% if termination.upstream_speed %}
|
||||
<i class="fa fa-arrow-down" title="Downstream"></i> {{ termination.port_speed_human }}
|
||||
<i class="fa fa-arrow-up" title="Upstream"></i> {{ termination.upstream_speed_human }}
|
||||
<i class="fa fa-arrow-down" title="Downstream"></i> {{ termination.port_speed|humanize_speed }}
|
||||
<i class="fa fa-arrow-up" title="Upstream"></i> {{ termination.upstream_speed|humanize_speed }}
|
||||
{% else %}
|
||||
{{ termination.port_speed_human }}
|
||||
{{ termination.port_speed|humanize_speed }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
17
netbox/templates/circuits/inc/speed_widget.html
Normal file
17
netbox/templates/circuits/inc/speed_widget.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a href="#" target="{{ target_field }}" data="10000" class="set_speed">10 Mbps</a></li>
|
||||
<li><a href="#" target="{{ target_field }}" data="100000" class="set_speed">100 Mbps</a></li>
|
||||
<li><a href="#" target="{{ target_field }}" data="1000000" class="set_speed">1 Gbps</a></li>
|
||||
<li><a href="#" target="{{ target_field }}" data="10000000" class="set_speed">10 Gbps</a></li>
|
||||
<li><a href="#" target="{{ target_field }}" data="25000000" class="set_speed">25 Gbps</a></li>
|
||||
<li><a href="#" target="{{ target_field }}" data="40000000" class="set_speed">40 Gbps</a></li>
|
||||
<li><a href="#" target="{{ target_field }}" data="100000000" class="set_speed">100 Gbps</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" target="{{ target_field }}" data="1544" class="set_speed">T1 (1.544 Mbps)</a></li>
|
||||
<li><a href="#" target="{{ target_field }}" data="2048" class="set_speed">E1 (2.048 Mbps)</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}{{ provider }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
@@ -45,7 +43,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ provider }}</h1>
|
||||
<h1>{% block title %}{{ provider }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=provider %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}Providers{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.circuits.add_provider %}
|
||||
@@ -16,7 +14,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='providers' %}
|
||||
</div>
|
||||
<h1>Providers</h1>
|
||||
<h1>{% block title %}Providers{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}Console Connections{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.change_consoleport %}
|
||||
@@ -12,7 +10,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='connections' %}
|
||||
</div>
|
||||
<h1>Console Connections</h1>
|
||||
<h1>{% block title %}Console Connections{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'responsive_table.html' %}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
@@ -21,7 +19,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Connect {{ consoleport.device }} {{ consoleport }}</div>
|
||||
<div class="panel-heading">{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %}</div>
|
||||
<div class="panel-body">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Connect {{ consoleserverport.device }} {{ consoleserverport }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
@@ -21,7 +19,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Connect {{ consoleserverport.device }} {{ consoleserverport }}</div>
|
||||
<div class="panel-heading">{% block title %}Connect {{ consoleserverport.device }} {{ consoleserverport }}{% endblock %}</div>
|
||||
<div class="panel-body">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif not obj.device_type.is_child_device %}
|
||||
{% else %}
|
||||
{% render_field form.face %}
|
||||
{% render_field form.position %}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Devices{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_device %}
|
||||
@@ -17,7 +15,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='devices' %}
|
||||
</div>
|
||||
<h1>Devices</h1>
|
||||
<h1>{% block title %}Devices{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
|
||||
|
||||
@@ -53,7 +53,7 @@ $(document).ready(function() {
|
||||
success: function(json) {
|
||||
$.each(json['get_lldp_neighbors'], function(iface, neighbors) {
|
||||
var neighbor = neighbors[0];
|
||||
var row = $('#' + iface.replace(/(\/)/g, "\\$1"));
|
||||
var row = $('#' + iface.split(".")[0].replace(/(\/)/g, "\\$1"));
|
||||
var configured_device = row.children('td.configured_device').attr('data');
|
||||
var configured_interface = row.children('td.configured_interface').attr('data');
|
||||
// Add LLDP neighbors to table
|
||||
@@ -62,7 +62,7 @@ $(document).ready(function() {
|
||||
// Apply colors to rows
|
||||
if (!configured_device && neighbor['hostname']) {
|
||||
row.addClass('info');
|
||||
} else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port']) {
|
||||
} else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port'].split(".")[0]) {
|
||||
row.addClass('success');
|
||||
} else {
|
||||
row.addClass('danger');
|
||||
|
||||
@@ -80,13 +80,19 @@ $(document).ready(function() {
|
||||
$('#model').html(json['get_facts']['model']);
|
||||
$('#serial_number').html(json['get_facts']['serial_number']);
|
||||
$('#os_version').html(json['get_facts']['os_version']);
|
||||
$('#uptime').html(json['get_facts']['uptime']);
|
||||
// Calculate uptime
|
||||
var uptime = json['get_facts']['uptime'];
|
||||
console.log(uptime);
|
||||
var uptime_days = Math.floor(uptime / 86400);
|
||||
var uptime_hours = Math.floor(uptime % 86400 / 3600);
|
||||
var uptime_minutes = Math.floor(uptime % 3600 / 60);
|
||||
$('#uptime').html(uptime_days + "d " + uptime_hours + "h " + uptime_minutes + "m");
|
||||
$.each(json['get_environment']['cpu'], function(name, obj) {
|
||||
var row="<tr><td>" + name + "</td><td>" + obj['%usage'] + "%</td></tr>";
|
||||
$("#cpu").after(row)
|
||||
});
|
||||
$('#memory').after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "MB</td></tr>");
|
||||
$('#memory').after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "MB</td></tr>");
|
||||
$('#memory').after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "</td></tr>");
|
||||
$('#memory').after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "</td></tr>");
|
||||
$.each(json['get_environment']['temperature'], function(name, obj) {
|
||||
var style = "success";
|
||||
if (obj['is_alert']) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Populate {{ device_bay }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
@@ -17,7 +15,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Populate {{ device_bay }}</div>
|
||||
<div class="panel-heading">{% block title %}Populate {{ device_bay }}{% endblock %}</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Parent Device</label>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Device Roles{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_devicerole %}
|
||||
@@ -12,7 +10,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>Device Roles</h1>
|
||||
<h1>{% block title %}Device Roles{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicerole_bulk_delete' %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -31,7 +29,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
|
||||
<h1>{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Device Types{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_devicetype %}
|
||||
@@ -17,7 +15,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='devicetypes' %}
|
||||
</div>
|
||||
<h1>Device Types</h1>
|
||||
<h1>{% block title %}Device Types{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status %} success{% elif iface.connection and not iface.connection.connection_status %} info{% endif %}">
|
||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% endif %}">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||
@@ -113,41 +113,55 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% for ip in iface.ip_addresses.all %}
|
||||
<tr class="ipaddress">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td colspan="3">
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
{% if ip.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
||||
{% endif %}
|
||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||
<span class="label label-success">Primary</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if ip.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
|
||||
{% with iface.ip_addresses.all as ipaddresses %}
|
||||
{% if ipaddresses %}
|
||||
<tr class="ipaddress">
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td></td>
|
||||
<td colspan="6" class="subtable">
|
||||
{% else %}
|
||||
<span class="text-muted">Global</span>
|
||||
<td colspan="7" class="subtable">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<table class="table table-hover">
|
||||
{% for ip in ipaddresses %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
{% if ip.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
|
||||
<span class="label label-success">Primary</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if ip.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Global table</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}Interface Connections{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_interfaceconnection %}
|
||||
@@ -12,7 +10,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='connections' %}
|
||||
</div>
|
||||
<h1>Interface Connections</h1>
|
||||
<h1>{% block title %}Interface Connections{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'responsive_table.html' %}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Assign a New IP Address{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>IP Address</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.address %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interface Assignment</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface %}
|
||||
{% render_field form.set_as_primary %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
|
||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Manufacturers{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_manufacturer %}
|
||||
@@ -17,7 +15,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='manufacturers' %}
|
||||
</div>
|
||||
<h1>Manufacturers</h1>
|
||||
<h1>{% block title %}Manufacturers{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:manufacturer_bulk_delete' %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Platforms{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_platform %}
|
||||
@@ -12,7 +10,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>Platforms</h1>
|
||||
<h1>{% block title %}Platforms{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:platform_bulk_delete' %}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}Power Connections{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.change_powerport %}
|
||||
@@ -12,7 +10,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='connections' %}
|
||||
</div>
|
||||
<h1>Power Connections</h1>
|
||||
<h1>{% block title %}Power Connections{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'responsive_table.html' %}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Connect {{ poweroutlet.device }} {{ poweroutlet }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
@@ -21,7 +19,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Connect {{ poweroutlet.device }} {{ poweroutlet }}</div>
|
||||
<div class="panel-heading">{% block title %}Connect {{ poweroutlet.device }} {{ poweroutlet }}{% endblock %}</div>
|
||||
<div class="panel-body">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Connect {{ powerport.device }} {{ powerport }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
@@ -21,7 +19,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Connect {{ powerport.device }} {{ powerport }}</div>
|
||||
<div class="panel-heading">{% block title %}Connect {{ powerport.device }} {{ powerport }}{% endblock %}</div>
|
||||
<div class="panel-body">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}{{ rack.site }} - Rack {{ rack.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
@@ -51,7 +49,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>Rack {{ rack.name }}</h1>
|
||||
<h1>{% block title %}Rack {{ rack.name }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=rack %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@@ -261,13 +259,13 @@
|
||||
<div class="rack_header">
|
||||
<h4>Front</h4>
|
||||
</div>
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 %}
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<div class="rack_header">
|
||||
<h4>Rear</h4>
|
||||
</div>
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 %}
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
<p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
|
||||
</div>
|
||||
{% if face_id %}
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
|
||||
{% else %}
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 %}
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
<div class="rack_header">
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Racks{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_rack %}
|
||||
@@ -17,7 +15,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='racks' %}
|
||||
</div>
|
||||
<h1>Racks</h1>
|
||||
<h1>{% block title %}Racks{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %}
|
||||
@@ -27,3 +25,36 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
|
||||
var site_list = $('#id_site');
|
||||
var rack_group_list = $('#id_group_id');
|
||||
|
||||
// Update rack group and rack options based on selected site
|
||||
site_list.change(function() {
|
||||
var selected_sites = $(this).val();
|
||||
if (selected_sites) {
|
||||
|
||||
// Update rack group options
|
||||
rack_group_list.empty();
|
||||
$.ajax({
|
||||
url: netbox_api_path + 'dcim/rack-groups/?limit=500&site=' + selected_sites.join('&site='),
|
||||
dataType: 'json',
|
||||
success: function (response, status) {
|
||||
$.each(response["results"], function (index, group) {
|
||||
var option = $("<option></option>").attr("value", group.id).text(group.name);
|
||||
rack_group_list.append(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Rack Groups{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_rackgroup %}
|
||||
@@ -17,7 +15,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='rackgroups' %}
|
||||
</div>
|
||||
<h1>Rack Groups</h1>
|
||||
<h1>{% block title %}Rack Groups{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Rack Role{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_rackrole %}
|
||||
@@ -12,7 +10,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>Rack Roles</h1>
|
||||
<h1>{% block title %}Rack Roles{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackrole_bulk_delete' %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Regions{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_region %}
|
||||
@@ -17,7 +15,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='regions' %}
|
||||
</div>
|
||||
<h1>{{ block.title }}</h1>
|
||||
<h1>{% block title %}Regions{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}{{ site }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
@@ -50,7 +48,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ site.name }}</h1>
|
||||
<h1>{% block title %}{{ site }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=site %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}Sites{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_site %}
|
||||
@@ -16,7 +14,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='sites' %}
|
||||
</div>
|
||||
<h1>Sites</h1>
|
||||
<h1>{% block title %}Sites{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block content %}
|
||||
{% if settings.NETBOX_USERNAME or settings.NETBOX_PASSWORD %}
|
||||
<div class="alert alert-warning alert-dismissable" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<strong>Warning:</strong> The <code>NETBOX_USERNAME</code> and <code>NETBOX_PASSWORD</code> configuration parameters have been deprecated. Please replace them in configuration.py with <code>NAPALM_USERNAME</code> and <code>NAPALM_PASSWORD</code>.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'search_form.html' %}
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-md-4">
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{% empty %}
|
||||
{% if table.empty_text %}
|
||||
<tr>
|
||||
<td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td>
|
||||
<td colspan="{{ table.columns|length }}" class="text-center text-muted">— {{ table.empty_text }} —</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}Aggregate: {{ aggregate }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
@@ -38,7 +36,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ aggregate }}</h1>
|
||||
<h1>{% block title %}{{ aggregate }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=aggregate %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load humanize %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Aggregates{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_aggregate %}
|
||||
@@ -18,15 +16,22 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='aggregates' %}
|
||||
</div>
|
||||
<h1>Aggregates</h1>
|
||||
<h1>{% block title %}Aggregates{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:aggregate_bulk_edit' bulk_delete_url='ipam:aggregate_bulk_delete' %}
|
||||
<p class="text-right">IPv4 total: <strong>{{ ipv4_total|intcomma }} /32s</strong></p>
|
||||
<p class="text-right">IPv6 total: <strong>{{ ipv6_total|intcomma }} /64s</strong></p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong><i class="fa fa-bar-chart"></i> Statistics</strong>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">Total IPv4 IPs <span class="badge">{{ ipv4_total|intcomma }}</span></li>
|
||||
<li class="list-group-item">Total IPv6 /64s <span class="badge">{{ ipv6_total|intcomma }}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}{{ ipaddress }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
@@ -40,10 +38,10 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ ipaddress }}</h1>
|
||||
<h1>{% block title %}{{ ipaddress }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=ipaddress %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>IP Address</strong>
|
||||
@@ -85,7 +83,11 @@
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>
|
||||
<a href="{% url 'ipam:ipaddress_list' %}?role={{ ipaddress.role }}">{{ ipaddress.get_role_display }}</a>
|
||||
{% if ipaddress.role %}
|
||||
<a href="{% url 'ipam:ipaddress_list' %}?role={{ ipaddress.role }}">{{ ipaddress.get_role_display }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -137,7 +139,7 @@
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-8">
|
||||
{% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
|
||||
{% if duplicate_ips_table.rows %}
|
||||
{% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Assign an IP Address{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Assign an IP Address</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">IP Address</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ ipaddress }}</p>
|
||||
</div>
|
||||
<label class="col-md-3 control-label">VRF</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
{% if ipaddress.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=ipaddress.vrf.pk %}">{{ ipaddress.vrf }}</a> ({{ ipaddress.vrf.rd }})
|
||||
{% else %}
|
||||
<span>Global</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||
<li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="search">
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
<div class="tab-pane" id="select">
|
||||
{% render_field form.site %}
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.device %}
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface %}
|
||||
{% render_field form.set_as_primary %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<button type="submit" name="_assign" class="btn btn-primary">Assign</button>
|
||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/livesearch.js' %}?v{{ settings.VERSION }}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}IP Addresses{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
@@ -17,7 +15,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='IPs' %}
|
||||
</div>
|
||||
<h1>IP Addresses</h1>
|
||||
<h1>{% block title %}IP Addresses{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Remove {{ ipaddress }} from {{ ipaddress.interface }}?{% endblock %}
|
||||
|
||||
{% block message %}
|
||||
<p>Are you sure you want to remove this IP address from <strong>{{ ipaddress.interface.device }} {{ ipaddress.interface }}</strong>?</p>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}{{ prefix }}{% endblock %}
|
||||
{% block title %}{{ prefix }} - IP Addresses{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'ipam/inc/prefix_header.html' with active_tab='ip-addresses' %}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Prefixes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
<div class="btn-group" role="group">
|
||||
@@ -22,7 +20,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='prefixes' %}
|
||||
</div>
|
||||
<h1>Prefixes</h1>
|
||||
<h1>{% block title %}Prefixes{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load humanize %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}RIRs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if request.GET.family == '6' %}
|
||||
@@ -24,7 +22,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>RIRs</h1>
|
||||
<h1>{% block title %}RIRs{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Prefix/VLAN Roles{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_devicerole %}
|
||||
{% if perms.ipam.add_role %}
|
||||
<a href="{% url 'ipam:role_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a role
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>Prefix/VLAN Roles</h1>
|
||||
<h1>{% block title %}Prefix/VLAN Roles{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:role_bulk_delete' %}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}VLAN {{ vlan.display_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
@@ -43,7 +41,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>VLAN {{ vlan.display_name }}</h1>
|
||||
<h1>{% block title %}VLAN {{ vlan.display_name }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=vlan %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}VLANs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_vlan %}
|
||||
@@ -18,7 +16,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='VLANs' %}
|
||||
</div>
|
||||
<h1>VLANs</h1>
|
||||
<h1>{% block title %}VLANs{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}VLAN Groups{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_vlangroup %}
|
||||
@@ -12,7 +10,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>VLAN Groups</h1>
|
||||
<h1>{% block title %}VLAN Groups{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:vlangroup_bulk_delete' %}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block title %}VRF {{ vrf }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
@@ -37,7 +35,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ vrf }}</h1>
|
||||
<h1>{% block title %}VRF {{ vrf }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=vrf %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}VRFs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.ipam.add_vrf %}
|
||||
@@ -18,7 +16,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='VRFs' %}
|
||||
</div>
|
||||
<h1>VRFs</h1>
|
||||
<h1>{% block title %}VRFs{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vrf_bulk_edit' bulk_delete_url='ipam:vrf_bulk_delete' %}
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row" style="margin-top: 150px;">
|
||||
<div class="row" style="margin-top: {% if settings.BANNER_LOGIN %}100{% else %}150{% endif %}px;">
|
||||
<div class="col-sm-4 col-sm-offset-4">
|
||||
{% if settings.BANNER_LOGIN %}
|
||||
<div style="margin-bottom: 25px">
|
||||
{{ settings.BANNER_LOGIN|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load secret_helpers %}
|
||||
|
||||
{% block title %}Secret: {{ secret }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -28,7 +26,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ secret }}</h1>
|
||||
<h1>{% block title %}{{ secret }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=secret %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% if secret.pk %}Editing {{ secret }}{% else %}Add a Secret{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{{ form.private_key }}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h3>{% if secret.pk %}Editing {{ secret }}{% else %}Add a Secret{% endif %}</h3>
|
||||
<h3>{% block title %}{% if secret.pk %}Editing {{ secret }}{% else %}Add a Secret{% endif %}{% endblock %}</h3>
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Secret Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Secret Import</h1>
|
||||
<h1>{% block title %}Secret Import{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% if form.non_field_errors %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Secrets{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.secrets.add_secret %}
|
||||
@@ -12,7 +10,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>Secrets</h1>
|
||||
<h1>{% block title %}Secrets{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='secrets:secret_bulk_edit' bulk_delete_url='secrets:secret_bulk_delete' %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Secret Roles{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_devicerole %}
|
||||
@@ -12,7 +10,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>Secret Roles</h1>
|
||||
<h1>{% block title %}Secret Roles{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='secrets:secretrole_bulk_delete' %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}{{ tenant }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
@@ -41,7 +39,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ tenant }}</h1>
|
||||
<h1>{% block title %}{{ tenant }}{% endblock %}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=tenant %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Tenants{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.tenancy.add_tenant %}
|
||||
@@ -17,7 +15,7 @@
|
||||
{% endif %}
|
||||
{% include 'inc/export_button.html' with obj_type='tenants' %}
|
||||
</div>
|
||||
<h1>Tenants</h1>
|
||||
<h1>{% block title %}Tenants{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='tenancy:tenant_bulk_edit' bulk_delete_url='tenancy:tenant_bulk_delete' %}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Tenant Groups{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.tenancy.add_tenantgroup %}
|
||||
@@ -12,7 +10,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>Tenant Groups</h1>
|
||||
<h1>{% block title %}Tenant Groups{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='tenancy:tenantgroup_bulk_delete' %}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<td>
|
||||
{{ field.help_text|default:field.label }}
|
||||
{% if field.choices %}
|
||||
<br /><small class="text-muted">Choices: {{ field.choices|example_choices }}</small>
|
||||
<br /><small class="text-muted">Choices: {{ field|example_choices }}</small>
|
||||
{% elif field|widget_type == 'dateinput' %}
|
||||
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
|
||||
@@ -4,14 +4,14 @@ from rest_framework import serializers
|
||||
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.api import ModelValidationMixin
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
# Tenant groups
|
||||
#
|
||||
|
||||
class TenantGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class TenantGroupSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = TenantGroup
|
||||
|
||||
@@ -244,6 +244,7 @@ class TokenEditView(LoginRequiredMixin, View):
|
||||
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
|
||||
form = TokenForm(request.POST, instance=token)
|
||||
else:
|
||||
token = Token()
|
||||
form = TokenForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
@@ -259,6 +260,13 @@ class TokenEditView(LoginRequiredMixin, View):
|
||||
else:
|
||||
return redirect('user:token_list')
|
||||
|
||||
return render(request, 'utilities/obj_edit.html', {
|
||||
'obj': token,
|
||||
'obj_type': token._meta.verbose_name,
|
||||
'form': form,
|
||||
'return_url': reverse('user:token_list'),
|
||||
})
|
||||
|
||||
|
||||
class TokenDeleteView(LoginRequiredMixin, View):
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@ from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from rest_framework import authentication, exceptions
|
||||
from rest_framework.compat import is_authenticated
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework.permissions import DjangoModelPermissions, SAFE_METHODS
|
||||
from rest_framework.serializers import Field, ValidationError
|
||||
from rest_framework.permissions import BasePermission, DjangoModelPermissions, SAFE_METHODS
|
||||
from rest_framework.renderers import BrowsableAPIRenderer
|
||||
from rest_framework.serializers import Field, ModelSerializer, ValidationError
|
||||
from rest_framework.views import get_view_name as drf_get_view_name
|
||||
|
||||
from users.models import Token
|
||||
|
||||
@@ -20,6 +23,10 @@ class ServiceUnavailable(APIException):
|
||||
default_detail = "Service temporarily unavailable, please try again later."
|
||||
|
||||
|
||||
#
|
||||
# Authentication
|
||||
#
|
||||
|
||||
class TokenAuthentication(authentication.TokenAuthentication):
|
||||
"""
|
||||
A custom authentication scheme which enforces Token expiration times.
|
||||
@@ -61,6 +68,42 @@ class TokenPermissions(DjangoModelPermissions):
|
||||
return super(TokenPermissions, self).has_permission(request, view)
|
||||
|
||||
|
||||
class IsAuthenticatedOrLoginNotRequired(BasePermission):
|
||||
"""
|
||||
Returns True if the user is authenticated or LOGIN_REQUIRED is False.
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
if not settings.LOGIN_REQUIRED:
|
||||
return True
|
||||
return request.user and is_authenticated(request.user)
|
||||
|
||||
|
||||
#
|
||||
# Serializers
|
||||
#
|
||||
|
||||
class ValidatedModelSerializer(ModelSerializer):
|
||||
"""
|
||||
Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation.
|
||||
"""
|
||||
def validate(self, data):
|
||||
|
||||
# Remove custom field data (if any) prior to model validation
|
||||
attrs = data.copy()
|
||||
attrs.pop('custom_fields', None)
|
||||
|
||||
# Run clean() on an instance of the model
|
||||
if self.instance is None:
|
||||
instance = self.Meta.model(**attrs)
|
||||
else:
|
||||
instance = self.instance
|
||||
for k, v in attrs.items():
|
||||
setattr(instance, k, v)
|
||||
instance.clean()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ChoiceFieldSerializer(Field):
|
||||
"""
|
||||
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
|
||||
@@ -98,16 +141,9 @@ class ContentTypeFieldSerializer(Field):
|
||||
raise ValidationError("Invalid content type")
|
||||
|
||||
|
||||
class ModelValidationMixin(object):
|
||||
"""
|
||||
Enforce a model's validation through clean() when validating serializer data. This is necessary to ensure we're
|
||||
employing the same validation logic via both forms and the API.
|
||||
"""
|
||||
def validate(self, attrs):
|
||||
instance = self.Meta.model(**attrs)
|
||||
instance.clean()
|
||||
return attrs
|
||||
|
||||
#
|
||||
# Mixins
|
||||
#
|
||||
|
||||
class WritableSerializerMixin(object):
|
||||
"""
|
||||
@@ -119,6 +155,10 @@ class WritableSerializerMixin(object):
|
||||
return self.serializer_class
|
||||
|
||||
|
||||
#
|
||||
# Pagination
|
||||
#
|
||||
|
||||
class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
||||
"""
|
||||
Override the stock paginator to allow setting limit=0 to disable pagination for a request. This returns all objects
|
||||
@@ -165,3 +205,33 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
||||
pass
|
||||
|
||||
return self.default_limit
|
||||
|
||||
|
||||
#
|
||||
# Renderers
|
||||
#
|
||||
|
||||
class FormlessBrowsableAPIRenderer(BrowsableAPIRenderer):
|
||||
"""
|
||||
Override the built-in BrowsableAPIRenderer to disable HTML forms.
|
||||
"""
|
||||
def show_form_for_method(self, *args, **kwargs):
|
||||
return False
|
||||
|
||||
|
||||
#
|
||||
# Miscellaneous
|
||||
#
|
||||
|
||||
def get_view_name(view_cls, suffix=None):
|
||||
"""
|
||||
Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name`.
|
||||
"""
|
||||
if hasattr(view_cls, 'queryset'):
|
||||
name = view_cls.queryset.model._meta.verbose_name
|
||||
name = ' '.join([w[0].upper() + w[1:] for w in name.split()]) # Capitalize each word
|
||||
if suffix:
|
||||
name = "{} {}".format(name, suffix)
|
||||
return name
|
||||
|
||||
return drf_get_view_name(view_cls, suffix)
|
||||
|
||||
@@ -19,6 +19,16 @@ class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
pass
|
||||
|
||||
|
||||
class NullableCharFieldFilter(django_filters.CharFilter):
|
||||
null_value = 'NULL'
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value != self.null_value:
|
||||
return super(NullableCharFieldFilter, self).filter(qs, value)
|
||||
qs = self.get_method(qs)(**{'{}__isnull'.format(self.name): True})
|
||||
return qs.distinct() if self.distinct else qs
|
||||
|
||||
|
||||
class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
"""
|
||||
This field operates like a normal ModelMultipleChoiceField except that it allows for one additional choice which is
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user