Browse Source

Closes #181: Implemented support for bulk IP address creation

Jeremy Stretch 8 years ago
parent
commit
9fd9719d0b

+ 10 - 1
netbox/ipam/forms.py

@@ -5,7 +5,8 @@ from dcim.models import Site, Rack, Device, Interface
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
-    APISelect, BootstrapMixin, CSVDataField, BulkImportForm, FilterChoiceField, Livesearch, SlugField, add_blank_choice,
+    APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch,
+    SlugField, add_blank_choice,
 )
 )
 
 
 from .models import (
 from .models import (
@@ -339,6 +340,14 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
                 self.fields['nat_inside'].choices = []
                 self.fields['nat_inside'].choices = []
 
 
 
 
+class IPAddressBulkAddForm(forms.Form, BootstrapMixin):
+    address = ExpandableIPAddressField()
+    vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
+    tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
+    status = forms.ChoiceField(choices=IPADDRESS_STATUS_CHOICES)
+    description = forms.CharField(max_length=100, required=False)
+
+
 class IPAddressAssignForm(BootstrapMixin, forms.Form):
 class IPAddressAssignForm(BootstrapMixin, forms.Form):
     site = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
     site = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
                                   widget=forms.Select(attrs={'filter-for': 'rack'}))
                                   widget=forms.Select(attrs={'filter-for': 'rack'}))

+ 1 - 0
netbox/ipam/urls.py

@@ -51,6 +51,7 @@ urlpatterns = [
     # IP addresses
     # IP addresses
     url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
     url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
     url(r'^ip-addresses/add/$', views.IPAddressEditView.as_view(), name='ipaddress_add'),
     url(r'^ip-addresses/add/$', views.IPAddressEditView.as_view(), name='ipaddress_add'),
+    url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkAddView.as_view(), name='ipaddress_bulk_add'),
     url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
     url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
     url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
     url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
     url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
     url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),

+ 9 - 1
netbox/ipam/views.py

@@ -12,7 +12,7 @@ from dcim.models import Device
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator
 from utilities.paginator import EnhancedPaginator
 from utilities.views import (
 from utilities.views import (
-    BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
+    BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
 
 
 from . import filters, forms, tables
 from . import filters, forms, tables
@@ -613,6 +613,14 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     redirect_url = 'ipam:ipaddress_list'
     redirect_url = 'ipam:ipaddress_list'
 
 
 
 
+class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
+    permission_required = 'ipam.add_ipaddress'
+    form = forms.IPAddressBulkAddForm
+    model = IPAddress
+    template_name = 'ipam/ipaddress_bulk_add.html'
+    redirect_url = 'ipam:ipaddress_list'
+
+
 class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
 class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
     permission_required = 'ipam.add_ipaddress'
     permission_required = 'ipam.add_ipaddress'
     form = forms.IPAddressImportForm
     form = forms.IPAddressImportForm

+ 4 - 0
netbox/templates/ipam/inc/ipadress_edit_header.html

@@ -0,0 +1,4 @@
+<ul class="nav nav-tabs" style="margin-bottom: 20px">
+    <li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}><a href="{% url 'ipam:ipaddress_add' %}">Individual</a></li>
+    <li role="presentation"{% if active_tab == 'bulk_add' %} class="active"{% endif %}><a href="{% url 'ipam:ipaddress_bulk_add' %}">Bulk</a></li>
+</ul>

+ 22 - 0
netbox/templates/ipam/ipaddress_bulk_add.html

@@ -0,0 +1,22 @@
+{% extends 'utilities/obj_edit.html' %}
+{% load static from staticfiles %}
+{% load form_helpers %}
+
+{% block title %}Bulk Add IP Addresses{% endblock %}
+
+{% block tabs %}
+    {% include 'ipam/inc/ipadress_edit_header.html' with active_tab='bulk_add' %}
+{% endblock %}
+
+{% block form %}
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>IP Address</strong></div>
+        <div class="panel-body">
+            {% render_field form.address %}
+            {% render_field form.vrf %}
+            {% render_field form.tenant %}
+            {% render_field form.status %}
+            {% render_field form.description %}
+        </div>
+    </div>
+{% endblock %}

+ 6 - 0
netbox/templates/ipam/ipaddress_edit.html

@@ -2,6 +2,12 @@
 {% load static from staticfiles %}
 {% load static from staticfiles %}
 {% load form_helpers %}
 {% load form_helpers %}
 
 
+{% block tabs %}
+    {% if not obj.pk %}
+        {% include 'ipam/inc/ipadress_edit_header.html' with active_tab='add' %}
+    {% endif %}
+{% endblock %}
+
 {% block form %}
 {% block form %}
     <div class="panel panel-default">
     <div class="panel panel-default">
         <div class="panel-heading"><strong>IP Address</strong></div>
         <div class="panel-heading"><strong>IP Address</strong></div>

+ 2 - 5
netbox/templates/utilities/obj_edit.html

@@ -1,10 +1,6 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load form_helpers %}
 {% load form_helpers %}
 
 
