From aea5612c39813b8278da6af8270f137a796eb287 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Apr 2017 12:32:27 -0400 Subject: [PATCH 01/14] Closes #1110: Expand bulk edit forms to include boolean fields (e.g. toggle is_pool for prefixes) --- netbox/dcim/forms.py | 15 ++++++++++++--- netbox/ipam/forms.py | 8 ++++++-- netbox/utilities/forms.py | 13 +++++++++++++ netbox/utilities/views.py | 4 ++-- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 776976e62..55e127ed4 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -11,9 +11,9 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi from ipam.models import IPAddress 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, CommentField, CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, + Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField, ) from .formfields import MACAddressFormField @@ -272,6 +272,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: @@ -375,7 +376,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 = [] @@ -484,6 +491,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 = [] @@ -1413,6 +1421,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: diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 33f93d26b..d9c2e30c3 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -5,8 +5,8 @@ from dcim.models import Site, Rack, Device, Interface from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm 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, CSVDataField, ExpandableIPAddressField, + FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice, ) from .models import ( @@ -61,6 +61,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: @@ -256,6 +259,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: diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index f59aa6984..610780223 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -138,6 +138,19 @@ class ColorSelect(forms.Select): option_value, selected_html, option_value, force_text(option_label)) +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 diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 1985df2ac..9ed35aaa0 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -423,7 +423,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 +475,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) From 1f7ef15ad168e9d68bbdf711b015e9f0c4e83abb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 2 May 2017 11:43:11 -0400 Subject: [PATCH 02/14] Fixes #1116: Correct object links on recursive deletion error --- netbox/utilities/error_handlers.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/netbox/utilities/error_handlers.py b/netbox/utilities/error_handlers.py index 1691b41cd..e87b6f0e1 100644 --- a/netbox/utilities/error_handlers.py +++ b/netbox/utilities/error_handlers.py @@ -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'{}'.format(o.get_absolute_url(), o)) + for obj in e.protected_objects: + if hasattr(obj, 'get_absolute_url'): + dependent_objects.append(u'{}'.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)) From 6791ff6192cee6b2f9a32d8aacc7fcdfa7d1c7a9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 2 May 2017 15:01:27 -0400 Subject: [PATCH 03/14] Fixes #1125: Include MAC addresses on a device's interface list --- netbox/templates/dcim/inc/consoleport.html | 1 + netbox/templates/dcim/inc/consoleserverport.html | 1 + netbox/templates/dcim/inc/interface.html | 1 + netbox/templates/dcim/inc/poweroutlet.html | 1 + netbox/templates/dcim/inc/powerport.html | 1 + 5 files changed, 5 insertions(+) diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index 5352c949c..e5e13a08f 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -7,6 +7,7 @@ {{ cp.name }} + {% if cp.cs_port %} {{ cp.cs_port.device }} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index d317cf5a4..62563b8dc 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -7,6 +7,7 @@ {{ csp.name }} + {% if csp.connected_console %} {{ csp.connected_console.device }} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 39f2947c5..9732e2641 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -16,6 +16,7 @@
{{ iface.member_interfaces.all|join:", "|default:"No members" }} {% endif %} + {{ iface.mac_address|default:"" }} {% if iface.is_lag %} LAG interface {% elif iface.is_virtual %} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 652ac8e47..1eb609037 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -7,6 +7,7 @@ {{ po.name }} + {% if po.connected_port %} {{ po.connected_port.device }} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index 785186670..202affa22 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -7,6 +7,7 @@ {{ pp.name }} + {% if pp.power_outlet %} {{ pp.power_outlet.device }} From d861d8bfb85871ddde258e78b70e45d1f03cd58e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 2 May 2017 16:46:23 -0400 Subject: [PATCH 04/14] Fixes #1118: Allow designating an IP as primary for a device while editing the IP --- netbox/ipam/forms.py | 49 ++++++++++++++++++++++- netbox/templates/ipam/ipaddress_edit.html | 1 + netbox/utilities/views.py | 9 +---- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index d9c2e30c3..3c47c27df 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -344,10 +344,11 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm): 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'] + fields = ['address', 'vrf', 'tenant', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside'] widgets = { 'interface': APISelect(api_url='/api/dcim/devices/{{interface_device}}/interfaces/'), 'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address') @@ -388,6 +389,15 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm): else: self.fields['interface'].choices = [] + # 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 + if self.instance.nat_inside: nat_inside = self.instance.nat_inside # If the IP is assigned to an interface, populate site/device fields accordingly @@ -420,6 +430,43 @@ class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm): else: self.fields['nat_inside'].choices = [] + def clean(self): + super(IPAddressForm, self).clean() + + # 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." + ) + + def save(self, *args, **kwargs): + + 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: + device.primary_ip6 = ipaddress + device.save() + + # Clear assignment as primary for device if set. + else: + 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') diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html index 82523eba0..531a91990 100644 --- a/netbox/templates/ipam/ipaddress_edit.html +++ b/netbox/templates/ipam/ipaddress_edit.html @@ -28,6 +28,7 @@ {% render_field form.interface_rack %} {% render_field form.interface_device %} {% render_field form.interface %} + {% render_field form.primary_for_device %}
diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 9ed35aaa0..80539a441 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -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, 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 From 572beb2311ec381cc0a30bd68ea21b75cf8b6ecb Mon Sep 17 00:00:00 2001 From: Matt Layher Date: Tue, 2 May 2017 20:39:43 -0400 Subject: [PATCH 05/14] Fix misleading build matrix At one point, I had intended to have a matrix of build badges for each different branch and Python version combination. It seems this is not possible with Travis. This change replaces "python 2.7" with "status" and clarifies that both Python 2.7 and 3.5 are tested, but Python 3.5 is recommended. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 66c35250b..4692438f8 100644 --- a/README.md +++ b/README.md @@ -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** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=master)](https://travis-ci.org/digitalocean/netbox) | | **develop** | [![Build Status](https://travis-ci.org/digitalocean/netbox.svg?branch=develop)](https://travis-ci.org/digitalocean/netbox) | From 4035b876938e50339cf7886bb74b40e278762bb9 Mon Sep 17 00:00:00 2001 From: Brian Ellwood Date: Wed, 3 May 2017 14:30:05 -0400 Subject: [PATCH 06/14] Allow responsive tables (#1124) * Make tables responsive #1115 Resolves #1115 --- netbox/templates/table.html | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/table.html b/netbox/templates/table.html index 8782f0796..bc038bad3 100644 --- a/netbox/templates/table.html +++ b/netbox/templates/table.html @@ -1,4 +1,4 @@ -{% extends 'django_tables2/table.html' %} +{% extends 'django_tables2/boostrap-responsive.html' %} {% load django_tables2 %} {# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #} diff --git a/requirements.txt b/requirements.txt index 9859ec187..e8a4a7784 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ django-debug-toolbar>=1.6 django-filter>=1.0.1 django-mptt==0.8.7 django-rest-swagger>=2.1.0 -django-tables2>=1.2.5 +django-tables2>=1.6.0 djangorestframework>=3.5.0 graphviz>=0.4.10 Markdown>=2.6.7 From 379c24a0128248534746856fefb44bb1a21de4be Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 May 2017 14:32:27 -0400 Subject: [PATCH 07/14] Fixed typo in template --- netbox/templates/table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/table.html b/netbox/templates/table.html index bc038bad3..2733e6848 100644 --- a/netbox/templates/table.html +++ b/netbox/templates/table.html @@ -1,4 +1,4 @@ -{% extends 'django_tables2/boostrap-responsive.html' %} +{% extends 'django_tables2/bootstrap-responsive.html' %} {% load django_tables2 %} {# Extends the stock django_tables2 template to provide custom formatting of the pagination controls #} From 3c631902e15b1c2cbe246f77068b330c5b336367 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 May 2017 15:27:26 -0400 Subject: [PATCH 08/14] Closes #1100: Add a "view all" link to completed bulk import views is_pool for prefixes --- netbox/dcim/views.py | 3 +++ netbox/secrets/views.py | 3 ++- .../dcim/console_connections_import.html | 4 +++- .../dcim/interface_connections_import.html | 4 +++- .../templates/dcim/power_connections_import.html | 4 +++- netbox/templates/import_success.html | 15 +++++++++------ netbox/utilities/views.py | 1 + 7 files changed, 24 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 066faf3d1..c2bc86fc7 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -924,6 +924,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' # @@ -1117,6 +1118,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' # @@ -1528,6 +1530,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' # diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 1d27c538a..75980ca08 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -183,6 +183,7 @@ def secret_import(request): return render(request, 'import_success.html', { 'table': table, + 'return_url': 'secrets:secret_list', }) except IntegrityError as e: @@ -193,7 +194,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', }) diff --git a/netbox/templates/dcim/console_connections_import.html b/netbox/templates/dcim/console_connections_import.html index 6b47ba3bb..31d24e58f 100644 --- a/netbox/templates/dcim/console_connections_import.html +++ b/netbox/templates/dcim/console_connections_import.html @@ -12,7 +12,9 @@ {% csrf_token %} {% render_form form %}
- +
+ +
diff --git a/netbox/templates/dcim/interface_connections_import.html b/netbox/templates/dcim/interface_connections_import.html index 6329e0680..9868a7b55 100644 --- a/netbox/templates/dcim/interface_connections_import.html +++ b/netbox/templates/dcim/interface_connections_import.html @@ -20,7 +20,9 @@ {% csrf_token %} {% render_form form %}
- +
+ +
diff --git a/netbox/templates/dcim/power_connections_import.html b/netbox/templates/dcim/power_connections_import.html index 7c436508a..606694a8d 100644 --- a/netbox/templates/dcim/power_connections_import.html +++ b/netbox/templates/dcim/power_connections_import.html @@ -12,7 +12,9 @@ {% csrf_token %} {% render_form form %}
- +
+ +
diff --git a/netbox/templates/import_success.html b/netbox/templates/import_success.html index 3056e39df..40a8b69af 100644 --- a/netbox/templates/import_success.html +++ b/netbox/templates/import_success.html @@ -4,10 +4,13 @@ {% block title %}Import Completed{% endblock %} {% block content %} -

Import Completed

-{% render_table table %} - - - Import more - +

{% block title %}Import Completed{% endblock %}

+ {% render_table table %} + + + Import more + + {% if return_url %} + View All + {% endif %} {% endblock %} diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 80539a441..b69be08d6 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -395,6 +395,7 @@ class BulkImportView(View): return render(request, "import_success.html", { 'table': obj_table, + 'return_url': self.default_return_url, }) except IntegrityError as e: From 79089cc47e466a1c8f206c1b654885e44e5a9893 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 May 2017 15:41:36 -0400 Subject: [PATCH 09/14] Introduced an object import template --- netbox/templates/circuits/circuit_import.html | 117 ++++++-------- .../templates/circuits/provider_import.html | 97 +++++------- .../dcim/console_connections_import.html | 98 +++++------- netbox/templates/dcim/device_import.html | 8 +- .../templates/dcim/device_import_child.html | 8 +- .../dcim/interface_connections_import.html | 106 +++++-------- .../dcim/power_connections_import.html | 98 +++++------- netbox/templates/dcim/rack_import.html | 147 ++++++++---------- netbox/templates/ipam/aggregate_import.html | 87 +++++------ netbox/templates/ipam/ipaddress_import.html | 127 +++++++-------- netbox/templates/ipam/prefix_import.html | 147 ++++++++---------- netbox/templates/ipam/vlan_import.html | 127 +++++++-------- netbox/templates/ipam/vrf_import.html | 97 +++++------- netbox/templates/secrets/secret_import.html | 12 +- netbox/templates/tenancy/tenant_import.html | 87 +++++------ netbox/templates/utilities/obj_import.html | 34 ++++ 16 files changed, 626 insertions(+), 771 deletions(-) create mode 100644 netbox/templates/utilities/obj_import.html diff --git a/netbox/templates/circuits/circuit_import.html b/netbox/templates/circuits/circuit_import.html index e2fc9fa36..991a99c9b 100644 --- a/netbox/templates/circuits/circuit_import.html +++ b/netbox/templates/circuits/circuit_import.html @@ -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 %} -

