diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 9c2993b60..cfceb15ef 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -48,6 +48,19 @@ IFACE_ORDERING_CHOICES = [ [IFACE_ORDERING_NAME, 'Name (alphabetically)'] ] +# Interface enabled as choice +IFACE_DISABLED = False +IFACE_ENABLED = True +IFACE_ENABLED_CHOICES = [ + [IFACE_DISABLED, 'Disabled'], + [IFACE_ENABLED, 'Enabled'], +] + +IFACE_STATUS_CLASSES = { + 0: 'disabled', + 1: 'enabled', +} + # Interface form factors # Virtual IFACE_FF_VIRTUAL = 0 diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index a9f58d2a4..6cb32b124 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -581,6 +581,94 @@ class InterfaceFilter(django_filters.FilterSet): return queryset.none() +class InterfaceListFilter(django_filters.FilterSet): + q = django_filters.CharFilter( + method='search', + label='Search', + ) + device = django_filters.CharFilter( + method='filter_device', + name='name', + label='Device', + ) + site = django_filters.CharFilter( + method='filter_site', + label='Site (slug)', + ) + role = django_filters.CharFilter( + method='filter_role', + label='Role (slug)', + ) + rack_group_id = NullableModelMultipleChoiceFilter( + name='device__rack__group', + queryset=RackGroup.objects.all(), + label='Rack Group(ID)', + ) + rack_id = NullableModelMultipleChoiceFilter( + name='device__rack', + queryset=Rack.objects.all(), + label='Rack (ID)', + ) + type = django_filters.CharFilter( + method='filter_type', + label='Interface type', + ) + mac_address = django_filters.CharFilter( + method='_mac_address', + label='MAC address', + ) + + class Meta: + model = Interface + fields = ['form_factor', 'enabled', 'mtu'] + + def filter_device(self, queryset, name, value): + try: + device = Device.objects.select_related('device_type').get(**{name: value}) + ordering = device.device_type.interface_ordering + return queryset.filter(device=device).order_naturally(ordering) + except Device.DoesNotExist: + return queryset.none() + + def filter_site(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter(device__site__slug=value) + + def filter_role(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter(device__device_role__slug=value) + + def filter_type(self, queryset, name, value): + value = value.strip().lower() + return { + 'physical': queryset.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES), + 'virtual': queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES), + 'wireless': queryset.filter(form_factor__in=WIRELESS_IFACE_TYPES), + 'lag': queryset.filter(form_factor=IFACE_FF_LAG), + }.get(value, queryset.none()) + + def _mac_address(self, queryset, name, value): + value = value.strip() + if not value: + return queryset + try: + return queryset.filter(mac_address__icontains=value) + except AddrFormatError: + return queryset.none() + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(device__name__icontains=value.strip()) | + Q(name__icontains=value.strip()) | + Q(description__icontains=value.strip()) | + Q(mac_address__icontains=value.strip()) + ).distinct() + + class DeviceBayFilter(DeviceComponentFilterSet): class Meta: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 440c12623..f39b21ee0 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -21,7 +21,7 @@ from .formfields import MACAddressFormField from .models import ( DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface, - IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer, + IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ENABLED_CHOICES, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, @@ -1531,6 +1531,117 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput) +class InterfaceCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={'invalid_choice': 'Device not found.'} + ) + name = forms.CharField( + help_text='Name of interface' + ) + lag = FlexibleModelChoiceField( + queryset=Interface.objects.filter(form_factor=IFACE_FF_LAG), + to_field_name='name', + required=False, + help_text='Lag Name or ID of interface', + error_messages={'invalid_choice': 'Lag not found.'} + ) + mac_address = forms.CharField( + required=False, + help_text='MAC address of interface' + ) + form_factor = forms.IntegerField( + required=False, + help_text='Interface Form Factor' + ) + description = forms.CharField( + required=False, + help_text='Description for interface' + ) + enabled = forms.BooleanField( + required=False, + help_text='Enabled/Disabled' + ) + mtu = forms.IntegerField( + required=False, + help_text='MTU' + ) + mgmt_only = forms.CharField( + required=False, + help_text='Management Only' + ) + is_virtual = forms.BooleanField( + required=False, + help_text='Is Virtual?' + ) + is_wireless = forms.BooleanField( + required=False, + help_text='Is Wireless?' + ) + is_lag = forms.BooleanField( + required=False, + help_text='Is Lag?' + ) + + class Meta: + model = Interface + fields = ('device', 'lag','name','mac_address','form_factor','enabled','description','mtu','mgmt_only','is_virtual','is_wireless','is_lag') + nullable_fields = ['lag','is_virtual','is_wireless','is_lag'] + + + def clean_interface(self): + interface_name = self.cleaned_data.get('interface_name') + if not interface: + return None + + return interface + + + def clean_lag(self): + device_id = self.cleaned_data.get('device') + lag_name = self.cleaned_data.get('lag') + if device_id is not None and lag_name is not None: + lag = Interface.objects.filter( + device=device_id, form_factor=IFACE_FF_LAG).get( + name=lag_name + ) + if not lag_name: + return None + return lag + + +class InterfaceFilterForm(BootstrapMixin, forms.Form): + site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug') + device = forms.CharField(required=False, label='Device name') + + +class InterfaceListFilterForm(BootstrapMixin, forms.Form): + q = forms.CharField(required=False, label='Search') + site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug') + rack_group_id = FilterChoiceField( + required=False, + queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')), + label='Rack Group', + null_option=(0, 'None') + ) + rack_id = FilterChoiceField( + required=False, + queryset=Rack.objects.annotate(filter_count=Count('devices')), + label='Rack', + null_option=(0, 'None') + ) + enabled = forms.ChoiceField(choices=add_blank_choice(IFACE_ENABLED_CHOICES), required=False) + role = FilterChoiceField( + required=False, + queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), + to_field_name='slug', + label='Device Role' + ) + device = forms.CharField(required=False, label='Device Name') + + # # Interface connections # diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index a44097f51..d8e84bbaf 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1168,9 +1168,10 @@ class Interface(models.Model): help_text="This interface is used only for out-of-band management" ) description = models.CharField(max_length=100, blank=True) - objects = InterfaceQuerySet.as_manager() - + + csv_headers = ['device','lag','name','mac_address','form_factor','enabled','description','mtu','mgmt_only','is_virtual','is_wireless','is_connected','is_lag'] + class Meta: ordering = ['device', 'name'] unique_together = ['device', 'name'] @@ -1255,6 +1256,27 @@ class Interface(models.Model): pass return None + def get_status_class(self): + return IFACE_STATUS_CLASSES[self.enabled] + + # Used for export + def to_csv(self): + return csv_format([ + self.device.identifier, + self.lag, + self.name, + self.mac_address, + self.form_factor, + self.enabled, + self.description, + self.mtu, + self.mgmt_only, + self.is_virtual, + self.is_wireless, + self.is_connected, + self.is_lag, + ]) + class InterfaceConnection(models.Model): """ diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 427f0bb42..36bf7b088 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -39,6 +39,12 @@ DEVICE_LINK = """ """ +INTERFACE_LINK = """ + + {{ record.name|default:' - ' }} + +""" + REGION_ACTIONS = """ {% if perms.dcim.change_region %} @@ -97,6 +103,10 @@ DEVICE_STATUS = """ {{ record.get_status_display }} """ +INTERFACE_ENABLED = """ +{{ record.enabled }} +""" + DEVICE_PRIMARY_IP = """ {{ record.primary_ip6.address.ip|default:"" }} {% if record.primary_ip6 and record.primary_ip4 %}{% endif %} @@ -523,3 +533,37 @@ class InterfaceConnectionTable(BaseTable): class Meta(BaseTable.Meta): model = Interface fields = ('device_a', 'interface_a', 'device_b', 'interface_b') + + +class InterfaceImportTable(BaseTable): + device = tables.LinkColumn('dcim:device', accessor=Accessor('interface.device'), + args=[Accessor('interface.device.pk')], verbose_name='Device') + lag = tables.LinkColumn('dcim:interface', accessor=Accessor('self.name'), + args=[Accessor('self.pk')], verbose_name='Lag ID') + name = tables.Column(verbose_name='Interface') + mac_address = tables.Column(verbose_name='MAC Address') + form_factor = tables.Column(verbose_name='Form Factor') + enabled = tables.Column(verbose_name='Enabled') + description = tables.Column(verbose_name='Description') + mtu = tables.Column(verbose_name='MTU') + mgmt_only = tables.Column(verbose_name='MGMT Only') + is_virtual = tables.Column(verbose_name='Is Virtual?') + is_wireless = tables.Column(verbose_name='Is Wireless?') + is_lag = tables.Column(verbose_name='Is Lag?') + class Meta(BaseTable.Meta): + model = Interface + fields = ('device', 'lag','name','mac_address','form_factor','enabled','description','mtu','mgmt_only','is_virtual','is_wireless','is_lag') + + +class InterfaceListTable(BaseTable): + device = tables.LinkColumn('dcim:device', accessor=Accessor('device'), + args=[Accessor('device.pk')], verbose_name='Device') + name = tables.TemplateColumn(template_code=INTERFACE_LINK, verbose_name='Interface') + enabled = tables.TemplateColumn(template_code=INTERFACE_ENABLED, verbose_name='Enabled') + form_factor = tables.Column(verbose_name='Form Factor') + mac_address = tables.Column(verbose_name='MAC Address') + description = tables.Column(verbose_name='Description') + + class Meta(BaseTable.Meta): + model = Interface + fields = ('device','name','enabled','form_factor','mac_address','description') diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 172f634fb..6ef058bf9 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -177,6 +177,8 @@ urlpatterns = [ url(r'^interface-connections/(?P\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'), url(r'^interfaces/(?P\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'), url(r'^interfaces/(?P\d+)/delete/$', views.InterfaceDeleteView.as_view(), name='interface_delete'), + url(r'^interfaces/$', views.InterfaceListView.as_view(), name='interface_list'), + url(r'^interfaces/import/$', views.InterfaceBulkImportView.as_view(), name='interface_import'), # Device bays url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index d2c75fc24..af99bc3eb 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1543,6 +1543,21 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): table = tables.InterfaceTable +class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.change_interface' + model_form = forms.InterfaceCSVForm + table = tables.InterfaceImportTable + default_return_url = 'dcim:interface_list' + + +class InterfaceListView(ObjectListView): + queryset = Interface.objects.all() + filter = filters.InterfaceListFilter + filter_form = forms.InterfaceListFilterForm + table = tables.InterfaceListTable + template_name = 'dcim/interface_list.html' + + # # Device bays # diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 0f6b24077..52de04569 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -389,4 +389,24 @@ td .progress { } textarea { font-family: Consolas, Lucida Console, monospace; +} +.label-enabled { + background-color: #6fd86f; +} +.label-enabled[href]:hover, +.label-enabled[href]:focus { + background-color: #3ccd3c; +} +.label-disabled { + background-color: #d86f6f; +} +.label-disabled[href]:hover, +.label-disabled[href]:focus { + background-color: #cd3c3c; +} +i.fa-comment-o { + display:inline; +} +.iface-description { + display:none; } \ No newline at end of file diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 66f23a4cb..83459e1ad 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -85,6 +85,10 @@ Add a Device Import Devices {% endif %} + Interfaces + {% if perms.dcim.add_interface %} + Import Interfaces + {% endif %} {% if perms.ipam.add_device or perms.ipam.add_devicetype %} {% endif %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 3bf92fbca..59604e2fd 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -374,12 +374,26 @@ {% endif %} + + {% if perms.dcim.add_interface %} + + + Import Interfaces + + + + Export Interfaces + + {% endif %} Interfaces Show IPs + + Show Descriptions + {% if perms.dcim.change_interface and interfaces|length > 1 %} Select all @@ -619,6 +633,20 @@ $('button.toggle-ips').click(function() { $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked'); return false; }); +// Toggle the display of Descriptions under interfaces +$('button.toggle-description').click(function() { + var selected = $(this).attr('selected'); + if (selected) { + $('span.iface-description').hide(); + $('i.fa-comment-o').show(); + } else { + $('span.iface-description').show(); + $('i.fa-comment-o').hide(); + } + $(this).attr('selected', !selected); + $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked'); + return false; +}); diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 75d0f027d..4ae8a0aa9 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -11,8 +11,13 @@ {{ iface.lag.name }} {% endif %} {% if iface.description %} - + {% endif %} + + {% if iface.description %} + {{ iface.description }} + {% endif %} + {{ iface.mtu|default:"" }} {{ iface.mac_address|default:"" }} diff --git a/netbox/templates/dcim/interface_list.html b/netbox/templates/dcim/interface_list.html new file mode 100644 index 000000000..f3346980a --- /dev/null +++ b/netbox/templates/dcim/interface_list.html @@ -0,0 +1,24 @@ +{% extends '_base.html' %} + +{% block title %}Interfaces{% endblock %} + +{% block content %} + + {% if perms.dcim.add_interface %} + + + Import Interfaces + + {% endif %} + {% include 'inc/export_button.html' with obj_type='interfaces' %} + +Interfaces + + + {% include 'responsive_table.html' %} + + + {% include 'inc/search_panel.html' %} + + +{% endblock %}