diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index e47e92133..0e116a29f 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -20,7 +20,8 @@ Python 3: ```no-highlight # yum install -y epel-release -# yum install -y gcc python3 python3-devel python3-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel +# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel +# easy_install-3.4 pip ``` Python 2: diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 15903eb7e..9cc4dd3c4 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -95,7 +95,7 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView): model = CircuitType form_class = forms.CircuitTypeForm - def get_return_url(self, obj): + def get_return_url(self, request, obj): return reverse('circuits:circuittype_list') @@ -142,7 +142,6 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'circuits.change_circuit' model = Circuit form_class = forms.CircuitForm - fields_initial = ['provider'] template_name = 'circuits/circuit_edit.html' default_return_url = 'circuits:circuit_list' @@ -230,7 +229,6 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'circuits.change_circuittermination' model = CircuitTermination form_class = forms.CircuitTerminationForm - fields_initial = ['term_side'] template_name = 'circuits/circuittermination_edit.html' def alter_obj(self, obj, request, url_args, url_kwargs): @@ -238,7 +236,7 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView): obj.circuit = get_object_or_404(Circuit, pk=url_kwargs['circuit']) return obj - def get_return_url(self, obj): + def get_return_url(self, request, obj): return obj.circuit.get_absolute_url() diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 351103027..f3a353f5a 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1422,9 +1422,16 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): super(InterfaceBulkEditForm, self).__init__(*args, **kwargs) # Limit LAG choices to interfaces which belong to the parent device. + device = None if self.initial.get('device'): - self.fields['lag'].queryset = Interface.objects.filter( - device=self.initial['device'], form_factor=IFACE_FF_LAG + try: + device = Device.objects.get(pk=self.initial.get('device')) + except Device.DoesNotExist: + pass + if device is not None: + interface_ordering = device.device_type.interface_ordering + self.fields['lag'].queryset = Interface.objects.order_naturally(method=interface_ordering).filter( + device=device, form_factor=IFACE_FF_LAG ) else: self.fields['lag'].choices = [] @@ -1684,36 +1691,6 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form): device = forms.CharField(required=False, label='Device name') -# -# IP addresses -# - -class IPAddressForm(BootstrapMixin, CustomFieldForm): - set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False) - - class Meta: - model = IPAddress - fields = ['address', 'vrf', 'tenant', 'status', 'interface', 'description'] - - def __init__(self, device, *args, **kwargs): - - super(IPAddressForm, self).__init__(*args, **kwargs) - - self.fields['vrf'].empty_label = 'Global' - - interfaces = device.interfaces.all() - self.fields['interface'].queryset = interfaces - self.fields['interface'].required = True - - # If this device has only one interface, select it by default. - if len(interfaces) == 1: - self.fields['interface'].initial = interfaces[0] - - # If this device does not have any IP addresses assigned, default to setting the first IP as its primary. - if not IPAddress.objects.filter(interface__device=device).count(): - self.fields['set_as_primary'].initial = True - - # # Inventory items # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index fcf488b06..023d4e87d 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -121,7 +121,6 @@ urlpatterns = [ url(r'^devices/(?P\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), url(r'^devices/(?P\d+)/inventory/$', views.device_inventory, name='device_inventory'), url(r'^devices/(?P\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'), - url(r'^devices/(?P\d+)/ip-addresses/assign/$', views.ipaddress_assign, name='ipaddress_assign'), url(r'^devices/(?P\d+)/add-secret/$', secret_add, name='device_addsecret'), url(r'^devices/(?P\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'), url(r'^devices/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c6d0f4eaf..9500b1914 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -13,7 +13,7 @@ from django.urls import reverse from django.utils.http import urlencode from django.views.generic import View -from ipam.models import Prefix, IPAddress, Service, VLAN +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 utilities.forms import ConfirmationForm @@ -124,13 +124,13 @@ class ComponentCreateView(View): class ComponentEditView(ObjectEditView): - def get_return_url(self, obj): + def get_return_url(self, request, obj): return obj.device.get_absolute_url() class ComponentDeleteView(ObjectDeleteView): - def get_return_url(self, obj): + def get_return_url(self, request, obj): return obj.device.get_absolute_url() @@ -149,7 +149,7 @@ class RegionEditView(PermissionRequiredMixin, ObjectEditView): model = Region form_class = forms.RegionForm - def get_return_url(self, obj): + def get_return_url(self, request, obj): return reverse('dcim:region_list') @@ -242,7 +242,7 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView): model = RackGroup form_class = forms.RackGroupForm - def get_return_url(self, obj): + def get_return_url(self, request, obj): return reverse('dcim:rackgroup_list') @@ -268,7 +268,7 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView): model = RackRole form_class = forms.RackRoleForm - def get_return_url(self, obj): + def get_return_url(self, request, obj): return reverse('dcim:rackrole_list') @@ -379,7 +379,7 @@ class RackReservationEditView(PermissionRequiredMixin, ObjectEditView): obj.user = request.user return obj - def get_return_url(self, obj): + def get_return_url(self, request, obj): return obj.rack.get_absolute_url() @@ -387,7 +387,7 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_rackreservation' model = RackReservation - def get_return_url(self, obj): + def get_return_url(self, request, obj): return obj.rack.get_absolute_url() @@ -412,7 +412,7 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView): model = Manufacturer form_class = forms.ManufacturerForm - def get_return_url(self, obj): + def get_return_url(self, request, obj): return reverse('dcim:manufacturer_list') @@ -632,7 +632,7 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView): model = DeviceRole form_class = forms.DeviceRoleForm - def get_return_url(self, obj): + def get_return_url(self, request, obj): return reverse('dcim:devicerole_list') @@ -657,7 +657,7 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView): model = Platform form_class = forms.PlatformForm - def get_return_url(self, obj): + def get_return_url(self, request, obj): return reverse('dcim:platform_list') @@ -700,19 +700,15 @@ def device(request, pk): interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ .filter(device=device, mgmt_only=False)\ .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', - 'circuit_termination__circuit') + 'circuit_termination__circuit').prefetch_related('ip_addresses') mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ .filter(device=device, mgmt_only=True)\ .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', - 'circuit_termination__circuit') + 'circuit_termination__circuit').prefetch_related('ip_addresses') device_bays = natsorted( DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), key=attrgetter('name') ) - - # Gather relevant device objects - ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface', 'vrf')\ - .order_by('address') services = Service.objects.filter(device=device) secrets = device.secrets.all() @@ -743,7 +739,6 @@ def device(request, pk): 'interfaces': interfaces, 'mgmt_interfaces': mgmt_interfaces, 'device_bays': device_bays, - 'ip_addresses': ip_addresses, 'services': services, 'secrets': secrets, 'related_devices': related_devices, @@ -755,7 +750,6 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'dcim.change_device' model = Device form_class = forms.DeviceForm - fields_initial = ['site', 'rack', 'position', 'face', 'device_bay'] template_name = 'dcim/device_edit.html' default_return_url = 'dcim:device_list' @@ -1567,47 +1561,6 @@ class InterfaceConnectionsListView(ObjectListView): template_name = 'dcim/interface_connections_list.html' -# -# IP addresses -# - -@permission_required(['dcim.change_device', 'ipam.add_ipaddress']) -def ipaddress_assign(request, pk): - - device = get_object_or_404(Device, pk=pk) - - if request.method == 'POST': - form = forms.IPAddressForm(device, request.POST) - if form.is_valid(): - - ipaddress = form.save(commit=False) - ipaddress.interface = form.cleaned_data['interface'] - ipaddress.save() - form.save_custom_fields() - messages.success(request, u"Added new IP address {} to interface {}.".format(ipaddress, ipaddress.interface)) - - if form.cleaned_data['set_as_primary']: - if ipaddress.family == 4: - device.primary_ip4 = ipaddress - elif ipaddress.family == 6: - device.primary_ip6 = ipaddress - device.save() - - if '_addanother' in request.POST: - return redirect('dcim:ipaddress_assign', pk=device.pk) - else: - return redirect('dcim:device', pk=device.pk) - - else: - form = forms.IPAddressForm(device) - - return render(request, 'dcim/ipaddress_assign.html', { - 'device': device, - 'form': form, - 'return_url': reverse('dcim:device', kwargs={'pk': device.pk}), - }) - - # # Inventory items # diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index d93dccdbe..d68798dbb 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -6,7 +6,7 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi from tenancy.models import Tenant from utilities.forms import ( APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch, - SlugField, add_blank_choice, + ReturnURLForm, SlugField, add_blank_choice, ) from .models import ( @@ -210,28 +210,33 @@ class PrefixFromCSVForm(forms.ModelForm): site = self.cleaned_data.get('site') vlan_group_name = self.cleaned_data.get('vlan_group_name') vlan_vid = self.cleaned_data.get('vlan_vid') - - # Validate VLAN vlan_group = None + vlan = None + + # Validate VLAN group if vlan_group_name: try: vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name) except VLANGroup.DoesNotExist: - self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name)) - if vlan_vid and vlan_group: + if site: + self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name)) + else: + self.add_error('vlan_group_name', "Invalid global VLAN group ({}).".format(vlan_group_name)) + + # Validate VLAN + if vlan_vid: try: - self.instance.vlan = VLAN.objects.get(group=vlan_group, vid=vlan_vid) + self.instance.vlan = VLAN.objects.get(site=site, group=vlan_group, vid=vlan_vid) except VLAN.DoesNotExist: - self.add_error('vlan_vid', "Invalid VLAN ID ({} - {}).".format(vlan_group, vlan_vid)) - elif vlan_vid and site: - try: - self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid) - except VLAN.DoesNotExist: - self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site)) + if site: + self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site)) + elif vlan_group: + self.add_error('vlan_vid', "Invalid VLAN ID ({}) for group {}.".format(vlan_vid, vlan_group_name)) + elif not vlan_group_name: + self.add_error('vlan_vid', "Invalid global VLAN ID ({}).".format(vlan_vid)) except VLAN.MultipleObjectsReturned: self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid)) - elif vlan_vid: - self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.") + self.instance.vlan = vlan def save(self, *args, **kwargs): @@ -302,21 +307,46 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): # IP addresses # -class IPAddressForm(BootstrapMixin, CustomFieldForm): - nat_site = forms.ModelChoiceField(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', - 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') +class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm): + interface_site = forms.ModelChoiceField( + 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', + attrs={'filter-for': 'interface_device'} + ) + ) + interface_device = forms.ModelChoiceField( + queryset=Device.objects.all(), 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'} + ) + ) + nat_site = forms.ModelChoiceField( + 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', + 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' + ) ) class Meta: model = IPAddress - fields = ['address', 'vrf', 'tenant', 'status', 'nat_inside', 'description'] + 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') } @@ -325,8 +355,37 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm): self.fields['vrf'].empty_label = 'Global' - if self.instance.nat_inside: + # 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 + # 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 = [] + + # 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 = [] + + # 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 = [] + + 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: @@ -340,9 +399,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm): ) else: self.fields['nat_inside'].queryset = IPAddress.objects.filter(pk=nat_inside.pk) - 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']) @@ -350,7 +407,6 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm): 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( diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 51ce4f524..a8d7d4528 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -58,8 +58,6 @@ urlpatterns = [ url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), url(r'^ip-addresses/(?P\d+)/$', views.ipaddress, name='ipaddress'), url(r'^ip-addresses/(?P\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'), - url(r'^ip-addresses/(?P\d+)/assign/$', views.ipaddress_assign, name='ipaddress_assign'), - url(r'^ip-addresses/(?P\d+)/remove/$', views.ipaddress_remove, name='ipaddress_remove'), url(r'^ip-addresses/(?P\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), # VLAN groups diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6a10859e1..ae06418ba 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -244,7 +244,7 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView): model = RIR form_class = forms.RIRForm - def get_return_url(self, obj): + def get_return_url(self, request, obj): return reverse('ipam:rir_list') @@ -370,7 +370,7 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView): model = Role form_class = forms.RoleForm - def get_return_url(self, obj): + def get_return_url(self, request, obj): return reverse('ipam:role_list') @@ -464,7 +464,6 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView): model = Prefix form_class = forms.PrefixForm template_name = 'ipam/prefix_edit.html' - fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan'] default_return_url = 'ipam:prefix_list' @@ -572,80 +571,10 @@ def ipaddress(request, pk): }) -@permission_required(['dcim.change_device', 'ipam.change_ipaddress']) -def ipaddress_assign(request, pk): - - ipaddress = get_object_or_404(IPAddress, pk=pk) - - if request.method == 'POST': - form = forms.IPAddressAssignForm(request.POST) - if form.is_valid(): - - interface = form.cleaned_data['interface'] - ipaddress.interface = interface - ipaddress.save() - messages.success(request, u"Assigned IP address {} to interface {}.".format(ipaddress, ipaddress.interface)) - - if form.cleaned_data['set_as_primary']: - device = interface.device - if ipaddress.family == 4: - device.primary_ip4 = ipaddress - elif ipaddress.family == 6: - device.primary_ip6 = ipaddress - device.save() - - return redirect('ipam:ipaddress', pk=ipaddress.pk) - else: - assert False, form.errors - - else: - form = forms.IPAddressAssignForm() - - return render(request, 'ipam/ipaddress_assign.html', { - 'ipaddress': ipaddress, - 'form': form, - 'return_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}), - }) - - -@permission_required(['dcim.change_device', 'ipam.change_ipaddress']) -def ipaddress_remove(request, pk): - - ipaddress = get_object_or_404(IPAddress, pk=pk) - - if request.method == 'POST': - form = ConfirmationForm(request.POST) - if form.is_valid(): - - device = ipaddress.interface.device - ipaddress.interface = None - ipaddress.save() - messages.success(request, u"Removed IP address {} from {}.".format(ipaddress, device)) - - if device.primary_ip4 == ipaddress.pk: - device.primary_ip4 = None - device.save() - elif device.primary_ip6 == ipaddress.pk: - device.primary_ip6 = None - device.save() - - return redirect('ipam:ipaddress', pk=ipaddress.pk) - - else: - form = ConfirmationForm() - - return render(request, 'ipam/ipaddress_unassign.html', { - 'ipaddress': ipaddress, - 'form': form, - 'return_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}), - }) - - class IPAddressEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'ipam.change_ipaddress' model = IPAddress form_class = forms.IPAddressForm - fields_initial = ['address', 'vrf'] template_name = 'ipam/ipaddress_edit.html' default_return_url = 'ipam:ipaddress_list' @@ -718,7 +647,7 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView): model = VLANGroup form_class = forms.VLANGroupForm - def get_return_url(self, obj): + def get_return_url(self, request, obj): return reverse('ipam:vlangroup_list') @@ -807,7 +736,7 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView): obj.device = get_object_or_404(Device, pk=url_kwargs['device']) return obj - def get_return_url(self, obj): + def get_return_url(self, request, obj): return obj.device.get_absolute_url() diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index db37c3535..1eadac7e4 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -316,6 +316,16 @@ li.occupied + li.available { border-top: 1px solid #474747; } +/* Devices */ +table.component-list tr.ipaddress td { + background-color: #eeffff; + padding-bottom: 4px; + padding-top: 4px; +} +table.component-list tr.ipaddress:hover td { + background-color: #e6f7f7; +} + /* Misc */ .banner-bottom { margin-bottom: 50px; diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index e6debfc2e..32f1ca1cf 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -42,7 +42,7 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView): model = SecretRole form_class = forms.SecretRoleForm - def get_return_url(self, obj): + def get_return_url(self, request, obj): return reverse('secrets:secretrole_list') diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 3b0ef5642..df760de21 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -274,10 +274,10 @@ -
+
{% if settings.BANNER_TOP %} {% endif %} {% if settings.MAINTENANCE_MODE %} @@ -286,24 +286,24 @@