Circuit Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
Circuit IDAlphanumeric circuit identifierIC-603122
ProviderName of circuit providerTeliaSonera
TypeCircuit typeTransit
TenantName of tenant (optional)Strickland Propane
Install DateDate in YYYY-MM-DD format (optional)2016-02-23
Commit rateCommited rate in Kbps (optional)2000
DescriptionShort description (optional)Primary for voice
-

Example

-
IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
Circuit IDAlphanumeric circuit identifierIC-603122
ProviderName of circuit providerTeliaSonera
TypeCircuit typeTransit
TenantName of tenant (optional)Strickland Propane
Install DateDate in YYYY-MM-DD format (optional)2016-02-23
Commit rateCommited rate in Kbps (optional)2000
DescriptionShort description (optional)Primary for voice
+

Example

+
IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice
{% endblock %} diff --git a/netbox/templates/circuits/provider_import.html b/netbox/templates/circuits/provider_import.html index a605164df..e60ee3e76 100644 --- a/netbox/templates/circuits/provider_import.html +++ b/netbox/templates/circuits/provider_import.html @@ -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 %} -

Provider Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
NameProvider's proper nameLevel 3
SlugURL-friendly namelevel3
ASNAutonomous system number (optional)3356
AccountAccount number (optional)08931544
Portal URLCustomer service portal URL (optional)https://mylevel3.net
-

