mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -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.
|
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.
|
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.
|
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
|
!!! 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.
|
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
|
# Installation
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ NetBox requires a PostgreSQL database to store data. MySQL is not supported, as
|
|||||||
# postgresql-setup initdb
|
# 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
|
```no-highlight
|
||||||
host all all 127.0.0.1/32 md5
|
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
|
# 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
|
```no-highlight
|
||||||
command = '/usr/bin/gunicorn'
|
command = '/usr/bin/gunicorn'
|
||||||
@ -113,7 +113,7 @@ user = 'www-data'
|
|||||||
|
|
||||||
# supervisord Installation
|
# 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
|
```no-highlight
|
||||||
[program:netbox]
|
[program:netbox]
|
||||||
|
@ -21,11 +21,9 @@ class CircuitTypeAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Circuit)
|
@admin.register(Circuit)
|
||||||
class CircuitAdmin(admin.ModelAdmin):
|
class CircuitAdmin(admin.ModelAdmin):
|
||||||
list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed_human',
|
list_display = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate_human']
|
||||||
'upstream_speed_human', 'commit_rate_human', 'xconnect_id']
|
|
||||||
list_filter = ['provider', 'type', 'tenant']
|
list_filter = ['provider', 'type', 'tenant']
|
||||||
exclude = ['interface']
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(CircuitAdmin, self).get_queryset(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 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 dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||||
from extras.api.serializers import CustomFieldSerializer
|
from extras.api.serializers import CustomFieldSerializer
|
||||||
from tenancy.api.serializers import TenantNestedSerializer
|
from tenancy.api.serializers import TenantNestedSerializer
|
||||||
@ -45,17 +45,24 @@ class CircuitTypeNestedSerializer(CircuitTypeSerializer):
|
|||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
class CircuitTerminationSerializer(serializers.ModelSerializer):
|
||||||
provider = ProviderNestedSerializer()
|
|
||||||
type = CircuitTypeNestedSerializer()
|
|
||||||
tenant = TenantNestedSerializer()
|
|
||||||
site = SiteNestedSerializer()
|
site = SiteNestedSerializer()
|
||||||
interface = InterfaceNestedSerializer()
|
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:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
|
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'comments', 'terminations', 'custom_fields']
|
||||||
'upstream_speed', 'commit_rate', 'xconnect_id', 'comments', 'custom_fields']
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitNestedSerializer(CircuitSerializer):
|
class CircuitNestedSerializer(CircuitSerializer):
|
||||||
|
@ -43,7 +43,7 @@ class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView):
|
|||||||
"""
|
"""
|
||||||
List circuits (filterable)
|
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')
|
.prefetch_related('custom_field_values__field')
|
||||||
serializer_class = serializers.CircuitSerializer
|
serializer_class = serializers.CircuitSerializer
|
||||||
filter_class = CircuitFilter
|
filter_class = CircuitFilter
|
||||||
@ -53,6 +53,6 @@ class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
|||||||
"""
|
"""
|
||||||
Retrieve a single circuit
|
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')
|
.prefetch_related('custom_field_values__field')
|
||||||
serializer_class = serializers.CircuitSerializer
|
serializer_class = serializers.CircuitSerializer
|
||||||
|
@ -16,12 +16,12 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='circuits__site',
|
name='circuits__terminations__site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site',
|
label='Site',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='circuits__site',
|
name='circuits__terminations__site__slug',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@ -29,7 +29,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = ['q', 'name', 'account', 'asn']
|
fields = ['name', 'account', 'asn']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
@ -50,7 +50,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Provider (ID)',
|
label='Provider (ID)',
|
||||||
)
|
)
|
||||||
provider = django_filters.ModelMultipleChoiceFilter(
|
provider = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='provider',
|
name='provider__slug',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Provider (slug)',
|
label='Provider (slug)',
|
||||||
@ -61,7 +61,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Circuit type (ID)',
|
label='Circuit type (ID)',
|
||||||
)
|
)
|
||||||
type = django_filters.ModelMultipleChoiceFilter(
|
type = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='type',
|
name='type__slug',
|
||||||
queryset=CircuitType.objects.all(),
|
queryset=CircuitType.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Circuit type (slug)',
|
label='Circuit type (slug)',
|
||||||
@ -78,12 +78,12 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='terminations__site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='terminations__site__slug',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@ -91,12 +91,11 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date']
|
fields = ['install_date']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(cid__icontains=value) |
|
Q(cid__icontains=value) |
|
||||||
Q(xconnect_id__icontains=value) |
|
Q(xconnect_id__icontains=value) |
|
||||||
Q(pp_info__icontains=value) |
|
|
||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
|
@ -9,7 +9,7 @@ from utilities.forms import (
|
|||||||
SlugField,
|
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']
|
fields = ['name', 'slug', 'asn', 'account', 'portal_url']
|
||||||
|
|
||||||
|
|
||||||
class ProviderImportForm(BulkImportForm, BootstrapMixin):
|
class ProviderImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=ProviderFromCSVForm)
|
csv = CSVDataField(csv_form=ProviderFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
# Circuit types
|
# Circuit types
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
|
class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -82,6 +82,64 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitForm(BootstrapMixin, CustomFieldForm):
|
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'}))
|
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',
|
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
|
||||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
|
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',
|
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface',
|
||||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
||||||
disabled_indicator='is_connected'))
|
disabled_indicator='is_connected'))
|
||||||
comments = CommentField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = CircuitTermination
|
||||||
fields = [
|
fields = ['term_side', 'site', 'rack', 'device', 'livesearch', 'interface', 'port_speed', 'upstream_speed',
|
||||||
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
|
'xconnect_id', 'pp_info']
|
||||||
'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
|
|
||||||
]
|
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'cid': "Unique circuit ID",
|
|
||||||
'install_date': "Format: YYYY-MM-DD",
|
|
||||||
'port_speed': "Physical circuit speed",
|
'port_speed': "Physical circuit speed",
|
||||||
'commit_rate': "Commited rate",
|
|
||||||
'xconnect_id': "ID of the local cross-connect",
|
'xconnect_id': "ID of the local cross-connect",
|
||||||
'pp_info': "Patch panel ID and port number(s)"
|
'pp_info': "Patch panel ID and port number(s)"
|
||||||
}
|
}
|
||||||
|
widgets = {
|
||||||
|
'term_side': forms.HiddenInput(),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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:
|
if self.instance.interface:
|
||||||
self.initial['rack'] = self.instance.interface.device.rack
|
self.initial['rack'] = self.instance.interface.device.rack
|
||||||
self.initial['device'] = self.instance.interface.device
|
self.initial['device'] = self.instance.interface.device
|
||||||
@ -140,11 +195,13 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
|||||||
# Limit interface choices
|
# Limit interface choices
|
||||||
if self.is_bound and self.data.get('device'):
|
if self.is_bound and self.data.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.data['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')
|
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
||||||
elif self.initial.get('device'):
|
elif self.initial.get('device'):
|
||||||
interfaces = Interface.objects.filter(device=self.initial['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')
|
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||||
else:
|
else:
|
||||||
interfaces = []
|
interfaces = []
|
||||||
@ -154,47 +211,3 @@ class CircuitForm(BootstrapMixin, CustomFieldForm):
|
|||||||
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
|
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
|
||||||
}) for iface in interfaces
|
}) 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 django.db import models
|
||||||
|
|
||||||
from dcim.fields import ASNField
|
from dcim.fields import ASNField
|
||||||
from dcim.models import Site, Interface
|
|
||||||
from extras.models import CustomFieldModel, CustomFieldValue
|
from extras.models import CustomFieldModel, CustomFieldValue
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.models import CreatedUpdatedModel
|
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):
|
class Provider(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
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)
|
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
|
||||||
type = models.ForeignKey('CircuitType', 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)
|
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')
|
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)')
|
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)
|
comments = models.TextField(blank=True)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
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.provider.name,
|
||||||
self.type.name,
|
self.type.name,
|
||||||
self.tenant.name if self.tenant else '',
|
self.tenant.name if self.tenant else '',
|
||||||
self.site.name,
|
|
||||||
self.install_date.isoformat() if self.install_date else '',
|
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 '',
|
str(self.commit_rate) if self.commit_rate else '',
|
||||||
self.xconnect_id,
|
|
||||||
self.pp_info,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def _humanize_speed(self, speed):
|
def _get_termination(self, side):
|
||||||
"""
|
for ct in self.terminations.all():
|
||||||
Humanize speeds given in Kbps (e.g. 10000000 becomes '10 Gbps')
|
if ct.term_side == side:
|
||||||
"""
|
return ct
|
||||||
if speed >= 1000000000 and speed % 1000000000 == 0:
|
return None
|
||||||
return '{} Tbps'.format(speed / 1000000000)
|
|
||||||
elif speed >= 1000000 and speed % 1000000 == 0:
|
@property
|
||||||
return '{} Gbps'.format(speed / 1000000)
|
def termination_a(self):
|
||||||
elif speed >= 1000 and speed % 1000 == 0:
|
return self._get_termination('A')
|
||||||
return '{} Mbps'.format(speed / 1000)
|
|
||||||
elif speed >= 1000:
|
@property
|
||||||
return '{} Mbps'.format(float(speed) / 1000)
|
def termination_z(self):
|
||||||
else:
|
return self._get_termination('Z')
|
||||||
return '{} Kbps'.format(speed)
|
|
||||||
|
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):
|
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'
|
port_speed_human.admin_order_field = 'port_speed'
|
||||||
|
|
||||||
def upstream_speed_human(self):
|
def upstream_speed_human(self):
|
||||||
if not self.upstream_speed:
|
return '' if not self.upstream_speed else humanize_speed(self.upstream_speed)
|
||||||
return ''
|
|
||||||
return self._humanize_speed(self.upstream_speed)
|
|
||||||
upstream_speed_human.admin_order_field = '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')
|
type = tables.Column(verbose_name='Type')
|
||||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
a_side = tables.LinkColumn('dcim:site', accessor=Accessor('termination_a.site'), orderable=False,
|
||||||
port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'),
|
args=[Accessor('termination_a.site.slug')])
|
||||||
verbose_name='Port Speed')
|
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'),
|
commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'),
|
||||||
verbose_name='Commit Rate')
|
verbose_name='Commit Rate')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Circuit
|
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+)/$', views.circuit, name='circuit'),
|
||||||
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
|
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+)/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.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
|
||||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||||
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import filters, forms, tables
|
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):
|
def provider(request, slug):
|
||||||
|
|
||||||
provider = get_object_or_404(Provider, slug=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()
|
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||||
|
|
||||||
return render(request, 'circuits/provider.html', {
|
return render(request, 'circuits/provider.html', {
|
||||||
@ -103,7 +107,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitListView(ObjectListView):
|
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 = filters.CircuitFilter
|
||||||
filter_form = forms.CircuitFilterForm
|
filter_form = forms.CircuitFilterForm
|
||||||
table = tables.CircuitTable
|
table = tables.CircuitTable
|
||||||
@ -114,9 +118,13 @@ class CircuitListView(ObjectListView):
|
|||||||
def circuit(request, pk):
|
def circuit(request, pk):
|
||||||
|
|
||||||
circuit = get_object_or_404(Circuit, pk=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', {
|
return render(request, 'circuits/circuit.html', {
|
||||||
'circuit': circuit,
|
'circuit': circuit,
|
||||||
|
'termination_a': termination_a,
|
||||||
|
'termination_z': termination_z,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -124,7 +132,7 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'circuits.change_circuit'
|
permission_required = 'circuits.change_circuit'
|
||||||
model = Circuit
|
model = Circuit
|
||||||
form_class = forms.CircuitForm
|
form_class = forms.CircuitForm
|
||||||
fields_initial = ['site']
|
fields_initial = ['provider']
|
||||||
template_name = 'circuits/circuit_edit.html'
|
template_name = 'circuits/circuit_edit.html'
|
||||||
obj_list_url = 'circuits:circuit_list'
|
obj_list_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
@ -155,3 +163,71 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
permission_required = 'circuits.delete_circuit'
|
permission_required = 'circuits.delete_circuit'
|
||||||
cls = Circuit
|
cls = Circuit
|
||||||
default_redirect_url = 'circuits:circuit_list'
|
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:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
|
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||||
'custom_fields', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes',
|
||||||
|
'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
||||||
|
|
||||||
|
|
||||||
class SiteNestedSerializer(SiteSerializer):
|
class SiteNestedSerializer(SiteSerializer):
|
||||||
@ -130,14 +131,14 @@ class ManufacturerNestedSerializer(ManufacturerSerializer):
|
|||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTypeSerializer(serializers.ModelSerializer):
|
class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||||
manufacturer = ManufacturerNestedSerializer()
|
manufacturer = ManufacturerNestedSerializer()
|
||||||
subdevice_role = serializers.SerializerMethodField()
|
subdevice_role = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
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):
|
def get_subdevice_role(self, obj):
|
||||||
return {
|
return {
|
||||||
@ -197,8 +198,9 @@ class DeviceTypeDetailSerializer(DeviceTypeSerializer):
|
|||||||
|
|
||||||
class Meta(DeviceTypeSerializer.Meta):
|
class Meta(DeviceTypeSerializer.Meta):
|
||||||
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
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',
|
'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
|
||||||
'power_port_templates', 'power_outlet_templates', 'interface_templates']
|
'console_port_templates', 'cs_port_templates', 'power_port_templates', 'power_outlet_templates',
|
||||||
|
'interface_templates']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -381,7 +383,7 @@ class InterfaceNestedSerializer(InterfaceSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceDetailSerializer(InterfaceSerializer):
|
class InterfaceDetailSerializer(InterfaceSerializer):
|
||||||
connected_interface = InterfaceSerializer(source='get_connected_interface')
|
connected_interface = InterfaceSerializer()
|
||||||
|
|
||||||
class Meta(InterfaceSerializer.Meta):
|
class Meta(InterfaceSerializer.Meta):
|
||||||
fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected',
|
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)
|
rack = get_object_or_404(Rack, pk=pk)
|
||||||
face = request.GET.get('face', 0)
|
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
|
# Serialize Devices within the rack elevation
|
||||||
for u in elevation:
|
for u in elevation:
|
||||||
@ -152,20 +156,20 @@ class ManufacturerDetailView(generics.RetrieveAPIView):
|
|||||||
# Device Types
|
# Device Types
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTypeListView(generics.ListAPIView):
|
class DeviceTypeListView(CustomFieldModelAPIView, generics.ListAPIView):
|
||||||
"""
|
"""
|
||||||
List device types (filterable)
|
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
|
serializer_class = serializers.DeviceTypeSerializer
|
||||||
filter_class = filters.DeviceTypeFilter
|
filter_class = filters.DeviceTypeFilter
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeDetailView(generics.RetrieveAPIView):
|
class DeviceTypeDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
||||||
"""
|
"""
|
||||||
Retrieve a single device type
|
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
|
serializer_class = serializers.DeviceTypeDetailSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -451,7 +455,7 @@ class RelatedConnectionsView(APIView):
|
|||||||
peer_iface = Interface.objects.get(device__name=peer_device, name=peer_interface)
|
peer_iface = Interface.objects.get(device__name=peer_device, name=peer_interface)
|
||||||
except Interface.DoesNotExist:
|
except Interface.DoesNotExist:
|
||||||
raise Http404()
|
raise Http404()
|
||||||
local_iface = peer_iface.get_connected_interface()
|
local_iface = peer_iface.connected_interface
|
||||||
if local_iface:
|
if local_iface:
|
||||||
device = local_iface.device
|
device = local_iface.device
|
||||||
else:
|
else:
|
||||||
@ -484,7 +488,7 @@ class RelatedConnectionsView(APIView):
|
|||||||
|
|
||||||
# Interface connections
|
# Interface connections
|
||||||
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
|
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
|
||||||
'circuit')
|
'circuit_termination')
|
||||||
for iface in interfaces:
|
for iface in interfaces:
|
||||||
data = serializers.InterfaceDetailSerializer(instance=iface).data
|
data = serializers.InterfaceDetailSerializer(instance=iface).data
|
||||||
del(data['device'])
|
del(data['device'])
|
||||||
|
@ -50,7 +50,7 @@ class RackGroupFilter(django_filters.FilterSet):
|
|||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site__slug',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@ -58,7 +58,6 @@ class RackGroupFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackGroup
|
model = RackGroup
|
||||||
fields = ['site_id', 'site']
|
|
||||||
|
|
||||||
|
|
||||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
@ -72,7 +71,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site__slug',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@ -113,7 +112,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ['q', 'site_id', 'site', 'u_height']
|
fields = ['u_height']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
return queryset.filter(
|
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(
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='manufacturer',
|
name='manufacturer',
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
label='Manufacturer (ID)',
|
label='Manufacturer (ID)',
|
||||||
)
|
)
|
||||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='manufacturer',
|
name='manufacturer__slug',
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Manufacturer (slug)',
|
label='Manufacturer (slug)',
|
||||||
@ -138,8 +141,16 @@ class DeviceTypeFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['manufacturer_id', 'manufacturer', 'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu',
|
fields = ['model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device',
|
||||||
'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):
|
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
@ -157,7 +168,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rack__site',
|
name='rack__site__slug',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site name (slug)',
|
label='Site name (slug)',
|
||||||
@ -178,7 +189,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Role (ID)',
|
label='Role (ID)',
|
||||||
)
|
)
|
||||||
role = django_filters.ModelMultipleChoiceFilter(
|
role = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='device_role',
|
name='device_role__slug',
|
||||||
queryset=DeviceRole.objects.all(),
|
queryset=DeviceRole.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
@ -205,13 +216,13 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Manufacturer (ID)',
|
label='Manufacturer (ID)',
|
||||||
)
|
)
|
||||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='device_type__manufacturer',
|
name='device_type__manufacturer__slug',
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Manufacturer (slug)',
|
label='Manufacturer (slug)',
|
||||||
)
|
)
|
||||||
model = django_filters.ModelMultipleChoiceFilter(
|
model = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='device_type',
|
name='device_type__slug',
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Device model (slug)',
|
label='Device model (slug)',
|
||||||
@ -246,9 +257,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ['q', 'name', 'serial', 'asset_tag', 'site_id', 'site', 'rack_id', 'role_id', 'role', 'device_type_id',
|
fields = ['name', 'serial', 'asset_tag']
|
||||||
'manufacturer_id', 'manufacturer', 'model', 'platform_id', 'platform', 'status', 'is_console_server',
|
|
||||||
'is_pdu', 'is_network_device']
|
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
@ -284,7 +293,7 @@ class ConsolePortFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = ['device_id', 'device', 'name']
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortFilter(django_filters.FilterSet):
|
class ConsoleServerPortFilter(django_filters.FilterSet):
|
||||||
@ -302,7 +311,7 @@ class ConsoleServerPortFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = ['device_id', 'device', 'name']
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class PowerPortFilter(django_filters.FilterSet):
|
class PowerPortFilter(django_filters.FilterSet):
|
||||||
@ -320,7 +329,7 @@ class PowerPortFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = ['device_id', 'device', 'name']
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletFilter(django_filters.FilterSet):
|
class PowerOutletFilter(django_filters.FilterSet):
|
||||||
@ -338,7 +347,7 @@ class PowerOutletFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = ['device_id', 'device', 'name']
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceFilter(django_filters.FilterSet):
|
class InterfaceFilter(django_filters.FilterSet):
|
||||||
@ -356,7 +365,7 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['device_id', 'device', 'name']
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||||
|
@ -13,6 +13,7 @@ from utilities.forms import (
|
|||||||
SlugField,
|
SlugField,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from formfields import MACAddressFormField
|
||||||
from .models import (
|
from .models import (
|
||||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||||
@ -61,7 +62,8 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
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 = {
|
widgets = {
|
||||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||||
'shipping_address': SmallTextarea(attrs={'rows': 3}),
|
'shipping_address': SmallTextarea(attrs={'rows': 3}),
|
||||||
@ -81,19 +83,20 @@ class SiteFromCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
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)
|
csv = CSVDataField(csv_form=SiteFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
|
asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = ['tenant']
|
nullable_fields = ['tenant', 'asn']
|
||||||
|
|
||||||
|
|
||||||
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
@ -106,7 +109,7 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
# Rack groups
|
# Rack groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackGroupForm(forms.ModelForm, BootstrapMixin):
|
class RackGroupForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -114,7 +117,7 @@ class RackGroupForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['site', 'name', 'slug']
|
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')
|
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
|
# Rack roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackRoleForm(forms.ModelForm, BootstrapMixin):
|
class RackRoleForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -208,7 +211,7 @@ class RackFromCSVForm(forms.ModelForm):
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
class RackImportForm(BulkImportForm, BootstrapMixin):
|
class RackImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=RackFromCSVForm)
|
csv = CSVDataField(csv_form=RackFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
@ -242,7 +245,7 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
|
||||||
class ManufacturerForm(forms.ModelForm, BootstrapMixin):
|
class ManufacturerForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -254,16 +257,16 @@ class ManufacturerForm(forms.ModelForm, BootstrapMixin):
|
|||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
|
class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
||||||
slug = SlugField(slug_source='model')
|
slug = SlugField(slug_source='model')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
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)
|
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
||||||
u_height = forms.IntegerField(min_value=1, required=False)
|
u_height = forms.IntegerField(min_value=1, required=False)
|
||||||
@ -272,7 +275,8 @@ class DeviceTypeBulkEditForm(BulkEditForm, BootstrapMixin):
|
|||||||
nullable_fields = []
|
nullable_fields = []
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
|
class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
|
model = DeviceType
|
||||||
manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||||
to_field_name='slug')
|
to_field_name='slug')
|
||||||
|
|
||||||
@ -281,44 +285,76 @@ class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
|
|||||||
# Device component templates
|
# Device component templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortTemplateForm(forms.ModelForm, BootstrapMixin):
|
class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePortTemplate
|
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')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPortTemplate
|
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')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPortTemplate
|
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')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutletTemplate
|
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')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceTemplate
|
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):
|
class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
@ -329,19 +365,25 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
nullable_fields = []
|
nullable_fields = []
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateForm(forms.ModelForm, BootstrapMixin):
|
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBayTemplate
|
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
|
# Device roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceRoleForm(forms.ModelForm, BootstrapMixin):
|
class DeviceRoleForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -353,7 +395,7 @@ class DeviceRoleForm(forms.ModelForm, BootstrapMixin):
|
|||||||
# Platforms
|
# Platforms
|
||||||
#
|
#
|
||||||
|
|
||||||
class PlatformForm(forms.ModelForm, BootstrapMixin):
|
class PlatformForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
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]
|
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
|
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:
|
else:
|
||||||
|
|
||||||
# An object that doesn't exist yet can't have any IPs assigned to it
|
# 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))
|
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)
|
csv = CSVDataField(csv_form=DeviceFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
class ChildDeviceImportForm(BulkImportForm, BootstrapMixin):
|
class ChildDeviceImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
|
csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
@ -587,18 +633,6 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
nullable_fields = ['tenant', 'platform']
|
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):
|
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Device
|
model = Device
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug')
|
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')
|
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
|
# Console ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortForm(forms.ModelForm, BootstrapMixin):
|
class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
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')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -670,7 +720,7 @@ class ConsoleConnectionCSVForm(forms.Form):
|
|||||||
.format(self.cleaned_data['device'], self.cleaned_data['console_port']))
|
.format(self.cleaned_data['device'], self.cleaned_data['console_port']))
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionImportForm(BulkImportForm, BootstrapMixin):
|
class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
|
csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -700,7 +750,7 @@ class ConsoleConnectionImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
self.cleaned_data['csv'] = connection_list
|
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,
|
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||||
widget=forms.Select(attrs={'filter-for': 'console_server'}))
|
widget=forms.Select(attrs={'filter-for': 'console_server'}))
|
||||||
console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
|
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
|
# Console server ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsoleServerPortForm(forms.ModelForm, BootstrapMixin):
|
class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
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')
|
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,
|
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||||
@ -816,7 +866,7 @@ class ConsoleServerPortConnectionForm(forms.Form, BootstrapMixin):
|
|||||||
# Power ports
|
# Power ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPortForm(forms.ModelForm, BootstrapMixin):
|
class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
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')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
@ -867,7 +917,7 @@ class PowerConnectionCSVForm(forms.Form):
|
|||||||
.format(self.cleaned_data['device'], self.cleaned_data['power_port']))
|
.format(self.cleaned_data['device'], self.cleaned_data['power_port']))
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionImportForm(BulkImportForm, BootstrapMixin):
|
class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=PowerConnectionCSVForm)
|
csv = CSVDataField(csv_form=PowerConnectionCSVForm)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -897,7 +947,7 @@ class PowerConnectionImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
self.cleaned_data['csv'] = connection_list
|
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,
|
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||||
widget=forms.Select(attrs={'filter-for': 'pdu'}))
|
widget=forms.Select(attrs={'filter-for': 'pdu'}))
|
||||||
pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
|
pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
|
||||||
@ -950,7 +1000,7 @@ class PowerPortConnectionForm(forms.ModelForm, BootstrapMixin):
|
|||||||
# Power outlets
|
# Power outlets
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerOutletForm(forms.ModelForm, BootstrapMixin):
|
class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
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')
|
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,
|
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||||
@ -1012,7 +1062,7 @@ class PowerOutletConnectionForm(forms.Form, BootstrapMixin):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceForm(forms.ModelForm, BootstrapMixin):
|
class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
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')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||||
class Meta:
|
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
||||||
model = Interface
|
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||||
fields = ['name_pattern', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
@ -1043,10 +1093,13 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
# Interface connections
|
# Interface connections
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
|
class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||||
interface_a = forms.ChoiceField(choices=[], widget=SelectWithDisabled, label='Interface')
|
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,
|
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,
|
device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
|
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
|
||||||
display_field='display_name',
|
display_field='display_name',
|
||||||
@ -1060,21 +1113,27 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceConnection
|
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):
|
def __init__(self, device_a, *args, **kwargs):
|
||||||
|
|
||||||
super(InterfaceConnectionForm, self).__init__(*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
|
# Initialize interface A choices
|
||||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \
|
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL)\
|
||||||
.select_related('circuit', 'connected_as_a', 'connected_as_b')
|
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
||||||
self.fields['interface_a'].choices = [
|
self.fields['interface_a'].choices = [
|
||||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
|
(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
|
# Initialize device_b choices if rack_b is set
|
||||||
if self.is_bound and self.data.get('rack_b'):
|
if self.is_bound and self.data.get('rack_b'):
|
||||||
self.fields['device_b'].queryset = Device.objects.filter(rack__pk=self.data['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
|
# Initialize interface_b choices if device_b is set
|
||||||
if self.is_bound:
|
if self.is_bound:
|
||||||
device_b_interfaces = Interface.objects.filter(device=self.data['device_b']) \
|
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')
|
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
||||||
|
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
||||||
elif self.initial.get('device_b'):
|
elif self.initial.get('device_b'):
|
||||||
device_b_interfaces = Interface.objects.filter(device=self.initial['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')
|
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
||||||
|
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
||||||
else:
|
else:
|
||||||
device_b_interfaces = []
|
device_b_interfaces = []
|
||||||
self.fields['interface_b'].choices = [
|
self.fields['interface_b'].choices = [
|
||||||
@ -1139,7 +1200,7 @@ class InterfaceConnectionCSVForm(forms.Form):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionImportForm(BulkImportForm, BootstrapMixin):
|
class InterfaceConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
|
csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -1179,7 +1240,7 @@ class InterfaceConnectionImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
self.cleaned_data['csv'] = connection_list
|
self.cleaned_data['csv'] = connection_list
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin):
|
class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form):
|
||||||
confirm = forms.BooleanField(required=True)
|
confirm = forms.BooleanField(required=True)
|
||||||
# Used for HTTP redirect upon successful deletion
|
# Used for HTTP redirect upon successful deletion
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
|
||||||
@ -1189,7 +1250,7 @@ class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin):
|
|||||||
# Device bays
|
# Device bays
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceBayForm(forms.ModelForm, BootstrapMixin):
|
class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBay
|
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')
|
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',
|
installed_device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Child Device',
|
||||||
help_text="Child devices must first be created within the rack occupied "
|
help_text="Child devices must first be created within the rack occupied "
|
||||||
"by the parent device. Then they can be assigned to a bay.")
|
"by the parent device. Then they can be assigned to a bay.")
|
||||||
@ -1224,15 +1285,15 @@ class PopulateDeviceBayForm(forms.Form, BootstrapMixin):
|
|||||||
# Connections
|
# Connections
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsoleConnectionFilterForm(forms.Form, BootstrapMixin):
|
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
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')
|
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')
|
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||||
|
|
||||||
|
|
||||||
@ -1270,7 +1331,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
|||||||
# Modules
|
# Modules
|
||||||
#
|
#
|
||||||
|
|
||||||
class ModuleForm(forms.ModelForm, BootstrapMixin):
|
class ModuleForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
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 import models
|
||||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||||
|
|
||||||
|
from circuits.models import Circuit
|
||||||
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
|
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
|
||||||
from extras.rpc import RPC_CLIENTS
|
from extras.rpc import RPC_CLIENTS
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -244,6 +245,9 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
||||||
physical_address = models.CharField(max_length=200, blank=True)
|
physical_address = models.CharField(max_length=200, blank=True)
|
||||||
shipping_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)
|
comments = models.TextField(blank=True)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
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.slug,
|
||||||
self.tenant.name if self.tenant else '',
|
self.tenant.name if self.tenant else '',
|
||||||
self.facility,
|
self.facility,
|
||||||
str(self.asn),
|
str(self.asn) if self.asn else '',
|
||||||
|
self.contact_name,
|
||||||
|
self.contact_phone,
|
||||||
|
self.contact_email,
|
||||||
])
|
])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -285,7 +292,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def count_circuits(self):
|
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 '',
|
self.get_type_display() if self.type else '',
|
||||||
str(self.width),
|
str(self.width),
|
||||||
str(self.u_height),
|
str(self.u_height),
|
||||||
|
'True' if self.desc_units else '',
|
||||||
])
|
])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -520,7 +528,7 @@ class Manufacturer(models.Model):
|
|||||||
return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug)
|
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
|
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).
|
well as high-level functional role(s).
|
||||||
@ -552,6 +560,8 @@ class DeviceType(models.Model):
|
|||||||
choices=SUBDEVICE_ROLE_CHOICES,
|
choices=SUBDEVICE_ROLE_CHOICES,
|
||||||
help_text="Parent devices house child devices in device bays. Select "
|
help_text="Parent devices house child devices in device bays. Select "
|
||||||
"\"None\" if this device type is neither a parent nor a child.")
|
"\"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:
|
class Meta:
|
||||||
ordering = ['manufacturer', 'model']
|
ordering = ['manufacturer', 'model']
|
||||||
@ -1136,7 +1146,7 @@ class Interface(models.Model):
|
|||||||
@property
|
@property
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
try:
|
try:
|
||||||
return bool(self.circuit)
|
return bool(self.circuit_termination)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
return bool(self.connection)
|
return bool(self.connection)
|
||||||
@ -1153,13 +1163,18 @@ class Interface(models.Model):
|
|||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_connected_interface(self):
|
@property
|
||||||
connection = InterfaceConnection.objects.select_related().filter(Q(interface_a=self) | Q(interface_b=self))\
|
def connected_interface(self):
|
||||||
.first()
|
try:
|
||||||
if connection and connection.interface_a == self:
|
if self.connected_as_a:
|
||||||
return connection.interface_b
|
return self.connected_as_a.interface_b
|
||||||
elif connection:
|
except ObjectDoesNotExist:
|
||||||
return connection.interface_a
|
pass
|
||||||
|
try:
|
||||||
|
if self.connected_as_b:
|
||||||
|
return self.connected_as_b.interface_a
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,6 +22,9 @@ class SiteTest(APITestCase):
|
|||||||
'asn',
|
'asn',
|
||||||
'physical_address',
|
'physical_address',
|
||||||
'shipping_address',
|
'shipping_address',
|
||||||
|
'contact_name',
|
||||||
|
'contact_phone',
|
||||||
|
'contact_email',
|
||||||
'comments',
|
'comments',
|
||||||
'custom_fields',
|
'custom_fields',
|
||||||
'count_prefixes',
|
'count_prefixes',
|
||||||
@ -233,6 +236,8 @@ class DeviceTypeTest(APITestCase):
|
|||||||
'is_pdu',
|
'is_pdu',
|
||||||
'is_network_device',
|
'is_network_device',
|
||||||
'subdevice_role',
|
'subdevice_role',
|
||||||
|
'comments',
|
||||||
|
'custom_fields',
|
||||||
]
|
]
|
||||||
|
|
||||||
nested_fields = [
|
nested_fields = [
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from ipam.views import ServiceEditView
|
||||||
from secrets.views import secret_add
|
from secrets.views import secret_add
|
||||||
|
|
||||||
from . import views
|
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+)/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+)/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<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
|
# 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'^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+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
|
||||||
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
|
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'),
|
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||||
|
|
||||||
# Console server ports
|
# 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'^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+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
|
||||||
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
|
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'),
|
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||||
|
|
||||||
# Power ports
|
# 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'^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+)/connect/$', views.powerport_connect, name='powerport_connect'),
|
||||||
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
|
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'),
|
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||||
|
|
||||||
# Power outlets
|
# 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'^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+)/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+)/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+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
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
|
# 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'^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+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
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/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
|
||||||
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
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
|
# 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+)/edit/$', views.ModuleEditView.as_view(), name='module_edit'),
|
||||||
url(r'^modules/(?P<pk>\d+)/delete/$', views.ModuleDeleteView.as_view(), name='module_delete'),
|
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 import messages
|
||||||
from django.contrib.auth.decorators import permission_required
|
from django.contrib.auth.decorators import permission_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.http import HttpResponseRedirect
|
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.utils.http import urlencode
|
||||||
from django.views.generic import View
|
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 circuits.models import Circuit
|
||||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
@ -57,6 +56,66 @@ def expand_pattern(string):
|
|||||||
yield "{0}{1}".format(lead, i)
|
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
|
# Sites
|
||||||
#
|
#
|
||||||
@ -78,7 +137,7 @@ def site(request, slug):
|
|||||||
'device_count': Device.objects.filter(rack__site=site).count(),
|
'device_count': Device.objects.filter(rack__site=site).count(),
|
||||||
'prefix_count': Prefix.objects.filter(site=site).count(),
|
'prefix_count': Prefix.objects.filter(site=site).count(),
|
||||||
'vlan_count': VLAN.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'))
|
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
|
||||||
topology_maps = TopologyMap.objects.filter(site=site)
|
topology_maps = TopologyMap.objects.filter(site=site)
|
||||||
@ -331,6 +390,7 @@ class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
permission_required = 'dcim.change_devicetype'
|
permission_required = 'dcim.change_devicetype'
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
form_class = forms.DeviceTypeForm
|
form_class = forms.DeviceTypeForm
|
||||||
|
template_name = 'dcim/devicetype_edit.html'
|
||||||
obj_list_url = 'dcim:devicetype_list'
|
obj_list_url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
@ -358,69 +418,30 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
# Device type components
|
# Device type components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ComponentTemplateCreateView(View):
|
class ConsolePortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
model = None
|
permission_required = 'dcim.add_consoleporttemplate'
|
||||||
form = None
|
parent_model = DeviceType
|
||||||
|
parent_field = 'device_type'
|
||||||
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):
|
|
||||||
model = ConsolePortTemplate
|
model = ConsolePortTemplate
|
||||||
form = forms.ConsolePortTemplateForm
|
form = forms.ConsolePortTemplateCreateForm
|
||||||
|
model_form = forms.ConsolePortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_consoleporttemplate'
|
permission_required = 'dcim.delete_consoleporttemplate'
|
||||||
|
parent_model = DeviceType
|
||||||
|
parent_field = 'device_type'
|
||||||
cls = ConsolePortTemplate
|
cls = ConsolePortTemplate
|
||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateAddView(ComponentTemplateCreateView):
|
class ConsoleServerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
|
permission_required = 'dcim.add_consoleserverporttemplate'
|
||||||
|
parent_model = DeviceType
|
||||||
|
parent_field = 'device_type'
|
||||||
model = ConsoleServerPortTemplate
|
model = ConsoleServerPortTemplate
|
||||||
form = forms.ConsoleServerPortTemplateForm
|
form = forms.ConsoleServerPortTemplateCreateForm
|
||||||
|
model_form = forms.ConsoleServerPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
@ -429,9 +450,13 @@ class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDelet
|
|||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateAddView(ComponentTemplateCreateView):
|
class PowerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
|
permission_required = 'dcim.add_powerporttemplate'
|
||||||
|
parent_model = DeviceType
|
||||||
|
parent_field = 'device_type'
|
||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
form = forms.PowerPortTemplateForm
|
form = forms.PowerPortTemplateCreateForm
|
||||||
|
model_form = forms.PowerPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
@ -440,9 +465,13 @@ class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateAddView(ComponentTemplateCreateView):
|
class PowerOutletTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
|
permission_required = 'dcim.add_poweroutlettemplate'
|
||||||
|
parent_model = DeviceType
|
||||||
|
parent_field = 'device_type'
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
form = forms.PowerOutletTemplateForm
|
form = forms.PowerOutletTemplateCreateForm
|
||||||
|
model_form = forms.PowerOutletTemplateForm
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
@ -451,9 +480,13 @@ class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
|
|||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateAddView(ComponentTemplateCreateView):
|
class InterfaceTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
|
permission_required = 'dcim.add_interfacetemplate'
|
||||||
|
parent_model = DeviceType
|
||||||
|
parent_field = 'device_type'
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
form = forms.InterfaceTemplateForm
|
form = forms.InterfaceTemplateCreateForm
|
||||||
|
model_form = forms.InterfaceTemplateForm
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
@ -470,9 +503,13 @@ class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateAddView(ComponentTemplateCreateView):
|
class DeviceBayTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
|
permission_required = 'dcim.add_devicebaytemplate'
|
||||||
|
parent_model = DeviceType
|
||||||
|
parent_field = 'device_type'
|
||||||
model = DeviceBayTemplate
|
model = DeviceBayTemplate
|
||||||
form = forms.DeviceBayTemplateForm
|
form = forms.DeviceBayTemplateCreateForm
|
||||||
|
model_form = forms.DeviceBayTemplateForm
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
@ -560,21 +597,26 @@ def device(request, pk):
|
|||||||
power_outlets = natsorted(
|
power_outlets = natsorted(
|
||||||
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
||||||
)
|
)
|
||||||
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
|
interfaces = Interface.objects.filter(device=device, mgmt_only=False).select_related(
|
||||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
'connected_as_a__interface_b__device',
|
||||||
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
|
'connected_as_b__interface_a__device',
|
||||||
.select_related('connected_as_a', 'connected_as_b', 'circuit')
|
'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(
|
device_bays = natsorted(
|
||||||
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
||||||
key=attrgetter('name')
|
key=attrgetter('name')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Gather any secrets which belong to this device
|
# Gather relevant device objects
|
||||||
secrets = device.secrets.all()
|
|
||||||
|
|
||||||
# Find all IP addresses assigned to this device
|
|
||||||
ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface', 'vrf')\
|
ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface', 'vrf')\
|
||||||
.order_by('address')
|
.order_by('address')
|
||||||
|
services = Service.objects.filter(device=device)
|
||||||
|
secrets = device.secrets.all()
|
||||||
|
|
||||||
# Find any related devices for convenient linking in the UI
|
# Find any related devices for convenient linking in the UI
|
||||||
related_devices = []
|
related_devices = []
|
||||||
@ -604,6 +646,7 @@ def device(request, pk):
|
|||||||
'mgmt_interfaces': mgmt_interfaces,
|
'mgmt_interfaces': mgmt_interfaces,
|
||||||
'device_bays': device_bays,
|
'device_bays': device_bays,
|
||||||
'ip_addresses': ip_addresses,
|
'ip_addresses': ip_addresses,
|
||||||
|
'services': services,
|
||||||
'secrets': secrets,
|
'secrets': secrets,
|
||||||
'related_devices': related_devices,
|
'related_devices': related_devices,
|
||||||
'show_graphs': show_graphs,
|
'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
|
# Console ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@permission_required('dcim.add_consoleport')
|
class ConsolePortAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
def consoleport_add(request, pk):
|
permission_required = 'dcim.add_consoleport'
|
||||||
|
parent_model = Device
|
||||||
device = get_object_or_404(Device, pk=pk)
|
parent_field = 'device'
|
||||||
|
model = ConsolePort
|
||||||
if request.method == 'POST':
|
form = forms.ConsolePortCreateForm
|
||||||
form = forms.ConsolePortCreateForm(request.POST)
|
model_form = forms.ConsolePortForm
|
||||||
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}),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@permission_required('dcim.change_consoleport')
|
@permission_required('dcim.change_consoleport')
|
||||||
@ -891,44 +830,13 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
# Console server ports
|
# Console server ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@permission_required('dcim.add_consoleserverport')
|
class ConsoleServerPortAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
def consoleserverport_add(request, pk):
|
permission_required = 'dcim.add_consoleserverport'
|
||||||
|
parent_model = Device
|
||||||
device = get_object_or_404(Device, pk=pk)
|
parent_field = 'device'
|
||||||
|
model = ConsoleServerPort
|
||||||
if request.method == 'POST':
|
form = forms.ConsoleServerPortCreateForm
|
||||||
form = forms.ConsoleServerPortCreateForm(request.POST)
|
model_form = forms.ConsoleServerPortForm
|
||||||
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}),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@permission_required('dcim.change_consoleserverport')
|
@permission_required('dcim.change_consoleserverport')
|
||||||
@ -1012,43 +920,13 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
# Power ports
|
# Power ports
|
||||||
#
|
#
|
||||||
|
|
||||||
@permission_required('dcim.add_powerport')
|
class PowerPortAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
def powerport_add(request, pk):
|
permission_required = 'dcim.add_powerport'
|
||||||
|
parent_model = Device
|
||||||
device = get_object_or_404(Device, pk=pk)
|
parent_field = 'device'
|
||||||
|
model = PowerPort
|
||||||
if request.method == 'POST':
|
form = forms.PowerPortCreateForm
|
||||||
form = forms.PowerPortCreateForm(request.POST)
|
model_form = forms.PowerPortForm
|
||||||
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}),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@permission_required('dcim.change_powerport')
|
@permission_required('dcim.change_powerport')
|
||||||
@ -1138,43 +1016,13 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
# Power outlets
|
# Power outlets
|
||||||
#
|
#
|
||||||
|
|
||||||
@permission_required('dcim.add_poweroutlet')
|
class PowerOutletAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
def poweroutlet_add(request, pk):
|
permission_required = 'dcim.add_poweroutlet'
|
||||||
|
parent_model = Device
|
||||||
device = get_object_or_404(Device, pk=pk)
|
parent_field = 'device'
|
||||||
|
model = PowerOutlet
|
||||||
if request.method == 'POST':
|
form = forms.PowerOutletCreateForm
|
||||||
form = forms.PowerOutletCreateForm(request.POST)
|
model_form = forms.PowerOutletForm
|
||||||
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}),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@permission_required('dcim.change_poweroutlet')
|
@permission_required('dcim.change_poweroutlet')
|
||||||
@ -1257,47 +1105,13 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
@permission_required('dcim.add_interface')
|
class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
def interface_add(request, pk):
|
permission_required = 'dcim.add_interface'
|
||||||
|
parent_model = Device
|
||||||
device = get_object_or_404(Device, pk=pk)
|
parent_field = 'device'
|
||||||
|
model = Interface
|
||||||
if request.method == 'POST':
|
form = forms.InterfaceCreateForm
|
||||||
form = forms.InterfaceCreateForm(request.POST)
|
model_form = forms.InterfaceForm
|
||||||
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 InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
|
class InterfaceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
@ -1329,44 +1143,13 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
# Device bays
|
# Device bays
|
||||||
#
|
#
|
||||||
|
|
||||||
@permission_required('dcim.add_devicebay')
|
class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
def devicebay_add(request, pk):
|
permission_required = 'dcim.add_devicebay'
|
||||||
|
parent_model = Device
|
||||||
device = get_object_or_404(Device, pk=pk)
|
parent_field = 'device'
|
||||||
|
model = DeviceBay
|
||||||
if request.method == 'POST':
|
form = forms.DeviceBayCreateForm
|
||||||
form = forms.DeviceBayCreateForm(request.POST)
|
model_form = forms.DeviceBayForm
|
||||||
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 DeviceBayEditView(PermissionRequiredMixin, ObjectEditView):
|
class DeviceBayEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
@ -1436,6 +1219,112 @@ class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
parent_cls = Device
|
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
|
# Interface connections
|
||||||
#
|
#
|
||||||
@ -1467,9 +1356,11 @@ def interfaceconnection_add(request, pk):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
form = forms.InterfaceConnectionForm(device, initial={
|
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),
|
'rack_b': request.GET.get('rack_b', None),
|
||||||
'device_b': request.GET.get('device_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', {
|
return render(request, 'dcim/interfaceconnection_edit.html', {
|
||||||
@ -1602,39 +1493,17 @@ def ipaddress_assign(request, pk):
|
|||||||
# Modules
|
# 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):
|
class ModuleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_module'
|
permission_required = 'dcim.change_module'
|
||||||
model = Module
|
model = Module
|
||||||
form_class = forms.ModuleForm
|
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):
|
class ModuleDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_module'
|
permission_required = 'dcim.delete_module'
|
||||||
|
@ -44,7 +44,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
|
|||||||
|
|
||||||
# Date
|
# Date
|
||||||
elif cf.type == CF_TYPE_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
|
# Select
|
||||||
elif cf.type == CF_TYPE_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.model = cf
|
||||||
field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize()
|
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
|
field_dict[field_name] = field
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from django.utils.safestring import mark_safe
|
|||||||
|
|
||||||
|
|
||||||
CUSTOMFIELD_MODELS = (
|
CUSTOMFIELD_MODELS = (
|
||||||
'site', 'rack', 'device', # DCIM
|
'site', 'rack', 'devicetype', 'device', # DCIM
|
||||||
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
|
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
|
||||||
'provider', 'circuit', # Circuits
|
'provider', 'circuit', # Circuits
|
||||||
'tenant', # Tenants
|
'tenant', # Tenants
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from rest_framework import serializers
|
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 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
|
from tenancy.api.serializers import TenantNestedSerializer
|
||||||
|
|
||||||
|
|
||||||
@ -138,7 +138,7 @@ class PrefixSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
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']
|
'custom_fields']
|
||||||
|
|
||||||
|
|
||||||
@ -170,3 +170,22 @@ class IPAddressNestedSerializer(IPAddressSerializer):
|
|||||||
|
|
||||||
IPAddressSerializer._declared_fields['nat_inside'] = IPAddressNestedSerializer()
|
IPAddressSerializer._declared_fields['nat_inside'] = IPAddressNestedSerializer()
|
||||||
IPAddressSerializer._declared_fields['nat_outside'] = 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/$', VLANListView.as_view(), name='vlan_list'),
|
||||||
url(r'^vlans/(?P<pk>\d+)/$', VLANDetailView.as_view(), name='vlan_detail'),
|
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 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 ipam import filters
|
||||||
|
|
||||||
from extras.api.views import CustomFieldModelAPIView
|
from extras.api.views import CustomFieldModelAPIView
|
||||||
@ -177,3 +177,24 @@ class VLANDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
|
|||||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')\
|
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')\
|
||||||
.prefetch_related('custom_field_values__field')
|
.prefetch_related('custom_field_values__field')
|
||||||
serializer_class = serializers.VLANSerializer
|
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 tenancy.models import Tenant
|
||||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
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):
|
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
@ -43,7 +43,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ['name', 'rd']
|
fields = ['rd']
|
||||||
|
|
||||||
|
|
||||||
class RIRFilter(django_filters.FilterSet):
|
class RIRFilter(django_filters.FilterSet):
|
||||||
@ -64,7 +64,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='RIR (ID)',
|
label='RIR (ID)',
|
||||||
)
|
)
|
||||||
rir = django_filters.ModelMultipleChoiceFilter(
|
rir = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rir',
|
name='rir__slug',
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='RIR (slug)',
|
label='RIR (slug)',
|
||||||
@ -72,7 +72,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
fields = ['family', 'rir_id', 'rir', 'date_added']
|
fields = ['family', 'date_added']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
@ -149,7 +149,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['family', 'site_id', 'site', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
|
fields = ['family', 'status']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
@ -226,7 +226,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
)
|
)
|
||||||
device = django_filters.ModelMultipleChoiceFilter(
|
device = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='interface__device',
|
name='interface__device__name',
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Device (name)',
|
label='Device (name)',
|
||||||
@ -239,7 +239,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['q', 'family', 'status', 'device_id', 'device', 'interface_id']
|
fields = ['q', 'family', 'status']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
qs_filter = Q(description__icontains=value)
|
qs_filter = Q(description__icontains=value)
|
||||||
@ -268,7 +268,7 @@ class VLANGroupFilter(django_filters.FilterSet):
|
|||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site__slug',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@ -276,7 +276,6 @@ class VLANGroupFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = ['site_id', 'site']
|
|
||||||
|
|
||||||
|
|
||||||
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
@ -290,7 +289,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site__slug',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@ -340,7 +339,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['site_id', 'site', 'vid', 'name', 'status', 'role_id', 'role']
|
fields = ['status']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
||||||
@ -349,3 +348,21 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
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 extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
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 (
|
from .models import (
|
||||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup,
|
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
|
||||||
VLAN_STATUS_CHOICES, VRF,
|
VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ class VRFFromCSVForm(forms.ModelForm):
|
|||||||
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||||
|
|
||||||
|
|
||||||
class VRFImportForm(BulkImportForm, BootstrapMixin):
|
class VRFImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=VRFFromCSVForm)
|
csv = CSVDataField(csv_form=VRFFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
# RIRs
|
# RIRs
|
||||||
#
|
#
|
||||||
|
|
||||||
class RIRForm(forms.ModelForm, BootstrapMixin):
|
class RIRForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -78,7 +79,7 @@ class RIRForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['name', 'slug', 'is_private']
|
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=[
|
is_private = forms.NullBooleanField(required=False, label='Private', widget=forms.Select(choices=[
|
||||||
('', '---------'),
|
('', '---------'),
|
||||||
('True', 'Yes'),
|
('True', 'Yes'),
|
||||||
@ -111,7 +112,7 @@ class AggregateFromCSVForm(forms.ModelForm):
|
|||||||
fields = ['prefix', 'rir', 'date_added', 'description']
|
fields = ['prefix', 'rir', 'date_added', 'description']
|
||||||
|
|
||||||
|
|
||||||
class AggregateImportForm(BulkImportForm, BootstrapMixin):
|
class AggregateImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=AggregateFromCSVForm)
|
csv = CSVDataField(csv_form=AggregateFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
@ -136,7 +137,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
# Roles
|
# Roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class RoleForm(forms.ModelForm, BootstrapMixin):
|
class RoleForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -157,15 +158,7 @@ class PrefixForm(BootstrapMixin, CustomFieldForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description']
|
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'is_pool', '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",
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(PrefixForm, self).__init__(*args, **kwargs)
|
super(PrefixForm, self).__init__(*args, **kwargs)
|
||||||
@ -196,7 +189,7 @@ class PrefixFromCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
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']
|
'description']
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -235,7 +228,7 @@ class PrefixFromCSVForm(forms.ModelForm):
|
|||||||
return super(PrefixFromCSVForm, self).save(*args, **kwargs)
|
return super(PrefixFromCSVForm, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class PrefixImportForm(BulkImportForm, BootstrapMixin):
|
class PrefixImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=PrefixFromCSVForm)
|
csv = CSVDataField(csv_form=PrefixFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
@ -339,6 +332,14 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
|||||||
self.fields['nat_inside'].choices = []
|
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):
|
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
|
||||||
widget=forms.Select(attrs={'filter-for': 'rack'}))
|
widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||||
@ -417,7 +418,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
|||||||
return super(IPAddressFromCSVForm, self).save(*args, **kwargs)
|
return super(IPAddressFromCSVForm, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class IPAddressImportForm(BulkImportForm, BootstrapMixin):
|
class IPAddressImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=IPAddressFromCSVForm)
|
csv = CSVDataField(csv_form=IPAddressFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
@ -456,7 +457,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
# VLAN groups
|
# VLAN groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class VLANGroupForm(forms.ModelForm, BootstrapMixin):
|
class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -464,7 +465,7 @@ class VLANGroupForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['site', 'name', 'slug']
|
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')
|
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
|
return m
|
||||||
|
|
||||||
|
|
||||||
class VLANImportForm(BulkImportForm, BootstrapMixin):
|
class VLANImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=VLANFromCSVForm)
|
csv = CSVDataField(csv_form=VLANFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
@ -563,3 +564,25 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
|
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
|
||||||
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||||
null_option=(0, 'None'))
|
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):
|
class VRF(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
|
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.
|
assigned to a VLAN where appropriate.
|
||||||
"""
|
"""
|
||||||
family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
|
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)
|
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,
|
vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
|
||||||
verbose_name='VRF')
|
verbose_name='VRF')
|
||||||
tenant = models.ForeignKey(Tenant, related_name='prefixes', blank=True, null=True, on_delete=models.PROTECT)
|
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,
|
vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
|
||||||
verbose_name='VLAN')
|
verbose_name='VLAN')
|
||||||
status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
|
status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=PREFIX_STATUS_ACTIVE,
|
||||||
role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True)
|
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)
|
description = models.CharField(max_length=100, blank=True)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
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.vrf.rd if self.vrf else '',
|
||||||
self.tenant.name if self.tenant else '',
|
self.tenant.name if self.tenant else '',
|
||||||
self.site.name if self.site 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.get_status_display(),
|
||||||
self.role.name if self.role else '',
|
self.role.name if self.role else '',
|
||||||
|
'True' if self.is_pool else '',
|
||||||
self.description,
|
self.description,
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -525,3 +540,28 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
def get_status_class(self):
|
def get_status_class(self):
|
||||||
return STATUS_CHOICE_CLASSES[self.status]
|
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>
|
</span>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
PREFIX_ROLE_LINK = """
|
||||||
|
{% if record.role %}
|
||||||
|
<a href="{% url 'ipam:prefix_list' %}?role={{ record.role.slug }}">{{ record.role }}</a>
|
||||||
|
{% else %}
|
||||||
|
—
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
IPADDRESS_LINK = """
|
IPADDRESS_LINK = """
|
||||||
{% if record.pk %}
|
{% if record.pk %}
|
||||||
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||||
@ -86,6 +94,22 @@ STATUS_LABEL = """
|
|||||||
{% endif %}
|
{% 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 = """
|
VLANGROUP_ACTIONS = """
|
||||||
{% if perms.ipam.change_vlangroup %}
|
{% 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>
|
<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):
|
class PrefixTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
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')
|
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||||
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
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')
|
description = tables.Column(orderable=False, verbose_name='Description')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
|
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': lambda record: 'success' if not record.pk else '',
|
'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')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||||
name = tables.Column(verbose_name='Name')
|
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')
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
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):
|
class Meta(BaseTable.Meta):
|
||||||
model = VLAN
|
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
|
# IP addresses
|
||||||
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
|
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/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/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/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
||||||
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
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+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
|
||||||
url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),
|
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.forms import ConfirmationForm
|
||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import filters, forms, tables
|
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):
|
def add_available_prefixes(parent, prefix_list):
|
||||||
@ -35,24 +38,21 @@ def add_available_prefixes(parent, prefix_list):
|
|||||||
return 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 = []
|
output = []
|
||||||
prev_ip = None
|
prev_ip = None
|
||||||
|
|
||||||
# Ignore the "network address" for IPv4 prefixes larger than /31
|
# Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31.
|
||||||
if prefix.version == 4 and prefix.prefixlen < 31:
|
if prefix.version == 4 and prefix.prefixlen < 31 and not is_pool:
|
||||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
|
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)
|
last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
|
||||||
else:
|
else:
|
||||||
|
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
|
||||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
|
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
|
||||||
|
|
||||||
if not ipaddress_list:
|
if not ipaddress_list:
|
||||||
@ -290,7 +290,6 @@ def aggregate(request, pk):
|
|||||||
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
|
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
|
||||||
|
|
||||||
prefix_table = tables.PrefixTable(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'):
|
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||||
prefix_table.base_columns['pk'].visible = True
|
prefix_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
|
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
|
||||||
@ -367,7 +366,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class PrefixListView(ObjectListView):
|
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 = filters.PrefixFilter
|
||||||
filter_form = forms.PrefixFilterForm
|
filter_form = forms.PrefixFilterForm
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixTable
|
||||||
@ -416,7 +415,6 @@ def prefix(request, pk):
|
|||||||
if child_prefixes:
|
if child_prefixes:
|
||||||
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
||||||
child_prefix_table = tables.PrefixTable(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'):
|
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||||
child_prefix_table.base_columns['pk'].visible = True
|
child_prefix_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table)
|
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
|
# Find all IPAddresses belonging to this Prefix
|
||||||
ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
|
ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
|
||||||
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
||||||
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses)
|
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
|
||||||
|
|
||||||
ip_table = tables.IPAddressTable(ipaddresses)
|
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'):
|
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
||||||
ip_table.base_columns['pk'].visible = True
|
ip_table.base_columns['pk'].visible = True
|
||||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
|
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
|
||||||
@ -610,6 +607,14 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
redirect_url = 'ipam:ipaddress_list'
|
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):
|
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'ipam.add_ipaddress'
|
permission_required = 'ipam.add_ipaddress'
|
||||||
form = forms.IPAddressImportForm
|
form = forms.IPAddressImportForm
|
||||||
@ -679,7 +684,7 @@ class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VLANListView(ObjectListView):
|
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 = filters.VLANFilter
|
||||||
filter_form = forms.VLANFilterForm
|
filter_form = forms.VLANFilterForm
|
||||||
table = tables.VLANTable
|
table = tables.VLANTable
|
||||||
@ -733,3 +738,24 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
permission_required = 'ipam.delete_vlan'
|
permission_required = 'ipam.delete_vlan'
|
||||||
cls = VLAN
|
cls = VLAN
|
||||||
default_redirect_url = 'ipam:vlan_list'
|
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.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.7.3'
|
VERSION = '1.8.0'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
@ -117,7 +117,8 @@ INSTALLED_APPS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Middleware
|
# Middleware
|
||||||
MIDDLEWARE_CLASSES = (
|
MIDDLEWARE = (
|
||||||
|
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
@ -193,6 +194,12 @@ SWAGGER_SETTINGS = {
|
|||||||
'base_path': '{}/{}api/docs'.format(ALLOWED_HOSTS[0], BASE_PATH),
|
'base_path': '{}/{}api/docs'.format(ALLOWED_HOSTS[0], BASE_PATH),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Django debug toolbar
|
||||||
|
INTERNAL_IPS = (
|
||||||
|
'127.0.0.1',
|
||||||
|
'::1',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
HOSTNAME = socket.gethostname()
|
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
|
# Prepend BASE_PATH
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
|
url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
|
||||||
|
@ -13,7 +13,7 @@ body {
|
|||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 1340px;
|
max-width: 1600px;
|
||||||
}
|
}
|
||||||
.wrapper {
|
.wrapper {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
@ -35,7 +35,8 @@ footer p {
|
|||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
/* Collapse the nav menu on displays less than 1200px wide */
|
||||||
|
@media (max-width: 1199px) {
|
||||||
.navbar-header {
|
.navbar-header {
|
||||||
float: none;
|
float: none;
|
||||||
}
|
}
|
||||||
@ -58,7 +59,7 @@ footer p {
|
|||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
.navbar-nav {
|
.navbar-nav {
|
||||||
float: none!important;
|
float: none !important;
|
||||||
margin-top: 7.5px;
|
margin-top: 7.5px;
|
||||||
}
|
}
|
||||||
.navbar-nav>li {
|
.navbar-nav>li {
|
||||||
@ -88,10 +89,17 @@ th.pk, td.pk {
|
|||||||
tfoot td {
|
tfoot td {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
table.attr-table td:nth-child(1) {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Paginator */
|
/* Paginator */
|
||||||
|
div.paginator {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
nav ul.pagination {
|
nav ul.pagination {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Racks */
|
/* Racks */
|
||||||
|
@ -51,6 +51,14 @@ $(document).ready(function() {
|
|||||||
$('#id_' + this.value).toggle('disabled');
|
$('#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
|
// API select widget
|
||||||
$('select[filter-for]').change(function () {
|
$('select[filter-for]').change(function () {
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ class SecretFilter(django_filters.FilterSet):
|
|||||||
label='Role (ID)',
|
label='Role (ID)',
|
||||||
)
|
)
|
||||||
role = django_filters.ModelMultipleChoiceFilter(
|
role = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='role',
|
name='role__slug',
|
||||||
queryset=SecretRole.objects.all(),
|
queryset=SecretRole.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
@ -31,7 +31,7 @@ class SecretFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = ['name', 'role_id', 'role', 'device']
|
fields = ['name']
|
||||||
|
|
||||||
def search(self, queryset, value):
|
def search(self, queryset, value):
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
|
@ -34,7 +34,7 @@ def validate_rsa_key(key, is_secret=True):
|
|||||||
# Secret roles
|
# Secret roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class SecretRoleForm(forms.ModelForm, BootstrapMixin):
|
class SecretRoleForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -46,7 +46,7 @@ class SecretRoleForm(forms.ModelForm, BootstrapMixin):
|
|||||||
# Secrets
|
# Secrets
|
||||||
#
|
#
|
||||||
|
|
||||||
class SecretForm(forms.ModelForm, BootstrapMixin):
|
class SecretForm(BootstrapMixin, forms.ModelForm):
|
||||||
private_key = forms.CharField(required=False, widget=forms.HiddenInput())
|
private_key = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||||
plaintext = forms.CharField(max_length=65535, required=False, label='Plaintext',
|
plaintext = forms.CharField(max_length=65535, required=False, label='Plaintext',
|
||||||
widget=forms.PasswordInput(attrs={'class': 'requires-private-key'}))
|
widget=forms.PasswordInput(attrs={'class': 'requires-private-key'}))
|
||||||
@ -85,12 +85,12 @@ class SecretFromCSVForm(forms.ModelForm):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
class SecretImportForm(BulkImportForm, BootstrapMixin):
|
class SecretImportForm(BootstrapMixin, BulkImportForm):
|
||||||
private_key = forms.CharField(widget=forms.HiddenInput())
|
private_key = forms.CharField(widget=forms.HiddenInput())
|
||||||
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-private-key'}))
|
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)
|
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
|
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
|
||||||
name = forms.CharField(max_length=100, required=False)
|
name = forms.CharField(max_length=100, required=False)
|
||||||
@ -99,7 +99,7 @@ class SecretBulkEditForm(BulkEditForm, BootstrapMixin):
|
|||||||
nullable_fields = ['name']
|
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')
|
role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')
|
||||||
|
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ class SecretFilterForm(forms.Form, BootstrapMixin):
|
|||||||
# UserKeys
|
# UserKeys
|
||||||
#
|
#
|
||||||
|
|
||||||
class UserKeyForm(forms.ModelForm, BootstrapMixin):
|
class UserKeyForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserKey
|
model = UserKey
|
||||||
|
@ -5,14 +5,14 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
|
<li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
|
||||||
<li><a href="{% url 'circuits:circuit_list' %}?provider={{ circuit.provider.slug }}">{{ circuit.provider }}</a></li>
|
<li><a href="{% url 'circuits:circuit_list' %}?provider={{ circuit.provider.slug }}">{{ circuit.provider }}</a></li>
|
||||||
<li>{{ circuit.cid }}</li>
|
<li>{{ circuit.cid }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" />
|
<input type="text" name="q" class="form-control" />
|
||||||
@ -40,13 +40,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
|
<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=circuit %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Circuit</strong>
|
<strong>Circuit</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Provider</td>
|
<td>Provider</td>
|
||||||
<td>
|
<td>
|
||||||
@ -81,17 +82,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<td>Commit Rate</td>
|
<td>Commit Rate</td>
|
||||||
<td>
|
<td>
|
||||||
@ -107,67 +97,6 @@
|
|||||||
{% with circuit.get_custom_fields as custom_fields %}
|
{% with circuit.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% 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 panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Comments</strong>
|
<strong>Comments</strong>
|
||||||
@ -180,6 +109,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
{% extends 'utilities/obj_edit.html' %}
|
{% extends 'utilities/obj_edit.html' %}
|
||||||
{% load static from staticfiles %}
|
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
@ -11,15 +10,6 @@
|
|||||||
{% render_field form.type %}
|
{% render_field form.type %}
|
||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
{% render_field form.install_date %}
|
{% 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 %}
|
{% render_field form.commit_rate %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -31,26 +21,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 panel-default">
|
||||||
<div class="panel-heading"><strong>Comments</strong></div>
|
<div class="panel-heading"><strong>Comments</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@ -58,7 +28,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
|
||||||
<script src="{% static 'js/livesearch.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
@ -48,45 +48,20 @@
|
|||||||
<td>Name of tenant (optional)</td>
|
<td>Name of tenant (optional)</td>
|
||||||
<td>Strickland Propane</td>
|
<td>Strickland Propane</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>Site</td>
|
|
||||||
<td>Site name</td>
|
|
||||||
<td>ASH-4</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Install Date</td>
|
<td>Install Date</td>
|
||||||
<td>Date in YYYY-MM-DD format (optional)</td>
|
<td>Date in YYYY-MM-DD format (optional)</td>
|
||||||
<td>2016-02-23</td>
|
<td>2016-02-23</td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<td>Commit rate</td>
|
<td>Commit rate</td>
|
||||||
<td>Commited rate in Kbps (optional)</td>
|
<td>Commited rate in Kbps (optional)</td>
|
||||||
<td>2000</td>
|
<td>2000</td>
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h4>Example</h4>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
|
<li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
|
||||||
<li>{{ provider }}</li>
|
<li>{{ provider }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'circuits:provider_list' %}" method="get">
|
<form action="{% url 'circuits:provider_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" />
|
<input type="text" name="q" class="form-control" />
|
||||||
@ -46,13 +46,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ provider }}</h1>
|
<h1>{{ provider }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=provider %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Provider</strong>
|
<strong>Provider</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>ASN</td>
|
<td>ASN</td>
|
||||||
<td>
|
<td>
|
||||||
@ -120,7 +121,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/created_updated.html' with obj=provider %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@ -134,14 +134,8 @@
|
|||||||
<a href="{% url 'circuits:circuit' pk=c.pk %}">{{ c.cid }}</a>
|
<a href="{% url 'circuits:circuit' pk=c.pk %}">{{ c.cid }}</a>
|
||||||
</td>
|
</td>
|
||||||
<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>
|
||||||
<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>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -149,6 +143,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</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>
|
</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 %}
|
{% block content %}
|
||||||
{% include 'dcim/inc/_device_header.html' with active_tab='info' %}
|
{% include 'dcim/inc/_device_header.html' with active_tab='info' %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-5 col-lg-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Device</strong>
|
<strong>Device</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Tenant</td>
|
<td>Tenant</td>
|
||||||
<td>
|
<td>
|
||||||
@ -85,7 +85,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Management</strong>
|
<strong>Management</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Role</td>
|
<td>Role</td>
|
||||||
<td>
|
<td>
|
||||||
@ -205,6 +205,29 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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 panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Critical Connections</strong>
|
<strong>Critical Connections</strong>
|
||||||
@ -301,9 +324,8 @@
|
|||||||
<div class="panel-body text-muted">None found</div>
|
<div class="panel-body text-muted">None found</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/created_updated.html' with obj=device %}
|
|
||||||
</div>
|
</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 device_bays or device.device_type.is_parent_device %}
|
||||||
{% if perms.dcim.delete_devicebay %}
|
{% if perms.dcim.delete_devicebay %}
|
||||||
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
||||||
@ -313,9 +335,11 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Device Bays</strong>
|
<strong>Device Bays</strong>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<button class="btn btn-default btn-xs toggle">
|
{% if perms.dcim.change_devicebay and device_bays|length > 1 %}
|
||||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
<button class="btn btn-default btn-xs toggle">
|
||||||
</button>
|
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% if perms.dcim.add_devicebay and device_bays|length > 10 %}
|
{% 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">
|
<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
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
|
||||||
@ -363,9 +387,11 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Interfaces</strong>
|
<strong>Interfaces</strong>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<button class="btn btn-default btn-xs toggle">
|
{% if perms.dcim.change_interface and interfaces|length > 1 %}
|
||||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
<button class="btn btn-default btn-xs toggle">
|
||||||
</button>
|
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% if perms.dcim.add_interface and interfaces|length > 10 %}
|
{% if perms.dcim.add_interface and interfaces|length > 10 %}
|
||||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
<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
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
|
||||||
@ -418,9 +444,11 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Console Server Ports</strong>
|
<strong>Console Server Ports</strong>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<button class="btn btn-default btn-xs toggle">
|
{% if perms.dcim.change_consoleserverport and cs_ports|length > 1 %}
|
||||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
<button class="btn btn-default btn-xs toggle">
|
||||||
</button>
|
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% if perms.dcim.add_consoleserverport and cs_ports|length > 10 %}
|
{% 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">
|
<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
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
|
||||||
@ -468,9 +496,11 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Power Outlets</strong>
|
<strong>Power Outlets</strong>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<button class="btn btn-default btn-xs toggle">
|
{% if perms.dcim.change_poweroutlet and cs_ports|length > 1 %}
|
||||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
<button class="btn btn-default btn-xs toggle">
|
||||||
</button>
|
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% if perms.dcim.add_poweroutlet and power_outlets|length > 10 %}
|
{% 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">
|
<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
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% load form_helpers %}
|
{% 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">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -18,13 +18,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>{{ component_type }}</strong>
|
<strong>{{ component_type|title }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label required">Device</label>
|
<label class="col-md-3 control-label required">Device</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<p class="form-control-static">{{ device }}</p>
|
<p class="form-control-static">{{ parent }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% render_form form %}
|
{% render_form form %}
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_field form.platform %}
|
{% render_field form.platform %}
|
||||||
{% render_field form.status %}
|
{% render_field form.status %}
|
||||||
{% if obj %}
|
{% if obj.pk %}
|
||||||
{% render_field form.primary_ip4 %}
|
{% render_field form.primary_ip4 %}
|
||||||
{% render_field form.primary_ip6 %}
|
{% render_field form.primary_ip6 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Chassis</strong>
|
<strong>Chassis</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Model</td>
|
<td>Model</td>
|
||||||
<td>{{ device.device_type.full_name }}</td>
|
<td>{{ device.device_type.full_name }}</td>
|
||||||
@ -127,7 +127,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if perms.dcim.add_module %}
|
{% 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>
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
Add a Module
|
Add a Module
|
||||||
</a>
|
</a>
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<tr id="{{ iface }}">
|
<tr id="{{ iface }}">
|
||||||
<td>{{ iface }}</td>
|
<td>{{ iface }}</td>
|
||||||
{% if iface.connection %}
|
{% 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 }}">
|
<td class="configured_device" data="{{ connected_iface.device }}">
|
||||||
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a>
|
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Chassis</strong>
|
<strong>Chassis</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Manufacturer</td>
|
<td>Manufacturer</td>
|
||||||
<td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
|
<td><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></td>
|
||||||
@ -145,6 +145,21 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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=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=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' %}
|
{% 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' %}
|
{% extends '_base.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Add {{ component_type }} to {{ devicetype }}{% endblock %}
|
{% block title %}Add {{ component_type }} to {{ parent }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form action="." method="post" class="form form-horizontal">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
@ -24,7 +24,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label required">Device Type</label>
|
<label class="col-md-3 control-label required">Device Type</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<p class="form-control-static">{{ devicetype }}</p>
|
<p class="form-control-static">{{ parent }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% render_form form %}
|
{% 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' %}
|
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/filter_panel.html' %}
|
{% include 'inc/filter_panel.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
{% if device.rack %}
|
{% if device.rack %}
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a></li>
|
<li><a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a></li>
|
||||||
@ -13,7 +13,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
<form action="{% url 'dcim:device_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" placeholder="Search devices" />
|
<input type="text" name="q" class="form-control" placeholder="Search devices" />
|
||||||
@ -41,6 +41,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ device }}</h1>
|
<h1>{{ device }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=device %}
|
||||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
<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 == '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>
|
<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>
|
<small>{{ iface.mac_address|default:'' }}</small>
|
||||||
</td>
|
</td>
|
||||||
{% if not iface.is_physical %}
|
{% if not iface.is_physical %}
|
||||||
<td colspan="2">Virtual</td>
|
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||||
{% elif iface.connection %}
|
{% elif iface.connection %}
|
||||||
{% with iface.get_connected_interface as connected_iface %}
|
{% with iface.connected_interface as connected_iface %}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a>
|
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
@ -24,10 +24,16 @@
|
|||||||
<span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span>
|
<span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span>
|
||||||
</td>
|
</td>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% elif iface.circuit %}
|
{% elif iface.circuit_termination %}
|
||||||
<td colspan="2">
|
{% with iface.circuit_termination.get_peer_termination as peer_termination %}
|
||||||
<a href="{% url 'circuits:circuit' pk=iface.circuit.pk %}">{{ iface.circuit }}</a>
|
<td colspan="2">
|
||||||
</td>
|
<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 %}
|
{% else %}
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<span class="text-muted">Not connected</span>
|
<span class="text-muted">Not connected</span>
|
||||||
@ -35,7 +41,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if show_graphs %}
|
{% 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">
|
<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>
|
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
||||||
</button>
|
</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">
|
<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>
|
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% elif iface.circuit and perms.circuits.change_circuit %}
|
{% elif iface.circuit_termination and perms.circuits.change_circuittermination %}
|
||||||
<a href="{% url 'circuits:circuit_edit' pk=iface.circuit.pk %}" class="btn btn-danger btn-xs" title="Edit circuit">
|
<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>
|
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% 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>
|
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -71,7 +80,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_interface %}
|
{% 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">
|
<button class="btn btn-danger btn-xs" disabled="disabled">
|
||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</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' %}
|
{% extends 'utilities/obj_table.html' %}
|
||||||
|
|
||||||
{% block extra_actions %}
|
{% block extra_actions %}
|
||||||
{% if perms.dcim.add_interface %}
|
{% if perms.dcim.change_device %}
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:device_bulk_add_interface' %}" class="btn btn-primary btn-sm">
|
<div class="btn-group">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Interfaces
|
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
</button>
|
<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 %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>{{ title }}</strong>
|
<strong>{{ title }}</strong>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% if table.rows|length > 3 %}
|
{% if table.rows|length > 1 %}
|
||||||
<button class="btn btn-default btn-xs toggle">
|
<button class="btn btn-default btn-xs toggle">
|
||||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||||
</button>
|
</button>
|
||||||
|
@ -27,6 +27,12 @@
|
|||||||
<strong>A Side</strong>
|
<strong>A Side</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<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">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label required">Rack</label>
|
<label class="col-md-3 control-label required">Rack</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
@ -61,6 +67,7 @@
|
|||||||
{% render_field form.livesearch %}
|
{% render_field form.livesearch %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="select">
|
<div class="tab-pane" id="select">
|
||||||
|
{% render_field form.site_b %}
|
||||||
{% render_field form.rack_b %}
|
{% render_field form.rack_b %}
|
||||||
{% render_field form.device_b %}
|
{% render_field form.device_b %}
|
||||||
</div>
|
</div>
|
||||||
@ -77,8 +84,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
<button type="submit" name="_create" class="btn btn-primary">Connect</button>
|
||||||
<button type="submit" name="_addanother" class="btn btn-primary">Create and Connect Another</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>
|
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,14 +6,14 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
|
<li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
|
||||||
<li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">{{ rack.site }}</a></li>
|
<li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">{{ rack.site }}</a></li>
|
||||||
<li>{{ rack }}</li>
|
<li>{{ rack }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'dcim:rack_list' %}" method="get">
|
<form action="{% url 'dcim:rack_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" placeholder="Search racks" />
|
<input type="text" name="q" class="form-control" placeholder="Search racks" />
|
||||||
@ -53,13 +53,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>Rack {{ rack.name }}</h1>
|
<h1>Rack {{ rack.name }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=rack %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Rack</strong>
|
<strong>Rack</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Site</td>
|
<td>Site</td>
|
||||||
<td>
|
<td>
|
||||||
@ -188,7 +189,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/created_updated.html' with obj=rack %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row col-md-6">
|
<div class="row col-md-6">
|
||||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||||
|
@ -7,13 +7,13 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
|
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
|
||||||
<li>{{ site }}</li>
|
<li>{{ site }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'dcim:site_list' %}" method="get">
|
<form action="{% url 'dcim:site_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" placeholder="Search sites" />
|
<input type="text" name="q" class="form-control" placeholder="Search sites" />
|
||||||
@ -47,13 +47,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ site.name }}</h1>
|
<h1>{{ site.name }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=site %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-7">
|
<div class="col-md-7">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Site</strong>
|
<strong>Site</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Tenant</td>
|
<td>Tenant</td>
|
||||||
<td>
|
<td>
|
||||||
@ -109,6 +110,36 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% with site.get_custom_fields as custom_fields %}
|
{% with site.get_custom_fields as custom_fields %}
|
||||||
@ -126,7 +157,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/created_updated.html' with obj=site %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -10,8 +10,16 @@
|
|||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
{% render_field form.facility %}
|
{% render_field form.facility %}
|
||||||
{% render_field form.asn %}
|
{% 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.physical_address %}
|
||||||
{% render_field form.shipping_address %}
|
{% render_field form.shipping_address %}
|
||||||
|
{% render_field form.contact_name %}
|
||||||
|
{% render_field form.contact_phone %}
|
||||||
|
{% render_field form.contact_email %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if form.custom_fields %}
|
{% if form.custom_fields %}
|
||||||
|
@ -53,10 +53,25 @@
|
|||||||
<td>Autonomous system number (optional)</td>
|
<td>Autonomous system number (optional)</td>
|
||||||
<td>65000</td>
|
<td>65000</td>
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h4>Example</h4>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row home-search" style="padding: 15px 0px 20px">
|
<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">
|
<form action="{% url 'dcim:device_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" placeholder="Search devices" class="form-control" />
|
<input type="text" name="q" placeholder="Search devices" class="form-control" />
|
||||||
@ -17,7 +17,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<p></p>
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-6 col-md-3">
|
||||||
<form action="{% url 'ipam:prefix_list' %}" method="get">
|
<form action="{% url 'ipam:prefix_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" placeholder="Search prefixes" class="form-control" />
|
<input type="text" name="q" placeholder="Search prefixes" class="form-control" />
|
||||||
@ -31,7 +31,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<p></p>
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-6 col-md-3">
|
||||||
<form action="{% url 'ipam:ipaddress_list' %}" method="get">
|
<form action="{% url 'ipam:ipaddress_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" placeholder="Search IPs" class="form-control" />
|
<input type="text" name="q" placeholder="Search IPs" class="form-control" />
|
||||||
@ -45,7 +45,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<p></p>
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-6 col-md-3">
|
||||||
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" placeholder="Search circuits" class="form-control" />
|
<input type="text" name="q" placeholder="Search circuits" class="form-control" />
|
||||||
@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-sm-6 col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Organization</strong>
|
<strong>Organization</strong>
|
||||||
@ -106,7 +106,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-sm-6 col-md-4">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>IPAM</strong>
|
<strong>IPAM</strong>
|
||||||
@ -157,7 +157,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-sm-6 col-md-4">
|
||||||
{% if perms.secrets %}
|
{% if perms.secrets %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Custom Fields</strong>
|
<strong>Custom Fields</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
{% for field, value in custom_fields.items %}
|
{% for field, value in custom_fields.items %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ field }}</td>
|
<td>{{ field }}</td>
|
||||||
|
@ -5,14 +5,14 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></li>
|
<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><a href="{% url 'ipam:aggregate_list' %}?rir={{ aggregate.rir.slug }}">{{ aggregate.rir }}</a></li>
|
||||||
<li>{{ aggregate }}</li>
|
<li>{{ aggregate }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'ipam:aggregate_list' %}" method="get">
|
<form action="{% url 'ipam:aggregate_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" placeholder="Search aggregates" />
|
<input type="text" name="q" class="form-control" placeholder="Search aggregates" />
|
||||||
@ -40,13 +40,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ aggregate }}</h1>
|
<h1>{{ aggregate }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=aggregate %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Aggregate</strong>
|
<strong>Aggregate</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Family</td>
|
<td>Family</td>
|
||||||
<td>{{ aggregate.get_family_display }}</td>
|
<td>{{ aggregate.get_family_display }}</td>
|
||||||
@ -79,7 +80,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/created_updated.html' with obj=aggregate %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% with aggregate.get_custom_fields as custom_fields %}
|
{% 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="row">
|
||||||
<div class="col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
|
<li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
|
||||||
{% if prefix.vrf %}
|
{% if prefix.vrf %}
|
||||||
@ -8,7 +8,7 @@
|
|||||||
<li>{{ prefix }}</li>
|
<li>{{ prefix }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'ipam:prefix_list' %}" method="get">
|
<form action="{% url 'ipam:prefix_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" placeholder="Search prefixes" />
|
<input type="text" name="q" class="form-control" placeholder="Search prefixes" />
|
||||||
@ -42,6 +42,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ prefix }}</h1>
|
<h1>{{ prefix }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=prefix %}
|
||||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
<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 == '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>
|
<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 %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
|
<li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
|
||||||
{% if ipaddress.vrf %}
|
{% if ipaddress.vrf %}
|
||||||
@ -14,7 +14,7 @@
|
|||||||
<li>{{ ipaddress }}</li>
|
<li>{{ ipaddress }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'ipam:ipaddress_list' %}" method="get">
|
<form action="{% url 'ipam:ipaddress_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" placeholder="Search IPs" />
|
<input type="text" name="q" class="form-control" placeholder="Search IPs" />
|
||||||
@ -42,13 +42,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ ipaddress }}</h1>
|
<h1>{{ ipaddress }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=ipaddress %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>IP Address</strong>
|
<strong>IP Address</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Family</td>
|
<td>Family</td>
|
||||||
<td>{{ ipaddress.get_family_display }}</td>
|
<td>{{ ipaddress.get_family_display }}</td>
|
||||||
@ -136,7 +137,6 @@
|
|||||||
{% with ipaddress.get_custom_fields as custom_fields %}
|
{% with ipaddress.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% include 'inc/created_updated.html' with obj=ipaddress %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% with heading='Parent Prefixes' %}
|
{% 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 static from staticfiles %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block tabs %}
|
||||||
|
{% if not obj.pk %}
|
||||||
|
{% include 'ipam/inc/ipadress_edit_header.html' with active_tab='add' %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>IP Address</strong></div>
|
<div class="panel-heading"><strong>IP Address</strong></div>
|
||||||
@ -10,7 +16,7 @@
|
|||||||
{% render_field form.vrf %}
|
{% render_field form.vrf %}
|
||||||
{% render_field form.tenant %}
|
{% render_field form.tenant %}
|
||||||
{% render_field form.status %}
|
{% render_field form.status %}
|
||||||
{% if obj %}
|
{% if obj.pk %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label">Device</label>
|
<label class="col-md-3 control-label">Device</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Prefix</strong>
|
<strong>Prefix</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Family</td>
|
<td>Family</td>
|
||||||
<td>{{ prefix.get_family_display }}</td>
|
<td>{{ prefix.get_family_display }}</td>
|
||||||
@ -85,6 +85,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<td>Description</td>
|
<td>Description</td>
|
||||||
<td>
|
<td>
|
||||||
@ -104,7 +114,6 @@
|
|||||||
{% with prefix.get_custom_fields as custom_fields %}
|
{% with prefix.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% include 'inc/created_updated.html' with obj=prefix %}
|
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-7">
|
<div class="col-md-7">
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
{% render_field form.vlan %}
|
{% render_field form.vlan %}
|
||||||
{% render_field form.status %}
|
{% render_field form.status %}
|
||||||
{% render_field form.role %}
|
{% render_field form.role %}
|
||||||
|
{% render_field form.is_pool %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -68,6 +68,11 @@
|
|||||||
<td>Functional role (optional)</td>
|
<td>Functional role (optional)</td>
|
||||||
<td>Customer</td>
|
<td>Customer</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Is a pool</td>
|
||||||
|
<td>True if all IPs are considered usable</td>
|
||||||
|
<td>False</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Description</td>
|
<td>Description</td>
|
||||||
<td>Short description (optional)</td>
|
<td>Short description (optional)</td>
|
||||||
@ -76,7 +81,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h4>Example</h4>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
|
<li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
|
||||||
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
|
<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>
|
<li>{{ vlan.name }} ({{ vlan.vid }})</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'ipam:vlan_list' %}" method="get">
|
<form action="{% url 'ipam:vlan_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" placeholder="Search VLANs" />
|
<input type="text" name="q" class="form-control" placeholder="Search VLANs" />
|
||||||
@ -43,13 +43,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>VLAN {{ vlan.display_name }}</h1>
|
<h1>VLAN {{ vlan.display_name }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=vlan %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>VLAN</strong>
|
<strong>VLAN</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Site</td>
|
<td>Site</td>
|
||||||
<td><a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a></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 %}
|
{% with vlan.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% include 'inc/created_updated.html' with obj=vlan %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -5,13 +5,13 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'ipam:vrf_list' %}">VRFs</a></li>
|
<li><a href="{% url 'ipam:vrf_list' %}">VRFs</a></li>
|
||||||
<li>{{ vrf }}</li>
|
<li>{{ vrf }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'ipam:vrf_list' %}" method="get">
|
<form action="{% url 'ipam:vrf_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" placeholder="Search VRFs" />
|
<input type="text" name="q" class="form-control" placeholder="Search VRFs" />
|
||||||
@ -39,13 +39,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ vrf }}</h1>
|
<h1>{{ vrf }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=vrf %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>VRF</strong>
|
<strong>VRF</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Route Distinguisher</td>
|
<td>Route Distinguisher</td>
|
||||||
<td>{{ vrf.rd }}</td>
|
<td>{{ vrf.rd }}</td>
|
||||||
@ -85,7 +86,6 @@
|
|||||||
{% with vrf.get_custom_fields as custom_fields %}
|
{% with vrf.get_custom_fields as custom_fields %}
|
||||||
{% include 'inc/custom_fields_panel.html' %}
|
{% include 'inc/custom_fields_panel.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% include 'inc/created_updated.html' with obj=vrf %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -2,11 +2,10 @@
|
|||||||
|
|
||||||
{# Custom pagination controls to render nicely with Bootstrap CSS. smart_pages requires EnhancedPaginator. #}
|
{# Custom pagination controls to render nicely with Bootstrap CSS. smart_pages requires EnhancedPaginator. #}
|
||||||
|
|
||||||
<div class="row">
|
<div class="paginator pull-right">
|
||||||
<div class="col-md-7">
|
|
||||||
{% if table.paginator.num_pages > 1 %}
|
{% if table.paginator.num_pages > 1 %}
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="pagination">
|
<ul class="pagination pull-right">
|
||||||
{% if table.page.has_previous %}
|
{% if table.page.has_previous %}
|
||||||
<li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">«</a></li>
|
<li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">«</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -23,8 +22,8 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
<div class="clearfix"></div>
|
||||||
<div class="col-md-5 text-right text-muted">
|
<div class="text-right text-muted">
|
||||||
Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }}
|
Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }}
|
||||||
{% if total == 1 %}
|
{% if total == 1 %}
|
||||||
{{ table.data.verbose_name }}
|
{{ table.data.verbose_name }}
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ secret }}</h1>
|
<h1>{{ secret }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=secret %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
@ -58,7 +59,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/created_updated.html' with obj=secret %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% if secret|decryptable_by:request.user %}
|
{% if secret|decryptable_by:request.user %}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></li>
|
<li><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></li>
|
||||||
{% if tenant.group %}
|
{% if tenant.group %}
|
||||||
@ -14,7 +14,7 @@
|
|||||||
<li>{{ tenant }}</li>
|
<li>{{ tenant }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-sm-4 col-md-3">
|
||||||
<form action="{% url 'tenancy:tenant_list' %}" method="get">
|
<form action="{% url 'tenancy:tenant_list' %}" method="get">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" name="q" class="form-control" placeholder="Name" />
|
<input type="text" name="q" class="form-control" placeholder="Name" />
|
||||||
@ -42,13 +42,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ tenant }}</h1>
|
<h1>{{ tenant }}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=tenant %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-7">
|
<div class="col-md-7">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Tenant</strong>
|
<strong>Tenant</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Group</td>
|
<td>Group</td>
|
||||||
<td>
|
<td>
|
||||||
@ -86,7 +87,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/created_updated.html' with obj=tenant %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
<div class="panel panel-default">
|
<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 %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Change Password{% endblock %}
|
{% block title %}Change Password{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block usercontent %}
|
||||||
<div class="row">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
<div class="col-md-8 col-md-offset-2">
|
{% csrf_token %}
|
||||||
<h1>Change Password</h1>
|
{% if form.non_field_errors %}
|
||||||
</div>
|
<div class="panel panel-danger">
|
||||||
</div>
|
<div class="panel-heading"><strong>Errors</strong></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>
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% render_field form.old_password %}
|
{{ form.non_field_errors }}
|
||||||
{% render_field form.new_password1 %}
|
|
||||||
{% render_field form.new_password2 %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
{% endif %}
|
||||||
<div class="form-group">
|
<div class="panel panel-default">
|
||||||
<div class="col-md-12 text-center">
|
<div class="panel-heading"><strong>Password</strong></div>
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
<div class="panel-body">
|
||||||
<a href="{% url 'users:profile' %}" class="btn btn-default">Cancel</a>
|
{% render_field form.old_password %}
|
||||||
</div>
|
{% render_field form.new_password1 %}
|
||||||
</div>
|
{% render_field form.new_password2 %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
<div class="text-right">
|
||||||
</div>
|
<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 %}
|
{% 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 %}
|
{% load helpers %}
|
||||||
|
|
||||||
{% block title %}User Profile{% endblock %}
|
{% block title %}User Profile{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block usercontent %}
|
||||||
<div class="row">
|
<small class="text-muted">User login</small>
|
||||||
<div class="col-md-8 col-md-offset-2">
|
<h5>{{ request.user.username }}</h5>
|
||||||
<h1>User Profile</h1>
|
<small class="text-muted">Full name</small>
|
||||||
</div>
|
<h5>{{ request.user.first_name }} {{ request.user.last_name }}</h5>
|
||||||
</div>
|
<small class="text-muted">Email</small>
|
||||||
<div class="row">
|
<h5>{{ request.user.email }}</h5>
|
||||||
<div class="col-md-2 col-md-offset-2">
|
<small class="text-muted">Registered</small>
|
||||||
{% include 'users/inc/profile_nav.html' with active_tab="profile" %}
|
<h5>{{ request.user.date_joined }}</h5>
|
||||||
</div>
|
<small class="text-muted">Groups</small>
|
||||||
<div class="col-md-6">
|
<h5>{{ request.user.groups.all|join:', ' }}</h5>
|
||||||
<small class="text-muted">User login</small>
|
<small class="text-muted">Admin access</small>
|
||||||
<h5>{{ request.user.username }}</h5>
|
<h5>{{ request.user.is_staff|yesno|capfirst }}</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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,35 +1,22 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends 'users/_user.html' %}
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Recent Activity{% endblock %}
|
{% block title %}Recent Activity{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block usercontent %}
|
||||||
<div class="row">
|
<table class="table table-hover">
|
||||||
<div class="col-md-8 col-md-offset-2">
|
<thead>
|
||||||
<h1>Recent Activity</h1>
|
<tr>
|
||||||
</div>
|
<th>Time</th>
|
||||||
</div>
|
<th>Action</th>
|
||||||
<div class="row">
|
</tr>
|
||||||
<div class="col-md-2 col-md-offset-2">
|
</thead>
|
||||||
{% include 'users/inc/profile_nav.html' with active_tab="recent_activity" %}
|
<tbody>
|
||||||
</div>
|
{% for action in recent_activity %}
|
||||||
<div class="col-md-6">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>Time</th>
|
<td>{{ action.time|date:'SHORT_DATETIME_FORMAT' }}</td>
|
||||||
<th>Action</th>
|
<td>{{ action.icon }} {{ action.message|safe }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
{% endfor %}
|
||||||
<tbody>
|
</tbody>
|
||||||
{% for action in recent_activity %}
|
</table>
|
||||||
<tr>
|
|
||||||
<td>{{ action.time|date:'SHORT_DATETIME_FORMAT' }}</td>
|
|
||||||
<td>{{ action.icon }} {{ action.message|safe }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,46 +1,33 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends 'users/_user.html' %}
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}User Key{% endblock %}
|
{% block title %}User Key{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block usercontent %}
|
||||||
<div class="row">
|
{% if userkey %}
|
||||||
<div class="col-md-8 col-md-offset-2">
|
<h4>
|
||||||
<h1>User Key</h1>
|
Your user key is:
|
||||||
</div>
|
{% if userkey.is_active %}
|
||||||
</div>
|
<span class="label label-success">Active</span>
|
||||||
<div class="row">
|
{% else %}
|
||||||
<div class="col-md-2 col-md-offset-2">
|
<span class="label label-danger">Inactive</span>
|
||||||
{% include 'users/inc/profile_nav.html' with active_tab="userkey" %}
|
{% endif %}
|
||||||
</div>
|
</h4>
|
||||||
<div class="col-md-6">
|
<p>Your public key is below.</p>
|
||||||
{% if userkey %}
|
<pre>{{ userkey.public_key }}</pre>
|
||||||
<h4>
|
<div class="pull-right">
|
||||||
Your user key is:
|
<a href="{% url 'users:userkey_edit' %}" class="btn btn-warning">
|
||||||
{% if userkey.is_active %}
|
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||||
<span class="label label-success">Active</span>
|
Edit user key
|
||||||
{% else %}
|
</a>
|
||||||
<span class="label label-danger">Inactive</span>
|
</div>
|
||||||
{% endif %}
|
{% include 'inc/created_updated.html' with obj=userkey %}
|
||||||
</h4>
|
{% else %}
|
||||||
<p>Your public key is below.</p>
|
<p>You don't have a user key on file.</p>
|
||||||
<pre>{{ userkey.public_key }}</pre>
|
<p>
|
||||||
<div class="pull-right">
|
<a href="{% url 'users:userkey_edit' %}" class="btn btn-primary">
|
||||||
<a href="{% url 'users:userkey_edit' %}" class="btn btn-warning">
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
Create a User Key
|
||||||
Edit user key
|
</a>
|
||||||
</a>
|
</p>
|
||||||
</div>
|
{% endif %}
|
||||||
{% 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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,71 +1,58 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends 'users/_user.html' %}
|
||||||
{% load static from staticfiles %}
|
{% load static from staticfiles %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}User Key{% endblock %}
|
{% block title %}User Key{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block usercontent %}
|
||||||
<div class="row">
|
{% if userkey.is_active %}
|
||||||
<div class="col-md-8 col-md-offset-2">
|
<div class="alert alert-danger" role="alert">
|
||||||
<h1>User Key</h1>
|
<strong>Warning:</strong> Changing your public key will require your user key to be re-activated by another
|
||||||
</div>
|
user. You will be unable to retrieve any secrets until your key has been reactivated.
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
{% endif %}
|
||||||
<div class="col-md-2 col-md-offset-2">
|
<form action="." method="post" class="form">
|
||||||
{% include 'users/inc/profile_nav.html' with active_tab="userkey" %}
|
{% csrf_token %}
|
||||||
</div>
|
<div class="form-group">
|
||||||
<div class="col-md-6">
|
{% render_field form.public_key %}
|
||||||
{% if userkey.is_active %}
|
</div>
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="row">
|
||||||
<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">
|
<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>
|
||||||
<div class="row">
|
</div>
|
||||||
<div class="form-group">
|
</form>
|
||||||
<div class="col-md-6">
|
<div class="modal fade" id="new_keypair_modal" tabindex="-1" role="dialog">
|
||||||
<button type="button" class="btn btn-info" id="generate_keypair">Generate a New Key Pair</button>
|
<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>
|
||||||
<div class="col-md-6 text-right">
|
<strong>New Private Key</strong>
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
<div class="form-group">
|
||||||
<a href="{% url 'users:userkey' %}" class="btn btn-default">Cancel</a>
|
<textarea class="form-control" id="new_privkey" style="height: 250px;"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="modal-footer text-center">
|
||||||
</form>
|
<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 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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div class="col-md-6 col-md-offset-3">
|
<div class="col-md-6 col-md-offset-3">
|
||||||
<form action="." method="post" class="form">
|
<form action="." method="post" class="form">
|
||||||
{% csrf_token %}
|
{% 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-heading">{% block title %}{% endblock %}</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% block message %}<p>Are you sure?</p>{% endblock %}
|
{% block message %}<p>Are you sure?</p>{% endblock %}
|
||||||
@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<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>
|
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
{% if obj %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form action="." method="post" class="form form-horizontal">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-md-offset-3">
|
<div class="col-md-6 col-md-offset-3">
|
||||||
<h3>{% if 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 %}
|
{% if form.non_field_errors %}
|
||||||
<div class="panel panel-danger">
|
<div class="panel panel-danger">
|
||||||
<div class="panel-heading"><strong>Errors</strong></div>
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-md-offset-3 text-right">
|
<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>
|
<button type="submit" name="_update" class="btn btn-primary">Update</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<form method="post" class="form form-horizontal">
|
<form method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="redirect_url" value="{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" />
|
<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 %}
|
{% if table.paginator.num_pages > 1 %}
|
||||||
<div id="select_all_box" class="hidden alert alert-info">
|
<div id="select_all_box" class="hidden alert alert-info">
|
||||||
<div class="checkbox-inline">
|
<div class="checkbox-inline">
|
||||||
@ -31,3 +31,4 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% render_table table table_template|default:'table.html' %}
|
{% render_table table table_template|default:'table.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
@ -11,7 +11,7 @@ from .models import Tenant, TenantGroup
|
|||||||
# Tenant groups
|
# Tenant groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class TenantGroupForm(forms.ModelForm, BootstrapMixin):
|
class TenantGroupForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -41,7 +41,7 @@ class TenantFromCSVForm(forms.ModelForm):
|
|||||||
fields = ['name', 'slug', 'group', 'description']
|
fields = ['name', 'slug', 'group', 'description']
|
||||||
|
|
||||||
|
|
||||||
class TenantImportForm(BulkImportForm, BootstrapMixin):
|
class TenantImportForm(BootstrapMixin, BulkImportForm):
|
||||||
csv = CSVDataField(csv_form=TenantFromCSVForm)
|
csv = CSVDataField(csv_form=TenantFromCSVForm)
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as
|
|||||||
from utilities.forms import BootstrapMixin
|
from utilities.forms import BootstrapMixin
|
||||||
|
|
||||||
|
|
||||||
class LoginForm(AuthenticationForm, BootstrapMixin):
|
class LoginForm(BootstrapMixin, AuthenticationForm):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(LoginForm, self).__init__(*args, **kwargs)
|
super(LoginForm, self).__init__(*args, **kwargs)
|
||||||
@ -12,5 +12,5 @@ class LoginForm(AuthenticationForm, BootstrapMixin):
|
|||||||
self.fields['password'].widget.attrs['placeholder'] = ''
|
self.fields['password'].widget.attrs['placeholder'] = ''
|
||||||
|
|
||||||
|
|
||||||
class PasswordChangeForm(DjangoPasswordChangeForm, BootstrapMixin):
|
class PasswordChangeForm(BootstrapMixin, DjangoPasswordChangeForm):
|
||||||
pass
|
pass
|
||||||
|
@ -56,6 +56,7 @@ def logout(request):
|
|||||||
def profile(request):
|
def profile(request):
|
||||||
|
|
||||||
return render(request, 'users/profile.html', {
|
return render(request, 'users/profile.html', {
|
||||||
|
'active_tab': 'profile',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -75,6 +76,7 @@ def change_password(request):
|
|||||||
|
|
||||||
return render(request, 'users/change_password.html', {
|
return render(request, 'users/change_password.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
|
'active_tab': 'change_password',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -88,6 +90,7 @@ def userkey(request):
|
|||||||
|
|
||||||
return render(request, 'users/userkey.html', {
|
return render(request, 'users/userkey.html', {
|
||||||
'userkey': userkey,
|
'userkey': userkey,
|
||||||
|
'active_tab': 'userkey',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -114,6 +117,7 @@ def userkey_edit(request):
|
|||||||
return render(request, 'users/userkey_edit.html', {
|
return render(request, 'users/userkey_edit.html', {
|
||||||
'userkey': userkey,
|
'userkey': userkey,
|
||||||
'form': form,
|
'form': form,
|
||||||
|
'active_tab': 'userkey',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -121,5 +125,6 @@ def userkey_edit(request):
|
|||||||
def recent_activity(request):
|
def recent_activity(request):
|
||||||
|
|
||||||
return render(request, 'users/recent_activity.html', {
|
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