Merge branch 'import_headers' into develop

This commit is contained in:
Jeremy Stretch 2017-06-07 15:54:59 -04:00
commit e06221bc89
35 changed files with 838 additions and 1516 deletions

View File

@ -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):

View File

@ -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'

View File

@ -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):

View File

@ -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):

View File

@ -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')
# #

View File

@ -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'

View File

@ -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):

View File

@ -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)

View File

@ -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'

View File

@ -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)

View File

@ -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'),

View File

@ -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):

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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):

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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
# #

View File

@ -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):
""" """