Example

-
Level 3,level3,3356,08931544,https://mylevel3.net
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
NameProvider's proper nameLevel 3
SlugURL-friendly namelevel3
ASNAutonomous system number (optional)3356
AccountAccount number (optional)08931544
Portal URLCustomer service portal URL (optional)https://mylevel3.net
+

Example

+
Level 3,level3,3356,08931544,https://mylevel3.net
{% endblock %} diff --git a/netbox/templates/dcim/console_connections_import.html b/netbox/templates/dcim/console_connections_import.html index 31d24e58f..c7308168b 100644 --- a/netbox/templates/dcim/console_connections_import.html +++ b/netbox/templates/dcim/console_connections_import.html @@ -1,63 +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 %} -

Console Connections Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
-
- -
-
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
Console serverDevice name or {ID}abc1-cs3
Console server portFull CS port namePort 35
DeviceDevice name or {ID}abc1-switch7
Console PortConsole port nameConsole
Connection Status"planned" or "connected"planned
-

Example

-
abc1-cs3,Port 35,abc1-switch7,Console,planned
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
Console serverDevice name or {ID}abc1-cs3
Console server portFull CS port namePort 35
DeviceDevice name or {ID}abc1-switch7
Console PortConsole port nameConsole
Connection Status"planned" or "connected"planned
+

