mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-28 03:16:25 -06:00
Merge remote-tracking branch 'digitalocean/develop' into develop
This commit is contained in:
commit
75437eb79d
@ -402,7 +402,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
self.fields['primary_ip6'].widget.attrs['readonly'] = True
|
self.fields['primary_ip6'].widget.attrs['readonly'] = True
|
||||||
|
|
||||||
# Limit rack choices
|
# Limit rack choices
|
||||||
if self.is_bound:
|
if self.is_bound and self.data.get('site'):
|
||||||
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
|
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
|
||||||
elif self.initial.get('site'):
|
elif self.initial.get('site'):
|
||||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||||
@ -443,6 +443,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
|||||||
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
|
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
|
||||||
self.fields['site'].disabled = True
|
self.fields['site'].disabled = True
|
||||||
self.fields['rack'].disabled = True
|
self.fields['rack'].disabled = True
|
||||||
|
self.initial['site'] = self.instance.parent_bay.device.rack.site_id
|
||||||
|
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
||||||
|
|
||||||
|
|
||||||
class BaseDeviceFromCSVForm(forms.ModelForm):
|
class BaseDeviceFromCSVForm(forms.ModelForm):
|
||||||
|
25
netbox/dcim/migrations/0013_add_interface_form_factors.py
Normal file
25
netbox/dcim/migrations/0013_add_interface_form_factors.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-08-06 20:24
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0012_site_rack_device_add_tenant'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
|
||||||
|
),
|
||||||
|
]
|
@ -57,20 +57,63 @@ DEVICE_ROLE_COLOR_CHOICES = [
|
|||||||
IFACE_FF_VIRTUAL = 0
|
IFACE_FF_VIRTUAL = 0
|
||||||
IFACE_FF_100M_COPPER = 800
|
IFACE_FF_100M_COPPER = 800
|
||||||
IFACE_FF_1GE_COPPER = 1000
|
IFACE_FF_1GE_COPPER = 1000
|
||||||
|
IFACE_FF_GBIC = 1050
|
||||||
IFACE_FF_SFP = 1100
|
IFACE_FF_SFP = 1100
|
||||||
IFACE_FF_10GE_COPPER = 1150
|
IFACE_FF_10GE_COPPER = 1150
|
||||||
IFACE_FF_SFP_PLUS = 1200
|
IFACE_FF_SFP_PLUS = 1200
|
||||||
IFACE_FF_XFP = 1300
|
IFACE_FF_XFP = 1300
|
||||||
IFACE_FF_QSFP_PLUS = 1400
|
IFACE_FF_QSFP_PLUS = 1400
|
||||||
|
IFACE_FF_CFP = 1500
|
||||||
|
IFACE_FF_QSFP28 = 1600
|
||||||
|
IFACE_FF_T1 = 4000
|
||||||
|
IFACE_FF_E1 = 4010
|
||||||
|
IFACE_FF_T3 = 4040
|
||||||
|
IFACE_FF_E3 = 4050
|
||||||
|
IFACE_FF_STACKWISE = 5000
|
||||||
|
IFACE_FF_STACKWISE_PLUS = 5050
|
||||||
IFACE_FF_CHOICES = [
|
IFACE_FF_CHOICES = [
|
||||||
|
[
|
||||||
|
'Virtual interfaces',
|
||||||
|
[
|
||||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||||
[IFACE_FF_100M_COPPER, '10/100M (100BASE-TX)'],
|
]
|
||||||
[IFACE_FF_1GE_COPPER, '1GE (1000BASE-T)'],
|
],
|
||||||
[IFACE_FF_SFP, '1GE (SFP)'],
|
[
|
||||||
[IFACE_FF_10GE_COPPER, '10GE (10GBASE-T)'],
|
'Ethernet',
|
||||||
[IFACE_FF_SFP_PLUS, '10GE (SFP+)'],
|
[
|
||||||
[IFACE_FF_XFP, '10GE (XFP)'],
|
[IFACE_FF_100M_COPPER, '100BASE-TX (10/100M)'],
|
||||||
[IFACE_FF_QSFP_PLUS, '40GE (QSFP+)'],
|
[IFACE_FF_1GE_COPPER, '1000BASE-T (1GE)'],
|
||||||
|
[IFACE_FF_10GE_COPPER, '10GBASE-T (10GE)'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Modular',
|
||||||
|
[
|
||||||
|
[IFACE_FF_GBIC, 'GBIC (1GE)'],
|
||||||
|
[IFACE_FF_SFP, 'SFP (1GE)'],
|
||||||
|
[IFACE_FF_XFP, 'XFP (10GE)'],
|
||||||
|
[IFACE_FF_SFP_PLUS, 'SFP+ (10GE)'],
|
||||||
|
[IFACE_FF_QSFP_PLUS, 'QSFP+ (40GE)'],
|
||||||
|
[IFACE_FF_CFP, 'CFP (100GE)'],
|
||||||
|
[IFACE_FF_QSFP28, 'QSFP28 (100GE)'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Serial',
|
||||||
|
[
|
||||||
|
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
|
||||||
|
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
|
||||||
|
[IFACE_FF_T3, 'T3 (45 Mbps)'],
|
||||||
|
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Stacking',
|
||||||
|
[
|
||||||
|
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
|
||||||
|
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
|
||||||
|
]
|
||||||
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
STATUS_ACTIVE = True
|
STATUS_ACTIVE = True
|
||||||
|
@ -19,3 +19,9 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
|||||||
prepopulated_fields = {
|
prepopulated_fields = {
|
||||||
'slug': ['name'],
|
'slug': ['name'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserAction)
|
||||||
|
class UserActionAdmin(admin.ModelAdmin):
|
||||||
|
actions = None
|
||||||
|
list_display = ['user', 'action', 'content_type', 'object_id', 'message']
|
||||||
|
@ -191,6 +191,10 @@ class IPAddressFilter(django_filters.FilterSet):
|
|||||||
action='search',
|
action='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
parent = django_filters.MethodFilter(
|
||||||
|
action='search_by_parent',
|
||||||
|
label='Parent prefix',
|
||||||
|
)
|
||||||
vrf = django_filters.MethodFilter(
|
vrf = django_filters.MethodFilter(
|
||||||
action='_vrf',
|
action='_vrf',
|
||||||
label='VRF',
|
label='VRF',
|
||||||
@ -238,6 +242,16 @@ class IPAddressFilter(django_filters.FilterSet):
|
|||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
def search_by_parent(self, queryset, value):
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
return queryset
|
||||||
|
try:
|
||||||
|
query = str(IPNetwork(value).cidr)
|
||||||
|
return queryset.filter(address__net_contained_or_equal=query)
|
||||||
|
except AddrFormatError:
|
||||||
|
return queryset.none()
|
||||||
|
|
||||||
def _vrf(self, queryset, value):
|
def _vrf(self, queryset, value):
|
||||||
if str(value) == '':
|
if str(value) == '':
|
||||||
return queryset
|
return queryset
|
||||||
|
@ -317,7 +317,9 @@ def prefix_role_choices():
|
|||||||
|
|
||||||
|
|
||||||
class PrefixFilterForm(forms.Form, BootstrapMixin):
|
class PrefixFilterForm(forms.Form, BootstrapMixin):
|
||||||
parent = forms.CharField(required=False, label='Search Within')
|
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
||||||
|
'placeholder': 'Network',
|
||||||
|
}))
|
||||||
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
|
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
||||||
@ -470,6 +472,9 @@ def ipaddress_vrf_choices():
|
|||||||
|
|
||||||
|
|
||||||
class IPAddressFilterForm(forms.Form, BootstrapMixin):
|
class IPAddressFilterForm(forms.Form, BootstrapMixin):
|
||||||
|
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
||||||
|
'placeholder': 'Prefix',
|
||||||
|
}))
|
||||||
family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
|
family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
|
||||||
vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF',
|
vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
|
@ -39,6 +39,16 @@ PREFIX_LINK_BRIEF = """
|
|||||||
</span>
|
</span>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
IPADDRESS_LINK = """
|
||||||
|
{% if record.pk %}
|
||||||
|
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||||
|
{% elif perms.ipam.add_ipaddress %}
|
||||||
|
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ record.0 }}
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
STATUS_LABEL = """
|
STATUS_LABEL = """
|
||||||
{% if record.pk %}
|
{% if record.pk %}
|
||||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||||
@ -148,6 +158,9 @@ class PrefixTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
|
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
|
||||||
|
row_attrs = {
|
||||||
|
'class': lambda record: 'success' if not record.pk else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PrefixBriefTable(BaseTable):
|
class PrefixBriefTable(BaseTable):
|
||||||
@ -169,7 +182,7 @@ class PrefixBriefTable(BaseTable):
|
|||||||
|
|
||||||
class IPAddressTable(BaseTable):
|
class IPAddressTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
|
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
||||||
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
|
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
|
||||||
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
||||||
ptr = tables.Column(verbose_name='PTR')
|
ptr = tables.Column(verbose_name='PTR')
|
||||||
@ -181,6 +194,9 @@ class IPAddressTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ('pk', 'address', 'vrf', 'tenant', 'ptr', 'device', 'interface', 'description')
|
fields = ('pk', 'address', 'vrf', 'tenant', 'ptr', 'device', 'interface', 'description')
|
||||||
|
row_attrs = {
|
||||||
|
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBriefTable(BaseTable):
|
class IPAddressBriefTable(BaseTable):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from netaddr import IPSet
|
import netaddr
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
@ -23,7 +23,7 @@ def add_available_prefixes(parent, prefix_list):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Find all unallocated space
|
# Find all unallocated space
|
||||||
available_prefixes = IPSet(parent) ^ IPSet([p.prefix for p in prefix_list])
|
available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
|
||||||
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
|
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
|
||||||
|
|
||||||
# Concatenate and sort complete list of children
|
# Concatenate and sort complete list of children
|
||||||
@ -33,6 +33,57 @@ def add_available_prefixes(parent, prefix_list):
|
|||||||
return prefix_list
|
return prefix_list
|
||||||
|
|
||||||
|
|
||||||
|
def add_available_ipaddresses(prefix, ipaddress_list):
|
||||||
|
"""
|
||||||
|
Annotate ranges of available IP addresses within a given prefix.
|
||||||
|
"""
|
||||||
|
|
||||||
|
output = []
|
||||||
|
prev_ip = None
|
||||||
|
|
||||||
|
# Ignore the "network address" for IPv4 prefixes larger than /31
|
||||||
|
if prefix.version == 4 and prefix.prefixlen < 31:
|
||||||
|
first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
|
||||||
|
else:
|
||||||
|
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
|
||||||
|
|
||||||
|
# Ignore the broadcast address for IPv4 prefixes larger than /31
|
||||||
|
if prefix.version == 4 and prefix.prefixlen < 31:
|
||||||
|
last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
|
||||||
|
else:
|
||||||
|
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
|
||||||
|
|
||||||
|
if not ipaddress_list:
|
||||||
|
return [(
|
||||||
|
int(last_ip_in_prefix - first_ip_in_prefix + 1),
|
||||||
|
'{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||||
|
)]
|
||||||
|
|
||||||
|
# Account for any available IPs before the first real IP
|
||||||
|
if ipaddress_list[0].address.ip > first_ip_in_prefix:
|
||||||
|
skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
|
||||||
|
first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||||
|
output.append((skipped_count, first_skipped))
|
||||||
|
|
||||||
|
# Iterate through existing IPs and annotate free ranges
|
||||||
|
for ip in ipaddress_list:
|
||||||
|
if prev_ip:
|
||||||
|
diff = int(ip.address.ip - prev_ip.address.ip)
|
||||||
|
if diff > 1:
|
||||||
|
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||||
|
output.append((diff - 1, first_skipped))
|
||||||
|
output.append(ip)
|
||||||
|
prev_ip = ip
|
||||||
|
|
||||||
|
# Include any remaining available IPs
|
||||||
|
if prev_ip.address.ip < last_ip_in_prefix:
|
||||||
|
skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
|
||||||
|
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||||
|
output.append((skipped_count, first_skipped))
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VRFs
|
# VRFs
|
||||||
#
|
#
|
||||||
@ -325,7 +376,7 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'ipam.change_prefix'
|
permission_required = 'ipam.change_prefix'
|
||||||
model = Prefix
|
model = Prefix
|
||||||
form_class = forms.PrefixForm
|
form_class = forms.PrefixForm
|
||||||
fields_initial = ['site', 'vrf', 'prefix']
|
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
|
||||||
cancel_url = 'ipam:prefix_list'
|
cancel_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
@ -381,6 +432,7 @@ def prefix_ipaddresses(request, pk):
|
|||||||
# Find all IPAddresses belonging to this Prefix
|
# Find all IPAddresses belonging to this Prefix
|
||||||
ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
|
ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
|
||||||
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
||||||
|
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses)
|
||||||
|
|
||||||
ip_table = tables.IPAddressTable(ipaddresses)
|
ip_table = tables.IPAddressTable(ipaddresses)
|
||||||
ip_table.model = IPAddress
|
ip_table.model = IPAddress
|
||||||
|
@ -12,7 +12,7 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.4.1-dev'
|
VERSION = '1.4.3-dev'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
|
@ -48,7 +48,7 @@ def home(request):
|
|||||||
|
|
||||||
return render(request, 'home.html', {
|
return render(request, 'home.html', {
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
'recent_activity': UserAction.objects.select_related('user')[:15]
|
'recent_activity': UserAction.objects.select_related('user')[:50]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ $(document).ready(function() {
|
|||||||
// Slugify
|
// Slugify
|
||||||
function slugify(s, num_chars) {
|
function slugify(s, num_chars) {
|
||||||
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
|
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
|
||||||
s = s.replace(/^\s+|\s+$/g, ''); // Trim leading/trailing spaces
|
s = s.replace(/^[\s\.]+|[\s\.]+$/g, ''); // Trim leading/trailing spaces
|
||||||
s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens
|
s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens
|
||||||
s = s.toLowerCase(); // Convert to lowercase
|
s = s.toLowerCase(); // Convert to lowercase
|
||||||
return s.substring(0, num_chars); // Trim to first num_chars chars
|
return s.substring(0, num_chars); // Trim to first num_chars chars
|
||||||
|
@ -83,8 +83,8 @@ $(document).ready(function() {
|
|||||||
},
|
},
|
||||||
success: function (response, status) {
|
success: function (response, status) {
|
||||||
$('#secret_' + secret_id).html(response.plaintext);
|
$('#secret_' + secret_id).html(response.plaintext);
|
||||||
$('button.unlock-secret').hide();
|
$('button.unlock-secret[secret-id=' + secret_id + ']').hide();
|
||||||
$('button.lock-secret').show();
|
$('button.lock-secret[secret-id=' + secret_id + ']').show();
|
||||||
},
|
},
|
||||||
error: function (xhr, ajaxOptions, thrownError) {
|
error: function (xhr, ajaxOptions, thrownError) {
|
||||||
if (xhr.status == 403) {
|
if (xhr.status == 403) {
|
||||||
|
@ -92,7 +92,7 @@ def secret_add(request, pk):
|
|||||||
|
|
||||||
messages.success(request, "Added new secret: {0}".format(secret))
|
messages.success(request, "Added new secret: {0}".format(secret))
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
return redirect('secrets:secret_add')
|
return redirect('dcim:device_addsecret', pk=device.pk)
|
||||||
else:
|
else:
|
||||||
return redirect('secrets:secret', pk=secret.pk)
|
return redirect('secrets:secret', pk=secret.pk)
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Port Speed</td>
|
<td>Port Speed</td>
|
||||||
<td>Physical speed in Kbps/td>
|
<td>Physical speed in Kbps</td>
|
||||||
<td>10000</td>
|
<td>10000</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -36,6 +36,11 @@
|
|||||||
<td>Functional role of device</td>
|
<td>Functional role of device</td>
|
||||||
<td>Blade Server</td>
|
<td>Blade Server</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tenant</td>
|
||||||
|
<td>Name of tenant (optional)</td>
|
||||||
|
<td>Pied Piper</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Device manufacturer</td>
|
<td>Device manufacturer</td>
|
||||||
<td>Hardware manufacturer</td>
|
<td>Hardware manufacturer</td>
|
||||||
@ -69,7 +74,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h4>Example</h4>
|
<h4>Example</h4>
|
||||||
<pre>Blade12,Blade Server,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4</pre>
|
<pre>Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -125,6 +125,14 @@
|
|||||||
<strong>Prefixes</strong>
|
<strong>Prefixes</strong>
|
||||||
</div>
|
</div>
|
||||||
{% render_table prefix_table %}
|
{% render_table prefix_table %}
|
||||||
|
{% if perms.ipam.add_prefix %}
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
<a href="{% url 'ipam:prefix_add' %}?{% if vlan.tenant %}tenant={{ vlan.tenant.pk }}&{% endif %}site={{ vlan.site.pk }}&vlan={{ vlan.pk }}" class="btn btn-primary btn-xs">
|
||||||
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
|
Add a prefix
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,19 +2,15 @@
|
|||||||
{% load static from staticfiles %}
|
{% load static from staticfiles %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}{% if secret.pk %}Editing Secret: {{ secret }}{% else %}Add a Secret{% endif %}{% endblock %}
|
{% block title %}{% if secret.pk %}Editing {{ secret }}{% else %}Add a Secret{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if secret.pk %}
|
|
||||||
<h1>Editing Secret: {{ secret }}</h1>
|
|
||||||
{% else %}
|
|
||||||
<h1>Add a Secret</h1>
|
|
||||||
{% endif %}
|
|
||||||
<form action="." method="post" class="form form-horizontal requires-private-key">
|
<form action="." method="post" class="form form-horizontal requires-private-key">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.private_key }}
|
{{ form.private_key }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-md-offset-3">
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
<h3>{% if secret.pk %}Editing {{ secret }}{% else %}Add a Secret{% endif %}</h3>
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
<div class="panel panel-danger">
|
<div class="panel panel-danger">
|
||||||
<div class="panel-heading"><strong>Errors</strong></div>
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
@ -23,10 +19,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Secret Attributes</strong></div>
|
<div class="panel-heading"><strong>Secret Attributes</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@ -41,8 +33,6 @@
|
|||||||
{% render_field form.userkeys %}
|
{% render_field form.userkeys %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Secret Data</strong></div>
|
<div class="panel-heading"><strong>Secret Data</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
@ -93,35 +93,35 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row panel-body">
|
<div class="row panel-body">
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'dcim:site_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.site_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.site_count }}</a></h2>
|
<h2><a href="{% url 'dcim:site_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.site_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.site_count }}</a></h2>
|
||||||
<p>Sites</p>
|
<p>Sites</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'dcim:rack_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.rack_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.rack_count }}</a></h2>
|
<h2><a href="{% url 'dcim:rack_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.rack_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.rack_count }}</a></h2>
|
||||||
<p>Racks</p>
|
<p>Racks</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'dcim:device_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.device_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.device_count }}</a></h2>
|
<h2><a href="{% url 'dcim:device_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.device_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.device_count }}</a></h2>
|
||||||
<p>Devices</p>
|
<p>Devices</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'ipam:vrf_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.vrf_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.vrf_count }}</a></h2>
|
<h2><a href="{% url 'ipam:vrf_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.vrf_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vrf_count }}</a></h2>
|
||||||
<p>VRFs</p>
|
<p>VRFs</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'ipam:prefix_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.prefix_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.prefix_count }}</a></h2>
|
<h2><a href="{% url 'ipam:prefix_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.prefix_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
|
||||||
<p>Prefixes</p>
|
<p>Prefixes</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'ipam:ipaddress_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.ipaddress_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.ipaddress_count }}</a></h2>
|
<h2><a href="{% url 'ipam:ipaddress_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.ipaddress_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.ipaddress_count }}</a></h2>
|
||||||
<p>IP addresses</p>
|
<p>IP addresses</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'ipam:vlan_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.vlan_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.vlan_count }}</a></h2>
|
<h2><a href="{% url 'ipam:vlan_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.vlan_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vlan_count }}</a></h2>
|
||||||
<p>VLANs</p>
|
<p>VLANs</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'circuits:circuit_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.circuit_count }}</a></h2>
|
<h2><a href="{% url 'circuits:circuit_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
|
||||||
<p>Circuits</p>
|
<p>Circuits</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,9 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
|
||||||
|
from circuits.models import Circuit
|
||||||
|
from dcim.models import Site, Rack, Device
|
||||||
|
from ipam.models import IPAddress, Prefix, VLAN, VRF
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
@ -50,19 +53,21 @@ class TenantListView(ObjectListView):
|
|||||||
|
|
||||||
def tenant(request, slug):
|
def tenant(request, slug):
|
||||||
|
|
||||||
tenant = get_object_or_404(Tenant.objects.annotate(
|
tenant = get_object_or_404(Tenant, slug=slug)
|
||||||
site_count=Count('sites', distinct=True),
|
stats = {
|
||||||
rack_count=Count('racks', distinct=True),
|
'site_count': Site.objects.filter(tenant=tenant).count(),
|
||||||
device_count=Count('devices', distinct=True),
|
'rack_count': Rack.objects.filter(tenant=tenant).count(),
|
||||||
vrf_count=Count('vrfs', distinct=True),
|
'device_count': Device.objects.filter(tenant=tenant).count(),
|
||||||
prefix_count=Count('prefixes', distinct=True),
|
'vrf_count': VRF.objects.filter(tenant=tenant).count(),
|
||||||
ipaddress_count=Count('ip_addresses', distinct=True),
|
'prefix_count': Prefix.objects.filter(tenant=tenant).count(),
|
||||||
vlan_count=Count('vlans', distinct=True),
|
'ipaddress_count': IPAddress.objects.filter(tenant=tenant).count(),
|
||||||
circuit_count=Count('circuits', distinct=True),
|
'vlan_count': VLAN.objects.filter(tenant=tenant).count(),
|
||||||
), slug=slug)
|
'circuit_count': Circuit.objects.filter(tenant=tenant).count(),
|
||||||
|
}
|
||||||
|
|
||||||
return render(request, 'tenancy/tenant.html', {
|
return render(request, 'tenancy/tenant.html', {
|
||||||
'tenant': tenant,
|
'tenant': tenant,
|
||||||
|
'stats': stats,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import csv
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -118,7 +119,8 @@ class Livesearch(forms.TextInput):
|
|||||||
|
|
||||||
class CSVDataField(forms.CharField):
|
class CSVDataField(forms.CharField):
|
||||||
"""
|
"""
|
||||||
A field for comma-separated values (CSV)
|
A field for comma-separated values (CSV). Values containing commas should be encased within double quotes. Example:
|
||||||
|
'"New York, NY",new-york-ny,Other stuff' => ['New York, NY', 'new-york-ny', 'Other stuff']
|
||||||
"""
|
"""
|
||||||
csv_form = None
|
csv_form = None
|
||||||
|
|
||||||
@ -136,16 +138,16 @@ class CSVDataField(forms.CharField):
|
|||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
# Return a list of dictionaries, each representing an individual record
|
# Return a list of dictionaries, each representing an individual record
|
||||||
records = []
|
records = []
|
||||||
for i, row in enumerate(value.split('\n'), start=1):
|
reader = csv.reader(value.splitlines())
|
||||||
if row.strip():
|
for i, row in enumerate(reader, start=1):
|
||||||
values = row.strip().split(',')
|
if row:
|
||||||
if len(values) < len(self.columns):
|
if len(row) < len(self.columns):
|
||||||
raise forms.ValidationError("Line {}: Field(s) missing (found {}; expected {})"
|
raise forms.ValidationError("Line {}: Field(s) missing (found {}; expected {})"
|
||||||
.format(i, len(values), len(self.columns)))
|
.format(i, len(row), len(self.columns)))
|
||||||
elif len(values) > len(self.columns):
|
elif len(row) > len(self.columns):
|
||||||
raise forms.ValidationError("Line {}: Too many fields (found {}; expected {})"
|
raise forms.ValidationError("Line {}: Too many fields (found {}; expected {})"
|
||||||
.format(i, len(values), len(self.columns)))
|
.format(i, len(row), len(self.columns)))
|
||||||
record = dict(zip(self.columns, values))
|
record = dict(zip(self.columns, row))
|
||||||
records.append(record)
|
records.append(record)
|
||||||
return records
|
return records
|
||||||
|
|
||||||
@ -229,6 +231,7 @@ class BootstrapMixin(forms.BaseForm):
|
|||||||
field.widget.attrs['class'] = 'form-control'
|
field.widget.attrs['class'] = 'form-control'
|
||||||
if field.required:
|
if field.required:
|
||||||
field.widget.attrs['required'] = 'required'
|
field.widget.attrs['required'] = 'required'
|
||||||
|
if 'placeholder' not in field.widget.attrs:
|
||||||
field.widget.attrs['placeholder'] = field.label
|
field.widget.attrs['placeholder'] = field.label
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user