NetBox is currently in maintenance mode. Functionality may be limited.

{% endif %} - {% for message in messages %} - - {% endfor %} - {% block content %}{% endblock %} + {% for message in messages %} + + {% endfor %} + {% block content %}{% endblock %}
- {% if settings.BANNER_BOTTOM %} - -
-
+
+
+

{{ settings.HOSTNAME }} (v{{ settings.VERSION }})

@@ -320,8 +320,8 @@

-
-
+
+ diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 4e634d243..82a3a765e 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -194,35 +194,6 @@ {% endif %} {% endif %} -
-
- IP Addresses -
- {% if ip_addresses %} - - {% for ip in ip_addresses %} - {% include 'dcim/inc/ipaddress.html' %} - {% endfor %} -
- {% elif interfaces or mgmt_interfaces %} -
- None assigned -
- {% else %} -
- Create an interface to assign an IP. -
- {% endif %} - {% if perms.ipam.add_ipaddress %} - {% if interfaces or mgmt_interfaces %} - - {% endif %} - {% endif %} -
Services @@ -250,7 +221,7 @@
Critical Connections
- +
{% for iface in mgmt_interfaces %} {% include 'dcim/inc/interface.html' with icon='wrench' %} {% empty %} @@ -389,7 +360,7 @@ {% endif %} -
+
{% for devicebay in device_bays %} {% include 'dcim/inc/devicebay.html' with selectable=True %} {% empty %} @@ -430,6 +401,9 @@
Interfaces
+ {% if perms.dcim.change_interface and interfaces|length > 1 %}
-
+
{% for iface in interfaces %} {% include 'dcim/inc/interface.html' with selectable=True %} {% empty %} @@ -499,7 +473,7 @@ {% endif %} -
+
{% for csp in cs_ports %} {% include 'dcim/inc/consoleserverport.html' with selectable=True %} {% empty %} @@ -551,7 +525,7 @@ {% endif %} -
+
{% for po in power_outlets %} {% include 'dcim/inc/poweroutlet.html' with selectable=True %} {% empty %} @@ -642,6 +616,18 @@ $(".powerport-toggle").click(function() { $(".interface-toggle").click(function() { return toggleConnection($(this), "dcim/interface-connections/"); }); +// Toggle the display of IP addresses under interfaces +$('button.toggle-ips').click(function() { + var selected = $(this).attr('selected'); + if (selected) { + $('#interfaces_table tr.ipaddress').hide(); + } else { + $('#interfaces_table tr.ipaddress').show(); + } + $(this).attr('selected', !selected); + $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked'); + return false; +}); diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index e6c816780..5352c949c 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -1,4 +1,4 @@ - + {% if selectable and perms.dcim.change_consoleport or perms.dcim.delete_consoleport %} - {% if cp.cs_port %} {% endif %} - {% if selectable and perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %} {% endif %} - + {% if selectable and perms.dcim.change_devicebay or perms.dcim.delete_devicebay %} {% endif %} - {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} - - {% if iface.is_virtual %} + {% if iface.is_lag %} + + {% elif iface.is_virtual %} {% elif iface.connection %} {% with iface.connected_interface as connected_iface %} @@ -51,7 +50,7 @@ Not connected {% endif %} - +{% for ip in iface.ip_addresses.all %} + + {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} + + {% endif %} + + + + + +{% endfor %} diff --git a/netbox/templates/dcim/inc/ipaddress.html b/netbox/templates/dcim/inc/ipaddress.html deleted file mode 100644 index 72920986e..000000000 --- a/netbox/templates/dcim/inc/ipaddress.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 241ebc15c..652ac8e47 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -1,4 +1,4 @@ - + {% if selectable and perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %} {% endif %} - {% if selectable and perms.dcim.change_powerport or perms.dcim.delete_powerport %} - {% if pp.power_outlet %} {% endif %} - diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html index c0a210cc1..67fb64eea 100644 --- a/netbox/templates/ipam/ipaddress_edit.html +++ b/netbox/templates/ipam/ipaddress_edit.html @@ -16,39 +16,20 @@ {% render_field form.vrf %} {% render_field form.tenant %} {% render_field form.status %} - {% if obj.pk %} -
- -
-

- {% if obj.interface %} - {{ obj.interface.device }} - Remove - {% else %} - None - {% if obj.pk %} - Assign - {% endif %} - {% endif %} -

-
-
-
- -
-

- {% if obj.interface %} - {{ obj.interface }} - {% else %} - None - {% endif %} -

-
-
- {% endif %} {% render_field form.description %} +
+
+ Interface Assignment +
+
+ {% render_field form.interface_site %} + {% render_field form.interface_rack %} + {% render_field form.interface_device %} + {% render_field form.interface %} +
+
NAT IP (Inside)
diff --git a/netbox/templates/utilities/confirmation_form.html b/netbox/templates/utilities/confirmation_form.html index 8c97056da..16383d6f7 100644 --- a/netbox/templates/utilities/confirmation_form.html +++ b/netbox/templates/utilities/confirmation_form.html @@ -6,13 +6,13 @@
{% csrf_token %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %}
{% block title %}{% endblock %}
{% block message %}

Are you sure?

{% endblock %} - {% for field in form.hidden_fields %} - {{ field }} - {% endfor %}
@@ -7,7 +7,6 @@ {{ cp.name }} {{ cp.cs_port.device }} @@ -20,7 +19,7 @@ Not connected + {% if perms.dcim.change_consoleport %} {% if cp.cs_port %} {% if cp.connection_status %} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index d3c923e43..d317cf5a4 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -1,4 +1,4 @@ - +
@@ -19,7 +19,7 @@ Not connected + {% if perms.dcim.change_consoleserverport %} {% if csp.connected_console %} {% if csp.connected_console.connection_status %} diff --git a/netbox/templates/dcim/inc/devicebay.html b/netbox/templates/dcim/inc/devicebay.html index eacb27440..f69299bfb 100644 --- a/netbox/templates/dcim/inc/devicebay.html +++ b/netbox/templates/dcim/inc/devicebay.html @@ -1,4 +1,4 @@ -
@@ -19,7 +19,7 @@ Vacant + {% if perms.dcim.change_devicebay %} {% if devicebay.installed_device %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 399210193..8d8457d71 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -1,4 +1,4 @@ - +
@@ -16,10 +16,9 @@
{{ iface.member_interfaces.all|join:", "|default:"No members" }} {% endif %}
- {{ iface.mac_address|default:'' }} - LAG interfaceVirtual interface + {% if show_graphs %} {% if iface.circuit_termination or iface.connection %} {% endif %} {% endif %} + {% if perms.ipam.add_ipaddress %} + + + + {% endif %} {% if perms.dcim.change_interface %} {% if not iface.is_virtual %} {% if iface.connection %} @@ -71,19 +75,19 @@ {% endif %} - - + + {% elif iface.circuit_termination and perms.circuits.change_circuittermination %} - + {% else %} - + {% endif %} {% endif %} @@ -104,3 +108,41 @@ {% endif %}
+ {{ ip }} + {% if ip.description %} + + {% endif %} + {% if device.primary_ip4 == ip or device.primary_ip6 == ip %} + Primary + {% endif %} + + {% if ip.vrf %} + {{ ip.vrf }} + {% else %} + Global + {% endif %} + + {{ ip.get_status_display }} + + {% if perms.ipam.edit_ipaddress %} + + + + {% endif %} + {% if perms.ipam.delete_ipaddress %} + + + + {% endif %} +
- {{ ip }} - - {{ ip.vrf|default:"Global" }} - {{ ip.interface }} - {% if device.primary_ip4 == ip or device.primary_ip6 == ip %} - Primary - {% endif %} - - {% if perms.ipam.delete_ipaddress %} - - - - {% endif %} -
@@ -19,7 +19,7 @@ Not connected + {% if perms.dcim.change_poweroutlet %} {% if po.connected_port %} {% if po.connected_port.connection_status %} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index aacb96839..785186670 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -1,4 +1,4 @@ - +
@@ -7,7 +7,6 @@ {{ pp.name }} {{ pp.power_outlet.device }} @@ -20,7 +19,7 @@ Not connected + {% if perms.dcim.change_powerport %} {% if pp.power_outlet %} {% if pp.connection_status %} diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index c4ed601c6..357ea5dac 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -98,14 +98,8 @@ {% if ipaddress.interface %} {{ ipaddress.interface.device }} ({{ ipaddress.interface }}) - {% if perms.dcim.change_device and perms.ipam.change_ipaddress %} - Remove - {% endif %} {% else %} None - {% if perms.dcim.change_device and perms.ipam.change_ipaddress %} - Assign - {% endif %} {% endif %}