Example

+
abc1-cs3,Port 35,abc1-switch7,Console,planned
{% endblock %} diff --git a/netbox/templates/dcim/device_import.html b/netbox/templates/dcim/device_import.html index 50d2f81db..83d0d2195 100644 --- a/netbox/templates/dcim/device_import.html +++ b/netbox/templates/dcim/device_import.html @@ -12,8 +12,12 @@ {% csrf_token %} {% render_form form %}
- - Cancel +
+ + {% if return_url %} + Cancel + {% endif %} +

CSV Format

diff --git a/netbox/templates/dcim/device_import_child.html b/netbox/templates/dcim/device_import_child.html index ca69d7aa5..49433686f 100644 --- a/netbox/templates/dcim/device_import_child.html +++ b/netbox/templates/dcim/device_import_child.html @@ -12,8 +12,12 @@ {% csrf_token %} {% render_form form %}
- - Cancel +
+ + {% if return_url %} + Cancel + {% endif %} +

CSV Format

diff --git a/netbox/templates/dcim/interface_connections_import.html b/netbox/templates/dcim/interface_connections_import.html index 9868a7b55..eab0acdba 100644 --- a/netbox/templates/dcim/interface_connections_import.html +++ b/netbox/templates/dcim/interface_connections_import.html @@ -1,71 +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 %} -

