Merge remote-tracking branch 'digitalocean/develop' into develop

This commit is contained in:
rdujardin 2016-08-08 18:44:43 +02:00
commit 75437eb79d
20 changed files with 240 additions and 66 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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