From fbc5e74318bd58b3bcb0cdec78627cb76a4df5ef Mon Sep 17 00:00:00 2001 From: Faidon Liambotis Date: Tue, 18 Jul 2017 01:50:26 +0300 Subject: [PATCH 1/4] Allow import/export of regions (#1347) --- netbox/dcim/forms.py | 22 ++++++++++++++++++++++ netbox/dcim/models.py | 11 +++++++++++ netbox/dcim/urls.py | 1 + netbox/dcim/views.py | 7 +++++++ netbox/extras/constants.py | 3 ++- netbox/templates/_base.html | 1 + netbox/templates/dcim/region_list.html | 5 +++++ 7 files changed, 49 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 440c12623..8399b0de6 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -70,6 +70,28 @@ class RegionForm(BootstrapMixin, forms.ModelForm): fields = ['parent', 'name', 'slug'] +class RegionCSVForm(forms.ModelForm): + parent = forms.ModelChoiceField( + queryset=Region.objects.all(), + required=False, + to_field_name='name', + help_text='Name of parent region', + error_messages={ + 'invalid_choice': 'Region not found.', + } + ) + + class Meta: + model = Region + fields = [ + 'name', 'slug', 'parent', + ] + help_texts = { + 'name': 'Region name', + 'slug': 'URL-friendly slug', + } + + # # Sites # diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 8dd11e663..af3387368 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -43,6 +43,10 @@ class Region(MPTTModel): name = models.CharField(max_length=50, unique=True) slug = models.SlugField(unique=True) + csv_headers = [ + 'name', 'slug', 'parent', + ] + class MPTTMeta: order_insertion_by = ['name'] @@ -52,6 +56,13 @@ class Region(MPTTModel): def get_absolute_url(self): return "{}?region={}".format(reverse('dcim:site_list'), self.slug) + def to_csv(self): + return csv_format([ + self.name, + self.slug, + self.parent.name if self.parent else None, + ]) + # # Sites diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 172f634fb..b03b7d030 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -15,6 +15,7 @@ urlpatterns = [ # Regions url(r'^regions/$', views.RegionListView.as_view(), name='region_list'), url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'), + url(r'^regions/import/$', views.RegionBulkImportView.as_view(), name='region_import'), url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), url(r'^regions/(?P\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ea07138d5..ca144afe8 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -202,6 +202,13 @@ class RegionEditView(RegionCreateView): permission_required = 'dcim.change_region' +class RegionBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_region' + model_form = forms.RegionCSVForm + table = tables.RegionTable + default_return_url = 'dcim:region_list' + + class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_region' cls = Region diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 86da90895..0ebd49af0 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -37,7 +37,8 @@ GRAPH_TYPE_CHOICES = ( # Models which support export templates EXPORTTEMPLATE_MODELS = [ - 'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM + 'site', 'region', 'rack', 'device', # DCIM + 'consoleport', 'powerport', 'interfaceconnection', # DCIM 'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM 'provider', 'circuit', # Circuits 'tenant', # Tenants diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 10b4970a8..65cf39b30 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -40,6 +40,7 @@
  • Regions
  • {% if perms.dcim.add_region %}
  • Add a Region
  • +
  • Import Regions
  • {% endif %}
  • Tenants
  • diff --git a/netbox/templates/dcim/region_list.html b/netbox/templates/dcim/region_list.html index b54201a34..f5dde06d8 100644 --- a/netbox/templates/dcim/region_list.html +++ b/netbox/templates/dcim/region_list.html @@ -10,7 +10,12 @@ Add a region + + + Import regions + {% endif %} + {% include 'inc/export_button.html' with obj_type='regions' %}

    {{ block.title }}

    From beb91559e5e2215c671c3ff00e247a86540bcee9 Mon Sep 17 00:00:00 2001 From: Faidon Liambotis Date: Tue, 18 Jul 2017 02:04:54 +0300 Subject: [PATCH 2/4] Allow import/export of rack groups (#1347) --- netbox/dcim/forms.py | 21 +++++++++++++++++++++ netbox/dcim/models.py | 11 +++++++++++ netbox/dcim/urls.py | 1 + netbox/dcim/views.py | 7 +++++++ netbox/extras/constants.py | 2 +- netbox/templates/_base.html | 1 + netbox/templates/dcim/rackgroup_list.html | 5 +++++ 7 files changed, 47 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 8399b0de6..a216bb75f 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -190,6 +190,27 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm): fields = ['site', 'name', 'slug'] +class RackGroupCSVForm(forms.ModelForm): + site = forms.ModelChoiceField( + queryset=Site.objects.all(), + to_field_name='name', + help_text='Name of parent site', + error_messages={ + 'invalid_choice': 'Site not found.', + } + ) + + class Meta: + model = RackGroup + fields = [ + 'site', 'name', 'slug', + ] + help_texts = { + 'name': 'Name of rack group', + 'slug': 'URL-friendly slug', + } + + class RackGroupFilterForm(BootstrapMixin, forms.Form): site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('rack_groups')), to_field_name='slug') diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index af3387368..1cc948643 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -159,6 +159,10 @@ class RackGroup(models.Model): slug = models.SlugField() site = models.ForeignKey('Site', related_name='rack_groups', on_delete=models.CASCADE) + csv_headers = [ + 'site', 'name', 'slug', + ] + class Meta: ordering = ['site', 'name'] unique_together = [ @@ -172,6 +176,13 @@ class RackGroup(models.Model): def get_absolute_url(self): return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk) + def to_csv(self): + return csv_format([ + self.site, + self.name, + self.slug, + ]) + @python_2_unicode_compatible class RackRole(models.Model): diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index b03b7d030..12f8ebf86 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -32,6 +32,7 @@ urlpatterns = [ # Rack groups url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'), url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'), + url(r'^rack-groups/import/$', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'), url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), url(r'^rack-groups/(?P\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ca144afe8..c155774db 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -314,6 +314,13 @@ class RackGroupEditView(RackGroupCreateView): permission_required = 'dcim.change_rackgroup' +class RackGroupBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_rackgroup' + model_form = forms.RackGroupCSVForm + table = tables.RackGroupTable + default_return_url = 'dcim:rackgroup_list' + + class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackgroup' cls = RackGroup diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 0ebd49af0..2c3918f75 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -37,7 +37,7 @@ GRAPH_TYPE_CHOICES = ( # Models which support export templates EXPORTTEMPLATE_MODELS = [ - 'site', 'region', 'rack', 'device', # DCIM + 'site', 'region', 'rack', 'rackgroup', 'device', # DCIM 'consoleport', 'powerport', 'interfaceconnection', # DCIM 'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM 'provider', 'circuit', # Circuits diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 65cf39b30..ebc8bc680 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -68,6 +68,7 @@
  • Rack Groups
  • {% if perms.dcim.add_rackgroup %}
  • Add a Rack Group
  • +
  • Import Rack Groups
  • {% endif %}
  • Rack Roles
  • diff --git a/netbox/templates/dcim/rackgroup_list.html b/netbox/templates/dcim/rackgroup_list.html index dee6472fb..086a7df27 100644 --- a/netbox/templates/dcim/rackgroup_list.html +++ b/netbox/templates/dcim/rackgroup_list.html @@ -10,7 +10,12 @@ Add a rack group + + + Import rack groups + {% endif %} + {% include 'inc/export_button.html' with obj_type='rackgroups' %}

    Rack Groups

    From f30fa925eaa2f562a65f88319aa8681116436867 Mon Sep 17 00:00:00 2001 From: Faidon Liambotis Date: Tue, 18 Jul 2017 02:37:28 +0300 Subject: [PATCH 3/4] Allow import/export of manufacturers (#1347) --- netbox/dcim/forms.py | 12 ++++++++++++ netbox/dcim/models.py | 10 ++++++++++ netbox/dcim/urls.py | 1 + netbox/dcim/views.py | 7 +++++++ netbox/extras/constants.py | 2 +- netbox/templates/_base.html | 1 + netbox/templates/dcim/manufacturer_list.html | 5 +++++ 7 files changed, 37 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index a216bb75f..6d37621c3 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -423,6 +423,18 @@ class ManufacturerForm(BootstrapMixin, forms.ModelForm): fields = ['name', 'slug'] +class ManufacturerCSVForm(forms.ModelForm): + class Meta: + model = Manufacturer + fields = [ + 'name', 'slug' + ] + help_texts = { + 'name': 'Manufacturer name', + 'slug': 'URL-friendly slug', + } + + # # Device types # diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 1cc948643..eb4aae53d 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -455,6 +455,10 @@ class Manufacturer(models.Model): name = models.CharField(max_length=50, unique=True) slug = models.SlugField(unique=True) + csv_headers = [ + 'name', 'slug', + ] + class Meta: ordering = ['name'] @@ -464,6 +468,12 @@ class Manufacturer(models.Model): def get_absolute_url(self): return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug) + def to_csv(self): + return csv_format([ + self.name, + self.slug, + ]) + @python_2_unicode_compatible class DeviceType(models.Model, CustomFieldModel): diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 12f8ebf86..ffd9b9005 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -64,6 +64,7 @@ urlpatterns = [ # Manufacturers url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'), url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'), + url(r'^manufacturers/import/$', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'), url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), url(r'^manufacturers/(?P[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c155774db..6766ab246 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -561,6 +561,13 @@ class ManufacturerEditView(ManufacturerCreateView): permission_required = 'dcim.change_manufacturer' +class ManufacturerBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_manufacturer' + model_form = forms.ManufacturerCSVForm + table = tables.ManufacturerTable + default_return_url = 'dcim:manufacturer_list' + + class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_manufacturer' cls = Manufacturer diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 2c3918f75..815c896c5 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -37,7 +37,7 @@ GRAPH_TYPE_CHOICES = ( # Models which support export templates EXPORTTEMPLATE_MODELS = [ - 'site', 'region', 'rack', 'rackgroup', 'device', # DCIM + 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'device', # DCIM 'consoleport', 'powerport', 'interfaceconnection', # DCIM 'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM 'provider', 'circuit', # Circuits diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index ebc8bc680..d41b2c66f 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -105,6 +105,7 @@
  • Manufacturers
  • {% if perms.dcim.add_manufacturer %}
  • Add a Manufacturer
  • +
  • Import Manufacturers
  • {% endif %} {% if perms.dcim.add_manufacturer or perms.dcim.add_platform %}
  • diff --git a/netbox/templates/dcim/manufacturer_list.html b/netbox/templates/dcim/manufacturer_list.html index d535e11e8..e0d47dba1 100644 --- a/netbox/templates/dcim/manufacturer_list.html +++ b/netbox/templates/dcim/manufacturer_list.html @@ -10,7 +10,12 @@ Add a manufacturer + + + Import manufacturers + {% endif %} + {% include 'inc/export_button.html' with obj_type='manufacturers' %}

    Manufacturers

    From 4544893b4cbf3301ee66b3dbc79f5cbf6217ba77 Mon Sep 17 00:00:00 2001 From: Faidon Liambotis Date: Tue, 18 Jul 2017 04:50:24 +0300 Subject: [PATCH 4/4] Allow import/export of device types (#1347) --- netbox/dcim/forms.py | 33 +++++++++++++++++++++- netbox/dcim/models.py | 20 +++++++++++++ netbox/dcim/urls.py | 1 + netbox/dcim/views.py | 7 +++++ netbox/extras/constants.py | 2 +- netbox/templates/_base.html | 1 + netbox/templates/dcim/devicetype_list.html | 5 ++++ 7 files changed, 67 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6d37621c3..bf6516b55 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -24,7 +24,7 @@ from .models import ( IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN, - Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, + Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES, ) @@ -451,6 +451,37 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm): } +class DeviceTypeCSVForm(forms.ModelForm): + manufacturer = forms.ModelChoiceField( + queryset=Manufacturer.objects.all(), + required=True, + to_field_name='name', + help_text='Manufacturer name', + error_messages={ + 'invalid_choice': 'Manufacturer not found.', + } + ) + subdevice_role = CSVChoiceField( + choices=SUBDEVICE_ROLE_CHOICES, + required=False, + help_text='Parent/child status' + ) + interface_ordering = CSVChoiceField( + choices=IFACE_ORDERING_CHOICES, + required=False, + help_text='Interface ordering' + ) + + class Meta: + model = DeviceType + fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', + 'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments'] + help_texts = { + 'model': 'Model name', + 'slug': 'URL-friendly slug', + } + + class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput) manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index eb4aae53d..e24a05e18 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -513,6 +513,11 @@ class DeviceType(models.Model, CustomFieldModel): comments = models.TextField(blank=True) custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') + csv_headers = [ + 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', + 'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', + ] + class Meta: ordering = ['manufacturer', 'model'] unique_together = [ @@ -532,6 +537,21 @@ class DeviceType(models.Model, CustomFieldModel): def get_absolute_url(self): return reverse('dcim:devicetype', args=[self.pk]) + def to_csv(self): + return csv_format([ + self.manufacturer.name, + self.model, + self.slug, + self.part_number, + self.u_height, + self.is_full_depth, + self.is_console_server, + self.is_pdu, + self.is_network_device, + self.get_subdevice_role_display() if self.subdevice_role else None, + self.get_interface_ordering_display(), + ]) + def clean(self): # If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index ffd9b9005..63b933d31 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -71,6 +71,7 @@ urlpatterns = [ # Device types url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'), url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), + url(r'^device-types/import/$', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'), url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), url(r'^device-types/(?P\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 6766ab246..0e3114b8c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -658,6 +658,13 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView): default_return_url = 'dcim:devicetype_list' +class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView): + permission_required = 'dcim.add_devicetype' + model_form = forms.DeviceTypeCSVForm + table = tables.DeviceTypeTable + default_return_url = 'dcim:devicetype_list' + + class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_devicetype' cls = DeviceType diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 815c896c5..51bd97159 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -37,7 +37,7 @@ GRAPH_TYPE_CHOICES = ( # Models which support export templates EXPORTTEMPLATE_MODELS = [ - 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'device', # DCIM + 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', # DCIM 'consoleport', 'powerport', 'interfaceconnection', # DCIM 'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM 'provider', 'circuit', # Circuits diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index d41b2c66f..d718f60c1 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -93,6 +93,7 @@
  • Device Types
  • {% if perms.dcim.add_devicetype %}
  • Add a Device Type
  • +
  • Import Device Types
  • {% endif %}
  • Device Roles
  • diff --git a/netbox/templates/dcim/devicetype_list.html b/netbox/templates/dcim/devicetype_list.html index 5ab97a481..c06633ef6 100644 --- a/netbox/templates/dcim/devicetype_list.html +++ b/netbox/templates/dcim/devicetype_list.html @@ -10,7 +10,12 @@ Add a device type + + + Import device types + {% endif %} + {% include 'inc/export_button.html' with obj_type='devicetypes' %}

    Device Types