Interface Connections Import

-
-
- {% if form.non_field_errors %} -
-
Errors
-
- {{ form.non_field_errors }} -
-
- {% endif %} -
- {% csrf_token %} - {% render_form form %} -
-
- -
-
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
Device ADevice name or {ID}abc1-core1
Interface AInterface namexe-0/0/6
Device BDevice name or {ID}abc1-switch7
Interface BInterface namexe-0/0/0
Connection Status"planned" or "connected"planned
-

Example

-
abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
Device ADevice name or {ID}abc1-core1
Interface AInterface namexe-0/0/6
Device BDevice name or {ID}abc1-switch7
Interface BInterface namexe-0/0/0
Connection Status"planned" or "connected"planned
+

Example

+
abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned
{% endblock %} diff --git a/netbox/templates/dcim/power_connections_import.html b/netbox/templates/dcim/power_connections_import.html index 606694a8d..56f34c456 100644 --- a/netbox/templates/dcim/power_connections_import.html +++ b/netbox/templates/dcim/power_connections_import.html @@ -1,63 +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 %} -

Power Connections Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
-
- -
-
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
PDUDevice name or {ID}abc1-pdu1
Power OutletPower outlet nameAC4
DeviceDevice name or {ID}abc1-switch7
Power PortPower port namePSU0
Connection Status"planned" or "connected"connected
-

Example

-
abc1-pdu1,AC4,abc1-switch7,PSU0,connected
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
PDUDevice name or {ID}abc1-pdu1
Power OutletPower outlet nameAC4
DeviceDevice name or {ID}abc1-switch7
Power PortPower port namePSU0
Connection Status"planned" or "connected"connected
+

Example

+
abc1-pdu1,AC4,abc1-switch7,PSU0,connected
{% endblock %} diff --git a/netbox/templates/dcim/rack_import.html b/netbox/templates/dcim/rack_import.html index c462a0be9..207fcfcab 100644 --- a/netbox/templates/dcim/rack_import.html +++ b/netbox/templates/dcim/rack_import.html @@ -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 %} -

Rack Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
SiteName of the assigned siteDC-4
GroupRack group name (optional)Cage 1400
NameInternal rack nameR101
Facility IDRack ID assigned by the facility (optional)J12.100
TenantName of tenant (optional)Pied Piper
RoleFunctional role (optional)Compute
TypeRack type (optional)4-post cabinet
WidthRail-to-rail width (19 or 23 inches)19
HeightHeight in rack units42
Descending unitsUnits are numbered top-to-bottomFalse
-

Example

-
DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
SiteName of the assigned siteDC-4
GroupRack group name (optional)Cage 1400
NameInternal rack nameR101
Facility IDRack ID assigned by the facility (optional)J12.100
TenantName of tenant (optional)Pied Piper
RoleFunctional role (optional)Compute
TypeRack type (optional)4-post cabinet
WidthRail-to-rail width (19 or 23 inches)19
HeightHeight in rack units42
Descending unitsUnits are numbered top-to-bottomFalse
+

Example

+
DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False
{% endblock %} diff --git a/netbox/templates/ipam/aggregate_import.html b/netbox/templates/ipam/aggregate_import.html index 8075b4874..1f0a50feb 100644 --- a/netbox/templates/ipam/aggregate_import.html +++ b/netbox/templates/ipam/aggregate_import.html @@ -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 %} -

Aggregate Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
PrefixIPv4 or IPv6 network172.16.0.0/12
RIRName of RIRRFC 1918
Date AddedDate in YYYY-MM-DD format (optional)2016-02-23
DescriptionShort description (optional)Private IPv4 space
-