-{% block title %}
-    {% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}
-{% endblock %}
-
 {% block content %}
 {% block content %}
     <form action="." method="post" class="form form-horizontal">
     <form action="." method="post" class="form form-horizontal">
         {% csrf_token %}
         {% csrf_token %}
@@ -13,7 +9,8 @@
         {% endfor %}
         {% endfor %}
         <div class="row">
         <div class="row">
             <div class="col-md-6 col-md-offset-3">
             <div class="col-md-6 col-md-offset-3">
-                <h3>{% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}</h3>
+                <h3>{% block title %}{% if obj.pk %}Editing {{ obj_type }} {{ obj }}{% else %}Add a new {{ obj_type }}{% endif %}{% endblock %}</h3>
+                {% block tabs %}{% endblock %}
                 {% if form.non_field_errors %}
                 {% if form.non_field_errors %}
                     <div class="panel panel-danger">
                     <div class="panel panel-danger">
                         <div class="panel-heading"><strong>Errors</strong></div>
                         <div class="panel-heading"><strong>Errors</strong></div>

+ 52 - 1
netbox/utilities/views.py

@@ -3,7 +3,7 @@ from django_tables2 import RequestConfig
 
 
 from django.contrib import messages
 from django.contrib import messages
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ImproperlyConfigured
+from django.core.exceptions import ImproperlyConfigured, ValidationError
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.db import transaction, IntegrityError
 from django.db import transaction, IntegrityError
 from django.db.models import ProtectedError
 from django.db.models import ProtectedError
@@ -254,6 +254,57 @@ class ObjectDeleteView(View):
         })
         })
 
 
 
 
+class BulkAddView(View):
+    form = None
+    model = None
+    template_name = None
+    redirect_url = None
+
+    def get(self, request):
+
+        form = self.form()
+
+        return render(request, self.template_name, {
+            'obj_type': self.model._meta.verbose_name,
+            'form': form,
+            'cancel_url': reverse(self.redirect_url),
+        })
+
+    def post(self, request):
+
+        form = self.form(request.POST)
+        if form.is_valid():
+
+            # The first field will be used as the pattern
+            pattern_field = form.fields.keys()[0]
+            pattern = form.cleaned_data[pattern_field]
+
+            # All other fields will be copied as object attributes
+            kwargs = {k: form.cleaned_data[k] for k in form.fields.keys()[1:]}
+
+            new_objs = []
+            try:
+                with transaction.atomic():
+                    for value in pattern:
+                        obj = self.model(**kwargs)
+                        setattr(obj, pattern_field, value)
+                        obj.full_clean()
+                        obj.save()
+                        new_objs.append(obj)
+            except ValidationError as e:
+                form.add_error(None, e)
+
+            if not form.errors:
+                messages.success(request, u"Added {} {}.".format(len(new_objs), self.model._meta.verbose_name_plural))
+                return redirect(self.redirect_url)
+
+        return render(request, self.template_name, {
+            'form': form,
+            'obj_type': self.model._meta.verbose_name,
+            'cancel_url': reverse(self.redirect_url),
+        })
+
+
 class BulkImportView(View):
 class BulkImportView(View):
     form = None
     form = None
     table = None
     table = None