mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-21 17:38:05 +01:00
Compare commits
88 Commits
v2.0-beta3
...
v2.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43e1e0dbc8 | ||
|
|
a1c12cfd77 | ||
|
|
aa6ca21a34 | ||
|
|
a49521d683 | ||
|
|
3be6e5b015 | ||
|
|
ca1725b98c | ||
|
|
d11dfe2ced | ||
|
|
ab30ba1e1b | ||
|
|
7f23cb9bf5 | ||
|
|
c9d3cf301e | ||
|
|
67282882fa | ||
|
|
73bf4f45c3 | ||
|
|
66ae62fb91 | ||
|
|
8bae804508 | ||
|
|
d87acc97c3 | ||
|
|
f9b2c59974 | ||
|
|
a870a3b918 | ||
|
|
008ed34553 | ||
|
|
e239045688 | ||
|
|
ed80bfaf02 | ||
|
|
473b35f9a3 | ||
|
|
45bb7eec0b | ||
|
|
58bb029666 | ||
|
|
0f97478b55 | ||
|
|
9efa70a551 | ||
|
|
ed65721085 | ||
|
|
83688fceb7 | ||
|
|
088f75ba0c | ||
|
|
188cfa08a9 | ||
|
|
f731900e2f | ||
|
|
b89bd24bed | ||
|
|
effda88b51 | ||
|
|
3844f70a4d | ||
|
|
8e333757f9 | ||
|
|
0fb12bcc9c | ||
|
|
44d78ef92a | ||
|
|
ebb6729a26 | ||
|
|
b1bcaa33e7 | ||
|
|
a35f8bddde | ||
|
|
8fbe7ba742 | ||
|
|
f039b0b6e9 | ||
|
|
9ad9ef7957 | ||
|
|
5c7db04465 | ||
|
|
838105fb65 | ||
|
|
5ca87c0f20 | ||
|
|
af4edff370 | ||
|
|
f40c048475 | ||
|
|
77247cccbe | ||
|
|
fcfcd77bfd | ||
|
|
b3667befb4 | ||
|
|
a6cb0e0a96 | ||
|
|
c047f943de | ||
|
|
79089cc47e | ||
|
|
3c631902e1 | ||
|
|
379c24a012 | ||
|
|
4035b87693 | ||
|
|
11d1a8c3cf | ||
|
|
7eb9c8265c | ||
|
|
572beb2311 | ||
|
|
d861d8bfb8 | ||
|
|
6791ff6192 | ||
|
|
9d9de6b2a3 | ||
|
|
1f7ef15ad1 | ||
|
|
16c582ec7a | ||
|
|
de58d0ecca | ||
|
|
010f6c7f1a | ||
|
|
aea5612c39 | ||
|
|
b8b912bdd5 | ||
|
|
17873706b7 | ||
|
|
e0ad2b4555 | ||
|
|
f89d91783b | ||
|
|
3ffe36e5ed | ||
|
|
be393a9d10 | ||
|
|
27eefd8705 | ||
|
|
097e0f38ff | ||
|
|
ce26b566a4 | ||
|
|
0e14bc1e02 | ||
|
|
ce6796ed9b | ||
|
|
c90cecc2fb | ||
|
|
b6bbcb0609 | ||
|
|
23f6832d9c | ||
|
|
88dace75a1 | ||
|
|
8eb140fd65 | ||
|
|
1f09f3d096 | ||
|
|
66be85a41f | ||
|
|
814c11167e | ||
|
|
57ddd5086f | ||
|
|
c171547037 |
@@ -10,7 +10,9 @@ Questions? Comments? Please subscribe to [the netbox-discuss mailing list](https
|
||||
|
||||
### Build Status
|
||||
|
||||
| | python 2.7 |
|
||||
NetBox is built against both Python 2.7 and 3.5. Python 3.5 is recommended.
|
||||
|
||||
| | status |
|
||||
|-------------|------------|
|
||||
| **master** | [](https://travis-ci.org/digitalocean/netbox) |
|
||||
| **develop** | [](https://travis-ci.org/digitalocean/netbox) |
|
||||
|
||||
@@ -25,7 +25,7 @@ server {
|
||||
|
||||
server_name netbox.example.com;
|
||||
|
||||
access_log off;
|
||||
client_max_body_size 25m;
|
||||
|
||||
location /static/ {
|
||||
alias /opt/netbox/netbox/static/;
|
||||
|
||||
@@ -3,10 +3,11 @@ from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface, Rack, VIRTUAL_IFACE_TYPES
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, Livesearch, SmallTextarea,
|
||||
SlugField,
|
||||
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
|
||||
FilterChoiceField, Livesearch, SmallTextarea, SlugField,
|
||||
)
|
||||
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
@@ -83,12 +84,15 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitForm(BootstrapMixin, CustomFieldForm):
|
||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['cid', 'type', 'provider', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
|
||||
'comments',
|
||||
]
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'install_date': "Format: YYYY-MM-DD",
|
||||
@@ -152,15 +156,16 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# Circuit terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'rack'}
|
||||
)
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains={'site': 'site'},
|
||||
required=False,
|
||||
label='Rack',
|
||||
widget=APISelect(
|
||||
@@ -168,8 +173,9 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = forms.ModelChoiceField(
|
||||
device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains={'site': 'site', 'rack': 'rack'},
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=APISelect(
|
||||
@@ -187,8 +193,11 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
field_to_update='device'
|
||||
)
|
||||
)
|
||||
interface = forms.ModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
interface = ChainedModelChoiceField(
|
||||
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
),
|
||||
chains={'device': 'device'},
|
||||
required=False,
|
||||
label='Interface',
|
||||
widget=APISelect(
|
||||
@@ -212,49 +221,17 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
if instance and instance.interface is not None:
|
||||
initial = kwargs.get('initial', {})
|
||||
initial['rack'] = instance.interface.device.rack
|
||||
initial['device'] = instance.interface.device
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# If an interface has been assigned, initialize rack and device
|
||||
if self.instance.interface:
|
||||
self.initial['rack'] = self.instance.interface.device.rack
|
||||
self.initial['device'] = self.instance.interface.device
|
||||
|
||||
# Limit rack choices
|
||||
if self.is_bound:
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Limit device choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
|
||||
elif self.initial.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
|
||||
# Limit interface choices
|
||||
if self.is_bound and self.data.get('device'):
|
||||
interfaces = Interface.objects.filter(device=self.data['device']).exclude(
|
||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||
).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
)
|
||||
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
||||
elif self.initial.get('device'):
|
||||
interfaces = Interface.objects.filter(device=self.initial['device']).exclude(
|
||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||
).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
)
|
||||
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||
else:
|
||||
interfaces = []
|
||||
# Mark connected interfaces as disabled
|
||||
self.fields['interface'].choices = [
|
||||
(iface.id, {
|
||||
'label': iface.name,
|
||||
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
|
||||
}) for iface in interfaces
|
||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface'].queryset
|
||||
]
|
||||
|
||||
@@ -79,7 +79,13 @@ class CircuitSearchTable(SearchTable):
|
||||
cid = tables.LinkColumn(verbose_name='ID')
|
||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
a_side = tables.LinkColumn(
|
||||
'dcim:site', accessor=Accessor('termination_a.site'), args=[Accessor('termination_a.site.slug')]
|
||||
)
|
||||
z_side = tables.LinkColumn(
|
||||
'dcim:site', accessor=Accessor('termination_z.site'), args=[Accessor('termination_z.site.slug')]
|
||||
)
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Circuit
|
||||
fields = ('cid', 'type', 'provider', 'tenant', 'description')
|
||||
fields = ('cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
||||
|
||||
@@ -608,6 +608,7 @@ class InterfaceSerializer(serializers.ModelSerializer):
|
||||
class PeerInterfaceSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
|
||||
@@ -8,9 +8,9 @@ from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES,
|
||||
)
|
||||
|
||||
|
||||
@@ -373,10 +373,6 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
status = django_filters.BooleanFilter(
|
||||
name='status',
|
||||
label='Status',
|
||||
)
|
||||
is_console_server = django_filters.BooleanFilter(
|
||||
name='device_type__is_console_server',
|
||||
label='Is a console server',
|
||||
@@ -393,6 +389,9 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='_has_primary_ip',
|
||||
label='Has a primary IP',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=STATUS_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
|
||||
@@ -8,11 +8,13 @@ from django.db.models import Count, Q
|
||||
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
|
||||
CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
|
||||
SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
|
||||
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||
BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField, ExpandableNameField,
|
||||
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||
FilterTreeNodeMultipleChoiceField,
|
||||
)
|
||||
|
||||
from .formfields import MACAddressFormField
|
||||
@@ -80,7 +82,7 @@ class RegionForm(BootstrapMixin, forms.ModelForm):
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteForm(BootstrapMixin, CustomFieldForm):
|
||||
class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
@@ -88,8 +90,8 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||
'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
'name', 'slug', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'physical_address',
|
||||
'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
]
|
||||
widgets = {
|
||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||
@@ -184,16 +186,23 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm):
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackForm(BootstrapMixin, CustomFieldForm):
|
||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group', widget=APISelect(
|
||||
api_url='/api/dcim/rack-groups/?site_id={{site}}',
|
||||
))
|
||||
class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
group = ChainedModelChoiceField(
|
||||
queryset=RackGroup.objects.all(),
|
||||
chains={'site': 'site'},
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/rack-groups/?site_id={{site}}',
|
||||
)
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
|
||||
'comments']
|
||||
fields = [
|
||||
'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'type', 'width', 'u_height',
|
||||
'desc_units', 'comments',
|
||||
]
|
||||
help_texts = {
|
||||
'site': "The site at which the rack exists",
|
||||
'name': "Organizational rack name",
|
||||
@@ -204,18 +213,6 @@ class RackForm(BootstrapMixin, CustomFieldForm):
|
||||
'site': forms.Select(attrs={'filter-for': 'group'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(RackForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Limit rack group choices
|
||||
if self.is_bound and self.data.get('site'):
|
||||
self.fields['group'].queryset = RackGroup.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['group'].queryset = RackGroup.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['group'].choices = []
|
||||
|
||||
|
||||
class RackFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||
@@ -271,6 +268,7 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
|
||||
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
|
||||
u_height = forms.IntegerField(required=False, label='Height (U)')
|
||||
desc_units = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Descending units')
|
||||
comments = CommentField(widget=SmallTextarea)
|
||||
|
||||
class Meta:
|
||||
@@ -374,7 +372,13 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
|
||||
u_height = forms.IntegerField(min_value=1, required=False)
|
||||
is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
|
||||
interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
|
||||
is_console_server = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
|
||||
is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU')
|
||||
is_network_device = forms.NullBooleanField(
|
||||
required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
@@ -483,6 +487,7 @@ class InterfaceTemplateCreateForm(DeviceComponentForm):
|
||||
class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
|
||||
|
||||
class Meta:
|
||||
nullable_fields = []
|
||||
@@ -530,48 +535,77 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'position'}
|
||||
))
|
||||
position = forms.TypedChoiceField(required=False, empty_value=None,
|
||||
help_text="The lowest-numbered unit occupied by the device",
|
||||
widget=APISelect(api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}',
|
||||
disabled_indicator='device'))
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
|
||||
widget=forms.Select(attrs={'filter-for': 'device_type'}))
|
||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Device type', widget=APISelect(
|
||||
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
|
||||
display_field='model'
|
||||
))
|
||||
class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'rack'}
|
||||
)
|
||||
)
|
||||
rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains={'site': 'site'},
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'position'}
|
||||
)
|
||||
)
|
||||
position = forms.TypedChoiceField(
|
||||
required=False,
|
||||
empty_value=None,
|
||||
help_text="The lowest-numbered unit occupied by the device",
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}',
|
||||
disabled_indicator='device'
|
||||
)
|
||||
)
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'device_type'}
|
||||
)
|
||||
)
|
||||
device_type = ChainedModelChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
chains={'manufacturer': 'manufacturer'},
|
||||
label='Device type',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
|
||||
display_field='model'
|
||||
)
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['name', 'device_role', 'tenant', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position',
|
||||
'face', 'status', 'platform', 'primary_ip4', 'primary_ip6', 'comments']
|
||||
fields = [
|
||||
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'status',
|
||||
'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments',
|
||||
]
|
||||
help_texts = {
|
||||
'device_role': "The function this device serves",
|
||||
'serial': "Chassis serial number",
|
||||
}
|
||||
widgets = {
|
||||
'face': forms.Select(attrs={'filter-for': 'position'}),
|
||||
'manufacturer': forms.Select(attrs={'filter-for': 'device_type'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
# Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
|
||||
if instance and hasattr(instance, 'device_type'):
|
||||
initial = kwargs.get('initial', {})
|
||||
initial['manufacturer'] = instance.device_type.manufacturer
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super(DeviceForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if self.instance.pk:
|
||||
|
||||
# Initialize helper selections
|
||||
self.initial['site'] = self.instance.site
|
||||
self.initial['manufacturer'] = self.instance.device_type.manufacturer
|
||||
|
||||
# Compile list of choices for primary IPv4 and IPv6 addresses
|
||||
for family in [4, 6]:
|
||||
ip_choices = []
|
||||
@@ -594,14 +628,6 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
self.fields['primary_ip6'].choices = []
|
||||
self.fields['primary_ip6'].widget.attrs['readonly'] = True
|
||||
|
||||
# Limit rack choices
|
||||
if self.is_bound and self.data.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Rack position
|
||||
pk = self.instance.pk if self.instance.pk else None
|
||||
try:
|
||||
@@ -622,16 +648,6 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
}) for p in position_choices
|
||||
]
|
||||
|
||||
# Limit device_type choices
|
||||
if self.is_bound:
|
||||
self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer__pk=self.data['manufacturer'])\
|
||||
.select_related('manufacturer')
|
||||
elif self.initial.get('manufacturer'):
|
||||
self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer=self.initial['manufacturer'])\
|
||||
.select_related('manufacturer')
|
||||
else:
|
||||
self.fields['device_type'].choices = []
|
||||
|
||||
# Disable rack assignment if this is a child device installed in a parent device
|
||||
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
|
||||
self.fields['site'].disabled = True
|
||||
@@ -641,15 +657,24 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
|
||||
class BaseDeviceFromCSVForm(forms.ModelForm):
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid device role.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid manufacturer.'})
|
||||
device_role = forms.ModelChoiceField(
|
||||
queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid device role.'}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'}
|
||||
)
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid manufacturer.'}
|
||||
)
|
||||
model_name = forms.CharField()
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid platform.'})
|
||||
platform = forms.ModelChoiceField(
|
||||
queryset=Platform.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid platform.'}
|
||||
)
|
||||
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in STATUS_CHOICES])
|
||||
|
||||
class Meta:
|
||||
fields = []
|
||||
@@ -667,17 +692,24 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
|
||||
except DeviceType.DoesNotExist:
|
||||
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
|
||||
|
||||
def clean_status_name(self):
|
||||
return dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
|
||||
|
||||
class DeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
|
||||
'invalid_choice': 'Invalid site name.',
|
||||
})
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(), to_field_name='name', error_messages={
|
||||
'invalid_choice': 'Invalid site name.',
|
||||
}
|
||||
)
|
||||
rack_name = forms.CharField(required=False)
|
||||
face = forms.CharField(required=False)
|
||||
|
||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
|
||||
'site', 'rack_name', 'position', 'face']
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
|
||||
'status_name', 'site', 'rack_name', 'position', 'face',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -719,8 +751,8 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
|
||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'parent',
|
||||
'device_bay_name',
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
|
||||
'status_name', 'parent', 'device_bay_name',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
@@ -764,6 +796,13 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
nullable_fields = ['tenant', 'platform']
|
||||
|
||||
|
||||
def device_status_choices():
|
||||
status_counts = {}
|
||||
for status in Device.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||
status_counts[status['status']] = status['count']
|
||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in STATUS_CHOICES]
|
||||
|
||||
|
||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Device
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
@@ -775,6 +814,10 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')),
|
||||
label='Rack group',
|
||||
)
|
||||
rack_id = FilterChoiceField(
|
||||
queryset=Rack.objects.annotate(filter_count=Count('devices')),
|
||||
label='Rack',
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
|
||||
to_field_name='slug',
|
||||
@@ -783,10 +826,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
|
||||
null_option=(0, 'None'),
|
||||
)
|
||||
manufacturer_id = FilterChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer',
|
||||
)
|
||||
manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
|
||||
device_type_id = FilterChoiceField(
|
||||
queryset=DeviceType.objects.select_related('manufacturer').order_by('model').annotate(
|
||||
filter_count=Count('instances'),
|
||||
@@ -798,14 +838,8 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None'),
|
||||
)
|
||||
status = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=forms.Select(choices=FORM_STATUS_CHOICES),
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label='MAC address',
|
||||
)
|
||||
status = forms.MultipleChoiceField(choices=device_status_choices, required=False)
|
||||
mac_address = forms.CharField(required=False, label='MAC address')
|
||||
|
||||
|
||||
#
|
||||
@@ -913,21 +947,23 @@ class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.HiddenInput(),
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains={'site': 'site'},
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'console_server', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
console_server = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
console_server = ChainedModelChoiceField(
|
||||
queryset=Device.objects.filter(device_type__is_console_server=True),
|
||||
chains={'site': 'site', 'rack': 'rack'},
|
||||
label='Console Server',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -945,11 +981,12 @@ class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
field_to_update='console_server',
|
||||
)
|
||||
)
|
||||
cs_port = forms.ModelChoiceField(
|
||||
cs_port = ChainedModelChoiceField(
|
||||
queryset=ConsoleServerPort.objects.all(),
|
||||
chains={'device': 'console_server'},
|
||||
label='Port',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/console-server-ports/?device_id={{device}}',
|
||||
api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
|
||||
disabled_indicator='connected_console',
|
||||
)
|
||||
)
|
||||
@@ -969,32 +1006,6 @@ class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
if not self.instance.pk:
|
||||
raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
|
||||
|
||||
# Initialize rack choices if site is set
|
||||
if self.initial.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Initialize console_server choices if rack or site is set
|
||||
if self.initial.get('rack'):
|
||||
self.fields['console_server'].queryset = Device.objects.filter(
|
||||
rack=self.initial['rack'], device_type__is_console_server=True
|
||||
)
|
||||
elif self.initial.get('site'):
|
||||
self.fields['console_server'].queryset = Device.objects.filter(
|
||||
site=self.initial['site'], rack__isnull=True, device_type__is_console_server=True
|
||||
)
|
||||
else:
|
||||
self.fields['console_server'].choices = []
|
||||
|
||||
# Initialize CS port choices if console_server is set
|
||||
if self.initial.get('console_server'):
|
||||
self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(
|
||||
device=self.initial['console_server']
|
||||
)
|
||||
else:
|
||||
self.fields['cs_port'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
@@ -1014,21 +1025,23 @@ class ConsoleServerPortCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
|
||||
class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.HiddenInput(),
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains={'site': 'site'},
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = forms.ModelChoiceField(
|
||||
device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains={'site': 'site', 'rack': 'rack'},
|
||||
label='Device',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -1046,8 +1059,9 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
|
||||
field_to_update='device'
|
||||
)
|
||||
)
|
||||
port = forms.ModelChoiceField(
|
||||
port = ChainedModelChoiceField(
|
||||
queryset=ConsolePort.objects.all(),
|
||||
chains={'device': 'device'},
|
||||
label='Port',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/console-ports/?device_id={{device}}',
|
||||
@@ -1069,30 +1083,6 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Initialize rack choices if site is set
|
||||
if self.initial.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Initialize device choices if rack or site is set
|
||||
if self.initial.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['device'].queryset = Device.objects.filter(site=self.initial['site'], rack__isnull=True)
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
|
||||
# Initialize port choices if device is set
|
||||
if self.initial.get('device'):
|
||||
self.fields['port'].queryset = ConsolePort.objects.filter(device=self.initial['device'])
|
||||
else:
|
||||
self.fields['port'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
@@ -1184,18 +1174,20 @@ class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.HiddenInput())
|
||||
rack = forms.ModelChoiceField(
|
||||
rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains={'site': 'site'},
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'pdu', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
pdu = forms.ModelChoiceField(
|
||||
pdu = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains={'site': 'site', 'rack': 'rack'},
|
||||
label='PDU',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -1213,11 +1205,12 @@ class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
field_to_update='pdu'
|
||||
)
|
||||
)
|
||||
power_outlet = forms.ModelChoiceField(
|
||||
power_outlet = ChainedModelChoiceField(
|
||||
queryset=PowerOutlet.objects.all(),
|
||||
chains={'device': 'device'},
|
||||
label='Outlet',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/power-outlets/?device_id={{device}}',
|
||||
api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
|
||||
disabled_indicator='connected_port'
|
||||
)
|
||||
)
|
||||
@@ -1237,30 +1230,6 @@ class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
if not self.instance.pk:
|
||||
raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
|
||||
|
||||
# Initialize rack choices if site is set
|
||||
if self.initial.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Initialize pdu choices if rack or site is set
|
||||
if self.initial.get('rack'):
|
||||
self.fields['pdu'].queryset = Device.objects.filter(
|
||||
rack=self.initial['rack'], device_type__is_pdu=True
|
||||
)
|
||||
elif self.initial.get('site'):
|
||||
self.fields['pdu'].queryset = Device.objects.filter(
|
||||
site=self.initial['site'], rack__isnull=True, device_type__is_pdu=True
|
||||
)
|
||||
else:
|
||||
self.fields['pdu'].choices = []
|
||||
|
||||
# Initialize power outlet choices if pdu is set
|
||||
if self.initial.get('pdu'):
|
||||
self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device=self.initial['pdu'])
|
||||
else:
|
||||
self.fields['power_outlet'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
@@ -1280,21 +1249,23 @@ class PowerOutletCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
|
||||
class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains={'site': 'site'},
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = forms.ModelChoiceField(
|
||||
device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains={'site': 'site', 'rack': 'rack'},
|
||||
label='Device',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -1312,8 +1283,9 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
|
||||
field_to_update='device'
|
||||
)
|
||||
)
|
||||
port = forms.ModelChoiceField(
|
||||
port = ChainedModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
chains={'device': 'device'},
|
||||
label='Port',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/power-ports/?device_id={{device}}',
|
||||
@@ -1335,30 +1307,6 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Initialize rack choices if site is set
|
||||
if self.initial.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Initialize device choices if rack or site is set
|
||||
if self.initial.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['device'].queryset = Device.objects.filter(site=self.initial['site'], rack__isnull=True)
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
|
||||
# Initialize port choices if device is set
|
||||
if self.initial.get('device'):
|
||||
self.fields['port'].queryset = PowerPort.objects.filter(device=self.initial['device'])
|
||||
else:
|
||||
self.fields['port'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
@@ -1412,6 +1360,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
|
||||
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
@@ -1440,7 +1389,7 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
# Interface connections
|
||||
#
|
||||
|
||||
class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
interface_a = forms.ChoiceField(
|
||||
choices=[],
|
||||
widget=SelectWithDisabled,
|
||||
@@ -1454,8 +1403,9 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
attrs={'filter-for': 'rack_b'}
|
||||
)
|
||||
)
|
||||
rack_b = forms.ModelChoiceField(
|
||||
rack_b = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains={'site': 'site_b'},
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -1463,8 +1413,9 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
attrs={'filter-for': 'device_b', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device_b = forms.ModelChoiceField(
|
||||
device_b = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains={'site': 'site_b', 'rack': 'rack_b'},
|
||||
label='Device',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -1482,8 +1433,11 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
field_to_update='device_b'
|
||||
)
|
||||
)
|
||||
interface_b = forms.ModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
interface_b = ChainedModelChoiceField(
|
||||
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
),
|
||||
chains={'device': 'device_b'},
|
||||
label='Interface',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
|
||||
@@ -1509,31 +1463,9 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
|
||||
]
|
||||
|
||||
# Initialize rack_b choices if site_b is set
|
||||
if self.initial.get('site_b'):
|
||||
self.fields['rack_b'].queryset = Rack.objects.filter(site=self.initial['site_b'])
|
||||
else:
|
||||
self.fields['rack_b'].choices = []
|
||||
|
||||
# Initialize device_b choices if rack_b or site_b is set
|
||||
if self.initial.get('rack_b'):
|
||||
self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
|
||||
elif self.initial.get('site_b'):
|
||||
self.fields['device_b'].queryset = Device.objects.filter(site=self.initial['site_b'], rack__isnull=True)
|
||||
else:
|
||||
self.fields['device_b'].choices = []
|
||||
|
||||
# Initialize interface_b choices if device_b is set
|
||||
if self.initial.get('device_b'):
|
||||
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']).exclude(
|
||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||
).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
)
|
||||
else:
|
||||
device_b_interfaces = []
|
||||
# Mark connected interfaces as disabled
|
||||
self.fields['interface_b'].choices = [
|
||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_b_interfaces
|
||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
|
||||
]
|
||||
|
||||
|
||||
|
||||
27
netbox/dcim/migrations/0035_device_expand_status_choices.py
Normal file
27
netbox/dcim/migrations/0035_device_expand_status_choices.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-05-08 15:57
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0034_rename_module_to_inventoryitem'),
|
||||
]
|
||||
|
||||
# We convert the BooleanField to an IntegerField first as PostgreSQL does not provide a direct cast for boolean to
|
||||
# smallint (attempting to convert directly yields the error "cannot cast type boolean to smallint").
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'),
|
||||
),
|
||||
]
|
||||
25
netbox/dcim/migrations/0036_add_ff_juniper_vcp.py
Normal file
25
netbox/dcim/migrations/0036_add_ff_juniper_vcp.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-05-09 16:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0035_device_expand_status_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
]
|
||||
@@ -102,6 +102,7 @@ IFACE_FF_STACKWISE = 5000
|
||||
IFACE_FF_STACKWISE_PLUS = 5050
|
||||
IFACE_FF_FLEXSTACK = 5100
|
||||
IFACE_FF_FLEXSTACK_PLUS = 5150
|
||||
IFACE_FF_JUNIPER_VCP = 5200
|
||||
# Other
|
||||
IFACE_FF_OTHER = 32767
|
||||
|
||||
@@ -163,6 +164,7 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
|
||||
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
|
||||
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
|
||||
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -178,13 +180,30 @@ VIRTUAL_IFACE_TYPES = [
|
||||
IFACE_FF_LAG,
|
||||
]
|
||||
|
||||
STATUS_ACTIVE = True
|
||||
STATUS_OFFLINE = False
|
||||
STATUS_OFFLINE = 0
|
||||
STATUS_ACTIVE = 1
|
||||
STATUS_PLANNED = 2
|
||||
STATUS_STAGED = 3
|
||||
STATUS_FAILED = 4
|
||||
STATUS_INVENTORY = 5
|
||||
STATUS_CHOICES = [
|
||||
[STATUS_ACTIVE, 'Active'],
|
||||
[STATUS_OFFLINE, 'Offline'],
|
||||
[STATUS_PLANNED, 'Planned'],
|
||||
[STATUS_STAGED, 'Staged'],
|
||||
[STATUS_FAILED, 'Failed'],
|
||||
[STATUS_INVENTORY, 'Inventory'],
|
||||
]
|
||||
|
||||
DEVICE_STATUS_CLASSES = {
|
||||
0: 'warning',
|
||||
1: 'success',
|
||||
2: 'info',
|
||||
3: 'primary',
|
||||
4: 'danger',
|
||||
5: 'default',
|
||||
}
|
||||
|
||||
CONNECTION_STATUS_PLANNED = False
|
||||
CONNECTION_STATUS_CONNECTED = True
|
||||
CONNECTION_STATUS_CHOICES = [
|
||||
@@ -391,7 +410,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name
|
||||
return self.display_name or super(Rack, self).__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rack', args=[self.pk])
|
||||
@@ -448,7 +467,9 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
def display_name(self):
|
||||
if self.facility_id:
|
||||
return u"{} ({})".format(self.name, self.facility_id)
|
||||
return self.name
|
||||
elif self.name:
|
||||
return self.name
|
||||
return u""
|
||||
|
||||
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
|
||||
"""
|
||||
@@ -791,13 +812,13 @@ class InterfaceManager(models.Manager):
|
||||
|
||||
def order_naturally(self, method=IFACE_ORDERING_POSITION):
|
||||
"""
|
||||
Naturally order interfaces by their name and numeric position. The sort method must be one of the defined
|
||||
Naturally order interfaces by their type and numeric position. The sort method must be one of the defined
|
||||
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
|
||||
|
||||
To order interfaces naturally, the `name` field is split into five distinct components: leading text (name),
|
||||
To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
|
||||
slot, subslot, position, channel, and virtual circuit:
|
||||
|
||||
{name}{slot}/{subslot}/{position}:{channel}.{vc}
|
||||
{type}{slot}/{subslot}/{position}:{channel}.{vc}
|
||||
|
||||
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
|
||||
be parsed as follows:
|
||||
@@ -809,16 +830,17 @@ class InterfaceManager(models.Manager):
|
||||
channel = None
|
||||
vc = 0
|
||||
|
||||
The chosen sorting method will determine which fields are ordered first in the query.
|
||||
The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of
|
||||
the prescribed fields.
|
||||
"""
|
||||
queryset = self.get_queryset()
|
||||
sql_col = '{}.name'.format(queryset.model._meta.db_table)
|
||||
ordering = {
|
||||
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_name'),
|
||||
IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel', '_vc'),
|
||||
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
|
||||
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
|
||||
}[method]
|
||||
return queryset.extra(select={
|
||||
'_name': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
||||
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
||||
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
@@ -933,19 +955,26 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
|
||||
name = NullableCharField(max_length=64, blank=True, null=True, unique=True)
|
||||
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
|
||||
asset_tag = NullableCharField(max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
|
||||
help_text='A unique tag used to identify this device')
|
||||
asset_tag = NullableCharField(
|
||||
max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
|
||||
help_text='A unique tag used to identify this device'
|
||||
)
|
||||
site = models.ForeignKey('Site', related_name='devices', on_delete=models.PROTECT)
|
||||
rack = models.ForeignKey('Rack', related_name='devices', blank=True, null=True, on_delete=models.PROTECT)
|
||||
position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)],
|
||||
verbose_name='Position (U)',
|
||||
help_text='The lowest-numbered unit occupied by the device')
|
||||
position = models.PositiveSmallIntegerField(
|
||||
blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)',
|
||||
help_text='The lowest-numbered unit occupied by the device'
|
||||
)
|
||||
face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
|
||||
status = models.BooleanField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
|
||||
primary_ip4 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL,
|
||||
blank=True, null=True, verbose_name='Primary IPv4')
|
||||
primary_ip6 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL,
|
||||
blank=True, null=True, verbose_name='Primary IPv6')
|
||||
status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
|
||||
primary_ip4 = models.OneToOneField(
|
||||
'ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL, blank=True, null=True,
|
||||
verbose_name='Primary IPv4'
|
||||
)
|
||||
primary_ip6 = models.OneToOneField(
|
||||
'ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL, blank=True, null=True,
|
||||
verbose_name='Primary IPv6'
|
||||
)
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
images = GenericRelation(ImageAttachment)
|
||||
@@ -957,7 +986,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
unique_together = ['rack', 'position', 'face']
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name
|
||||
return self.display_name or super(Device, self).__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device', args=[self.pk])
|
||||
@@ -1065,6 +1094,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.platform.name if self.platform else None,
|
||||
self.serial,
|
||||
self.asset_tag,
|
||||
self.get_status_display(),
|
||||
self.site.name,
|
||||
self.rack.name if self.rack else None,
|
||||
self.position,
|
||||
@@ -1075,12 +1105,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
def display_name(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
elif self.position:
|
||||
return u"{} ({} U{})".format(self.device_type, self.rack.name, self.position)
|
||||
elif self.rack:
|
||||
return u"{} ({})".format(self.device_type, self.rack.name)
|
||||
else:
|
||||
return u"{} ({})".format(self.device_type, self.site.name)
|
||||
elif hasattr(self, 'device_type'):
|
||||
return u"{}".format(self.device_type)
|
||||
return u""
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
@@ -1108,6 +1135,9 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
return Device.objects.filter(parent_bay__device=self.pk)
|
||||
|
||||
def get_status_class(self):
|
||||
return DEVICE_STATUS_CLASSES[self.status]
|
||||
|
||||
def get_rpc_client(self):
|
||||
"""
|
||||
Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined.
|
||||
|
||||
@@ -92,12 +92,8 @@ DEVICE_ROLE = """
|
||||
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
|
||||
"""
|
||||
|
||||
STATUS_ICON = """
|
||||
{% if record.status %}
|
||||
<span class="glyphicon glyphicon-ok-sign text-success" title="Active" aria-hidden="true"></span>
|
||||
{% else %}
|
||||
<span class="glyphicon glyphicon-minus-sign text-danger" title="Offline" aria-hidden="true"></span>
|
||||
{% endif %}
|
||||
DEVICE_STATUS = """
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
"""
|
||||
|
||||
DEVICE_PRIMARY_IP = """
|
||||
@@ -432,7 +428,7 @@ class PlatformTable(BaseTable):
|
||||
class DeviceTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
||||
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
|
||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
@@ -452,7 +448,7 @@ class DeviceTable(BaseTable):
|
||||
|
||||
class DeviceSearchTable(SearchTable):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
||||
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
|
||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
@@ -469,6 +465,7 @@ class DeviceSearchTable(SearchTable):
|
||||
|
||||
class DeviceImportTable(BaseTable):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||
@@ -478,7 +475,7 @@ class DeviceImportTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = ('name', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
fields = ('name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
empty_text = False
|
||||
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@ from django.db.models import Count
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
|
||||
from ipam.models import Prefix, Service, VLAN
|
||||
from circuits.models import Circuit
|
||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import (
|
||||
@@ -103,6 +105,9 @@ class ComponentCreateView(View):
|
||||
new_components.append(component_form.save(commit=False))
|
||||
else:
|
||||
for field, errors in component_form.errors.as_data().items():
|
||||
# Assign errors on the child form's name field to name_pattern on the parent form
|
||||
if field == 'name':
|
||||
field = 'name_pattern'
|
||||
for e in errors:
|
||||
form.add_error(field, u'{}: {}'.format(name, ', '.join(e)))
|
||||
|
||||
@@ -892,12 +897,16 @@ def consoleport_connect(request, pk):
|
||||
form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport)
|
||||
if form.is_valid():
|
||||
consoleport = form.save()
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
consoleport.device,
|
||||
consoleport.name,
|
||||
consoleport.cs_port.device,
|
||||
consoleport.cs_port.name,
|
||||
))
|
||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||
consoleport.device.get_absolute_url(),
|
||||
escape(consoleport.device),
|
||||
escape(consoleport.name),
|
||||
consoleport.cs_port.device.get_absolute_url(),
|
||||
escape(consoleport.cs_port.device),
|
||||
escape(consoleport.cs_port.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -921,17 +930,28 @@ def consoleport_disconnect(request, pk):
|
||||
consoleport = get_object_or_404(ConsolePort, pk=pk)
|
||||
|
||||
if not consoleport.cs_port:
|
||||
messages.warning(request, u"Cannot disconnect console port {}: It is not connected to anything."
|
||||
.format(consoleport))
|
||||
messages.warning(
|
||||
request, u"Cannot disconnect console port {}: It is not connected to anything.".format(consoleport)
|
||||
)
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
cs_port = consoleport.cs_port
|
||||
consoleport.cs_port = None
|
||||
consoleport.connection_status = None
|
||||
consoleport.save()
|
||||
messages.success(request, u"Console port {} has been disconnected.".format(consoleport))
|
||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||
consoleport.device.get_absolute_url(),
|
||||
escape(consoleport.device),
|
||||
escape(consoleport.name),
|
||||
cs_port.device.get_absolute_url(),
|
||||
escape(cs_port.device),
|
||||
escape(cs_port.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
return redirect('dcim:device', pk=consoleport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -966,6 +986,7 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
form = forms.ConsoleConnectionImportForm
|
||||
table = tables.ConsoleConnectionTable
|
||||
template_name = 'dcim/console_connections_import.html'
|
||||
default_return_url = 'dcim:console_connections_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -993,12 +1014,16 @@ def consoleserverport_connect(request, pk):
|
||||
consoleport.cs_port = consoleserverport
|
||||
consoleport.connection_status = form.cleaned_data['connection_status']
|
||||
consoleport.save()
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
consoleport.device,
|
||||
consoleport.name,
|
||||
consoleserverport.device,
|
||||
consoleserverport.name,
|
||||
))
|
||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||
consoleport.device.get_absolute_url(),
|
||||
escape(consoleport.device),
|
||||
escape(consoleport.name),
|
||||
consoleserverport.device.get_absolute_url(),
|
||||
escape(consoleserverport.device),
|
||||
escape(consoleserverport.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1022,8 +1047,9 @@ def consoleserverport_disconnect(request, pk):
|
||||
consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
|
||||
|
||||
if not hasattr(consoleserverport, 'connected_console'):
|
||||
messages.warning(request, u"Cannot disconnect console server port {}: Nothing is connected to it."
|
||||
.format(consoleserverport))
|
||||
messages.warning(
|
||||
request, u"Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport)
|
||||
)
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
@@ -1033,7 +1059,16 @@ def consoleserverport_disconnect(request, pk):
|
||||
consoleport.cs_port = None
|
||||
consoleport.connection_status = None
|
||||
consoleport.save()
|
||||
messages.success(request, u"Console server port {} has been disconnected.".format(consoleserverport))
|
||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||
consoleport.device.get_absolute_url(),
|
||||
escape(consoleport.device),
|
||||
escape(consoleport.name),
|
||||
consoleserverport.device.get_absolute_url(),
|
||||
escape(consoleserverport.device),
|
||||
escape(consoleserverport.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, consoleport, msg)
|
||||
return redirect('dcim:device', pk=consoleserverport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1085,12 +1120,16 @@ def powerport_connect(request, pk):
|
||||
form = forms.PowerPortConnectionForm(request.POST, instance=powerport)
|
||||
if form.is_valid():
|
||||
powerport = form.save()
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
powerport.device,
|
||||
powerport.name,
|
||||
powerport.power_outlet.device,
|
||||
powerport.power_outlet.name,
|
||||
))
|
||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||
powerport.device.get_absolute_url(),
|
||||
escape(powerport.device),
|
||||
escape(powerport.name),
|
||||
powerport.power_outlet.device.get_absolute_url(),
|
||||
escape(powerport.power_outlet.device),
|
||||
escape(powerport.power_outlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1114,17 +1153,28 @@ def powerport_disconnect(request, pk):
|
||||
powerport = get_object_or_404(PowerPort, pk=pk)
|
||||
|
||||
if not powerport.power_outlet:
|
||||
messages.warning(request, u"Cannot disconnect power port {}: It is not connected to an outlet."
|
||||
.format(powerport))
|
||||
messages.warning(
|
||||
request, u"Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport)
|
||||
)
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
power_outlet = powerport.power_outlet
|
||||
powerport.power_outlet = None
|
||||
powerport.connection_status = None
|
||||
powerport.save()
|
||||
messages.success(request, u"Power port {} has been disconnected.".format(powerport))
|
||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||
powerport.device.get_absolute_url(),
|
||||
escape(powerport.device),
|
||||
escape(powerport.name),
|
||||
power_outlet.device.get_absolute_url(),
|
||||
escape(power_outlet.device),
|
||||
escape(power_outlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
return redirect('dcim:device', pk=powerport.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1159,6 +1209,7 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
form = forms.PowerConnectionImportForm
|
||||
table = tables.PowerConnectionTable
|
||||
template_name = 'dcim/power_connections_import.html'
|
||||
default_return_url = 'dcim:power_connections_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -1186,12 +1237,16 @@ def poweroutlet_connect(request, pk):
|
||||
powerport.power_outlet = poweroutlet
|
||||
powerport.connection_status = form.cleaned_data['connection_status']
|
||||
powerport.save()
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
powerport.device,
|
||||
powerport.name,
|
||||
poweroutlet.device,
|
||||
poweroutlet.name,
|
||||
))
|
||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||
powerport.device.get_absolute_url(),
|
||||
escape(powerport.device),
|
||||
escape(powerport.name),
|
||||
poweroutlet.device.get_absolute_url(),
|
||||
escape(poweroutlet.device),
|
||||
escape(poweroutlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1215,7 +1270,9 @@ def poweroutlet_disconnect(request, pk):
|
||||
poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
|
||||
|
||||
if not hasattr(poweroutlet, 'connected_port'):
|
||||
messages.warning(request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet))
|
||||
messages.warning(
|
||||
request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet)
|
||||
)
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
@@ -1225,7 +1282,16 @@ def poweroutlet_disconnect(request, pk):
|
||||
powerport.power_outlet = None
|
||||
powerport.connection_status = None
|
||||
powerport.save()
|
||||
messages.success(request, u"Power outlet {} has been disconnected.".format(poweroutlet))
|
||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||
powerport.device.get_absolute_url(),
|
||||
escape(powerport.device),
|
||||
escape(powerport.name),
|
||||
poweroutlet.device.get_absolute_url(),
|
||||
escape(poweroutlet.device),
|
||||
escape(poweroutlet.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, powerport, msg)
|
||||
return redirect('dcim:device', pk=poweroutlet.device.pk)
|
||||
|
||||
else:
|
||||
@@ -1491,13 +1557,19 @@ def interfaceconnection_add(request, pk):
|
||||
if request.method == 'POST':
|
||||
form = forms.InterfaceConnectionForm(device, request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
interfaceconnection = form.save()
|
||||
messages.success(request, u"Connected {} {} to {} {}.".format(
|
||||
interfaceconnection.interface_a.device,
|
||||
interfaceconnection.interface_a,
|
||||
interfaceconnection.interface_b.device,
|
||||
interfaceconnection.interface_b,
|
||||
))
|
||||
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
|
||||
interfaceconnection.interface_a.device.get_absolute_url(),
|
||||
escape(interfaceconnection.interface_a.device),
|
||||
escape(interfaceconnection.interface_a.name),
|
||||
interfaceconnection.interface_b.device.get_absolute_url(),
|
||||
escape(interfaceconnection.interface_b.device),
|
||||
escape(interfaceconnection.interface_b.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, interfaceconnection, msg)
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
|
||||
device_b = interfaceconnection.interface_b.device
|
||||
@@ -1535,12 +1607,16 @@ def interfaceconnection_delete(request, pk):
|
||||
form = forms.InterfaceConnectionDeletionForm(request.POST)
|
||||
if form.is_valid():
|
||||
interfaceconnection.delete()
|
||||
messages.success(request, u"Deleted the connection between {} {} and {} {}.".format(
|
||||
interfaceconnection.interface_a.device,
|
||||
interfaceconnection.interface_a,
|
||||
interfaceconnection.interface_b.device,
|
||||
interfaceconnection.interface_b,
|
||||
))
|
||||
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
|
||||
interfaceconnection.interface_a.device.get_absolute_url(),
|
||||
escape(interfaceconnection.interface_a.device),
|
||||
escape(interfaceconnection.interface_a.name),
|
||||
interfaceconnection.interface_b.device.get_absolute_url(),
|
||||
escape(interfaceconnection.interface_b.device),
|
||||
escape(interfaceconnection.interface_b.name),
|
||||
)
|
||||
messages.success(request, mark_safe(msg))
|
||||
UserAction.objects.log_edit(request.user, interfaceconnection, msg)
|
||||
if form.cleaned_data['device']:
|
||||
return redirect('dcim:device', pk=form.cleaned_data['device'].pk)
|
||||
else:
|
||||
@@ -1570,6 +1646,7 @@ class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView
|
||||
form = forms.InterfaceConnectionImportForm
|
||||
table = tables.InterfaceConnectionTable
|
||||
template_name = 'dcim/interface_connections_import.html'
|
||||
default_return_url = 'dcim:interface_connections_list'
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
from dcim.models import Device, InventoryItem, Site
|
||||
from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -39,7 +39,7 @@ class Command(BaseCommand):
|
||||
self.password = getpass("Password: ")
|
||||
|
||||
# Attempt to inventory only active devices
|
||||
device_list = Device.objects.filter(status=True)
|
||||
device_list = Device.objects.filter(status=STATUS_ACTIVE)
|
||||
|
||||
# --site: Include only devices belonging to specified site(s)
|
||||
if options['site']:
|
||||
@@ -72,7 +72,7 @@ class Command(BaseCommand):
|
||||
|
||||
# Skip inactive devices
|
||||
if not device.status:
|
||||
self.stdout.write("Skipped (inactive)")
|
||||
self.stdout.write("Skipped (not active)")
|
||||
continue
|
||||
|
||||
# Skip devices without primary_ip set
|
||||
|
||||
@@ -13,6 +13,8 @@ from django.template import Template, Context
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from utilities.utils import foreground_color
|
||||
|
||||
|
||||
CUSTOMFIELD_MODELS = (
|
||||
'site', 'rack', 'devicetype', 'device', # DCIM
|
||||
@@ -316,7 +318,7 @@ class TopologyMap(models.Model):
|
||||
def render(self, img_format='png'):
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.models import Device, InterfaceConnection
|
||||
from dcim.models import CONNECTION_STATUS_CONNECTED, Device, InterfaceConnection
|
||||
|
||||
# Construct the graph
|
||||
graph = graphviz.Graph()
|
||||
@@ -336,8 +338,9 @@ class TopologyMap(models.Model):
|
||||
for query in device_set.split(';'): # Split regexes on semicolons
|
||||
devices += Device.objects.filter(name__regex=query).select_related('device_role')
|
||||
for d in devices:
|
||||
fillcolor = '#{}'.format(d.device_role.color)
|
||||
subgraph.node(d.name, style='filled', fillcolor=fillcolor)
|
||||
bg_color = '#{}'.format(d.device_role.color)
|
||||
fg_color = '#{}'.format(foreground_color(d.device_role.color))
|
||||
subgraph.node(d.name, style='filled', fillcolor=bg_color, fontcolor=fg_color, fontname='sans')
|
||||
|
||||
# Add an invisible connection to each successive device in a set to enforce horizontal order
|
||||
for j in range(0, len(devices) - 1):
|
||||
@@ -357,7 +360,8 @@ class TopologyMap(models.Model):
|
||||
interface_a__device__in=devices, interface_b__device__in=devices
|
||||
)
|
||||
for c in connections:
|
||||
graph.edge(c.interface_a.device.name, c.interface_b.device.name)
|
||||
style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style)
|
||||
|
||||
# Add all circuits to the graph
|
||||
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
|
||||
@@ -383,7 +387,7 @@ def image_upload(instance, filename):
|
||||
elif instance.name:
|
||||
filename = instance.name
|
||||
|
||||
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
|
||||
return u'{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@@ -422,6 +426,16 @@ class ImageAttachment(models.Model):
|
||||
# before the request finishes. (For example, to display a message indicating the ImageAttachment was deleted.)
|
||||
self.image.name = _name
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
"""
|
||||
Wrapper around `image.size` to suppress an OSError in case the file is inaccessible.
|
||||
"""
|
||||
try:
|
||||
return self.image.size
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
|
||||
@@ -27,4 +27,4 @@ class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
model = ImageAttachment
|
||||
|
||||
def get_return_url(self, request, imageattachment):
|
||||
return imageattachment.obj.get_absolute_url()
|
||||
return imageattachment.parent.get_absolute_url()
|
||||
|
||||
@@ -9,7 +9,10 @@ from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from .models import (
|
||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
|
||||
VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
||||
)
|
||||
|
||||
|
||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
@@ -153,10 +156,13 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=PREFIX_STATUS_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['family', 'status']
|
||||
fields = ['family']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -237,10 +243,13 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
queryset=Interface.objects.all(),
|
||||
label='Interface (ID)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=IPADDRESS_STATUS_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['family', 'status']
|
||||
fields = ['family']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -337,10 +346,13 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=VLAN_STATUS_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['name', 'vid', 'status']
|
||||
fields = ['name', 'vid']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
||||
@@ -3,10 +3,11 @@ from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Rack, Device, Interface
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch,
|
||||
ReturnURLForm, SlugField, add_blank_choice,
|
||||
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField,
|
||||
ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
@@ -32,11 +33,11 @@ IPADDRESS_MASK_LENGTH_CHOICES = PREFIX_MASK_LENGTH_CHOICES + [(128, 128)]
|
||||
# VRFs
|
||||
#
|
||||
|
||||
class VRFForm(BootstrapMixin, CustomFieldForm):
|
||||
class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant']
|
||||
labels = {
|
||||
'rd': "RD",
|
||||
}
|
||||
@@ -61,6 +62,9 @@ class VRFImportForm(BootstrapMixin, BulkImportForm):
|
||||
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
enforce_unique = forms.NullBooleanField(
|
||||
required=False, widget=BulkEditNullBooleanSelect, label='Enforce unique space'
|
||||
)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
@@ -160,30 +164,27 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
class PrefixForm(BootstrapMixin, CustomFieldForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
||||
widget=forms.Select(attrs={'filter-for': 'vlan', 'nullable': 'true'}))
|
||||
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
|
||||
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
|
||||
display_field='display_name'))
|
||||
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
|
||||
attrs={'filter-for': 'vlan', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
vlan = ChainedModelChoiceField(
|
||||
queryset=VLAN.objects.all(), chains={'site': 'site'}, required=False, label='VLAN', widget=APISelect(
|
||||
api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name'
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'is_pool', 'description']
|
||||
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PrefixForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
# Initialize field without choices to avoid pulling all VLANs from the database
|
||||
if self.is_bound and self.data.get('site'):
|
||||
self.fields['vlan'].queryset = VLAN.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['vlan'].queryset = VLAN.objects.filter(site=None)
|
||||
|
||||
|
||||
class PrefixFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
@@ -211,7 +212,6 @@ class PrefixFromCSVForm(forms.ModelForm):
|
||||
vlan_group_name = self.cleaned_data.get('vlan_group_name')
|
||||
vlan_vid = self.cleaned_data.get('vlan_vid')
|
||||
vlan_group = None
|
||||
vlan = None
|
||||
|
||||
# Validate VLAN group
|
||||
if vlan_group_name:
|
||||
@@ -256,6 +256,7 @@ class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
status = forms.ChoiceField(choices=add_blank_choice(PREFIX_STATUS_CHOICES), required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
is_pool = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a pool')
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
@@ -306,126 +307,183 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
|
||||
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm):
|
||||
interface_site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label='Site',
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'interface_rack'}
|
||||
)
|
||||
)
|
||||
interface_rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(), required=False, label='Rack', widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{interface_site}}', display_field='display_name',
|
||||
interface_rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains={'site': 'interface_site'},
|
||||
required=False,
|
||||
label='Rack',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{interface_site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'interface_device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
interface_device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(), required=False, label='Device', widget=APISelect(
|
||||
interface_device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains={'site': 'interface_site', 'rack': 'interface_rack'},
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{interface_site}}&rack_id={{interface_rack}}',
|
||||
display_field='display_name', attrs={'filter-for': 'interface'}
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'interface'}
|
||||
)
|
||||
)
|
||||
interface = ChainedModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
chains={'device': 'interface_device'},
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/interfaces/?device_id={{interface_device}}'
|
||||
)
|
||||
)
|
||||
nat_site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label='Site',
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'nat_device'}
|
||||
)
|
||||
)
|
||||
nat_device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(), required=False, label='Device', widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{nat_site}}', display_field='display_name',
|
||||
nat_rack = ChainedModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains={'site': 'nat_site'},
|
||||
required=False,
|
||||
label='Rack',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{interface_site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'nat_device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
nat_device = ChainedModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains={'site': 'nat_site'},
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{nat_site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'nat_inside'}
|
||||
)
|
||||
)
|
||||
livesearch = forms.CharField(
|
||||
required=False, label='IP Address', widget=Livesearch(
|
||||
query_key='q', query_url='ipam-api:ipaddress-list', field_to_update='nat_inside', obj_label='address'
|
||||
nat_inside = ChainedModelChoiceField(
|
||||
queryset=IPAddress.objects.all(),
|
||||
chains={'interface__device': 'nat_device'},
|
||||
required=False,
|
||||
label='IP Address',
|
||||
widget=APISelect(
|
||||
api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
|
||||
display_field='address'
|
||||
)
|
||||
)
|
||||
livesearch = forms.CharField(
|
||||
required=False,
|
||||
label='IP Address',
|
||||
widget=Livesearch(
|
||||
query_key='q',
|
||||
query_url='ipam-api:ipaddress-list',
|
||||
field_to_update='nat_inside',
|
||||
obj_label='address'
|
||||
)
|
||||
)
|
||||
primary_for_device = forms.BooleanField(required=False, label='Make this the primary IP for the device')
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'tenant', 'status', 'interface', 'nat_inside', 'description']
|
||||
widgets = {
|
||||
'interface': APISelect(api_url='/api/dcim/devices/interfaces/?device_id={{interface_device}}'),
|
||||
'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address')
|
||||
}
|
||||
fields = [
|
||||
'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside', 'tenant_group',
|
||||
'tenant',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
if instance and instance.interface is not None:
|
||||
initial['interface_site'] = instance.interface.device.site
|
||||
initial['interface_rack'] = instance.interface.device.rack
|
||||
initial['interface_device'] = instance.interface.device
|
||||
if instance and instance.nat_inside is not None:
|
||||
initial['nat_site'] = instance.nat_inside.device.site
|
||||
initial['nat_rack'] = instance.nat_inside.device.rack
|
||||
initial['nat_device'] = instance.nat_inside.device
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super(IPAddressForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
# If an interface has been assigned, initialize site, rack, and device
|
||||
if self.instance.interface:
|
||||
self.initial['interface_site'] = self.instance.interface.device.site
|
||||
self.initial['interface_rack'] = self.instance.interface.device.rack
|
||||
self.initial['interface_device'] = self.instance.interface.device
|
||||
# Initialize primary_for_device if IP address is already assigned
|
||||
if self.instance.interface is not None:
|
||||
device = self.instance.interface.device
|
||||
if (
|
||||
self.instance.address.version == 4 and device.primary_ip4 == self.instance or
|
||||
self.instance.address.version == 6 and device.primary_ip6 == self.instance
|
||||
):
|
||||
self.initial['primary_for_device'] = True
|
||||
|
||||
# Limit rack choices
|
||||
if self.is_bound and self.data.get('interface_site'):
|
||||
self.fields['interface_rack'].queryset = Rack.objects.filter(site__pk=self.data['interface_site'])
|
||||
elif self.initial.get('interface_site'):
|
||||
self.fields['interface_rack'].queryset = Rack.objects.filter(site=self.initial['interface_site'])
|
||||
else:
|
||||
self.fields['interface_rack'].choices = []
|
||||
def clean(self):
|
||||
super(IPAddressForm, self).clean()
|
||||
|
||||
# Limit device choices
|
||||
if self.is_bound and self.data.get('interface_rack'):
|
||||
self.fields['interface_device'].queryset = Device.objects.filter(rack=self.data['interface_rack'])
|
||||
elif self.initial.get('interface_rack'):
|
||||
self.fields['interface_device'].queryset = Device.objects.filter(rack=self.initial['interface_rack'])
|
||||
else:
|
||||
self.fields['interface_device'].choices = []
|
||||
# Primary IP assignment is only available if an interface has been assigned.
|
||||
if self.cleaned_data.get('primary_for_device') and not self.cleaned_data.get('interface'):
|
||||
self.add_error(
|
||||
'primary_for_device', "Only IP addresses assigned to an interface can be designated as primary IPs."
|
||||
)
|
||||
|
||||
# Limit interface choices
|
||||
if self.is_bound and self.data.get('interface_device'):
|
||||
self.fields['interface'].queryset = Interface.objects.filter(device=self.data['interface_device'])
|
||||
elif self.initial.get('interface_device'):
|
||||
self.fields['interface'].queryset = Interface.objects.filter(device=self.initial['interface_device'])
|
||||
else:
|
||||
self.fields['interface'].choices = []
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
if self.instance.nat_inside:
|
||||
nat_inside = self.instance.nat_inside
|
||||
# If the IP is assigned to an interface, populate site/device fields accordingly
|
||||
if self.instance.nat_inside.interface:
|
||||
self.initial['nat_site'] = self.instance.nat_inside.interface.device.site.pk
|
||||
self.initial['nat_device'] = self.instance.nat_inside.interface.device.pk
|
||||
self.fields['nat_device'].queryset = Device.objects.filter(
|
||||
site=nat_inside.interface.device.site
|
||||
)
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
|
||||
interface__device=nat_inside.interface.device
|
||||
)
|
||||
ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
|
||||
|
||||
# Assign this IPAddress as the primary for the associated Device.
|
||||
if self.cleaned_data['primary_for_device']:
|
||||
device = self.cleaned_data['interface'].device
|
||||
if ipaddress.address.version == 4:
|
||||
device.primary_ip4 = ipaddress
|
||||
else:
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(pk=nat_inside.pk)
|
||||
device.primary_ip6 = ipaddress
|
||||
device.save()
|
||||
|
||||
# Clear assignment as primary for device if set.
|
||||
else:
|
||||
# Initialize nat_device choices if nat_site is set
|
||||
if self.is_bound and self.data.get('nat_site'):
|
||||
self.fields['nat_device'].queryset = Device.objects.filter(site__pk=self.data['nat_site'])
|
||||
elif self.initial.get('nat_site'):
|
||||
self.fields['nat_device'].queryset = Device.objects.filter(site=self.initial['nat_site'])
|
||||
else:
|
||||
self.fields['nat_device'].choices = []
|
||||
# Initialize nat_inside choices if nat_device is set
|
||||
if self.is_bound and self.data.get('nat_device'):
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
|
||||
interface__device__pk=self.data['nat_device'])
|
||||
elif self.initial.get('nat_device'):
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
|
||||
interface__device__pk=self.initial['nat_device'])
|
||||
else:
|
||||
self.fields['nat_inside'].choices = []
|
||||
try:
|
||||
if ipaddress.address.version == 4:
|
||||
device = ipaddress.primary_ip4_for
|
||||
device.primary_ip4 = None
|
||||
else:
|
||||
device = ipaddress.primary_ip6_for
|
||||
device.primary_ip6 = None
|
||||
device.save()
|
||||
except Device.DoesNotExist:
|
||||
pass
|
||||
|
||||
return ipaddress
|
||||
|
||||
|
||||
class IPAddressBulkAddForm(BootstrapMixin, CustomFieldForm):
|
||||
address_pattern = ExpandableIPAddressField(label='Address Pattern')
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global')
|
||||
class IPAddressPatternForm(BootstrapMixin, forms.Form):
|
||||
pattern = ExpandableIPAddressField(label='Address pattern')
|
||||
|
||||
pattern_map = ('address_pattern', 'address')
|
||||
|
||||
class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address_pattern', 'vrf', 'tenant', 'status', 'description']
|
||||
fields = ['address', 'status', 'vrf', 'description', 'tenant_group', 'tenant']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
|
||||
class IPAddressFromCSVForm(forms.ModelForm):
|
||||
@@ -551,14 +609,26 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
# VLANs
|
||||
#
|
||||
|
||||
class VLANForm(BootstrapMixin, CustomFieldForm):
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect(
|
||||
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
|
||||
))
|
||||
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'group', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
group = ChainedModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
chains={'site': 'site'},
|
||||
required=False,
|
||||
label='Group',
|
||||
widget=APISelect(
|
||||
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||
fields = ['site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant']
|
||||
help_texts = {
|
||||
'site': "Leave blank if this VLAN spans multiple sites",
|
||||
'group': "VLAN group (optional)",
|
||||
@@ -567,21 +637,6 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
||||
'status': "Operational status of this VLAN",
|
||||
'role': "The primary function of this VLAN",
|
||||
}
|
||||
widgets = {
|
||||
'site': forms.Select(attrs={'filter-for': 'group', 'nullable': 'true'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(VLANForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Limit VLAN group choices
|
||||
if self.is_bound and self.data.get('site'):
|
||||
self.fields['group'].queryset = VLANGroup.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['group'].queryset = VLANGroup.objects.filter(site=None)
|
||||
|
||||
|
||||
class VLANFromCSVForm(forms.ModelForm):
|
||||
@@ -612,7 +667,7 @@ class VLANFromCSVForm(forms.ModelForm):
|
||||
group_name = self.cleaned_data.get('group_name')
|
||||
if group_name:
|
||||
try:
|
||||
vlan_group = VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
|
||||
VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
|
||||
except VLANGroup.DoesNotExist:
|
||||
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
|
||||
|
||||
@@ -646,7 +701,7 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['group', 'tenant', 'role', 'description']
|
||||
nullable_fields = ['site', 'group', 'tenant', 'role', 'description']
|
||||
|
||||
|
||||
def vlan_status_choices():
|
||||
|
||||
@@ -538,7 +538,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
verbose_name_plural = 'VLANs'
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name
|
||||
return self.display_name or super(VLAN, self).__str__()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:vlan', args=[self.pk])
|
||||
@@ -565,7 +565,9 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return u'{} ({})'.format(self.vid, self.name)
|
||||
if self.vid and self.name:
|
||||
return u"{} ({})".format(self.vid, self.name)
|
||||
return None
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
@@ -76,6 +76,15 @@ IPADDRESS_LINK = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
IPADDRESS_DEVICE = """
|
||||
{% if record.interface %}
|
||||
<a href="{{ record.interface.device.get_absolute_url }}">{{ record.interface.device }}</a>
|
||||
({{ record.interface.name }})
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VRF_LINK = """
|
||||
{% if record.vrf %}
|
||||
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
||||
@@ -281,12 +290,14 @@ class IPAddressTable(BaseTable):
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
|
||||
interface = tables.Column(orderable=False)
|
||||
nat_inside = tables.LinkColumn(
|
||||
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
|
||||
)
|
||||
device = tables.TemplateColumn(IPADDRESS_DEVICE, orderable=False)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description')
|
||||
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'nat_inside', 'device', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||
}
|
||||
|
||||
@@ -2,15 +2,12 @@ from django_tables2 import RequestConfig
|
||||
import netaddr
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.contrib import messages
|
||||
from django.db.models import Count, Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.models import Device
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import (
|
||||
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
@@ -536,7 +533,7 @@ def prefix_ipaddresses(request, pk):
|
||||
#
|
||||
|
||||
class IPAddressListView(ObjectListView):
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
|
||||
filter = filters.IPAddressFilter
|
||||
filter_form = forms.IPAddressFilterForm
|
||||
table = tables.IPAddressTable
|
||||
@@ -587,8 +584,9 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
|
||||
permission_required = 'ipam.add_ipaddress'
|
||||
form = forms.IPAddressBulkAddForm
|
||||
model_form = forms.IPAddressForm
|
||||
pattern_form = forms.IPAddressPatternForm
|
||||
model_form = forms.IPAddressBulkAddForm
|
||||
pattern_target = 'address'
|
||||
template_name = 'ipam/ipaddress_bulk_add.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.0-beta3'
|
||||
VERSION = '2.0.2'
|
||||
|
||||
# Import local configuration
|
||||
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
|
||||
|
||||
@@ -36,7 +36,7 @@ SEARCH_TYPES = {
|
||||
'url': 'circuits:provider_list',
|
||||
},
|
||||
'circuit': {
|
||||
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant'),
|
||||
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
|
||||
'filter': CircuitFilter,
|
||||
'table': CircuitSearchTable,
|
||||
'url': 'circuits:circuit_list',
|
||||
@@ -191,7 +191,7 @@ class SearchView(View):
|
||||
results.append({
|
||||
'name': queryset.model._meta.verbose_name_plural,
|
||||
'table': table,
|
||||
'url': '{}?q={}'.format(reverse(url), form.cleaned_data['q'])
|
||||
'url': u'{}?q={}'.format(reverse(url), form.cleaned_data['q'])
|
||||
})
|
||||
|
||||
return render(request, 'search.html', {
|
||||
|
||||
@@ -15,11 +15,15 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
// Adding/editing a secret
|
||||
$('form.requires-session-key').submit(function(event) {
|
||||
if ($('#id_plaintext').val() && document.cookie.indexOf('session_key') == -1) {
|
||||
$('#privkey_modal').modal('show');
|
||||
event.preventDefault();
|
||||
}
|
||||
$('form').submit(function(event) {
|
||||
$(this).find('input.requires-session-key').each(function() {
|
||||
if (this.value && document.cookie.indexOf('session_key') == -1) {
|
||||
console.log('Field ' + this.value + ' requires a session key');
|
||||
$('#privkey_modal').modal('show');
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Retrieve a session key
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.contrib import admin, messages
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from .forms import ActivateUserKeyForm
|
||||
from .models import UserKey, SecretRole, Secret
|
||||
from .models import UserKey
|
||||
|
||||
|
||||
@admin.register(UserKey)
|
||||
@@ -10,7 +10,7 @@ class UserKeyAdmin(admin.ModelAdmin):
|
||||
actions = ['activate_selected']
|
||||
list_display = ['user', 'is_filled', 'is_active', 'created']
|
||||
fields = ['user', 'public_key', 'is_active', 'last_updated']
|
||||
readonly_fields = ['is_active', 'last_updated']
|
||||
readonly_fields = ['user', 'is_active', 'last_updated']
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
# Don't allow a user to modify an existing public key directly.
|
||||
|
||||
@@ -48,7 +48,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class SecretForm(BootstrapMixin, forms.ModelForm):
|
||||
plaintext = forms.CharField(max_length=65535, required=False, label='Plaintext',
|
||||
widget=forms.PasswordInput())
|
||||
widget=forms.PasswordInput(attrs={'class': 'requires-session-key'}))
|
||||
plaintext2 = forms.CharField(max_length=65535, required=False, label='Plaintext (verify)',
|
||||
widget=forms.PasswordInput())
|
||||
|
||||
@@ -82,7 +82,7 @@ class SecretFromCSVForm(forms.ModelForm):
|
||||
|
||||
|
||||
class SecretImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=SecretFromCSVForm)
|
||||
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-session-key'}))
|
||||
|
||||
|
||||
class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
||||
@@ -44,6 +44,7 @@ class SecretTable(BaseTable):
|
||||
|
||||
|
||||
class SecretSearchTable(SearchTable):
|
||||
device = tables.LinkColumn()
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Secret
|
||||
|
||||
@@ -221,6 +221,7 @@ def secret_import(request):
|
||||
|
||||
return render(request, 'import_success.html', {
|
||||
'table': table,
|
||||
'return_url': 'secrets:secret_list',
|
||||
})
|
||||
|
||||
except IntegrityError as e:
|
||||
@@ -231,7 +232,7 @@ def secret_import(request):
|
||||
|
||||
return render(request, 'secrets/secret_import.html', {
|
||||
'form': form,
|
||||
'return_url': reverse('secrets:secret_list'),
|
||||
'return_url': 'secrets:secret_list',
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -8,12 +8,18 @@
|
||||
{% render_field form.provider %}
|
||||
{% render_field form.cid %}
|
||||
{% render_field form.type %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.install_date %}
|
||||
{% render_field form.commit_rate %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
|
||||
@@ -1,72 +1,57 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Circuit Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Circuit 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>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>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Circuit ID</td>
|
||||
<td>Alphanumeric circuit identifier</td>
|
||||
<td>IC-603122</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td>Name of circuit provider</td>
|
||||
<td>TeliaSonera</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>Circuit type</td>
|
||||
<td>Transit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Strickland Propane</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Install Date</td>
|
||||
<td>Date in YYYY-MM-DD format (optional)</td>
|
||||
<td>2016-02-23</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Commit rate</td>
|
||||
<td>Commited rate in Kbps (optional)</td>
|
||||
<td>2000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Primary for voice</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,62 +1,47 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Provider Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Provider 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>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>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Provider's proper name</td>
|
||||
<td>Level 3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slug</td>
|
||||
<td>URL-friendly name</td>
|
||||
<td>level3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ASN</td>
|
||||
<td>Autonomous system number (optional)</td>
|
||||
<td>3356</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Account</td>
|
||||
<td>Account number (optional)</td>
|
||||
<td>08931544</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Portal URL</td>
|
||||
<td>Customer service portal URL (optional)</td>
|
||||
<td>https://mylevel3.net</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>Level 3,level3,3356,08931544,https://mylevel3.net</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,61 +1,47 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Console Connections Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Console Connections 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>
|
||||
</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>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>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
@@ -123,11 +123,7 @@
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
{% if device.status %}
|
||||
<span class="label label-success">{{ device.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="label label-danger">{{ device.get_status_display }}</span>
|
||||
{% endif %}
|
||||
<span class="label label-{{ device.get_status_class }}">{{ device.get_status_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<div class="panel-body">
|
||||
{% render_field form.name %}
|
||||
{% render_field form.device_role %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
@@ -55,14 +54,21 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Management</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.platform %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.platform %}
|
||||
{% if obj.pk %}
|
||||
{% render_field form.primary_ip4 %}
|
||||
{% render_field form.primary_ip6 %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
|
||||
@@ -12,8 +12,12 @@
|
||||
{% 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 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>
|
||||
@@ -66,6 +70,11 @@
|
||||
<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>
|
||||
@@ -89,7 +98,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,ABC123456,Ashburn-VA,R101,21,Rear</pre>
|
||||
<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,ABC123456,Active,Ashburn-VA,R101,21,Rear</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,8 +12,12 @@
|
||||
{% 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 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>
|
||||
|
||||
@@ -31,24 +31,83 @@
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
|
||||
var site_list = $('#id_site');
|
||||
var rack_group_list = $('#id_rack_group_id');
|
||||
var rack_list = $('#id_rack_id');
|
||||
var manufacturer_list = $('#id_manufacturer_id');
|
||||
var model_list = $('#id_device_type_id');
|
||||
$('#id_manufacturer_id').change(function() {
|
||||
model_list.empty();
|
||||
|
||||
// Update device type options based on selected manufacturer
|
||||
manufacturer_list.change(function() {
|
||||
var selected_manufacturers = $(this).val();
|
||||
if (selected_manufacturers) {
|
||||
var api_url = netbox_api_path + 'dcim/device-types/?manufacturer_id=' + selected_manufacturers.join('&manufacturer_id=');
|
||||
model_list.empty();
|
||||
$.ajax({
|
||||
url: api_url,
|
||||
url: netbox_api_path + 'dcim/device-types/?limit=500&manufacturer_id=' + selected_manufacturers.join('&manufacturer_id='),
|
||||
dataType: 'json',
|
||||
success: function (response, status) {
|
||||
$.each(response, function (index, device_type) {
|
||||
var option = $("<option></option>").attr("value", device_type.id).text(device_type["model"] + " (" + device_type["instance_count"] + ")");
|
||||
$.each(response["results"], function (index, device_type) {
|
||||
var option = $("<option></option>").attr("value", device_type.id).text(device_type.model + " (" + device_type.instance_count + ")");
|
||||
model_list.append(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update rack group and rack options based on selected site
|
||||
site_list.change(function() {
|
||||
var selected_sites = $(this).val();
|
||||
if (selected_sites) {
|
||||
|
||||
// Update rack group options
|
||||
rack_group_list.empty();
|
||||
$.ajax({
|
||||
url: netbox_api_path + 'dcim/rack-groups/?limit=500&site=' + selected_sites.join('&site='),
|
||||
dataType: 'json',
|
||||
success: function (response, status) {
|
||||
$.each(response["results"], function (index, group) {
|
||||
var option = $("<option></option>").attr("value", group.id).text(group.name);
|
||||
rack_group_list.append(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update rack options
|
||||
rack_list.empty();
|
||||
$.ajax({
|
||||
url: netbox_api_path + 'dcim/racks/?limit=500&site=' + selected_sites.join('&site='),
|
||||
dataType: 'json',
|
||||
success: function (response, status) {
|
||||
$.each(response["results"], function (index, rack) {
|
||||
var option = $("<option></option>").attr("value", rack.id).text(rack.display_name);
|
||||
rack_list.append(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// Update rack options based on selected rack group
|
||||
rack_group_list.change(function() {
|
||||
var selected_rack_groups = $(this).val();
|
||||
if (selected_rack_groups) {
|
||||
rack_list.empty();
|
||||
$.ajax({
|
||||
url: netbox_api_path + 'dcim/racks/?limit=500&group_id=' + selected_rack_groups.join('&group_id='),
|
||||
dataType: 'json',
|
||||
success: function (response, status) {
|
||||
$.each(response["results"], function (index, rack) {
|
||||
var option = $("<option></option>").attr("value", rack.id).text(rack.display_name);
|
||||
rack_list.append(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
|
||||
</td>
|
||||
<td></td>
|
||||
{% if cp.cs_port %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
|
||||
@@ -32,11 +33,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:consoleport_disconnect' pk=cp.pk %}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" class="btn btn-info btn-xs">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp.name }}
|
||||
</td>
|
||||
<td></td>
|
||||
{% if csp.connected_console %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
|
||||
@@ -32,11 +33,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:consoleserverport_disconnect' pk=csp.pk %}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:consoleserverport_connect' pk=csp.pk %}" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" class="btn btn-info btn-xs">
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
{% if iface.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
||||
{% endif %}
|
||||
{% if iface.is_lag %}
|
||||
<br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ iface.mac_address|default:"" }}</td>
|
||||
{% if iface.is_lag %}
|
||||
<td colspan="2" class="text-muted">LAG interface</td>
|
||||
<td colspan="2" class="text-muted">
|
||||
LAG interface<br />
|
||||
<small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
|
||||
</td>
|
||||
{% elif iface.is_virtual %}
|
||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||
{% elif iface.connection %}
|
||||
@@ -133,7 +134,7 @@
|
||||
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if perms.ipam.edit_ipaddress %}
|
||||
{% if perms.ipam.change_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
|
||||
</a>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ po.name }}
|
||||
</td>
|
||||
<td></td>
|
||||
{% if po.connected_port %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
|
||||
@@ -32,11 +33,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:poweroutlet_disconnect' pk=po.pk %}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:poweroutlet_connect' pk=po.pk %}" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" class="btn btn-info btn-xs">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
|
||||
</td>
|
||||
<td></td>
|
||||
{% if pp.power_outlet %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
|
||||
@@ -32,11 +33,11 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:powerport_disconnect' pk=pp.pk %}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Delete connection"></i>
|
||||
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" class="btn btn-success btn-xs">
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Connect"></i>
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" class="btn btn-info btn-xs">
|
||||
|
||||
@@ -1,69 +1,47 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Interface Connections Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Interface Connections Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</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>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>
|
||||
</div>
|
||||
</div>
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Device A</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-core1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interface A</td>
|
||||
<td>Interface name</td>
|
||||
<td>xe-0/0/6</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device B</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-switch7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interface B</td>
|
||||
<td>Interface name</td>
|
||||
<td>xe-0/0/0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>"planned" or "connected"</td>
|
||||
<td>planned</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned</pre>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,61 +1,47 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Power Connections Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Power Connections 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>
|
||||
</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>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>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
@@ -6,11 +6,22 @@
|
||||
<div class="panel-heading"><strong>Rack</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.site %}
|
||||
{% render_field form.group %}
|
||||
{% render_field form.name %}
|
||||
{% render_field form.facility_id %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.group %}
|
||||
{% render_field form.role %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Dimensions</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.type %}
|
||||
{% render_field form.width %}
|
||||
{% render_field form.u_height %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% for rack in page %}
|
||||
<div style="display: inline-block; width: 266px">
|
||||
<div class="rack_header">
|
||||
<h4>{{ rack.name }}</h4>
|
||||
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
|
||||
</div>
|
||||
{% if face_id %}
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
|
||||
@@ -23,7 +23,7 @@
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
<div class="rack_header">
|
||||
<h4>{{ rack.name }}</h4>
|
||||
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,87 +1,72 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Rack Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Rack 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>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>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
@@ -8,11 +8,17 @@
|
||||
{% render_field form.name %}
|
||||
{% render_field form.slug %}
|
||||
{% render_field form.region %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.facility %}
|
||||
{% render_field form.asn %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Contact Info</strong></div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}Import Completed{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Import Completed</h1>
|
||||
{% render_table table %}
|
||||
<a href="{{ request.path }}" class="btn btn-primary">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import more
|
||||
</a>
|
||||
<h1>{% block title %}Import Completed{% endblock %}</h1>
|
||||
{% render_table table %}
|
||||
<a href="{{ request.path }}" class="btn btn-primary">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
Import more
|
||||
</a>
|
||||
{% if return_url %}
|
||||
<a href="{% url return_url %}" class="btn btn-default">View All</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for attachment in images %}
|
||||
<tr>
|
||||
<tr{% if not attachment.size %} class="danger"{% endif %}>
|
||||
<td>
|
||||
<i class="fa fa-image"></i>
|
||||
<a href="{{ attachment.image.url }}" target="_blank">{{ attachment }}</a>
|
||||
</td>
|
||||
<td>{{ attachment.image.size|filesizeformat }}</td>
|
||||
<td>{{ attachment.size|filesizeformat }}</td>
|
||||
<td>{{ attachment.created }}</td>
|
||||
<td class="text-right">
|
||||
{% if perms.extras.change_imageattachment %}
|
||||
|
||||
@@ -1,57 +1,42 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Aggregate Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Aggregate 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>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>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
@@ -12,18 +12,24 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>IP Addresses</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.address_pattern %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.description %}
|
||||
{% render_field pattern_form.pattern %}
|
||||
{% render_field model_form.status %}
|
||||
{% render_field model_form.vrf %}
|
||||
{% render_field model_form.description %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field model_form.tenant_group %}
|
||||
{% render_field model_form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
{% if model_form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_custom_fields form %}
|
||||
{% render_custom_fields model_form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -13,12 +13,18 @@
|
||||
<div class="panel-heading"><strong>IP Address</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.address %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interface Assignment</strong>
|
||||
@@ -28,6 +34,7 @@
|
||||
{% render_field form.interface_rack %}
|
||||
{% render_field form.interface_device %}
|
||||
{% render_field form.interface %}
|
||||
{% render_field form.primary_for_device %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
|
||||
@@ -1,77 +1,62 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}IP Address Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>IP Address 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>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>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
@@ -6,14 +6,20 @@
|
||||
<div class="panel-heading"><strong>Prefix</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.prefix %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.site %}
|
||||
{% render_field form.vlan %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.role %}
|
||||
{% render_field form.is_pool %}
|
||||
{% render_field form.description %}
|
||||
{% render_field form.is_pool %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
|
||||
@@ -1,87 +1,72 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Prefix Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Prefix 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>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>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
@@ -9,12 +9,18 @@
|
||||
{% render_field form.group %}
|
||||
{% render_field form.vid %}
|
||||
{% render_field form.name %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.role %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
|
||||
@@ -1,77 +1,62 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}VLAN Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>VLAN 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>Site</td>
|
||||
<td>Name of assigned site</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>
|
||||
</div>
|
||||
</div>
|
||||
{% 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</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 %}
|
||||
|
||||
@@ -7,11 +7,17 @@
|
||||
<div class="panel-body">
|
||||
{% render_field form.name %}
|
||||
{% render_field form.rd %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.enforce_unique %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.custom_fields %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||
|
||||
@@ -1,62 +1,47 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}VRF Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>VRF 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>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>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block title %}{% if secret.pk %}Editing {{ secret }}{% else %}Add a Secret{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal requires-session-key">
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{{ form.private_key }}
|
||||
<div class="row">
|
||||
|
||||
@@ -20,10 +20,14 @@
|
||||
<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="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'django_tables2/table.html' %}
|
||||
{% extends 'django_tables2/bootstrap-responsive.html' %}
|
||||
{% load django_tables2 %}
|
||||
|
||||
{# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<nav>
|
||||
<ul class="pagination pull-right">
|
||||
{% if table.page.has_previous %}
|
||||
<li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">«</a></li>
|
||||
<li><a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}"><i class="fa fa-angle-double-left"></i></a></li>
|
||||
{% endif %}
|
||||
{% for p in table.page.smart_pages %}
|
||||
{% if p %}
|
||||
@@ -17,18 +17,20 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if table.page.has_next %}
|
||||
<li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">»</a></li>
|
||||
<li><a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}"><i class="fa fa-angle-double-right"></i></a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
<div class="text-right text-muted">
|
||||
Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }}
|
||||
{% if total == 1 %}
|
||||
{{ table.data.verbose_name }}
|
||||
{% else %}
|
||||
{{ table.data.verbose_name_plural }}
|
||||
{% endif %}
|
||||
{% with table.page.paginator.count as total %}
|
||||
Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ total }}
|
||||
{% if total == 1 %}
|
||||
{{ table.data.verbose_name }}
|
||||
{% else %}
|
||||
{{ table.data.verbose_name_plural }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,57 +1,42 @@
|
||||
{% extends '_base.html' %}
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Tenant Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Tenant 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>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>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
34
netbox/templates/utilities/obj_import.html
Normal file
34
netbox/templates/utilities/obj_import.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% block title %}{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% block instructions %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -2,8 +2,10 @@ from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from utilities.forms import BootstrapMixin, BulkImportForm, CommentField, CSVDataField, FilterChoiceField, SlugField
|
||||
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
|
||||
FilterChoiceField, SlugField,
|
||||
)
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
@@ -61,3 +63,36 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tenancy form extension
|
||||
#
|
||||
|
||||
class TenancyForm(ChainedFieldsMixin, forms.Form):
|
||||
tenant_group = forms.ModelChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'tenant', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
tenant = ChainedModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
chains={'group': 'tenant_group'},
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/tenancy/tenants/?group_id={{tenant_group}}'
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selector
|
||||
instance = kwargs.get('instance')
|
||||
if instance and instance.tenant is not None:
|
||||
initial = kwargs.get('initial', {})
|
||||
initial['tenant_group'] = instance.tenant.group
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super(TenancyForm, self).__init__(*args, **kwargs)
|
||||
|
||||
@@ -44,6 +44,7 @@ class TenantTable(BaseTable):
|
||||
|
||||
|
||||
class TenantSearchTable(SearchTable):
|
||||
name = tables.LinkColumn()
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Tenant
|
||||
|
||||
@@ -216,10 +216,13 @@ class TokenEditView(LoginRequiredMixin, View):
|
||||
token.user = request.user
|
||||
token.save()
|
||||
|
||||
msg = "Token updated" if pk else "New token created"
|
||||
msg = "Modified token {}".format(token) if pk else "Created token {}".format(token)
|
||||
messages.success(request, msg)
|
||||
|
||||
return redirect('user:token_list')
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
else:
|
||||
return redirect('user:token_list')
|
||||
|
||||
|
||||
class TokenDeleteView(LoginRequiredMixin, View):
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from django.contrib import messages
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
def handle_protectederror(obj, request, e):
|
||||
@@ -25,11 +27,11 @@ def handle_protectederror(obj, request, e):
|
||||
|
||||
# Append dependent objects to error message
|
||||
dependent_objects = []
|
||||
for o in e.protected_objects:
|
||||
if hasattr(o, 'get_absolute_url'):
|
||||
dependent_objects.append(u'<a href="{}">{}</a>'.format(o.get_absolute_url(), o))
|
||||
for obj in e.protected_objects:
|
||||
if hasattr(obj, 'get_absolute_url'):
|
||||
dependent_objects.append(u'<a href="{}">{}</a>'.format(obj.get_absolute_url(), escape(obj)))
|
||||
else:
|
||||
dependent_objects.append(str(o))
|
||||
dependent_objects.append(str(obj))
|
||||
err_message += u', '.join(dependent_objects)
|
||||
|
||||
messages.error(request, err_message)
|
||||
messages.error(request, mark_safe(err_message))
|
||||
|
||||
@@ -125,6 +125,19 @@ class ColorSelect(forms.Select):
|
||||
super(ColorSelect, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BulkEditNullBooleanSelect, self).__init__(*args, **kwargs)
|
||||
|
||||
# Override the built-in choice labels
|
||||
self.choices = (
|
||||
('1', '---------'),
|
||||
('2', 'Yes'),
|
||||
('3', 'No'),
|
||||
)
|
||||
|
||||
|
||||
class SelectWithDisabled(forms.Select):
|
||||
"""
|
||||
Modified the stock Select widget to accept choices using a dict() for a label. The dict for each option must include
|
||||
@@ -318,6 +331,25 @@ class FlexibleModelChoiceField(forms.ModelChoiceField):
|
||||
return value
|
||||
|
||||
|
||||
class ChainedModelChoiceField(forms.ModelChoiceField):
|
||||
"""
|
||||
A ModelChoiceField which is initialized based on the values of other fields within a form. `chains` is a dictionary
|
||||
mapping of model fields to peer fields within the form. For example:
|
||||
|
||||
country1 = forms.ModelChoiceField(queryset=Country.objects.all())
|
||||
city1 = ChainedModelChoiceField(queryset=City.objects.all(), chains={'country': 'country1'}
|
||||
|
||||
The queryset of the `city1` field will be modified as
|
||||
|
||||
.filter(country=<value>)
|
||||
|
||||
where <value> is the value of the `country1` field. (Note: The form must inherit from ChainedFieldsMixin.)
|
||||
"""
|
||||
def __init__(self, chains=None, *args, **kwargs):
|
||||
self.chains = chains
|
||||
super(ChainedModelChoiceField, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class SlugField(forms.SlugField):
|
||||
|
||||
def __init__(self, slug_source='name', *args, **kwargs):
|
||||
@@ -398,6 +430,32 @@ class BootstrapMixin(forms.BaseForm):
|
||||
field.widget.attrs['placeholder'] = field.label
|
||||
|
||||
|
||||
class ChainedFieldsMixin(forms.BaseForm):
|
||||
"""
|
||||
Iterate through all ChainedModelChoiceFields in the form and modify their querysets based on chained fields.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ChainedFieldsMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
for field_name, field in self.fields.items():
|
||||
|
||||
if isinstance(field, ChainedModelChoiceField):
|
||||
|
||||
filters_dict = {}
|
||||
for db_field, parent_field in field.chains.items():
|
||||
if self.is_bound:
|
||||
filters_dict[db_field] = self.data.get(parent_field) or None
|
||||
elif self.initial.get(parent_field):
|
||||
filters_dict[db_field] = self.initial[parent_field]
|
||||
else:
|
||||
filters_dict[db_field] = None
|
||||
|
||||
if filters_dict:
|
||||
field.queryset = field.queryset.filter(**filters_dict)
|
||||
else:
|
||||
field.queryset = field.queryset.none()
|
||||
|
||||
|
||||
class ReturnURLForm(forms.Form):
|
||||
"""
|
||||
Provides a hidden return URL field to control where the user is directed after the form is submitted.
|
||||
|
||||
@@ -24,3 +24,15 @@ def csv_format(data):
|
||||
csv.append(u'{}'.format(value))
|
||||
|
||||
return u','.join(csv)
|
||||
|
||||
|
||||
def foreground_color(bg_color):
|
||||
"""
|
||||
Return the ideal foreground color (black or white) for a given background color in hexadecimal RGB format.
|
||||
"""
|
||||
bg_color = bg_color.strip('#')
|
||||
r, g, b = [int(bg_color[c:c + 2], 16) for c in (0, 2, 4)]
|
||||
if r * 0.299 + g * 0.587 + b * 0.114 > 186:
|
||||
return '000000'
|
||||
else:
|
||||
return 'ffffff'
|
||||
|
||||
@@ -17,7 +17,6 @@ from django.utils.http import is_safe_url
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
|
||||
from extras.forms import CustomFieldForm
|
||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate, UserAction
|
||||
|
||||
from .error_handlers import handle_protectederror
|
||||
@@ -195,12 +194,8 @@ class ObjectEditView(GetReturnURLMixin, View):
|
||||
form = self.form_class(request.POST, request.FILES, instance=obj)
|
||||
|
||||
if form.is_valid():
|
||||
obj = form.save(commit=False)
|
||||
obj_created = not obj.pk
|
||||
obj.save()
|
||||
form.save_m2m()
|
||||
if isinstance(form, CustomFieldForm):
|
||||
form.save_custom_fields()
|
||||
obj_created = not form.instance.pk
|
||||
obj = form.save()
|
||||
|
||||
msg = u'Created ' if obj_created else u'Modified '
|
||||
msg += self.model._meta.verbose_name
|
||||
@@ -295,66 +290,78 @@ class BulkAddView(View):
|
||||
"""
|
||||
Create new objects in bulk.
|
||||
|
||||
form: Form class
|
||||
pattern_form: Form class which provides the `pattern` field
|
||||
model_form: The ModelForm used to create individual objects
|
||||
template_name: The name of the template
|
||||
default_return_url: Name of the URL to which the user is redirected after creating the objects
|
||||
"""
|
||||
form = None
|
||||
pattern_form = None
|
||||
model_form = None
|
||||
pattern_target = ''
|
||||
template_name = None
|
||||
default_return_url = 'home'
|
||||
|
||||
def get(self, request):
|
||||
|
||||
form = self.form()
|
||||
pattern_form = self.pattern_form()
|
||||
model_form = self.model_form()
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj_type': self.model_form._meta.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'pattern_form': pattern_form,
|
||||
'model_form': model_form,
|
||||
'return_url': reverse(self.default_return_url),
|
||||
})
|
||||
|
||||
def post(self, request):
|
||||
|
||||
model = self.model_form._meta.model
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
pattern_form = self.pattern_form(request.POST)
|
||||
model_form = self.model_form(request.POST)
|
||||
|
||||
# Read the pattern field and target from the form's pattern_map
|
||||
pattern_field, pattern_target = form.pattern_map
|
||||
pattern = form.cleaned_data[pattern_field]
|
||||
model_form_data = form.cleaned_data
|
||||
if pattern_form.is_valid():
|
||||
|
||||
pattern = pattern_form.cleaned_data['pattern']
|
||||
new_objs = []
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Validate and save each object individually
|
||||
|
||||
# Create objects from the expanded. Abort the transaction on the first validation error.
|
||||
for value in pattern:
|
||||
model_form_data[pattern_target] = value
|
||||
model_form = self.model_form(model_form_data)
|
||||
|
||||
# Reinstantiate the model form each time to avoid overwriting the same instance. Use a mutable
|
||||
# copy of the POST QueryDict so that we can update the target field value.
|
||||
model_form = self.model_form(request.POST.copy())
|
||||
model_form.data[self.pattern_target] = value
|
||||
|
||||
# Validate each new object independently.
|
||||
if model_form.is_valid():
|
||||
obj = model_form.save()
|
||||
new_objs.append(obj)
|
||||
else:
|
||||
for error in model_form.errors.as_data().values():
|
||||
form.add_error(None, error)
|
||||
# Abort the creation of all objects if errors exist
|
||||
if form.errors:
|
||||
raise ValidationError("Validation of one or more model forms failed.")
|
||||
except ValidationError:
|
||||
# Copy any errors on the pattern target field to the pattern form.
|
||||
errors = model_form.errors.as_data()
|
||||
if errors.get(self.pattern_target):
|
||||
pattern_form.add_error('pattern', errors[self.pattern_target])
|
||||
# Raise an IntegrityError to break the for loop and abort the transaction.
|
||||
raise IntegrityError()
|
||||
|
||||
# If we make it to this point, validation has succeeded on all new objects.
|
||||
msg = u"Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(model), msg)
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
return redirect(self.default_return_url)
|
||||
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
if not form.errors:
|
||||
msg = u"Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(model), msg)
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
return redirect(self.default_return_url)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'pattern_form': pattern_form,
|
||||
'model_form': model_form,
|
||||
'obj_type': model._meta.verbose_name,
|
||||
'return_url': reverse(self.default_return_url),
|
||||
})
|
||||
@@ -400,6 +407,7 @@ class BulkImportView(View):
|
||||
|
||||
return render(request, "import_success.html", {
|
||||
'table': obj_table,
|
||||
'return_url': self.default_return_url,
|
||||
})
|
||||
|
||||
except IntegrityError as e:
|
||||
@@ -423,7 +431,7 @@ class BulkEditView(View):
|
||||
filter: FilterSet to apply when deleting by QuerySet
|
||||
form: The form class used to edit objects in bulk
|
||||
template_name: The name of the template
|
||||
default_return_url: Name of the URL to which the user is redirected after editing the objects (can be overriden by
|
||||
default_return_url: Name of the URL to which the user is redirected after editing the objects (can be overridden by
|
||||
POSTing return_url)
|
||||
"""
|
||||
cls = None
|
||||
@@ -475,7 +483,7 @@ class BulkEditView(View):
|
||||
fields_to_update[field] = ''
|
||||
else:
|
||||
fields_to_update[field] = None
|
||||
elif form.cleaned_data[field]:
|
||||
elif form.cleaned_data[field] not in (None, ''):
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ django-debug-toolbar>=1.7
|
||||
django-filter>=1.0.2
|
||||
django-mptt==0.8.7
|
||||
django-rest-swagger>=2.1.0
|
||||
django-tables2>=1.4.0
|
||||
django-tables2>=1.6.0
|
||||
djangorestframework>=3.6.2
|
||||
graphviz>=0.6
|
||||
Markdown>=2.6.7
|
||||
|
||||
@@ -18,7 +18,7 @@ fi
|
||||
# Delete stale bytecode
|
||||
COMMAND="${PREFIX}find . -name \"*.pyc\" -delete"
|
||||
echo "Cleaning up stale Python bytecode ($COMMAND)..."
|
||||
#eval $COMMAND
|
||||
eval $COMMAND
|
||||
|
||||
# Fall back to pip3 if pip is missing
|
||||
PIP="pip"
|
||||
|
||||
Reference in New Issue
Block a user