Example

-
172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
PrefixIPv4 or IPv6 network172.16.0.0/12
RIRName of RIRRFC 1918
Date AddedDate in YYYY-MM-DD format (optional)2016-02-23
DescriptionShort description (optional)Private IPv4 space
+

Example

+
172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space
{% endblock %} diff --git a/netbox/templates/ipam/ipaddress_import.html b/netbox/templates/ipam/ipaddress_import.html index 3c01b4af0..362f64829 100644 --- a/netbox/templates/ipam/ipaddress_import.html +++ b/netbox/templates/ipam/ipaddress_import.html @@ -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 %} -

IP Address Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
AddressIPv4 or IPv6 address192.0.2.42/24
VRFVRF route distinguisher (optional)65000:123
TenantName of tenant (optional)ABC01
StatusCurrent statusActive
DeviceDevice name (optional)switch12
InterfaceInterface name (optional)ge-0/0/31
Is PrimaryIf "true", IP will be primary for device (optional)True
DescriptionShort description (optional)Management IP
-

Example

-
192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
AddressIPv4 or IPv6 address192.0.2.42/24
VRFVRF route distinguisher (optional)65000:123
TenantName of tenant (optional)ABC01
StatusCurrent statusActive
DeviceDevice name (optional)switch12
InterfaceInterface name (optional)ge-0/0/31
Is PrimaryIf "true", IP will be primary for device (optional)True
DescriptionShort description (optional)Management IP
+

Example

+
192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP
{% endblock %} diff --git a/netbox/templates/ipam/prefix_import.html b/netbox/templates/ipam/prefix_import.html index 0a9cc8694..b9aa7ff47 100644 --- a/netbox/templates/ipam/prefix_import.html +++ b/netbox/templates/ipam/prefix_import.html @@ -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 %} -

Prefix Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
PrefixIPv4 or IPv6 network192.168.42.0/24
VRFVRF route distinguisher (optional)65000:123
TenantName of tenant (optional)ABC01
SiteName of assigned site (optional)HQ
VLAN GroupName of group for VLAN selection (optional)Customers
VLAN IDNumeric VLAN ID (optional)801
StatusCurrent statusActive
RoleFunctional role (optional)Customer
Is a poolTrue if all IPs are considered usableFalse
DescriptionShort description (optional)7th floor WiFi
-

Example

-
192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
PrefixIPv4 or IPv6 network192.168.42.0/24
VRFVRF route distinguisher (optional)65000:123
TenantName of tenant (optional)ABC01
SiteName of assigned site (optional)HQ
VLAN GroupName of group for VLAN selection (optional)Customers
VLAN IDNumeric VLAN ID (optional)801
StatusCurrent statusActive
RoleFunctional role (optional)Customer
Is a poolTrue if all IPs are considered usableFalse
DescriptionShort description (optional)7th floor WiFi
+

Example

+
192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi
{% endblock %} diff --git a/netbox/templates/ipam/vlan_import.html b/netbox/templates/ipam/vlan_import.html index 16456ba01..8d1741fd4 100644 --- a/netbox/templates/ipam/vlan_import.html +++ b/netbox/templates/ipam/vlan_import.html @@ -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 %} -

VLAN Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
SiteName of assigned siteLAS2
GroupName of VLAN group (optional)Backend Network
IDConfigured VLAN ID1400
NameConfigured VLAN nameCameras
TenantName of tenant (optional)Internal
StatusCurrent statusActive
RoleFunctional role (optional)Security
DescriptionShort description (optional)Security team only
-

Example

-
LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
SiteName of assigned siteLAS2
GroupName of VLAN group (optional)Backend Network
IDConfigured VLAN ID1400
NameConfigured VLAN nameCameras
TenantName of tenant (optional)Internal
StatusCurrent statusActive
RoleFunctional role (optional)Security
DescriptionShort description (optional)Security team only
+

Example

