mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
commit
88dace75a1
@ -86,3 +86,9 @@ One IP address can be designated as the network address translation (NAT) IP add
|
||||
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094). Note that while it is good practice, neither VLAN names nor IDs must be unique within a site. This is to accommodate the fact that many real-world network use less-than-optimal VLAN allocations and may have overlapping VLAN ID assignments in practice.
|
||||
|
||||
Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
|
||||
|
||||
---
|
||||
|
||||
# Services
|
||||
|
||||
A service represents a TCP or UDP service available on a device. Each service must be defined with a name, protocol, and port number; for example, SSH (TCP/22). A service may optionally be bound to one or more specific IP addresses belonging to a device. (If no IP addresses are bound, the service is assumed to be reachable via any IP address.)
|
||||
|
@ -195,7 +195,7 @@ Starting development server at http://0.0.0.0:8000/
|
||||
Quit the server with CONTROL-C.
|
||||
```
|
||||
|
||||
Now if we navigate to the name or IP of the server (as defined in `ALLOWED_HOSTS`) we should be greeted with the NetBox home page. Note that this built-in web service is for development and testing purposes only. It is not suited for production use.
|
||||
Now if we navigate to the name or IP of the server (as defined in `ALLOWED_HOSTS`) we should be greeted with the NetBox home page. Note that this built-in web service is for development and testing purposes only. **It is not suited for production use.**
|
||||
|
||||
!!! warning
|
||||
If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected.
|
||||
|
@ -1,4 +1,4 @@
|
||||
NetBox requires a PostgreSQL database to store data. MySQL is not supported, as NetBox leverage's PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/9.1/static/datatype-net-types.html).
|
||||
NetBox requires a PostgreSQL database to store data. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/9.1/static/datatype-net-types.html).)
|
||||
|
||||
# Installation
|
||||
|
||||
@ -15,7 +15,7 @@ NetBox requires a PostgreSQL database to store data. MySQL is not supported, as
|
||||
# postgresql-setup initdb
|
||||
```
|
||||
|
||||
If using CentOS, modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/data/pg_hba.conf`. For example:
|
||||
CentOS users should modify the PostgreSQL configuration to accept password-based authentication by replacing `ident` with `md5` for all host entries within `/var/lib/pgsql/data/pg_hba.conf`. For example:
|
||||
|
||||
```no-highlight
|
||||
host all all 127.0.0.1/32 md5
|
||||
|
@ -101,7 +101,7 @@ To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https
|
||||
|
||||
# gunicorn Installation
|
||||
|
||||
Save the following configuration file in the root netbox installation path (in this example, `/opt/netbox/`) as `gunicorn_config.py`. Be sure to verify the location of the gunicorn executable (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL change the username from `www-data` to `nginx` or `apache`.
|
||||
Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
|
||||
|
||||
```no-highlight
|
||||
command = '/usr/bin/gunicorn'
|
||||
@ -113,7 +113,7 @@ user = 'www-data'
|
||||
|
||||
# supervisord Installation
|
||||
|
||||
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed.
|
||||
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
|
||||
|
||||
```no-highlight
|
||||
[program:netbox]
|
||||
|
@ -21,11 +21,9 @@ class CircuitTypeAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Circuit)
|
||||
class CircuitAdmin(admin.ModelAdmin):
|
||||
list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed_human',
|
||||
'upstream_speed_human', 'commit_rate_human', 'xconnect_id']
|
||||
list_display = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate_human']
|
||||
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', 'tenant', 'site')
|
||||
return qs.select_related('provider', 'type', 'tenant')
|
||||
|
@ -1,6 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import Provider, CircuitType, Circuit
|
||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||
from extras.api.serializers import CustomFieldSerializer
|
||||
from tenancy.api.serializers import TenantNestedSerializer
|
||||
@ -45,17 +45,24 @@ class CircuitTypeNestedSerializer(CircuitTypeSerializer):
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
provider = ProviderNestedSerializer()
|
||||
type = CircuitTypeNestedSerializer()
|
||||
tenant = TenantNestedSerializer()
|
||||
class CircuitTerminationSerializer(serializers.ModelSerializer):
|
||||
site = SiteNestedSerializer()
|
||||
interface = InterfaceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['id', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info']
|
||||
|
||||
|
||||
class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
provider = ProviderNestedSerializer()
|
||||
type = CircuitTypeNestedSerializer()
|
||||
tenant = TenantNestedSerializer()
|
||||
terminations = CircuitTerminationSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
|
||||
'upstream_speed', 'commit_rate', 'xconnect_id', 'comments', 'custom_fields']
|
||||
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'comments', 'terminations', 'custom_fields']
|
||||
|
||||
|
||||
class CircuitNestedSerializer(CircuitSerializer):
|
||||
|
@ -43,7 +43,7 @@ class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView):
|
||||
"""
|
||||
List circuits (filterable)
|
||||
"""
|
||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\
|
||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
|
||||
.prefetch_related('custom_field_values__field')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filter_class = CircuitFilter
|
||||
@ -53,6 +53,6 @@ class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single circuit
|
||||
"""
|
||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\
|
||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\
|
||||
.prefetch_related('custom_field_values__field')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
|
@ -16,12 +16,12 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='circuits__site',
|
||||
name='circuits__terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='circuits__site',
|
||||
name='circuits__terminations__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@ -29,7 +29,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['q', 'name', 'account', 'asn']
|
||||
fields = ['name', 'account', 'asn']
|
||||
|
||||
def search(self, queryset, value):
|
||||
return queryset.filter(
|
||||
@ -50,7 +50,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Provider (ID)',
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
name='provider',
|
||||
name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Provider (slug)',
|
||||
@ -61,7 +61,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Circuit type (ID)',
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
name='type',
|
||||
name='type__slug',
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Circuit type (slug)',
|
||||
@ -78,12 +78,12 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
name='terminations__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
name='terminations__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@ -91,12 +91,11 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date']
|
||||
fields = ['install_date']
|
||||
|
||||
def search(self, queryset, value):
|
||||
return queryset.filter(
|
||||
Q(cid__icontains=value) |
|
||||
Q(xconnect_id__icontains=value) |
|
||||
Q(pp_info__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
@ -9,7 +9,7 @@ from utilities.forms import (
|
||||
SlugField,
|
||||
)
|
||||
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
|
||||
|
||||
#
|
||||
@ -43,7 +43,7 @@ class ProviderFromCSVForm(forms.ModelForm):
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url']
|
||||
|
||||
|
||||
class ProviderImportForm(BulkImportForm, BootstrapMixin):
|
||||
class ProviderImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=ProviderFromCSVForm)
|
||||
|
||||
|
||||
@ -69,7 +69,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# Circuit types
|
||||
#
|
||||
|
||||
class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
|
||||
class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -82,6 +82,64 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
|
||||
#
|
||||
|
||||
class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['cid', 'type', 'provider', 'tenant', 'install_date', 'commit_rate', 'comments']
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'install_date': "Format: YYYY-MM-DD",
|
||||
'commit_rate': "Committed rate",
|
||||
}
|
||||
|
||||
|
||||
class CircuitFromCSVForm(forms.ModelForm):
|
||||
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
||||
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.'})
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate']
|
||||
|
||||
|
||||
class CircuitImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
||||
|
||||
|
||||
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||
comments = CommentField(widget=SmallTextarea)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant', 'commit_rate', 'comments']
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Circuit
|
||||
type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug')
|
||||
provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug')
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
||||
to_field_name='slug')
|
||||
|
||||
|
||||
#
|
||||
# Circuit terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
|
||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
@ -95,28 +153,25 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
||||
disabled_indicator='is_connected'))
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
|
||||
'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
|
||||
]
|
||||
model = CircuitTermination
|
||||
fields = ['term_side', 'site', 'rack', 'device', 'livesearch', 'interface', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info']
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'install_date': "Format: YYYY-MM-DD",
|
||||
'port_speed': "Physical circuit speed",
|
||||
'commit_rate': "Commited rate",
|
||||
'xconnect_id': "ID of the local cross-connect",
|
||||
'pp_info': "Patch panel ID and port number(s)"
|
||||
}
|
||||
widgets = {
|
||||
'term_side': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(CircuitForm, self).__init__(*args, **kwargs)
|
||||
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# If this circuit has been assigned to an interface, initialize rack and device
|
||||
# If an interface has been assigned, initialize rack and device
|
||||
if self.instance.interface:
|
||||
self.initial['rack'] = self.instance.interface.device.rack
|
||||
self.initial['device'] = self.instance.interface.device
|
||||
@ -140,11 +195,13 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||
# Limit interface choices
|
||||
if self.is_bound and self.data.get('device'):
|
||||
interfaces = Interface.objects.filter(device=self.data['device'])\
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
||||
'connected_as_b')
|
||||
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
||||
elif self.initial.get('device'):
|
||||
interfaces = Interface.objects.filter(device=self.initial['device'])\
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
||||
'connected_as_b')
|
||||
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||
else:
|
||||
interfaces = []
|
||||
@ -154,47 +211,3 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
|
||||
}) for iface in interfaces
|
||||
]
|
||||
|
||||
|
||||
class CircuitFromCSVForm(forms.ModelForm):
|
||||
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
||||
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', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed',
|
||||
'commit_rate', 'xconnect_id', 'pp_info']
|
||||
|
||||
|
||||
class CircuitImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
||||
|
||||
|
||||
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
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.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
|
||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||
comments = CommentField(widget=SmallTextarea)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant', 'port_speed', 'commit_rate', 'comments']
|
||||
|
||||
|
||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Circuit
|
||||
type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug')
|
||||
provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug')
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('circuits')), to_field_name='slug')
|
||||
|
99
netbox/circuits/migrations/0006_terminations.py
Normal file
99
netbox/circuits/migrations/0006_terminations.py
Normal file
@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-12-13 16:30
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def circuits_to_terms(apps, schema_editor):
|
||||
Circuit = apps.get_model('circuits', 'Circuit')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
for c in Circuit.objects.all():
|
||||
CircuitTermination(
|
||||
circuit=c,
|
||||
term_side=b'A',
|
||||
site=c.site,
|
||||
interface=c.interface,
|
||||
port_speed=c.port_speed,
|
||||
upstream_speed=c.upstream_speed,
|
||||
xconnect_id=c.xconnect_id,
|
||||
pp_info=c.pp_info,
|
||||
).save()
|
||||
|
||||
|
||||
def terms_to_circuits(apps, schema_editor):
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
for ct in CircuitTermination.objects.filter(term_side='A'):
|
||||
c = ct.circuit
|
||||
c.site = ct.site
|
||||
c.interface = ct.interface
|
||||
c.port_speed = ct.port_speed
|
||||
c.upstream_speed = ct.upstream_speed
|
||||
c.xconnect_id = ct.xconnect_id
|
||||
c.pp_info = ct.pp_info
|
||||
c.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0022_color_names_to_rgb'),
|
||||
('circuits', '0005_circuit_add_upstream_speed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CircuitTermination',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('term_side', models.CharField(choices=[(b'A', b'A'), (b'Z', b'Z')], max_length=1,
|
||||
verbose_name='Termination')),
|
||||
('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')),
|
||||
('upstream_speed',
|
||||
models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed',
|
||||
null=True, verbose_name=b'Upstream speed (Kbps)')),
|
||||
('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')),
|
||||
('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')),
|
||||
('circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations',
|
||||
to='circuits.Circuit')),
|
||||
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_termination', to='dcim.Interface')),
|
||||
('site',
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations',
|
||||
to='dcim.Site')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['circuit', 'term_side'],
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='circuittermination',
|
||||
unique_together=set([('circuit', 'term_side')]),
|
||||
),
|
||||
migrations.RunPython(circuits_to_terms, terms_to_circuits),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='interface',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='port_speed',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='pp_info',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='site',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='upstream_speed',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='circuit',
|
||||
name='xconnect_id',
|
||||
),
|
||||
]
|
@ -3,12 +3,35 @@ from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
|
||||
from dcim.fields import ASNField
|
||||
from dcim.models import Site, Interface
|
||||
from extras.models import CustomFieldModel, CustomFieldValue
|
||||
from tenancy.models import Tenant
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
|
||||
|
||||
TERM_SIDE_A = 'A'
|
||||
TERM_SIDE_Z = 'Z'
|
||||
TERM_SIDE_CHOICES = (
|
||||
(TERM_SIDE_A, 'A'),
|
||||
(TERM_SIDE_Z, 'Z'),
|
||||
)
|
||||
|
||||
|
||||
def humanize_speed(speed):
|
||||
"""
|
||||
Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps')
|
||||
"""
|
||||
if speed >= 1000000000 and speed % 1000000000 == 0:
|
||||
return '{} Tbps'.format(speed / 1000000000)
|
||||
elif speed >= 1000000 and speed % 1000000 == 0:
|
||||
return '{} Gbps'.format(speed / 1000000)
|
||||
elif speed >= 1000 and speed % 1000 == 0:
|
||||
return '{} Mbps'.format(speed / 1000)
|
||||
elif speed >= 1000:
|
||||
return '{} Mbps'.format(float(speed) / 1000)
|
||||
else:
|
||||
return '{} Kbps'.format(speed)
|
||||
|
||||
|
||||
class Provider(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||
@ -71,15 +94,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
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')
|
||||
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
|
||||
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
||||
help_text='Upstream speed, if different from port speed')
|
||||
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
|
||||
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
|
||||
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
@ -99,42 +115,61 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
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),
|
||||
str(self.upstream_speed),
|
||||
str(self.commit_rate) if self.commit_rate else '',
|
||||
self.xconnect_id,
|
||||
self.pp_info,
|
||||
])
|
||||
|
||||
def _humanize_speed(self, speed):
|
||||
"""
|
||||
Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps')
|
||||
"""
|
||||
if speed >= 1000000000 and speed % 1000000000 == 0:
|
||||
return '{} Tbps'.format(speed / 1000000000)
|
||||
elif speed >= 1000000 and speed % 1000000 == 0:
|
||||
return '{} Gbps'.format(speed / 1000000)
|
||||
elif speed >= 1000 and speed % 1000 == 0:
|
||||
return '{} Mbps'.format(speed / 1000)
|
||||
elif speed >= 1000:
|
||||
return '{} Mbps'.format(float(speed) / 1000)
|
||||
else:
|
||||
return '{} Kbps'.format(speed)
|
||||
def _get_termination(self, side):
|
||||
for ct in self.terminations.all():
|
||||
if ct.term_side == side:
|
||||
return ct
|
||||
return None
|
||||
|
||||
@property
|
||||
def termination_a(self):
|
||||
return self._get_termination('A')
|
||||
|
||||
@property
|
||||
def termination_z(self):
|
||||
return self._get_termination('Z')
|
||||
|
||||
def commit_rate_human(self):
|
||||
return '' if not self.commit_rate else humanize_speed(self.commit_rate)
|
||||
commit_rate_human.admin_order_field = 'commit_rate'
|
||||
|
||||
|
||||
class CircuitTermination(models.Model):
|
||||
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
|
||||
term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
|
||||
site = models.ForeignKey('dcim.Site', related_name='circuit_terminations', on_delete=models.PROTECT)
|
||||
interface = models.OneToOneField('dcim.Interface', related_name='circuit_termination', blank=True, null=True)
|
||||
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
|
||||
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
||||
help_text='Upstream speed, if different from port speed')
|
||||
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
|
||||
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
|
||||
|
||||
class Meta:
|
||||
ordering = ['circuit', 'term_side']
|
||||
unique_together = ['circuit', 'term_side']
|
||||
|
||||
def __unicode__(self):
|
||||
return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.circuit.get_absolute_url()
|
||||
|
||||
def get_peer_termination(self):
|
||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||
try:
|
||||
return CircuitTermination.objects.select_related('site').get(circuit=self.circuit, term_side=peer_side)
|
||||
except CircuitTermination.DoesNotExist:
|
||||
return None
|
||||
|
||||
def port_speed_human(self):
|
||||
return self._humanize_speed(self.port_speed)
|
||||
return humanize_speed(self.port_speed)
|
||||
port_speed_human.admin_order_field = 'port_speed'
|
||||
|
||||
def upstream_speed_human(self):
|
||||
if not self.upstream_speed:
|
||||
return ''
|
||||
return self._humanize_speed(self.upstream_speed)
|
||||
return '' if not self.upstream_speed else humanize_speed(self.upstream_speed)
|
||||
upstream_speed_human.admin_order_field = 'upstream_speed'
|
||||
|
||||
def commit_rate_human(self):
|
||||
if not self.commit_rate:
|
||||
return ''
|
||||
return self._humanize_speed(self.commit_rate)
|
||||
commit_rate_human.admin_order_field = 'commit_rate'
|
||||
|
@ -56,12 +56,13 @@ class CircuitTable(BaseTable):
|
||||
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 = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'),
|
||||
verbose_name='Port Speed')
|
||||
a_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_a.site'), orderable=False,
|
||||
args=[Accessor('termination_a.site.slug')])
|
||||
z_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_z.site'), orderable=False,
|
||||
args=[Accessor('termination_z.site.slug')])
|
||||
commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'),
|
||||
verbose_name='Commit Rate')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Circuit
|
||||
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed', 'commit_rate')
|
||||
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'commit_rate')
|
||||
|
@ -30,5 +30,11 @@ urlpatterns = [
|
||||
url(r'^circuits/(?P<pk>\d+)/$', views.circuit, name='circuit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
|
||||
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
||||
|
||||
# Circuit terminations
|
||||
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
|
||||
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
||||
|
||||
]
|
||||
|
@ -1,14 +1,18 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
|
||||
from . import filters, forms, tables
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
|
||||
|
||||
|
||||
#
|
||||
@ -27,7 +31,7 @@ class ProviderListView(ObjectListView):
|
||||
def provider(request, slug):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device')
|
||||
circuits = Circuit.objects.filter(provider=provider)
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
@ -103,7 +107,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class CircuitListView(ObjectListView):
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant', 'site')
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||
filter = filters.CircuitFilter
|
||||
filter_form = forms.CircuitFilterForm
|
||||
table = tables.CircuitTable
|
||||
@ -114,9 +118,13 @@ class CircuitListView(ObjectListView):
|
||||
def circuit(request, pk):
|
||||
|
||||
circuit = get_object_or_404(Circuit, pk=pk)
|
||||
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
|
||||
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
|
||||
|
||||
return render(request, 'circuits/circuit.html', {
|
||||
'circuit': circuit,
|
||||
'termination_a': termination_a,
|
||||
'termination_z': termination_z,
|
||||
})
|
||||
|
||||
|
||||
@ -124,7 +132,7 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
model = Circuit
|
||||
form_class = forms.CircuitForm
|
||||
fields_initial = ['site']
|
||||
fields_initial = ['provider']
|
||||
template_name = 'circuits/circuit_edit.html'
|
||||
obj_list_url = 'circuits:circuit_list'
|
||||
|
||||
@ -155,3 +163,71 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuit'
|
||||
cls = Circuit
|
||||
default_redirect_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
@permission_required('circuits.change_circuittermination')
|
||||
def circuit_terminations_swap(request, pk):
|
||||
|
||||
circuit = get_object_or_404(Circuit, pk=pk)
|
||||
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
|
||||
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
|
||||
if not termination_a and not termination_z:
|
||||
messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
if termination_a and termination_z:
|
||||
# Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint
|
||||
with transaction.atomic():
|
||||
termination_a.term_side = '_'
|
||||
termination_a.save()
|
||||
termination_z.term_side = 'A'
|
||||
termination_z.save()
|
||||
termination_a.term_side = 'Z'
|
||||
termination_a.save()
|
||||
elif termination_a:
|
||||
termination_a.term_side = 'Z'
|
||||
termination_a.save()
|
||||
else:
|
||||
termination_z.term_side = 'A'
|
||||
termination_z.save()
|
||||
messages.success(request, "Swapped terminations for circuit {}.".format(circuit))
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'circuits/circuit_terminations_swap.html', {
|
||||
'circuit': circuit,
|
||||
'termination_a': termination_a,
|
||||
'termination_z': termination_z,
|
||||
'form': form,
|
||||
'panel_class': 'default',
|
||||
'button_class': 'primary',
|
||||
'cancel_url': circuit.get_absolute_url(),
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# Circuit terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuittermination'
|
||||
model = CircuitTermination
|
||||
form_class = forms.CircuitTerminationForm
|
||||
fields_initial = ['term_side']
|
||||
template_name = 'circuits/circuittermination_edit.html'
|
||||
|
||||
def alter_obj(self, obj, args, kwargs):
|
||||
if 'circuit' in kwargs:
|
||||
circuit = get_object_or_404(Circuit, pk=kwargs['circuit'])
|
||||
obj.circuit = circuit
|
||||
return obj
|
||||
|
||||
|
||||
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'circuits.delete_circuittermination'
|
||||
model = CircuitTermination
|
||||
|
@ -20,8 +20,9 @@ class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
|
||||
'custom_fields', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
||||
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes',
|
||||
'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
||||
|
||||
|
||||
class SiteNestedSerializer(SiteSerializer):
|
||||
@ -130,14 +131,14 @@ class ManufacturerNestedSerializer(ManufacturerSerializer):
|
||||
# Device types
|
||||
#
|
||||
|
||||
class DeviceTypeSerializer(serializers.ModelSerializer):
|
||||
class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
manufacturer = ManufacturerNestedSerializer()
|
||||
subdevice_role = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role']
|
||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields']
|
||||
|
||||
def get_subdevice_role(self, obj):
|
||||
return {
|
||||
@ -197,8 +198,9 @@ class DeviceTypeDetailSerializer(DeviceTypeSerializer):
|
||||
|
||||
class Meta(DeviceTypeSerializer.Meta):
|
||||
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||
'is_console_server', 'is_pdu', 'is_network_device', 'console_port_templates', 'cs_port_templates',
|
||||
'power_port_templates', 'power_outlet_templates', 'interface_templates']
|
||||
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
|
||||
'console_port_templates', 'cs_port_templates', 'power_port_templates', 'power_outlet_templates',
|
||||
'interface_templates']
|
||||
|
||||
|
||||
#
|
||||
@ -381,7 +383,7 @@ class InterfaceNestedSerializer(InterfaceSerializer):
|
||||
|
||||
|
||||
class InterfaceDetailSerializer(InterfaceSerializer):
|
||||
connected_interface = InterfaceSerializer(source='get_connected_interface')
|
||||
connected_interface = InterfaceSerializer()
|
||||
|
||||
class Meta(InterfaceSerializer.Meta):
|
||||
fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected',
|
||||
|
@ -118,7 +118,11 @@ class RackUnitListView(APIView):
|
||||
|
||||
rack = get_object_or_404(Rack, pk=pk)
|
||||
face = request.GET.get('face', 0)
|
||||
elevation = rack.get_rack_units(face)
|
||||
try:
|
||||
exclude = int(request.GET.get('exclude', None))
|
||||
except ValueError:
|
||||
exclude = None
|
||||
elevation = rack.get_rack_units(face, exclude)
|
||||
|
||||
# Serialize Devices within the rack elevation
|
||||
for u in elevation:
|
||||
@ -152,20 +156,20 @@ class ManufacturerDetailView(generics.RetrieveAPIView):
|
||||
# Device Types
|
||||
#
|
||||
|
||||
class DeviceTypeListView(generics.ListAPIView):
|
||||
class DeviceTypeListView(CustomFieldModelAPIView, generics.ListAPIView):
|
||||
"""
|
||||
List device types (filterable)
|
||||
"""
|
||||
queryset = DeviceType.objects.select_related('manufacturer')
|
||||
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('custom_field_values__field')
|
||||
serializer_class = serializers.DeviceTypeSerializer
|
||||
filter_class = filters.DeviceTypeFilter
|
||||
|
||||
|
||||
class DeviceTypeDetailView(generics.RetrieveAPIView):
|
||||
class DeviceTypeDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single device type
|
||||
"""
|
||||
queryset = DeviceType.objects.select_related('manufacturer')
|
||||
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('custom_field_values__field')
|
||||
serializer_class = serializers.DeviceTypeDetailSerializer
|
||||
|
||||
|
||||
@ -451,7 +455,7 @@ class RelatedConnectionsView(APIView):
|
||||
peer_iface = Interface.objects.get(device__name=peer_device, name=peer_interface)
|
||||
except Interface.DoesNotExist:
|
||||
raise Http404()
|
||||
local_iface = peer_iface.get_connected_interface()
|
||||
local_iface = peer_iface.connected_interface
|
||||
if local_iface:
|
||||
device = local_iface.device
|
||||
else:
|
||||
@ -484,7 +488,7 @@ class RelatedConnectionsView(APIView):
|
||||
|
||||
# Interface connections
|
||||
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
|
||||
'circuit')
|
||||
'circuit_termination')
|
||||
for iface in interfaces:
|
||||
data = serializers.InterfaceDetailSerializer(instance=iface).data
|
||||
del(data['device'])
|
||||
|
@ -50,7 +50,7 @@ class RackGroupFilter(django_filters.FilterSet):
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@ -58,7 +58,6 @@ class RackGroupFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['site_id', 'site']
|
||||
|
||||
|
||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
@ -72,7 +71,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@ -113,7 +112,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['q', 'site_id', 'site', 'u_height']
|
||||
fields = ['u_height']
|
||||
|
||||
def search(self, queryset, value):
|
||||
return queryset.filter(
|
||||
@ -123,14 +122,18 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeFilter(django_filters.FilterSet):
|
||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='manufacturer',
|
||||
name='manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
@ -138,8 +141,16 @@ class DeviceTypeFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['manufacturer_id', 'manufacturer', 'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu',
|
||||
'is_network_device']
|
||||
fields = ['model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device',
|
||||
'subdevice_role']
|
||||
|
||||
def search(self, queryset, value):
|
||||
return queryset.filter(
|
||||
Q(manufacturer__name__icontains=value) |
|
||||
Q(model__icontains=value) |
|
||||
Q(part_number__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
@ -157,7 +168,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
name='rack__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
@ -178,7 +189,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_role',
|
||||
name='device_role__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
@ -205,13 +216,13 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__manufacturer',
|
||||
name='device_type__manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
model = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type',
|
||||
name='device_type__slug',
|
||||
queryset=DeviceType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Device model (slug)',
|
||||
@ -246,9 +257,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['q', 'name', 'serial', 'asset_tag', 'site_id', 'site', 'rack_id', 'role_id', 'role', 'device_type_id',
|
||||
'manufacturer_id', 'manufacturer', 'model', 'platform_id', 'platform', 'status', 'is_console_server',
|
||||
'is_pdu', 'is_network_device']
|
||||
fields = ['name', 'serial', 'asset_tag']
|
||||
|
||||
def search(self, queryset, value):
|
||||
return queryset.filter(
|
||||
@ -284,7 +293,7 @@ class ConsolePortFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['device_id', 'device', 'name']
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class ConsoleServerPortFilter(django_filters.FilterSet):
|
||||
@ -302,7 +311,7 @@ class ConsoleServerPortFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['device_id', 'device', 'name']
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerPortFilter(django_filters.FilterSet):
|
||||
@ -320,7 +329,7 @@ class PowerPortFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['device_id', 'device', 'name']
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerOutletFilter(django_filters.FilterSet):
|
||||
@ -338,7 +347,7 @@ class PowerOutletFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['device_id', 'device', 'name']
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class InterfaceFilter(django_filters.FilterSet):
|
||||
@ -356,7 +365,7 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['device_id', 'device', 'name']
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
|
@ -13,6 +13,7 @@ from utilities.forms import (
|
||||
SlugField,
|
||||
)
|
||||
|
||||
from formfields import MACAddressFormField
|
||||
from .models import (
|
||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||
@ -61,7 +62,8 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
|
||||
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name',
|
||||
'contact_phone', 'contact_email', 'comments']
|
||||
widgets = {
|
||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||
'shipping_address': SmallTextarea(attrs={'rows': 3}),
|
||||
@ -81,19 +83,20 @@ class SiteFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['name', 'slug', 'tenant', 'facility', 'asn']
|
||||
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
|
||||
|
||||
|
||||
class SiteImportForm(BulkImportForm, BootstrapMixin):
|
||||
class SiteImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=SiteFromCSVForm)
|
||||
|
||||
|
||||
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant']
|
||||
nullable_fields = ['tenant', 'asn']
|
||||
|
||||
|
||||
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
@ -106,7 +109,7 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
class RackGroupForm(forms.ModelForm, BootstrapMixin):
|
||||
class RackGroupForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -114,7 +117,7 @@ class RackGroupForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['site', 'name', 'slug']
|
||||
|
||||
|
||||
class RackGroupFilterForm(forms.Form, BootstrapMixin):
|
||||
class RackGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('rack_groups')), to_field_name='slug')
|
||||
|
||||
|
||||
@ -122,7 +125,7 @@ class RackGroupFilterForm(forms.Form, BootstrapMixin):
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleForm(forms.ModelForm, BootstrapMixin):
|
||||
class RackRoleForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -208,7 +211,7 @@ class RackFromCSVForm(forms.ModelForm):
|
||||
))
|
||||
|
||||
|
||||
class RackImportForm(BulkImportForm, BootstrapMixin):
|
||||
class RackImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=RackFromCSVForm)
|
||||
|
||||
|
||||
@ -242,7 +245,7 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerForm(forms.ModelForm, BootstrapMixin):
|
||||
class ManufacturerForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -254,16 +257,16 @@ class ManufacturerForm(forms.ModelForm, BootstrapMixin):
|
||||
# Device types
|
||||
#
|
||||
|
||||
class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
|
||||
class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
||||
slug = SlugField(slug_source='model')
|
||||
|
||||
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']
|
||||
'is_pdu', 'is_network_device', 'subdevice_role', 'comments']
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(BulkEditForm, BootstrapMixin):
|
||||
class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
||||
u_height = forms.IntegerField(min_value=1, required=False)
|
||||
@ -272,7 +275,8 @@ class DeviceTypeBulkEditForm(BulkEditForm, BootstrapMixin):
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
|
||||
class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = DeviceType
|
||||
manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||
to_field_name='slug')
|
||||
|
||||
@ -281,44 +285,76 @@ class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
|
||||
# Device component templates
|
||||
#
|
||||
|
||||
class ConsolePortTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['name_pattern']
|
||||
fields = ['device_type', 'name']
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||
class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['name_pattern']
|
||||
fields = ['device_type', 'name']
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class PowerPortTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||
class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['name_pattern']
|
||||
fields = ['device_type', 'name']
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||
class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['name_pattern']
|
||||
fields = ['device_type', 'name']
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class InterfaceTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||
class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['name_pattern', 'form_factor', 'mgmt_only']
|
||||
fields = ['device_type', 'name', 'form_factor', 'mgmt_only']
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||
|
||||
|
||||
class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
@ -329,19 +365,25 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
nullable_fields = []
|
||||
|
||||
|
||||
class DeviceBayTemplateForm(forms.ModelForm, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['name_pattern']
|
||||
fields = ['device_type', 'name']
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
|
||||
class DeviceRoleForm(forms.ModelForm, BootstrapMixin):
|
||||
class DeviceRoleForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -353,7 +395,7 @@ class DeviceRoleForm(forms.ModelForm, BootstrapMixin):
|
||||
# Platforms
|
||||
#
|
||||
|
||||
class PlatformForm(forms.ModelForm, BootstrapMixin):
|
||||
class PlatformForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -417,6 +459,10 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
ip_choices += [(ip.id, u'{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
||||
self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
|
||||
|
||||
# If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
|
||||
# can be flipped from one face to another.
|
||||
self.fields['position'].widget.attrs['api-url'] += '&exclude={}'.format(self.instance.pk)
|
||||
|
||||
else:
|
||||
|
||||
# An object that doesn't exist yet can't have any IPs assigned to it
|
||||
@ -566,11 +612,11 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
self.add_error('device_bay_name', "Parent device/bay ({} {}) not found".format(parent, device_bay_name))
|
||||
|
||||
|
||||
class DeviceImportForm(BulkImportForm, BootstrapMixin):
|
||||
class DeviceImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=DeviceFromCSVForm)
|
||||
|
||||
|
||||
class ChildDeviceImportForm(BulkImportForm, BootstrapMixin):
|
||||
class ChildDeviceImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
|
||||
|
||||
|
||||
@ -587,18 +633,6 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
nullable_fields = ['tenant', 'platform']
|
||||
|
||||
|
||||
class DeviceBulkAddComponentForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class DeviceBulkAddInterfaceForm(forms.ModelForm, DeviceBulkAddComponentForm):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['name_pattern', 'form_factor', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Device
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug')
|
||||
@ -615,11 +649,27 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
mac_address = forms.CharField(required=False, label='MAC address')
|
||||
|
||||
|
||||
#
|
||||
# Bulk device component creation
|
||||
#
|
||||
|
||||
class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class DeviceBulkAddInterfaceForm(forms.ModelForm, DeviceBulkAddComponentForm):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['pk', 'name_pattern', 'form_factor', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
class ConsolePortForm(forms.ModelForm, BootstrapMixin):
|
||||
class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
@ -629,7 +679,7 @@ class ConsolePortForm(forms.ModelForm, BootstrapMixin):
|
||||
}
|
||||
|
||||
|
||||
class ConsolePortCreateForm(forms.Form, BootstrapMixin):
|
||||
class ConsolePortCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
@ -670,7 +720,7 @@ class ConsoleConnectionCSVForm(forms.Form):
|
||||
.format(self.cleaned_data['device'], self.cleaned_data['console_port']))
|
||||
|
||||
|
||||
class ConsoleConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
|
||||
|
||||
def clean(self):
|
||||
@ -700,7 +750,7 @@ class ConsoleConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class ConsolePortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'console_server'}))
|
||||
console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
|
||||
@ -754,7 +804,7 @@ class ConsolePortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortForm(forms.ModelForm, BootstrapMixin):
|
||||
class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
@ -764,11 +814,11 @@ class ConsoleServerPortForm(forms.ModelForm, BootstrapMixin):
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortCreateForm(forms.Form, BootstrapMixin):
|
||||
class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class ConsoleServerPortConnectionForm(forms.Form, BootstrapMixin):
|
||||
class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
@ -816,7 +866,7 @@ class ConsoleServerPortConnectionForm(forms.Form, BootstrapMixin):
|
||||
# Power ports
|
||||
#
|
||||
|
||||
class PowerPortForm(forms.ModelForm, BootstrapMixin):
|
||||
class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
@ -826,7 +876,7 @@ class PowerPortForm(forms.ModelForm, BootstrapMixin):
|
||||
}
|
||||
|
||||
|
||||
class PowerPortCreateForm(forms.Form, BootstrapMixin):
|
||||
class PowerPortCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
@ -867,7 +917,7 @@ class PowerConnectionCSVForm(forms.Form):
|
||||
.format(self.cleaned_data['device'], self.cleaned_data['power_port']))
|
||||
|
||||
|
||||
class PowerConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=PowerConnectionCSVForm)
|
||||
|
||||
def clean(self):
|
||||
@ -897,7 +947,7 @@ class PowerConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class PowerPortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'pdu'}))
|
||||
pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
|
||||
@ -950,7 +1000,7 @@ class PowerPortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletForm(forms.ModelForm, BootstrapMixin):
|
||||
class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
@ -960,11 +1010,11 @@ class PowerOutletForm(forms.ModelForm, BootstrapMixin):
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletCreateForm(forms.Form, BootstrapMixin):
|
||||
class PowerOutletCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class PowerOutletConnectionForm(forms.Form, BootstrapMixin):
|
||||
class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
@ -1012,7 +1062,7 @@ class PowerOutletConnectionForm(forms.Form, BootstrapMixin):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceForm(forms.ModelForm, BootstrapMixin):
|
||||
class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
@ -1022,12 +1072,12 @@ class InterfaceForm(forms.ModelForm, BootstrapMixin):
|
||||
}
|
||||
|
||||
|
||||
class InterfaceCreateForm(forms.ModelForm, BootstrapMixin):
|
||||
class InterfaceCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['name_pattern', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
|
||||
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
@ -1043,10 +1093,13 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
# Interface connections
|
||||
#
|
||||
|
||||
class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
interface_a = forms.ChoiceField(choices=[], widget=SelectWithDisabled, label='Interface')
|
||||
site_b = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'rack_b'}))
|
||||
rack_b = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'device_b'}))
|
||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site_b}}',
|
||||
attrs={'filter-for': 'device_b'}))
|
||||
device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
|
||||
display_field='display_name',
|
||||
@ -1060,21 +1113,27 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['interface_a', 'rack_b', 'device_b', 'interface_b', 'livesearch', 'connection_status']
|
||||
fields = ['interface_a', 'site_b', 'rack_b', 'device_b', 'interface_b', 'livesearch', 'connection_status']
|
||||
|
||||
def __init__(self, device_a, *args, **kwargs):
|
||||
|
||||
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack_b'].queryset = Rack.objects.filter(site=device_a.rack.site)
|
||||
|
||||
# Initialize interface A choices
|
||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \
|
||||
.select_related('circuit', 'connected_as_a', 'connected_as_b')
|
||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL)\
|
||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
||||
self.fields['interface_a'].choices = [
|
||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
|
||||
]
|
||||
|
||||
# Initialize rack_b choices if site_b is set
|
||||
if self.is_bound and self.data.get('site_b'):
|
||||
self.fields['rack_b'].queryset = Rack.objects.filter(site__pk=self.data['site_b'])
|
||||
elif self.initial.get('site_b'):
|
||||
self.fields['rack_b'].queryset = Rack.objects.filter(site=self.initial['site_b'])
|
||||
else:
|
||||
self.fields['rack_b'].choices = []
|
||||
|
||||
# Initialize device_b choices if rack_b is set
|
||||
if self.is_bound and self.data.get('rack_b'):
|
||||
self.fields['device_b'].queryset = Device.objects.filter(rack__pk=self.data['rack_b'])
|
||||
@ -1085,11 +1144,13 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
# Initialize interface_b choices if device_b is set
|
||||
if self.is_bound:
|
||||
device_b_interfaces = Interface.objects.filter(device=self.data['device_b']) \
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
|
||||
device_b_interfaces = Interface.objects.filter(device=self.data['device_b'])\
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
||||
elif self.initial.get('device_b'):
|
||||
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']) \
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
|
||||
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b'])\
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
||||
else:
|
||||
device_b_interfaces = []
|
||||
self.fields['interface_b'].choices = [
|
||||
@ -1139,7 +1200,7 @@ class InterfaceConnectionCSVForm(forms.Form):
|
||||
pass
|
||||
|
||||
|
||||
class InterfaceConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
class InterfaceConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
|
||||
|
||||
def clean(self):
|
||||
@ -1179,7 +1240,7 @@ class InterfaceConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin):
|
||||
class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form):
|
||||
confirm = forms.BooleanField(required=True)
|
||||
# Used for HTTP redirect upon successful deletion
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
|
||||
@ -1189,7 +1250,7 @@ class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin):
|
||||
# Device bays
|
||||
#
|
||||
|
||||
class DeviceBayForm(forms.ModelForm, BootstrapMixin):
|
||||
class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
@ -1199,11 +1260,11 @@ class DeviceBayForm(forms.ModelForm, BootstrapMixin):
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayCreateForm(forms.Form, BootstrapMixin):
|
||||
class DeviceBayCreateForm(BootstrapMixin, forms.Form):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class PopulateDeviceBayForm(forms.Form, BootstrapMixin):
|
||||
class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
installed_device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Child Device',
|
||||
help_text="Child devices must first be created within the rack occupied "
|
||||
"by the parent device. Then they can be assigned to a bay.")
|
||||
@ -1224,15 +1285,15 @@ class PopulateDeviceBayForm(forms.Form, BootstrapMixin):
|
||||
# Connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionFilterForm(forms.Form, BootstrapMixin):
|
||||
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
|
||||
|
||||
class PowerConnectionFilterForm(forms.Form, BootstrapMixin):
|
||||
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
|
||||
|
||||
class InterfaceConnectionFilterForm(forms.Form, BootstrapMixin):
|
||||
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
|
||||
|
||||
@ -1270,7 +1331,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
# Modules
|
||||
#
|
||||
|
||||
class ModuleForm(forms.ModelForm, BootstrapMixin):
|
||||
class ModuleForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
|
20
netbox/dcim/migrations/0023_devicetype_comments.py
Normal file
20
netbox/dcim/migrations/0023_devicetype_comments.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-12-16 16:08
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0022_color_names_to_rgb'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
30
netbox/dcim/migrations/0024_site_add_contact_fields.py
Normal file
30
netbox/dcim/migrations/0024_site_add_contact_fields.py
Normal file
@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2016-12-29 16:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0023_devicetype_comments'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='contact_email',
|
||||
field=models.EmailField(blank=True, max_length=254, verbose_name=b'Contact E-mail'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='contact_name',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='contact_phone',
|
||||
field=models.CharField(blank=True, max_length=20),
|
||||
),
|
||||
]
|
@ -9,6 +9,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||
|
||||
from circuits.models import Circuit
|
||||
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
|
||||
from extras.rpc import RPC_CLIENTS
|
||||
from tenancy.models import Tenant
|
||||
@ -244,6 +245,9 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
||||
physical_address = models.CharField(max_length=200, blank=True)
|
||||
shipping_address = models.CharField(max_length=200, blank=True)
|
||||
contact_name = models.CharField(max_length=50, blank=True)
|
||||
contact_phone = models.CharField(max_length=20, blank=True)
|
||||
contact_email = models.EmailField(blank=True, verbose_name="Contact E-mail")
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
@ -264,7 +268,10 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.slug,
|
||||
self.tenant.name if self.tenant else '',
|
||||
self.facility,
|
||||
str(self.asn),
|
||||
str(self.asn) if self.asn else '',
|
||||
self.contact_name,
|
||||
self.contact_phone,
|
||||
self.contact_email,
|
||||
])
|
||||
|
||||
@property
|
||||
@ -285,7 +292,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
@property
|
||||
def count_circuits(self):
|
||||
return self.circuits.count()
|
||||
return Circuit.objects.filter(terminations__site=self).count()
|
||||
|
||||
|
||||
#
|
||||
@ -401,6 +408,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.get_type_display() if self.type else '',
|
||||
str(self.width),
|
||||
str(self.u_height),
|
||||
'True' if self.desc_units else '',
|
||||
])
|
||||
|
||||
@property
|
||||
@ -520,7 +528,7 @@ class Manufacturer(models.Model):
|
||||
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
|
||||
|
||||
|
||||
class DeviceType(models.Model):
|
||||
class DeviceType(models.Model, CustomFieldModel):
|
||||
"""
|
||||
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
||||
well as high-level functional role(s).
|
||||
@ -552,6 +560,8 @@ class DeviceType(models.Model):
|
||||
choices=SUBDEVICE_ROLE_CHOICES,
|
||||
help_text="Parent devices house child devices in device bays. Select "
|
||||
"\"None\" if this device type is neither a parent nor a child.")
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
class Meta:
|
||||
ordering = ['manufacturer', 'model']
|
||||
@ -1136,7 +1146,7 @@ class Interface(models.Model):
|
||||
@property
|
||||
def is_connected(self):
|
||||
try:
|
||||
return bool(self.circuit)
|
||||
return bool(self.circuit_termination)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
return bool(self.connection)
|
||||
@ -1153,13 +1163,18 @@ class Interface(models.Model):
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_connected_interface(self):
|
||||
connection = InterfaceConnection.objects.select_related().filter(Q(interface_a=self) | Q(interface_b=self))\
|
||||
.first()
|
||||
if connection and connection.interface_a == self:
|
||||
return connection.interface_b
|
||||
elif connection:
|
||||
return connection.interface_a
|
||||
@property
|
||||
def connected_interface(self):
|
||||
try:
|
||||
if self.connected_as_a:
|
||||
return self.connected_as_a.interface_b
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
try:
|
||||
if self.connected_as_b:
|
||||
return self.connected_as_b.interface_a
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
|
@ -22,6 +22,9 @@ class SiteTest(APITestCase):
|
||||
'asn',
|
||||
'physical_address',
|
||||
'shipping_address',
|
||||
'contact_name',
|
||||
'contact_phone',
|
||||
'contact_email',
|
||||
'comments',
|
||||
'custom_fields',
|
||||
'count_prefixes',
|
||||
@ -233,6 +236,8 @@ class DeviceTypeTest(APITestCase):
|
||||
'is_pdu',
|
||||
'is_network_device',
|
||||
'subdevice_role',
|
||||
'comments',
|
||||
'custom_fields',
|
||||
]
|
||||
|
||||
nested_fields = [
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from ipam.views import ServiceEditView
|
||||
from secrets.views import secret_add
|
||||
|
||||
from . import views
|
||||
@ -104,9 +105,11 @@ urlpatterns = [
|
||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'),
|
||||
url(r'^devices/(?P<pk>\d+)/ip-addresses/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
|
||||
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
||||
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'),
|
||||
|
||||
# Console ports
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),
|
||||
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortAddView.as_view(), name='consoleport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
|
||||
@ -114,7 +117,8 @@ urlpatterns = [
|
||||
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||
|
||||
# Console server ports
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.consoleserverport_add, name='consoleserverport_add'),
|
||||
url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortAddView.as_view(), name='consoleserverport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
|
||||
@ -122,7 +126,8 @@ urlpatterns = [
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
|
||||
# Power ports
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.powerport_add, name='powerport_add'),
|
||||
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortAddView.as_view(), name='powerport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
|
||||
@ -130,15 +135,27 @@ urlpatterns = [
|
||||
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||
|
||||
# Power outlets
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.poweroutlet_add, name='poweroutlet_add'),
|
||||
url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletAddView.as_view(), name='poweroutlet_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
|
||||
# Interfaces
|
||||
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceAddView.as_view(), name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||
|
||||
# Device bays
|
||||
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.devicebay_add, name='devicebay_add'),
|
||||
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayAddView.as_view(), name='devicebay_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||
@ -153,18 +170,8 @@ urlpatterns = [
|
||||
url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
|
||||
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
||||
|
||||
# Interfaces
|
||||
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||
|
||||
# Modules
|
||||
url(r'^devices/(?P<pk>\d+)/modules/add/$', views.module_add, name='module_add'),
|
||||
url(r'^devices/(?P<device>\d+)/modules/add/$', views.ModuleEditView.as_view(), name='module_add'),
|
||||
url(r'^modules/(?P<pk>\d+)/edit/$', views.ModuleEditView.as_view(), name='module_edit'),
|
||||
url(r'^modules/(?P<pk>\d+)/delete/$', views.ModuleDeleteView.as_view(), name='module_delete'),
|
||||
|
||||
|
@ -6,7 +6,6 @@ from operator import attrgetter
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Count
|
||||
from django.http import HttpResponseRedirect
|
||||
@ -14,7 +13,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.http import urlencode
|
||||
from django.views.generic import View
|
||||
|
||||
from ipam.models import Prefix, IPAddress, VLAN
|
||||
from ipam.models import Prefix, IPAddress, Service, VLAN
|
||||
from circuits.models import Circuit
|
||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from utilities.forms import ConfirmationForm
|
||||
@ -57,6 +56,66 @@ def expand_pattern(string):
|
||||
yield "{0}{1}".format(lead, i)
|
||||
|
||||
|
||||
class ComponentCreateView(View):
|
||||
parent_model = None
|
||||
parent_field = None
|
||||
model = None
|
||||
form = None
|
||||
model_form = None
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
parent = get_object_or_404(self.parent_model, pk=pk)
|
||||
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'parent': parent,
|
||||
'component_type': self.model._meta.verbose_name,
|
||||
'form': self.form(initial=request.GET),
|
||||
'cancel_url': parent.get_absolute_url(),
|
||||
})
|
||||
|
||||
def post(self, request, pk):
|
||||
|
||||
parent = get_object_or_404(self.parent_model, pk=pk)
|
||||
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
new_components = []
|
||||
data = deepcopy(form.cleaned_data)
|
||||
|
||||
for name in form.cleaned_data['name_pattern']:
|
||||
component_data = {
|
||||
self.parent_field: parent.pk,
|
||||
'name': name,
|
||||
}
|
||||
component_data.update(data)
|
||||
component_form = self.model_form(component_data)
|
||||
if component_form.is_valid():
|
||||
new_components.append(component_form.save(commit=False))
|
||||
else:
|
||||
for field, errors in component_form.errors.as_data().items():
|
||||
for e in errors:
|
||||
form.add_error(field, u'{}: {}'.format(name, ', '.join(e)))
|
||||
|
||||
if not form.errors:
|
||||
self.model.objects.bulk_create(new_components)
|
||||
messages.success(request, u"Added {} {} to {}.".format(
|
||||
len(new_components), self.model._meta.verbose_name_plural, parent
|
||||
))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
else:
|
||||
return redirect(parent.get_absolute_url())
|
||||
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'parent': parent,
|
||||
'component_type': self.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'cancel_url': parent.get_absolute_url(),
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
@ -78,7 +137,7 @@ def site(request, slug):
|
||||
'device_count': Device.objects.filter(rack__site=site).count(),
|
||||
'prefix_count': Prefix.objects.filter(site=site).count(),
|
||||
'vlan_count': VLAN.objects.filter(site=site).count(),
|
||||
'circuit_count': Circuit.objects.filter(site=site).count(),
|
||||
'circuit_count': Circuit.objects.filter(terminations__site=site).count(),
|
||||
}
|
||||
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
|
||||
topology_maps = TopologyMap.objects.filter(site=site)
|
||||
@ -331,6 +390,7 @@ class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_devicetype'
|
||||
model = DeviceType
|
||||
form_class = forms.DeviceTypeForm
|
||||
template_name = 'dcim/devicetype_edit.html'
|
||||
obj_list_url = 'dcim:devicetype_list'
|
||||
|
||||
|
||||
@ -358,69 +418,30 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Device type components
|
||||
#
|
||||
|
||||
class ComponentTemplateCreateView(View):
|
||||
model = None
|
||||
form = None
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
devicetype = get_object_or_404(DeviceType, pk=pk)
|
||||
|
||||
return render(request, 'dcim/component_template_add.html', {
|
||||
'devicetype': devicetype,
|
||||
'component_type': self.model._meta.verbose_name,
|
||||
'form': self.form(initial=request.GET),
|
||||
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
|
||||
})
|
||||
|
||||
def post(self, request, pk):
|
||||
|
||||
devicetype = get_object_or_404(DeviceType, pk=pk)
|
||||
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
component_templates = []
|
||||
for name in form.cleaned_data['name_pattern']:
|
||||
component_template = self.form(request.POST).save(commit=False)
|
||||
component_template.device_type = devicetype
|
||||
component_template.name = name
|
||||
try:
|
||||
component_template.full_clean()
|
||||
component_templates.append(component_template)
|
||||
except ValidationError:
|
||||
form.add_error('name_pattern', "Duplicate name found: {}".format(name))
|
||||
|
||||
if not form.errors:
|
||||
self.model.objects.bulk_create(component_templates)
|
||||
messages.success(request, u"Added {} component(s) to {}.".format(len(component_templates), devicetype))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
else:
|
||||
return redirect('dcim:devicetype', pk=devicetype.pk)
|
||||
|
||||
return render(request, 'dcim/component_template_add.html', {
|
||||
'devicetype': devicetype,
|
||||
'component_type': self.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
|
||||
})
|
||||
|
||||
|
||||
class ConsolePortTemplateAddView(ComponentTemplateCreateView):
|
||||
class ConsolePortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_consoleporttemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = ConsolePortTemplate
|
||||
form = forms.ConsolePortTemplateForm
|
||||
form = forms.ConsolePortTemplateCreateForm
|
||||
model_form = forms.ConsolePortTemplateForm
|
||||
|
||||
|
||||
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleporttemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
cls = ConsolePortTemplate
|
||||
parent_cls = DeviceType
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateAddView(ComponentTemplateCreateView):
|
||||
class ConsoleServerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_consoleserverporttemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = ConsoleServerPortTemplate
|
||||
form = forms.ConsoleServerPortTemplateForm
|
||||
form = forms.ConsoleServerPortTemplateCreateForm
|
||||
model_form = forms.ConsoleServerPortTemplateForm
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -429,9 +450,13 @@ class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDelet
|
||||
parent_cls = DeviceType
|
||||
|
||||
|
||||
class PowerPortTemplateAddView(ComponentTemplateCreateView):
|
||||
class PowerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_powerporttemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = PowerPortTemplate
|
||||
form = forms.PowerPortTemplateForm
|
||||
form = forms.PowerPortTemplateCreateForm
|
||||
model_form = forms.PowerPortTemplateForm
|
||||
|
||||
|
||||
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -440,9 +465,13 @@ class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
parent_cls = DeviceType
|
||||
|
||||
|
||||
class PowerOutletTemplateAddView(ComponentTemplateCreateView):
|
||||
class PowerOutletTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_poweroutlettemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = PowerOutletTemplate
|
||||
form = forms.PowerOutletTemplateForm
|
||||
form = forms.PowerOutletTemplateCreateForm
|
||||
model_form = forms.PowerOutletTemplateForm
|
||||
|
||||
|
||||
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -451,9 +480,13 @@ class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
|
||||
parent_cls = DeviceType
|
||||
|
||||
|
||||
class InterfaceTemplateAddView(ComponentTemplateCreateView):
|
||||
class InterfaceTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_interfacetemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = InterfaceTemplate
|
||||
form = forms.InterfaceTemplateForm
|
||||
form = forms.InterfaceTemplateCreateForm
|
||||
model_form = forms.InterfaceTemplateForm
|
||||
|
||||
|
||||
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
@ -470,9 +503,13 @@ class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
parent_cls = DeviceType
|
||||
|
||||
|
||||
class DeviceBayTemplateAddView(ComponentTemplateCreateView):
|
||||
class DeviceBayTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_devicebaytemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = DeviceBayTemplate
|
||||
form = forms.DeviceBayTemplateForm
|
||||
form = forms.DeviceBayTemplateCreateForm
|
||||
model_form = forms.DeviceBayTemplateForm
|
||||
|
||||
|
||||
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -560,21 +597,26 @@ def device(request, pk):
|
||||
power_outlets = natsorted(
|
||||
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
||||
)
|
||||
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
|
||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
||||
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
|
||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
||||
interfaces = Interface.objects.filter(device=device, mgmt_only=False).select_related(
|
||||
'connected_as_a__interface_b__device',
|
||||
'connected_as_b__interface_a__device',
|
||||
'circuit_termination__circuit',
|
||||
)
|
||||
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True).select_related(
|
||||
'connected_as_a__interface_b__device',
|
||||
'connected_as_b__interface_a__device',
|
||||
'circuit_termination__circuit',
|
||||
)
|
||||
device_bays = natsorted(
|
||||
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
||||
key=attrgetter('name')
|
||||
)
|
||||
|
||||
# Gather any secrets which belong to this device
|
||||
secrets = device.secrets.all()
|
||||
|
||||
# Find all IP addresses assigned to this device
|
||||
# Gather relevant device objects
|
||||
ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface', 'vrf')\
|
||||
.order_by('address')
|
||||
services = Service.objects.filter(device=device)
|
||||
secrets = device.secrets.all()
|
||||
|
||||
# Find any related devices for convenient linking in the UI
|
||||
related_devices = []
|
||||
@ -604,6 +646,7 @@ def device(request, pk):
|
||||
'mgmt_interfaces': mgmt_interfaces,
|
||||
'device_bays': device_bays,
|
||||
'ip_addresses': ip_addresses,
|
||||
'services': services,
|
||||
'secrets': secrets,
|
||||
'related_devices': related_devices,
|
||||
'show_graphs': show_graphs,
|
||||
@ -687,121 +730,17 @@ def device_lldp_neighbors(request, pk):
|
||||
})
|
||||
|
||||
|
||||
class DeviceBulkAddComponentView(View):
|
||||
"""
|
||||
Add one or more components (e.g. interfaces) to a selected set of Devices.
|
||||
"""
|
||||
form = None
|
||||
component_cls = None
|
||||
component_form = None
|
||||
|
||||
def get(self):
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
def post(self, request):
|
||||
|
||||
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all'):
|
||||
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
|
||||
else:
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
if '_create' in request.POST:
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
new_components = []
|
||||
data = deepcopy(form.cleaned_data)
|
||||
for device in data['pk']:
|
||||
|
||||
names = data['name_pattern']
|
||||
for name in names:
|
||||
component_data = {
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
}
|
||||
component_data.update(data)
|
||||
component_form = self.component_form(component_data)
|
||||
if component_form.is_valid():
|
||||
new_components.append(component_form.save(commit=False))
|
||||
else:
|
||||
form.add_error('name_pattern', "Duplicate {} name for {}: {}".format(
|
||||
self.component_cls._meta.verbose_name, device, name
|
||||
))
|
||||
|
||||
if not form.errors:
|
||||
self.component_cls.objects.bulk_create(new_components)
|
||||
messages.success(request, u"Added {} {} to {} devices.".format(
|
||||
len(new_components), self.component_cls._meta.verbose_name_plural, len(form.cleaned_data['pk'])
|
||||
))
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
else:
|
||||
form = self.form(initial={'pk': pk_list})
|
||||
|
||||
selected_devices = Device.objects.filter(pk__in=pk_list)
|
||||
if not selected_devices:
|
||||
messages.warning(request, u"No devices were selected.")
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
return render(request, 'dcim/device_bulk_add_component.html', {
|
||||
'form': form,
|
||||
'component_name': self.component_cls._meta.verbose_name_plural,
|
||||
'selected_devices': selected_devices,
|
||||
'cancel_url': reverse('dcim:device_list'),
|
||||
})
|
||||
|
||||
|
||||
class DeviceBulkAddInterfaceView(DeviceBulkAddComponentView):
|
||||
"""
|
||||
Add one or more components (e.g. interfaces) to a selected set of Devices.
|
||||
"""
|
||||
form = forms.DeviceBulkAddInterfaceForm
|
||||
component_cls = Interface
|
||||
component_form = forms.InterfaceForm
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
@permission_required('dcim.add_consoleport')
|
||||
def consoleport_add(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.ConsolePortCreateForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
console_ports = []
|
||||
for name in form.cleaned_data['name_pattern']:
|
||||
cp_form = forms.ConsolePortForm({
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
})
|
||||
if cp_form.is_valid():
|
||||
console_ports.append(cp_form.save(commit=False))
|
||||
else:
|
||||
form.add_error('name_pattern', "Duplicate console port name for this device: {}".format(name))
|
||||
|
||||
if not form.errors:
|
||||
ConsolePort.objects.bulk_create(console_ports)
|
||||
messages.success(request, u"Added {} console port(s) to {}.".format(len(console_ports), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:consoleport_add', pk=device.pk)
|
||||
else:
|
||||
return redirect('dcim:device', pk=device.pk)
|
||||
|
||||
else:
|
||||
form = forms.ConsolePortCreateForm()
|
||||
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Console Port',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
class ConsolePortAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_consoleport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = ConsolePort
|
||||
form = forms.ConsolePortCreateForm
|
||||
model_form = forms.ConsolePortForm
|
||||
|
||||
|
||||
@permission_required('dcim.change_consoleport')
|
||||
@ -891,44 +830,13 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
@permission_required('dcim.add_consoleserverport')
|
||||
def consoleserverport_add(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.ConsoleServerPortCreateForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
cs_ports = []
|
||||
for name in form.cleaned_data['name_pattern']:
|
||||
csp_form = forms.ConsoleServerPortForm({
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
})
|
||||
if csp_form.is_valid():
|
||||
cs_ports.append(csp_form.save(commit=False))
|
||||
else:
|
||||
form.add_error('name_pattern', "Duplicate console server port name for this device: {}"
|
||||
.format(name))
|
||||
|
||||
if not form.errors:
|
||||
ConsoleServerPort.objects.bulk_create(cs_ports)
|
||||
messages.success(request, u"Added {} console server port(s) to {}.".format(len(cs_ports), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:consoleserverport_add', pk=device.pk)
|
||||
else:
|
||||
return redirect('dcim:device', pk=device.pk)
|
||||
|
||||
else:
|
||||
form = forms.ConsoleServerPortCreateForm()
|
||||
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Console Server Port',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
class ConsoleServerPortAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_consoleserverport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = ConsoleServerPort
|
||||
form = forms.ConsoleServerPortCreateForm
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
|
||||
|
||||
@permission_required('dcim.change_consoleserverport')
|
||||
@ -1012,43 +920,13 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Power ports
|
||||
#
|
||||
|
||||
@permission_required('dcim.add_powerport')
|
||||
def powerport_add(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.PowerPortCreateForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
power_ports = []
|
||||
for name in form.cleaned_data['name_pattern']:
|
||||
pp_form = forms.PowerPortForm({
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
})
|
||||
if pp_form.is_valid():
|
||||
power_ports.append(pp_form.save(commit=False))
|
||||
else:
|
||||
form.add_error('name_pattern', "Duplicate power port name for this device: {}".format(name))
|
||||
|
||||
if not form.errors:
|
||||
PowerPort.objects.bulk_create(power_ports)
|
||||
messages.success(request, u"Added {} power port(s) to {}.".format(len(power_ports), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:powerport_add', pk=device.pk)
|
||||
else:
|
||||
return redirect('dcim:device', pk=device.pk)
|
||||
|
||||
else:
|
||||
form = forms.PowerPortCreateForm()
|
||||
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Power Port',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
class PowerPortAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_powerport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = PowerPort
|
||||
form = forms.PowerPortCreateForm
|
||||
model_form = forms.PowerPortForm
|
||||
|
||||
|
||||
@permission_required('dcim.change_powerport')
|
||||
@ -1138,43 +1016,13 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
@permission_required('dcim.add_poweroutlet')
|
||||
def poweroutlet_add(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.PowerOutletCreateForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
power_outlets = []
|
||||
for name in form.cleaned_data['name_pattern']:
|
||||
po_form = forms.PowerOutletForm({
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
})
|
||||
if po_form.is_valid():
|
||||
power_outlets.append(po_form.save(commit=False))
|
||||
else:
|
||||
form.add_error('name_pattern', "Duplicate power outlet name for this device: {}".format(name))
|
||||
|
||||
if not form.errors:
|
||||
PowerOutlet.objects.bulk_create(power_outlets)
|
||||
messages.success(request, u"Added {} power outlet(s) to {}.".format(len(power_outlets), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:poweroutlet_add', pk=device.pk)
|
||||
else:
|
||||
return redirect('dcim:device', pk=device.pk)
|
||||
|
||||
else:
|
||||
form = forms.PowerOutletCreateForm()
|
||||
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Power Outlet',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
class PowerOutletAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_poweroutlet'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = PowerOutlet
|
||||
form = forms.PowerOutletCreateForm
|
||||
model_form = forms.PowerOutletForm
|
||||
|
||||
|
||||
@permission_required('dcim.change_poweroutlet')
|
||||
@ -1257,47 +1105,13 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
@permission_required('dcim.add_interface')
|
||||
def interface_add(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.InterfaceCreateForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
interfaces = []
|
||||
for name in form.cleaned_data['name_pattern']:
|
||||
iface_form = forms.InterfaceForm({
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
'form_factor': form.cleaned_data['form_factor'],
|
||||
'mac_address': form.cleaned_data['mac_address'],
|
||||
'mgmt_only': form.cleaned_data['mgmt_only'],
|
||||
'description': form.cleaned_data['description'],
|
||||
})
|
||||
if iface_form.is_valid():
|
||||
interfaces.append(iface_form.save(commit=False))
|
||||
else:
|
||||
form.add_error('name_pattern', "Duplicate interface name for this device: {}".format(name))
|
||||
|
||||
if not form.errors:
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
messages.success(request, u"Added {} interface(s) to {}.".format(len(interfaces), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:interface_add', pk=device.pk)
|
||||
else:
|
||||
return redirect('dcim:device', pk=device.pk)
|
||||
|
||||
else:
|
||||
form = forms.InterfaceCreateForm(initial={'mgmt_only': request.GET.get('mgmt_only')})
|
||||
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Interface',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_interface'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = Interface
|
||||
form = forms.InterfaceCreateForm
|
||||
model_form = forms.InterfaceForm
|
||||
|
||||
|
||||
class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
@ -1329,44 +1143,13 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Device bays
|
||||
#
|
||||
|
||||
@permission_required('dcim.add_devicebay')
|
||||
def devicebay_add(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.DeviceBayCreateForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
device_bays = []
|
||||
for name in form.cleaned_data['name_pattern']:
|
||||
devicebay_form = forms.DeviceBayForm({
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
})
|
||||
if devicebay_form.is_valid():
|
||||
device_bays.append(devicebay_form.save(commit=False))
|
||||
else:
|
||||
for err in devicebay_form.errors.get('__all__', []):
|
||||
form.add_error('name_pattern', err)
|
||||
|
||||
if not form.errors:
|
||||
DeviceBay.objects.bulk_create(device_bays)
|
||||
messages.success(request, u"Added {} device bay(s) to {}.".format(len(device_bays), device))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:devicebay_add', pk=device.pk)
|
||||
else:
|
||||
return redirect('dcim:device', pk=device.pk)
|
||||
|
||||
else:
|
||||
form = forms.DeviceBayCreateForm()
|
||||
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Device Bay',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
})
|
||||
class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_devicebay'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = DeviceBay
|
||||
form = forms.DeviceBayCreateForm
|
||||
model_form = forms.DeviceBayForm
|
||||
|
||||
|
||||
class DeviceBayEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
@ -1436,6 +1219,112 @@ class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
parent_cls = Device
|
||||
|
||||
|
||||
#
|
||||
# Bulk device component creation
|
||||
#
|
||||
|
||||
class DeviceBulkAddComponentView(View):
|
||||
"""
|
||||
Add one or more components (e.g. interfaces) to a selected set of Devices.
|
||||
"""
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
model = None
|
||||
model_form = None
|
||||
|
||||
def get(self):
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
def post(self, request):
|
||||
|
||||
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all'):
|
||||
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
|
||||
else:
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
if '_create' in request.POST:
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
new_components = []
|
||||
data = deepcopy(form.cleaned_data)
|
||||
for device in data['pk']:
|
||||
|
||||
names = data['name_pattern']
|
||||
for name in names:
|
||||
component_data = {
|
||||
'device': device.pk,
|
||||
'name': name,
|
||||
}
|
||||
component_data.update(data)
|
||||
component_form = self.model_form(component_data)
|
||||
if component_form.is_valid():
|
||||
new_components.append(component_form.save(commit=False))
|
||||
else:
|
||||
for field, errors in component_form.errors.as_data().items():
|
||||
for e in errors:
|
||||
form.add_error(field, u'{} {}: {}'.format(device, name, ', '.join(e)))
|
||||
|
||||
if not form.errors:
|
||||
self.model.objects.bulk_create(new_components)
|
||||
messages.success(request, u"Added {} {} to {} devices.".format(
|
||||
len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk'])
|
||||
))
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
else:
|
||||
form = self.form(initial={'pk': pk_list})
|
||||
|
||||
selected_devices = Device.objects.filter(pk__in=pk_list)
|
||||
if not selected_devices:
|
||||
messages.warning(request, u"No devices were selected.")
|
||||
return redirect('dcim:device_list')
|
||||
|
||||
return render(request, 'dcim/device_bulk_add_component.html', {
|
||||
'form': form,
|
||||
'component_name': self.model._meta.verbose_name_plural,
|
||||
'selected_devices': selected_devices,
|
||||
'cancel_url': reverse('dcim:device_list'),
|
||||
})
|
||||
|
||||
|
||||
class DeviceBulkAddConsolePortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
|
||||
permission_required = 'dcim.add_consoleport'
|
||||
model = ConsolePort
|
||||
model_form = forms.ConsolePortForm
|
||||
|
||||
|
||||
class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
|
||||
permission_required = 'dcim.add_consoleserverport'
|
||||
model = ConsoleServerPort
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
|
||||
|
||||
class DeviceBulkAddPowerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
|
||||
permission_required = 'dcim.add_powerport'
|
||||
model = PowerPort
|
||||
model_form = forms.PowerPortForm
|
||||
|
||||
|
||||
class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, DeviceBulkAddComponentView):
|
||||
permission_required = 'dcim.add_poweroutlet'
|
||||
model = PowerOutlet
|
||||
model_form = forms.PowerOutletForm
|
||||
|
||||
|
||||
class DeviceBulkAddInterfaceView(PermissionRequiredMixin, DeviceBulkAddComponentView):
|
||||
permission_required = 'dcim.add_interface'
|
||||
form = forms.DeviceBulkAddInterfaceForm
|
||||
model = Interface
|
||||
model_form = forms.InterfaceForm
|
||||
|
||||
|
||||
class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, DeviceBulkAddComponentView):
|
||||
permission_required = 'dcim.add_devicebay'
|
||||
model = DeviceBay
|
||||
model_form = forms.DeviceBayForm
|
||||
|
||||
|
||||
#
|
||||
# Interface connections
|
||||
#
|
||||
@ -1467,9 +1356,11 @@ def interfaceconnection_add(request, pk):
|
||||
|
||||
else:
|
||||
form = forms.InterfaceConnectionForm(device, initial={
|
||||
'interface_a': request.GET.get('interface', None),
|
||||
'interface_a': request.GET.get('interface_a', None),
|
||||
'site_b': request.GET.get('site_b', device.rack.site),
|
||||
'rack_b': request.GET.get('rack_b', None),
|
||||
'device_b': request.GET.get('device_b', None),
|
||||
'interface_b': request.GET.get('interface_b', None),
|
||||
})
|
||||
|
||||
return render(request, 'dcim/interfaceconnection_edit.html', {
|
||||
@ -1602,39 +1493,17 @@ def ipaddress_assign(request, pk):
|
||||
# Modules
|
||||
#
|
||||
|
||||
@permission_required('dcim.add_module')
|
||||
def module_add(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = forms.ModuleForm(request.POST)
|
||||
if form.is_valid():
|
||||
module = form.save(commit=False)
|
||||
module.device = device
|
||||
module.save()
|
||||
messages.success(request, u"Added module {} to {}".format(module.name, module.device.name))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:module_add', pk=module.device.pk)
|
||||
else:
|
||||
return redirect('dcim:device_inventory', pk=module.device.pk)
|
||||
|
||||
else:
|
||||
form = forms.ModuleForm()
|
||||
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'device': device,
|
||||
'component_type': 'Module',
|
||||
'form': form,
|
||||
'cancel_url': reverse('dcim:device_inventory', kwargs={'pk': device.pk}),
|
||||
})
|
||||
|
||||
|
||||
class ModuleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_module'
|
||||
model = Module
|
||||
form_class = forms.ModuleForm
|
||||
|
||||
def alter_obj(self, obj, args, kwargs):
|
||||
if 'device' in kwargs:
|
||||
device = get_object_or_404(Device, pk=kwargs['device'])
|
||||
obj.device = device
|
||||
return obj
|
||||
|
||||
|
||||
class ModuleDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_module'
|
||||
|
@ -44,7 +44,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
|
||||
# Date
|
||||
elif cf.type == CF_TYPE_DATE:
|
||||
field = forms.DateField(required=cf.required, initial=cf.default)
|
||||
field = forms.DateField(required=cf.required, initial=cf.default, help_text="Date format: YYYY-MM-DD")
|
||||
|
||||
# Select
|
||||
elif cf.type == CF_TYPE_SELECT:
|
||||
@ -63,7 +63,8 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
||||
|
||||
field.model = cf
|
||||
field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize()
|
||||
field.help_text = cf.description
|
||||
if cf.description:
|
||||
field.help_text = cf.description
|
||||
|
||||
field_dict[field_name] = field
|
||||
|
||||
|
@ -12,7 +12,7 @@ from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
CUSTOMFIELD_MODELS = (
|
||||
'site', 'rack', 'device', # DCIM
|
||||
'site', 'rack', 'devicetype', 'device', # DCIM
|
||||
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
|
||||
'provider', 'circuit', # Circuits
|
||||
'tenant', # Tenants
|
||||
|
@ -1,8 +1,8 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||
from dcim.api.serializers import DeviceNestedSerializer, InterfaceNestedSerializer, SiteNestedSerializer
|
||||
from extras.api.serializers import CustomFieldSerializer
|
||||
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from tenancy.api.serializers import TenantNestedSerializer
|
||||
|
||||
|
||||
@ -138,7 +138,7 @@ class PrefixSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description',
|
||||
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
|
||||
'custom_fields']
|
||||
|
||||
|
||||
@ -170,3 +170,22 @@ class IPAddressNestedSerializer(IPAddressSerializer):
|
||||
|
||||
IPAddressSerializer._declared_fields['nat_inside'] = IPAddressNestedSerializer()
|
||||
IPAddressSerializer._declared_fields['nat_outside'] = IPAddressNestedSerializer()
|
||||
|
||||
|
||||
#
|
||||
# Services
|
||||
#
|
||||
|
||||
class ServiceSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
ipaddresses = IPAddressNestedSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Service
|
||||
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
|
||||
|
||||
|
||||
class ServiceNestedSerializer(ServiceSerializer):
|
||||
|
||||
class Meta(ServiceSerializer.Meta):
|
||||
fields = ['id', 'name', 'port', 'protocol']
|
||||
|
@ -37,4 +37,8 @@ urlpatterns = [
|
||||
url(r'^vlans/$', VLANListView.as_view(), name='vlan_list'),
|
||||
url(r'^vlans/(?P<pk>\d+)/$', VLANDetailView.as_view(), name='vlan_detail'),
|
||||
|
||||
# Services
|
||||
url(r'^services/$', ServiceListView.as_view(), name='service_list'),
|
||||
url(r'^services/(?P<pk>\d+)/$', ServiceDetailView.as_view(), name='service_detail'),
|
||||
|
||||
]
|
||||
|
@ -1,6 +1,6 @@
|
||||
from rest_framework import generics
|
||||
|
||||
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from ipam import filters
|
||||
|
||||
from extras.api.views import CustomFieldModelAPIView
|
||||
@ -177,3 +177,24 @@ class VLANDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')\
|
||||
.prefetch_related('custom_field_values__field')
|
||||
serializer_class = serializers.VLANSerializer
|
||||
|
||||
|
||||
#
|
||||
# Services
|
||||
#
|
||||
|
||||
class ServiceListView(generics.ListAPIView):
|
||||
"""
|
||||
List services (filterable)
|
||||
"""
|
||||
queryset = Service.objects.select_related('device').prefetch_related('ipaddresses')
|
||||
serializer_class = serializers.ServiceSerializer
|
||||
filter_class = filters.ServiceFilter
|
||||
|
||||
|
||||
class ServiceDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single service
|
||||
"""
|
||||
queryset = Service.objects.select_related('device').prefetch_related('ipaddresses')
|
||||
serializer_class = serializers.ServiceSerializer
|
||||
|
@ -9,7 +9,7 @@ from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
|
||||
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
@ -43,7 +43,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['name', 'rd']
|
||||
fields = ['rd']
|
||||
|
||||
|
||||
class RIRFilter(django_filters.FilterSet):
|
||||
@ -64,7 +64,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='RIR (ID)',
|
||||
)
|
||||
rir = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rir',
|
||||
name='rir__slug',
|
||||
queryset=RIR.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='RIR (slug)',
|
||||
@ -72,7 +72,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = ['family', 'rir_id', 'rir', 'date_added']
|
||||
fields = ['family', 'date_added']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(description__icontains=value)
|
||||
@ -149,7 +149,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['family', 'site_id', 'site', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
|
||||
fields = ['family', 'status']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(description__icontains=value)
|
||||
@ -226,7 +226,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='interface__device',
|
||||
name='interface__device__name',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
@ -239,7 +239,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['q', 'family', 'status', 'device_id', 'device', 'interface_id']
|
||||
fields = ['q', 'family', 'status']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(description__icontains=value)
|
||||
@ -268,7 +268,7 @@ class VLANGroupFilter(django_filters.FilterSet):
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@ -276,7 +276,6 @@ class VLANGroupFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = ['site_id', 'site']
|
||||
|
||||
|
||||
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
@ -290,7 +289,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@ -340,7 +339,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site_id', 'site', 'vid', 'name', 'status', 'role_id', 'role']
|
||||
fields = ['status']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
||||
@ -349,3 +348,21 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
except ValueError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class ServiceFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device__name',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Service
|
||||
fields = ['name', 'protocol', 'port']
|
||||
|
@ -5,12 +5,13 @@ from dcim.models import Site, Rack, Device, Interface
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField, add_blank_choice,
|
||||
APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch,
|
||||
SlugField, add_blank_choice,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup,
|
||||
VLAN_STATUS_CHOICES, VRF,
|
||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
|
||||
VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
||||
)
|
||||
|
||||
|
||||
@ -47,7 +48,7 @@ class VRFFromCSVForm(forms.ModelForm):
|
||||
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
|
||||
|
||||
class VRFImportForm(BulkImportForm, BootstrapMixin):
|
||||
class VRFImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=VRFFromCSVForm)
|
||||
|
||||
|
||||
@ -70,7 +71,7 @@ class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# RIRs
|
||||
#
|
||||
|
||||
class RIRForm(forms.ModelForm, BootstrapMixin):
|
||||
class RIRForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -78,7 +79,7 @@ class RIRForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name', 'slug', 'is_private']
|
||||
|
||||
|
||||
class RIRFilterForm(forms.Form, BootstrapMixin):
|
||||
class RIRFilterForm(BootstrapMixin, forms.Form):
|
||||
is_private = forms.NullBooleanField(required=False, label='Private', widget=forms.Select(choices=[
|
||||
('', '---------'),
|
||||
('True', 'Yes'),
|
||||
@ -111,7 +112,7 @@ class AggregateFromCSVForm(forms.ModelForm):
|
||||
fields = ['prefix', 'rir', 'date_added', 'description']
|
||||
|
||||
|
||||
class AggregateImportForm(BulkImportForm, BootstrapMixin):
|
||||
class AggregateImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=AggregateFromCSVForm)
|
||||
|
||||
|
||||
@ -136,7 +137,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# Roles
|
||||
#
|
||||
|
||||
class RoleForm(forms.ModelForm, BootstrapMixin):
|
||||
class RoleForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -157,15 +158,7 @@ class PrefixForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description']
|
||||
help_texts = {
|
||||
'prefix': "IPv4 or IPv6 network",
|
||||
'vrf': "VRF (if applicable)",
|
||||
'site': "The site to which this prefix is assigned (if applicable)",
|
||||
'vlan': "The VLAN to which this prefix is assigned (if applicable)",
|
||||
'status': "Operational status of this prefix",
|
||||
'role': "The primary function of this prefix",
|
||||
}
|
||||
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'is_pool', 'description']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PrefixForm, self).__init__(*args, **kwargs)
|
||||
@ -196,7 +189,7 @@ class PrefixFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role',
|
||||
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'is_pool',
|
||||
'description']
|
||||
|
||||
def clean(self):
|
||||
@ -235,7 +228,7 @@ class PrefixFromCSVForm(forms.ModelForm):
|
||||
return super(PrefixFromCSVForm, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class PrefixImportForm(BulkImportForm, BootstrapMixin):
|
||||
class PrefixImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=PrefixFromCSVForm)
|
||||
|
||||
|
||||
@ -339,6 +332,14 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
self.fields['nat_inside'].choices = []
|
||||
|
||||
|
||||
class IPAddressBulkAddForm(BootstrapMixin, forms.Form):
|
||||
address = ExpandableIPAddressField()
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=IPADDRESS_STATUS_CHOICES)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
|
||||
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
@ -417,7 +418,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
||||
return super(IPAddressFromCSVForm, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class IPAddressImportForm(BulkImportForm, BootstrapMixin):
|
||||
class IPAddressImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=IPAddressFromCSVForm)
|
||||
|
||||
|
||||
@ -456,7 +457,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# VLAN groups
|
||||
#
|
||||
|
||||
class VLANGroupForm(forms.ModelForm, BootstrapMixin):
|
||||
class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -464,7 +465,7 @@ class VLANGroupForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['site', 'name', 'slug']
|
||||
|
||||
|
||||
class VLANGroupFilterForm(forms.Form, BootstrapMixin):
|
||||
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug')
|
||||
|
||||
|
||||
@ -529,7 +530,7 @@ class VLANFromCSVForm(forms.ModelForm):
|
||||
return m
|
||||
|
||||
|
||||
class VLANImportForm(BulkImportForm, BootstrapMixin):
|
||||
class VLANImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=VLANFromCSVForm)
|
||||
|
||||
|
||||
@ -563,3 +564,25 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
|
||||
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
|
||||
|
||||
#
|
||||
# Services
|
||||
#
|
||||
|
||||
class ServiceForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Service
|
||||
fields = ['name', 'protocol', 'port', 'ipaddresses', 'description']
|
||||
help_texts = {
|
||||
'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
|
||||
"reachable via all IPs assigned to the device.",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(ServiceForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Limit IP address choices to those assigned to interfaces of the parent device
|
||||
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(interface__device=self.instance.device)
|
||||
|
39
netbox/ipam/migrations/0012_services.py
Normal file
39
netbox/ipam/migrations/0012_services.py
Normal file
@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10 on 2016-12-15 20:22
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0022_color_names_to_rgb'),
|
||||
('ipam', '0011_rir_add_is_private'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Service',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('protocol', models.PositiveSmallIntegerField(choices=[(6, b'TCP'), (17, b'UDP')])),
|
||||
('port', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)], verbose_name=b'Port number')),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name=b'device')),
|
||||
('ipaddresses', models.ManyToManyField(blank=True, related_name='services', to='ipam.IPAddress', verbose_name=b'IP addresses')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'protocol', 'port'],
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='service',
|
||||
unique_together=set([('device', 'protocol', 'port')]),
|
||||
),
|
||||
]
|
37
netbox/ipam/migrations/0013_prefix_add_is_pool.py
Normal file
37
netbox/ipam/migrations/0013_prefix_add_is_pool.py
Normal file
@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2016-12-27 19:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import ipam.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0012_services'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='is_pool',
|
||||
field=models.BooleanField(default=False, help_text=b'All IP addresses within this prefix are considered usable', verbose_name=b'Is a pool'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='prefix',
|
||||
field=ipam.fields.IPNetworkField(help_text=b'IPv4 or IPv6 network with mask'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='role',
|
||||
field=models.ForeignKey(blank=True, help_text=b'The primary function of this prefix', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prefixes', to='ipam.Role'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='prefix',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(0, b'Container'), (1, b'Active'), (2, b'Reserved'), (3, b'Deprecated')], default=1, help_text=b'Operational status of this prefix', verbose_name=b'Status'),
|
||||
),
|
||||
]
|
@ -61,6 +61,14 @@ STATUS_CHOICE_CLASSES = {
|
||||
}
|
||||
|
||||
|
||||
IP_PROTOCOL_TCP = 6
|
||||
IP_PROTOCOL_UDP = 17
|
||||
IP_PROTOCOL_CHOICES = (
|
||||
(IP_PROTOCOL_TCP, 'TCP'),
|
||||
(IP_PROTOCOL_UDP, 'UDP'),
|
||||
)
|
||||
|
||||
|
||||
class VRF(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
|
||||
@ -261,15 +269,19 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
assigned to a VLAN where appropriate.
|
||||
"""
|
||||
family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
|
||||
prefix = IPNetworkField()
|
||||
prefix = IPNetworkField(help_text="IPv4 or IPv6 network with mask")
|
||||
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)
|
||||
role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True)
|
||||
status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=PREFIX_STATUS_ACTIVE,
|
||||
help_text="Operational status of this prefix")
|
||||
role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True,
|
||||
help_text="The primary function of this prefix")
|
||||
is_pool = models.BooleanField(verbose_name='Is a pool', default=False,
|
||||
help_text="All IP addresses within this prefix are considered usable")
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
@ -312,8 +324,11 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.vrf.rd if self.vrf else '',
|
||||
self.tenant.name if self.tenant else '',
|
||||
self.site.name if self.site else '',
|
||||
self.vlan.group.name if self.vlan and self.vlan.group else '',
|
||||
str(self.vlan.vid) if self.vlan else '',
|
||||
self.get_status_display(),
|
||||
self.role.name if self.role else '',
|
||||
'True' if self.is_pool else '',
|
||||
self.description,
|
||||
])
|
||||
|
||||
@ -525,3 +540,28 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
|
||||
class Service(CreatedUpdatedModel):
|
||||
"""
|
||||
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device. A Service may optionally be tied
|
||||
to one or more specific IPAddresses belonging to the Device.
|
||||
"""
|
||||
device = models.ForeignKey('dcim.Device', related_name='services', on_delete=models.CASCADE, verbose_name='device')
|
||||
name = models.CharField(max_length=30)
|
||||
protocol = models.PositiveSmallIntegerField(choices=IP_PROTOCOL_CHOICES)
|
||||
port = models.PositiveIntegerField(validators=[MinValueValidator(1), MaxValueValidator(65535)],
|
||||
verbose_name='Port number')
|
||||
ipaddresses = models.ManyToManyField('ipam.IPAddress', related_name='services', blank=True,
|
||||
verbose_name='IP addresses')
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'protocol', 'port']
|
||||
unique_together = ['device', 'protocol', 'port']
|
||||
|
||||
def __unicode__(self):
|
||||
return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
|
||||
|
||||
def get_parent_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
|
@ -58,6 +58,14 @@ PREFIX_LINK_BRIEF = """
|
||||
</span>
|
||||
"""
|
||||
|
||||
PREFIX_ROLE_LINK = """
|
||||
{% if record.role %}
|
||||
<a href="{% url 'ipam:prefix_list' %}?role={{ record.role.slug }}">{{ record.role }}</a>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
IPADDRESS_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||
@ -86,6 +94,22 @@ STATUS_LABEL = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VLAN_PREFIXES = """
|
||||
{% for prefix in record.prefixes.all %}
|
||||
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
|
||||
{% empty %}
|
||||
—
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
VLAN_ROLE_LINK = """
|
||||
{% if record.role %}
|
||||
<a href="{% url 'ipam:vlan_list' %}?role={{ record.role.slug }}">{{ record.role }}</a>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VLANGROUP_ACTIONS = """
|
||||
{% if perms.ipam.change_vlangroup %}
|
||||
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@ -189,16 +213,17 @@ class RoleTable(BaseTable):
|
||||
class PrefixTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix')
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix', attrs={'th': {'style': 'padding-left: 17px'}})
|
||||
vrf = tables.TemplateColumn(VRF_LINK, 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')
|
||||
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
|
||||
role = tables.TemplateColumn(PREFIX_ROLE_LINK, verbose_name='Role')
|
||||
description = tables.Column(orderable=False, verbose_name='Description')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Prefix
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not record.pk else '',
|
||||
}
|
||||
@ -281,10 +306,11 @@ 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')
|
||||
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
|
||||
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')
|
||||
role = tables.TemplateColumn(VLAN_ROLE_LINK, verbose_name='Role')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLAN
|
||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role')
|
||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role')
|
||||
|
@ -51,6 +51,7 @@ urlpatterns = [
|
||||
# IP addresses
|
||||
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
|
||||
url(r'^ip-addresses/add/$', views.IPAddressEditView.as_view(), name='ipaddress_add'),
|
||||
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkAddView.as_view(), name='ipaddress_bulk_add'),
|
||||
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
|
||||
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
||||
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
||||
@ -76,4 +77,8 @@ urlpatterns = [
|
||||
url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
|
||||
url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),
|
||||
|
||||
# Services
|
||||
url(r'^services/(?P<pk>\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'),
|
||||
url(r'^services/(?P<pk>\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'),
|
||||
|
||||
]
|
||||
|
@ -12,11 +12,14 @@ from dcim.models import Device
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
|
||||
from . import filters, forms, tables
|
||||
from .models import Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||
from .models import (
|
||||
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
|
||||
Service, VLAN, VLANGroup, VRF,
|
||||
)
|
||||
|
||||
|
||||
def add_available_prefixes(parent, prefix_list):
|
||||
@ -35,24 +38,21 @@ def add_available_prefixes(parent, prefix_list):
|
||||
return prefix_list
|
||||
|
||||
|
||||
def add_available_ipaddresses(prefix, ipaddress_list):
|
||||
def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
|
||||
"""
|
||||
Annotate ranges of available IP addresses within a given prefix.
|
||||
Annotate ranges of available IP addresses within a given prefix. If is_pool is True, the first and last IP will be
|
||||
considered usable (regardless of mask length).
|
||||
"""
|
||||
|
||||
output = []
|
||||
prev_ip = None
|
||||
|
||||
# Ignore the "network address" for IPv4 prefixes larger than /31
|
||||
if prefix.version == 4 and prefix.prefixlen < 31:
|
||||
# Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31.
|
||||
if prefix.version == 4 and prefix.prefixlen < 31 and not is_pool:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
|
||||
else:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
|
||||
|
||||
# Ignore the broadcast address for IPv4 prefixes larger than /31
|
||||
if prefix.version == 4 and prefix.prefixlen < 31:
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
|
||||
else:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
|
||||
|
||||
if not ipaddress_list:
|
||||
@ -290,7 +290,6 @@ def aggregate(request, pk):
|
||||
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
|
||||
|
||||
prefix_table = tables.PrefixTable(child_prefixes)
|
||||
prefix_table.model = Prefix
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
prefix_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
|
||||
@ -367,7 +366,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class PrefixListView(ObjectListView):
|
||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'role')
|
||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
filter = filters.PrefixFilter
|
||||
filter_form = forms.PrefixFilterForm
|
||||
table = tables.PrefixTable
|
||||
@ -416,7 +415,6 @@ def prefix(request, pk):
|
||||
if child_prefixes:
|
||||
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
||||
child_prefix_table = tables.PrefixTable(child_prefixes)
|
||||
child_prefix_table.model = Prefix
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
child_prefix_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table)
|
||||
@ -475,10 +473,9 @@ def prefix_ipaddresses(request, pk):
|
||||
# Find all IPAddresses belonging to this 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')
|
||||
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses)
|
||||
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
|
||||
|
||||
ip_table = tables.IPAddressTable(ipaddresses)
|
||||
ip_table.model = IPAddress
|
||||
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
||||
ip_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
|
||||
@ -610,6 +607,14 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
redirect_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
|
||||
permission_required = 'ipam.add_ipaddress'
|
||||
form = forms.IPAddressBulkAddForm
|
||||
model = IPAddress
|
||||
template_name = 'ipam/ipaddress_bulk_add.html'
|
||||
redirect_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_ipaddress'
|
||||
form = forms.IPAddressImportForm
|
||||
@ -679,7 +684,7 @@ class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class VLANListView(ObjectListView):
|
||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
|
||||
filter = filters.VLANFilter
|
||||
filter_form = forms.VLANFilterForm
|
||||
table = tables.VLANTable
|
||||
@ -733,3 +738,24 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vlan'
|
||||
cls = VLAN
|
||||
default_redirect_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
#
|
||||
# Services
|
||||
#
|
||||
|
||||
class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_service'
|
||||
model = Service
|
||||
form_class = forms.ServiceForm
|
||||
template_name = 'ipam/service_edit.html'
|
||||
|
||||
def alter_obj(self, obj, args, kwargs):
|
||||
if 'device' in kwargs:
|
||||
obj.device = get_object_or_404(Device, pk=kwargs['device'])
|
||||
return obj
|
||||
|
||||
|
||||
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'ipam.delete_service'
|
||||
model = Service
|
||||
|
@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.7.3'
|
||||
VERSION = '1.8.0'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
@ -117,7 +117,8 @@ INSTALLED_APPS = (
|
||||
)
|
||||
|
||||
# Middleware
|
||||
MIDDLEWARE_CLASSES = (
|
||||
MIDDLEWARE = (
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
@ -193,6 +194,12 @@ SWAGGER_SETTINGS = {
|
||||
'base_path': '{}/{}api/docs'.format(ALLOWED_HOSTS[0], BASE_PATH),
|
||||
}
|
||||
|
||||
# Django debug toolbar
|
||||
INTERNAL_IPS = (
|
||||
'127.0.0.1',
|
||||
'::1',
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
HOSTNAME = socket.gethostname()
|
||||
|
@ -42,6 +42,12 @@ _patterns = [
|
||||
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
_patterns += [
|
||||
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||
]
|
||||
|
||||
# Prepend BASE_PATH
|
||||
urlpatterns = [
|
||||
url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
|
||||
|
@ -13,7 +13,7 @@ body {
|
||||
}
|
||||
.container {
|
||||
width: auto;
|
||||
max-width: 1340px;
|
||||
max-width: 1600px;
|
||||
}
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
@ -35,7 +35,8 @@ footer p {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
/* Collapse the nav menu on displays less than 1200px wide */
|
||||
@media (max-width: 1199px) {
|
||||
.navbar-header {
|
||||
float: none;
|
||||
}
|
||||
@ -58,7 +59,7 @@ footer p {
|
||||
max-height: none;
|
||||
}
|
||||
.navbar-nav {
|
||||
float: none!important;
|
||||
float: none !important;
|
||||
margin-top: 7.5px;
|
||||
}
|
||||
.navbar-nav>li {
|
||||
@ -88,10 +89,17 @@ th.pk, td.pk {
|
||||
tfoot td {
|
||||
font-weight: bold;
|
||||
}
|
||||
table.attr-table td:nth-child(1) {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
/* Paginator */
|
||||
div.paginator {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
nav ul.pagination {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
/* Racks */
|
||||
@ -322,4 +330,4 @@ td .progress {
|
||||
}
|
||||
textarea {
|
||||
font-family: Consolas, Lucida Console, monospace;
|
||||
}
|
||||
}
|
@ -51,6 +51,14 @@ $(document).ready(function() {
|
||||
$('#id_' + this.value).toggle('disabled');
|
||||
});
|
||||
|
||||
// Set formaction and submit using a link
|
||||
$('a.formaction').click(function (event) {
|
||||
event.preventDefault();
|
||||
var form = $(this).closest('form');
|
||||
form.attr('action', $(this).attr('href'));
|
||||
form.submit();
|
||||
});
|
||||
|
||||
// API select widget
|
||||
$('select[filter-for]').change(function () {
|
||||
|
||||
|
@ -17,7 +17,7 @@ class SecretFilter(django_filters.FilterSet):
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
name='role__slug',
|
||||
queryset=SecretRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
@ -31,7 +31,7 @@ class SecretFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
fields = ['name', 'role_id', 'role', 'device']
|
||||
fields = ['name']
|
||||
|
||||
def search(self, queryset, value):
|
||||
return queryset.filter(
|
||||
|
@ -34,7 +34,7 @@ def validate_rsa_key(key, is_secret=True):
|
||||
# Secret roles
|
||||
#
|
||||
|
||||
class SecretRoleForm(forms.ModelForm, BootstrapMixin):
|
||||
class SecretRoleForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -46,7 +46,7 @@ class SecretRoleForm(forms.ModelForm, BootstrapMixin):
|
||||
# Secrets
|
||||
#
|
||||
|
||||
class SecretForm(forms.ModelForm, BootstrapMixin):
|
||||
class SecretForm(BootstrapMixin, forms.ModelForm):
|
||||
private_key = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
plaintext = forms.CharField(max_length=65535, required=False, label='Plaintext',
|
||||
widget=forms.PasswordInput(attrs={'class': 'requires-private-key'}))
|
||||
@ -85,12 +85,12 @@ class SecretFromCSVForm(forms.ModelForm):
|
||||
return s
|
||||
|
||||
|
||||
class SecretImportForm(BulkImportForm, BootstrapMixin):
|
||||
class SecretImportForm(BootstrapMixin, BulkImportForm):
|
||||
private_key = forms.CharField(widget=forms.HiddenInput())
|
||||
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-private-key'}))
|
||||
|
||||
|
||||
class SecretBulkEditForm(BulkEditForm, BootstrapMixin):
|
||||
class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
|
||||
name = forms.CharField(max_length=100, required=False)
|
||||
@ -99,7 +99,7 @@ class SecretBulkEditForm(BulkEditForm, BootstrapMixin):
|
||||
nullable_fields = ['name']
|
||||
|
||||
|
||||
class SecretFilterForm(forms.Form, BootstrapMixin):
|
||||
class SecretFilterForm(BootstrapMixin, forms.Form):
|
||||
role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')
|
||||
|
||||
|
||||
@ -107,7 +107,7 @@ class SecretFilterForm(forms.Form, BootstrapMixin):
|
||||
# UserKeys
|
||||
#
|
||||
|
||||
class UserKeyForm(forms.ModelForm, BootstrapMixin):
|
||||
class UserKeyForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = UserKey
|
||||
|
@ -5,14 +5,14 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<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">
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" />
|
||||
@ -40,13 +40,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=circuit %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Circuit</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td>
|
||||
@ -81,17 +82,6 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Speed</td>
|
||||
<td>
|
||||
{% if circuit.upstream_speed %}
|
||||
<i class="fa fa-arrow-down" title="Downstream"></i> {{ circuit.port_speed_human }}
|
||||
<i class="fa fa-arrow-up" title="Upstream"></i> {{ circuit.upstream_speed_human }}
|
||||
{% else %}
|
||||
{{ circuit.port_speed_human }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Commit Rate</td>
|
||||
<td>
|
||||
@ -107,67 +97,6 @@
|
||||
{% with circuit.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
{% include 'inc/created_updated.html' with obj=circuit %}
|
||||
</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>
|
||||
<a href="{% url 'dcim:site' slug=circuit.site.slug %}">{{ circuit.site }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Termination</td>
|
||||
<td>
|
||||
{% if circuit.interface %}
|
||||
<span><a href="{% url 'dcim:device' pk=circuit.interface.device.pk %}">{{ circuit.interface.device }}</a> {{ circuit.interface }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">Not defined</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP Addressing</td>
|
||||
<td>
|
||||
{% if circuit.interface %}
|
||||
{% for ip in circuit.interface.ip_addresses.all %}
|
||||
{% if not forloop.first %}<br />{% endif %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> ({{ ip.vrf|default:"Global" }})
|
||||
{% empty %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cross-Connect</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>
|
||||
{% if circuit.pp_info %}
|
||||
{{ circuit.pp_info }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
@ -180,6 +109,10 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=termination_a side='A' %}
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=termination_z side='Z' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,5 +1,4 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
@ -11,15 +10,6 @@
|
||||
{% render_field form.type %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.install_date %}
|
||||
{% 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.upstream_speed %}
|
||||
{% render_field form.commit_rate %}
|
||||
</div>
|
||||
</div>
|
||||
@ -31,26 +21,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Termination</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.site %}
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
|
||||
<li role="presentation"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="select">
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.device %}
|
||||
</div>
|
||||
<div class="tab-pane" id="search">
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Comments</strong></div>
|
||||
<div class="panel-body">
|
||||
@ -58,7 +28,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/livesearch.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
@ -48,45 +48,20 @@
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Strickland Propane</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Site name</td>
|
||||
<td>ASH-4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Install Date</td>
|
||||
<td>Date in YYYY-MM-DD format (optional)</td>
|
||||
<td>2016-02-23</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Port Speed</td>
|
||||
<td>Physical speed in Kbps</td>
|
||||
<td>100000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Upstream Speed</td>
|
||||
<td>Upstream speed in Kbps (optional)</td>
|
||||
<td>20000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Commit rate</td>
|
||||
<td>Commited rate in Kbps (optional)</td>
|
||||
<td>2000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cross-connect ID</td>
|
||||
<td>ID of cross-connect (optional)</td>
|
||||
<td>937649</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Patch Panel</td>
|
||||
<td>Patch panel/port ID (optional)</td>
|
||||
<td>PP8371 ports 13/14</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,ASH-4,2016-02-23,100000,,2000,937649,PP8371 ports 13/14</pre>
|
||||
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
25
netbox/templates/circuits/circuit_terminations_swap.html
Normal file
25
netbox/templates/circuits/circuit_terminations_swap.html
Normal file
@ -0,0 +1,25 @@
|
||||
{% extends 'utilities/confirmation_form.html' %}
|
||||
|
||||
{% block title %}Swap Circuit Terminations{% endblock %}
|
||||
|
||||
{% block message %}
|
||||
<p>Swap these terminations for circuit {{ circuit }}?</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>A side:</strong>
|
||||
{% if termination_a %}
|
||||
{{ termination_a.site }} {% if termination_a.interface %}- {{ termination_a.interface.device }} {{ termination_a.interface }}{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Z side:</strong>
|
||||
{% if termination_z %}
|
||||
{{ termination_z.site }} {% if termination_z.interface %}- {{ termination_z.interface.device }} {{ termination_z.interface }}{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
94
netbox/templates/circuits/circuittermination_edit.html
Normal file
94
netbox/templates/circuits/circuittermination_edit.html
Normal file
@ -0,0 +1,94 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}
|
||||
Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h3>Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}</h3>
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Location</strong></div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Provider</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ obj.circuit.provider }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Circuit</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ obj.circuit.cid }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Termination</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ form.term_side.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.site %}
|
||||
<div class="row">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
|
||||
<li role="presentation"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="select">
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.device %}
|
||||
</div>
|
||||
<div class="tab-pane" id="search">
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Termination Details</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.port_speed %}
|
||||
{% render_field form.upstream_speed %}
|
||||
{% render_field form.xconnect_id %}
|
||||
{% render_field form.pp_info %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3 text-right">
|
||||
{% if obj.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
{% endif %}
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script src="{% static 'js/livesearch.js' %}"></script>
|
||||
{% endblock %}
|
95
netbox/templates/circuits/inc/circuit_termination.html
Normal file
95
netbox/templates/circuits/inc/circuit_termination.html
Normal file
@ -0,0 +1,95 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-right">
|
||||
{% if not termination and perms.circuits.add_circuittermination %}
|
||||
<a href="{% url 'circuits:circuittermination_add' circuit=circuit.pk %}?term_side={{ side }}" class="btn btn-xs btn-success">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span> Add
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if termination and perms.circuits.change_circuittermination %}
|
||||
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}" class="btn btn-xs btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span> Edit
|
||||
</a>
|
||||
<a href="{% url 'circuits:circuit_terminations_swap' pk=circuit.pk %}" class="btn btn-xs btn-primary">
|
||||
<span class="fa fa-refresh" aria-hidden="true"></span> Swap
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if termination and perms.circuits.delete_circuittermination %}
|
||||
<a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}" class="btn btn-xs btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span> Delete
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<strong>Termination - {{ side }} Side</strong>
|
||||
</div>
|
||||
{% if termination %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>
|
||||
<a href="{% url 'dcim:site' slug=termination.site.slug %}">{{ termination.site }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Termination</td>
|
||||
<td>
|
||||
{% if termination.interface %}
|
||||
<span><a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a> {{ termination.interface }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">Not defined</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Speed</td>
|
||||
<td>
|
||||
{% if termination.upstream_speed %}
|
||||
<i class="fa fa-arrow-down" title="Downstream"></i> {{ termination.port_speed_human }}
|
||||
<i class="fa fa-arrow-up" title="Upstream"></i> {{ termination.upstream_speed_human }}
|
||||
{% else %}
|
||||
{{ termination.port_speed_human }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP Addressing</td>
|
||||
<td>
|
||||
{% if termination.interface %}
|
||||
{% for ip in termination.interface.ip_addresses.all %}
|
||||
{% if not forloop.first %}<br />{% endif %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> ({{ ip.vrf|default:"Global" }})
|
||||
{% empty %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cross-Connect</td>
|
||||
<td>
|
||||
{% if termination.xconnect_id %}
|
||||
{{ termination.xconnect_id }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Patch Panel/Port</td>
|
||||
<td>
|
||||
{% if termination.pp_info %}
|
||||
{{ termination.pp_info }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body">
|
||||
<span class="text-muted">None</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
@ -6,13 +6,13 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-sm-8 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">
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'circuits:provider_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" />
|
||||
@ -46,13 +46,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ provider }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=provider %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Provider</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>ASN</td>
|
||||
<td>
|
||||
@ -120,7 +121,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/created_updated.html' with obj=provider %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
@ -134,14 +134,8 @@
|
||||
<a href="{% url 'circuits:circuit' pk=c.pk %}">{{ c.cid }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'dcim:site' slug=c.site.slug %}">{{ c.site }}</a>
|
||||
<a href="{% url 'circuits:circuit_list' %}?type={{ c.type.slug }}">{{ c.type }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if c.interface %}
|
||||
<a href="{% url 'dcim:device' pk=c.interface.device.pk %}">{{ c.interface.device }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ c.port_speed_human }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
@ -149,6 +143,13 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.circuits.add_circuit %}
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'circuits:circuit_add' %}?provider={{ provider.pk }}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add circuit
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,13 +0,0 @@
|
||||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete device type components?{% endblock %}
|
||||
|
||||
{% block message %}
|
||||
<p>Are you sure you want to delete these components from <strong>{{ devicetype }}</strong>?</p>
|
||||
<ul>
|
||||
{% for o in selected_objects %}
|
||||
<li>{{ o }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
@ -8,12 +8,12 @@
|
||||
{% block content %}
|
||||
{% include 'dcim/inc/_device_header.html' with active_tab='info' %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-5 col-lg-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Device</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
@ -85,7 +85,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Management</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>
|
||||
@ -205,6 +205,29 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Services</strong>
|
||||
</div>
|
||||
{% if services %}
|
||||
<table class="table table-hover panel-body">
|
||||
{% for service in services %}
|
||||
{% include 'dcim/inc/_service.html' %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
None
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_service %}
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'dcim:service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Critical Connections</strong>
|
||||
@ -301,9 +324,8 @@
|
||||
<div class="panel-body text-muted">None found</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/created_updated.html' with obj=device %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-7 col-lg-6">
|
||||
{% if device_bays or device.device_type.is_parent_device %}
|
||||
{% if perms.dcim.delete_devicebay %}
|
||||
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
||||
@ -313,9 +335,11 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Device Bays</strong>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.change_devicebay and device_bays|length > 1 %}
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_devicebay and device_bays|length > 10 %}
|
||||
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
|
||||
@ -363,9 +387,11 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Interfaces</strong>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.change_interface and interfaces|length > 1 %}
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_interface and interfaces|length > 10 %}
|
||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
||||
@ -418,9 +444,11 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Console Server Ports</strong>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.change_consoleserverport and cs_ports|length > 1 %}
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_consoleserverport and cs_ports|length > 10 %}
|
||||
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
|
||||
@ -468,9 +496,11 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Power Outlets</strong>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% if perms.dcim.change_poweroutlet and cs_ports|length > 1 %}
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_poweroutlet and power_outlets|length > 10 %}
|
||||
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
|
||||
|
@ -1,9 +1,9 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Create {{ component_type }} ({{ device }}){% endblock %}
|
||||
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block content %}{{ form.errors }}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
@ -18,13 +18,13 @@
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>{{ component_type }}</strong>
|
||||
<strong>{{ component_type|title }}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device }}</p>
|
||||
<p class="form-control-static">{{ parent }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
||||
|
@ -57,7 +57,7 @@
|
||||
<div class="panel-body">
|
||||
{% render_field form.platform %}
|
||||
{% render_field form.status %}
|
||||
{% if obj %}
|
||||
{% if obj.pk %}
|
||||
{% render_field form.primary_ip4 %}
|
||||
{% render_field form.primary_ip6 %}
|
||||
{% endif %}
|
||||
|
@ -10,7 +10,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Chassis</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Model</td>
|
||||
<td>{{ device.device_type.full_name }}</td>
|
||||
@ -127,7 +127,7 @@
|
||||
</table>
|
||||
</div>
|
||||
{% if perms.dcim.add_module %}
|
||||
<a href="{% url 'dcim:module_add' pk=device.pk %}" class="btn btn-success">
|
||||
<a href="{% url 'dcim:module_add' device=device.pk %}" class="btn btn-success">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a Module
|
||||
</a>
|
||||
|
@ -23,7 +23,7 @@
|
||||
<tr id="{{ iface }}">
|
||||
<td>{{ iface }}</td>
|
||||
{% if iface.connection %}
|
||||
{% with iface.get_connected_interface as connected_iface %}
|
||||
{% with iface.connected_interface as connected_iface %}
|
||||
<td class="configured_device" data="{{ connected_iface.device }}">
|
||||
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a>
|
||||
</td>
|
||||
|
@ -39,7 +39,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Chassis</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Manufacturer</td>
|
||||
<td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
|
||||
@ -145,6 +145,21 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with devicetype.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if devicetype.comments %}
|
||||
{{ devicetype.comments|gfm }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
|
||||
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Add {{ component_type }} to {{ devicetype }}{% endblock %}
|
||||
{% block title %}Add {{ component_type }} to {{ parent }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
@ -24,7 +24,7 @@
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device Type</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ devicetype }}</p>
|
||||
<p class="form-control-static">{{ parent }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_form form %}
|
34
netbox/templates/dcim/devicetype_edit.html
Normal file
34
netbox/templates/dcim/devicetype_edit.html
Normal file
@ -0,0 +1,34 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Device Type</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.manufacturer %}
|
||||
{% render_field form.model %}
|
||||
{% render_field form.slug %}
|
||||
{% render_field form.part_number %}
|
||||
{% render_field form.u_height %}
|
||||
{% render_field form.is_full_depth %}
|
||||
{% render_field form.is_console_server %}
|
||||
{% render_field form.is_pdu %}
|
||||
{% render_field form.is_network_device %}
|
||||
{% render_field form.subdevice_role %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Comments</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.comments %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -18,6 +18,7 @@
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
{% include 'inc/filter_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
{% if device.rack %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a></li>
|
||||
@ -13,7 +13,7 @@
|
||||
</ol>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search devices" />
|
||||
@ -41,6 +41,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ device }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=device %}
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if active_tab == 'info' %} class="active"{% endif %}><a href="{% url 'dcim:device' pk=device.pk %}">Info</a></li>
|
||||
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}><a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a></li>
|
||||
|
@ -14,9 +14,9 @@
|
||||
<small>{{ iface.mac_address|default:'' }}</small>
|
||||
</td>
|
||||
{% if not iface.is_physical %}
|
||||
<td colspan="2">Virtual</td>
|
||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||
{% elif iface.connection %}
|
||||
{% with iface.get_connected_interface as connected_iface %}
|
||||
{% with iface.connected_interface as connected_iface %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a>
|
||||
</td>
|
||||
@ -24,10 +24,16 @@
|
||||
<span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span>
|
||||
</td>
|
||||
{% endwith %}
|
||||
{% elif iface.circuit %}
|
||||
<td colspan="2">
|
||||
<a href="{% url 'circuits:circuit' pk=iface.circuit.pk %}">{{ iface.circuit }}</a>
|
||||
</td>
|
||||
{% elif iface.circuit_termination %}
|
||||
{% with iface.circuit_termination.get_peer_termination as peer_termination %}
|
||||
<td colspan="2">
|
||||
<i class="fa fa-fw fa-globe" title="Circuit"></i>
|
||||
{% if peer_termination %}
|
||||
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a> via
|
||||
{% endif %}
|
||||
<a href="{% url 'circuits:circuit' pk=iface.circuit_termination.circuit_id %}">{{ iface.circuit_termination.circuit }}</a>
|
||||
</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
<span class="text-muted">Not connected</span>
|
||||
@ -35,7 +41,7 @@
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
{% if show_graphs %}
|
||||
{% if iface.circuit or iface.connection %}
|
||||
{% if iface.circuit_termination 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>
|
||||
@ -56,12 +62,15 @@
|
||||
<a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% elif iface.circuit and perms.circuits.change_circuit %}
|
||||
<a href="{% url 'circuits:circuit_edit' pk=iface.circuit.pk %}" class="btn btn-danger btn-xs" title="Edit circuit">
|
||||
{% elif iface.circuit_termination and perms.circuits.change_circuittermination %}
|
||||
<button class="btn btn-warning btn-xs interface-toggle connected" disabled="disabled" title="Circuits cannot be marked as planned or connected">
|
||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||
</button>
|
||||
<a href="{% url 'circuits:circuittermination_edit' pk=iface.circuit_termination.pk %}" class="btn btn-danger btn-xs" title="Edit circuit termination">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:interfaceconnection_add' pk=device.pk %}?interface={{ iface.pk }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<a href="{% url 'dcim:interfaceconnection_add' pk=device.pk %}?interface_a={{ iface.pk }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -71,7 +80,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_interface %}
|
||||
{% if iface.connection or iface.circuit %}
|
||||
{% if iface.connection or iface.circuit_termination %}
|
||||
<button class="btn btn-danger btn-xs" disabled="disabled">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
26
netbox/templates/dcim/inc/_service.html
Normal file
26
netbox/templates/dcim/inc/_service.html
Normal file
@ -0,0 +1,26 @@
|
||||
<tr>
|
||||
<td>{{ service.name }}</td>
|
||||
<td>
|
||||
{{ service.get_protocol_display }}/{{ service.port }}
|
||||
</td>
|
||||
<td>
|
||||
{% for ip in service.ipaddresses.all %}
|
||||
<span>{{ ip.address.ip }}</span><br />
|
||||
{% empty %}
|
||||
<span class="text-muted">All IPs</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>{{ service.description }}</td>
|
||||
<td class="text-right">
|
||||
{% if perms.ipam.change_service %}
|
||||
<a href="{% url 'ipam:service_edit' pk=service.pk %}" class="btn btn-info btn-xs" title="Edit service">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.ipam.delete_service %}
|
||||
<a href="{% url 'ipam:service_delete' pk=service.pk %}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete service"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
@ -1,9 +1,19 @@
|
||||
{% extends 'utilities/obj_table.html' %}
|
||||
|
||||
{% block extra_actions %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:device_bulk_add_interface' %}" class="btn btn-primary btn-sm">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Interfaces
|
||||
</button>
|
||||
{% if perms.dcim.change_device %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}" class="formaction">Console Ports</a></li>{% endif %}
|
||||
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}" class="formaction">Console Server Ports</a></li>{% endif %}
|
||||
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}" class="formaction">Power Ports</a></li>{% endif %}
|
||||
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}" class="formaction">Power Outlets</a></li>{% endif %}
|
||||
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}" class="formaction">Interfaces</a></li>{% endif %}
|
||||
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}" class="formaction">Device Bays</a></li>{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -6,7 +6,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>{{ title }}</strong>
|
||||
<div class="pull-right">
|
||||
{% if table.rows|length > 3 %}
|
||||
{% if table.rows|length > 1 %}
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
</button>
|
||||
|
@ -27,6 +27,12 @@
|
||||
<strong>A Side</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Site</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device.rack.site }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Rack</label>
|
||||
<div class="col-md-9">
|
||||
@ -61,6 +67,7 @@
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
<div class="tab-pane" id="select">
|
||||
{% render_field form.site_b %}
|
||||
{% render_field form.rack_b %}
|
||||
{% render_field form.device_b %}
|
||||
</div>
|
||||
@ -77,8 +84,8 @@
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="form-group">
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Connect Another</button>
|
||||
<button type="submit" name="_create" class="btn btn-primary">Connect</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Connect and Add Another</button>
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,14 +6,14 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<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">
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'dcim:rack_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search racks" />
|
||||
@ -53,13 +53,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>Rack {{ rack.name }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=rack %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Rack</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>
|
||||
@ -188,7 +189,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/created_updated.html' with obj=rack %}
|
||||
</div>
|
||||
<div class="row col-md-6">
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
|
@ -7,13 +7,13 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
|
||||
<li>{{ site }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-sm-4 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 sites" />
|
||||
@ -47,13 +47,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ site.name }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=site %}
|
||||
<div class="row">
|
||||
<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">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
@ -109,6 +110,36 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contact Name</td>
|
||||
<td>
|
||||
{% if site.contact_name %}
|
||||
<span>{{ site.contact_name }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contact Phone</td>
|
||||
<td>
|
||||
{% if site.contact_phone %}
|
||||
<a href="tel:{{ site.contact_phone }}">{{ site.contact_phone }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contact E-Mail</td>
|
||||
<td>
|
||||
{% if site.contact_email %}
|
||||
<a href="mailto:{{ site.contact_email }}">{{ site.contact_email }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with site.get_custom_fields as custom_fields %}
|
||||
@ -126,7 +157,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/created_updated.html' with obj=site %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
|
@ -10,8 +10,16 @@
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.facility %}
|
||||
{% render_field form.asn %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Contact Info</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.physical_address %}
|
||||
{% render_field form.shipping_address %}
|
||||
{% render_field form.contact_name %}
|
||||
{% render_field form.contact_phone %}
|
||||
{% render_field form.contact_email %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
|
@ -53,10 +53,25 @@
|
||||
<td>Autonomous system number (optional)</td>
|
||||
<td>65000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contact Name</td>
|
||||
<td>Name of administrative contact (optional)</td>
|
||||
<td>Hank Hill</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contact Phone</td>
|
||||
<td>Phone number (optional)</td>
|
||||
<td>+1-214-555-1234</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contact E-mail</td>
|
||||
<td>E-mail address (optional)</td>
|
||||
<td>hhill@example.com</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>ASH-4 South,ash4-south,Pied Piper,Equinix DC6,65000</pre>
|
||||
<pre>ASH-4 South,ash4-south,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row home-search" style="padding: 15px 0px 20px">
|
||||
<div class="col-md-3">
|
||||
<div class="col-sm-6 col-md-3">
|
||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" placeholder="Search devices" class="form-control" />
|
||||
@ -17,7 +17,7 @@
|
||||
</form>
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-sm-6 col-md-3">
|
||||
<form action="{% url 'ipam:prefix_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" placeholder="Search prefixes" class="form-control" />
|
||||
@ -31,7 +31,7 @@
|
||||
</form>
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-sm-6 col-md-3">
|
||||
<form action="{% url 'ipam:ipaddress_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" placeholder="Search IPs" class="form-control" />
|
||||
@ -45,7 +45,7 @@
|
||||
</form>
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-sm-6 col-md-3">
|
||||
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" placeholder="Search circuits" class="form-control" />
|
||||
@ -61,7 +61,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Organization</strong>
|
||||
@ -106,7 +106,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>IPAM</strong>
|
||||
@ -157,7 +157,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-sm-6 col-md-4">
|
||||
{% if perms.secrets %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Custom Fields</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% for field, value in custom_fields.items %}
|
||||
<tr>
|
||||
<td>{{ field }}</td>
|
||||
|
@ -5,14 +5,14 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-sm-8 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">
|
||||
<div class="col-sm-4 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" />
|
||||
@ -40,13 +40,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ aggregate }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=aggregate %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Aggregate</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Family</td>
|
||||
<td>{{ aggregate.get_family_display }}</td>
|
||||
@ -79,7 +80,6 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/created_updated.html' with obj=aggregate %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% with aggregate.get_custom_fields as custom_fields %}
|
||||
|
4
netbox/templates/ipam/inc/ipadress_edit_header.html
Normal file
4
netbox/templates/ipam/inc/ipadress_edit_header.html
Normal file
@ -0,0 +1,4 @@
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}><a href="{% url 'ipam:ipaddress_add' %}">Individual</a></li>
|
||||
<li role="presentation"{% if active_tab == 'bulk_add' %} class="active"{% endif %}><a href="{% url 'ipam:ipaddress_bulk_add' %}">Bulk</a></li>
|
||||
</ul>
|
@ -1,5 +1,5 @@
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
|
||||
{% if prefix.vrf %}
|
||||
@ -8,7 +8,7 @@
|
||||
<li>{{ prefix }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'ipam:prefix_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search prefixes" />
|
||||
@ -42,6 +42,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ prefix }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=prefix %}
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if active_tab == 'prefix' %} class="active"{% endif %}><a href="{% url 'ipam:prefix' pk=prefix.pk %}">Prefix</a></li>
|
||||
<li role="presentation"{% if active_tab == 'ip-addresses' %} class="active"{% endif %}><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">IP Addresses</a></li>
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
|
||||
{% if ipaddress.vrf %}
|
||||
@ -14,7 +14,7 @@
|
||||
<li>{{ ipaddress }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'ipam:ipaddress_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search IPs" />
|
||||
@ -42,13 +42,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ ipaddress }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=ipaddress %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>IP Address</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Family</td>
|
||||
<td>{{ ipaddress.get_family_display }}</td>
|
||||
@ -136,7 +137,6 @@
|
||||
{% with ipaddress.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
{% include 'inc/created_updated.html' with obj=ipaddress %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% with heading='Parent Prefixes' %}
|
||||
|
22
netbox/templates/ipam/ipaddress_bulk_add.html
Normal file
22
netbox/templates/ipam/ipaddress_bulk_add.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Bulk Add IP Addresses{% endblock %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include 'ipam/inc/ipadress_edit_header.html' with active_tab='bulk_add' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>IP Address</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.address %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -2,6 +2,12 @@
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block tabs %}
|
||||
{% if not obj.pk %}
|
||||
{% include 'ipam/inc/ipadress_edit_header.html' with active_tab='add' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>IP Address</strong></div>
|
||||
@ -10,7 +16,7 @@
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% if obj %}
|
||||
{% if obj.pk %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Device</label>
|
||||
<div class="col-md-9">
|
||||
|
@ -11,7 +11,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Prefix</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Family</td>
|
||||
<td>{{ prefix.get_family_display }}</td>
|
||||
@ -85,6 +85,16 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Is a pool</td>
|
||||
<td>
|
||||
{% if prefix.is_pool %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>
|
||||
@ -104,7 +114,6 @@
|
||||
{% with prefix.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
{% include 'inc/created_updated.html' with obj=prefix %}
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
|
@ -12,6 +12,7 @@
|
||||
{% render_field form.vlan %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.role %}
|
||||
{% render_field form.is_pool %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -68,6 +68,11 @@
|
||||
<td>Functional role (optional)</td>
|
||||
<td>Customer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Is a pool</td>
|
||||
<td>True if all IPs are considered usable</td>
|
||||
<td>False</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
@ -76,7 +81,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,7th floor WiFi</pre>
|
||||
<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
26
netbox/templates/ipam/service_edit.html
Normal file
26
netbox/templates/ipam/service_edit.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block form %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Service</strong></div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ obj.device }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.name %}
|
||||
<div class="form-group form-inline">
|
||||
<label class="col-md-3 control-label required">Port</label>
|
||||
<div class="col-md-9">
|
||||
{{ form.protocol }}
|
||||
{{ form.port }}
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.ipaddresses %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -5,7 +5,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<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>
|
||||
@ -15,7 +15,7 @@
|
||||
<li>{{ vlan.name }} ({{ vlan.vid }})</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'ipam:vlan_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search VLANs" />
|
||||
@ -43,13 +43,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>VLAN {{ vlan.display_name }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=vlan %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>VLAN</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td><a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a></td>
|
||||
@ -113,7 +114,6 @@
|
||||
{% with vlan.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
{% include 'inc/created_updated.html' with obj=vlan %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
|
@ -5,13 +5,13 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:vrf_list' %}">VRFs</a></li>
|
||||
<li>{{ vrf }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'ipam:vrf_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search VRFs" />
|
||||
@ -39,13 +39,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ vrf }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=vrf %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>VRF</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Route Distinguisher</td>
|
||||
<td>{{ vrf.rd }}</td>
|
||||
@ -85,7 +86,6 @@
|
||||
{% with vrf.get_custom_fields as custom_fields %}
|
||||
{% include 'inc/custom_fields_panel.html' %}
|
||||
{% endwith %}
|
||||
{% include 'inc/created_updated.html' with obj=vrf %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
|
@ -2,11 +2,10 @@
|
||||
|
||||
{# Custom pagination controls to render nicely with Bootstrap CSS. smart_pages requires EnhancedPaginator. #}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="paginator pull-right">
|
||||
{% if table.paginator.num_pages > 1 %}
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
<ul class="pagination pull-right">
|
||||
{% if table.page.has_previous %}
|
||||
<li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">«</a></li>
|
||||
{% endif %}
|
||||
@ -23,8 +22,8 @@
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-5 text-right text-muted">
|
||||
<div class="clearfix"></div>
|
||||
<div class="text-right text-muted">
|
||||
Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }}
|
||||
{% if total == 1 %}
|
||||
{{ table.data.verbose_name }}
|
||||
@ -32,4 +31,4 @@
|
||||
{{ table.data.verbose_name_plural }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,6 +29,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ secret }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=secret %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
@ -58,7 +59,6 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/created_updated.html' with obj=secret %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if secret|decryptable_by:request.user %}
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></li>
|
||||
{% if tenant.group %}
|
||||
@ -14,7 +14,7 @@
|
||||
<li>{{ tenant }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'tenancy:tenant_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Name" />
|
||||
@ -42,13 +42,14 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ tenant }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=tenant %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Tenant</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>
|
||||
@ -86,7 +87,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/created_updated.html' with obj=tenant %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
|
22
netbox/templates/users/_user.html
Normal file
22
netbox/templates/users/_user.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends '_base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-8 col-md-offset-2">
|
||||
<h1>{% block title %}{% endblock %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-3 col-md-2 col-md-offset-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li{% ifequal active_tab "profile" %} class="active"{% endifequal %}><a href="{% url 'users:profile' %}">Profile</a></li>
|
||||
<li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}><a href="{% url 'users:change_password' %}">Change Password</a></li>
|
||||
<li{% ifequal active_tab "userkey" %} class="active"{% endifequal %}><a href="{% url 'users:userkey' %}">User Key</a></li>
|
||||
<li{% ifequal active_tab "recent_activity" %} class="active"{% endifequal %}><a href="{% url 'users:recent_activity' %}">Recent Activity</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-9 col-md-6">
|
||||
{% block usercontent %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,46 +1,30 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'users/_user.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Change Password{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<h1>Change Password</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 col-md-offset-2">
|
||||
{% include 'users/inc/profile_nav.html' with active_tab="change_password" %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Password</strong></div>
|
||||
{% block usercontent %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.old_password %}
|
||||
{% render_field form.new_password1 %}
|
||||
{% render_field form.new_password2 %}
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||
<a href="{% url 'users:profile' %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Password</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.old_password %}
|
||||
{% render_field form.new_password1 %}
|
||||
{% render_field form.new_password2 %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||
<a href="{% url 'users:profile' %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
@ -1,6 +0,0 @@
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li{% ifequal active_tab "profile" %} class="active"{% endifequal %}><a href="{% url 'users:profile' %}">Profile</a></li>
|
||||
<li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}><a href="{% url 'users:change_password' %}">Change Password</a></li>
|
||||
<li{% ifequal active_tab "userkey" %} class="active"{% endifequal %}><a href="{% url 'users:userkey' %}">User Key</a></li>
|
||||
<li{% ifequal active_tab "recent_activity" %} class="active"{% endifequal %}><a href="{% url 'users:recent_activity' %}">Recent Activity</a></li>
|
||||
</ul>
|
@ -1,31 +1,19 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'users/_user.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}User Profile{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<h1>User Profile</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 col-md-offset-2">
|
||||
{% include 'users/inc/profile_nav.html' with active_tab="profile" %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">User login</small>
|
||||
<h5>{{ request.user.username }}</h5>
|
||||
<small class="text-muted">Full name</small>
|
||||
<h5>{{ request.user.first_name }} {{ request.user.last_name }}</h5>
|
||||
<small class="text-muted">Email</small>
|
||||
<h5>{{ request.user.email }}</h5>
|
||||
<small class="text-muted">Registered</small>
|
||||
<h5>{{ request.user.date_joined }}</h5>
|
||||
<small class="text-muted">Groups</small>
|
||||
<h5>{{ request.user.groups.all|join:', ' }}</h5>
|
||||
<small class="text-muted">Admin access</small>
|
||||
<h5>{{ request.user.is_staff|yesno|capfirst }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{% block usercontent %}
|
||||
<small class="text-muted">User login</small>
|
||||
<h5>{{ request.user.username }}</h5>
|
||||
<small class="text-muted">Full name</small>
|
||||
<h5>{{ request.user.first_name }} {{ request.user.last_name }}</h5>
|
||||
<small class="text-muted">Email</small>
|
||||
<h5>{{ request.user.email }}</h5>
|
||||
<small class="text-muted">Registered</small>
|
||||
<h5>{{ request.user.date_joined }}</h5>
|
||||
<small class="text-muted">Groups</small>
|
||||
<h5>{{ request.user.groups.all|join:', ' }}</h5>
|
||||
<small class="text-muted">Admin access</small>
|
||||
<h5>{{ request.user.is_staff|yesno|capfirst }}</h5>
|
||||
{% endblock %}
|
||||
|
@ -1,35 +1,22 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
{% extends 'users/_user.html' %}
|
||||
|
||||
{% block title %}Recent Activity{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<h1>Recent Activity</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 col-md-offset-2">
|
||||
{% include 'users/inc/profile_nav.html' with active_tab="recent_activity" %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
{% block usercontent %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for action in recent_activity %}
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Action</th>
|
||||
<td>{{ action.time|date:'SHORT_DATETIME_FORMAT' }}</td>
|
||||
<td>{{ action.icon }} {{ action.message|safe }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for action in recent_activity %}
|
||||
<tr>
|
||||
<td>{{ action.time|date:'SHORT_DATETIME_FORMAT' }}</td>
|
||||
<td>{{ action.icon }} {{ action.message|safe }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
@ -1,46 +1,33 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
{% extends 'users/_user.html' %}
|
||||
|
||||
{% block title %}User Key{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<h1>User Key</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 col-md-offset-2">
|
||||
{% include 'users/inc/profile_nav.html' with active_tab="userkey" %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if userkey %}
|
||||
<h4>
|
||||
Your user key is:
|
||||
{% if userkey.is_active %}
|
||||
<span class="label label-success">Active</span>
|
||||
{% else %}
|
||||
<span class="label label-danger">Inactive</span>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<p>Your public key is below.</p>
|
||||
<pre>{{ userkey.public_key }}</pre>
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'users:userkey_edit' %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
Edit user key
|
||||
</a>
|
||||
</div>
|
||||
{% include 'inc/created_updated.html' with obj=userkey %}
|
||||
{% else %}
|
||||
<p>You don't have a user key on file.</p>
|
||||
<p>
|
||||
<a href="{% url 'users:userkey_edit' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Create a User Key
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% block usercontent %}
|
||||
{% if userkey %}
|
||||
<h4>
|
||||
Your user key is:
|
||||
{% if userkey.is_active %}
|
||||
<span class="label label-success">Active</span>
|
||||
{% else %}
|
||||
<span class="label label-danger">Inactive</span>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<p>Your public key is below.</p>
|
||||
<pre>{{ userkey.public_key }}</pre>
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'users:userkey_edit' %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
Edit user key
|
||||
</a>
|
||||
</div>
|
||||
{% include 'inc/created_updated.html' with obj=userkey %}
|
||||
{% else %}
|
||||
<p>You don't have a user key on file.</p>
|
||||
<p>
|
||||
<a href="{% url 'users:userkey_edit' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Create a User Key
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -1,71 +1,58 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'users/_user.html' %}
|
||||
{% load static from staticfiles %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}User Key{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<h1>User Key</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 col-md-offset-2">
|
||||
{% include 'users/inc/profile_nav.html' with active_tab="userkey" %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if userkey.is_active %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<strong>Warning:</strong> Changing your public key will require your user key to be re-activated by another
|
||||
user. You will be unable to retrieve any secrets until your key has been reactivated.
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% block usercontent %}
|
||||
{% if userkey.is_active %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<strong>Warning:</strong> Changing your public key will require your user key to be re-activated by another
|
||||
user. You will be unable to retrieve any secrets until your key has been reactivated.
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
{% render_field form.public_key %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
{% render_field form.public_key %}
|
||||
<div class="col-sm-6 col-md-6">
|
||||
<button type="button" class="btn btn-info" id="generate_keypair">Generate a New Key Pair</button>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-6 text-right">
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
<a href="{% url 'users:userkey' %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="col-md-6">
|
||||
<button type="button" class="btn btn-info" id="generate_keypair">Generate a New Key Pair</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal fade" id="new_keypair_modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-md" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="new_keypair_modal_title">
|
||||
New RSA Key Pair
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<strong>New Public Key</strong>
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" id="new_pubkey" style="height: 250px;"></textarea>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
<a href="{% url 'users:userkey' %}" class="btn btn-default">Cancel</a>
|
||||
<strong>New Private Key</strong>
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" id="new_privkey" style="height: 250px;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="new_keypair_modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-md" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="new_keypair_modal_title">
|
||||
New RSA Key Pair
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<strong>New Public Key</strong>
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" id="new_pubkey" style="height: 250px;"></textarea>
|
||||
<div class="modal-footer text-center">
|
||||
<button type="button" class="btn btn-danger" id="use_new_pubkey" data-dismiss="modal">I have saved my new private key</button>
|
||||
</div>
|
||||
<strong>New Private Key</strong>
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" id="new_privkey" style="height: 250px;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer text-center">
|
||||
<button type="button" class="btn btn-danger" id="use_new_pubkey" data-dismiss="modal">I have saved my new private key</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
|
@ -6,7 +6,7 @@
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel panel-{{ panel_class|default:"danger" }}">
|
||||
<div class="panel-heading">{% block title %}{% endblock %}</div>
|
||||
<div class="panel-body">
|
||||
{% block message %}<p>Are you sure?</p>{% endblock %}
|
||||
@ -22,7 +22,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="submit" name="_confirm" class="btn btn-danger">Confirm</button>
|
||||
<button type="submit" name="_confirm" class="btn btn-{{ button_class|default:"danger" }}">Confirm</button>
|
||||
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,16 +1,16 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}
|
||||
{% if obj %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<h3>{% if obj %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}</h3>
|
||||
<h3>{% block title %}{% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}{% endblock %}</h3>
|
||||
{% block tabs %}{% endblock %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3 text-right">
|
||||
{% if obj %}
|
||||
{% if obj.pk %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||
{% else %}
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<form method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="redirect_url" value="{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" />
|
||||
<input type="hidden" name="pk_all" value="{% for row in table.rows %}{{ row.record.pk|default:'' }}{% if not forloop.last %},{% endif %}{% endfor %}" />
|
||||
<input type="hidden" name="pk_all" value="{% for obj in table.data.queryset %}{{ obj.pk|default:'' }}{% if not forloop.last %},{% endif %}{% endfor %}" />
|
||||
{% if table.paginator.num_pages > 1 %}
|
||||
<div id="select_all_box" class="hidden alert alert-info">
|
||||
<div class="checkbox-inline">
|
||||
@ -31,3 +31,4 @@
|
||||
{% else %}
|
||||
{% render_table table table_template|default:'table.html' %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
|
@ -11,7 +11,7 @@ from .models import Tenant, TenantGroup
|
||||
# Tenant groups
|
||||
#
|
||||
|
||||
class TenantGroupForm(forms.ModelForm, BootstrapMixin):
|
||||
class TenantGroupForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@ -41,7 +41,7 @@ class TenantFromCSVForm(forms.ModelForm):
|
||||
fields = ['name', 'slug', 'group', 'description']
|
||||
|
||||
|
||||
class TenantImportForm(BulkImportForm, BootstrapMixin):
|
||||
class TenantImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=TenantFromCSVForm)
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@ from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as
|
||||
from utilities.forms import BootstrapMixin
|
||||
|
||||
|
||||
class LoginForm(AuthenticationForm, BootstrapMixin):
|
||||
class LoginForm(BootstrapMixin, AuthenticationForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoginForm, self).__init__(*args, **kwargs)
|
||||
@ -12,5 +12,5 @@ class LoginForm(AuthenticationForm, BootstrapMixin):
|
||||
self.fields['password'].widget.attrs['placeholder'] = ''
|
||||
|
||||
|
||||
class PasswordChangeForm(DjangoPasswordChangeForm, BootstrapMixin):
|
||||
class PasswordChangeForm(BootstrapMixin, DjangoPasswordChangeForm):
|
||||
pass
|
||||
|
@ -56,6 +56,7 @@ def logout(request):
|
||||
def profile(request):
|
||||
|
||||
return render(request, 'users/profile.html', {
|
||||
'active_tab': 'profile',
|
||||
})
|
||||
|
||||
|
||||
@ -75,6 +76,7 @@ def change_password(request):
|
||||
|
||||
return render(request, 'users/change_password.html', {
|
||||
'form': form,
|
||||
'active_tab': 'change_password',
|
||||
})
|
||||
|
||||
|
||||
@ -88,6 +90,7 @@ def userkey(request):
|
||||
|
||||
return render(request, 'users/userkey.html', {
|
||||
'userkey': userkey,
|
||||
'active_tab': 'userkey',
|
||||
})
|
||||
|
||||
|
||||
@ -114,6 +117,7 @@ def userkey_edit(request):
|
||||
return render(request, 'users/userkey_edit.html', {
|
||||
'userkey': userkey,
|
||||
'form': form,
|
||||
'active_tab': 'userkey',
|
||||
})
|
||||
|
||||
|
||||
@ -121,5 +125,6 @@ def userkey_edit(request):
|
||||
def recent_activity(request):
|
||||
|
||||
return render(request, 'users/recent_activity.html', {
|
||||
'recent_activity': request.user.actions.all()[:50]
|
||||
'recent_activity': request.user.actions.all()[:50],
|
||||
'active_tab': 'recent_activity',
|
||||
})
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user