Merge pull request #408 from digitalocean/develop

Release v1.4.0
This commit is contained in:
Jeremy Stretch 2016-08-01 13:43:48 -04:00 committed by GitHub
commit 9889e120bd
131 changed files with 2585 additions and 703 deletions

View File

@ -0,0 +1,9 @@
NetBox supports the concept of individual tenants within its parent organization. Typically, these are used to represent individual customers or internal departments.
# Tenants
A tenant represents a discrete organization. Certain resources within NetBox can be assigned to a tenant. This makes it very convenient to track which resources are assigned to which customers, for instance.
### Tenant Groups
Tenants are grouped by type. For instance, you might create one group called "Customers" and one called "Acquisitions."

View File

@ -163,6 +163,18 @@ Are you sure you want to do this?
Type 'yes' to continue, or 'no' to cancel: yes
```
# Load Initial Data (Optional)
NetBox ships with some initial data to help you get started: RIR definitions, common devices roles, etc. You can delete any seed data that you don't want to keep.
!!! note
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
```
# ./manage.py loaddata initial_data
Installed 43 object(s) from 4 fixture(s)
```
# Test the Application
At this point, NetBox should be able to run. We can verify this by starting a development instance:

View File

@ -17,6 +17,7 @@ pages:
- 'DCIM': 'data-model/dcim.md'
- 'IPAM': 'data-model/ipam.md'
- 'Secrets': 'data-model/secrets.md'
- 'Tenancy': 'data-model/tenancy.md'
- 'Extras': 'data-model/extras.md'
- 'API Integration': 'api-integration.md'

View File

@ -21,10 +21,11 @@ class CircuitTypeAdmin(admin.ModelAdmin):
@admin.register(Circuit)
class CircuitAdmin(admin.ModelAdmin):
list_display = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id']
list_filter = ['provider']
list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'commit_rate',
'xconnect_id']
list_filter = ['provider', 'type', 'tenant']
exclude = ['interface']
def get_queryset(self, request):
qs = super(CircuitAdmin, self).get_queryset(request)
return qs.select_related('provider', 'type', 'site')
return qs.select_related('provider', 'type', 'tenant', 'site')

View File

@ -2,6 +2,7 @@ from rest_framework import serializers
from circuits.models import Provider, CircuitType, Circuit
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
from tenancy.api.serializers import TenantNestedSerializer
#
@ -45,13 +46,14 @@ class CircuitTypeNestedSerializer(CircuitTypeSerializer):
class CircuitSerializer(serializers.ModelSerializer):
provider = ProviderNestedSerializer()
type = CircuitTypeNestedSerializer()
tenant = TenantNestedSerializer()
site = SiteNestedSerializer()
interface = InterfaceNestedSerializer()
class Meta:
model = Circuit
fields = ['id', 'cid', 'provider', 'type', 'site', 'interface', 'install_date', 'port_speed', 'commit_rate',
'xconnect_id', 'comments']
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
'commit_rate', 'xconnect_id', 'comments']
class CircuitNestedSerializer(CircuitSerializer):

View File

@ -42,7 +42,7 @@ class CircuitListView(generics.ListAPIView):
"""
List circuits (filterable)
"""
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
serializer_class = serializers.CircuitSerializer
filter_class = CircuitFilter
@ -51,5 +51,5 @@ class CircuitDetailView(generics.RetrieveAPIView):
"""
Retrieve a single circuit
"""
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
serializer_class = serializers.CircuitSerializer

View File

@ -3,6 +3,7 @@ import django_filters
from django.db.models import Q
from dcim.models import Site
from tenancy.models import Tenant
from .models import Provider, Circuit, CircuitType
@ -28,10 +29,10 @@ class ProviderFilter(django_filters.FilterSet):
fields = ['q', 'name', 'account', 'asn']
def search(self, queryset, value):
value = value.strip()
return queryset.filter(
Q(name__icontains=value) |
Q(account__icontains=value)
Q(account__icontains=value) |
Q(comments__icontains=value)
)
@ -62,6 +63,17 @@ class CircuitFilter(django_filters.FilterSet):
to_field_name='slug',
label='Circuit type (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
@ -79,5 +91,9 @@ class CircuitFilter(django_filters.FilterSet):
fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date']
def search(self, queryset, value):
value = value.strip()
return queryset.filter(cid__icontains=value)
return queryset.filter(
Q(cid__icontains=value) |
Q(xconnect_id__icontains=value) |
Q(pp_info__icontains=value) |
Q(comments__icontains=value)
)

View File

@ -0,0 +1,26 @@
[
{
"model": "circuits.circuittype",
"pk": 1,
"fields": {
"name": "Internet",
"slug": "internet"
}
},
{
"model": "circuits.circuittype",
"pk": 2,
"fields": {
"name": "Private WAN",
"slug": "private-wan"
}
},
{
"model": "circuits.circuittype",
"pk": 3,
"fields": {
"name": "Out-of-Band",
"slug": "out-of-band"
}
}
]

View File

@ -2,6 +2,8 @@ from django import forms
from django.db.models import Count
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField,
)
@ -99,7 +101,7 @@ class CircuitForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Circuit
fields = [
'cid', 'type', 'provider', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
'port_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
]
help_texts = {
@ -160,13 +162,15 @@ class CircuitFromCSVForm(forms.ModelForm):
error_messages={'invalid_choice': 'Provider not found.'})
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid circuit type.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'})
class Meta:
model = Circuit
fields = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id',
'pp_info']
fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'commit_rate',
'xconnect_id', 'pp_info']
class CircuitImportForm(BulkImportForm, BootstrapMixin):
@ -177,6 +181,7 @@ class CircuitBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
comments = CommentField()
@ -192,6 +197,11 @@ def circuit_provider_choices():
return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
def circuit_tenant_choices():
tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits'))
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices]
def circuit_site_choices():
site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
@ -201,5 +211,7 @@ class CircuitFilterForm(forms.Form, BootstrapMixin):
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-26 21:59
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0001_initial'),
('circuits', '0003_provider_32bit_asn_support'),
]
operations = [
migrations.AddField(
model_name='circuit',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant'),
),
]

View File

@ -3,6 +3,7 @@ from django.db import models
from dcim.fields import ASNField
from dcim.models import Site, Interface
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
@ -66,6 +67,7 @@ class Circuit(CreatedUpdatedModel):
cid = models.CharField(max_length=50, verbose_name='Circuit ID')
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
site = models.ForeignKey(Site, related_name='circuits', on_delete=models.PROTECT)
interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True)
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
@ -90,6 +92,7 @@ class Circuit(CreatedUpdatedModel):
self.cid,
self.provider.name,
self.type.name,
self.tenant.name if self.tenant else '',
self.site.name,
self.install_date.isoformat() if self.install_date else '',
str(self.port_speed),

View File

@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import Circuit, CircuitType, Provider
CIRCUITTYPE_EDIT_LINK = """
CIRCUITTYPE_ACTIONS = """
{% if perms.circuit.change_circuittype %}
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}">Edit</a>
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -21,11 +21,12 @@ class ProviderTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
asn = tables.Column(verbose_name='ASN')
account = tables.Column(verbose_name='Account')
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
class Meta(BaseTable.Meta):
model = Provider
fields = ('pk', 'name', 'asn', 'circuit_count')
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
#
@ -37,11 +38,12 @@ class CircuitTypeTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
circuit_count = tables.Column(verbose_name='Circuits')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=CIRCUITTYPE_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = CircuitType
fields = ('pk', 'name', 'circuit_count', 'slug', 'edit')
fields = ('pk', 'name', 'circuit_count', 'slug', 'actions')
#
@ -53,10 +55,11 @@ class CircuitTable(BaseTable):
cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
type = tables.Column(verbose_name='Type')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
port_speed_human = tables.Column(verbose_name='Port Speed')
commit_rate_human = tables.Column(verbose_name='Commit Rate')
class Meta(BaseTable.Meta):
model = Circuit
fields = ('pk', 'cid', 'type', 'provider', 'site', 'port_speed_human', 'commit_rate_human')
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed_human', 'commit_rate_human')

View File

@ -2,6 +2,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count
from django.shortcuts import get_object_or_404, render
from extras.models import Graph, GRAPH_TYPE_PROVIDER
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
@ -27,10 +28,12 @@ def provider(request, slug):
provider = get_object_or_404(Provider, slug=slug)
circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device')
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
return render(request, 'circuits/provider.html', {
'provider': provider,
'circuits': circuits,
'show_graphs': show_graphs,
})
@ -109,7 +112,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class CircuitListView(ObjectListView):
queryset = Circuit.objects.select_related('provider', 'type', 'site')
queryset = Circuit.objects.select_related('provider', 'type', 'tenant', 'site')
filter = filters.CircuitFilter
filter_form = forms.CircuitFilterForm
table = tables.CircuitTable
@ -159,6 +162,10 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]

View File

@ -6,6 +6,7 @@ from dcim.models import (
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
)
from tenancy.api.serializers import TenantNestedSerializer
#
@ -13,10 +14,11 @@ from dcim.models import (
#
class SiteSerializer(serializers.ModelSerializer):
tenant = TenantNestedSerializer()
class Meta:
model = Site
fields = ['id', 'name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
@ -52,10 +54,11 @@ class RackGroupNestedSerializer(RackGroupSerializer):
class RackSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
group = RackGroupNestedSerializer()
tenant = TenantNestedSerializer()
class Meta:
model = Rack
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments']
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments']
class RackNestedSerializer(RackSerializer):
@ -69,8 +72,8 @@ class RackDetailSerializer(RackSerializer):
rear_units = serializers.SerializerMethodField()
class Meta(RackSerializer.Meta):
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments', 'front_units',
'rear_units']
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments',
'front_units', 'rear_units']
def get_front_units(self, obj):
units = obj.get_rack_units(face=RACK_FACE_FRONT)
@ -218,6 +221,7 @@ class DeviceIPAddressNestedSerializer(serializers.ModelSerializer):
class DeviceSerializer(serializers.ModelSerializer):
device_type = DeviceTypeNestedSerializer()
device_role = DeviceRoleNestedSerializer()
tenant = TenantNestedSerializer()
platform = PlatformNestedSerializer()
rack = RackNestedSerializer()
primary_ip = DeviceIPAddressNestedSerializer()
@ -227,8 +231,8 @@ class DeviceSerializer(serializers.ModelSerializer):
class Meta:
model = Device
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'rack',
'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
def get_parent_device(self, obj):
try:

View File

@ -27,7 +27,7 @@ class SiteListView(generics.ListAPIView):
"""
List all sites
"""
queryset = Site.objects.all()
queryset = Site.objects.select_related('tenant')
serializer_class = serializers.SiteSerializer
@ -35,7 +35,7 @@ class SiteDetailView(generics.RetrieveAPIView):
"""
Retrieve a single site
"""
queryset = Site.objects.all()
queryset = Site.objects.select_related('tenant')
serializer_class = serializers.SiteSerializer
@ -47,7 +47,7 @@ class RackGroupListView(generics.ListAPIView):
"""
List all rack groups
"""
queryset = RackGroup.objects.all()
queryset = RackGroup.objects.select_related('site')
serializer_class = serializers.RackGroupSerializer
filter_class = filters.RackGroupFilter
@ -56,7 +56,7 @@ class RackGroupDetailView(generics.RetrieveAPIView):
"""
Retrieve a single rack group
"""
queryset = RackGroup.objects.all()
queryset = RackGroup.objects.select_related('site')
serializer_class = serializers.RackGroupSerializer
@ -68,7 +68,7 @@ class RackListView(generics.ListAPIView):
"""
List racks (filterable)
"""
queryset = Rack.objects.select_related('site')
queryset = Rack.objects.select_related('site', 'group', 'tenant')
serializer_class = serializers.RackSerializer
filter_class = filters.RackFilter
@ -77,7 +77,7 @@ class RackDetailView(generics.RetrieveAPIView):
"""
Retrieve a single rack
"""
queryset = Rack.objects.select_related('site')
queryset = Rack.objects.select_related('site', 'group', 'tenant')
serializer_class = serializers.RackDetailSerializer
@ -193,8 +193,9 @@ class DeviceListView(generics.ListAPIView):
"""
List devices (filterable)
"""
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'platform', 'rack__site')\
.prefetch_related('primary_ip4__nat_outside', 'primary_ip6__nat_outside')
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside',
'primary_ip6__nat_outside')
serializer_class = serializers.DeviceSerializer
filter_class = filters.DeviceFilter
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
@ -204,7 +205,8 @@ class DeviceDetailView(generics.RetrieveAPIView):
"""
Retrieve a single device
"""
queryset = Device.objects.all()
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
'rack__site', 'parent_bay')
serializer_class = serializers.DeviceSerializer

View File

@ -6,6 +6,7 @@ from .models import (
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
)
from tenancy.models import Tenant
class SiteFilter(django_filters.FilterSet):
@ -13,17 +14,27 @@ class SiteFilter(django_filters.FilterSet):
action='search',
label='Search',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
class Meta:
model = Site
fields = ['q', 'name', 'facility', 'asn']
def search(self, queryset, value):
value = value.strip()
qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
Q(shipping_address__icontains=value)
Q(shipping_address__icontains=value) | Q(comments__icontains=value)
try:
qs_filter |= Q(asn=int(value))
qs_filter |= Q(asn=int(value.strip()))
except ValueError:
pass
return queryset.filter(qs_filter)
@ -74,16 +85,27 @@ class RackFilter(django_filters.FilterSet):
to_field_name='slug',
label='Group',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
class Meta:
model = Rack
fields = ['q', 'site_id', 'site', 'u_height']
def search(self, queryset, value):
value = value.strip()
return queryset.filter(
Q(name__icontains=value) |
Q(facility_id__icontains=value)
Q(facility_id__icontains=value) |
Q(comments__icontains=value)
)
@ -143,6 +165,17 @@ class DeviceFilter(django_filters.FilterSet):
to_field_name='slug',
label='Role (slug)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(),
@ -200,11 +233,11 @@ class DeviceFilter(django_filters.FilterSet):
'is_network_device']
def search(self, queryset, value):
value = value.strip()
return queryset.filter(
Q(name__icontains=value) |
Q(serial__icontains=value) |
Q(modules__serial__icontains=value)
Q(modules__serial__icontains=value) |
Q(comments__icontains=value)
).distinct()

View File

@ -0,0 +1,201 @@
[
{
"model": "dcim.devicerole",
"pk": 1,
"fields": {
"name": "Console Server",
"slug": "console-server",
"color": "teal"
}
},
{
"model": "dcim.devicerole",
"pk": 2,
"fields": {
"name": "Core Switch",
"slug": "core-switch",
"color": "blue"
}
},
{
"model": "dcim.devicerole",
"pk": 3,
"fields": {
"name": "Distribution Switch",
"slug": "distribution-switch",
"color": "blue"
}
},
{
"model": "dcim.devicerole",
"pk": 4,
"fields": {
"name": "Access Switch",
"slug": "access-switch",
"color": "blue"
}
},
{
"model": "dcim.devicerole",
"pk": 5,
"fields": {
"name": "Management Switch",
"slug": "management-switch",
"color": "orange"
}
},
{
"model": "dcim.devicerole",
"pk": 6,
"fields": {
"name": "Firewall",
"slug": "firewall",
"color": "red"
}
},
{
"model": "dcim.devicerole",
"pk": 7,
"fields": {
"name": "Router",
"slug": "router",
"color": "purple"
}
},
{
"model": "dcim.devicerole",
"pk": 8,
"fields": {
"name": "Server",
"slug": "server",
"color": "medium_gray"
}
},
{
"model": "dcim.devicerole",
"pk": 9,
"fields": {
"name": "PDU",
"slug": "pdu",
"color": "dark_gray"
}
},
{
"model": "dcim.manufacturer",
"pk": 1,
"fields": {
"name": "APC",
"slug": "apc"
}
},
{
"model": "dcim.manufacturer",
"pk": 2,
"fields": {
"name": "Cisco",
"slug": "cisco"
}
},
{
"model": "dcim.manufacturer",
"pk": 3,
"fields": {
"name": "Dell",
"slug": "dell"
}
},
{
"model": "dcim.manufacturer",
"pk": 4,
"fields": {
"name": "HP",
"slug": "hp"
}
},
{
"model": "dcim.manufacturer",
"pk": 5,
"fields": {
"name": "Juniper",
"slug": "juniper"
}
},
{
"model": "dcim.manufacturer",
"pk": 6,
"fields": {
"name": "Arista",
"slug": "arista"
}
},
{
"model": "dcim.manufacturer",
"pk": 7,
"fields": {
"name": "Opengear",
"slug": "opengear"
}
},
{
"model": "dcim.manufacturer",
"pk": 8,
"fields": {
"name": "Super Micro",
"slug": "super-micro"
}
},
{
"model": "dcim.platform",
"pk": 1,
"fields": {
"name": "Cisco IOS",
"slug": "cisco-ios",
"rpc_client": "cisco-ios"
}
},
{
"model": "dcim.platform",
"pk": 2,
"fields": {
"name": "Cisco NX-OS",
"slug": "cisco-nx-os",
"rpc_client": ""
}
},
{
"model": "dcim.platform",
"pk": 3,
"fields": {
"name": "Juniper Junos",
"slug": "juniper-junos",
"rpc_client": "juniper-junos"
}
},
{
"model": "dcim.platform",
"pk": 4,
"fields": {
"name": "Arista EOS",
"slug": "arista-eos",
"rpc_client": ""
}
},
{
"model": "dcim.platform",
"pk": 5,
"fields": {
"name": "Linux",
"slug": "linux",
"rpc_client": ""
}
},
{
"model": "dcim.platform",
"pk": 6,
"fields": {
"name": "Opengear",
"slug": "opengear",
"rpc_client": "opengear"
}
}
]

View File

@ -4,6 +4,8 @@ from django import forms
from django.db.models import Count, Q
from ipam.models import IPAddress
from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant
from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
@ -38,6 +40,15 @@ def get_device_by_name_or_pk(name):
return device
def bulkedit_platform_choices():
choices = [
(None, '---------'),
(0, 'None'),
]
choices += [(p.pk, p.name) for p in Platform.objects.all()]
return choices
#
# Sites
#
@ -48,7 +59,7 @@ class SiteForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Site
fields = ['name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
widgets = {
'physical_address': SmallTextarea(attrs={'rows': 3}),
'shipping_address': SmallTextarea(attrs={'rows': 3}),
@ -63,16 +74,33 @@ class SiteForm(forms.ModelForm, BootstrapMixin):
class SiteFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
class Meta:
model = Site
fields = ['name', 'slug', 'facility', 'asn']
fields = ['name', 'slug', 'tenant', 'facility', 'asn']
class SiteImportForm(BulkImportForm, BootstrapMixin):
csv = CSVDataField(csv_form=SiteFromCSVForm)
class SiteBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
def site_tenant_choices():
tenant_choices = Tenant.objects.annotate(site_count=Count('sites'))
return [(t.slug, u'{} ({})'.format(t.name, t.site_count)) for t in tenant_choices]
class SiteFilterForm(forms.Form, BootstrapMixin):
tenant = forms.MultipleChoiceField(required=False, choices=site_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
# Rack groups
#
@ -107,7 +135,7 @@ class RackForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Rack
fields = ['site', 'group', 'name', 'facility_id', 'u_height', 'comments']
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'u_height', 'comments']
help_texts = {
'site': "The site at which the rack exists",
'name': "Organizational rack name",
@ -135,10 +163,12 @@ class RackFromCSVForm(forms.ModelForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'})
group_name = forms.CharField(required=False)
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
class Meta:
model = Rack
fields = ['site', 'group_name', 'name', 'facility_id', 'u_height']
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'u_height']
def clean(self):
@ -161,6 +191,7 @@ class RackBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
u_height = forms.IntegerField(required=False, label='Height (U)')
comments = CommentField()
@ -175,11 +206,18 @@ def rack_group_choices():
return [(g.pk, u'{} ({})'.format(g, g.rack_count)) for g in group_choices]
def rack_tenant_choices():
tenant_choices = Tenant.objects.annotate(rack_count=Count('racks'))
return [(t.slug, u'{} ({})'.format(t.name, t.rack_count)) for t in tenant_choices]
class RackFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
@ -203,8 +241,8 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = DeviceType
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'subdevice_role']
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
'is_pdu', 'is_network_device', 'subdevice_role']
class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
@ -324,7 +362,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Device
fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
fields = ['name', 'device_role', 'tenant', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
'platform', 'primary_ip4', 'primary_ip6', 'comments']
help_texts = {
'device_role': "The function this device serves",
@ -410,6 +448,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
class BaseDeviceFromCSVForm(forms.ModelForm):
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid device role.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
error_messages={'invalid_choice': 'Invalid manufacturer.'})
model_name = forms.CharField()
@ -441,8 +481,8 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
face = forms.CharField(required=False)
class Meta(BaseDeviceFromCSVForm.Meta):
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
'position', 'face']
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'site',
'rack_name', 'position', 'face']
def clean(self):
@ -477,7 +517,7 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
device_bay_name = forms.CharField(required=False)
class Meta(BaseDeviceFromCSVForm.Meta):
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'parent',
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'parent',
'device_bay_name']
def clean(self):
@ -512,8 +552,9 @@ class DeviceBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, label='Platform')
platform_delete = forms.BooleanField(required=False, label='Set platform to "none"')
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
platform = forms.TypedChoiceField(choices=bulkedit_platform_choices, coerce=int, required=False,
label='Platform')
status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
@ -533,6 +574,11 @@ def device_role_choices():
return [(r.slug, u'{} ({})'.format(r.name, r.device_count)) for r in role_choices]
def device_tenant_choices():
tenant_choices = Tenant.objects.annotate(device_count=Count('devices'))
return [(t.slug, u'{} ({})'.format(t.name, t.device_count)) for t in tenant_choices]
def device_type_choices():
type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
return [(t.pk, u'{} ({})'.format(t, t.device_count)) for t in type_choices]
@ -550,6 +596,8 @@ class DeviceFilterForm(forms.Form, BootstrapMixin):
widget=forms.SelectMultiple(attrs={'size': 8}))
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=device_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
widget=forms.SelectMultiple(attrs={'size': 8}))
platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-26 21:59
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0001_initial'),
('dcim', '0011_devicetype_part_number'),
]
operations = [
migrations.AddField(
model_name='device',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='rack',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='site',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'),
),
]

View File

@ -8,6 +8,7 @@ from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist
from extras.rpc import RPC_CLIENTS
from tenancy.models import Tenant
from utilities.fields import NullableCharField
from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel
@ -152,6 +153,7 @@ class Site(CreatedUpdatedModel):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='sites', on_delete=models.PROTECT)
facility = models.CharField(max_length=50, blank=True)
asn = ASNField(blank=True, null=True, verbose_name='ASN')
physical_address = models.CharField(max_length=200, blank=True)
@ -173,6 +175,7 @@ class Site(CreatedUpdatedModel):
return ','.join([
self.name,
self.slug,
self.tenant.name if self.tenant else '',
self.facility,
str(self.asn),
])
@ -237,6 +240,7 @@ class Rack(CreatedUpdatedModel):
facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID')
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
comments = models.TextField(blank=True)
@ -272,6 +276,7 @@ class Rack(CreatedUpdatedModel):
self.group.name if self.group else '',
self.name,
self.facility_id or '',
self.tenant.name if self.tenant else '',
str(self.u_height),
])
@ -631,6 +636,7 @@ class Device(CreatedUpdatedModel):
"""
device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='devices', on_delete=models.PROTECT)
platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
name = NullableCharField(max_length=50, blank=True, null=True, unique=True)
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
@ -724,6 +730,7 @@ class Device(CreatedUpdatedModel):
return ','.join([
self.name or '',
self.device_role.name,
self.tenant.name if self.tenant else '',
self.device_type.manufacturer.name,
self.device_type.model,
self.platform.name if self.platform else '',

View File

@ -16,27 +16,27 @@ DEVICE_LINK = """
</a>
"""
RACKGROUP_EDIT_LINK = """
RACKGROUP_ACTIONS = """
{% if perms.dcim.change_rackgroup %}
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}">Edit</a>
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
DEVICEROLE_EDIT_LINK = """
DEVICEROLE_ACTIONS = """
{% if perms.dcim.change_devicerole %}
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}">Edit</a>
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
MANUFACTURER_EDIT_LINK = """
MANUFACTURER_ACTIONS = """
{% if perms.dcim.change_manufacturer %}
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}">Edit</a>
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
PLATFORM_EDIT_LINK = """
PLATFORM_ACTIONS = """
{% if perms.dcim.change_platform %}
<a href="{% url 'dcim:platform_edit' slug=record.slug %}">Edit</a>
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -59,8 +59,10 @@ UTILIZATION_GRAPH = """
#
class SiteTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
facility = tables.Column(verbose_name='Facility')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
asn = tables.Column(verbose_name='ASN')
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
@ -70,8 +72,8 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta):
model = Site
fields = ('name', 'facility', 'asn', 'rack_count', 'device_count', 'prefix_count', 'vlan_count',
'circuit_count')
fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count')
#
@ -84,11 +86,12 @@ class RackGroupTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
rack_count = tables.Column(verbose_name='Racks')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=RACKGROUP_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=RACKGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = RackGroup
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'edit')
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions')
#
@ -101,14 +104,16 @@ class RackTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
facility_id = tables.Column(verbose_name='Facility ID')
u_height = tables.Column(verbose_name='Height (U)')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
u_consumed = tables.Column(accessor=Accessor('u_consumed'), verbose_name='Used (U)')
u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used')
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(BaseTable.Meta):
model = Rack
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'u_height', 'devices', 'u_consumed', 'utilization')
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'u_height', 'devices', 'u_consumed',
'utilization')
class RackImportTable(BaseTable):
@ -116,11 +121,12 @@ class RackImportTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
facility_id = tables.Column(verbose_name='Facility ID')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
u_height = tables.Column(verbose_name='Height (U)')
class Meta(BaseTable.Meta):
model = Rack
fields = ('site', 'group', 'name', 'facility_id', 'u_height')
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
#
@ -132,11 +138,12 @@ class ManufacturerTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
devicetype_count = tables.Column(verbose_name='Device Types')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=MANUFACTURER_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = Manufacturer
fields = ('pk', 'name', 'devicetype_count', 'slug', 'edit')
fields = ('pk', 'name', 'devicetype_count', 'slug', 'actions')
#
@ -228,11 +235,12 @@ class DeviceRoleTable(BaseTable):
device_count = tables.Column(verbose_name='Devices')
slug = tables.Column(verbose_name='Slug')
color = tables.Column(verbose_name='Color')
edit = tables.TemplateColumn(template_code=DEVICEROLE_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = DeviceRole
fields = ('pk', 'name', 'device_count', 'slug', 'color')
fields = ('pk', 'name', 'device_count', 'slug', 'color', 'actions')
#
@ -244,11 +252,11 @@ class PlatformTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
device_count = tables.Column(verbose_name='Devices')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=PLATFORM_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta):
model = Platform
fields = ('pk', 'name', 'device_count', 'slug', 'edit')
fields = ('pk', 'name', 'device_count', 'slug', 'actions')
#
@ -259,6 +267,7 @@ class DeviceTable(BaseTable):
pk = ToggleColumn()
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
device_role = tables.Column(verbose_name='Role')
@ -268,11 +277,12 @@ class DeviceTable(BaseTable):
class Meta(BaseTable.Meta):
model = Device
fields = ('pk', 'name', 'status', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
class DeviceImportTable(BaseTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
position = tables.Column(verbose_name='Position')
@ -281,7 +291,7 @@ class DeviceImportTable(BaseTable):
class Meta(BaseTable.Meta):
model = Device
fields = ('name', 'site', 'rack', 'position', 'device_role', 'device_type')
fields = ('name', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
empty_text = False

View File

@ -15,6 +15,7 @@ class SiteTest(APITestCase):
'id',
'name',
'slug',
'tenant',
'facility',
'asn',
'physical_address',
@ -40,6 +41,7 @@ class SiteTest(APITestCase):
'display_name',
'site',
'group',
'tenant',
'u_height',
'comments'
]
@ -115,6 +117,7 @@ class RackTest(APITestCase):
'display_name',
'site',
'group',
'tenant',
'u_height',
'comments'
]
@ -126,6 +129,7 @@ class RackTest(APITestCase):
'display_name',
'site',
'group',
'tenant',
'u_height',
'comments',
'front_units',
@ -311,6 +315,7 @@ class DeviceTest(APITestCase):
'display_name',
'device_type',
'device_role',
'tenant',
'platform',
'serial',
'rack',
@ -388,6 +393,7 @@ class DeviceTest(APITestCase):
'rack_name',
'serial',
'status',
'tenant',
]
response = self.client.get(endpoint)

View File

@ -15,6 +15,7 @@ urlpatterns = [
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),

View File

@ -15,7 +15,7 @@ from django.views.generic import View
from ipam.models import Prefix, IPAddress, VLAN
from circuits.models import Circuit
from extras.models import TopologyMap
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.forms import ConfirmationForm
from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
@ -61,9 +61,11 @@ def expand_pattern(string):
#
class SiteListView(ObjectListView):
queryset = Site.objects.all()
queryset = Site.objects.select_related('tenant')
filter = filters.SiteFilter
filter_form = forms.SiteFilterForm
table = tables.SiteTable
edit_permissions = ['dcim.change_rack', 'dcim.delete_rack']
template_name = 'dcim/site_list.html'
@ -79,12 +81,14 @@ def site(request, slug):
}
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
topology_maps = TopologyMap.objects.filter(site=site)
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
return render(request, 'dcim/site.html', {
'site': site,
'stats': stats,
'rack_groups': rack_groups,
'topology_maps': topology_maps,
'show_graphs': show_graphs,
})
@ -110,6 +114,24 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
obj_list_url = 'dcim:site_list'
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_site'
cls = Site
form = forms.SiteBulkEditForm
template_name = 'dcim/site_bulk_edit.html'
default_redirect_url = 'dcim:site_list'
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
#
# Rack groups
#
@ -141,7 +163,8 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class RackListView(ObjectListView):
queryset = Rack.objects.select_related('site').prefetch_related('devices__device_type').annotate(device_count=Count('devices', distinct=True), u_consumed=Sum('devices__device_type__u_height'))
queryset = Rack.objects.select_related('site').prefetch_related('devices__device_type')\
.annotate(device_count=Count('devices', distinct=True), u_consumed=Sum('devices__device_type__u_height'))
filter = filters.RackFilter
filter_form = forms.RackFilterForm
table = tables.RackTable
@ -153,7 +176,7 @@ def rack(request, pk):
rack = get_object_or_404(Rack, pk=pk)
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True)\
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
.select_related('device_type__manufacturer')
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
@ -200,7 +223,11 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['site', 'group', 'u_height', 'comments']:
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
for field in ['site', 'group', 'tenant', 'u_height', 'comments']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@ -560,6 +587,9 @@ def device(request, pk):
related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
.select_related('rack', 'device_type__manufacturer')[:10]
# Show graph button on interfaces only if at least one graph has been created.
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
return render(request, 'dcim/device.html', {
'device': device,
'console_ports': console_ports,
@ -572,6 +602,7 @@ def device(request, pk):
'ip_addresses': ip_addresses,
'secrets': secrets,
'related_devices': related_devices,
'show_graphs': show_graphs,
})
@ -625,14 +656,15 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['platform']:
fields_to_update['platform'] = form.cleaned_data['platform']
elif form.cleaned_data['platform_delete']:
fields_to_update['platform'] = None
for field in ['tenant', 'platform']:
if form.cleaned_data[field] == 0:
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
if form.cleaned_data['status']:
status = form.cleaned_data['status']
fields_to_update['status'] = True if status == 'True' else False
for field in ['device_type', 'device_role', 'serial']:
for field in ['tenant', 'device_type', 'device_role', 'serial']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]

View File

@ -7,7 +7,12 @@ from .models import (
@admin.register(VRF)
class VRFAdmin(admin.ModelAdmin):
list_display = ['name', 'rd']
list_display = ['name', 'rd', 'tenant', 'enforce_unique']
list_filter = ['tenant']
def get_queryset(self, request):
qs = super(VRFAdmin, self).get_queryset(request)
return qs.select_related('tenant')
@admin.register(Role)
@ -35,7 +40,7 @@ class AggregateAdmin(admin.ModelAdmin):
@admin.register(Prefix)
class PrefixAdmin(admin.ModelAdmin):
list_display = ['prefix', 'vrf', 'site', 'status', 'role', 'vlan']
list_display = ['prefix', 'vrf', 'tenant', 'site', 'status', 'role', 'vlan']
list_filter = ['family', 'site', 'status', 'role']
search_fields = ['prefix']
@ -46,7 +51,7 @@ class PrefixAdmin(admin.ModelAdmin):
@admin.register(IPAddress)
class IPAddressAdmin(admin.ModelAdmin):
list_display = ['address', 'vrf', 'nat_inside']
list_display = ['address', 'vrf', 'tenant', 'nat_inside']
list_filter = ['family']
fields = ['address', 'vrf', 'device', 'interface', 'nat_inside']
readonly_fields = ['interface', 'device', 'nat_inside']
@ -67,10 +72,10 @@ class VLANGroupAdmin(admin.ModelAdmin):
@admin.register(VLAN)
class VLANAdmin(admin.ModelAdmin):
list_display = ['site', 'vid', 'name', 'status', 'role']
list_filter = ['site', 'status', 'role']
list_display = ['site', 'vid', 'name', 'tenant', 'status', 'role']
list_filter = ['site', 'tenant', 'status', 'role']
search_fields = ['vid', 'name']
def get_queryset(self, request):
qs = super(VLANAdmin, self).get_queryset(request)
return qs.select_related('site', 'role')
return qs.select_related('site', 'tenant', 'role')

View File

@ -2,6 +2,7 @@ from rest_framework import serializers
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
from tenancy.api.serializers import TenantNestedSerializer
#
@ -9,10 +10,11 @@ from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLAN
#
class VRFSerializer(serializers.ModelSerializer):
tenant = TenantNestedSerializer()
class Meta:
model = VRF
fields = ['id', 'name', 'rd', 'enforce_unique', 'description']
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description']
class VRFNestedSerializer(VRFSerializer):
@ -21,6 +23,15 @@ class VRFNestedSerializer(VRFSerializer):
fields = ['id', 'name', 'rd']
class VRFTenantSerializer(VRFSerializer):
"""
Include tenant serializer. Useful for determining tenant inheritance for Prefixes and IPAddresses.
"""
class Meta(VRFSerializer.Meta):
fields = ['id', 'name', 'rd', 'tenant']
#
# Roles
#
@ -98,11 +109,12 @@ class VLANGroupNestedSerializer(VLANGroupSerializer):
class VLANSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
group = VLANGroupNestedSerializer()
tenant = TenantNestedSerializer()
role = RoleNestedSerializer()
class Meta:
model = VLAN
fields = ['id', 'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'display_name']
fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name']
class VLANNestedSerializer(VLANSerializer):
@ -117,13 +129,14 @@ class VLANNestedSerializer(VLANSerializer):
class PrefixSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
vrf = VRFNestedSerializer()
vrf = VRFTenantSerializer()
tenant = TenantNestedSerializer()
vlan = VLANNestedSerializer()
role = RoleNestedSerializer()
class Meta:
model = Prefix
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'vlan', 'status', 'role', 'description']
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description']
class PrefixNestedSerializer(PrefixSerializer):
@ -137,12 +150,13 @@ class PrefixNestedSerializer(PrefixSerializer):
#
class IPAddressSerializer(serializers.ModelSerializer):
vrf = VRFNestedSerializer()
vrf = VRFTenantSerializer()
tenant = TenantNestedSerializer()
interface = InterfaceNestedSerializer()
class Meta:
model = IPAddress
fields = ['id', 'family', 'address', 'vrf', 'interface', 'description', 'nat_inside', 'nat_outside']
fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside']
class IPAddressNestedSerializer(IPAddressSerializer):

View File

@ -14,7 +14,7 @@ class VRFListView(generics.ListAPIView):
"""
List all VRFs
"""
queryset = VRF.objects.all()
queryset = VRF.objects.select_related('tenant')
serializer_class = serializers.VRFSerializer
filter_class = filters.VRFFilter
@ -23,7 +23,7 @@ class VRFDetailView(generics.RetrieveAPIView):
"""
Retrieve a single VRF
"""
queryset = VRF.objects.all()
queryset = VRF.objects.select_related('tenant')
serializer_class = serializers.VRFSerializer
@ -96,7 +96,7 @@ class PrefixListView(generics.ListAPIView):
"""
List prefixes (filterable)
"""
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
serializer_class = serializers.PrefixSerializer
filter_class = filters.PrefixFilter
@ -105,7 +105,7 @@ class PrefixDetailView(generics.RetrieveAPIView):
"""
Retrieve a single prefix
"""
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
serializer_class = serializers.PrefixSerializer
@ -117,7 +117,7 @@ class IPAddressListView(generics.ListAPIView):
"""
List IP addresses (filterable)
"""
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
.prefetch_related('nat_outside')
serializer_class = serializers.IPAddressSerializer
filter_class = filters.IPAddressFilter
@ -127,7 +127,7 @@ class IPAddressDetailView(generics.RetrieveAPIView):
"""
Retrieve a single IP address
"""
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
.prefetch_related('nat_outside')
serializer_class = serializers.IPAddressSerializer
@ -140,7 +140,7 @@ class VLANGroupListView(generics.ListAPIView):
"""
List all VLAN groups
"""
queryset = VLANGroup.objects.all()
queryset = VLANGroup.objects.select_related('site')
serializer_class = serializers.VLANGroupSerializer
filter_class = filters.VLANGroupFilter
@ -149,7 +149,7 @@ class VLANGroupDetailView(generics.RetrieveAPIView):
"""
Retrieve a single VLAN group
"""
queryset = VLANGroup.objects.all()
queryset = VLANGroup.objects.select_related('site')
serializer_class = serializers.VLANGroupSerializer
@ -161,7 +161,7 @@ class VLANListView(generics.ListAPIView):
"""
List VLANs (filterable)
"""
queryset = VLAN.objects.select_related('site', 'role')
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
serializer_class = serializers.VLANSerializer
filter_class = filters.VLANFilter
@ -170,5 +170,5 @@ class VLANDetailView(generics.RetrieveAPIView):
"""
Retrieve a single VLAN
"""
queryset = VLAN.objects.select_related('site', 'role')
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
serializer_class = serializers.VLANSerializer

View File

@ -2,17 +2,42 @@ import django_filters
from netaddr import IPNetwork
from netaddr.core import AddrFormatError
from django.db.models import Q
from dcim.models import Site, Device, Interface
from tenancy.models import Tenant
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
class VRFFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
name = django_filters.CharFilter(
name='name',
lookup_type='icontains',
label='Name',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
def search(self, queryset, value):
return queryset.filter(
Q(name__icontains=value) |
Q(rd__icontains=value) |
Q(description__icontains=value)
)
class Meta:
model = VRF
@ -20,6 +45,10 @@ class VRFFilter(django_filters.FilterSet):
class AggregateFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
rir_id = django_filters.ModelMultipleChoiceFilter(
name='rir',
queryset=RIR.objects.all(),
@ -36,6 +65,15 @@ class AggregateFilter(django_filters.FilterSet):
model = Aggregate
fields = ['family', 'rir_id', 'rir', 'date_added']
def search(self, queryset, value):
qs_filter = Q(description__icontains=value)
try:
prefix = str(IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except AddrFormatError:
pass
return queryset.filter(qs_filter)
class PrefixFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
@ -55,6 +93,14 @@ class PrefixFilter(django_filters.FilterSet):
action='_vrf',
label='VRF',
)
tenant_id = django_filters.MethodFilter(
action='_tenant_id',
label='Tenant (ID)',
)
tenant = django_filters.MethodFilter(
action='_tenant',
label='Tenant',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
@ -92,12 +138,13 @@ class PrefixFilter(django_filters.FilterSet):
fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
def search(self, queryset, value):
value = value.strip()
qs_filter = Q(description__icontains=value)
try:
query = str(IPNetwork(value).cidr)
return queryset.filter(prefix__net_contains_or_equals=query)
prefix = str(IPNetwork(value.strip()).cidr)
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
except AddrFormatError:
return queryset.none()
pass
return queryset.filter(qs_filter)
def search_by_parent(self, queryset, value):
value = value.strip()
@ -120,6 +167,24 @@ class PrefixFilter(django_filters.FilterSet):
return queryset.filter(vrf__isnull=True)
return queryset.filter(vrf__pk=value)
def _tenant(self, queryset, value):
if str(value) == '':
return queryset
return queryset.filter(
Q(tenant__slug=value) |
Q(tenant__isnull=True, vrf__tenant__slug=value)
)
def _tenant_id(self, queryset, value):
try:
value = int(value)
except ValueError:
return queryset.none()
return queryset.filter(
Q(tenant__pk=value) |
Q(tenant__isnull=True, vrf__tenant__pk=value)
)
class IPAddressFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
@ -135,6 +200,14 @@ class IPAddressFilter(django_filters.FilterSet):
action='_vrf',
label='VRF',
)
tenant_id = django_filters.MethodFilter(
action='_tenant_id',
label='Tenant (ID)',
)
tenant = django_filters.MethodFilter(
action='_tenant',
label='Tenant',
)
device_id = django_filters.ModelMultipleChoiceFilter(
name='interface__device',
queryset=Device.objects.all(),
@ -157,12 +230,13 @@ class IPAddressFilter(django_filters.FilterSet):
fields = ['q', 'family', 'vrf_id', 'vrf', 'device_id', 'device', 'interface_id']
def search(self, queryset, value):
value = value.strip()
qs_filter = Q(description__icontains=value)
try:
query = str(IPNetwork(value))
return queryset.filter(address__net_host=query)
ipaddress = str(IPNetwork(value.strip()))
qs_filter |= Q(address__net_host=ipaddress)
except AddrFormatError:
return queryset.none()
pass
return queryset.filter(qs_filter)
def _vrf(self, queryset, value):
if str(value) == '':
@ -175,6 +249,24 @@ class IPAddressFilter(django_filters.FilterSet):
return queryset.filter(vrf__isnull=True)
return queryset.filter(vrf__pk=value)
def _tenant(self, queryset, value):
if str(value) == '':
return queryset
return queryset.filter(
Q(tenant__slug=value) |
Q(tenant__isnull=True, vrf__tenant__slug=value)
)
def _tenant_id(self, queryset, value):
try:
value = int(value)
except ValueError:
return queryset.none()
return queryset.filter(
Q(tenant__pk=value) |
Q(tenant__isnull=True, vrf__tenant__pk=value)
)
class VLANGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
@ -195,6 +287,10 @@ class VLANGroupFilter(django_filters.FilterSet):
class VLANFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
@ -226,6 +322,17 @@ class VLANFilter(django_filters.FilterSet):
name='vid',
label='VLAN number (1-4095)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=Role.objects.all(),
@ -241,3 +348,11 @@ class VLANFilter(django_filters.FilterSet):
class Meta:
model = VLAN
fields = ['site_id', 'site', 'vid', 'name', 'status', 'role_id', 'role']
def search(self, queryset, value):
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
try:
qs_filter |= Q(vid=int(value))
except ValueError:
pass
return queryset.filter(qs_filter)

View File

@ -0,0 +1,125 @@
[
{
"model": "ipam.aggregate",
"pk": 1,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:20.938Z",
"family": 4,
"prefix": "10.0.0.0/8",
"rir": 6,
"date_added": null,
"description": "Private IPv4 space"
}
},
{
"model": "ipam.aggregate",
"pk": 2,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:32.679Z",
"family": 4,
"prefix": "172.16.0.0/12",
"rir": 6,
"date_added": null,
"description": "Private IPv4 space"
}
},
{
"model": "ipam.aggregate",
"pk": 3,
"fields": {
"created": "2016-08-01",
"last_updated": "2016-08-01T15:22:42.289Z",
"family": 4,
"prefix": "192.168.0.0/16",
"rir": 6,
"date_added": null,
"description": "Private IPv4 space"
}
},
{
"model": "ipam.rir",
"pk": 1,
"fields": {
"name": "ARIN",
"slug": "arin"
}
},
{
"model": "ipam.rir",
"pk": 2,
"fields": {
"name": "RIPE",
"slug": "ripe"
}
},
{
"model": "ipam.rir",
"pk": 3,
"fields": {
"name": "APNIC",
"slug": "apnic"
}
},
{
"model": "ipam.rir",
"pk": 4,
"fields": {
"name": "LACNIC",
"slug": "lacnic"
}
},
{
"model": "ipam.rir",
"pk": 5,
"fields": {
"name": "AFRINIC",
"slug": "afrinic"
}
},
{
"model": "ipam.rir",
"pk": 6,
"fields": {
"name": "RFC 1918",
"slug": "rfc-1918"
}
},
{
"model": "ipam.role",
"pk": 1,
"fields": {
"name": "Production",
"slug": "production",
"weight": 1000
}
},
{
"model": "ipam.role",
"pk": 2,
"fields": {
"name": "Development",
"slug": "development",
"weight": 1000
}
},
{
"model": "ipam.role",
"pk": 3,
"fields": {
"name": "Management",
"slug": "management",
"weight": 1000
}
},
{
"model": "ipam.role",
"pk": 4,
"fields": {
"name": "Backup",
"slug": "backup",
"weight": 1000
}
}
]

View File

@ -4,6 +4,8 @@ from django import forms
from django.db.models import Count
from dcim.models import Site, Device, Interface
from tenancy.forms import bulkedit_tenant_choices
from tenancy.models import Tenant
from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
from .models import (
@ -15,6 +17,18 @@ FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
def bulkedit_vrf_choices():
"""
Include an option to assign the object to the global table.
"""
choices = [
(None, '---------'),
(0, 'Global'),
]
choices += [(v.pk, v.name) for v in VRF.objects.all()]
return choices
#
# VRFs
#
@ -23,7 +37,7 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = VRF
fields = ['name', 'rd', 'enforce_unique', 'description']
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
labels = {
'rd': "RD",
}
@ -33,10 +47,12 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
class VRFFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
class Meta:
model = VRF
fields = ['name', 'rd', 'enforce_unique', 'description']
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
class VRFImportForm(BulkImportForm, BootstrapMixin):
@ -45,9 +61,20 @@ class VRFImportForm(BulkImportForm, BootstrapMixin):
class VRFBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
description = forms.CharField(max_length=100, required=False)
def vrf_tenant_choices():
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vrfs'))
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
class VRFFilterForm(forms.Form, BootstrapMixin):
tenant = forms.MultipleChoiceField(required=False, choices=vrf_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
# RIRs
#
@ -131,7 +158,7 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = Prefix
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'description']
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description']
help_texts = {
'prefix': "IPv4 or IPv6 network",
'vrf': "VRF (if applicable)",
@ -172,6 +199,8 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
class PrefixFromCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
error_messages={'invalid_choice': 'VRF not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Site not found.'})
vlan_group_name = forms.CharField(required=False)
@ -182,7 +211,8 @@ class PrefixFromCSVForm(forms.ModelForm):
class Meta:
model = Prefix
fields = ['prefix', 'vrf', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'description']
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role',
'description']
def clean(self):
@ -228,18 +258,16 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin):
class PrefixBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
help_text="Select the VRF to assign, or check below to remove VRF assignment")
vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
description = forms.CharField(max_length=100, required=False)
def prefix_vrf_choices():
vrf_choices = [('', 'All'), (0, 'Global')]
vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
return vrf_choices
vrf_choices = VRF.objects.annotate(prefix_count=Count('prefixes'))
return [(v.pk, u'{} ({})'.format(v.name, v.prefix_count)) for v in vrf_choices]
def prefix_site_choices():
@ -261,12 +289,16 @@ def prefix_role_choices():
class PrefixFilterForm(forms.Form, BootstrapMixin):
parent = forms.CharField(required=False, label='Search Within')
vrf = forms.ChoiceField(required=False, choices=prefix_vrf_choices, label='VRF')
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices)
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
widget=forms.SelectMultiple(attrs={'size': 6}))
tenant = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False,
widget=forms.SelectMultiple(attrs={'size': 6}))
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices,
widget=forms.SelectMultiple(attrs={'size': 6}))
site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
widget=forms.SelectMultiple(attrs={'size': 6}))
role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
widget=forms.SelectMultiple(attrs={'size': 6}))
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
@ -289,7 +321,7 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'nat_device', 'nat_inside', 'description']
fields = ['address', 'vrf', 'tenant', 'nat_device', 'nat_inside', 'description']
help_texts = {
'address': "IPv4 or IPv6 address and mask",
'vrf': "VRF (if applicable)",
@ -338,6 +370,8 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
class IPAddressFromCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
error_messages={'invalid_choice': 'VRF not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Device not found.'})
interface_name = forms.CharField(required=False)
@ -345,7 +379,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'device', 'interface_name', 'is_primary', 'description']
fields = ['address', 'vrf', 'tenant', 'device', 'interface_name', 'is_primary', 'description']
def clean(self):
@ -390,9 +424,8 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin):
class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
help_text="Select the VRF to assign, or check below to remove VRF assignment")
vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
description = forms.CharField(max_length=100, required=False)
@ -401,14 +434,16 @@ def ipaddress_family_choices():
def ipaddress_vrf_choices():
vrf_choices = [('', 'All'), (0, 'Global')]
vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
return vrf_choices
vrf_choices = VRF.objects.annotate(ipaddress_count=Count('ip_addresses'))
return [(v.pk, u'{} ({})'.format(v.name, v.ipaddress_count)) for v in vrf_choices]
class IPAddressFilterForm(forms.Form, BootstrapMixin):
family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
vrf = forms.ChoiceField(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}))
tenant = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False,
widget=forms.SelectMultiple(attrs={'size': 6}))
#
@ -444,7 +479,7 @@ class VLANForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = VLAN
fields = ['site', 'group', 'vid', 'name', 'description', 'status', 'role']
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
help_texts = {
'site': "The site at which this VLAN exists",
'group': "VLAN group (optional)",
@ -475,13 +510,15 @@ class VLANFromCSVForm(forms.ModelForm):
error_messages={'invalid_choice': 'Device not found.'})
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'VLAN group not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'})
class Meta:
model = VLAN
fields = ['site', 'group', 'vid', 'name', 'status_name', 'role', 'description']
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
def save(self, *args, **kwargs):
m = super(VLANFromCSVForm, self).save(commit=False)
@ -500,6 +537,7 @@ class VLANBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
description = forms.CharField(max_length=100, required=False)
@ -515,6 +553,11 @@ def vlan_group_choices():
return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
def vlan_tenant_choices():
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vlans'))
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
def vlan_status_choices():
status_counts = {}
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
@ -532,6 +575,8 @@ class VLANFilterForm(forms.Form, BootstrapMixin):
widget=forms.SelectMultiple(attrs={'size': 8}))
group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
widget=forms.SelectMultiple(attrs={'size': 8}))
tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-27 14:39
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0001_initial'),
('ipam', '0005_auto_20160725_1842'),
]
operations = [
migrations.AddField(
model_name='vlan',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='vrf',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vrfs', to='tenancy.Tenant'),
),
]

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-28 15:32
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0001_initial'),
('ipam', '0006_vrf_vlan_add_tenant'),
]
operations = [
migrations.AddField(
model_name='ipaddress',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='prefix',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='tenancy.Tenant'),
),
]

View File

@ -7,6 +7,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from dcim.models import Interface
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
from .fields import IPNetworkField, IPAddressField
@ -46,6 +47,7 @@ class VRF(CreatedUpdatedModel):
"""
name = models.CharField(max_length=50)
rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
tenant = models.ForeignKey(Tenant, related_name='vrfs', blank=True, null=True, on_delete=models.PROTECT)
enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space',
help_text="Prevent duplicate prefixes/IP addresses within this VRF")
description = models.CharField(max_length=100, blank=True)
@ -65,6 +67,8 @@ class VRF(CreatedUpdatedModel):
return ','.join([
self.name,
self.rd,
self.tenant.name if self.tenant else '',
'True' if self.enforce_unique else '',
self.description,
])
@ -229,6 +233,7 @@ class Prefix(CreatedUpdatedModel):
site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True)
vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VRF')
tenant = models.ForeignKey(Tenant, related_name='prefixes', blank=True, null=True, on_delete=models.PROTECT)
vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VLAN')
status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
@ -291,7 +296,7 @@ class Prefix(CreatedUpdatedModel):
class IPAddress(CreatedUpdatedModel):
"""
An IPAddress represents an individual IPV4 or IPv6 address and its mask. The mask length should match what is
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
Prefixes, IPAddresses can optionally be assigned to a VRF. An IPAddress can optionally be assigned to an Interface.
Interfaces can have zero or more IPAddresses assigned to them.
@ -304,6 +309,7 @@ class IPAddress(CreatedUpdatedModel):
address = IPAddressField()
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VRF')
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
null=True)
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
@ -407,9 +413,10 @@ class VLAN(CreatedUpdatedModel):
MaxValueValidator(4094)
])
name = models.CharField(max_length=64)
description = models.CharField(max_length=100, blank=True)
tenant = models.ForeignKey(Tenant, related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
description = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['site', 'group', 'vid']
@ -438,6 +445,7 @@ class VLAN(CreatedUpdatedModel):
self.group.name if self.group else '',
str(self.vid),
self.name,
self.tenant.name if self.tenant else '',
self.get_status_display(),
self.role.name if self.role else '',
self.description,

View File

@ -6,8 +6,10 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
RIR_EDIT_LINK = """
{% if perms.ipam.change_rir %}<a href="{% url 'ipam:rir_edit' slug=record.slug %}">Edit</a>{% endif %}
RIR_ACTIONS = """
{% if perms.ipam.change_rir %}
<a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
UTILIZATION_GRAPH = """
@ -15,8 +17,10 @@ UTILIZATION_GRAPH = """
{% utilization_graph record.get_utilization %}
"""
ROLE_EDIT_LINK = """
{% if perms.ipam.change_role %}<a href="{% url 'ipam:role_edit' slug=record.slug %}">Edit</a>{% endif %}
ROLE_ACTIONS = """
{% if perms.ipam.change_role %}
<a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
PREFIX_LINK = """
@ -43,9 +47,19 @@ STATUS_LABEL = """
{% endif %}
"""
VLANGROUP_EDIT_LINK = """
VLANGROUP_ACTIONS = """
{% if perms.ipam.change_vlangroup %}
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}">Edit</a>
<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 %}
"""
TENANT_LINK = """
{% if record.tenant %}
<a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}">{{ record.tenant }}</a>
{% elif record.vrf.tenant %}
<a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}">{{ record.vrf.tenant }}</a>*
{% else %}
&mdash;
{% endif %}
"""
@ -58,11 +72,12 @@ class VRFTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name')
rd = tables.Column(verbose_name='RD')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
description = tables.Column(orderable=False, verbose_name='Description')
class Meta(BaseTable.Meta):
model = VRF
fields = ('pk', 'name', 'rd', 'description')
fields = ('pk', 'name', 'rd', 'tenant', 'description')
#
@ -74,11 +89,11 @@ class RIRTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
aggregate_count = tables.Column(verbose_name='Aggregates')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=RIR_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta):
model = RIR
fields = ('pk', 'name', 'aggregate_count', 'slug', 'edit')
fields = ('pk', 'name', 'aggregate_count', 'slug', 'actions')
#
@ -109,11 +124,11 @@ class RoleTable(BaseTable):
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=ROLE_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta):
model = Role
fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'edit')
fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'actions')
#
@ -124,14 +139,15 @@ class PrefixTable(BaseTable):
pk = ToggleColumn()
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix')
vrf = tables.Column(orderable=False, 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')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
role = tables.Column(verbose_name='Role')
description = tables.Column(orderable=False, verbose_name='Description')
class Meta(BaseTable.Meta):
model = Prefix
fields = ('pk', 'prefix', 'status', 'vrf', 'site', 'role', 'description')
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
class PrefixBriefTable(BaseTable):
@ -143,6 +159,7 @@ class PrefixBriefTable(BaseTable):
class Meta(BaseTable.Meta):
model = Prefix
fields = ('prefix', 'status', 'site', 'role')
orderable = False
#
@ -152,7 +169,8 @@ class PrefixBriefTable(BaseTable):
class IPAddressTable(BaseTable):
pk = ToggleColumn()
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
vrf = tables.Column(orderable=False, 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')
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
verbose_name='Device')
interface = tables.Column(orderable=False, verbose_name='Interface')
@ -160,7 +178,7 @@ class IPAddressTable(BaseTable):
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('pk', 'address', 'vrf', 'device', 'interface', 'description')
fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
class IPAddressBriefTable(BaseTable):
@ -186,11 +204,12 @@ class VLANGroupTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
vlan_count = tables.Column(verbose_name='VLANs')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=VLANGROUP_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = VLANGroup
fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'edit')
fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'actions')
#
@ -203,9 +222,10 @@ class VLANTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
name = tables.Column(verbose_name='Name')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
role = tables.Column(verbose_name='Role')
class Meta(BaseTable.Meta):
model = VLAN
fields = ('pk', 'vid', 'site', 'group', 'name', 'status', 'role')
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role')

View File

@ -36,8 +36,9 @@ def add_available_prefixes(parent, prefix_list):
#
class VRFListView(ObjectListView):
queryset = VRF.objects.all()
queryset = VRF.objects.select_related('tenant')
filter = filters.VRFFilter
filter_form = forms.VRFFilterForm
table = tables.VRFTable
edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
template_name = 'ipam/vrf_list.html'
@ -47,10 +48,11 @@ def vrf(request, pk):
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
prefixes = Prefix.objects.filter(vrf=vrf)
prefix_table = tables.PrefixBriefTable(prefixes)
return render(request, 'ipam/vrf.html', {
'vrf': vrf,
'prefixes': prefixes,
'prefix_table': prefix_table,
})
@ -85,6 +87,10 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
for field in ['description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@ -145,7 +151,7 @@ class AggregateListView(ObjectListView):
if a.prefix.version == 4:
ipv4_total += a.prefix.size
elif a.prefix.version == 6:
ipv6_total += a.prefix.size / 2**64
ipv6_total += a.prefix.size / 2 ** 64
return {
'ipv4_total': ipv4_total,
@ -248,7 +254,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
#
class PrefixListView(ObjectListView):
queryset = Prefix.objects.select_related('site', 'role')
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'role')
filter = filters.PrefixFilter
filter_form = forms.PrefixFilterForm
table = tables.PrefixTable
@ -271,7 +277,8 @@ def prefix(request, pk):
aggregate = None
# Count child IP addresses
ipaddress_count = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix)).count()
ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
.count()
# Parent prefixes table
parent_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contains=str(prefix.prefix))\
@ -336,10 +343,11 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['vrf']:
fields_to_update['vrf'] = form.cleaned_data['vrf']
elif form.cleaned_data['vrf_global']:
fields_to_update['vrf'] = None
for field in ['vrf', 'tenant']:
if form.cleaned_data[field] == 0:
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
for field in ['site', 'status', 'role', 'description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@ -358,7 +366,7 @@ def prefix_ipaddresses(request, pk):
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
# Find all IPAddresses belonging to this Prefix
ipaddresses = IPAddress.objects.filter(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')
ip_table = tables.IPAddressTable(ipaddresses)
@ -378,7 +386,7 @@ def prefix_ipaddresses(request, pk):
#
class IPAddressListView(ObjectListView):
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
queryset = IPAddress.objects.select_related('vrf__tenant', 'interface__device')
filter = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm
table = tables.IPAddressTable
@ -460,10 +468,11 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['vrf']:
fields_to_update['vrf'] = form.cleaned_data['vrf']
elif form.cleaned_data['vrf_global']:
fields_to_update['vrf'] = None
for field in ['vrf', 'tenant']:
if form.cleaned_data[field] == 0:
fields_to_update[field] = None
elif form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
for field in ['description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@ -520,10 +529,11 @@ def vlan(request, pk):
vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan)
prefix_table = tables.PrefixBriefTable(prefixes)
return render(request, 'ipam/vlan.html', {
'vlan': vlan,
'prefixes': prefixes,
'prefix_table': prefix_table,
})
@ -558,6 +568,10 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
if form.cleaned_data['tenant'] == 0:
fields_to_update['tenant'] = None
elif form.cleaned_data['tenant']:
fields_to_update['tenant'] = form.cleaned_data['tenant']
for field in ['site', 'group', 'status', 'role', 'description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]

View File

@ -12,7 +12,7 @@ except ImportError:
"the documentation.")
VERSION = '1.3.2'
VERSION = '1.4.0'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@ -108,6 +108,7 @@ INSTALLED_APPS = (
'ipam',
'extras',
'secrets',
'tenancy',
'users',
'utilities',
)

View File

@ -22,6 +22,7 @@ urlpatterns = [
url(r'^dcim/', include('dcim.urls', namespace='dcim')),
url(r'^ipam/', include('ipam.urls', namespace='ipam')),
url(r'^secrets/', include('secrets.urls', namespace='secrets')),
url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')),
url(r'^profile/', include('users.urls', namespace='users')),
# API
@ -29,6 +30,7 @@ urlpatterns = [
url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')),
url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
url(r'^api/docs/', include('rest_framework_swagger.urls')),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),

View File

@ -5,16 +5,20 @@ from django.shortcuts import render
from circuits.models import Provider, Circuit
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
from extras.models import UserAction
from ipam.models import Aggregate, Prefix, IPAddress, VLAN
from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF
from secrets.models import Secret
from tenancy.models import Tenant
def home(request):
stats = {
# DCIM
# Organization
'site_count': Site.objects.count(),
'tenant_count': Tenant.objects.count(),
# DCIM
'rack_count': Rack.objects.count(),
'device_count': Device.objects.count(),
'interface_connections_count': InterfaceConnection.objects.count(),
@ -22,6 +26,7 @@ def home(request):
'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
# IPAM
'vrf_count': VRF.objects.count(),
'aggregate_count': Aggregate.objects.count(),
'prefix_count': Prefix.objects.count(),
'ipaddress_count': IPAddress.objects.count(),

View File

@ -1,10 +1,16 @@
import django_filters
from django.db.models import Q
from .models import Secret, SecretRole
from dcim.models import Device
class SecretFilter(django_filters.FilterSet):
q = django_filters.MethodFilter(
action='search',
label='Search',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=SecretRole.objects.all(),
@ -26,3 +32,9 @@ class SecretFilter(django_filters.FilterSet):
class Meta:
model = Secret
fields = ['name', 'role_id', 'role', 'device']
def search(self, queryset, value):
return queryset.filter(
Q(name__icontains=value) |
Q(device__name__icontains=value)
)

View File

@ -0,0 +1,42 @@
[
{
"model": "secrets.secretrole",
"pk": 1,
"fields": {
"name": "Login Credentials",
"slug": "login-credentials",
"users": [],
"groups": []
}
},
{
"model": "secrets.secretrole",
"pk": 2,
"fields": {
"name": "RADIUS Key",
"slug": "radius-key",
"users": [],
"groups": []
}
},
{
"model": "secrets.secretrole",
"pk": 3,
"fields": {
"name": "SNMPv2 Community",
"slug": "snmpv2-community",
"users": [],
"groups": []
}
},
{
"model": "secrets.secretrole",
"pk": 4,
"fields": {
"name": "SNMPv3 Credentials",
"slug": "snmpv3-credentials",
"users": [],
"groups": []
}
}
]

View File

@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
from .models import SecretRole, Secret
SECRETROLE_EDIT_LINK = """
SECRETROLE_ACTIONS = """
{% if perms.secrets.change_secretrole %}
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}">Edit</a>
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@ -22,11 +22,12 @@ class SecretRoleTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
secret_count = tables.Column(verbose_name='Secrets')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=SECRETROLE_EDIT_LINK, verbose_name='')
actions = tables.TemplateColumn(template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
model = SecretRole
fields = ('pk', 'name', 'secret_count', 'slug', 'edit')
fields = ('pk', 'name', 'secret_count', 'slug', 'actions')
#

View File

@ -13,7 +13,7 @@
<div class="panel panel-danger" style="margin-top: 200px">
<div class="panel-heading">
<strong>
<i class="glyphicon glyphicon-warning-sign"></i>
<i class="fa fa-warning"></i>
Server Error
</strong>
</div>

View File

@ -24,173 +24,182 @@
<div id="navbar" class="navbar-collapse collapse">
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
<ul class="nav navbar-nav">
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' %} active{% endif %}">
{% if perms.dcim.add_site %}
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Sites <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:site_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Sites</a></li>
<li><a href="{% url 'dcim:site_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Site</a></li>
<li><a href="{% url 'dcim:site_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Sites</a></li>
</ul>
{% else %}
<a href="{% url 'dcim:site_list' %}">Sites</a>
{% endif %}
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' or 'tenancy' in request.path %} 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>
{% 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>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</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>
{% 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>
{% 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>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/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="glyphicon glyphicon-search" aria-hidden="true"></i> Racks</a></li>
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
{% if perms.dcim.add_rack %}
<li><a href="{% url 'dcim:rack_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Rack</a></li>
<li><a href="{% url 'dcim:rack_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Racks</a></li>
<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>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Rack Groups</a></li>
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Groups</a></li>
{% if perms.dcim.add_rackgroup %}
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/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="glyphicon glyphicon-search" aria-hidden="true"></i> Devices</a></li>
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
{% if perms.dcim.add_device %}
<li><a href="{% url 'dcim:device_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device</a></li>
<li><a href="{% url 'dcim:device_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Devices</a></li>
<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>
{% 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="glyphicon glyphicon-search" aria-hidden="true"></i> Device Types</a></li>
<li><a href="{% url 'dcim:devicetype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Types</a></li>
{% if perms.dcim.add_devicetype %}
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device Type</a></li>
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Type</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Device Roles</a></li>
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Roles</a></li>
{% if perms.dcim.add_devicerole %}
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device Role</a></li>
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus" aria-hidden="true"></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="glyphicon glyphicon-search" aria-hidden="true"></i> Manufacturers</a></li>
<li><a href="{% url 'dcim:manufacturer_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Manufacturers</a></li>
{% if perms.dcim.add_manufacturer %}
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Manufacturer</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="glyphicon glyphicon-search" aria-hidden="true"></i> Platforms</a></li>
<li><a href="{% url 'dcim:platform_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Platforms</a></li>
{% if perms.dcim.add_platform %}
<li><a href="{% url 'dcim:platform_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Platform</a></li>
<li><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Platform</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/dcim/console-connections/' or request.path|startswith:'/dcim/power-connections/' or request.path|startswith:'/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="glyphicon glyphicon-search" aria-hidden="true"></i> Console Connections</a></li>
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li>
{% if perms.dcim.change_consoleport %}
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Console Connections</a></li>
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></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="glyphicon glyphicon-search" aria-hidden="true"></i> Power Connections</a></li>
<li><a href="{% url 'dcim:power_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Power Connections</a></li>
{% if perms.dcim.change_powerport %}
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Power Connections</a></li>
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></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="glyphicon glyphicon-search" aria-hidden="true"></i> Interface Connections</a></li>
<li><a href="{% url 'dcim:interface_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Interface Connections</a></li>
{% if perms.dcim.add_interfaceconnection %}
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Interface Connections</a></li>
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Interface Connections</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/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="glyphicon glyphicon-search" aria-hidden="true"></i> IP Addresses</a></li>
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li>
{% if perms.ipam.add_ipaddress %}
<li><a href="{% url 'ipam:ipaddress_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add an IP</a></li>
<li><a href="{% url 'ipam:ipaddress_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import IPs</a></li>
<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>
{% 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="glyphicon glyphicon-search" aria-hidden="true"></i> Prefixes</a></li>
<li><a href="{% url 'ipam:prefix_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefixes</a></li>
{% if perms.ipam.add_prefix %}
<li><a href="{% url 'ipam:prefix_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Prefix</a></li>
<li><a href="{% url 'ipam:prefix_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Prefixes</a></li>
<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>
{% 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="glyphicon glyphicon-search" aria-hidden="true"></i> Aggregates</a></li>
<li><a href="{% url 'ipam:aggregate_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Aggregates</a></li>
{% if perms.ipam.add_aggregate %}
<li><a href="{% url 'ipam:aggregate_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
<li><a href="{% url 'ipam:aggregate_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Aggregates</a></li>
<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>
{% 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="glyphicon glyphicon-search" aria-hidden="true"></i> VRFs</a></li>
<li><a href="{% url 'ipam:vrf_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VRFs</a></li>
{% if perms.ipam.add_vrf %}
<li><a href="{% url 'ipam:vrf_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VRF</a></li>
<li><a href="{% url 'ipam:vrf_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VRFs</a></li>
<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>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:rir_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> RIRs</a></li>
<li><a href="{% url 'ipam:rir_list' %}"><i class="fa fa-search" aria-hidden="true"></i> RIRs</a></li>
{% if perms.ipam.add_rir %}
<li><a href="{% url 'ipam:rir_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a RIR</a></li>
<li><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus" aria-hidden="true"></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="glyphicon glyphicon-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li>
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li>
{% if perms.ipam.add_role %}
<li><a href="{% url 'ipam:role_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Role</a></li>
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/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="glyphicon glyphicon-search" aria-hidden="true"></i> VLANs</a></li>
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
{% if perms.ipam.add_vlan %}
<li><a href="{% url 'ipam:vlan_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VLAN</a></li>
<li><a href="{% url 'ipam:vlan_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VLANs</a></li>
<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>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLAN Groups</a></li>
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Groups</a></li>
{% if perms.ipam.add_vlangroup %}
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|startswith:'/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="glyphicon glyphicon-search" aria-hidden="true"></i> Providers</a></li>
<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="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Provider</a></li>
<li><a href="{% url 'circuits:provider_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Providers</a></li>
<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="glyphicon glyphicon-search" aria-hidden="true"></i> Circuits</a></li>
<li><a href="{% url 'circuits:circuit_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuits</a></li>
{% if perms.circuits.add_circuit %}
<li><a href="{% url 'circuits:circuit_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Circuit</a></li>
<li><a href="{% url 'circuits:circuit_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Circuits</a></li>
<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>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Circuit Types</a></li>
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuit Types</a></li>
{% if perms.circuits.add_circuittype %}
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
{% endif %}
</ul>
</li>
@ -198,14 +207,14 @@
<li class="dropdown{% if request.path|startswith:'/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="glyphicon glyphicon-search" aria-hidden="true"></i> Secrets</a></li>
<li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li>
{% if perms.secrets.add_secret %}
<li><a href="{% url 'secrets:secret_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Secrets</a></li>
<li><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Secrets</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Secret Roles</a></li>
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secret Roles</a></li>
{% if perms.secrets.add_secretrole %}
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
{% endif %}
</ul>
</li>
@ -215,12 +224,12 @@
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
{% if request.user.is_staff %}
<li><a href="{% url 'admin:index' %}"><i class="glyphicon glyphicon-cog" aria-hidden="true"></i> Admin</a></li>
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs" aria-hidden="true"></i> Admin</a></li>
{% endif %}
<li><a href="{% url 'users:profile' %}"><i class="glyphicon glyphicon-user" aria-hidden="true"></i> Profile</a></li>
<li><a href="{% url 'logout' %}"><i class="glyphicon glyphicon-log-out" aria-hidden="true"></i> Log out</a></li>
<li><a href="{% url 'users:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> Log out</a></li>
{% else %}
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="glyphicon glyphicon-log-in" aria-hidden="true"></i> Log in</a></li>
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
{% endif %}
</ul>
</div>

View File

@ -1,24 +1,24 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}{{ circuit.provider }} Circuit {{ circuit.cid }}{% endblock %}
{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'dcim:site' slug=circuit.site.slug %}">{{ circuit.site }}</a></li>
<li><a href="{% url 'circuits:circuit_list' %}?site={{ circuit.site.slug }}">Circuits</a></li>
<li>{{ circuit }}</li>
<li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
<li><a href="{% url 'circuits:circuit_list' %}?provider={{ circuit.provider.slug }}">{{ circuit.provider }}</a></li>
<li>{{ circuit.cid }}</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'circuits:circuit_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Circuit ID" />
<input type="text" name="q" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
@ -28,18 +28,18 @@
<div class="pull-right">
{% if perms.circuits.change_circuit %}
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this circuit
</a>
{% endif %}
{% if perms.circuits.delete_circuit %}
<a href="{% url 'circuits:circuit_delete' pk=circuit.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this circuit
</a>
{% endif %}
</div>
<h1>{{ circuit.provider }} Circuit {{ circuit.cid }}</h1>
<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
@ -57,6 +57,67 @@
<td>Circuit ID</td>
<td>{{ circuit.cid }}</td>
</tr>
<tr>
<td>Type</td>
<td><a href="{{ circuit.type.get_absolute_url }}">{{ circuit.type }}</a></td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if circuit.tenant %}
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Install Date</td>
<td>
{% if circuit.install_date %}
{{ circuit.install_date }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Port Speed</td>
<td>
{% if circuit.port_speed %}
{{ circuit.port_speed_human }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Commit Rate</td>
<td>
{% if circuit.commit_speed %}
{{ circuit.commit_speed_human }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Created</td>
<td>{{ circuit.created }}</td>
</tr>
<tr>
<td>Last Updated</td>
<td>{{ circuit.last_updated }}</td>
</tr>
</table>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Termination</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<td>Site</td>
<td>
@ -73,44 +134,34 @@
{% endif %}
</td>
</tr>
<tr>
<td>Install Date</td>
<td>{{ circuit.install_date }}</td>
</tr>
<tr>
<td>Port Speed</td>
<td>{{ circuit.port_speed_human }}</td>
</tr>
<tr>
<td>Commit Rate</td>
<td>{% if circuit.commit_rate %}{{ circuit.commit_rate_human }}{% else %}<span class="text-muted">N/A</span>{% endif %}</td>
</tr>
<tr>
<td>Cross-Connect</td>
<td>{{ circuit.xconnect_id }}</td>
<td>
{% if circuit.xconnect_id %}
{{ circuit.xconnect_id }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Patch Panel/Port</td>
<td>{{ circuit.pp_info }}</td>
</tr>
<tr>
<td>Created</td>
<td>{{ circuit.created }}</td>
</tr>
<tr>
<td>Last Updated</td>
<td>{{ circuit.last_updated }}</td>
<td>
{% if circuit.pp_info %}
{{ circuit.pp_info }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if circuit.comments %}
{% if circuit.comments %}
{{ circuit.comments|gfm }}
{% else %}
<span class="text-muted">None</span>

View File

@ -9,13 +9,19 @@
{% render_field form.provider %}
{% render_field form.cid %}
{% render_field form.type %}
{% render_field form.tenant %}
{% render_field form.install_date %}
{% render_field form.port_speed %}
{% render_field form.commit_rate %}
{% render_field form.xconnect_id %}
{% render_field form.pp_info %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Bandwidth</strong></div>
<div class="panel-body">
{% render_field form.port_speed %}
{% render_field form.commit_rate %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Termination</strong></div>
<div class="panel-body">

View File

@ -43,6 +43,11 @@
<td>Circuit type</td>
<td>Transit</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Strickland Propane</td>
</tr>
<tr>
<td>Site</td>
<td>Site name</td>
@ -76,7 +81,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>IC-603122,TeliaSonera,Transit,ASH-4,2016-02-23,10000,2000,937649,PP8371 ports 13/14</pre>
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,ASH-4,2016-02-23,10000,2000,937649,PP8371 ports 13/14</pre>
</div>
</div>
{% endblock %}

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.circuits.add_circuit %}
<a href="{% url 'circuits:circuit_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a circuit
</a>
{% endif %}
@ -19,23 +19,7 @@
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'circuits:circuit_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.circuits.add_circuittype %}
<a href="{% url 'circuits:circuittype_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a circuit type
</a>
{% endif %}

View File

@ -6,27 +6,41 @@
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
<li>{{ provider }}</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'circuits:provider_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
<div class="pull-right">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider_graphs' pk=provider.pk %}" title="Show graphs">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
Graphs
</button>
{% if show_graphs %}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider_graphs' pk=provider.pk %}" title="Show graphs">
<i class="fa fa-signal" aria-hidden="true"></i>
Graphs
</button>
{% endif %}
{% if perms.circuits.change_provider %}
<a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this provider
</a>
{% endif %}
{% if perms.circuits.delete_provider %}
<a href="{% url 'circuits:provider_delete' slug=provider.slug %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this provider
</a>
{% endif %}
@ -41,25 +55,53 @@
<table class="table table-hover panel-body">
<tr>
<td>ASN</td>
<td>{{ provider.asn }}</td>
<td>
{% if provider.asn %}
{{ provider.asn }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Account</td>
<td>{{ provider.account }}</td>
<td>
{% if provider.account %}
{{ provider.account }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Customer Portal</td>
<td>
<a href="{{ provider.portal_url }}">{{ provider.portal_url }}</a>
{% if provider.portal_url %}
<a href="{{ provider.portal_url }}">{{ provider.portal_url }}</a>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>NOC Contact</td>
<td>{{ provider.noc_contact|linebreaksbr }}</td>
<td>
{% if provider.noc_contact %}
{{ provider.noc_contact|linebreaksbr }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Admin Contact</td>
<td>{{ provider.admin_contact|linebreaksbr }}</td>
<td>
{% if provider.admin_contact %}
{{ provider.admin_contact|linebreaksbr }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Created</td>
@ -76,7 +118,7 @@
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if provider.comments %}
{% if provider.comments %}
{{ provider.comments|gfm }}
{% else %}
<span class="text-muted">None</span>

View File

@ -6,7 +6,7 @@
<div class="pull-right">
{% if perms.circuits.add_provider %}
<a href="{% url 'circuits:provider_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a provider
</a>
{% endif %}
@ -18,23 +18,7 @@
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'circuits:provider_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.change_consoleport %}
<a href="{% url 'dcim:console_connections_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import connections
</a>
{% endif %}

View File

@ -14,6 +14,16 @@
<strong>Device</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<td>Tenant</td>
<td>
{% if device.tenant %}
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Site</td>
<td>
@ -55,7 +65,7 @@
{% if device.serial %}
<span>{{ device.serial }}</span>
{% else %}
<span class="text-muted">Not defined</span>
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
@ -86,7 +96,7 @@
{% if device.platform %}
<span>{{ device.platform }}</span>
{% else %}
<span class="text-warning">Not assigned</span>
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
@ -111,7 +121,7 @@
<span>(NAT: {{ device.primary_ip4.nat_outside.address.ip }})</span>
{% endif %}
{% else %}
<span class="text-muted">Not defined</span>
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
@ -126,7 +136,7 @@
<span>(NAT: {{ device.primary_ip6.nat_outside.address.ip }})</span>
{% endif %}
{% else %}
<span class="text-muted">Not defined</span>
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
@ -257,7 +267,7 @@
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if device.comments %}
{% if device.comments %}
{{ device.comments|gfm }}
{% else %}
<span class="text-muted">None</span>

View File

@ -9,6 +9,7 @@
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
<td>{{ device.device_type }}</td>
<td>{{ device.device_role }}</td>
<td>{{ device.tenant }}</td>
<td>{{ device.serial }}</td>
</tr>
{% endfor %}

View File

@ -7,6 +7,7 @@
<div class="panel-body">
{% render_field form.name %}
{% render_field form.device_role %}
{% render_field form.tenant %}
</div>
</div>
<div class="panel panel-default">

View File

@ -36,6 +36,11 @@
<td>Functional role of device</td>
<td>ToR Switch</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr>
<td>Device manufacturer</td>
<td>Hardware manufacturer</td>
@ -79,7 +84,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>rack101_sw1,ToR Switch,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear</pre>
<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear</pre>
</div>
</div>
{% endblock %}

View File

@ -107,7 +107,7 @@
</div>
{% if perms.dcim.add_module %}
<a href="{% url 'dcim:module_add' pk=device.pk %}" class="btn btn-success">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a Module
</a>
{% endif %}

View File

@ -7,11 +7,11 @@
<div class="pull-right">
{% if perms.dcim.add_device %}
<a href="{% url 'dcim:device_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a device
</a>
<a href="{% url 'dcim:device_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import devices
</a>
{% endif %}
@ -23,24 +23,7 @@
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'dcim:device_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Name or serial" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_devicerole %}
<a href="{% url 'dcim:devicerole_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a device role
</a>
{% endif %}

View File

@ -19,13 +19,13 @@
<div class="pull-right">
{% if perms.dcim.change_devicetype %}
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this device type
</a>
{% endif %}
{% if perms.dcim.delete_devicetype %}
<a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this device type
</a>
{% endif %}

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_devicetype %}
<a href="{% url 'dcim:devicetype_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a device type
</a>
{% endif %}

View File

@ -16,10 +16,10 @@
<div class="col-md-3">
<form action="{% url 'dcim:device_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Device name or serial" />
<input type="text" name="q" class="form-control" placeholder="Search devices" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>

View File

@ -34,10 +34,12 @@
</td>
{% endif %}
<td class="text-right">
{% if iface.circuit or iface.connection %}
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
</button>
{% if show_graphs %}
{% if iface.circuit or iface.connection %}
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
</button>
{% endif %}
{% endif %}
{% if perms.dcim.change_interface %}
{% if iface.is_physical %}

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_interfaceconnection %}
<a href="{% url 'dcim:interface_connections_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import connections
</a>
{% endif %}

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_manufacturer %}
<a href="{% url 'dcim:manufacturer_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a manufacturer
</a>
{% endif %}

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_platform %}
<a href="{% url 'dcim:platform_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a platform
</a>
{% endif %}

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.change_powerport %}
<a href="{% url 'dcim:power_connections_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import connections
</a>
{% endif %}

View File

@ -8,18 +8,18 @@
<div class="row">
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'dcim:site' slug=rack.site.slug %}">{{ rack.site }}</a></li>
<li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">Racks</a></li>
<li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
<li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">{{ rack.site }}</a></li>
<li>{{ rack }}</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'dcim:rack_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Rack name or ID" />
<input type="text" name="q" class="form-control" placeholder="Search racks" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
@ -29,25 +29,25 @@
<div class="pull-right">
{% if prev_rack %}
<a href="{% url 'dcim:rack' pk=prev_rack.pk %}" class="btn btn-primary">
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
<span class="fa fa-chevron-left" aria-hidden="true"></span>
Previous Rack
</a>
{% endif %}
{% if next_rack %}
<a href="{% url 'dcim:rack' pk=next_rack.pk %}" class="btn btn-primary">
<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
<span class="fa fa-chevron-right" aria-hidden="true"></span>
Next Rack
</a>
{% endif %}
{% if perms.dcim.change_rack %}
<a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this rack
</a>
{% endif %}
{% if perms.dcim.delete_rack %}
<a href="{% url 'dcim:rack_delete' pk=rack.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this rack
</a>
{% endif %}
@ -81,6 +81,16 @@
<td>
{% if rack.facility_id %}
<span>{{ rack.facility_id }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if rack.tenant %}
<a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
@ -146,7 +156,7 @@
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if rack.comments %}
{% if rack.comments %}
{{ rack.comments|gfm }}
{% else %}
<span class="text-muted">None</span>

View File

@ -9,6 +9,7 @@
<td><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack }}</a></td>
<td>{{ rack.facility_id }}</td>
<td>{{ rack.site }}</td>
<td>{{ rack.tenant }}</td>
<td>{{ rack.u_height }}</td>
</tr>
{% endfor %}

View File

@ -9,6 +9,7 @@
{% render_field form.group %}
{% render_field form.name %}
{% render_field form.facility_id %}
{% render_field form.tenant %}
{% render_field form.u_height %}
</div>
</div>

View File

@ -48,6 +48,11 @@
<td>Rack ID assigned by the facility (optional)</td>
<td>J12.100</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr>
<td>Height</td>
<td>Height in rack units</td>
@ -56,7 +61,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>DC-4,Cage 1400,R101,J12.100,42</pre>
<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,42</pre>
</div>
</div>
{% endblock %}

View File

@ -7,11 +7,11 @@
<div class="pull-right">
{% if perms.dcim.add_rack %}
<a href="{% url 'dcim:rack_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a rack
</a>
<a href="{% url 'dcim:rack_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import racks
</a>
{% endif %}
@ -23,24 +23,7 @@
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'dcim:rack_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_rackgroup %}
<a href="{% url 'dcim:rackgroup_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a rack group
</a>
{% endif %}

View File

@ -16,10 +16,10 @@
<div class="col-md-3">
<form action="{% url 'dcim:site_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search" />
<input type="text" name="q" class="form-control" placeholder="Search sites" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
@ -27,38 +27,62 @@
</div>
</div>
<div class="pull-right">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site_graphs' pk=site.pk %}" title="Show graphs">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
Graphs
</button>
{% if show_graphs %}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site_graphs' pk=site.pk %}" title="Show graphs">
<i class="fa fa-signal" aria-hidden="true"></i>
Graphs
</button>
{% endif %}
{% if perms.dcim.change_site %}
<a href="{% url 'dcim:site_edit' slug=site.slug %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this site
</a>
{% endif %}
{% if perms.dcim.delete_site %}
<a href="{% url 'dcim:site_delete' slug=site.slug %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this site
</a>
{% endif %}
</div>
<h1>{{ site.name }}</h1>
<div class="row">
<div class="col-md-6">
<div class="col-md-7">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Site</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<td>Tenant</td>
<td>
{% if site.tenant %}
<a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Facility</td>
<td>{{ site.facility }}</td>
<td>
{% if site.facility %}
{{ site.facility }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>AS Number</td>
<td>{{ site.asn }}</td>
<td>
{% if site.asn %}
{{ site.asn }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Physical Address</td>
@ -69,8 +93,10 @@
<i class="glyphicon glyphicon-map-marker"></i> Map it
</a>
</div>
<span>{{ site.physical_address|linebreaksbr }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
<span>{{ site.physical_address|linebreaksbr }}</span>
</td>
</tr>
<tr>
@ -79,7 +105,7 @@
{% if site.shipping_address %}
<span>{{ site.shipping_address|linebreaksbr }}</span>
{% else %}
<span class="text-muted">See physical address</span>
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
@ -98,7 +124,7 @@
<strong>Comments</strong>
</div>
<div class="panel-body">
{% if site.comments %}
{% if site.comments %}
{{ site.comments|gfm }}
{% else %}
<span class="text-muted">None</span>
@ -106,43 +132,33 @@
</div>
</div>
</div>
<div class="col-md-6">
<div class="col-md-5">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Stats</strong>
</div>
<table class="table table-hover panel-body">
<tr>
<td>Racks</td>
<td>
<a href="{% url 'dcim:rack_list' %}?site={{ site.slug }}">{{ stats.rack_count }}</a>
</td>
</tr>
<tr>
<td>Devices</td>
<td>
<a href="{% url 'dcim:device_list' %}?site={{ site.slug }}">{{ stats.device_count }}</a>
</td>
</tr>
<tr>
<td>Prefixes</td>
<td>
<a href="{% url 'ipam:prefix_list' %}?site={{ site.slug }}">{{ stats.prefix_count }}</a>
</td>
</tr>
<tr>
<td>VLANs</td>
<td>
<a href="{% url 'ipam:vlan_list' %}?site={{ site.slug }}">{{ stats.vlan_count }}</a>
</td>
</tr>
<tr>
<td>Circuits</td>
<td>
<a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}">{{ stats.circuit_count }}</a>
</td>
</tr>
</table>
<div class="row panel-body">
<div class="col-md-4 text-center">
<h2><a href="{% url 'dcim:rack_list' %}?site={{ site.slug }}" class="btn {% if stats.rack_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.rack_count }}</a></h2>
<p>Racks</p>
</div>
<div class="col-md-4 text-center">
<h2><a href="{% url 'dcim:device_list' %}?site={{ site.slug }}" class="btn {% if stats.device_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.device_count }}</a></h2>
<p>Devices</p>
</div>
<div class="col-md-4 text-center">
<h2><a href="{% url 'ipam:prefix_list' %}?site={{ site.slug }}" class="btn {% if stats.prefix_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
<p>Prefixes</p>
</div>
<div class="col-md-4 text-center">
<h2><a href="{% url 'ipam:vlan_list' %}?site={{ site.slug }}" class="btn {% if stats.vlan_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vlan_count }}</a></h2>
<p>VLANs</p>
</div>
<div class="col-md-4 text-center">
<h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
<p>Circuits</p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">

View File

@ -0,0 +1,13 @@
{% extends 'utilities/bulk_edit_form.html' %}
{% load form_helpers %}
{% block title %}Site Bulk Edit{% endblock %}
{% block select_objects_table %}
{% for site in selected_objects %}
<tr>
<td><a href="{% url 'dcim:site' slug=site.slug %}">{{ site.slug }}</a></td>
<td>{{ site.tenant }}</td>
</tr>
{% endfor %}
{% endblock %}

View File

@ -7,6 +7,7 @@
<div class="panel-body">
{% render_field form.name %}
{% render_field form.slug %}
{% render_field form.tenant %}
{% render_field form.facility %}
{% render_field form.asn %}
{% render_field form.physical_address %}

View File

@ -38,6 +38,11 @@
<td>URL-friendly name</td>
<td>ash4-south</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Pied Piper</td>
</tr>
<tr>
<td>Facility</td>
<td>Name of the hosting facility (optional)</td>
@ -51,7 +56,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>ASH-4 South,ash4-south,Equinix DC6,65000</pre>
<pre>ASH-4 South,ash4-south,Pied Piper,Equinix DC6,65000</pre>
</div>
</div>
{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}Sites{% endblock %}
@ -7,36 +6,24 @@
<div class="pull-right">
{% if perms.dcim.add_site %}
<a href="{% url 'dcim:site_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a site
</a>
<a href="{% url 'dcim:site_import' %}" class="btn btn-info">
<span class="fa fa-download" aria-hidden="true"></span>
Import sites
</a>
{% endif %}
{% include 'inc/export_button.html' with obj_type='sites' %}
</div>
<h1>Sites</h1>
<div class="row">
<div class="col-md-9">
{% render_table table 'table.html' %}
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'dcim:site_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -6,10 +6,10 @@
<div class="col-md-4">
<form action="{% url 'dcim:device_list' %}" method="get">
<div class="input-group input-group-lg">
<input type="text" name="q" placeholder="Device name or serial" class="form-control" />
<input type="text" name="q" placeholder="Search devices" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
Devices
</button>
</span>
@ -20,11 +20,11 @@
<div class="col-md-4">
<form action="{% url 'ipam:prefix_list' %}" method="get">
<div class="input-group input-group-lg">
<input type="text" name="q" placeholder="IP or network" class="form-control" />
<input type="text" name="q" placeholder="Search prefixes" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
IP
<span class="fa fa-search" aria-hidden="true"></span>
Prefixes
</button>
</span>
</div>
@ -34,10 +34,10 @@
<div class="col-md-4">
<form action="{% url 'circuits:circuit_list' %}" method="get">
<div class="input-group input-group-lg">
<input type="text" name="q" placeholder="Circuit ID" class="form-control" />
<input type="text" name="q" placeholder="Search circuits" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
Circuits
</button>
</span>
@ -50,7 +50,7 @@
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<strong>DCIM</strong>
<strong>Organization</strong>
</div>
<div class="list-group">
<div class="list-group-item">
@ -58,6 +58,18 @@
<h4 class="list-group-item-heading"><a href="{% url 'dcim:site_list' %}">Sites</a></h4>
<p class="list-group-item-text text-muted">Geographic locations</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.tenant_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></h4>
<p class="list-group-item-text text-muted">Customers or departments</p>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>DCIM</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.rack_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'dcim:rack_list' %}">Racks</a></h4>
@ -79,20 +91,6 @@
</div>
</div>
</div>
{% if perms.secrets %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Secrets</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.secret_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'secrets:secret_list' %}">Secrets</a></h4>
<p class="list-group-item-text text-muted">Sensitive data (such as passwords) which has been stored securely</p>
</div>
</div>
</div>
{% endif %}
</div>
<div class="col-md-4">
<div class="panel panel-default">
@ -100,6 +98,11 @@
<strong>IPAM</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.vrf_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:vrf_list' %}">VRFs</a></h4>
<p class="list-group-item-text text-muted">Virtual routing and forwarding tables</p>
</div>
<div class="list-group-item">
<span class="badge pull-right">{{ stats.aggregate_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></h4>
@ -141,6 +144,20 @@
</div>
</div>
<div class="col-md-4">
{% if perms.secrets %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Secrets</strong>
</div>
<div class="list-group">
<div class="list-group-item">
<span class="badge pull-right">{{ stats.secret_count }}</span>
<h4 class="list-group-item-heading"><a href="{% url 'secrets:secret_list' %}">Secrets</a></h4>
<p class="list-group-item-text text-muted">Sensitive data (such as passwords) which has been stored securely</p>
</div>
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Recent Activity</strong>

View File

@ -7,7 +7,7 @@
<h1>Import Completed</h1>
{% render_table table %}
<a href="{{ request.path }}" class="btn btn-primary">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import more
</a>
{% endblock %}

View File

@ -1,7 +1,7 @@
{% if export_templates %}
<div class="btn-group">
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-export" aria-hidden="true"></span>
<span class="fa fa-upload" aria-hidden="true"></span>
Export {{ obj_type }} <span class="caret"></span>
</button>
<ul class="dropdown-menu">
@ -14,7 +14,7 @@
</div>
{% else %}
<a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}export" class="btn btn-success">
<span class="glyphicon glyphicon-export" aria-hidden="true"></span>
<span class="fa fa-upload" aria-hidden="true"></span>
Export {{ obj_type }}
</a>
{% endif %}

View File

@ -2,7 +2,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-filter" aria-hidden="true"></span>
<span class="fa fa-filter" aria-hidden="true"></span>
<strong>Filter</strong>
</div>
<div class="panel-body">
@ -19,7 +19,7 @@
{% endfor %}
<div class="text-right">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span> Apply filters
<span class="fa fa-search" aria-hidden="true"></span> Apply filters
</button>
</div>
</form>

View File

@ -0,0 +1,18 @@
<div class="panel panel-default">
<div class="panel-heading">
<span class="fa fa-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="." method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>

View File

@ -5,24 +5,36 @@
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></li>
<li><a href="{% url 'ipam:aggregate_list' %}?rir={{ aggregate.rir.slug }}">{{ aggregate.rir }}</a></li>
<li>{{ aggregate }}</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'ipam:aggregate_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search aggregates" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
<div class="pull-right">
{% if perms.ipam.change_aggregate %}
<a href="{% url 'ipam:aggregate_edit' pk=aggregate.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this aggregate
</a>
{% endif %}
{% if perms.ipam.delete_aggregate %}
<a href="{% url 'ipam:aggregate_delete' pk=aggregate.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this aggregate
</a>
{% endif %}
@ -51,17 +63,17 @@
{% if aggregate.date_added %}
<span>{{ aggregate.date_added }}</span>
{% else %}
<span class="text-muted">Not defined</span>
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Description</td>
<td>
{% if aggregate.description %}
{% if aggregate.description %}
<span>{{ aggregate.description }}</span>
{% else %}
<span class="text-muted">None</span>
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>

View File

@ -8,7 +8,7 @@
<div class="pull-right">
{% if perms.ipam.add_aggregate %}
<a href="{% url 'ipam:aggregate_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add an aggregate
</a>
{% endif %}
@ -22,6 +22,7 @@
<p class="text-right">IPv6 total: <strong>{{ ipv6_total|intcomma }} /64s</strong></p>
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

View File

@ -1,11 +1,9 @@
<div class="row">
<div class="col-md-9">
<ol class="breadcrumb">
{% if prefix.site %}
<li><a href="{% url 'dcim:site' slug=prefix.site.slug %}">{{ prefix.site }}</a></li>
<li><a href="{% url 'ipam:prefix_list' %}?site={{ prefix.site.slug }}">Prefixes</a></li>
{% else %}
<li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
<li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
{% if prefix.vrf %}
<li><a href="{% url 'ipam:prefix_list' %}?vrf={{ prefix.vrf.pk }}">{{ prefix.vrf }}</a></li>
{% endif %}
<li>{{ prefix }}</li>
</ol>
@ -13,10 +11,10 @@
<div class="col-md-3">
<form action="{% url 'ipam:prefix_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Network or IP" />
<input type="text" name="q" class="form-control" placeholder="Search prefixes" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
@ -26,19 +24,19 @@
<div class="pull-right">
{% if perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ prefix.prefix }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-success">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add an IP Address
</a>
{% endif %}
{% if perms.ipam.change_prefix %}
<a href="{% url 'ipam:prefix_edit' pk=prefix.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this prefix
</a>
{% endif %}
{% if perms.ipam.delete_prefix %}
<a href="{% url 'ipam:prefix_delete' pk=prefix.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this prefix
</a>
{% endif %}

View File

@ -7,21 +7,20 @@
<div class="row">
<div class="col-md-9">
<ol class="breadcrumb">
{% for p in parent_prefixes %}
<li><a href="{% url 'ipam:prefix' pk=p.pk %}">{{ p }}</a></li>
{% empty %}
<li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
{% endfor %}
<li>{{ ipaddress.address.ip }}</li>
<li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
{% if ipaddress.vrf %}
<li><a href="{% url 'ipam:ipaddress_list' %}?vrf={{ ipaddress.vrf.pk }}">{{ ipaddress.vrf }}</a></li>
{% endif %}
<li>{{ ipaddress }}</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'ipam:ipaddress_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="IP address" />
<input type="text" name="q" class="form-control" placeholder="Search IPs" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
@ -31,13 +30,13 @@
<div class="pull-right">
{% if perms.ipam.change_ipaddress %}
<a href="{% url 'ipam:ipaddress_edit' pk=ipaddress.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this IP
</a>
{% endif %}
{% if perms.ipam.delete_ipaddress %}
<a href="{% url 'ipam:ipaddress_delete' pk=ipaddress.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this IP
</a>
{% endif %}
@ -65,15 +64,28 @@
</td>
</tr>
<tr>
<td>Description</td>
<td>Tenant</td>
<td>
{% if ipaddress.description %}
<span>{{ ipaddress.description }}</span>
{% if ipaddress.tenant %}
<a href="{{ ipaddress.tenant.get_absolute_url }}">{{ ipaddress.tenant }}</a>
{% elif ipaddress.vrf.tenant %}
<a href="{{ ipaddress.vrf.tenant.get_absolute_url }}">{{ ipaddress.vrf.tenant }}</a>
<label class="label label-info">Inherited</label>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Description</td>
<td>
{% if ipaddress.description %}
<span>{{ ipaddress.description }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Assignment</td>
<td>

View File

@ -7,7 +7,8 @@
{% for ipaddress in selected_objects %}
<tr>
<td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
<td>{{ ipaddress.vrf }}</td>
<td>{{ ipaddress.vrf|default:"Global" }}</td>
<td>{{ ipaddress.tenant }}</td>
<td>{{ ipaddress.interface.device }}</td>
<td>{{ ipaddress.interface }}</td>
<td>{{ ipaddress.description }}</td>

View File

@ -8,6 +8,7 @@
<div class="panel-body">
{% render_field form.address %}
{% render_field form.vrf %}
{% render_field form.tenant %}
{% if obj %}
<div class="form-group">
<label class="col-md-3 control-label">Device</label>

View File

@ -38,6 +38,11 @@
<td>VRF route distinguisher (optional)</td>
<td>65000:123</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>ABC01</td>
</tr>
<tr>
<td>Device</td>
<td>Device name (optional)</td>
@ -61,7 +66,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>192.0.2.42/24,65000:123,switch12,ge-0/0/31,True,Management IP</pre>
<pre>192.0.2.42/24,65000:123,ABC01,switch12,ge-0/0/31,True,Management IP</pre>
</div>
</div>
{% endblock %}

View File

@ -8,11 +8,11 @@
<div class="pull-right">
{% if perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add an IP
</a>
<a href="{% url 'ipam:ipaddress_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import IPs
</a>
{% endif %}
@ -24,24 +24,7 @@
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'ipam:ipaddress_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="IP address" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

View File

@ -26,6 +26,19 @@
{% endif %}
</td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if prefix.tenant %}
<a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
{% elif prefix.vrf.tenant %}
<a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
<label class="label label-info">Inherited</label>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Aggregate</td>
<td>
@ -42,7 +55,7 @@
{% if prefix.site %}
<a href="{% url 'dcim:site' slug=prefix.site.slug %}">{{ prefix.site }}</a>
{% else %}
<span class="text-muted">Not assigned</span>
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
@ -52,7 +65,7 @@
{% if prefix.vlan %}
<a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a>
{% else %}
<span class="text-muted">Not assigned</span>
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
@ -64,15 +77,21 @@
</tr>
<tr>
<td>Role</td>
<td>{{ prefix.role }}</td>
<td>
{% if prefix.role %}
<span>{{ prefix.role }}</span>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Description</td>
<td>
{% if prefix.description %}
{% if prefix.description %}
<span>{{ prefix.description }}</span>
{% else %}
<span class="text-muted">None</span>
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
@ -108,7 +127,7 @@
{% include 'utilities/obj_table.html' with table=child_prefix_table table_template='panel_table.html' heading='Child Prefixes' parent=prefix bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
{% elif prefix.new_subnet %}
<a href="{% url 'ipam:prefix_add' %}?prefix={{ prefix.new_subnet }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.site %}&site={{ prefix.site.pk }}{% endif %}" class="btn btn-success">
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add Child Prefix
<i class="fa fa-plus" aria-hidden="true"></i> Add Child Prefix
</a>
{% endif %}
</div>

View File

@ -8,6 +8,7 @@
<tr>
<td><a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a></td>
<td>{{ prefix.vrf|default:"Global" }}</td>
<td>{{ prefix.tenant }}</td>
<td>{{ prefix.site }}</td>
<td>{{ prefix.status }}</td>
<td>{{ prefix.role }}</td>

View File

@ -38,6 +38,11 @@
<td>VRF route distinguisher (optional)</td>
<td>65000:123</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>ABC01</td>
</tr>
<tr>
<td>Site</td>
<td>Name of assigned site (optional)</td>
@ -71,7 +76,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>192.168.42.0/24,65000:123,HQ,Customers,801,Active,Customer,7th floor WiFi</pre>
<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,7th floor WiFi</pre>
</div>
</div>
{% endblock %}

View File

@ -8,11 +8,11 @@
<div class="pull-right">
{% if perms.ipam.add_prefix %}
<a href="{% url 'ipam:prefix_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a prefix
</a>
<a href="{% url 'ipam:prefix_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import prefixes
</a>
{% endif %}
@ -24,24 +24,7 @@
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search</strong>
</div>
<div class="panel-body">
<form action="{% url 'ipam:prefix_list' %}" method="get">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Network" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.ipam.add_rir %}
<a href="{% url 'ipam:rir_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a RIR
</a>
{% endif %}

View File

@ -7,7 +7,7 @@
<div class="pull-right">
{% if perms.dcim.add_devicerole %}
<a href="{% url 'ipam:role_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a role
</a>
{% endif %}

View File

@ -7,18 +7,21 @@
<div class="row">
<div class="col-md-9">
<ol class="breadcrumb">
<li><a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a></li>
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">VLANs</a></li>
<li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
{% if vlan.group %}
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}&group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li>
{% endif %}
<li>{{ vlan.name }} ({{ vlan.vid }})</li>
</ol>
</div>
<div class="col-md-3">
<form action="{% url 'ipam:vlan_list' %}" method="get">
<div class="input-group">
<input type="text" name="vid" class="form-control" placeholder="VLAN ID search" />
<input type="text" name="q" class="form-control" placeholder="Search VLANs" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<span class="fa fa-search" aria-hidden="true"></span>
</button>
</span>
</div>
@ -28,13 +31,13 @@
<div class="pull-right">
{% if perms.ipam.change_vlan %}
<a href="{% url 'ipam:vlan_edit' pk=vlan.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this VLAN
</a>
{% endif %}
{% if perms.ipam.delete_vlan %}
<a href="{% url 'ipam:vlan_delete' pk=vlan.pk %}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this VLAN
</a>
{% endif %}
@ -70,10 +73,10 @@
<td>{{ vlan.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>Tenant</td>
<td>
{% if vlan.description %}
{{ vlan.description }}
{% if vlan.tenant %}
<a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
@ -87,7 +90,23 @@
</tr>
<tr>
<td>Role</td>
<td>{{ vlan.role }}</td>
<td>
{% if vlan.role %}
<span>{{ vlan.role }}</span>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Description</td>
<td>
{% if vlan.description %}
{{ vlan.description }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
</tr>
<tr>
<td>Created</td>
@ -105,26 +124,7 @@
<div class="panel-heading">
<strong>Prefixes</strong>
</div>
{% if prefixes %}
<table class="table table-hover panel-body">
{% for p in prefixes %}
<tr>
<td>
<a href="{% url 'ipam:prefix' pk=p.pk %}">{{ p }}</a>
</td>
<td>
{% if p.site %}
<a href="{% url 'dcim:site' slug=p.site.slug %}">{{ p.site }}</a>
{% endif %}
</td>
<td>{{ p.get_status_display }}</td>
<td>{{ p.role }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="panel-body text-muted">None</div>
{% endif %}
{% render_table prefix_table %}
</div>
</div>
</div>

View File

@ -9,7 +9,8 @@
<td><a href="{% url 'ipam:vlan' pk=vlan.pk %}">{{ vlan.vid }}</a></td>
<td>{{ vlan.name }}</td>
<td>{{ vlan.site }}</td>
<td>{{ vlan.status }}</td>
<td>{{ vlan.tenant }}</td>
<td>{{ vlan.get_status_display }}</td>
<td>{{ vlan.role }}</td>
<td>{{ vlan.description }}</td>
</tr>

View File

@ -48,6 +48,11 @@
<td>Configured VLAN name</td>
<td>Cameras</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Internal</td>
</tr>
<tr>
<td>Status</td>
<td>Current status</td>
@ -66,7 +71,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>LAS2,Backend Network,1400,Cameras,Active,Security,Security team only</pre>
<pre>LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only</pre>
</div>
</div>
{% endblock %}

View File

@ -8,11 +8,11 @@
<div class="pull-right">
{% if perms.ipam.add_vlan %}
<a href="{% url 'ipam:vlan_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="fa fa-plus" aria-hidden="true"></span>
Add a VLAN
</a>
<a href="{% url 'ipam:vlan_import' %}" class="btn btn-info">
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
<span class="fa fa-download" aria-hidden="true"></span>
Import VLANs
</a>
{% endif %}
@ -24,24 +24,7 @@
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
<strong>Search by ID</strong>
</div>
<div class="panel-body">
<form action="{% url 'ipam:vlan_list' %}" method="get">
<div class="input-group">
<input type="text" name="vid" class="form-control" placeholder="VLAN ID" {% if request.GET.vid %}value="{{ request.GET.vid }}" {% endif %}/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</span>
</div>
</form>
</div>
</div>
{% include 'inc/search_panel.html' %}
{% include 'inc/filter_panel.html' %}
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More