+
LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only
{% endblock %} diff --git a/netbox/templates/ipam/vrf_import.html b/netbox/templates/ipam/vrf_import.html index 9953542d2..0a1a31205 100644 --- a/netbox/templates/ipam/vrf_import.html +++ b/netbox/templates/ipam/vrf_import.html @@ -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 %} -

VRF Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
NameName of VRFCustomer_ABC
RDRoute distinguisher65000:123456
TenantName of tenant (optional)ABC01
Enforce uniquenessPrevent duplicate prefixes/IP addressesTrue
DescriptionShort description (optional)Native VRF for customer ABC
-

Example

-
Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
NameName of VRFCustomer_ABC
RDRoute distinguisher65000:123456
TenantName of tenant (optional)ABC01
Enforce uniquenessPrevent duplicate prefixes/IP addressesTrue
DescriptionShort description (optional)Native VRF for customer ABC
+

Example

+
Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC
{% endblock %} diff --git a/netbox/templates/secrets/secret_import.html b/netbox/templates/secrets/secret_import.html index 0a9a11c69..ac45861e2 100644 --- a/netbox/templates/secrets/secret_import.html +++ b/netbox/templates/secrets/secret_import.html @@ -20,10 +20,14 @@
{% csrf_token %} {% render_form form %} -
- - Cancel -
+
+
+ + {% if return_url %} + Cancel + {% endif %} +
+
diff --git a/netbox/templates/tenancy/tenant_import.html b/netbox/templates/tenancy/tenant_import.html index 81f82989f..c0e94269a 100644 --- a/netbox/templates/tenancy/tenant_import.html +++ b/netbox/templates/tenancy/tenant_import.html @@ -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 %} -

Tenant Import

-
-
-
- {% csrf_token %} - {% render_form form %} -
- - Cancel -
-
-
-
-

CSV Format

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescriptionExample
NameTenant nameWIDG01
SlugURL-friendly namewidg01
GroupTenant group (optional)Customers
DescriptionLong-form name or other text (optional)Widgets Inc.
-

Example

-
WIDG01,widg01,Customers,Widgets Inc.
-
-
+{% block instructions %} +

CSV Format

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescriptionExample
NameTenant nameWIDG01
SlugURL-friendly namewidg01
GroupTenant group (optional)Customers
DescriptionLong-form name or other text (optional)Widgets Inc.
+

Example

+
WIDG01,widg01,Customers,Widgets Inc.
{% endblock %} diff --git a/netbox/templates/utilities/obj_import.html b/netbox/templates/utilities/obj_import.html new file mode 100644 index 000000000..bea9a2319 --- /dev/null +++ b/netbox/templates/utilities/obj_import.html @@ -0,0 +1,34 @@ +{% extends '_base.html' %} +{% load render_table from django_tables2 %} +{% load form_helpers %} + +{% block content %} +

{% block title %}{% endblock %}

