mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 13:06:30 -06:00
commit
f7b0d22f86
@ -45,6 +45,10 @@ sure to include:
|
|||||||
* Any error messages generated
|
* Any error messages generated
|
||||||
* Screenshots (if applicable)
|
* Screenshots (if applicable)
|
||||||
|
|
||||||
|
* Please avoid prepending any sort of tag (e.g. "[Bug]") to the issue title.
|
||||||
|
The issue will be reviewed by a moderator after submission and the appropriate
|
||||||
|
labels will be applied.
|
||||||
|
|
||||||
* Keep in mind that we prioritize bugs based on their severity and how
|
* Keep in mind that we prioritize bugs based on their severity and how
|
||||||
much work is required to resolve them. It may take some time for someone
|
much work is required to resolve them. It may take some time for someone
|
||||||
to address your issue.
|
to address your issue.
|
||||||
@ -91,6 +95,10 @@ following:
|
|||||||
* Any third-party libraries or other resources which would be
|
* Any third-party libraries or other resources which would be
|
||||||
involved
|
involved
|
||||||
|
|
||||||
|
* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue title.
|
||||||
|
The issue will be reviewed by a moderator after submission and the appropriate
|
||||||
|
labels will be applied.
|
||||||
|
|
||||||
## Submitting Pull Requests
|
## Submitting Pull Requests
|
||||||
|
|
||||||
* Be sure to open an issue before starting work on a pull request, and
|
* Be sure to open an issue before starting work on a pull request, and
|
||||||
|
@ -58,6 +58,14 @@ This script:
|
|||||||
* Applies any database migrations that were included in the release
|
* Applies any database migrations that were included in the release
|
||||||
* Collects all static files to be served by the HTTP service
|
* Collects all static files to be served by the HTTP service
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
It's possible that the upgrade script will display a notice warning of unreflected database migrations:
|
||||||
|
|
||||||
|
Your models have changes that are not yet reflected in a migration, and so won't be applied.
|
||||||
|
Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.
|
||||||
|
|
||||||
|
This may occur due to semantic differences in environment, and can be safely ignored. Never attempt to create new migrations unless you are inentionally modifying the database schema.
|
||||||
|
|
||||||
# Restart the WSGI Service
|
# Restart the WSGI Service
|
||||||
|
|
||||||
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:
|
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
from django.shortcuts import get_object_or_404
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from circuits import filters
|
from circuits import filters
|
||||||
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
||||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
@ -165,7 +167,9 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
|
|||||||
)
|
)
|
||||||
rack = ChainedModelChoiceField(
|
rack = ChainedModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
chains={'site': 'site'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
label='Rack',
|
label='Rack',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -175,7 +179,10 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
|
|||||||
)
|
)
|
||||||
device = ChainedModelChoiceField(
|
device = ChainedModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
chains={'site': 'site', 'rack': 'rack'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
('rack', 'rack'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
label='Device',
|
label='Device',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -184,20 +191,13 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
|
|||||||
attrs={'filter-for': 'interface'}
|
attrs={'filter-for': 'interface'}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
livesearch = forms.CharField(
|
|
||||||
required=False,
|
|
||||||
label='Device',
|
|
||||||
widget=Livesearch(
|
|
||||||
query_key='q',
|
|
||||||
query_url='dcim-api:device-list',
|
|
||||||
field_to_update='device'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
interface = ChainedModelChoiceField(
|
interface = ChainedModelChoiceField(
|
||||||
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
|
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
|
||||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
),
|
),
|
||||||
chains={'device': 'device'},
|
chains=(
|
||||||
|
('device', 'device'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
label='Interface',
|
label='Interface',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -208,8 +208,10 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
fields = ['term_side', 'site', 'rack', 'device', 'livesearch', 'interface', 'port_speed', 'upstream_speed',
|
fields = [
|
||||||
'xconnect_id', 'pp_info']
|
'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||||
|
'pp_info',
|
||||||
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'port_speed': "Physical circuit speed",
|
'port_speed': "Physical circuit speed",
|
||||||
'xconnect_id': "ID of the local cross-connect",
|
'xconnect_id': "ID of the local cross-connect",
|
||||||
|
81
netbox/circuits/migrations/0009_unicode_literals.py
Normal file
81
netbox/circuits/migrations/0009_unicode_literals.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import dcim.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0008_circuittermination_interface_protect_on_delete'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='cid',
|
||||||
|
field=models.CharField(max_length=50, verbose_name='Circuit ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='commit_rate',
|
||||||
|
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='install_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Date installed'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
name='port_speed',
|
||||||
|
field=models.PositiveIntegerField(verbose_name='Port speed (Kbps)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
name='pp_info',
|
||||||
|
field=models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
name='term_side',
|
||||||
|
field=models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
name='upstream_speed',
|
||||||
|
field=models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
name='xconnect_id',
|
||||||
|
field=models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='provider',
|
||||||
|
name='account',
|
||||||
|
field=models.CharField(blank=True, max_length=30, verbose_name='Account number'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='provider',
|
||||||
|
name='admin_contact',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Admin contact'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='provider',
|
||||||
|
name='asn',
|
||||||
|
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='provider',
|
||||||
|
name='noc_contact',
|
||||||
|
field=models.TextField(blank=True, verbose_name='NOC contact'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='provider',
|
||||||
|
name='portal_url',
|
||||||
|
field=models.URLField(blank=True, verbose_name='Portal'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -110,7 +112,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
unique_together = ['provider', 'cid']
|
unique_together = ['provider', 'cid']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u'{} {}'.format(self.provider, self.cid)
|
return '{} {}'.format(self.provider, self.cid)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('circuits:circuit', args=[self.pk])
|
return reverse('circuits:circuit', args=[self.pk])
|
||||||
@ -166,7 +168,7 @@ class CircuitTermination(models.Model):
|
|||||||
unique_together = ['circuit', 'term_side']
|
unique_together = ['circuit', 'term_side']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u'{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
return '{} (Side {})'.format(self.circuit, self.get_term_side_display())
|
||||||
|
|
||||||
def get_peer_termination(self):
|
def get_peer_termination(self):
|
||||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db.models.signals import post_delete, post_save
|
from django.db.models.signals import post_delete, post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||||
|
|
||||||
from .models import Circuit, CircuitType, Provider
|
from .models import Circuit, CircuitType, Provider
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
@ -12,7 +14,7 @@ urlpatterns = [
|
|||||||
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
||||||
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||||
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||||
url(r'^providers/(?P<slug>[\w-]+)/$', views.provider, name='provider'),
|
url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
|
||||||
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
|
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
|
||||||
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
||||||
|
|
||||||
@ -28,7 +30,7 @@ urlpatterns = [
|
|||||||
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
||||||
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
||||||
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
||||||
url(r'^circuits/(?P<pk>\d+)/$', views.circuit, name='circuit'),
|
url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), 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'),
|
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
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
|
||||||
@ -5,13 +7,13 @@ 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, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.views.generic import View
|
||||||
|
|
||||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||||
from utilities.forms import ConfirmationForm
|
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, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
|
from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
|
||||||
|
|
||||||
@ -28,11 +30,16 @@ class ProviderListView(ObjectListView):
|
|||||||
template_name = 'circuits/provider_list.html'
|
template_name = 'circuits/provider_list.html'
|
||||||
|
|
||||||
|
|
||||||
def provider(request, slug):
|
class ProviderView(View):
|
||||||
|
|
||||||
|
def get(self, 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('type', 'tenant')\
|
circuits = Circuit.objects.filter(provider=provider).select_related(
|
||||||
.prefetch_related('terminations__site')
|
'type', 'tenant'
|
||||||
|
).prefetch_related(
|
||||||
|
'terminations__site'
|
||||||
|
)
|
||||||
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', {
|
||||||
@ -117,7 +124,9 @@ class CircuitListView(ObjectListView):
|
|||||||
template_name = 'circuits/circuit_list.html'
|
template_name = 'circuits/circuit_list.html'
|
||||||
|
|
||||||
|
|
||||||
def circuit(request, pk):
|
class CircuitView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
|
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
|
||||||
termination_a = CircuitTermination.objects.select_related(
|
termination_a = CircuitTermination.objects.select_related(
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
|
||||||
@ -618,10 +620,11 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
|
|||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
||||||
|
lag = NestedInterfaceSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['id', 'url', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
fields = ['id', 'url', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
|
||||||
|
|
||||||
|
|
||||||
class WritableInterfaceSerializer(serializers.ModelSerializer):
|
class WritableInterfaceSerializer(serializers.ModelSerializer):
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
from rest_framework.mixins import ListModelMixin
|
from rest_framework.mixins import ListModelMixin
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from netaddr import EUI, mac_unix_expanded
|
from netaddr import EUI, mac_unix_expanded
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from netaddr import EUI, AddrFormatError
|
from netaddr import EUI, AddrFormatError
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from mptt.forms import TreeNodeChoiceField
|
from mptt.forms import TreeNodeChoiceField
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -16,7 +18,6 @@ from utilities.forms import (
|
|||||||
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||||
FilterTreeNodeMultipleChoiceField,
|
FilterTreeNodeMultipleChoiceField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .formfields import MACAddressFormField
|
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,
|
||||||
@ -189,7 +190,9 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||||
group = ChainedModelChoiceField(
|
group = ChainedModelChoiceField(
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
chains={'site': 'site'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/rack-groups/?site_id={{site}}',
|
api_url='/api/dcim/rack-groups/?site_id={{site}}',
|
||||||
@ -544,7 +547,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
)
|
)
|
||||||
rack = ChainedModelChoiceField(
|
rack = ChainedModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
chains={'site': 'site'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||||
@ -569,7 +574,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
)
|
)
|
||||||
device_type = ChainedModelChoiceField(
|
device_type = ChainedModelChoiceField(
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
chains={'manufacturer': 'manufacturer'},
|
chains=(
|
||||||
|
('manufacturer', 'manufacturer'),
|
||||||
|
),
|
||||||
label='Device type',
|
label='Device type',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
|
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
|
||||||
@ -610,10 +617,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
for family in [4, 6]:
|
for family in [4, 6]:
|
||||||
ip_choices = []
|
ip_choices = []
|
||||||
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
|
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
|
||||||
ip_choices += [(ip.id, u'{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||||
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
|
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
|
||||||
.select_related('nat_inside__interface')
|
.select_related('nat_inside__interface')
|
||||||
ip_choices += [(ip.id, u'{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
ip_choices += [(ip.id, '{} ({} 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
|
# If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
|
||||||
@ -804,7 +811,7 @@ def device_status_choices():
|
|||||||
status_counts = {}
|
status_counts = {}
|
||||||
for status in Device.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
for status in Device.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||||
status_counts[status['status']] = status['count']
|
status_counts[status['status']] = status['count']
|
||||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in STATUS_CHOICES]
|
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in STATUS_CHOICES]
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
@ -956,20 +963,29 @@ class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
|
|||||||
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
widget=forms.HiddenInput(),
|
required=False,
|
||||||
|
widget=forms.Select(
|
||||||
|
attrs={'filter-for': 'rack'}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
rack = ChainedModelChoiceField(
|
rack = ChainedModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
chains={'site': 'site'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
label='Rack',
|
label='Rack',
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||||
attrs={'filter-for': 'console_server', 'nullable': 'true'}
|
attrs={'filter-for': 'console_server', 'nullable': 'true'}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
console_server = ChainedModelChoiceField(
|
console_server = ChainedModelChoiceField(
|
||||||
queryset=Device.objects.filter(device_type__is_console_server=True),
|
queryset=Device.objects.filter(device_type__is_console_server=True),
|
||||||
chains={'site': 'site', 'rack': 'rack'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
('rack', 'rack'),
|
||||||
|
),
|
||||||
label='Console Server',
|
label='Console Server',
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -989,7 +1005,9 @@ class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelF
|
|||||||
)
|
)
|
||||||
cs_port = ChainedModelChoiceField(
|
cs_port = ChainedModelChoiceField(
|
||||||
queryset=ConsoleServerPort.objects.all(),
|
queryset=ConsoleServerPort.objects.all(),
|
||||||
chains={'device': 'console_server'},
|
chains=(
|
||||||
|
('device', 'console_server'),
|
||||||
|
),
|
||||||
label='Port',
|
label='Port',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
|
api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
|
||||||
@ -1034,20 +1052,29 @@ class ConsoleServerPortCreateForm(DeviceComponentForm):
|
|||||||
class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
widget=forms.HiddenInput(),
|
required=False,
|
||||||
|
widget=forms.Select(
|
||||||
|
attrs={'filter-for': 'rack'}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
rack = ChainedModelChoiceField(
|
rack = ChainedModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
chains={'site': 'site'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
label='Rack',
|
label='Rack',
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
device = ChainedModelChoiceField(
|
device = ChainedModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
chains={'site': 'site', 'rack': 'rack'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
('rack', 'rack'),
|
||||||
|
),
|
||||||
label='Device',
|
label='Device',
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -1067,7 +1094,9 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.
|
|||||||
)
|
)
|
||||||
port = ChainedModelChoiceField(
|
port = ChainedModelChoiceField(
|
||||||
queryset=ConsolePort.objects.all(),
|
queryset=ConsolePort.objects.all(),
|
||||||
chains={'device': 'device'},
|
chains=(
|
||||||
|
('device', 'device'),
|
||||||
|
),
|
||||||
label='Port',
|
label='Port',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/console-ports/?device_id={{device}}',
|
api_url='/api/dcim/console-ports/?device_id={{device}}',
|
||||||
@ -1181,19 +1210,31 @@ class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
|
|||||||
|
|
||||||
|
|
||||||
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.HiddenInput())
|
site = forms.ModelChoiceField(
|
||||||
rack = ChainedModelChoiceField(
|
queryset=Site.objects.all(),
|
||||||
queryset=Rack.objects.all(),
|
|
||||||
chains={'site': 'site'},
|
|
||||||
label='Rack',
|
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(
|
widget=forms.Select(
|
||||||
|
attrs={'filter-for': 'rack'}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rack = ChainedModelChoiceField(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
|
label='Rack',
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||||
attrs={'filter-for': 'pdu', 'nullable': 'true'}
|
attrs={'filter-for': 'pdu', 'nullable': 'true'}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
pdu = ChainedModelChoiceField(
|
pdu = ChainedModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
chains={'site': 'site', 'rack': 'rack'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
('rack', 'rack'),
|
||||||
|
),
|
||||||
label='PDU',
|
label='PDU',
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -1213,7 +1254,9 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
|||||||
)
|
)
|
||||||
power_outlet = ChainedModelChoiceField(
|
power_outlet = ChainedModelChoiceField(
|
||||||
queryset=PowerOutlet.objects.all(),
|
queryset=PowerOutlet.objects.all(),
|
||||||
chains={'device': 'pdu'},
|
chains=(
|
||||||
|
('device', 'pdu'),
|
||||||
|
),
|
||||||
label='Outlet',
|
label='Outlet',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
|
api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
|
||||||
@ -1258,20 +1301,29 @@ class PowerOutletCreateForm(DeviceComponentForm):
|
|||||||
class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
widget=forms.HiddenInput()
|
required=False,
|
||||||
|
widget=forms.Select(
|
||||||
|
attrs={'filter-for': 'rack'}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
rack = ChainedModelChoiceField(
|
rack = ChainedModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
chains={'site': 'site'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
label='Rack',
|
label='Rack',
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
device = ChainedModelChoiceField(
|
device = ChainedModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
chains={'site': 'site', 'rack': 'rack'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
('rack', 'rack'),
|
||||||
|
),
|
||||||
label='Device',
|
label='Device',
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -1291,7 +1343,9 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
|||||||
)
|
)
|
||||||
port = ChainedModelChoiceField(
|
port = ChainedModelChoiceField(
|
||||||
queryset=PowerPort.objects.all(),
|
queryset=PowerPort.objects.all(),
|
||||||
chains={'device': 'device'},
|
chains=(
|
||||||
|
('device', 'device'),
|
||||||
|
),
|
||||||
label='Port',
|
label='Port',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/power-ports/?device_id={{device}}',
|
api_url='/api/dcim/power-ports/?device_id={{device}}',
|
||||||
@ -1411,7 +1465,9 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
|||||||
)
|
)
|
||||||
rack_b = ChainedModelChoiceField(
|
rack_b = ChainedModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
chains={'site': 'site_b'},
|
chains=(
|
||||||
|
('site', 'site_b'),
|
||||||
|
),
|
||||||
label='Rack',
|
label='Rack',
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -1421,7 +1477,10 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
|||||||
)
|
)
|
||||||
device_b = ChainedModelChoiceField(
|
device_b = ChainedModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
chains={'site': 'site_b', 'rack': 'rack_b'},
|
chains=(
|
||||||
|
('site', 'site_b'),
|
||||||
|
('rack', 'rack_b'),
|
||||||
|
),
|
||||||
label='Device',
|
label='Device',
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -1443,7 +1502,9 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
|||||||
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
|
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
|
||||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
),
|
),
|
||||||
chains={'device': 'device_b'},
|
chains=(
|
||||||
|
('device', 'device_b'),
|
||||||
|
),
|
||||||
label='Interface',
|
label='Interface',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
|
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
|
||||||
|
209
netbox/dcim/migrations/0037_unicode_literals.py
Normal file
209
netbox/dcim/migrations/0037_unicode_literals.py
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import dcim.fields
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import utilities.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0036_add_ff_juniper_vcp'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='connection_status',
|
||||||
|
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='cs_port',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name='Console server port'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='asset_tag',
|
||||||
|
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='face',
|
||||||
|
field=models.PositiveSmallIntegerField(blank=True, choices=[[0, 'Front'], [1, 'Rear']], null=True, verbose_name='Rack face'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='position',
|
||||||
|
field=models.PositiveSmallIntegerField(blank=True, help_text='The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Position (U)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='primary_ip4',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name='Primary IPv4'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='primary_ip6',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name='Primary IPv6'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='serial',
|
||||||
|
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='status',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicebay',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=50, verbose_name='Name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='interface_ordering',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[[1, 'Slot/position'], [2, 'Name (alphabetically)']], default=1),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='is_console_server',
|
||||||
|
field=models.BooleanField(default=False, help_text='This type of device has console server ports', verbose_name='Is a console server'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='is_full_depth',
|
||||||
|
field=models.BooleanField(default=True, help_text='Device consumes both front and rear rack faces', verbose_name='Is full depth'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='is_network_device',
|
||||||
|
field=models.BooleanField(default=True, help_text='This type of device has network interfaces', verbose_name='Is a network device'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='is_pdu',
|
||||||
|
field=models.BooleanField(default=False, help_text='This type of device has power outlets', verbose_name='Is a PDU'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='part_number',
|
||||||
|
field=models.CharField(blank=True, help_text='Discrete part number (optional)', max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='subdevice_role',
|
||||||
|
field=models.NullBooleanField(choices=[(None, 'None'), (True, 'Parent'), (False, 'Child')], default=None, help_text='Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name='Parent/child status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='u_height',
|
||||||
|
field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='lag',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='mac_address',
|
||||||
|
field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='mgmt_only',
|
||||||
|
field=models.BooleanField(default=False, help_text='This interface is used only for out-of-band management', verbose_name='OOB Management'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfaceconnection',
|
||||||
|
name='connection_status',
|
||||||
|
field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='form_factor',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='mgmt_only',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Management only'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='discovered',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Discovered'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=50, verbose_name='Name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='part_id',
|
||||||
|
field=models.CharField(blank=True, max_length=50, verbose_name='Part ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='serial',
|
||||||
|
field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='platform',
|
||||||
|
name='rpc_client',
|
||||||
|
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='RPC client'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='connection_status',
|
||||||
|
field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rack',
|
||||||
|
name='desc_units',
|
||||||
|
field=models.BooleanField(default=False, help_text='Units are numbered top-to-bottom', verbose_name='Descending units'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rack',
|
||||||
|
name='facility_id',
|
||||||
|
field=utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name='Facility ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rack',
|
||||||
|
name='type',
|
||||||
|
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, '2-post frame'), (200, '4-post frame'), (300, '4-post cabinet'), (1000, 'Wall-mounted frame'), (1100, 'Wall-mounted cabinet')], null=True, verbose_name='Type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rack',
|
||||||
|
name='u_height',
|
||||||
|
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Height (U)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rack',
|
||||||
|
name='width',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(19, '19 inches'), (23, '23 inches')], default=19, help_text='Rail-to-rail width', verbose_name='Width'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='site',
|
||||||
|
name='asn',
|
||||||
|
field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='site',
|
||||||
|
name='contact_email',
|
||||||
|
field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,4 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from itertools import count, groupby
|
from itertools import count, groupby
|
||||||
|
|
||||||
@ -23,7 +24,6 @@ from utilities.fields import ColorField, NullableCharField
|
|||||||
from utilities.managers import NaturalOrderByManager
|
from utilities.managers import NaturalOrderByManager
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
from utilities.utils import csv_format
|
from utilities.utils import csv_format
|
||||||
|
|
||||||
from .fields import ASNField, MACAddressField
|
from .fields import ASNField, MACAddressField
|
||||||
|
|
||||||
|
|
||||||
@ -346,7 +346,7 @@ class RackGroup(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u'{} - {}'.format(self.site.name, self.name)
|
return '{} - {}'.format(self.site.name, self.name)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||||
@ -466,10 +466,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
@property
|
@property
|
||||||
def display_name(self):
|
def display_name(self):
|
||||||
if self.facility_id:
|
if self.facility_id:
|
||||||
return u"{} ({})".format(self.name, self.facility_id)
|
return "{} ({})".format(self.name, self.facility_id)
|
||||||
elif self.name:
|
elif self.name:
|
||||||
return self.name
|
return self.name
|
||||||
return u""
|
return ""
|
||||||
|
|
||||||
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
|
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
|
||||||
"""
|
"""
|
||||||
@ -569,7 +569,7 @@ class RackReservation(models.Model):
|
|||||||
ordering = ['created']
|
ordering = ['created']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u"Reservation for rack {}".format(self.rack)
|
return "Reservation for rack {}".format(self.rack)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
@ -579,7 +579,7 @@ class RackReservation(models.Model):
|
|||||||
invalid_units = [u for u in self.units if u not in self.rack.units]
|
invalid_units = [u for u in self.units if u not in self.rack.units]
|
||||||
if invalid_units:
|
if invalid_units:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'units': u"Invalid unit(s) for {}U rack: {}".format(
|
'units': "Invalid unit(s) for {}U rack: {}".format(
|
||||||
self.rack.u_height,
|
self.rack.u_height,
|
||||||
', '.join([str(u) for u in invalid_units]),
|
', '.join([str(u) for u in invalid_units]),
|
||||||
),
|
),
|
||||||
@ -733,7 +733,7 @@ class DeviceType(models.Model, CustomFieldModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
return u'{} {}'.format(self.manufacturer.name, self.model)
|
return '{} {}'.format(self.manufacturer.name, self.model)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_parent_device(self):
|
def is_parent_device(self):
|
||||||
@ -1106,8 +1106,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
if self.name:
|
if self.name:
|
||||||
return self.name
|
return self.name
|
||||||
elif hasattr(self, 'device_type'):
|
elif hasattr(self, 'device_type'):
|
||||||
return u"{}".format(self.device_type)
|
return "{}".format(self.device_type)
|
||||||
return u""
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self):
|
def identifier(self):
|
||||||
@ -1320,7 +1320,7 @@ class Interface(models.Model):
|
|||||||
# An interface's LAG must belong to the same device
|
# An interface's LAG must belong to the same device
|
||||||
if self.lag and self.lag.device != self.device:
|
if self.lag and self.lag.device != self.device:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'lag': u"The selected LAG interface ({}) belongs to a different device ({}).".format(
|
'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format(
|
||||||
self.lag.name, self.lag.device.name
|
self.lag.name, self.lag.device.name
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1328,14 +1328,14 @@ class Interface(models.Model):
|
|||||||
# A virtual interface cannot have a parent LAG
|
# A virtual interface cannot have a parent LAG
|
||||||
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None:
|
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
|
'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
|
||||||
})
|
})
|
||||||
|
|
||||||
# Only a LAG can have LAG members
|
# Only a LAG can have LAG members
|
||||||
if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists():
|
if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists():
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format(
|
'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format(
|
||||||
u", ".join([iface.name for iface in self.member_interfaces.all()])
|
", ".join([iface.name for iface in self.member_interfaces.all()])
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1428,7 +1428,7 @@ class DeviceBay(models.Model):
|
|||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u'{} - {}'.format(self.device.name, self.name)
|
return '{} - {}'.format(self.device.name, self.name)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||||
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.forms import *
|
from dcim.forms import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
|
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from extras.views import ImageAttachmentEditView
|
||||||
from ipam.views import ServiceEditView
|
from ipam.views import ServiceEditView
|
||||||
from secrets.views import secret_add
|
from secrets.views import secret_add
|
||||||
|
|
||||||
from extras.views import ImageAttachmentEditView
|
|
||||||
from .models import Device, Rack, Site
|
from .models import Device, Rack, Site
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ urlpatterns = [
|
|||||||
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
|
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
|
||||||
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
|
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||||
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
||||||
url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
|
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
|
||||||
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
|
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
|
||||||
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
|
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||||
url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
||||||
@ -52,7 +53,7 @@ urlpatterns = [
|
|||||||
url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'),
|
url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'),
|
||||||
url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
|
url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
|
||||||
url(r'^racks/delete/$', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
url(r'^racks/delete/$', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
||||||
url(r'^racks/(?P<pk>\d+)/$', views.rack, name='rack'),
|
url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
|
||||||
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
||||||
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||||
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
|
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
|
||||||
@ -69,7 +70,7 @@ urlpatterns = [
|
|||||||
url(r'^device-types/add/$', views.DeviceTypeEditView.as_view(), name='devicetype_add'),
|
url(r'^device-types/add/$', views.DeviceTypeEditView.as_view(), name='devicetype_add'),
|
||||||
url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||||
url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/$', views.devicetype, name='devicetype'),
|
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||||
|
|
||||||
@ -117,11 +118,11 @@ urlpatterns = [
|
|||||||
url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
|
url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
|
||||||
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
||||||
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||||
url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'),
|
url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
|
||||||
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
|
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
|
||||||
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
|
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
|
||||||
url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'),
|
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||||
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.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||||
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'),
|
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'),
|
||||||
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import re
|
import re
|
||||||
from natsort import natsorted
|
from natsort import natsorted
|
||||||
@ -24,7 +25,6 @@ from utilities.paginator import EnhancedPaginator
|
|||||||
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 (
|
from .models import (
|
||||||
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||||
@ -109,11 +109,11 @@ class ComponentCreateView(View):
|
|||||||
if field == 'name':
|
if field == 'name':
|
||||||
field = 'name_pattern'
|
field = 'name_pattern'
|
||||||
for e in errors:
|
for e in errors:
|
||||||
form.add_error(field, u'{}: {}'.format(name, ', '.join(e)))
|
form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
|
||||||
|
|
||||||
if not form.errors:
|
if not form.errors:
|
||||||
self.model.objects.bulk_create(new_components)
|
self.model.objects.bulk_create(new_components)
|
||||||
messages.success(request, u"Added {} {} to {}.".format(
|
messages.success(request, "Added {} {} to {}.".format(
|
||||||
len(new_components), self.model._meta.verbose_name_plural, parent
|
len(new_components), self.model._meta.verbose_name_plural, parent
|
||||||
))
|
))
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
@ -178,7 +178,9 @@ class SiteListView(ObjectListView):
|
|||||||
template_name = 'dcim/site_list.html'
|
template_name = 'dcim/site_list.html'
|
||||||
|
|
||||||
|
|
||||||
def site(request, slug):
|
class SiteView(View):
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
|
||||||
site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
|
site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
|
||||||
stats = {
|
stats = {
|
||||||
@ -290,8 +292,13 @@ class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class RackListView(ObjectListView):
|
class RackListView(ObjectListView):
|
||||||
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type')\
|
queryset = Rack.objects.select_related(
|
||||||
.annotate(device_count=Count('devices', distinct=True))
|
'site', 'group', 'tenant', 'role'
|
||||||
|
).prefetch_related(
|
||||||
|
'devices__device_type'
|
||||||
|
).annotate(
|
||||||
|
device_count=Count('devices', distinct=True)
|
||||||
|
)
|
||||||
filter = filters.RackFilter
|
filter = filters.RackFilter
|
||||||
filter_form = forms.RackFilterForm
|
filter_form = forms.RackFilterForm
|
||||||
table = tables.RackTable
|
table = tables.RackTable
|
||||||
@ -338,7 +345,9 @@ class RackElevationListView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def rack(request, pk):
|
class RackView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
|
rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
|
||||||
|
|
||||||
@ -481,7 +490,9 @@ class DeviceTypeListView(ObjectListView):
|
|||||||
template_name = 'dcim/devicetype_list.html'
|
template_name = 'dcim/devicetype_list.html'
|
||||||
|
|
||||||
|
|
||||||
def devicetype(request, pk):
|
class DeviceTypeView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
devicetype = get_object_or_404(DeviceType, pk=pk)
|
devicetype = get_object_or_404(DeviceType, pk=pk)
|
||||||
|
|
||||||
@ -499,12 +510,14 @@ def devicetype(request, pk):
|
|||||||
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
)
|
)
|
||||||
mgmt_interface_table = tables.InterfaceTemplateTable(
|
mgmt_interface_table = tables.InterfaceTemplateTable(
|
||||||
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype,
|
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
|
||||||
mgmt_only=True))
|
device_type=devicetype, mgmt_only=True
|
||||||
|
))
|
||||||
)
|
)
|
||||||
interface_table = tables.InterfaceTemplateTable(
|
interface_table = tables.InterfaceTemplateTable(
|
||||||
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype,
|
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
|
||||||
mgmt_only=False))
|
device_type=devicetype, mgmt_only=False
|
||||||
|
))
|
||||||
)
|
)
|
||||||
devicebay_table = tables.DeviceBayTemplateTable(
|
devicebay_table = tables.DeviceBayTemplateTable(
|
||||||
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||||
@ -727,7 +740,9 @@ class DeviceListView(ObjectListView):
|
|||||||
template_name = 'dcim/device_list.html'
|
template_name = 'dcim/device_list.html'
|
||||||
|
|
||||||
|
|
||||||
def device(request, pk):
|
class DeviceView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
device = get_object_or_404(Device.objects.select_related(
|
device = get_object_or_404(Device.objects.select_related(
|
||||||
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
|
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
|
||||||
@ -744,14 +759,18 @@ 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.order_naturally(device.device_type.interface_ordering)\
|
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
|
||||||
.filter(device=device, mgmt_only=False)\
|
device=device, mgmt_only=False
|
||||||
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
|
).select_related(
|
||||||
'circuit_termination__circuit').prefetch_related('ip_addresses')
|
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
|
||||||
mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
|
'circuit_termination__circuit'
|
||||||
.filter(device=device, mgmt_only=True)\
|
).prefetch_related('ip_addresses')
|
||||||
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
|
mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
|
||||||
'circuit_termination__circuit').prefetch_related('ip_addresses')
|
device=device, mgmt_only=True
|
||||||
|
).select_related(
|
||||||
|
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
|
||||||
|
'circuit_termination__circuit'
|
||||||
|
).prefetch_related('ip_addresses')
|
||||||
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')
|
||||||
@ -793,6 +812,44 @@ def device(request, pk):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceInventoryView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
|
device = get_object_or_404(Device, pk=pk)
|
||||||
|
inventory_items = InventoryItem.objects.filter(
|
||||||
|
device=device, parent=None
|
||||||
|
).select_related(
|
||||||
|
'manufacturer'
|
||||||
|
).prefetch_related(
|
||||||
|
'child_items'
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(request, 'dcim/device_inventory.html', {
|
||||||
|
'device': device,
|
||||||
|
'inventory_items': inventory_items,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceLLDPNeighborsView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
|
device = get_object_or_404(Device, pk=pk)
|
||||||
|
interfaces = Interface.objects.order_naturally(
|
||||||
|
device.device_type.interface_ordering
|
||||||
|
).filter(
|
||||||
|
device=device
|
||||||
|
).select_related(
|
||||||
|
'connected_as_a', 'connected_as_b'
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(request, 'dcim/device_lldp_neighbors.html', {
|
||||||
|
'device': device,
|
||||||
|
'interfaces': interfaces,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
|
class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_device'
|
permission_required = 'dcim.change_device'
|
||||||
model = Device
|
model = Device
|
||||||
@ -851,30 +908,6 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
default_return_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
def device_inventory(request, pk):
|
|
||||||
|
|
||||||
device = get_object_or_404(Device, pk=pk)
|
|
||||||
inventory_items = InventoryItem.objects.filter(device=device, parent=None).select_related('manufacturer')\
|
|
||||||
.prefetch_related('child_items')
|
|
||||||
|
|
||||||
return render(request, 'dcim/device_inventory.html', {
|
|
||||||
'device': device,
|
|
||||||
'inventory_items': inventory_items,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def device_lldp_neighbors(request, pk):
|
|
||||||
|
|
||||||
device = get_object_or_404(Device, pk=pk)
|
|
||||||
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\
|
|
||||||
.select_related('connected_as_a', 'connected_as_b')
|
|
||||||
|
|
||||||
return render(request, 'dcim/device_lldp_neighbors.html', {
|
|
||||||
'device': device,
|
|
||||||
'interfaces': interfaces,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console ports
|
# Console ports
|
||||||
#
|
#
|
||||||
@ -897,7 +930,7 @@ def consoleport_connect(request, pk):
|
|||||||
form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport)
|
form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
consoleport = form.save()
|
consoleport = form.save()
|
||||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||||
consoleport.device.get_absolute_url(),
|
consoleport.device.get_absolute_url(),
|
||||||
escape(consoleport.device),
|
escape(consoleport.device),
|
||||||
escape(consoleport.name),
|
escape(consoleport.name),
|
||||||
@ -911,9 +944,9 @@ def consoleport_connect(request, pk):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
form = forms.ConsolePortConnectionForm(instance=consoleport, initial={
|
form = forms.ConsolePortConnectionForm(instance=consoleport, initial={
|
||||||
'site': request.GET.get('site', consoleport.device.site),
|
'site': request.GET.get('site'),
|
||||||
'rack': request.GET.get('rack', None),
|
'rack': request.GET.get('rack'),
|
||||||
'console_server': request.GET.get('console_server', None),
|
'console_server': request.GET.get('console_server'),
|
||||||
'connection_status': CONNECTION_STATUS_CONNECTED,
|
'connection_status': CONNECTION_STATUS_CONNECTED,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -931,7 +964,7 @@ def consoleport_disconnect(request, pk):
|
|||||||
|
|
||||||
if not consoleport.cs_port:
|
if not consoleport.cs_port:
|
||||||
messages.warning(
|
messages.warning(
|
||||||
request, u"Cannot disconnect console port {}: It is not connected to anything.".format(consoleport)
|
request, "Cannot disconnect console port {}: It is not connected to anything.".format(consoleport)
|
||||||
)
|
)
|
||||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||||
|
|
||||||
@ -942,7 +975,7 @@ def consoleport_disconnect(request, pk):
|
|||||||
consoleport.cs_port = None
|
consoleport.cs_port = None
|
||||||
consoleport.connection_status = None
|
consoleport.connection_status = None
|
||||||
consoleport.save()
|
consoleport.save()
|
||||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||||
consoleport.device.get_absolute_url(),
|
consoleport.device.get_absolute_url(),
|
||||||
escape(consoleport.device),
|
escape(consoleport.device),
|
||||||
escape(consoleport.name),
|
escape(consoleport.name),
|
||||||
@ -1014,7 +1047,7 @@ def consoleserverport_connect(request, pk):
|
|||||||
consoleport.cs_port = consoleserverport
|
consoleport.cs_port = consoleserverport
|
||||||
consoleport.connection_status = form.cleaned_data['connection_status']
|
consoleport.connection_status = form.cleaned_data['connection_status']
|
||||||
consoleport.save()
|
consoleport.save()
|
||||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||||
consoleport.device.get_absolute_url(),
|
consoleport.device.get_absolute_url(),
|
||||||
escape(consoleport.device),
|
escape(consoleport.device),
|
||||||
escape(consoleport.name),
|
escape(consoleport.name),
|
||||||
@ -1028,9 +1061,9 @@ def consoleserverport_connect(request, pk):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
form = forms.ConsoleServerPortConnectionForm(initial={
|
form = forms.ConsoleServerPortConnectionForm(initial={
|
||||||
'site': request.GET.get('site', consoleserverport.device.site),
|
'site': request.GET.get('site'),
|
||||||
'rack': request.GET.get('rack', None),
|
'rack': request.GET.get('rack'),
|
||||||
'device': request.GET.get('device', None),
|
'device': request.GET.get('device'),
|
||||||
'connection_status': CONNECTION_STATUS_CONNECTED,
|
'connection_status': CONNECTION_STATUS_CONNECTED,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1048,7 +1081,7 @@ def consoleserverport_disconnect(request, pk):
|
|||||||
|
|
||||||
if not hasattr(consoleserverport, 'connected_console'):
|
if not hasattr(consoleserverport, 'connected_console'):
|
||||||
messages.warning(
|
messages.warning(
|
||||||
request, u"Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport)
|
request, "Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport)
|
||||||
)
|
)
|
||||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||||
|
|
||||||
@ -1059,7 +1092,7 @@ def consoleserverport_disconnect(request, pk):
|
|||||||
consoleport.cs_port = None
|
consoleport.cs_port = None
|
||||||
consoleport.connection_status = None
|
consoleport.connection_status = None
|
||||||
consoleport.save()
|
consoleport.save()
|
||||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||||
consoleport.device.get_absolute_url(),
|
consoleport.device.get_absolute_url(),
|
||||||
escape(consoleport.device),
|
escape(consoleport.device),
|
||||||
escape(consoleport.name),
|
escape(consoleport.name),
|
||||||
@ -1120,7 +1153,7 @@ def powerport_connect(request, pk):
|
|||||||
form = forms.PowerPortConnectionForm(request.POST, instance=powerport)
|
form = forms.PowerPortConnectionForm(request.POST, instance=powerport)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
powerport = form.save()
|
powerport = form.save()
|
||||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||||
powerport.device.get_absolute_url(),
|
powerport.device.get_absolute_url(),
|
||||||
escape(powerport.device),
|
escape(powerport.device),
|
||||||
escape(powerport.name),
|
escape(powerport.name),
|
||||||
@ -1134,9 +1167,9 @@ def powerport_connect(request, pk):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
form = forms.PowerPortConnectionForm(instance=powerport, initial={
|
form = forms.PowerPortConnectionForm(instance=powerport, initial={
|
||||||
'site': request.GET.get('site', powerport.device.site),
|
'site': request.GET.get('site'),
|
||||||
'rack': request.GET.get('rack', None),
|
'rack': request.GET.get('rack'),
|
||||||
'pdu': request.GET.get('pdu', None),
|
'pdu': request.GET.get('pdu'),
|
||||||
'connection_status': CONNECTION_STATUS_CONNECTED,
|
'connection_status': CONNECTION_STATUS_CONNECTED,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1154,7 +1187,7 @@ def powerport_disconnect(request, pk):
|
|||||||
|
|
||||||
if not powerport.power_outlet:
|
if not powerport.power_outlet:
|
||||||
messages.warning(
|
messages.warning(
|
||||||
request, u"Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport)
|
request, "Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport)
|
||||||
)
|
)
|
||||||
return redirect('dcim:device', pk=powerport.device.pk)
|
return redirect('dcim:device', pk=powerport.device.pk)
|
||||||
|
|
||||||
@ -1165,7 +1198,7 @@ def powerport_disconnect(request, pk):
|
|||||||
powerport.power_outlet = None
|
powerport.power_outlet = None
|
||||||
powerport.connection_status = None
|
powerport.connection_status = None
|
||||||
powerport.save()
|
powerport.save()
|
||||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||||
powerport.device.get_absolute_url(),
|
powerport.device.get_absolute_url(),
|
||||||
escape(powerport.device),
|
escape(powerport.device),
|
||||||
escape(powerport.name),
|
escape(powerport.name),
|
||||||
@ -1237,7 +1270,7 @@ def poweroutlet_connect(request, pk):
|
|||||||
powerport.power_outlet = poweroutlet
|
powerport.power_outlet = poweroutlet
|
||||||
powerport.connection_status = form.cleaned_data['connection_status']
|
powerport.connection_status = form.cleaned_data['connection_status']
|
||||||
powerport.save()
|
powerport.save()
|
||||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||||
powerport.device.get_absolute_url(),
|
powerport.device.get_absolute_url(),
|
||||||
escape(powerport.device),
|
escape(powerport.device),
|
||||||
escape(powerport.name),
|
escape(powerport.name),
|
||||||
@ -1251,9 +1284,9 @@ def poweroutlet_connect(request, pk):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
form = forms.PowerOutletConnectionForm(initial={
|
form = forms.PowerOutletConnectionForm(initial={
|
||||||
'site': request.GET.get('site', poweroutlet.device.site),
|
'site': request.GET.get('site'),
|
||||||
'rack': request.GET.get('rack', None),
|
'rack': request.GET.get('rack'),
|
||||||
'device': request.GET.get('device', None),
|
'device': request.GET.get('device'),
|
||||||
'connection_status': CONNECTION_STATUS_CONNECTED,
|
'connection_status': CONNECTION_STATUS_CONNECTED,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1271,7 +1304,7 @@ def poweroutlet_disconnect(request, pk):
|
|||||||
|
|
||||||
if not hasattr(poweroutlet, 'connected_port'):
|
if not hasattr(poweroutlet, 'connected_port'):
|
||||||
messages.warning(
|
messages.warning(
|
||||||
request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet)
|
request, "Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet)
|
||||||
)
|
)
|
||||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||||
|
|
||||||
@ -1282,7 +1315,7 @@ def poweroutlet_disconnect(request, pk):
|
|||||||
powerport.power_outlet = None
|
powerport.power_outlet = None
|
||||||
powerport.connection_status = None
|
powerport.connection_status = None
|
||||||
powerport.save()
|
powerport.save()
|
||||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||||
powerport.device.get_absolute_url(),
|
powerport.device.get_absolute_url(),
|
||||||
escape(powerport.device),
|
escape(powerport.device),
|
||||||
escape(powerport.name),
|
escape(powerport.name),
|
||||||
@ -1396,7 +1429,7 @@ def devicebay_populate(request, pk):
|
|||||||
device_bay.save()
|
device_bay.save()
|
||||||
|
|
||||||
if not form.errors:
|
if not form.errors:
|
||||||
messages.success(request, u"Added {} to {}.".format(device_bay.installed_device, device_bay))
|
messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
|
||||||
return redirect('dcim:device', pk=device_bay.device.pk)
|
return redirect('dcim:device', pk=device_bay.device.pk)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -1420,7 +1453,7 @@ def devicebay_depopulate(request, pk):
|
|||||||
removed_device = device_bay.installed_device
|
removed_device = device_bay.installed_device
|
||||||
device_bay.installed_device = None
|
device_bay.installed_device = None
|
||||||
device_bay.save()
|
device_bay.save()
|
||||||
messages.success(request, u"{} has been removed from {}.".format(removed_device, device_bay))
|
messages.success(request, "{} has been removed from {}.".format(removed_device, device_bay))
|
||||||
return redirect('dcim:device', pk=device_bay.device.pk)
|
return redirect('dcim:device', pk=device_bay.device.pk)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -1483,11 +1516,11 @@ class DeviceBulkAddComponentView(View):
|
|||||||
else:
|
else:
|
||||||
for field, errors in component_form.errors.as_data().items():
|
for field, errors in component_form.errors.as_data().items():
|
||||||
for e in errors:
|
for e in errors:
|
||||||
form.add_error(field, u'{} {}: {}'.format(device, name, ', '.join(e)))
|
form.add_error(field, '{} {}: {}'.format(device, name, ', '.join(e)))
|
||||||
|
|
||||||
if not form.errors:
|
if not form.errors:
|
||||||
self.model.objects.bulk_create(new_components)
|
self.model.objects.bulk_create(new_components)
|
||||||
messages.success(request, u"Added {} {} to {} devices.".format(
|
messages.success(request, "Added {} {} to {} devices.".format(
|
||||||
len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk'])
|
len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk'])
|
||||||
))
|
))
|
||||||
return redirect('dcim:device_list')
|
return redirect('dcim:device_list')
|
||||||
@ -1497,7 +1530,7 @@ class DeviceBulkAddComponentView(View):
|
|||||||
|
|
||||||
selected_devices = Device.objects.filter(pk__in=pk_list)
|
selected_devices = Device.objects.filter(pk__in=pk_list)
|
||||||
if not selected_devices:
|
if not selected_devices:
|
||||||
messages.warning(request, u"No devices were selected.")
|
messages.warning(request, "No devices were selected.")
|
||||||
return redirect('dcim:device_list')
|
return redirect('dcim:device_list')
|
||||||
|
|
||||||
return render(request, 'dcim/device_bulk_add_component.html', {
|
return render(request, 'dcim/device_bulk_add_component.html', {
|
||||||
@ -1559,7 +1592,7 @@ def interfaceconnection_add(request, pk):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
||||||
interfaceconnection = form.save()
|
interfaceconnection = form.save()
|
||||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||||
interfaceconnection.interface_a.device.get_absolute_url(),
|
interfaceconnection.interface_a.device.get_absolute_url(),
|
||||||
escape(interfaceconnection.interface_a.device),
|
escape(interfaceconnection.interface_a.device),
|
||||||
escape(interfaceconnection.interface_a.name),
|
escape(interfaceconnection.interface_a.name),
|
||||||
@ -1583,11 +1616,11 @@ def interfaceconnection_add(request, pk):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
form = forms.InterfaceConnectionForm(device, initial={
|
form = forms.InterfaceConnectionForm(device, initial={
|
||||||
'interface_a': request.GET.get('interface_a', None),
|
'interface_a': request.GET.get('interface_a'),
|
||||||
'site_b': request.GET.get('site_b', device.site),
|
'site_b': request.GET.get('site_b'),
|
||||||
'rack_b': request.GET.get('rack_b', None),
|
'rack_b': request.GET.get('rack_b'),
|
||||||
'device_b': request.GET.get('device_b', None),
|
'device_b': request.GET.get('device_b'),
|
||||||
'interface_b': request.GET.get('interface_b', None),
|
'interface_b': request.GET.get('interface_b'),
|
||||||
})
|
})
|
||||||
|
|
||||||
return render(request, 'dcim/interfaceconnection_edit.html', {
|
return render(request, 'dcim/interfaceconnection_edit.html', {
|
||||||
@ -1607,7 +1640,7 @@ def interfaceconnection_delete(request, pk):
|
|||||||
form = forms.InterfaceConnectionDeletionForm(request.POST)
|
form = forms.InterfaceConnectionDeletionForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
interfaceconnection.delete()
|
interfaceconnection.delete()
|
||||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||||
interfaceconnection.interface_a.device.get_absolute_url(),
|
interfaceconnection.interface_a.device.get_absolute_url(),
|
||||||
escape(interfaceconnection.interface_a.device),
|
escape(interfaceconnection.interface_a.device),
|
||||||
escape(interfaceconnection.interface_a.name),
|
escape(interfaceconnection.interface_a.name),
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from __future__ import unicode_literals
|
||||||
from django.db import transaction
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue
|
from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue
|
||||||
|
|
||||||
|
|
||||||
@ -25,14 +27,14 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
|||||||
|
|
||||||
# Validate custom field name
|
# Validate custom field name
|
||||||
if field_name not in custom_fields:
|
if field_name not in custom_fields:
|
||||||
raise ValidationError(u"Invalid custom field for {} objects: {}".format(content_type, field_name))
|
raise ValidationError("Invalid custom field for {} objects: {}".format(content_type, field_name))
|
||||||
|
|
||||||
# Validate selected choice
|
# Validate selected choice
|
||||||
cf = custom_fields[field_name]
|
cf = custom_fields[field_name]
|
||||||
if cf.type == CF_TYPE_SELECT:
|
if cf.type == CF_TYPE_SELECT:
|
||||||
valid_choices = [c.pk for c in cf.choices.all()]
|
valid_choices = [c.pk for c in cf.choices.all()]
|
||||||
if value not in valid_choices:
|
if value not in valid_choices:
|
||||||
raise ValidationError(u"Invalid choice ({}) for field {}".format(value, field_name))
|
raise ValidationError("Invalid choice ({}) for field {}".format(value, field_name))
|
||||||
|
|
||||||
# Check for missing required fields
|
# Check for missing required fields
|
||||||
missing_fields = []
|
missing_fields = []
|
||||||
@ -40,7 +42,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
|||||||
if field.required and field_name not in data:
|
if field.required and field_name not in data:
|
||||||
missing_fields.append(field_name)
|
missing_fields.append(field_name)
|
||||||
if missing_fields:
|
if missing_fields:
|
||||||
raise ValidationError(u"Missing required fields: {}".format(u", ".join(missing_fields)))
|
raise ValidationError("Missing required fields: {}".format(u", ".join(missing_fields)))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from rest_framework import serializers
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
|
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
|
||||||
from dcim.models import Device, Rack, Site
|
from dcim.models import Device, Rack, Site
|
||||||
from extras.models import (
|
from extras.models import (
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -104,7 +105,7 @@ class CustomFieldForm(forms.ModelForm):
|
|||||||
obj_id=self.instance.pk)
|
obj_id=self.instance.pk)
|
||||||
except CustomFieldValue.DoesNotExist:
|
except CustomFieldValue.DoesNotExist:
|
||||||
# Skip this field if none exists already and its value is empty
|
# Skip this field if none exists already and its value is empty
|
||||||
if self.cleaned_data[field_name] in [None, u'']:
|
if self.cleaned_data[field_name] in [None, '']:
|
||||||
continue
|
continue
|
||||||
cfv = CustomFieldValue(
|
cfv = CustomFieldValue(
|
||||||
field=self.fields[field_name].model,
|
field=self.fields[field_name].model,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
from ncclient.transport.errors import AuthenticationError
|
from ncclient.transport.errors import AuthenticationError
|
||||||
from paramiko import AuthenticationException
|
from paramiko import AuthenticationException
|
||||||
|
91
netbox/extras/migrations/0007_unicode_literals.py
Normal file
91
netbox/extras/migrations/0007_unicode_literals.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import extras.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0006_add_imageattachments'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='default',
|
||||||
|
field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='is_filterable',
|
||||||
|
field=models.BooleanField(default=True, help_text='This field can be used to filter objects.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='label',
|
||||||
|
field=models.CharField(blank=True, help_text="Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='obj_type',
|
||||||
|
field=models.ManyToManyField(help_text='The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='required',
|
||||||
|
field=models.BooleanField(default=False, help_text='Determines whether this field is required when creating new objects or editing an existing object.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='type',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(100, 'Text'), (200, 'Integer'), (300, 'Boolean (true/false)'), (400, 'Date'), (500, 'URL'), (600, 'Selection')], default=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='weight',
|
||||||
|
field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfieldchoice',
|
||||||
|
name='weight',
|
||||||
|
field=models.PositiveSmallIntegerField(default=100, help_text='Higher weights appear lower in the list'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='graph',
|
||||||
|
name='link',
|
||||||
|
field=models.URLField(blank=True, verbose_name='Link URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='graph',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=100, verbose_name='Name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='graph',
|
||||||
|
name='source',
|
||||||
|
field=models.CharField(max_length=500, verbose_name='Source URL'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='graph',
|
||||||
|
name='type',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(100, 'Interface'), (200, 'Provider'), (300, 'Site')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='imageattachment',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='topologymap',
|
||||||
|
name='device_patterns',
|
||||||
|
field=models.TextField(help_text='Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='useraction',
|
||||||
|
name='action',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(1, 'created'), (7, 'bulk created'), (2, 'imported'), (3, 'modified'), (4, 'bulk edited'), (5, 'deleted'), (6, 'bulk deleted')]),
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,4 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import graphviz
|
import graphviz
|
||||||
@ -175,7 +176,7 @@ class CustomFieldValue(models.Model):
|
|||||||
unique_together = ['field', 'obj_type', 'obj_id']
|
unique_together = ['field', 'obj_type', 'obj_id']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u'{} {}'.format(self.obj, self.field)
|
return '{} {}'.format(self.obj, self.field)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
@ -269,7 +270,7 @@ class ExportTemplate(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u'{}: {}'.format(self.content_type, self.name)
|
return '{}: {}'.format(self.content_type, self.name)
|
||||||
|
|
||||||
def to_response(self, context_dict, filename):
|
def to_response(self, context_dict, filename):
|
||||||
"""
|
"""
|
||||||
@ -387,7 +388,7 @@ def image_upload(instance, filename):
|
|||||||
elif instance.name:
|
elif instance.name:
|
||||||
filename = instance.name
|
filename = instance.name
|
||||||
|
|
||||||
return u'{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
|
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
@ -503,8 +504,8 @@ class UserAction(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.message:
|
if self.message:
|
||||||
return u'{} {}'.format(self.user, self.message)
|
return '{} {}'.format(self.user, self.message)
|
||||||
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
return '{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
if self.action in [ACTION_CREATE, ACTION_BULK_CREATE, ACTION_IMPORT]:
|
if self.action in [ACTION_CREATE, ACTION_BULK_CREATE, ACTION_IMPORT]:
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
from ncclient import manager
|
from ncclient import manager
|
||||||
import paramiko
|
import paramiko
|
||||||
import re
|
|
||||||
import xmltodict
|
import xmltodict
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
CONNECT_TIMEOUT = 5 # seconds
|
CONNECT_TIMEOUT = 5 # seconds
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -9,7 +10,6 @@ from django.test import TestCase
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
|
|
||||||
from extras.models import (
|
from extras.models import (
|
||||||
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
|
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
|
||||||
CF_TYPE_SELECT, CF_TYPE_URL,
|
CF_TYPE_SELECT, CF_TYPE_URL,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from extras import views
|
from extras import views
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
@ -8,7 +10,6 @@ from dcim.models import Site, Device, Interface
|
|||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
|
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
|
||||||
VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from netaddr import IPNetwork, AddrFormatError
|
from netaddr import IPNetwork, AddrFormatError
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
@ -10,7 +12,6 @@ from utilities.forms import (
|
|||||||
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField,
|
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField,
|
||||||
ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
|
ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
|
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
|
||||||
VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
||||||
@ -167,12 +168,21 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='Site',
|
||||||
|
widget=forms.Select(
|
||||||
attrs={'filter-for': 'vlan', 'nullable': 'true'}
|
attrs={'filter-for': 'vlan', 'nullable': 'true'}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
vlan = ChainedModelChoiceField(
|
vlan = ChainedModelChoiceField(
|
||||||
queryset=VLAN.objects.all(), chains={'site': 'site'}, required=False, label='VLAN', widget=APISelect(
|
queryset=VLAN.objects.all(),
|
||||||
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
label='VLAN',
|
||||||
|
widget=APISelect(
|
||||||
api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name'
|
api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -270,7 +280,7 @@ def prefix_status_choices():
|
|||||||
status_counts = {}
|
status_counts = {}
|
||||||
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||||
status_counts[status['status']] = status['count']
|
status_counts[status['status']] = status['count']
|
||||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
|
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
|
||||||
|
|
||||||
|
|
||||||
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
@ -321,7 +331,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
)
|
)
|
||||||
interface_rack = ChainedModelChoiceField(
|
interface_rack = ChainedModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
chains={'site': 'interface_site'},
|
chains=(
|
||||||
|
('site', 'interface_site'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
label='Rack',
|
label='Rack',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -332,7 +344,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
)
|
)
|
||||||
interface_device = ChainedModelChoiceField(
|
interface_device = ChainedModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
chains={'site': 'interface_site', 'rack': 'interface_rack'},
|
chains=(
|
||||||
|
('site', 'interface_site'),
|
||||||
|
('rack', 'interface_rack'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
label='Device',
|
label='Device',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -343,7 +358,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
)
|
)
|
||||||
interface = ChainedModelChoiceField(
|
interface = ChainedModelChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
chains={'device': 'interface_device'},
|
chains=(
|
||||||
|
('device', 'interface_device'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/interfaces/?device_id={{interface_device}}'
|
api_url='/api/dcim/interfaces/?device_id={{interface_device}}'
|
||||||
@ -354,34 +371,41 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
required=False,
|
required=False,
|
||||||
label='Site',
|
label='Site',
|
||||||
widget=forms.Select(
|
widget=forms.Select(
|
||||||
attrs={'filter-for': 'nat_device'}
|
attrs={'filter-for': 'nat_rack'}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
nat_rack = ChainedModelChoiceField(
|
nat_rack = ChainedModelChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
chains={'site': 'nat_site'},
|
chains=(
|
||||||
|
('site', 'nat_site'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
label='Rack',
|
label='Rack',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/racks/?site_id={{interface_site}}',
|
api_url='/api/dcim/racks/?site_id={{nat_site}}',
|
||||||
display_field='display_name',
|
display_field='display_name',
|
||||||
attrs={'filter-for': 'nat_device', 'nullable': 'true'}
|
attrs={'filter-for': 'nat_device', 'nullable': 'true'}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
nat_device = ChainedModelChoiceField(
|
nat_device = ChainedModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
chains={'site': 'nat_site'},
|
chains=(
|
||||||
|
('site', 'nat_site'),
|
||||||
|
('rack', 'nat_rack'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
label='Device',
|
label='Device',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/devices/?site_id={{nat_site}}',
|
api_url='/api/dcim/devices/?site_id={{nat_site}}&rack_id={{nat_rack}}',
|
||||||
display_field='display_name',
|
display_field='display_name',
|
||||||
attrs={'filter-for': 'nat_inside'}
|
attrs={'filter-for': 'nat_inside'}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
nat_inside = ChainedModelChoiceField(
|
nat_inside = ChainedModelChoiceField(
|
||||||
queryset=IPAddress.objects.all(),
|
queryset=IPAddress.objects.all(),
|
||||||
chains={'interface__device': 'nat_device'},
|
chains=(
|
||||||
|
('interface__device', 'nat_device'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
label='IP Address',
|
label='IP Address',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -391,7 +415,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
)
|
)
|
||||||
livesearch = forms.CharField(
|
livesearch = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label='IP Address',
|
label='Search',
|
||||||
widget=Livesearch(
|
widget=Livesearch(
|
||||||
query_key='q',
|
query_key='q',
|
||||||
query_url='ipam-api:ipaddress-list',
|
query_url='ipam-api:ipaddress-list',
|
||||||
@ -404,8 +428,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = [
|
fields = [
|
||||||
'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside', 'tenant_group',
|
'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
|
||||||
'tenant',
|
'nat_inside', 'tenant_group', 'tenant',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -567,7 +591,7 @@ def ipaddress_status_choices():
|
|||||||
status_counts = {}
|
status_counts = {}
|
||||||
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||||
status_counts[status['status']] = status['count']
|
status_counts[status['status']] = status['count']
|
||||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
|
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
|
||||||
|
|
||||||
|
|
||||||
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
@ -626,7 +650,9 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
)
|
)
|
||||||
group = ChainedModelChoiceField(
|
group = ChainedModelChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
chains={'site': 'site'},
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
label='Group',
|
label='Group',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -720,7 +746,7 @@ def vlan_status_choices():
|
|||||||
status_counts = {}
|
status_counts = {}
|
||||||
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||||
status_counts[status['status']] = status['count']
|
status_counts[status['status']] = status['count']
|
||||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
|
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
|
||||||
|
|
||||||
|
|
||||||
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db.models import Lookup, Transform, IntegerField
|
from django.db.models import Lookup, Transform, IntegerField
|
||||||
from django.db.models.lookups import BuiltinLookup
|
from django.db.models.lookups import BuiltinLookup
|
||||||
|
|
||||||
|
133
netbox/ipam/migrations/0016_unicode_literals.py
Normal file
133
netbox/ipam/migrations/0016_unicode_literals.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import ipam.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0015_global_vlans'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='aggregate',
|
||||||
|
name='family',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='aggregate',
|
||||||
|
name='rir',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='aggregates', to='ipam.RIR', verbose_name='RIR'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='address',
|
||||||
|
field=ipam.fields.IPAddressField(help_text='IPv4 or IPv6 address (with mask)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='family',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='nat_inside',
|
||||||
|
field=models.OneToOneField(blank=True, help_text='The IP for which this address is the "outside" IP', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name='NAT (Inside)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='status',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='vrf',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='ipam.VRF', verbose_name='VRF'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='family',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='is_pool',
|
||||||
|
field=models.BooleanField(default=False, help_text='All IP addresses within this prefix are considered usable', verbose_name='Is a pool'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='prefix',
|
||||||
|
field=ipam.fields.IPNetworkField(help_text='IPv4 or IPv6 network with mask'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='role',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='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, 'Container'), (1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, help_text='Operational status of this prefix', verbose_name='Status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='vlan',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VLAN', verbose_name='VLAN'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='vrf',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VRF', verbose_name='VRF'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rir',
|
||||||
|
name='is_private',
|
||||||
|
field=models.BooleanField(default=False, help_text='IP space managed by this RIR is considered private', verbose_name='Private'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='service',
|
||||||
|
name='device',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='service',
|
||||||
|
name='ipaddresses',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='services', to='ipam.IPAddress', verbose_name='IP addresses'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='service',
|
||||||
|
name='port',
|
||||||
|
field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)], verbose_name='Port number'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='service',
|
||||||
|
name='protocol',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(6, 'TCP'), (17, 'UDP')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='status',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='vid',
|
||||||
|
field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)], verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vrf',
|
||||||
|
name='enforce_unique',
|
||||||
|
field=models.BooleanField(default=True, help_text='Prevent duplicate prefixes/IP addresses within this VRF', verbose_name='Enforce unique space'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vrf',
|
||||||
|
name='rd',
|
||||||
|
field=models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from netaddr import IPNetwork, cidr_merge
|
from netaddr import IPNetwork, cidr_merge
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -15,7 +17,6 @@ from tenancy.models import Tenant
|
|||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
from utilities.sql import NullsFirstQuerySet
|
from utilities.sql import NullsFirstQuerySet
|
||||||
from utilities.utils import csv_format
|
from utilities.utils import csv_format
|
||||||
|
|
||||||
from .fields import IPNetworkField, IPAddressField
|
from .fields import IPNetworkField, IPAddressField
|
||||||
|
|
||||||
|
|
||||||
@ -499,7 +500,7 @@ class VLANGroup(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.site is None:
|
if self.site is None:
|
||||||
return self.name
|
return self.name
|
||||||
return u'{} - {}'.format(self.site.name, self.name)
|
return '{} - {}'.format(self.site.name, self.name)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||||
@ -566,7 +567,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
@property
|
@property
|
||||||
def display_name(self):
|
def display_name(self):
|
||||||
if self.vid and self.name:
|
if self.vid and self.name:
|
||||||
return u"{} ({})".format(self.vid, self.name)
|
return "{} ({})".format(self.vid, self.name)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_status_class(self):
|
def get_status_class(self):
|
||||||
@ -593,4 +594,4 @@ class Service(CreatedUpdatedModel):
|
|||||||
unique_together = ['device', 'protocol', 'port']
|
unique_together = ['device', 'protocol', 'port']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
|
return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||||
|
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from netaddr import IPNetwork
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from netaddr import IPNetwork
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from ipam.models import IPAddress, Prefix, VRF
|
from ipam.models import IPAddress, Prefix, VRF
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
|
|
||||||
class TestPrefix(TestCase):
|
class TestPrefix(TestCase):
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
@ -12,7 +14,7 @@ urlpatterns = [
|
|||||||
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
|
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
|
||||||
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
|
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
|
||||||
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
|
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
|
||||||
url(r'^vrfs/(?P<pk>\d+)/$', views.vrf, name='vrf'),
|
url(r'^vrfs/(?P<pk>\d+)/$', views.VRFView.as_view(), name='vrf'),
|
||||||
url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'),
|
url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'),
|
||||||
url(r'^vrfs/(?P<pk>\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'),
|
url(r'^vrfs/(?P<pk>\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'),
|
||||||
|
|
||||||
@ -28,7 +30,7 @@ urlpatterns = [
|
|||||||
url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
|
url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
|
||||||
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
|
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
|
||||||
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
|
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
|
||||||
url(r'^aggregates/(?P<pk>\d+)/$', views.aggregate, name='aggregate'),
|
url(r'^aggregates/(?P<pk>\d+)/$', views.AggregateView.as_view(), name='aggregate'),
|
||||||
url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'),
|
url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'),
|
||||||
url(r'^aggregates/(?P<pk>\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
|
url(r'^aggregates/(?P<pk>\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
|
||||||
|
|
||||||
@ -44,10 +46,10 @@ urlpatterns = [
|
|||||||
url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
|
url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
|
||||||
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
|
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
|
||||||
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
|
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
|
||||||
url(r'^prefixes/(?P<pk>\d+)/$', views.prefix, name='prefix'),
|
url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
|
||||||
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
|
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
|
||||||
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
|
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
|
||||||
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.prefix_ipaddresses, name='prefix_ipaddresses'),
|
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
|
||||||
|
|
||||||
# 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'),
|
||||||
@ -56,7 +58,7 @@ urlpatterns = [
|
|||||||
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'),
|
||||||
url(r'^ip-addresses/(?P<pk>\d+)/$', views.ipaddress, name='ipaddress'),
|
url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
|
||||||
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
||||||
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
||||||
|
|
||||||
@ -72,7 +74,7 @@ urlpatterns = [
|
|||||||
url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
|
url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
|
||||||
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
||||||
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||||
url(r'^vlans/(?P<pk>\d+)/$', views.vlan, name='vlan'),
|
url(r'^vlans/(?P<pk>\d+)/$', views.VLANView.as_view(), name='vlan'),
|
||||||
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'),
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
@ -6,13 +8,13 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
|
|||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.views.generic import View
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkAddView, 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 (
|
from .models import (
|
||||||
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
|
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
|
||||||
@ -96,7 +98,9 @@ class VRFListView(ObjectListView):
|
|||||||
template_name = 'ipam/vrf_list.html'
|
template_name = 'ipam/vrf_list.html'
|
||||||
|
|
||||||
|
|
||||||
def vrf(request, pk):
|
class VRFView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
||||||
prefix_table = tables.PrefixBriefTable(
|
prefix_table = tables.PrefixBriefTable(
|
||||||
@ -281,13 +285,20 @@ class AggregateListView(ObjectListView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def aggregate(request, pk):
|
class AggregateView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
aggregate = get_object_or_404(Aggregate, pk=pk)
|
aggregate = get_object_or_404(Aggregate, pk=pk)
|
||||||
|
|
||||||
# Find all child prefixes contained by this aggregate
|
# Find all child prefixes contained by this aggregate
|
||||||
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))\
|
child_prefixes = Prefix.objects.filter(
|
||||||
.select_related('site', 'role').annotate_depth(limit=0)
|
prefix__net_contained_or_equal=str(aggregate.prefix)
|
||||||
|
).select_related(
|
||||||
|
'site', 'role'
|
||||||
|
).annotate_depth(
|
||||||
|
limit=0
|
||||||
|
)
|
||||||
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)
|
||||||
@ -394,7 +405,9 @@ class PrefixListView(ObjectListView):
|
|||||||
return self.queryset.annotate_depth(limit=limit)
|
return self.queryset.annotate_depth(limit=limit)
|
||||||
|
|
||||||
|
|
||||||
def prefix(request, pk):
|
class PrefixView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
prefix = get_object_or_404(Prefix.objects.select_related(
|
prefix = get_object_or_404(Prefix.objects.select_related(
|
||||||
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
|
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
|
||||||
@ -406,25 +419,38 @@ def prefix(request, pk):
|
|||||||
aggregate = None
|
aggregate = None
|
||||||
|
|
||||||
# Count child IP addresses
|
# Count child IP addresses
|
||||||
ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\
|
ipaddress_count = IPAddress.objects.filter(
|
||||||
.count()
|
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
|
||||||
|
).count()
|
||||||
|
|
||||||
# Parent prefixes table
|
# Parent prefixes table
|
||||||
parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\
|
parent_prefixes = Prefix.objects.filter(
|
||||||
.filter(prefix__net_contains=str(prefix.prefix))\
|
Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
|
||||||
.select_related('site', 'role').annotate_depth()
|
).filter(
|
||||||
|
prefix__net_contains=str(prefix.prefix)
|
||||||
|
).select_related(
|
||||||
|
'site', 'role'
|
||||||
|
).annotate_depth()
|
||||||
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
|
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
|
||||||
parent_prefix_table.exclude = ('vrf',)
|
parent_prefix_table.exclude = ('vrf',)
|
||||||
|
|
||||||
# Duplicate prefixes table
|
# Duplicate prefixes table
|
||||||
duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\
|
duplicate_prefixes = Prefix.objects.filter(
|
||||||
.select_related('site', 'role')
|
vrf=prefix.vrf, prefix=str(prefix.prefix)
|
||||||
|
).exclude(
|
||||||
|
pk=prefix.pk
|
||||||
|
).select_related(
|
||||||
|
'site', 'role'
|
||||||
|
)
|
||||||
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
|
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
|
||||||
duplicate_prefix_table.exclude = ('vrf',)
|
duplicate_prefix_table.exclude = ('vrf',)
|
||||||
|
|
||||||
# Child prefixes table
|
# Child prefixes table
|
||||||
child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\
|
child_prefixes = Prefix.objects.filter(
|
||||||
.select_related('site', 'role').annotate_depth(limit=0)
|
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
|
||||||
|
).select_related(
|
||||||
|
'site', 'role'
|
||||||
|
).annotate_depth(limit=0)
|
||||||
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)
|
||||||
@ -456,6 +482,45 @@ def prefix(request, pk):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class PrefixIPAddressesView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
|
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
|
||||||
|
|
||||||
|
# Find all IPAddresses belonging to this Prefix
|
||||||
|
ipaddresses = IPAddress.objects.filter(
|
||||||
|
vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
|
||||||
|
).select_related(
|
||||||
|
'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
|
||||||
|
)
|
||||||
|
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
|
||||||
|
|
||||||
|
ip_table = tables.IPAddressTable(ipaddresses)
|
||||||
|
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
||||||
|
ip_table.base_columns['pk'].visible = True
|
||||||
|
|
||||||
|
paginate = {
|
||||||
|
'klass': EnhancedPaginator,
|
||||||
|
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||||
|
}
|
||||||
|
RequestConfig(request, paginate).configure(ip_table)
|
||||||
|
|
||||||
|
# Compile permissions list for rendering the object table
|
||||||
|
permissions = {
|
||||||
|
'add': request.user.has_perm('ipam.add_ipaddress'),
|
||||||
|
'change': request.user.has_perm('ipam.change_ipaddress'),
|
||||||
|
'delete': request.user.has_perm('ipam.delete_ipaddress'),
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'ipam/prefix_ipaddresses.html', {
|
||||||
|
'prefix': prefix,
|
||||||
|
'ip_table': ip_table,
|
||||||
|
'permissions': permissions,
|
||||||
|
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
|
class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.change_prefix'
|
permission_required = 'ipam.change_prefix'
|
||||||
model = Prefix
|
model = Prefix
|
||||||
@ -495,40 +560,6 @@ class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
default_return_url = 'ipam:prefix_list'
|
default_return_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
def prefix_ipaddresses(request, pk):
|
|
||||||
|
|
||||||
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
|
|
||||||
|
|
||||||
# Find all IPAddresses belonging to this Prefix
|
|
||||||
ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\
|
|
||||||
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
|
||||||
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
|
|
||||||
|
|
||||||
ip_table = tables.IPAddressTable(ipaddresses)
|
|
||||||
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
|
||||||
ip_table.base_columns['pk'].visible = True
|
|
||||||
|
|
||||||
paginate = {
|
|
||||||
'klass': EnhancedPaginator,
|
|
||||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
|
||||||
}
|
|
||||||
RequestConfig(request, paginate).configure(ip_table)
|
|
||||||
|
|
||||||
# Compile permissions list for rendering the object table
|
|
||||||
permissions = {
|
|
||||||
'add': request.user.has_perm('ipam.add_ipaddress'),
|
|
||||||
'change': request.user.has_perm('ipam.change_ipaddress'),
|
|
||||||
'delete': request.user.has_perm('ipam.delete_ipaddress'),
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, 'ipam/prefix_ipaddresses.html', {
|
|
||||||
'prefix': prefix,
|
|
||||||
'ip_table': ip_table,
|
|
||||||
'permissions': permissions,
|
|
||||||
'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# IP addresses
|
# IP addresses
|
||||||
#
|
#
|
||||||
@ -541,24 +572,39 @@ class IPAddressListView(ObjectListView):
|
|||||||
template_name = 'ipam/ipaddress_list.html'
|
template_name = 'ipam/ipaddress_list.html'
|
||||||
|
|
||||||
|
|
||||||
def ipaddress(request, pk):
|
class IPAddressView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
|
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
|
||||||
|
|
||||||
# Parent prefixes table
|
# Parent prefixes table
|
||||||
parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))\
|
parent_prefixes = Prefix.objects.filter(
|
||||||
.select_related('site', 'role')
|
vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip)
|
||||||
|
).select_related(
|
||||||
|
'site', 'role'
|
||||||
|
)
|
||||||
parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
|
parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
|
||||||
parent_prefixes_table.exclude = ('vrf',)
|
parent_prefixes_table.exclude = ('vrf',)
|
||||||
|
|
||||||
# Duplicate IPs table
|
# Duplicate IPs table
|
||||||
duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\
|
duplicate_ips = IPAddress.objects.filter(
|
||||||
.exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside')
|
vrf=ipaddress.vrf, address=str(ipaddress.address)
|
||||||
|
).exclude(
|
||||||
|
pk=ipaddress.pk
|
||||||
|
).select_related(
|
||||||
|
'interface__device', 'nat_inside'
|
||||||
|
)
|
||||||
duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
|
duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
|
||||||
|
|
||||||
# Related IP table
|
# Related IP table
|
||||||
related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\
|
related_ips = IPAddress.objects.select_related(
|
||||||
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
|
'interface__device'
|
||||||
|
).exclude(
|
||||||
|
address=str(ipaddress.address)
|
||||||
|
).filter(
|
||||||
|
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
|
||||||
|
)
|
||||||
related_ips_table = tables.IPAddressBriefTable(list(related_ips))
|
related_ips_table = tables.IPAddressBriefTable(list(related_ips))
|
||||||
|
|
||||||
return render(request, 'ipam/ipaddress.html', {
|
return render(request, 'ipam/ipaddress.html', {
|
||||||
@ -669,9 +715,13 @@ class VLANListView(ObjectListView):
|
|||||||
template_name = 'ipam/vlan_list.html'
|
template_name = 'ipam/vlan_list.html'
|
||||||
|
|
||||||
|
|
||||||
def vlan(request, pk):
|
class VLANView(View):
|
||||||
|
|
||||||
vlan = get_object_or_404(VLAN.objects.select_related('site__region', 'tenant__group', 'role'), pk=pk)
|
def get(self, request, pk):
|
||||||
|
|
||||||
|
vlan = get_object_or_404(VLAN.objects.select_related(
|
||||||
|
'site__region', 'tenant__group', 'role'
|
||||||
|
), pk=pk)
|
||||||
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
|
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
|
||||||
prefix_table = tables.PrefixBriefTable(list(prefixes))
|
prefix_table = tables.PrefixBriefTable(list(prefixes))
|
||||||
prefix_table.exclude = ('vlan',)
|
prefix_table.exclude = ('vlan',)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from utilities.forms import BootstrapMixin
|
from utilities.forms import BootstrapMixin
|
||||||
|
@ -13,7 +13,7 @@ except ImportError:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
VERSION = '2.0.3'
|
VERSION = '2.0.4'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
|
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
|
||||||
@ -112,6 +112,7 @@ INSTALLED_APPS = (
|
|||||||
'django.contrib.humanize',
|
'django.contrib.humanize',
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
'debug_toolbar',
|
'debug_toolbar',
|
||||||
|
'django_filters',
|
||||||
'django_tables2',
|
'django_tables2',
|
||||||
'mptt',
|
'mptt',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
@ -180,8 +181,8 @@ STATICFILES_DIRS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Media
|
# Media
|
||||||
MEDIA_URL = '/media/'
|
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
MEDIA_URL = '/{}media/'.format(BASE_PATH)
|
||||||
|
|
||||||
# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
|
# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
|
||||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = None
|
DATA_UPLOAD_MAX_NUMBER_FIELDS = None
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework_swagger.views import get_swagger_view
|
from rest_framework_swagger.views import get_swagger_view
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -5,8 +7,8 @@ from django.conf.urls import include, url
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.views.static import serve
|
from django.views.static import serve
|
||||||
|
|
||||||
from netbox.views import APIRootView, home, handle_500, SearchView, trigger_500
|
from netbox.views import APIRootView, handle_500, HomeView, SearchView, trigger_500
|
||||||
from users.views import login, logout
|
from users.views import LoginView, LogoutView
|
||||||
|
|
||||||
|
|
||||||
handler500 = handle_500
|
handler500 = handle_500
|
||||||
@ -15,12 +17,12 @@ swagger_view = get_swagger_view(title='NetBox API')
|
|||||||
_patterns = [
|
_patterns = [
|
||||||
|
|
||||||
# Base views
|
# Base views
|
||||||
url(r'^$', home, name='home'),
|
url(r'^$', HomeView.as_view(), name='home'),
|
||||||
url(r'^search/$', SearchView.as_view(), name='search'),
|
url(r'^search/$', SearchView.as_view(), name='search'),
|
||||||
|
|
||||||
# Login/logout
|
# Login/logout
|
||||||
url(r'^login/$', login, name='login'),
|
url(r'^login/$', LoginView.as_view(), name='login'),
|
||||||
url(r'^logout/$', logout, name='logout'),
|
url(r'^logout/$', LogoutView.as_view(), name='logout'),
|
||||||
|
|
||||||
# Apps
|
# Apps
|
||||||
url(r'^circuits/', include('circuits.urls')),
|
url(r'^circuits/', include('circuits.urls')),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@ -115,7 +116,10 @@ SEARCH_TYPES = OrderedDict((
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
def home(request):
|
class HomeView(View):
|
||||||
|
template_name = 'home.html'
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
|
|
||||||
@ -146,7 +150,7 @@ def home(request):
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'home.html', {
|
return render(request, self.template_name, {
|
||||||
'search_form': SearchForm(),
|
'search_form': SearchForm(),
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
'topology_maps': TopologyMap.objects.filter(site__isnull=True),
|
'topology_maps': TopologyMap.objects.filter(site__isnull=True),
|
||||||
@ -192,7 +196,7 @@ class SearchView(View):
|
|||||||
results.append({
|
results.append({
|
||||||
'name': queryset.model._meta.verbose_name_plural,
|
'name': queryset.model._meta.verbose_name_plural,
|
||||||
'table': table,
|
'table': table,
|
||||||
'url': u'{}?q={}'.format(reverse(url), form.cleaned_data['q'])
|
'url': '{}?q={}'.format(reverse(url), form.cleaned_data['q'])
|
||||||
})
|
})
|
||||||
|
|
||||||
return render(request, 'search.html', {
|
return render(request, 'search.html', {
|
||||||
@ -206,7 +210,7 @@ class APIRootView(APIView):
|
|||||||
exclude_from_schema = True
|
exclude_from_schema = True
|
||||||
|
|
||||||
def get_view_name(self):
|
def get_view_name(self):
|
||||||
return u"API Root"
|
return "API Root"
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
|
|
||||||
@ -235,5 +239,6 @@ def trigger_500(request):
|
|||||||
"""
|
"""
|
||||||
Hot-wired method of triggering a server error to test reporting
|
Hot-wired method of triggering a server error to test reporting
|
||||||
"""
|
"""
|
||||||
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
|
raise Exception(
|
||||||
"person you are.")
|
"Congratulations, you've triggered an exception! Go tell all your friends what an exceptional person you are."
|
||||||
|
)
|
||||||
|
@ -11,6 +11,7 @@ import os
|
|||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
|
||||||
@ -34,8 +36,8 @@ class UserKeyAdmin(admin.ModelAdmin):
|
|||||||
try:
|
try:
|
||||||
my_userkey = UserKey.objects.get(user=request.user)
|
my_userkey = UserKey.objects.get(user=request.user)
|
||||||
except UserKey.DoesNotExist:
|
except UserKey.DoesNotExist:
|
||||||
messages.error(request, u"You do not have an active User Key.")
|
messages.error(request, "You do not have an active User Key.")
|
||||||
return redirect('/admin/secrets/userkey/')
|
return redirect('admin:secrets_userkey_changelist')
|
||||||
|
|
||||||
if 'activate' in request.POST:
|
if 'activate' in request.POST:
|
||||||
form = ActivateUserKeyForm(request.POST)
|
form = ActivateUserKeyForm(request.POST)
|
||||||
@ -44,9 +46,9 @@ class UserKeyAdmin(admin.ModelAdmin):
|
|||||||
master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
|
master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
|
||||||
for uk in form.cleaned_data['_selected_action']:
|
for uk in form.cleaned_data['_selected_action']:
|
||||||
uk.activate(master_key)
|
uk.activate(master_key)
|
||||||
return redirect('/admin/secrets/userkey/')
|
return redirect('admin:secrets_userkey_changelist')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
messages.error(request, u"Invalid private key provided. Unable to retrieve master key.")
|
messages.error(request, "Invalid private key provided. Unable to retrieve master key.")
|
||||||
else:
|
else:
|
||||||
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
|
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
from django.http import HttpResponseBadRequest
|
|
||||||
|
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet, ViewSet
|
from rest_framework.viewsets import ModelViewSet, ViewSet
|
||||||
|
|
||||||
|
from django.http import HttpResponseBadRequest
|
||||||
|
|
||||||
from secrets import filters
|
from secrets import filters
|
||||||
from secrets.exceptions import InvalidKey
|
from secrets.exceptions import InvalidKey
|
||||||
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
@ -14,10 +16,10 @@ def userkey_required():
|
|||||||
try:
|
try:
|
||||||
uk = UserKey.objects.get(user=request.user)
|
uk = UserKey.objects.get(user=request.user)
|
||||||
except UserKey.DoesNotExist:
|
except UserKey.DoesNotExist:
|
||||||
messages.warning(request, u"This operation requires an active user key, but you don't have one.")
|
messages.warning(request, "This operation requires an active user key, but you don't have one.")
|
||||||
return redirect('user:userkey')
|
return redirect('user:userkey')
|
||||||
if not uk.is_active():
|
if not uk.is_active():
|
||||||
messages.warning(request, u"This operation is not available. Your user key has not been activated.")
|
messages.warning(request, "This operation is not available. Your user key has not been activated.")
|
||||||
return redirect('user:userkey')
|
return redirect('user:userkey')
|
||||||
return view(request, *args, **kwargs)
|
return view(request, *args, **kwargs)
|
||||||
return wrapped_view
|
return wrapped_view
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
class InvalidKey(Exception):
|
class InvalidKey(Exception):
|
||||||
"""
|
"""
|
||||||
Raised when a provided key is invalid.
|
Raised when a provided key is invalid.
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from Crypto.Cipher import PKCS1_OAEP
|
from Crypto.Cipher import PKCS1_OAEP
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
@ -6,7 +8,6 @@ from django.db.models import Count
|
|||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
|
from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
|
||||||
|
|
||||||
from .models import Secret, SecretRole, UserKey
|
from .models import Secret, SecretRole, UserKey
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
||||||
|
|
||||||
|
|
||||||
|
20
netbox/secrets/migrations/0003_unicode_literals.py
Normal file
20
netbox/secrets/migrations/0003_unicode_literals.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11 on 2017-05-24 15:34
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('secrets', '0002_userkey_add_session_key'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userkey',
|
||||||
|
name='public_key',
|
||||||
|
field=models.TextField(verbose_name='RSA public key'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,4 +1,6 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from Crypto.Cipher import AES, PKCS1_OAEP, XOR
|
from Crypto.Cipher import AES, PKCS1_OAEP, XOR
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
@ -12,7 +14,6 @@ from django.utils.encoding import force_bytes, python_2_unicode_compatible
|
|||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
|
|
||||||
from .exceptions import InvalidKey
|
from .exceptions import InvalidKey
|
||||||
from .hashers import SecretValidationHasher
|
from .hashers import SecretValidationHasher
|
||||||
|
|
||||||
@ -301,8 +302,8 @@ class Secret(CreatedUpdatedModel):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.role and self.device:
|
if self.role and self.device:
|
||||||
return u'{} for {}'.format(self.role, self.device)
|
return '{} for {}'.format(self.role, self.device)
|
||||||
return u'Secret'
|
return 'Secret'
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('secrets:secret', args=[self.pk])
|
return reverse('secrets:secret', args=[self.pk])
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
|
||||||
|
|
||||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||||
|
|
||||||
@ -22,8 +23,9 @@ class SecretRoleTable(BaseTable):
|
|||||||
name = tables.LinkColumn(verbose_name='Name')
|
name = tables.LinkColumn(verbose_name='Name')
|
||||||
secret_count = tables.Column(verbose_name='Secrets')
|
secret_count = tables.Column(verbose_name='Secrets')
|
||||||
slug = tables.Column(verbose_name='Slug')
|
slug = tables.Column(verbose_name='Slug')
|
||||||
actions = tables.TemplateColumn(template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
actions = tables.TemplateColumn(
|
||||||
verbose_name='')
|
template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = SecretRole
|
model = SecretRole
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
@ -17,7 +19,7 @@ urlpatterns = [
|
|||||||
url(r'^secrets/import/$', views.secret_import, name='secret_import'),
|
url(r'^secrets/import/$', views.secret_import, name='secret_import'),
|
||||||
url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
|
url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
|
||||||
url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
|
url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
|
||||||
url(r'^secrets/(?P<pk>\d+)/$', views.secret, name='secret'),
|
url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
|
||||||
url(r'^secrets/(?P<pk>\d+)/edit/$', views.secret_edit, name='secret_edit'),
|
url(r'^secrets/(?P<pk>\d+)/edit/$', views.secret_edit, name='secret_edit'),
|
||||||
url(r'^secrets/(?P<pk>\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'),
|
url(r'^secrets/(?P<pk>\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'),
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@ -8,10 +9,10 @@ from django.db.models import Count
|
|||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.generic import View
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
|
from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||||
|
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .decorators import userkey_required
|
from .decorators import userkey_required
|
||||||
from .models import SecretRole, Secret, SessionKey
|
from .models import SecretRole, Secret, SessionKey
|
||||||
@ -65,8 +66,10 @@ class SecretListView(ObjectListView):
|
|||||||
template_name = 'secrets/secret_list.html'
|
template_name = 'secrets/secret_list.html'
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@method_decorator(login_required, name='dispatch')
|
||||||
def secret(request, pk):
|
class SecretView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
secret = get_object_or_404(Secret, pk=pk)
|
secret = get_object_or_404(Secret, pk=pk)
|
||||||
|
|
||||||
@ -107,7 +110,7 @@ def secret_add(request, pk):
|
|||||||
secret.plaintext = str(form.cleaned_data['plaintext'])
|
secret.plaintext = str(form.cleaned_data['plaintext'])
|
||||||
secret.encrypt(master_key)
|
secret.encrypt(master_key)
|
||||||
secret.save()
|
secret.save()
|
||||||
messages.success(request, u"Added new secret: {}.".format(secret))
|
messages.success(request, "Added new secret: {}.".format(secret))
|
||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
return redirect('dcim:device_addsecret', pk=device.pk)
|
return redirect('dcim:device_addsecret', pk=device.pk)
|
||||||
else:
|
else:
|
||||||
@ -151,7 +154,7 @@ def secret_edit(request, pk):
|
|||||||
secret.plaintext = str(form.cleaned_data['plaintext'])
|
secret.plaintext = str(form.cleaned_data['plaintext'])
|
||||||
secret.encrypt(master_key)
|
secret.encrypt(master_key)
|
||||||
secret.save()
|
secret.save()
|
||||||
messages.success(request, u"Modified secret {}.".format(secret))
|
messages.success(request, "Modified secret {}.".format(secret))
|
||||||
return redirect('secrets:secret', pk=secret.pk)
|
return redirect('secrets:secret', pk=secret.pk)
|
||||||
else:
|
else:
|
||||||
form.add_error(None, "Invalid session key. Unable to encrypt secret data.")
|
form.add_error(None, "Invalid session key. Unable to encrypt secret data.")
|
||||||
@ -163,7 +166,7 @@ def secret_edit(request, pk):
|
|||||||
# If no new plaintext was specified, a session key is not needed.
|
# If no new plaintext was specified, a session key is not needed.
|
||||||
else:
|
else:
|
||||||
secret = form.save()
|
secret = form.save()
|
||||||
messages.success(request, u"Modified secret {}.".format(secret))
|
messages.success(request, "Modified secret {}.".format(secret))
|
||||||
return redirect('secrets:secret', pk=secret.pk)
|
return redirect('secrets:secret', pk=secret.pk)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -217,7 +220,7 @@ def secret_import(request):
|
|||||||
new_secrets.append(secret)
|
new_secrets.append(secret)
|
||||||
|
|
||||||
table = tables.SecretTable(new_secrets)
|
table = tables.SecretTable(new_secrets)
|
||||||
messages.success(request, u"Imported {} new secrets.".format(len(new_secrets)))
|
messages.success(request, "Imported {} new secrets.".format(len(new_secrets)))
|
||||||
|
|
||||||
return render(request, 'import_success.html', {
|
return render(request, 'import_success.html', {
|
||||||
'table': table,
|
'table': table,
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
|
{% load static from staticfiles %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Server Error</title>
|
<title>Server Error</title>
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
<link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
|
<link rel="stylesheet" href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4 col-md-offset-4">
|
<div class="col-md-4 col-md-offset-4">
|
||||||
<div class="panel panel-danger" style="margin-top: 200px">
|
<div class="panel panel-danger" style="margin-top: 200px">
|
||||||
@ -23,7 +25,7 @@
|
|||||||
<p>If you are responsible for this installation, please consider
|
<p>If you are responsible for this installation, please consider
|
||||||
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>. Additional
|
<a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>. Additional
|
||||||
information is provided below:</p>
|
information is provided below:</p>
|
||||||
<pre><strong>{{ exception }}</strong><br />
|
<pre><strong>{{ exception }}</strong><br />
|
||||||
{{ error }}</pre>
|
{{ error }}</pre>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>
|
<a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>
|
||||||
@ -32,6 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -45,23 +45,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% render_field form.site %}
|
{% 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.rack %}
|
||||||
{% render_field form.device %}
|
{% render_field form.device %}
|
||||||
</div>
|
|
||||||
<div class="tab-pane" id="search">
|
|
||||||
{% render_field form.livesearch %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% render_field form.interface %}
|
{% render_field form.interface %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,12 +32,7 @@
|
|||||||
{% render_field form.livesearch %}
|
{% render_field form.livesearch %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="select">
|
<div class="tab-pane" id="select">
|
||||||
<div class="form-group">
|
{% render_field form.site %}
|
||||||
<label class="col-md-3 control-label">Site</label>
|
|
||||||
<div class="col-md-9">
|
|
||||||
<p class="form-control-static">{{ consoleport.device.site }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
{% render_field form.console_server %}
|
{% render_field form.console_server %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,12 +32,7 @@
|
|||||||
{% render_field form.livesearch %}
|
{% render_field form.livesearch %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="select">
|
<div class="tab-pane" id="select">
|
||||||
<div class="form-group">
|
{% render_field form.site %}
|
||||||
<label class="col-md-3 control-label">Site</label>
|
|
||||||
<div class="col-md-9">
|
|
||||||
<p class="form-control-static">{{ consoleserverport.device.site }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
{% render_field form.device %}
|
{% render_field form.device %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,6 +69,11 @@
|
|||||||
<td>Unique alphanumeric tag (optional)</td>
|
<td>Unique alphanumeric tag (optional)</td>
|
||||||
<td>ABC123456</td>
|
<td>ABC123456</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td>Current status</td>
|
||||||
|
<td>Active</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Parent device</td>
|
<td>Parent device</td>
|
||||||
<td>Parent device</td>
|
<td>Parent device</td>
|
||||||
@ -82,7 +87,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h4>Example</h4>
|
<h4>Example</h4>
|
||||||
<pre>Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Server101,Slot4</pre>
|
<pre>Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Active,Server101,Slot4</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -32,12 +32,7 @@
|
|||||||
{% render_field form.livesearch %}
|
{% render_field form.livesearch %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="select">
|
<div class="tab-pane" id="select">
|
||||||
<div class="form-group">
|
{% render_field form.site %}
|
||||||
<label class="col-md-3 control-label">Site</label>
|
|
||||||
<div class="col-md-9">
|
|
||||||
<p class="form-control-static">{{ poweroutlet.device.site }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
{% render_field form.device %}
|
{% render_field form.device %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,12 +32,7 @@
|
|||||||
{% render_field form.livesearch %}
|
{% render_field form.livesearch %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="select">
|
<div class="tab-pane" id="select">
|
||||||
<div class="form-group">
|
{% render_field form.site %}
|
||||||
<label class="col-md-3 control-label">Site</label>
|
|
||||||
<div class="col-md-9">
|
|
||||||
<p class="form-control-static">{{ powerport.device.site }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% render_field form.rack %}
|
{% render_field form.rack %}
|
||||||
{% render_field form.pdu %}
|
{% render_field form.pdu %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
{% for column in table.columns %}
|
{% for column in table.columns %}
|
||||||
{% if column.orderable %}
|
{% if column.orderable %}
|
||||||
<th {{ column.attrs.th.as_html }}><a href="{% querystring page=column.order_by_alias.next %}">{{ column.header }}</a></th>
|
<th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
|
||||||
{% else %}
|
{% else %}
|
||||||
<th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
|
<th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-pane active" id="select">
|
<div class="tab-pane active" id="select">
|
||||||
{% render_field form.nat_site %}
|
{% render_field form.nat_site %}
|
||||||
|
{% render_field form.nat_rack %}
|
||||||
{% render_field form.nat_device %}
|
{% render_field form.nat_device %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="search">
|
<div class="tab-pane" id="search">
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from tenancy.filters import TenantFilter
|
from tenancy.filters import TenantFilter
|
||||||
|
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
|
||||||
from utilities.api import WritableSerializerMixin
|
from utilities.api import WritableSerializerMixin
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
@ -79,7 +81,9 @@ class TenancyForm(ChainedFieldsMixin, forms.Form):
|
|||||||
)
|
)
|
||||||
tenant = ChainedModelChoiceField(
|
tenant = ChainedModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
chains={'group': 'tenant_group'},
|
chains=(
|
||||||
|
('group', 'tenant_group'),
|
||||||
|
),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/tenancy/tenants/?group_id={{tenant_group}}'
|
api_url='/api/tenancy/tenants/?group_id={{tenant_group}}'
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user