mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Merge branch 'import_headers' into develop
This commit is contained in:
commit
e06221bc89
@ -8,8 +8,8 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
|||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
|
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
|
||||||
FilterChoiceField, Livesearch, SmallTextarea, SlugField,
|
SmallTextarea, SlugField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
@ -39,15 +39,18 @@ class ProviderForm(BootstrapMixin, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProviderFromCSVForm(forms.ModelForm):
|
class ProviderCSVForm(forms.ModelForm):
|
||||||
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url']
|
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'comments']
|
||||||
|
help_texts = {
|
||||||
|
'name': 'Provider name',
|
||||||
class ProviderImportForm(BootstrapMixin, BulkImportForm):
|
'asn': '32-bit autonomous system number',
|
||||||
csv = CSVDataField(csv_form=ProviderFromCSVForm)
|
'portal_url': 'Portal URL',
|
||||||
|
'comments': 'Free-form comments',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@ -102,21 +105,36 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CircuitFromCSVForm(forms.ModelForm):
|
class CircuitCSVForm(forms.ModelForm):
|
||||||
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
provider = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Provider not found.'})
|
queryset=Provider.objects.all(),
|
||||||
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
help_text='Name of parent provider',
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
error_messages={
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
'invalid_choice': 'Provider not found.'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
type = forms.ModelChoiceField(
|
||||||
|
queryset=CircuitType.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Type of circuit',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Invalid circuit type.'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tenant = forms.ModelChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned tenant',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
|
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
|
||||||
|
|
||||||
|
|
||||||
class CircuitImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
@ -65,9 +65,8 @@ class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'circuits.add_provider'
|
permission_required = 'circuits.add_provider'
|
||||||
form = forms.ProviderImportForm
|
model_form = forms.ProviderCSVForm
|
||||||
table = tables.ProviderTable
|
table = tables.ProviderTable
|
||||||
template_name = 'circuits/provider_import.html'
|
|
||||||
default_return_url = 'circuits:provider_list'
|
default_return_url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
@ -163,9 +162,8 @@ class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'circuits.add_circuit'
|
permission_required = 'circuits.add_circuit'
|
||||||
form = forms.CircuitImportForm
|
model_form = forms.CircuitCSVForm
|
||||||
table = tables.CircuitTable
|
table = tables.CircuitTable
|
||||||
template_name = 'circuits/circuit_import.html'
|
|
||||||
default_return_url = 'circuits:circuit_list'
|
default_return_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import re
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
|
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
@ -14,18 +13,18 @@ from tenancy.forms import TenancyForm
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||||
BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField, ExpandableNameField,
|
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVChoiceField, ExpandableNameField, FilterChoiceField,
|
||||||
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
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_CONNECTED, ConsolePort,
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface,
|
||||||
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate,
|
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES,
|
||||||
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES,
|
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
|
||||||
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES,
|
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -50,14 +49,6 @@ def get_device_by_name_or_pk(name):
|
|||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
def validate_connection_status(value):
|
|
||||||
"""
|
|
||||||
Custom validator for connection statuses. value must be either "planned" or "connected" (case-insensitive).
|
|
||||||
"""
|
|
||||||
if value.lower() not in ['planned', 'connected']:
|
|
||||||
raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceComponentForm(BootstrapMixin, forms.Form):
|
class DeviceComponentForm(BootstrapMixin, forms.Form):
|
||||||
"""
|
"""
|
||||||
Allow inclusion of the parent device as context for limiting field choices.
|
Allow inclusion of the parent device as context for limiting field choices.
|
||||||
@ -107,27 +98,37 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SiteFromCSVForm(forms.ModelForm):
|
class SiteCSVForm(forms.ModelForm):
|
||||||
region = forms.ModelChoiceField(
|
region = forms.ModelChoiceField(
|
||||||
Region.objects.all(), to_field_name='name', required=False, error_messages={
|
queryset=Region.objects.all(),
|
||||||
'invalid_choice': 'Tenant not found.'
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned region',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Region not found.',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = forms.ModelChoiceField(
|
||||||
Tenant.objects.all(), to_field_name='name', required=False, error_messages={
|
queryset=Tenant.objects.all(),
|
||||||
'invalid_choice': 'Tenant not found.'
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned tenant',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
|
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||||
|
'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||||
]
|
]
|
||||||
|
help_texts = {
|
||||||
|
'name': 'Site name',
|
||||||
class SiteImportForm(BootstrapMixin, BulkImportForm):
|
'slug': 'URL-friendly slug',
|
||||||
csv = CSVDataField(csv_form=SiteFromCSVForm)
|
'asn': '32-bit autonomous system number',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@ -217,49 +218,73 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RackFromCSVForm(forms.ModelForm):
|
class RackCSVForm(forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
site = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
queryset=Site.objects.all(),
|
||||||
group_name = forms.CharField(required=False)
|
to_field_name='name',
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
help_text='Name of parent site',
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
error_messages={
|
||||||
role = forms.ModelChoiceField(RackRole.objects.all(), to_field_name='name', required=False,
|
'invalid_choice': 'Site not found.',
|
||||||
error_messages={'invalid_choice': 'Role not found.'})
|
}
|
||||||
type = forms.CharField(required=False)
|
)
|
||||||
|
group_name = forms.CharField(
|
||||||
|
help_text='Name of rack group',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
tenant = forms.ModelChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned tenant',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
role = forms.ModelChoiceField(
|
||||||
|
queryset=RackRole.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned role',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Role not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
type = CSVChoiceField(
|
||||||
|
choices=RACK_TYPE_CHOICES,
|
||||||
|
required=False,
|
||||||
|
help_text='Rack type'
|
||||||
|
)
|
||||||
|
width = forms.ChoiceField(
|
||||||
|
choices = (
|
||||||
|
(RACK_WIDTH_19IN, '19'),
|
||||||
|
(RACK_WIDTH_23IN, '23'),
|
||||||
|
),
|
||||||
|
help_text='Rail-to-rail width (in inches)'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height',
|
fields = [
|
||||||
'desc_units']
|
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
|
||||||
|
]
|
||||||
|
help_texts = {
|
||||||
|
'name': 'Rack name',
|
||||||
|
'u_height': 'Height in rack units',
|
||||||
|
}
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
super(RackCSVForm, self).clean()
|
||||||
|
|
||||||
site = self.cleaned_data.get('site')
|
site = self.cleaned_data.get('site')
|
||||||
group = self.cleaned_data.get('group_name')
|
group_name = self.cleaned_data.get('group_name')
|
||||||
|
|
||||||
# Validate rack group
|
# Validate rack group
|
||||||
if site and group:
|
if group_name:
|
||||||
try:
|
try:
|
||||||
self.instance.group = RackGroup.objects.get(site=site, name=group)
|
self.instance.group = RackGroup.objects.get(site=site, name=group_name)
|
||||||
except RackGroup.DoesNotExist:
|
except RackGroup.DoesNotExist:
|
||||||
self.add_error('group_name', "Invalid rack group ({})".format(group))
|
raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site))
|
||||||
|
|
||||||
def clean_type(self):
|
|
||||||
rack_type = self.cleaned_data['type']
|
|
||||||
if not rack_type:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
choices = {v.lower(): k for k, v in RACK_TYPE_CHOICES}
|
|
||||||
return choices[rack_type.lower()]
|
|
||||||
except KeyError:
|
|
||||||
raise forms.ValidationError('Invalid rack type ({}). Valid choices are: {}.'.format(
|
|
||||||
rack_type,
|
|
||||||
', '.join({v: k for k, v in RACK_TYPE_CHOICES}),
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
class RackImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=RackFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@ -663,32 +688,60 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
||||||
|
|
||||||
|
|
||||||
class BaseDeviceFromCSVForm(forms.ModelForm):
|
class BaseDeviceCSVForm(forms.ModelForm):
|
||||||
device_role = forms.ModelChoiceField(
|
device_role = forms.ModelChoiceField(
|
||||||
queryset=DeviceRole.objects.all(), to_field_name='name',
|
queryset=DeviceRole.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Invalid device role.'}
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned role',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Invalid device role.',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = forms.ModelChoiceField(
|
||||||
Tenant.objects.all(), to_field_name='name', required=False,
|
queryset=Tenant.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'}
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned tenant',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
manufacturer = forms.ModelChoiceField(
|
manufacturer = forms.ModelChoiceField(
|
||||||
queryset=Manufacturer.objects.all(), to_field_name='name',
|
queryset=Manufacturer.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Invalid manufacturer.'}
|
to_field_name='name',
|
||||||
|
help_text='Device type manufacturer',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Invalid manufacturer.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
model_name = forms.CharField(
|
||||||
|
help_text='Device type model name'
|
||||||
)
|
)
|
||||||
model_name = forms.CharField()
|
|
||||||
platform = forms.ModelChoiceField(
|
platform = forms.ModelChoiceField(
|
||||||
queryset=Platform.objects.all(), required=False, to_field_name='name',
|
queryset=Platform.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Invalid platform.'}
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned platform',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Invalid platform.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
status = CSVChoiceField(
|
||||||
|
choices=STATUS_CHOICES,
|
||||||
|
help_text='Operational status of device'
|
||||||
)
|
)
|
||||||
status = forms.CharField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = []
|
fields = []
|
||||||
model = Device
|
model = Device
|
||||||
|
help_texts = {
|
||||||
|
'name': 'Device name',
|
||||||
|
}
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
super(BaseDeviceCSVForm, self).clean()
|
||||||
|
|
||||||
manufacturer = self.cleaned_data.get('manufacturer')
|
manufacturer = self.cleaned_data.get('manufacturer')
|
||||||
model_name = self.cleaned_data.get('model_name')
|
model_name = self.cleaned_data.get('model_name')
|
||||||
|
|
||||||
@ -697,70 +750,73 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
|
|||||||
try:
|
try:
|
||||||
self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
|
self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
|
||||||
except DeviceType.DoesNotExist:
|
except DeviceType.DoesNotExist:
|
||||||
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
|
raise forms.ValidationError("Device type {} {} not found".format(manufacturer, model_name))
|
||||||
|
|
||||||
def clean_status(self):
|
|
||||||
status_choices = {s[1].lower(): s[0] for s in STATUS_CHOICES}
|
|
||||||
try:
|
|
||||||
return status_choices[self.cleaned_data['status'].lower()]
|
|
||||||
except KeyError:
|
|
||||||
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceFromCSVForm(BaseDeviceFromCSVForm):
|
class DeviceCSVForm(BaseDeviceCSVForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(), to_field_name='name', error_messages={
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of parent site',
|
||||||
|
error_messages={
|
||||||
'invalid_choice': 'Invalid site name.',
|
'invalid_choice': 'Invalid site name.',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
rack_name = forms.CharField(required=False)
|
rack_group = forms.CharField(
|
||||||
face = forms.CharField(required=False)
|
required=False,
|
||||||
|
help_text='Parent rack\'s group (if any)'
|
||||||
|
)
|
||||||
|
rack_name = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
help_text='Name of parent rack'
|
||||||
|
)
|
||||||
|
face = CSVChoiceField(
|
||||||
|
choices=RACK_FACE_CHOICES,
|
||||||
|
required=False,
|
||||||
|
help_text='Mounted rack face'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
class Meta(BaseDeviceCSVForm.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
'site', 'rack_name', 'position', 'face',
|
'site', 'rack_group', 'rack_name', 'position', 'face',
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
super(DeviceFromCSVForm, self).clean()
|
super(DeviceCSVForm, self).clean()
|
||||||
|
|
||||||
site = self.cleaned_data.get('site')
|
site = self.cleaned_data.get('site')
|
||||||
|
rack_group = self.cleaned_data.get('rack_group')
|
||||||
rack_name = self.cleaned_data.get('rack_name')
|
rack_name = self.cleaned_data.get('rack_name')
|
||||||
|
|
||||||
# Validate rack
|
# Validate rack
|
||||||
if site and rack_name:
|
if site and rack_group and rack_name:
|
||||||
try:
|
try:
|
||||||
self.instance.rack = Rack.objects.get(site=site, name=rack_name)
|
self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
|
||||||
except Rack.DoesNotExist:
|
except Rack.DoesNotExist:
|
||||||
self.add_error('rack_name', "Invalid rack ({})".format(rack_name))
|
raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group))
|
||||||
|
elif site and rack_name:
|
||||||
def clean_face(self):
|
try:
|
||||||
face = self.cleaned_data['face']
|
self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name)
|
||||||
if not face:
|
except Rack.DoesNotExist:
|
||||||
return None
|
raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site))
|
||||||
try:
|
|
||||||
return {
|
|
||||||
'front': 0,
|
|
||||||
'rear': 1,
|
|
||||||
}[face.lower()]
|
|
||||||
except KeyError:
|
|
||||||
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
|
|
||||||
|
|
||||||
|
|
||||||
class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||||
parent = FlexibleModelChoiceField(
|
parent = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
required=False,
|
help_text='Name or ID of parent device',
|
||||||
error_messages={
|
error_messages={
|
||||||
'invalid_choice': 'Parent device not found.'
|
'invalid_choice': 'Parent device not found.',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
device_bay_name = forms.CharField(required=False)
|
device_bay_name = forms.CharField(
|
||||||
|
help_text='Name of device bay',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
class Meta(BaseDeviceCSVForm.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
'parent', 'device_bay_name',
|
'parent', 'device_bay_name',
|
||||||
@ -768,7 +824,7 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
super(ChildDeviceFromCSVForm, self).clean()
|
super(ChildDeviceCSVForm, self).clean()
|
||||||
|
|
||||||
parent = self.cleaned_data.get('parent')
|
parent = self.cleaned_data.get('parent')
|
||||||
device_bay_name = self.cleaned_data.get('device_bay_name')
|
device_bay_name = self.cleaned_data.get('device_bay_name')
|
||||||
@ -776,22 +832,12 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
|||||||
# Validate device bay
|
# Validate device bay
|
||||||
if parent and device_bay_name:
|
if parent and device_bay_name:
|
||||||
try:
|
try:
|
||||||
device_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
|
self.instance.parent_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
|
||||||
if device_bay.installed_device:
|
# Inherit site and rack from parent device
|
||||||
self.add_error('device_bay_name',
|
self.instance.site = parent.site
|
||||||
"Device bay ({} {}) is already occupied".format(parent, device_bay_name))
|
self.instance.rack = parent.rack
|
||||||
else:
|
|
||||||
self.instance.parent_bay = device_bay
|
|
||||||
except DeviceBay.DoesNotExist:
|
except DeviceBay.DoesNotExist:
|
||||||
self.add_error('device_bay_name', "Parent device/bay ({} {}) not found".format(parent, device_bay_name))
|
raise forms.ValidationError("Parent device/bay ({} {}) not found".format(parent, device_bay_name))
|
||||||
|
|
||||||
|
|
||||||
class DeviceImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=DeviceFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class ChildDeviceImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@ -889,75 +935,84 @@ class ConsolePortCreateForm(DeviceComponentForm):
|
|||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionCSVForm(forms.Form):
|
class ConsoleConnectionCSVForm(forms.ModelForm):
|
||||||
console_server = FlexibleModelChoiceField(
|
console_server = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.filter(device_type__is_console_server=True),
|
queryset=Device.objects.filter(device_type__is_console_server=True),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
help_text='Console server name or ID',
|
||||||
error_messages={
|
error_messages={
|
||||||
'invalid_choice': 'Console server not found',
|
'invalid_choice': 'Console server not found',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
cs_port = forms.CharField()
|
cs_port = forms.CharField(
|
||||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
help_text='Console server port name'
|
||||||
error_messages={'invalid_choice': 'Device not found'})
|
)
|
||||||
console_port = forms.CharField()
|
device = FlexibleModelChoiceField(
|
||||||
status = forms.CharField(validators=[validate_connection_status])
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Device name or ID',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Device not found',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
console_port = forms.CharField(
|
||||||
|
help_text='Console port name'
|
||||||
|
)
|
||||||
|
connection_status = CSVChoiceField(
|
||||||
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
|
help_text='Connection status'
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
class Meta:
|
||||||
|
model = ConsolePort
|
||||||
|
fields = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
|
||||||
|
|
||||||
# Validate console server port
|
def clean_console_port(self):
|
||||||
if self.cleaned_data.get('console_server'):
|
|
||||||
try:
|
|
||||||
cs_port = ConsoleServerPort.objects.get(device=self.cleaned_data['console_server'],
|
|
||||||
name=self.cleaned_data['cs_port'])
|
|
||||||
if ConsolePort.objects.filter(cs_port=cs_port):
|
|
||||||
raise forms.ValidationError("Console server port is already occupied (by {} {})"
|
|
||||||
.format(cs_port.connected_console.device, cs_port.connected_console))
|
|
||||||
except ConsoleServerPort.DoesNotExist:
|
|
||||||
raise forms.ValidationError("Invalid console server port ({} {})"
|
|
||||||
.format(self.cleaned_data['console_server'], self.cleaned_data['cs_port']))
|
|
||||||
|
|
||||||
# Validate console port
|
console_port_name = self.cleaned_data.get('console_port')
|
||||||
if self.cleaned_data.get('device'):
|
if not self.cleaned_data.get('device') or not console_port_name:
|
||||||
try:
|
return None
|
||||||
console_port = ConsolePort.objects.get(device=self.cleaned_data['device'],
|
|
||||||
name=self.cleaned_data['console_port'])
|
|
||||||
if console_port.cs_port:
|
|
||||||
raise forms.ValidationError("Console port is already connected (to {} {})"
|
|
||||||
.format(console_port.cs_port.device, console_port.cs_port))
|
|
||||||
except ConsolePort.DoesNotExist:
|
|
||||||
raise forms.ValidationError("Invalid console port ({} {})"
|
|
||||||
.format(self.cleaned_data['device'], self.cleaned_data['console_port']))
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Retrieve console port by name
|
||||||
|
consoleport = ConsolePort.objects.get(
|
||||||
|
device=self.cleaned_data['device'], name=console_port_name
|
||||||
|
)
|
||||||
|
# Check if the console port is already connected
|
||||||
|
if consoleport.cs_port is not None:
|
||||||
|
raise forms.ValidationError("{} {} is already connected".format(
|
||||||
|
self.cleaned_data['device'], console_port_name
|
||||||
|
))
|
||||||
|
except ConsolePort.DoesNotExist:
|
||||||
|
raise forms.ValidationError("Invalid console port ({} {})".format(
|
||||||
|
self.cleaned_data['device'], console_port_name
|
||||||
|
))
|
||||||
|
|
||||||
class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
|
self.instance = consoleport
|
||||||
csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
|
return consoleport
|
||||||
|
|
||||||
def clean(self):
|
def clean_cs_port(self):
|
||||||
records = self.cleaned_data.get('csv')
|
|
||||||
if not records:
|
|
||||||
return
|
|
||||||
|
|
||||||
connection_list = []
|
cs_port_name = self.cleaned_data.get('cs_port')
|
||||||
|
if not self.cleaned_data.get('console_server') or not cs_port_name:
|
||||||
|
return None
|
||||||
|
|
||||||
for i, record in enumerate(records, start=1):
|
try:
|
||||||
form = self.fields['csv'].csv_form(data=record)
|
# Retrieve console server port by name
|
||||||
if form.is_valid():
|
cs_port = ConsoleServerPort.objects.get(
|
||||||
console_port = ConsolePort.objects.get(device=form.cleaned_data['device'],
|
device=self.cleaned_data['console_server'], name=cs_port_name
|
||||||
name=form.cleaned_data['console_port'])
|
)
|
||||||
console_port.cs_port = ConsoleServerPort.objects.get(device=form.cleaned_data['console_server'],
|
# Check if the console server port is already connected
|
||||||
name=form.cleaned_data['cs_port'])
|
if ConsolePort.objects.filter(cs_port=cs_port).count():
|
||||||
if form.cleaned_data['status'] == 'planned':
|
raise forms.ValidationError("{} {} is already connected".format(
|
||||||
console_port.connection_status = CONNECTION_STATUS_PLANNED
|
self.cleaned_data['console_server'], cs_port_name
|
||||||
else:
|
))
|
||||||
console_port.connection_status = CONNECTION_STATUS_CONNECTED
|
except ConsoleServerPort.DoesNotExist:
|
||||||
connection_list.append(console_port)
|
raise forms.ValidationError("Invalid console server port ({} {})".format(
|
||||||
else:
|
self.cleaned_data['console_server'], cs_port_name
|
||||||
for field, errors in form.errors.items():
|
))
|
||||||
for e in errors:
|
|
||||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
|
||||||
|
|
||||||
self.cleaned_data['csv'] = connection_list
|
return cs_port
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||||
@ -1137,76 +1192,84 @@ class PowerPortCreateForm(DeviceComponentForm):
|
|||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionCSVForm(forms.Form):
|
class PowerConnectionCSVForm(forms.ModelForm):
|
||||||
pdu = FlexibleModelChoiceField(
|
pdu = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.filter(device_type__is_pdu=True),
|
queryset=Device.objects.filter(device_type__is_pdu=True),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
help_text='PDU name or ID',
|
||||||
error_messages={
|
error_messages={
|
||||||
'invalid_choice': 'PDU not found.',
|
'invalid_choice': 'PDU not found.',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
power_outlet = forms.CharField()
|
power_outlet = forms.CharField(
|
||||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
help_text='Power outlet name'
|
||||||
error_messages={'invalid_choice': 'Device not found'})
|
)
|
||||||
power_port = forms.CharField()
|
device = FlexibleModelChoiceField(
|
||||||
status = forms.CharField(validators=[validate_connection_status])
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Device name or ID',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Device not found',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
power_port = forms.CharField(
|
||||||
|
help_text='Power port name'
|
||||||
|
)
|
||||||
|
connection_status = CSVChoiceField(
|
||||||
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
|
help_text='Connection status'
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
class Meta:
|
||||||
|
model = PowerPort
|
||||||
|
fields = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
|
||||||
|
|
||||||
# Validate power outlet
|
def clean_power_port(self):
|
||||||
if self.cleaned_data.get('pdu'):
|
|
||||||
try:
|
|
||||||
power_outlet = PowerOutlet.objects.get(device=self.cleaned_data['pdu'],
|
|
||||||
name=self.cleaned_data['power_outlet'])
|
|
||||||
if PowerPort.objects.filter(power_outlet=power_outlet):
|
|
||||||
raise forms.ValidationError("Power outlet is already occupied (by {} {})"
|
|
||||||
.format(power_outlet.connected_port.device,
|
|
||||||
power_outlet.connected_port))
|
|
||||||
except PowerOutlet.DoesNotExist:
|
|
||||||
raise forms.ValidationError("Invalid PDU port ({} {})"
|
|
||||||
.format(self.cleaned_data['pdu'], self.cleaned_data['power_outlet']))
|
|
||||||
|
|
||||||
# Validate power port
|
power_port_name = self.cleaned_data.get('power_port')
|
||||||
if self.cleaned_data.get('device'):
|
if not self.cleaned_data.get('device') or not power_port_name:
|
||||||
try:
|
return None
|
||||||
power_port = PowerPort.objects.get(device=self.cleaned_data['device'],
|
|
||||||
name=self.cleaned_data['power_port'])
|
|
||||||
if power_port.power_outlet:
|
|
||||||
raise forms.ValidationError("Power port is already connected (to {} {})"
|
|
||||||
.format(power_port.power_outlet.device, power_port.power_outlet))
|
|
||||||
except PowerPort.DoesNotExist:
|
|
||||||
raise forms.ValidationError("Invalid power port ({} {})"
|
|
||||||
.format(self.cleaned_data['device'], self.cleaned_data['power_port']))
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Retrieve power port by name
|
||||||
|
powerport = PowerPort.objects.get(
|
||||||
|
device=self.cleaned_data['device'], name=power_port_name
|
||||||
|
)
|
||||||
|
# Check if the power port is already connected
|
||||||
|
if powerport.power_outlet is not None:
|
||||||
|
raise forms.ValidationError("{} {} is already connected".format(
|
||||||
|
self.cleaned_data['device'], power_port_name
|
||||||
|
))
|
||||||
|
except PowerPort.DoesNotExist:
|
||||||
|
raise forms.ValidationError("Invalid power port ({} {})".format(
|
||||||
|
self.cleaned_data['device'], power_port_name
|
||||||
|
))
|
||||||
|
|
||||||
class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
|
self.instance = powerport
|
||||||
csv = CSVDataField(csv_form=PowerConnectionCSVForm)
|
return powerport
|
||||||
|
|
||||||
def clean(self):
|
def clean_power_outlet(self):
|
||||||
records = self.cleaned_data.get('csv')
|
|
||||||
if not records:
|
|
||||||
return
|
|
||||||
|
|
||||||
connection_list = []
|
power_outlet_name = self.cleaned_data.get('power_outlet')
|
||||||
|
if not self.cleaned_data.get('pdu') or not power_outlet_name:
|
||||||
|
return None
|
||||||
|
|
||||||
for i, record in enumerate(records, start=1):
|
try:
|
||||||
form = self.fields['csv'].csv_form(data=record)
|
# Retrieve power outlet by name
|
||||||
if form.is_valid():
|
power_outlet = PowerOutlet.objects.get(
|
||||||
power_port = PowerPort.objects.get(device=form.cleaned_data['device'],
|
device=self.cleaned_data['pdu'], name=power_outlet_name
|
||||||
name=form.cleaned_data['power_port'])
|
)
|
||||||
power_port.power_outlet = PowerOutlet.objects.get(device=form.cleaned_data['pdu'],
|
# Check if the power outlet is already connected
|
||||||
name=form.cleaned_data['power_outlet'])
|
if PowerPort.objects.filter(power_outlet=power_outlet).count():
|
||||||
if form.cleaned_data['status'] == 'planned':
|
raise forms.ValidationError("{} {} is already connected".format(
|
||||||
power_port.connection_status = CONNECTION_STATUS_PLANNED
|
self.cleaned_data['pdu'], power_outlet_name
|
||||||
else:
|
))
|
||||||
power_port.connection_status = CONNECTION_STATUS_CONNECTED
|
except PowerOutlet.DoesNotExist:
|
||||||
connection_list.append(power_port)
|
raise forms.ValidationError("Invalid power outlet ({} {})".format(
|
||||||
else:
|
self.cleaned_data['pdu'], power_outlet_name
|
||||||
for field, errors in form.errors.items():
|
))
|
||||||
for e in errors:
|
|
||||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
|
||||||
|
|
||||||
self.cleaned_data['csv'] = connection_list
|
return power_outlet
|
||||||
|
|
||||||
|
|
||||||
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||||
@ -1536,94 +1599,79 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionCSVForm(forms.Form):
|
class InterfaceConnectionCSVForm(forms.ModelForm):
|
||||||
device_a = FlexibleModelChoiceField(
|
device_a = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
help_text='Name or ID of device A',
|
||||||
error_messages={'invalid_choice': 'Device A not found.'}
|
error_messages={'invalid_choice': 'Device A not found.'}
|
||||||
)
|
)
|
||||||
interface_a = forms.CharField()
|
interface_a = forms.CharField(
|
||||||
|
help_text='Name of interface A'
|
||||||
|
)
|
||||||
device_b = FlexibleModelChoiceField(
|
device_b = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
help_text='Name or ID of device B',
|
||||||
error_messages={'invalid_choice': 'Device B not found.'}
|
error_messages={'invalid_choice': 'Device B not found.'}
|
||||||
)
|
)
|
||||||
interface_b = forms.CharField()
|
interface_b = forms.CharField(
|
||||||
status = forms.CharField(
|
help_text='Name of interface B'
|
||||||
validators=[validate_connection_status]
|
)
|
||||||
|
connection_status = CSVChoiceField(
|
||||||
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
|
help_text='Connection status'
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
class Meta:
|
||||||
|
model = InterfaceConnection
|
||||||
|
fields = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
||||||
|
|
||||||
# Validate interface A
|
def clean_interface_a(self):
|
||||||
if self.cleaned_data.get('device_a'):
|
|
||||||
try:
|
|
||||||
interface_a = Interface.objects.get(device=self.cleaned_data['device_a'],
|
|
||||||
name=self.cleaned_data['interface_a'])
|
|
||||||
except Interface.DoesNotExist:
|
|
||||||
raise forms.ValidationError("Invalid interface ({} {})"
|
|
||||||
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
|
|
||||||
try:
|
|
||||||
InterfaceConnection.objects.get(Q(interface_a=interface_a) | Q(interface_b=interface_a))
|
|
||||||
raise forms.ValidationError("{} {} is already connected"
|
|
||||||
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
|
|
||||||
except InterfaceConnection.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Validate interface B
|
interface_name = self.cleaned_data.get('interface_a')
|
||||||
if self.cleaned_data.get('device_b'):
|
if not interface_name:
|
||||||
try:
|
return None
|
||||||
interface_b = Interface.objects.get(device=self.cleaned_data['device_b'],
|
|
||||||
name=self.cleaned_data['interface_b'])
|
|
||||||
except Interface.DoesNotExist:
|
|
||||||
raise forms.ValidationError("Invalid interface ({} {})"
|
|
||||||
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
|
|
||||||
try:
|
|
||||||
InterfaceConnection.objects.get(Q(interface_a=interface_b) | Q(interface_b=interface_b))
|
|
||||||
raise forms.ValidationError("{} {} is already connected"
|
|
||||||
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
|
|
||||||
except InterfaceConnection.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Retrieve interface by name
|
||||||
|
interface = Interface.objects.get(
|
||||||
|
device=self.cleaned_data['device_a'], name=interface_name
|
||||||
|
)
|
||||||
|
# Check for an existing connection to this interface
|
||||||
|
if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count():
|
||||||
|
raise forms.ValidationError("{} {} is already connected".format(
|
||||||
|
self.cleaned_data['device_a'], interface_name
|
||||||
|
))
|
||||||
|
except Interface.DoesNotExist:
|
||||||
|
raise forms.ValidationError("Invalid interface ({} {})".format(
|
||||||
|
self.cleaned_data['device_a'], interface_name
|
||||||
|
))
|
||||||
|
|
||||||
class InterfaceConnectionImportForm(BootstrapMixin, BulkImportForm):
|
return interface
|
||||||
csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean_interface_b(self):
|
||||||
records = self.cleaned_data.get('csv')
|
|
||||||
if not records:
|
|
||||||
return
|
|
||||||
|
|
||||||
connection_list = []
|
interface_name = self.cleaned_data.get('interface_b')
|
||||||
occupied_interfaces = []
|
if not interface_name:
|
||||||
|
return None
|
||||||
|
|
||||||
for i, record in enumerate(records, start=1):
|
try:
|
||||||
form = self.fields['csv'].csv_form(data=record)
|
# Retrieve interface by name
|
||||||
if form.is_valid():
|
interface = Interface.objects.get(
|
||||||
interface_a = Interface.objects.get(device=form.cleaned_data['device_a'],
|
device=self.cleaned_data['device_b'], name=interface_name
|
||||||
name=form.cleaned_data['interface_a'])
|
)
|
||||||
if interface_a in occupied_interfaces:
|
# Check for an existing connection to this interface
|
||||||
raise forms.ValidationError("{} {} found in multiple connections"
|
if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count():
|
||||||
.format(interface_a.device.name, interface_a.name))
|
raise forms.ValidationError("{} {} is already connected".format(
|
||||||
interface_b = Interface.objects.get(device=form.cleaned_data['device_b'],
|
self.cleaned_data['device_b'], interface_name
|
||||||
name=form.cleaned_data['interface_b'])
|
))
|
||||||
if interface_b in occupied_interfaces:
|
except Interface.DoesNotExist:
|
||||||
raise forms.ValidationError("{} {} found in multiple connections"
|
raise forms.ValidationError("Invalid interface ({} {})".format(
|
||||||
.format(interface_b.device.name, interface_b.name))
|
self.cleaned_data['device_b'], interface_name
|
||||||
connection = InterfaceConnection(interface_a=interface_a, interface_b=interface_b)
|
))
|
||||||
if form.cleaned_data['status'] == 'planned':
|
|
||||||
connection.connection_status = CONNECTION_STATUS_PLANNED
|
|
||||||
else:
|
|
||||||
connection.connection_status = CONNECTION_STATUS_CONNECTED
|
|
||||||
connection_list.append(connection)
|
|
||||||
occupied_interfaces.append(interface_a)
|
|
||||||
occupied_interfaces.append(interface_b)
|
|
||||||
else:
|
|
||||||
for field, errors in form.errors.items():
|
|
||||||
for e in errors:
|
|
||||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
|
||||||
|
|
||||||
self.cleaned_data['csv'] = connection_list
|
return interface
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form):
|
class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form):
|
||||||
|
@ -346,7 +346,7 @@ class RackGroup(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} - {}'.format(self.site.name, self.name)
|
return 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)
|
||||||
@ -1393,10 +1393,13 @@ class InterfaceConnection(models.Model):
|
|||||||
verbose_name='Status')
|
verbose_name='Status')
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.interface_a == self.interface_b:
|
try:
|
||||||
raise ValidationError({
|
if self.interface_a == self.interface_b:
|
||||||
'interface_b': "Cannot connect an interface to itself."
|
raise ValidationError({
|
||||||
})
|
'interface_b': "Cannot connect an interface to itself."
|
||||||
|
})
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
# Used for connections export
|
# Used for connections export
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
|
@ -247,7 +247,7 @@ class RackImportTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
|
fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'u_height')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -29,8 +29,8 @@ 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,
|
||||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
||||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||||
RackReservation, RackRole, Region, Site,
|
RackGroup, RackReservation, RackRole, Region, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -219,9 +219,8 @@ class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.add_site'
|
permission_required = 'dcim.add_site'
|
||||||
form = forms.SiteImportForm
|
model_form = forms.SiteCSVForm
|
||||||
table = tables.SiteTable
|
table = tables.SiteTable
|
||||||
template_name = 'dcim/site_import.html'
|
|
||||||
default_return_url = 'dcim:site_list'
|
default_return_url = 'dcim:site_list'
|
||||||
|
|
||||||
|
|
||||||
@ -390,9 +389,8 @@ class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.add_rack'
|
permission_required = 'dcim.add_rack'
|
||||||
form = forms.RackImportForm
|
model_form = forms.RackCSVForm
|
||||||
table = tables.RackImportTable
|
table = tables.RackImportTable
|
||||||
template_name = 'dcim/rack_import.html'
|
|
||||||
default_return_url = 'dcim:rack_list'
|
default_return_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
@ -866,7 +864,7 @@ class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.add_device'
|
permission_required = 'dcim.add_device'
|
||||||
form = forms.DeviceImportForm
|
model_form = forms.DeviceCSVForm
|
||||||
table = tables.DeviceImportTable
|
table = tables.DeviceImportTable
|
||||||
template_name = 'dcim/device_import.html'
|
template_name = 'dcim/device_import.html'
|
||||||
default_return_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
@ -874,23 +872,22 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
|
|
||||||
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.add_device'
|
permission_required = 'dcim.add_device'
|
||||||
form = forms.ChildDeviceImportForm
|
model_form = forms.ChildDeviceCSVForm
|
||||||
table = tables.DeviceImportTable
|
table = tables.DeviceImportTable
|
||||||
template_name = 'dcim/device_import_child.html'
|
template_name = 'dcim/device_import_child.html'
|
||||||
default_return_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
def save_obj(self, obj):
|
def _save_obj(self, obj_form):
|
||||||
|
|
||||||
# Inherit site and rack from parent device
|
obj = obj_form.save()
|
||||||
obj.site = obj.parent_bay.device.site
|
|
||||||
obj.rack = obj.parent_bay.device.rack
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
# Save the reverse relation
|
# Save the reverse relation to the parent device bay
|
||||||
device_bay = obj.parent_bay
|
device_bay = obj.parent_bay
|
||||||
device_bay.installed_device = obj
|
device_bay.installed_device = obj
|
||||||
device_bay.save()
|
device_bay.save()
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_device'
|
permission_required = 'dcim.change_device'
|
||||||
@ -1016,9 +1013,8 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
|
|
||||||
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.change_consoleport'
|
permission_required = 'dcim.change_consoleport'
|
||||||
form = forms.ConsoleConnectionImportForm
|
model_form = forms.ConsoleConnectionCSVForm
|
||||||
table = tables.ConsoleConnectionTable
|
table = tables.ConsoleConnectionTable
|
||||||
template_name = 'dcim/console_connections_import.html'
|
|
||||||
default_return_url = 'dcim:console_connections_list'
|
default_return_url = 'dcim:console_connections_list'
|
||||||
|
|
||||||
|
|
||||||
@ -1239,9 +1235,8 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
|
|
||||||
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.change_powerport'
|
permission_required = 'dcim.change_powerport'
|
||||||
form = forms.PowerConnectionImportForm
|
model_form = forms.PowerConnectionCSVForm
|
||||||
table = tables.PowerConnectionTable
|
table = tables.PowerConnectionTable
|
||||||
template_name = 'dcim/power_connections_import.html'
|
|
||||||
default_return_url = 'dcim:power_connections_list'
|
default_return_url = 'dcim:power_connections_list'
|
||||||
|
|
||||||
|
|
||||||
@ -1676,9 +1671,8 @@ def interfaceconnection_delete(request, pk):
|
|||||||
|
|
||||||
class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.change_interface'
|
permission_required = 'dcim.change_interface'
|
||||||
form = forms.InterfaceConnectionImportForm
|
model_form = forms.InterfaceConnectionCSVForm
|
||||||
table = tables.InterfaceConnectionTable
|
table = tables.InterfaceConnectionTable
|
||||||
template_name = 'dcim/interface_connections_import.html'
|
|
||||||
default_return_url = 'dcim:interface_connections_list'
|
default_return_url = 'dcim:interface_connections_list'
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Site, Rack, Device, Interface
|
from dcim.models import Site, Rack, Device, Interface
|
||||||
@ -9,8 +8,9 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
|||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField,
|
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField,
|
||||||
ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
|
ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, 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,
|
||||||
@ -48,17 +48,23 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class VRFFromCSVForm(forms.ModelForm):
|
class VRFCSVForm(forms.ModelForm):
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
tenant = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned tenant',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||||
|
help_texts = {
|
||||||
|
'name': 'VRF name',
|
||||||
class VRFImportForm(BootstrapMixin, BulkImportForm):
|
}
|
||||||
csv = CSVDataField(csv_form=VRFFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@ -116,19 +122,21 @@ class AggregateForm(BootstrapMixin, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AggregateFromCSVForm(forms.ModelForm):
|
class AggregateCSVForm(forms.ModelForm):
|
||||||
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), to_field_name='name',
|
rir = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'RIR not found.'})
|
queryset=RIR.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of parent RIR',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'RIR not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
fields = ['prefix', 'rir', 'date_added', 'description']
|
fields = ['prefix', 'rir', 'date_added', 'description']
|
||||||
|
|
||||||
|
|
||||||
class AggregateImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=AggregateFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
|
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
|
||||||
@ -197,69 +205,89 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
self.fields['vrf'].empty_label = 'Global'
|
self.fields['vrf'].empty_label = 'Global'
|
||||||
|
|
||||||
|
|
||||||
class PrefixFromCSVForm(forms.ModelForm):
|
class PrefixCSVForm(forms.ModelForm):
|
||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
vrf = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'VRF not found.'})
|
queryset=VRF.objects.all(),
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
required=False,
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
to_field_name='rd',
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
help_text='Route distinguisher of parent VRF',
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
error_messages={
|
||||||
vlan_group_name = forms.CharField(required=False)
|
'invalid_choice': 'VRF not found.',
|
||||||
vlan_vid = forms.IntegerField(required=False)
|
}
|
||||||
status = forms.CharField()
|
)
|
||||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
tenant = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Invalid role.'})
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned tenant',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
site = forms.ModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of parent site',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Site not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
vlan_group = forms.CharField(
|
||||||
|
help_text='Group name of assigned VLAN',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
vlan_vid = forms.IntegerField(
|
||||||
|
help_text='Numeric ID of assigned VLAN',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
status = CSVChoiceField(
|
||||||
|
choices=IPADDRESS_STATUS_CHOICES,
|
||||||
|
help_text='Operational status'
|
||||||
|
)
|
||||||
|
role = forms.ModelChoiceField(
|
||||||
|
queryset=Role.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Functional role',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Invalid role.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = [
|
fields = [
|
||||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status', 'role', 'is_pool',
|
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
||||||
'description',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
super(PrefixFromCSVForm, self).clean()
|
super(PrefixCSVForm, self).clean()
|
||||||
|
|
||||||
site = self.cleaned_data.get('site')
|
site = self.cleaned_data.get('site')
|
||||||
vlan_group_name = self.cleaned_data.get('vlan_group_name')
|
vlan_group = self.cleaned_data.get('vlan_group')
|
||||||
vlan_vid = self.cleaned_data.get('vlan_vid')
|
vlan_vid = self.cleaned_data.get('vlan_vid')
|
||||||
vlan_group = None
|
|
||||||
|
|
||||||
# Validate VLAN group
|
|
||||||
if vlan_group_name:
|
|
||||||
try:
|
|
||||||
vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
|
|
||||||
except VLANGroup.DoesNotExist:
|
|
||||||
if site:
|
|
||||||
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
|
|
||||||
else:
|
|
||||||
self.add_error('vlan_group_name', "Invalid global VLAN group ({}).".format(vlan_group_name))
|
|
||||||
|
|
||||||
# Validate VLAN
|
# Validate VLAN
|
||||||
if vlan_vid:
|
if vlan_group and vlan_vid:
|
||||||
try:
|
try:
|
||||||
self.instance.vlan = VLAN.objects.get(site=site, group=vlan_group, vid=vlan_vid)
|
self.instance.vlan = VLAN.objects.get(site=site, group__name=vlan_group, vid=vlan_vid)
|
||||||
except VLAN.DoesNotExist:
|
except VLAN.DoesNotExist:
|
||||||
if site:
|
if site:
|
||||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
|
raise forms.ValidationError("VLAN {} not found in site {} group {}".format(
|
||||||
elif vlan_group:
|
vlan_vid, site, vlan_group
|
||||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for group {}.".format(vlan_vid, vlan_group_name))
|
))
|
||||||
elif not vlan_group_name:
|
else:
|
||||||
self.add_error('vlan_vid', "Invalid global VLAN ID ({}).".format(vlan_vid))
|
raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group))
|
||||||
except VLAN.MultipleObjectsReturned:
|
elif vlan_vid:
|
||||||
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
|
try:
|
||||||
|
self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid)
|
||||||
def clean_status(self):
|
except VLAN.DoesNotExist:
|
||||||
status_choices = {s[1].lower(): s[0] for s in PREFIX_STATUS_CHOICES}
|
if site:
|
||||||
try:
|
raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site))
|
||||||
return status_choices[self.cleaned_data['status'].lower()]
|
else:
|
||||||
except KeyError:
|
raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid))
|
||||||
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
|
|
||||||
|
|
||||||
|
|
||||||
class PrefixImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=PrefixFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@ -513,16 +541,46 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
self.fields['vrf'].empty_label = 'Global'
|
self.fields['vrf'].empty_label = 'Global'
|
||||||
|
|
||||||
|
|
||||||
class IPAddressFromCSVForm(forms.ModelForm):
|
class IPAddressCSVForm(forms.ModelForm):
|
||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
vrf = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'VRF not found.'})
|
queryset=VRF.objects.all(),
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
required=False,
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
to_field_name='rd',
|
||||||
status = forms.CharField()
|
help_text='Route distinguisher of the assigned VRF',
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
error_messages={
|
||||||
error_messages={'invalid_choice': 'Device not found.'})
|
'invalid_choice': 'VRF not found.',
|
||||||
interface_name = forms.CharField(required=False)
|
}
|
||||||
is_primary = forms.BooleanField(required=False)
|
)
|
||||||
|
tenant = forms.ModelChoiceField(
|
||||||
|
queryset=Tenant.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
required=False,
|
||||||
|
help_text='Name of the assigned tenant',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
status = CSVChoiceField(
|
||||||
|
choices=PREFIX_STATUS_CHOICES,
|
||||||
|
help_text='Operational status'
|
||||||
|
)
|
||||||
|
device = FlexibleModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name or ID of assigned device',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Device not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
interface_name = forms.CharField(
|
||||||
|
help_text='Name of assigned interface',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
is_primary = forms.BooleanField(
|
||||||
|
help_text='Make this the primary IP for the assigned device',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
@ -530,6 +588,8 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
super(IPAddressCSVForm, self).clean()
|
||||||
|
|
||||||
device = self.cleaned_data.get('device')
|
device = self.cleaned_data.get('device')
|
||||||
interface_name = self.cleaned_data.get('interface_name')
|
interface_name = self.cleaned_data.get('interface_name')
|
||||||
is_primary = self.cleaned_data.get('is_primary')
|
is_primary = self.cleaned_data.get('is_primary')
|
||||||
@ -537,24 +597,17 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
|||||||
# Validate interface
|
# Validate interface
|
||||||
if device and interface_name:
|
if device and interface_name:
|
||||||
try:
|
try:
|
||||||
Interface.objects.get(device=device, name=interface_name)
|
self.instance.interface = Interface.objects.get(device=device, name=interface_name)
|
||||||
except Interface.DoesNotExist:
|
except Interface.DoesNotExist:
|
||||||
self.add_error('interface_name', "Invalid interface ({}) for {}".format(interface_name, device))
|
raise forms.ValidationError("Invalid interface {} for device {}".format(interface_name, device))
|
||||||
elif device and not interface_name:
|
elif device and not interface_name:
|
||||||
self.add_error('interface_name', "Device set ({}) but interface missing".format(device))
|
raise forms.ValidationError("Device set ({}) but interface missing".format(device))
|
||||||
elif interface_name and not device:
|
elif interface_name and not device:
|
||||||
self.add_error('device', "Interface set ({}) but device missing or invalid".format(interface_name))
|
raise forms.ValidationError("Interface set ({}) but device missing or invalid".format(interface_name))
|
||||||
|
|
||||||
# Validate is_primary
|
# Validate is_primary
|
||||||
if is_primary and not device:
|
if is_primary and not device:
|
||||||
self.add_error('is_primary', "No device specified; cannot set as primary IP")
|
raise forms.ValidationError("No device specified; cannot set as primary IP")
|
||||||
|
|
||||||
def clean_status(self):
|
|
||||||
status_choices = {s[1].lower(): s[0] for s in IPADDRESS_STATUS_CHOICES}
|
|
||||||
try:
|
|
||||||
return status_choices[self.cleaned_data['status'].lower()]
|
|
||||||
except KeyError:
|
|
||||||
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
@ -569,11 +622,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
|||||||
elif self.instance.address.version == 6:
|
elif self.instance.address.version == 6:
|
||||||
self.instance.primary_ip6_for = self.cleaned_data['device']
|
self.instance.primary_ip6_for = self.cleaned_data['device']
|
||||||
|
|
||||||
return super(IPAddressFromCSVForm, self).save(*args, **kwargs)
|
return super(IPAddressCSVForm, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class IPAddressImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=IPAddressFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@ -673,60 +722,67 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class VLANFromCSVForm(forms.ModelForm):
|
class VLANCSVForm(forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(), required=False, to_field_name='name',
|
queryset=Site.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Site not found.'}
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of parent site',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Site not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
group_name = forms.CharField(
|
||||||
|
help_text='Name of VLAN group',
|
||||||
|
required=False
|
||||||
)
|
)
|
||||||
group_name = forms.CharField(required=False)
|
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = forms.ModelChoiceField(
|
||||||
Tenant.objects.all(), to_field_name='name', required=False,
|
queryset=Tenant.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'}
|
to_field_name='name',
|
||||||
|
required=False,
|
||||||
|
help_text='Name of assigned tenant',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
status = CSVChoiceField(
|
||||||
|
choices=VLAN_STATUS_CHOICES,
|
||||||
|
help_text='Operational status'
|
||||||
)
|
)
|
||||||
status = forms.CharField()
|
|
||||||
role = forms.ModelChoiceField(
|
role = forms.ModelChoiceField(
|
||||||
queryset=Role.objects.all(), required=False, to_field_name='name',
|
queryset=Role.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Invalid role.'}
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Functional role',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Invalid role.',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||||
|
help_texts = {
|
||||||
|
'vid': 'Numeric VLAN ID (1-4095)',
|
||||||
|
'name': 'VLAN name',
|
||||||
|
}
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
super(VLANFromCSVForm, self).clean()
|
super(VLANCSVForm, self).clean()
|
||||||
|
|
||||||
# Validate VLANGroup
|
site = self.cleaned_data.get('site')
|
||||||
group_name = self.cleaned_data.get('group_name')
|
group_name = self.cleaned_data.get('group_name')
|
||||||
|
|
||||||
|
# Validate VLAN group
|
||||||
if group_name:
|
if group_name:
|
||||||
try:
|
try:
|
||||||
VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
|
self.instance.group = VLANGroup.objects.get(site=site, name=group_name)
|
||||||
except VLANGroup.DoesNotExist:
|
except VLANGroup.DoesNotExist:
|
||||||
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
|
if site:
|
||||||
|
raise forms.ValidationError("VLAN group {} not found for site {}".format(group_name, site))
|
||||||
def clean_status(self):
|
else:
|
||||||
status_choices = {s[1].lower(): s[0] for s in VLAN_STATUS_CHOICES}
|
raise forms.ValidationError("Global VLAN group {} not found".format(group_name))
|
||||||
try:
|
|
||||||
return status_choices[self.cleaned_data['status'].lower()]
|
|
||||||
except KeyError:
|
|
||||||
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
|
|
||||||
vlan = super(VLANFromCSVForm, self).save(commit=False)
|
|
||||||
|
|
||||||
# Assign VLANGroup by site and name
|
|
||||||
if self.cleaned_data['group_name']:
|
|
||||||
vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=self.cleaned_data['group_name'])
|
|
||||||
|
|
||||||
if kwargs.get('commit'):
|
|
||||||
vlan.save()
|
|
||||||
return vlan
|
|
||||||
|
|
||||||
|
|
||||||
class VLANImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=VLANFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
@ -498,9 +498,7 @@ class VLANGroup(models.Model):
|
|||||||
verbose_name_plural = 'VLAN groups'
|
verbose_name_plural = 'VLAN groups'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.site is None:
|
return self.name
|
||||||
return 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)
|
||||||
|
@ -130,9 +130,8 @@ class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'ipam.add_vrf'
|
permission_required = 'ipam.add_vrf'
|
||||||
form = forms.VRFImportForm
|
model_form = forms.VRFCSVForm
|
||||||
table = tables.VRFTable
|
table = tables.VRFTable
|
||||||
template_name = 'ipam/vrf_import.html'
|
|
||||||
default_return_url = 'ipam:vrf_list'
|
default_return_url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
@ -341,9 +340,8 @@ class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'ipam.add_aggregate'
|
permission_required = 'ipam.add_aggregate'
|
||||||
form = forms.AggregateImportForm
|
model_form = forms.AggregateCSVForm
|
||||||
table = tables.AggregateTable
|
table = tables.AggregateTable
|
||||||
template_name = 'ipam/aggregate_import.html'
|
|
||||||
default_return_url = 'ipam:aggregate_list'
|
default_return_url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
@ -538,9 +536,8 @@ class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'ipam.add_prefix'
|
permission_required = 'ipam.add_prefix'
|
||||||
form = forms.PrefixImportForm
|
model_form = forms.PrefixCSVForm
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixTable
|
||||||
template_name = 'ipam/prefix_import.html'
|
|
||||||
default_return_url = 'ipam:prefix_list'
|
default_return_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
@ -640,9 +637,8 @@ class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
|
|||||||
|
|
||||||
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'ipam.add_ipaddress'
|
permission_required = 'ipam.add_ipaddress'
|
||||||
form = forms.IPAddressImportForm
|
model_form = forms.IPAddressCSVForm
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
template_name = 'ipam/ipaddress_import.html'
|
|
||||||
default_return_url = 'ipam:ipaddress_list'
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
def save_obj(self, obj):
|
def save_obj(self, obj):
|
||||||
@ -748,9 +744,8 @@ class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'ipam.add_vlan'
|
permission_required = 'ipam.add_vlan'
|
||||||
form = forms.VLANImportForm
|
model_form = forms.VLANCSVForm
|
||||||
table = tables.VLANTable
|
table = tables.VLANTable
|
||||||
template_name = 'ipam/vlan_import.html'
|
|
||||||
default_return_url = 'ipam:vlan_list'
|
default_return_url = 'ipam:vlan_list'
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ from django import forms
|
|||||||
from django.db.models import Count
|
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, FilterChoiceField, FlexibleModelChoiceField, SlugField
|
||||||
from .models import Secret, SecretRole, UserKey
|
from .models import Secret, SecretRole, UserKey
|
||||||
|
|
||||||
|
|
||||||
@ -65,27 +65,40 @@ class SecretForm(BootstrapMixin, forms.ModelForm):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class SecretFromCSVForm(forms.ModelForm):
|
class SecretCSVForm(forms.ModelForm):
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
device = FlexibleModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Device not found.'})
|
queryset=Device.objects.all(),
|
||||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), to_field_name='name',
|
to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid secret role.'})
|
help_text='Device name or ID',
|
||||||
plaintext = forms.CharField()
|
error_messages={
|
||||||
|
'invalid_choice': 'Device not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
role = forms.ModelChoiceField(
|
||||||
|
queryset=SecretRole.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned role',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Invalid secret role.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
plaintext = forms.CharField(
|
||||||
|
help_text='Plaintext secret data'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = ['device', 'role', 'name', 'plaintext']
|
fields = ['device', 'role', 'name', 'plaintext']
|
||||||
|
help_texts = {
|
||||||
|
'name': 'Name or username',
|
||||||
|
}
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
s = super(SecretFromCSVForm, self).save(*args, **kwargs)
|
s = super(SecretCSVForm, self).save(*args, **kwargs)
|
||||||
s.plaintext = str(self.cleaned_data['plaintext'])
|
s.plaintext = str(self.cleaned_data['plaintext'])
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
class SecretImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-session-key'}))
|
|
||||||
|
|
||||||
|
|
||||||
class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
|
class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
|
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
|
||||||
|
@ -16,7 +16,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'),
|
url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'),
|
||||||
url(r'^secrets/import/$', views.secret_import, name='secret_import'),
|
url(r'^secrets/import/$', views.SecretBulkImportView.as_view(), 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.SecretView.as_view(), name='secret'),
|
url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
|
||||||
|
@ -12,7 +12,9 @@ from django.utils.decorators import method_decorator
|
|||||||
from django.views.generic import View
|
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, BulkImportView, 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
|
||||||
@ -185,58 +187,50 @@ class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
default_return_url = 'secrets:secret_list'
|
default_return_url = 'secrets:secret_list'
|
||||||
|
|
||||||
|
|
||||||
@permission_required('secrets.add_secret')
|
class SecretBulkImportView(BulkImportView):
|
||||||
@userkey_required()
|
permission_required = 'ipam.add_vlan'
|
||||||
def secret_import(request):
|
model_form = forms.SecretCSVForm
|
||||||
|
table = tables.SecretTable
|
||||||
|
default_return_url = 'secrets:secret_list'
|
||||||
|
|
||||||
session_key = request.COOKIES.get('session_key', None)
|
master_key = None
|
||||||
|
|
||||||
if request.method == 'POST':
|
def _save_obj(self, obj_form):
|
||||||
form = forms.SecretImportForm(request.POST)
|
"""
|
||||||
|
Encrypt each object before saving it to the database.
|
||||||
|
"""
|
||||||
|
obj = obj_form.save(commit=False)
|
||||||
|
obj.encrypt(self.master_key)
|
||||||
|
obj.save()
|
||||||
|
return obj
|
||||||
|
|
||||||
if session_key is None:
|
def post(self, request):
|
||||||
form.add_error(None, "No session key was provided with the request. Unable to encrypt secret data.")
|
|
||||||
|
|
||||||
if form.is_valid():
|
# Grab the session key from cookies.
|
||||||
|
session_key = request.COOKIES.get('session_key')
|
||||||
|
if session_key:
|
||||||
|
|
||||||
new_secrets = []
|
# Attempt to derive the master key using the provided session key.
|
||||||
|
|
||||||
session_key = base64.b64decode(session_key)
|
|
||||||
master_key = None
|
|
||||||
try:
|
try:
|
||||||
sk = SessionKey.objects.get(userkey__user=request.user)
|
sk = SessionKey.objects.get(userkey__user=request.user)
|
||||||
master_key = sk.get_master_key(session_key)
|
self.master_key = sk.get_master_key(base64.b64decode(session_key))
|
||||||
except SessionKey.DoesNotExist:
|
except SessionKey.DoesNotExist:
|
||||||
form.add_error(None, "No session key found for this user.")
|
messages.error(request, "No session key found for this user.")
|
||||||
|
|
||||||
if master_key is None:
|
if self.master_key is not None:
|
||||||
form.add_error(None, "Invalid private key! Unable to encrypt secret data.")
|
return super(SecretBulkImportView, self).post(request)
|
||||||
else:
|
else:
|
||||||
try:
|
messages.error(request, "Invalid private key! Unable to encrypt secret data.")
|
||||||
with transaction.atomic():
|
|
||||||
for secret in form.cleaned_data['csv']:
|
|
||||||
secret.encrypt(master_key)
|
|
||||||
secret.save()
|
|
||||||
new_secrets.append(secret)
|
|
||||||
|
|
||||||
table = tables.SecretTable(new_secrets)
|
else:
|
||||||
messages.success(request, "Imported {} new secrets.".format(len(new_secrets)))
|
messages.error(request, "No session key was provided with the request. Unable to encrypt secret data.")
|
||||||
|
|
||||||
return render(request, 'import_success.html', {
|
return render(request, self.template_name, {
|
||||||
'table': table,
|
'form': self._import_form(request.POST),
|
||||||
'return_url': 'secrets:secret_list',
|
'fields': self.model_form().fields,
|
||||||
})
|
'obj_type': self.model_form._meta.model._meta.verbose_name,
|
||||||
|
'return_url': self.default_return_url,
|
||||||
except IntegrityError as e:
|
})
|
||||||
form.add_error('csv', "Record {}: {}".format(len(new_secrets) + 1, e.__cause__))
|
|
||||||
|
|
||||||
else:
|
|
||||||
form = forms.SecretImportForm()
|
|
||||||
|
|
||||||
return render(request, 'secrets/secret_import.html', {
|
|
||||||
'form': form,
|
|
||||||
'return_url': 'secrets:secret_list',
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}Circuit Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Circuit ID</td>
|
|
||||||
<td>Alphanumeric circuit identifier</td>
|
|
||||||
<td>IC-603122</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Provider</td>
|
|
||||||
<td>Name of circuit provider</td>
|
|
||||||
<td>TeliaSonera</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Type</td>
|
|
||||||
<td>Circuit type</td>
|
|
||||||
<td>Transit</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Tenant</td>
|
|
||||||
<td>Name of tenant (optional)</td>
|
|
||||||
<td>Strickland Propane</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Install Date</td>
|
|
||||||
<td>Date in YYYY-MM-DD format (optional)</td>
|
|
||||||
<td>2016-02-23</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Commit rate</td>
|
|
||||||
<td>Commited rate in Kbps (optional)</td>
|
|
||||||
<td>2000</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Description</td>
|
|
||||||
<td>Short description (optional)</td>
|
|
||||||
<td>Primary for voice</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,45 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}Provider Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>Provider's proper name</td>
|
|
||||||
<td>Level 3</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Slug</td>
|
|
||||||
<td>URL-friendly name</td>
|
|
||||||
<td>level3</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>ASN</td>
|
|
||||||
<td>Autonomous system number (optional)</td>
|
|
||||||
<td>3356</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Account</td>
|
|
||||||
<td>Account number (optional)</td>
|
|
||||||
<td>08931544</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Portal URL</td>
|
|
||||||
<td>Customer service portal URL (optional)</td>
|
|
||||||
<td>https://mylevel3.net</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>Level 3,level3,3356,08931544,https://mylevel3.net</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,45 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}Console Connections Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Console server</td>
|
|
||||||
<td>Device name or {ID}</td>
|
|
||||||
<td>abc1-cs3</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Console server port</td>
|
|
||||||
<td>Full CS port name</td>
|
|
||||||
<td>Port 35</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Device</td>
|
|
||||||
<td>Device name or {ID}</td>
|
|
||||||
<td>abc1-switch7</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Console Port</td>
|
|
||||||
<td>Console port name</td>
|
|
||||||
<td>Console</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Connection Status</td>
|
|
||||||
<td>"planned" or "connected"</td>
|
|
||||||
<td>planned</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>abc1-cs3,Port 35,abc1-switch7,Console,planned</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,103 +1,5 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends 'utilities/obj_import.html' %}
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Device Import{% endblock %}
|
{% block tabs %}
|
||||||
|
{% include 'dcim/inc/device_import_header.html' %}
|
||||||
{% block content %}
|
|
||||||
{% include 'dcim/inc/device_import_header.html' %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<form action="." method="post" class="form">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% render_form form %}
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-md-12 text-right">
|
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
|
||||||
{% if return_url %}
|
|
||||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>Device name (optional)</td>
|
|
||||||
<td>rack101_sw1</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Device role</td>
|
|
||||||
<td>Functional role of device</td>
|
|
||||||
<td>ToR Switch</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Tenant</td>
|
|
||||||
<td>Name of tenant (optional)</td>
|
|
||||||
<td>Pied Piper</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Device manufacturer</td>
|
|
||||||
<td>Hardware manufacturer</td>
|
|
||||||
<td>Juniper</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Device model</td>
|
|
||||||
<td>Hardware model</td>
|
|
||||||
<td>EX4300-48T</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Platform</td>
|
|
||||||
<td>Software running on device (optional)</td>
|
|
||||||
<td>Juniper Junos</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Serial number</td>
|
|
||||||
<td>Physical serial number (optional)</td>
|
|
||||||
<td>CAB00577291</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Asset tag</td>
|
|
||||||
<td>Unique alphanumeric tag (optional)</td>
|
|
||||||
<td>ABC123456</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Status</td>
|
|
||||||
<td>Current status</td>
|
|
||||||
<td>Active</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Site</td>
|
|
||||||
<td>Site name</td>
|
|
||||||
<td>Ashburn-VA</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Rack</td>
|
|
||||||
<td>Rack name (optional)</td>
|
|
||||||
<td>R101</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Position (U)</td>
|
|
||||||
<td>Lowest-numbered rack unit occupied by the device (optional)</td>
|
|
||||||
<td>21</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Face</td>
|
|
||||||
<td>Rack face; front or rear (required if position is set)</td>
|
|
||||||
<td>Rear</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,ABC123456,Active,Ashburn-VA,R101,21,Rear</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,93 +1,5 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends 'utilities/obj_import.html' %}
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Device Import{% endblock %}
|
{% block tabs %}
|
||||||
|
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
||||||
{% block content %}
|
|
||||||
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<form action="." method="post" class="form">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% render_form form %}
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-md-12 text-right">
|
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
|
||||||
{% if return_url %}
|
|
||||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>Device name (optional)</td>
|
|
||||||
<td>Blade12</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Device role</td>
|
|
||||||
<td>Functional role of device</td>
|
|
||||||
<td>Blade Server</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Tenant</td>
|
|
||||||
<td>Name of tenant (optional)</td>
|
|
||||||
<td>Pied Piper</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Device manufacturer</td>
|
|
||||||
<td>Hardware manufacturer</td>
|
|
||||||
<td>Dell</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Device model</td>
|
|
||||||
<td>Hardware model</td>
|
|
||||||
<td>BS2000T</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Platform</td>
|
|
||||||
<td>Software running on device (optional)</td>
|
|
||||||
<td>Linux</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Serial number</td>
|
|
||||||
<td>Physical serial number (optional)</td>
|
|
||||||
<td>CAB00577291</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Asset tag</td>
|
|
||||||
<td>Unique alphanumeric tag (optional)</td>
|
|
||||||
<td>ABC123456</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Status</td>
|
|
||||||
<td>Current status</td>
|
|
||||||
<td>Active</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Parent device</td>
|
|
||||||
<td>Parent device</td>
|
|
||||||
<td>Server101</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Device bay</td>
|
|
||||||
<td>Device bay name</td>
|
|
||||||
<td>Slot 4</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Active,Server101,Slot4</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
<h1>Device Import</h1>
|
|
||||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}><a href="{% url 'dcim:device_import' %}">Racked Devices</a></li>
|
<li role="presentation"{% if not active_tab %} class="active"{% endif %}><a href="{% url 'dcim:device_import' %}">Racked Devices</a></li>
|
||||||
<li role="presentation"{% if active_tab == 'child_import' %} class="active"{% endif %}><a href="{% url 'dcim:device_import_child' %}">Child Devices</a></li>
|
<li role="presentation"{% if active_tab == 'child_import' %} class="active"{% endif %}><a href="{% url 'dcim:device_import_child' %}">Child Devices</a></li>
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}Interface Connections Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Device A</td>
|
|
||||||
<td>Device name or {ID}</td>
|
|
||||||
<td>abc1-core1</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Interface A</td>
|
|
||||||
<td>Interface name</td>
|
|
||||||
<td>xe-0/0/6</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Device B</td>
|
|
||||||
<td>Device name or {ID}</td>
|
|
||||||
<td>abc1-switch7</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Interface B</td>
|
|
||||||
<td>Interface name</td>
|
|
||||||
<td>xe-0/0/0</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Connection Status</td>
|
|
||||||
<td>"planned" or "connected"</td>
|
|
||||||
<td>planned</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,45 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}Power Connections Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>PDU</td>
|
|
||||||
<td>Device name or {ID}</td>
|
|
||||||
<td>abc1-pdu1</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Power Outlet</td>
|
|
||||||
<td>Power outlet name</td>
|
|
||||||
<td>AC4</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Device</td>
|
|
||||||
<td>Device name or {ID}</td>
|
|
||||||
<td>abc1-switch7</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Power Port</td>
|
|
||||||
<td>Power port name</td>
|
|
||||||
<td>PSU0</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Connection Status</td>
|
|
||||||
<td>"planned" or "connected"</td>
|
|
||||||
<td>connected</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>abc1-pdu1,AC4,abc1-switch7,PSU0,connected</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,70 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}Rack Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Site</td>
|
|
||||||
<td>Name of the assigned site</td>
|
|
||||||
<td>DC-4</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Group</td>
|
|
||||||
<td>Rack group name (optional)</td>
|
|
||||||
<td>Cage 1400</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>Internal rack name</td>
|
|
||||||
<td>R101</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Facility ID</td>
|
|
||||||
<td>Rack ID assigned by the facility (optional)</td>
|
|
||||||
<td>J12.100</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Tenant</td>
|
|
||||||
<td>Name of tenant (optional)</td>
|
|
||||||
<td>Pied Piper</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Role</td>
|
|
||||||
<td>Functional role (optional)</td>
|
|
||||||
<td>Compute</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Type</td>
|
|
||||||
<td>Rack type (optional)</td>
|
|
||||||
<td>4-post cabinet</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Width</td>
|
|
||||||
<td>Rail-to-rail width (19 or 23 inches)</td>
|
|
||||||
<td>19</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Height</td>
|
|
||||||
<td>Height in rack units</td>
|
|
||||||
<td>42</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Descending units</td>
|
|
||||||
<td>Units are numbered top-to-bottom</td>
|
|
||||||
<td>False</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,81 +0,0 @@
|
|||||||
{% extends '_base.html' %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Site Import{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>Site Import</h1>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<form action="." method="post" class="form">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% render_form form %}
|
|
||||||
<div class="form-group">
|
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
|
||||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>Site's proper name</td>
|
|
||||||
<td>ASH-4 South</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Slug</td>
|
|
||||||
<td>URL-friendly name</td>
|
|
||||||
<td>ash4-south</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Region</td>
|
|
||||||
<td>Name of region (optional)</td>
|
|
||||||
<td>North America</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Tenant</td>
|
|
||||||
<td>Name of tenant (optional)</td>
|
|
||||||
<td>Pied Piper</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Facility</td>
|
|
||||||
<td>Name of the hosting facility (optional)</td>
|
|
||||||
<td>Equinix DC6</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>ASN</td>
|
|
||||||
<td>Autonomous system number (optional)</td>
|
|
||||||
<td>65000</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Contact Name</td>
|
|
||||||
<td>Name of administrative contact (optional)</td>
|
|
||||||
<td>Hank Hill</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Contact Phone</td>
|
|
||||||
<td>Phone number (optional)</td>
|
|
||||||
<td>+1-214-555-1234</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Contact E-mail</td>
|
|
||||||
<td>E-mail address (optional)</td>
|
|
||||||
<td>hhill@example.com</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>ASH-4 South,ash4-south,North America,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,40 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}Aggregate Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Prefix</td>
|
|
||||||
<td>IPv4 or IPv6 network</td>
|
|
||||||
<td>172.16.0.0/12</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>RIR</td>
|
|
||||||
<td>Name of RIR</td>
|
|
||||||
<td>RFC 1918</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Date Added</td>
|
|
||||||
<td>Date in YYYY-MM-DD format (optional)</td>
|
|
||||||
<td>2016-02-23</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Description</td>
|
|
||||||
<td>Short description (optional)</td>
|
|
||||||
<td>Private IPv4 space</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,60 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}IP Address Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Address</td>
|
|
||||||
<td>IPv4 or IPv6 address</td>
|
|
||||||
<td>192.0.2.42/24</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>VRF</td>
|
|
||||||
<td>VRF route distinguisher (optional)</td>
|
|
||||||
<td>65000:123</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Tenant</td>
|
|
||||||
<td>Name of tenant (optional)</td>
|
|
||||||
<td>ABC01</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Status</td>
|
|
||||||
<td>Current status</td>
|
|
||||||
<td>Active</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Device</td>
|
|
||||||
<td>Device name (optional)</td>
|
|
||||||
<td>switch12</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Interface</td>
|
|
||||||
<td>Interface name (optional)</td>
|
|
||||||
<td>ge-0/0/31</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Is Primary</td>
|
|
||||||
<td>If "true", IP will be primary for device (optional)</td>
|
|
||||||
<td>True</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Description</td>
|
|
||||||
<td>Short description (optional)</td>
|
|
||||||
<td>Management IP</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,70 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}Prefix Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Prefix</td>
|
|
||||||
<td>IPv4 or IPv6 network</td>
|
|
||||||
<td>192.168.42.0/24</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>VRF</td>
|
|
||||||
<td>VRF route distinguisher (optional)</td>
|
|
||||||
<td>65000:123</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Tenant</td>
|
|
||||||
<td>Name of tenant (optional)</td>
|
|
||||||
<td>ABC01</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Site</td>
|
|
||||||
<td>Name of assigned site (optional)</td>
|
|
||||||
<td>HQ</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>VLAN Group</td>
|
|
||||||
<td>Name of group for VLAN selection (optional)</td>
|
|
||||||
<td>Customers</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>VLAN ID</td>
|
|
||||||
<td>Numeric VLAN ID (optional)</td>
|
|
||||||
<td>801</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Status</td>
|
|
||||||
<td>Current status</td>
|
|
||||||
<td>Active</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Role</td>
|
|
||||||
<td>Functional role (optional)</td>
|
|
||||||
<td>Customer</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Is a pool</td>
|
|
||||||
<td>True if all IPs are considered usable</td>
|
|
||||||
<td>False</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Description</td>
|
|
||||||
<td>Short description (optional)</td>
|
|
||||||
<td>7th floor WiFi</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,60 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}VLAN Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Site</td>
|
|
||||||
<td>Name of assigned site (optional)</td>
|
|
||||||
<td>LAS2</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Group</td>
|
|
||||||
<td>Name of VLAN group (optional)</td>
|
|
||||||
<td>Backend Network</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>ID</td>
|
|
||||||
<td>Configured VLAN ID</td>
|
|
||||||
<td>1400</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>Configured VLAN name</td>
|
|
||||||
<td>Cameras</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Tenant</td>
|
|
||||||
<td>Name of tenant (optional)</td>
|
|
||||||
<td>Internal</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Status</td>
|
|
||||||
<td>Current status</td>
|
|
||||||
<td>Active</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Role</td>
|
|
||||||
<td>Functional role (optional)</td>
|
|
||||||
<td>Security</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Description</td>
|
|
||||||
<td>Short description (optional)</td>
|
|
||||||
<td>Security team only</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,45 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}VRF Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>Name of VRF</td>
|
|
||||||
<td>Customer_ABC</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>RD</td>
|
|
||||||
<td>Route distinguisher</td>
|
|
||||||
<td>65000:123456</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Tenant</td>
|
|
||||||
<td>Name of tenant (optional)</td>
|
|
||||||
<td>ABC01</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Enforce uniqueness</td>
|
|
||||||
<td>Prevent duplicate prefixes/IP addresses</td>
|
|
||||||
<td>True</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Description</td>
|
|
||||||
<td>Short description (optional)</td>
|
|
||||||
<td>Native VRF for customer ABC</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,40 +0,0 @@
|
|||||||
{% extends 'utilities/obj_import.html' %}
|
|
||||||
|
|
||||||
{% block title %}Tenant Import{% endblock %}
|
|
||||||
|
|
||||||
{% block instructions %}
|
|
||||||
<h4>CSV Format</h4>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Example</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>Tenant name</td>
|
|
||||||
<td>WIDG01</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Slug</td>
|
|
||||||
<td>URL-friendly name</td>
|
|
||||||
<td>widg01</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Group</td>
|
|
||||||
<td>Tenant group (optional)</td>
|
|
||||||
<td>Customers</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Description</td>
|
|
||||||
<td>Long-form name or other text (optional)</td>
|
|
||||||
<td>Widgets Inc.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h4>Example</h4>
|
|
||||||
<pre>WIDG01,widg01,Customers,Widgets Inc.</pre>
|
|
||||||
{% endblock %}
|
|
@ -1,10 +1,12 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{% block title %}{% endblock %}</h1>
|
<h1>{% block title %}{{ obj_type|bettertitle }} Import{% endblock %}</h1>
|
||||||
|
{% block tabs %}{% endblock %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-7">
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
<div class="panel panel-danger">
|
<div class="panel panel-danger">
|
||||||
<div class="panel-heading"><strong>Errors</strong></div>
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
@ -26,8 +28,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-5">
|
||||||
{% block instructions %}{% endblock %}
|
{% if fields %}
|
||||||
|
<h4 class="text-center">CSV Format</h4>
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Required</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
{% for name, field in fields.items %}
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ name }}</code></td>
|
||||||
|
<td>{% if field.required %}<i class="glyphicon glyphicon-ok" title="Required"></i>{% endif %}</td>
|
||||||
|
<td>
|
||||||
|
{{ field.help_text|default:field.label }}
|
||||||
|
{% if field.choices %}
|
||||||
|
<br /><small class="text-muted">Choices: {{ field.choices|example_choices }}</small>
|
||||||
|
{% elif field|widget_type == 'dateinput' %}
|
||||||
|
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
||||||
|
{% elif field|widget_type == 'checkboxinput' %}
|
||||||
|
<br /><small class="text-muted">Specify "true" or "false"</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -5,8 +5,7 @@ from django.db.models import Count
|
|||||||
|
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
|
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField, SlugField,
|
||||||
FilterChoiceField, SlugField,
|
|
||||||
)
|
)
|
||||||
from .models import Tenant, TenantGroup
|
from .models import Tenant, TenantGroup
|
||||||
|
|
||||||
@ -36,17 +35,25 @@ class TenantForm(BootstrapMixin, CustomFieldForm):
|
|||||||
fields = ['name', 'slug', 'group', 'description', 'comments']
|
fields = ['name', 'slug', 'group', 'description', 'comments']
|
||||||
|
|
||||||
|
|
||||||
class TenantFromCSVForm(forms.ModelForm):
|
class TenantCSVForm(forms.ModelForm):
|
||||||
group = forms.ModelChoiceField(TenantGroup.objects.all(), required=False, to_field_name='name',
|
slug = SlugField()
|
||||||
error_messages={'invalid_choice': 'Group not found.'})
|
group = forms.ModelChoiceField(
|
||||||
|
queryset=TenantGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of parent group',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Group not found.'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tenant
|
model = Tenant
|
||||||
fields = ['name', 'slug', 'group', 'description']
|
fields = ['name', 'slug', 'group', 'description', 'comments']
|
||||||
|
help_texts = {
|
||||||
|
'name': 'Tenant name',
|
||||||
class TenantImportForm(BootstrapMixin, BulkImportForm):
|
'comments': 'Free-form comments'
|
||||||
csv = CSVDataField(csv_form=TenantFromCSVForm)
|
}
|
||||||
|
|
||||||
|
|
||||||
class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
@ -97,9 +97,8 @@ class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class TenantBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class TenantBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'tenancy.add_tenant'
|
permission_required = 'tenancy.add_tenant'
|
||||||
form = forms.TenantImportForm
|
model_form = forms.TenantCSVForm
|
||||||
table = tables.TenantTable
|
table = tables.TenantTable
|
||||||
template_name = 'tenancy/tenant_import.html'
|
|
||||||
default_return_url = 'tenancy:tenant_list'
|
default_return_url = 'tenancy:tenant_list'
|
||||||
|
|
||||||
|
|
||||||
|
@ -217,45 +217,79 @@ class Livesearch(forms.TextInput):
|
|||||||
|
|
||||||
class CSVDataField(forms.CharField):
|
class CSVDataField(forms.CharField):
|
||||||
"""
|
"""
|
||||||
A field for comma-separated values (CSV). Values containing commas should be encased within double quotes. Example:
|
A CharField (rendered as a Textarea) which accepts CSV-formatted data. It returns a list of dictionaries mapping
|
||||||
'"New York, NY",new-york-ny,Other stuff' => ['New York, NY', 'new-york-ny', 'Other stuff']
|
column headers to values. Each dictionary represents an individual record.
|
||||||
"""
|
"""
|
||||||
csv_form = None
|
|
||||||
widget = forms.Textarea
|
widget = forms.Textarea
|
||||||
|
|
||||||
def __init__(self, csv_form, *args, **kwargs):
|
def __init__(self, fields, required_fields=[], *args, **kwargs):
|
||||||
self.csv_form = csv_form
|
|
||||||
self.columns = self.csv_form().fields.keys()
|
self.fields = fields
|
||||||
|
self.required_fields = required_fields
|
||||||
|
|
||||||
super(CSVDataField, self).__init__(*args, **kwargs)
|
super(CSVDataField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.strip = False
|
self.strip = False
|
||||||
if not self.label:
|
if not self.label:
|
||||||
self.label = 'CSV Data'
|
self.label = 'CSV Data'
|
||||||
|
if not self.initial:
|
||||||
|
self.initial = ','.join(required_fields) + '\n'
|
||||||
if not self.help_text:
|
if not self.help_text:
|
||||||
self.help_text = 'Enter one line per record in CSV format.'
|
self.help_text = 'Enter the list of column headers followed by one line per record to be imported, using ' \
|
||||||
|
'commas to separate values. Multi-line data and values containing commas may be wrapped ' \
|
||||||
|
'in double quotes.'
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
"""
|
|
||||||
Return a list of dictionaries, each representing an individual record
|
|
||||||
"""
|
|
||||||
# Python 2's csv module has problems with Unicode
|
# Python 2's csv module has problems with Unicode
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
value = value.encode('utf-8')
|
value = value.encode('utf-8')
|
||||||
|
|
||||||
records = []
|
records = []
|
||||||
reader = csv.reader(value.splitlines())
|
reader = csv.reader(value.splitlines())
|
||||||
|
|
||||||
|
# Consume and valdiate the first line of CSV data as column headers
|
||||||
|
headers = reader.next()
|
||||||
|
for f in self.required_fields:
|
||||||
|
if f not in headers:
|
||||||
|
raise forms.ValidationError('Required column header "{}" not found.'.format(f))
|
||||||
|
for f in headers:
|
||||||
|
if f not in self.fields:
|
||||||
|
raise forms.ValidationError('Unexpected column header "{}" found.'.format(f))
|
||||||
|
|
||||||
|
# Parse CSV data
|
||||||
for i, row in enumerate(reader, start=1):
|
for i, row in enumerate(reader, start=1):
|
||||||
if row:
|
if row:
|
||||||
if len(row) < len(self.columns):
|
if len(row) != len(headers):
|
||||||
raise forms.ValidationError("Line {}: Field(s) missing (found {}; expected {})"
|
raise forms.ValidationError(
|
||||||
.format(i, len(row), len(self.columns)))
|
"Row {}: Expected {} columns but found {}".format(i, len(headers), len(row))
|
||||||
elif len(row) > len(self.columns):
|
)
|
||||||
raise forms.ValidationError("Line {}: Too many fields (found {}; expected {})"
|
|
||||||
.format(i, len(row), len(self.columns)))
|
|
||||||
row = [col.strip() for col in row]
|
row = [col.strip() for col in row]
|
||||||
record = dict(zip(self.columns, row))
|
record = dict(zip(headers, row))
|
||||||
records.append(record)
|
records.append(record)
|
||||||
|
|
||||||
return records
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
class CSVChoiceField(forms.ChoiceField):
|
||||||
|
"""
|
||||||
|
Invert the provided set of choices to take the human-friendly label as input, and return the database value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, choices, *args, **kwargs):
|
||||||
|
super(CSVChoiceField, self).__init__(choices, *args, **kwargs)
|
||||||
|
self.choices = [(label, label) for value, label in choices]
|
||||||
|
self.choice_values = {label: value for value, label in choices}
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
value = super(CSVChoiceField, self).clean(value)
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
if value not in self.choice_values:
|
||||||
|
raise forms.ValidationError("Invalid choice: {}".format(value))
|
||||||
|
return self.choice_values[value]
|
||||||
|
|
||||||
|
|
||||||
class ExpandableNameField(forms.CharField):
|
class ExpandableNameField(forms.CharField):
|
||||||
"""
|
"""
|
||||||
A field which allows for numeric range expansion
|
A field which allows for numeric range expansion
|
||||||
@ -483,28 +517,3 @@ class BulkEditForm(forms.Form):
|
|||||||
self.nullable_fields = [field for field in self.Meta.nullable_fields]
|
self.nullable_fields = [field for field in self.Meta.nullable_fields]
|
||||||
else:
|
else:
|
||||||
self.nullable_fields = []
|
self.nullable_fields = []
|
||||||
|
|
||||||
|
|
||||||
class BulkImportForm(forms.Form):
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
records = self.cleaned_data.get('csv')
|
|
||||||
if not records:
|
|
||||||
return
|
|
||||||
|
|
||||||
obj_list = []
|
|
||||||
|
|
||||||
for i, record in enumerate(records, start=1):
|
|
||||||
obj_form = self.fields['csv'].csv_form(data=record)
|
|
||||||
if obj_form.is_valid():
|
|
||||||
obj = obj_form.save(commit=False)
|
|
||||||
obj_list.append(obj)
|
|
||||||
else:
|
|
||||||
for field, errors in obj_form.errors.items():
|
|
||||||
for e in errors:
|
|
||||||
if field == '__all__':
|
|
||||||
self.add_error('csv', "Record {}: {}".format(i, e))
|
|
||||||
else:
|
|
||||||
self.add_error('csv', "Record {} ({}): {}".format(i, field, e))
|
|
||||||
|
|
||||||
self.cleaned_data['csv'] = obj_list
|
|
||||||
|
@ -40,7 +40,9 @@ def widget_type(field):
|
|||||||
"""
|
"""
|
||||||
Return the widget type
|
Return the widget type
|
||||||
"""
|
"""
|
||||||
try:
|
if hasattr(field, 'widget'):
|
||||||
|
return field.widget.__class__.__name__.lower()
|
||||||
|
elif hasattr(field, 'field'):
|
||||||
return field.field.widget.__class__.__name__.lower()
|
return field.field.widget.__class__.__name__.lower()
|
||||||
except AttributeError:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
@ -60,6 +62,22 @@ def bettertitle(value):
|
|||||||
return ' '.join([w[0].upper() + w[1:] for w in value.split()])
|
return ' '.join([w[0].upper() + w[1:] for w in value.split()])
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter()
|
||||||
|
def example_choices(value, arg=3):
|
||||||
|
"""
|
||||||
|
Returns a number (default: 3) of example choices for a ChoiceFiled (useful for CSV import forms).
|
||||||
|
"""
|
||||||
|
choices = []
|
||||||
|
for id, label in value:
|
||||||
|
if len(choices) == arg:
|
||||||
|
choices.append('etc.')
|
||||||
|
break
|
||||||
|
if not id:
|
||||||
|
continue
|
||||||
|
choices.append(label)
|
||||||
|
return ', '.join(choices) or 'None'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
@ -6,9 +6,10 @@ from django_tables2 import RequestConfig
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import ProtectedError
|
from django.db.models import ProtectedError
|
||||||
from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField
|
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, TypedChoiceField
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template import TemplateSyntaxError
|
from django.template import TemplateSyntaxError
|
||||||
@ -19,6 +20,7 @@ from django.utils.safestring import mark_safe
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
|
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
|
||||||
|
from utilities.forms import BootstrapMixin, CSVDataField
|
||||||
from .error_handlers import handle_protectederror
|
from .error_handlers import handle_protectederror
|
||||||
from .forms import ConfirmationForm
|
from .forms import ConfirmationForm
|
||||||
from .paginator import EnhancedPaginator
|
from .paginator import EnhancedPaginator
|
||||||
@ -371,56 +373,85 @@ class BulkImportView(View):
|
|||||||
"""
|
"""
|
||||||
Import objects in bulk (CSV format).
|
Import objects in bulk (CSV format).
|
||||||
|
|
||||||
form: Form class
|
model_form: The form used to create each imported object
|
||||||
table: The django-tables2 Table used to render the list of imported objects
|
table: The django-tables2 Table used to render the list of imported objects
|
||||||
template_name: The name of the template
|
template_name: The name of the template
|
||||||
default_return_url: The name of the URL to use for the cancel button
|
default_return_url: The name of the URL to use for the cancel button
|
||||||
"""
|
"""
|
||||||
form = None
|
model_form = None
|
||||||
table = None
|
table = None
|
||||||
template_name = None
|
|
||||||
default_return_url = None
|
default_return_url = None
|
||||||
|
template_name = 'utilities/obj_import.html'
|
||||||
|
|
||||||
|
def _import_form(self, *args, **kwargs):
|
||||||
|
|
||||||
|
fields = self.model_form().fields.keys()
|
||||||
|
required_fields = [name for name, field in self.model_form().fields.items() if field.required]
|
||||||
|
|
||||||
|
class ImportForm(BootstrapMixin, Form):
|
||||||
|
csv = CSVDataField(fields=fields, required_fields=required_fields)
|
||||||
|
|
||||||
|
return ImportForm(*args, **kwargs)
|
||||||
|
|
||||||
|
def _save_obj(self, obj_form):
|
||||||
|
"""
|
||||||
|
Provide a hook to modify the object immediately before saving it (e.g. to encrypt secret data).
|
||||||
|
"""
|
||||||
|
return obj_form.save()
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': self.form(),
|
'form': self._import_form(),
|
||||||
|
'fields': self.model_form().fields,
|
||||||
|
'obj_type': self.model_form._meta.model._meta.verbose_name,
|
||||||
'return_url': self.default_return_url,
|
'return_url': self.default_return_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
|
||||||
form = self.form(request.POST)
|
new_objs = []
|
||||||
if form.is_valid():
|
form = self._import_form(request.POST)
|
||||||
new_objs = []
|
|
||||||
try:
|
|
||||||
with transaction.atomic():
|
|
||||||
for obj in form.cleaned_data['csv']:
|
|
||||||
self.save_obj(obj)
|
|
||||||
new_objs.append(obj)
|
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Iterate through CSV data and bind each row to a new model form instance.
|
||||||
|
with transaction.atomic():
|
||||||
|
for row, data in enumerate(form.cleaned_data['csv'], start=1):
|
||||||
|
obj_form = self.model_form(data)
|
||||||
|
if obj_form.is_valid():
|
||||||
|
obj = self._save_obj(obj_form)
|
||||||
|
new_objs.append(obj)
|
||||||
|
else:
|
||||||
|
for field, err in obj_form.errors.items():
|
||||||
|
form.add_error('csv', "Row {} {}: {}".format(row, field, err[0]))
|
||||||
|
raise ValidationError("")
|
||||||
|
|
||||||
|
# Compile a table containing the imported objects
|
||||||
obj_table = self.table(new_objs)
|
obj_table = self.table(new_objs)
|
||||||
|
|
||||||
if new_objs:
|
if new_objs:
|
||||||
msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
|
msg = 'Imported {} {}'.format(len(new_objs), new_objs[0]._meta.verbose_name_plural)
|
||||||
messages.success(request, msg)
|
messages.success(request, msg)
|
||||||
UserAction.objects.log_import(request.user, ContentType.objects.get_for_model(new_objs[0]), msg)
|
UserAction.objects.log_import(request.user, ContentType.objects.get_for_model(new_objs[0]), msg)
|
||||||
|
|
||||||
return render(request, "import_success.html", {
|
return render(request, "import_success.html", {
|
||||||
'table': obj_table,
|
'table': obj_table,
|
||||||
'return_url': self.default_return_url,
|
'return_url': self.default_return_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
except IntegrityError as e:
|
except ValidationError:
|
||||||
form.add_error('csv', "Record {}: {}".format(len(new_objs) + 1, e.__cause__))
|
pass
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
|
'fields': self.model_form().fields,
|
||||||
|
'obj_type': self.model_form._meta.model._meta.verbose_name,
|
||||||
'return_url': self.default_return_url,
|
'return_url': self.default_return_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
def save_obj(self, obj):
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
|
|
||||||
class BulkEditView(View):
|
class BulkEditView(View):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user