+
+
+ {% if form.non_field_errors %} +
+
Errors
+
+ {{ form.non_field_errors }} +
+
+ {% endif %} +
+ {% csrf_token %} + {% render_form form %} +
+
+ + {% if return_url %} + Cancel + {% endif %} +
+
+
+
+
+ {% block instructions %}{% endblock %} +
+
+{% endblock %} From c047f943de904f80b7e8ab4fa178822c1fd2dc5b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 May 2017 17:12:34 -0400 Subject: [PATCH 10/14] Fixes #403: Record console/power/interface connects and disconnects as user actions --- netbox/dcim/views.py | 167 ++++++++++++++++++++++++++++++------------- 1 file changed, 119 insertions(+), 48 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c2bc86fc7..1f2e9fed7 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -10,12 +10,14 @@ from django.core.urlresolvers import reverse from django.db.models import Count from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render +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.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, @@ -850,12 +852,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 {} {} to {} {}'.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: @@ -879,17 +885,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 {} {} from {} {}'.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: @@ -952,12 +969,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 {} {} to {} {}'.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: @@ -981,8 +1002,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': @@ -992,7 +1014,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 {} {} from {} {}'.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: @@ -1044,12 +1075,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 {} {} to {} {}'.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: @@ -1073,17 +1108,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 {} {} from {} {}'.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: @@ -1146,12 +1192,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 {} {} to {} {}'.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: @@ -1175,7 +1225,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': @@ -1185,7 +1237,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 {} {} from {} {}'.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: @@ -1451,13 +1512,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 {} {} to {} {}'.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 @@ -1495,12 +1562,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 {} {} from {} {}'.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: From a6cb0e0a96193646500597372b5c521bcf228826 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 May 2017 17:24:57 -0400 Subject: [PATCH 11/14] Updated console/power connection icons --- netbox/templates/dcim/inc/consoleport.html | 4 ++-- netbox/templates/dcim/inc/consoleserverport.html | 4 ++-- netbox/templates/dcim/inc/poweroutlet.html | 4 ++-- netbox/templates/dcim/inc/powerport.html | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/netbox/templates/dcim/inc/consoleport.html b/netbox/templates/dcim/inc/consoleport.html index e5e13a08f..58f5fa7de 100644 --- a/netbox/templates/dcim/inc/consoleport.html +++ b/netbox/templates/dcim/inc/consoleport.html @@ -33,11 +33,11 @@ {% endif %} - + {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/consoleserverport.html b/netbox/templates/dcim/inc/consoleserverport.html index 62563b8dc..cfeab9212 100644 --- a/netbox/templates/dcim/inc/consoleserverport.html +++ b/netbox/templates/dcim/inc/consoleserverport.html @@ -33,11 +33,11 @@ {% endif %} - + {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/poweroutlet.html b/netbox/templates/dcim/inc/poweroutlet.html index 1eb609037..eef4874d6 100644 --- a/netbox/templates/dcim/inc/poweroutlet.html +++ b/netbox/templates/dcim/inc/poweroutlet.html @@ -33,11 +33,11 @@ {% endif %} - + {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/inc/powerport.html b/netbox/templates/dcim/inc/powerport.html index 202affa22..ce4ac6967 100644 --- a/netbox/templates/dcim/inc/powerport.html +++ b/netbox/templates/dcim/inc/powerport.html @@ -33,11 +33,11 @@ {% endif %} - + {% else %} - + {% endif %} From b3667befb4cc537e2234236a0249dffb72c7e41f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 5 May 2017 15:24:58 -0400 Subject: [PATCH 12/14] Removed reduntant title block --- netbox/templates/import_success.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/netbox/templates/import_success.html b/netbox/templates/import_success.html index 40a8b69af..04c454e1d 100644 --- a/netbox/templates/import_success.html +++ b/netbox/templates/import_success.html @@ -1,8 +1,6 @@ {% extends '_base.html' %} {% load render_table from django_tables2 %} -{% block title %}Import Completed{% endblock %} - {% block content %}

{% block title %}Import Completed{% endblock %}

{% render_table table %} From fcfcd77bfd05d983f0993482cdcb30683060257e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 5 May 2017 15:37:42 -0400 Subject: [PATCH 13/14] Moved LAG members list to the description column --- netbox/templates/dcim/inc/interface.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 9732e2641..7b3144fd6 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -12,13 +12,13 @@ {% if iface.description %} {% endif %} - {% if iface.is_lag %} -
{{ iface.member_interfaces.all|join:", "|default:"No members" }} - {% endif %} {{ iface.mac_address|default:"" }} {% if iface.is_lag %} - LAG interface + + LAG interface
+ {{ iface.member_interfaces.all|join:", "|default:"No members" }} + {% elif iface.is_virtual %} Virtual interface {% elif iface.connection %} From f40c048475e234ae1402abe502072f2943262feb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 8 May 2017 14:32:29 -0400 Subject: [PATCH 14/14] Fixes #1144: Allow multiple status selections for Prefix, IP address, and VLAN filters --- netbox/ipam/filters.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 3c39a4308..3229ad2b8 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -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():