Parcourir la source

Enable creating services from templates in the UI

jeremystretch il y a 4 ans
Parent
commit
bb5ded2039

+ 34 - 0
netbox/ipam/forms/models.py

@@ -31,6 +31,7 @@ __all__ = (
     'RoleForm',
     'RouteTargetForm',
     'ServiceForm',
+    'ServiceCreateForm',
     'ServiceTemplateForm',
     'VLANForm',
     'VLANGroupForm',
@@ -880,3 +881,36 @@ class ServiceForm(CustomFieldModelForm):
             'protocol': StaticSelect(),
             'ipaddresses': StaticSelectMultiple(),
         }
+
+
+class ServiceCreateForm(ServiceForm):
+    service_template = DynamicModelChoiceField(
+        queryset=ServiceTemplate.objects.all(),
+        required=False
+    )
+
+    class Meta(ServiceForm.Meta):
+        fields = [
+            'device', 'virtual_machine', 'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description',
+            'tags',
+        ]
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Fields which may be populated from a ServiceTemplate are not required
+        for field in ('name', 'protocol', 'ports'):
+            self.fields[field].required = False
+            del(self.fields[field].widget.attrs['required'])
+
+    def clean(self):
+        if self.cleaned_data['service_template']:
+            # Create a new Service from the specified template
+            service_template = self.cleaned_data['service_template']
+            self.cleaned_data['name'] = service_template.name
+            self.cleaned_data['protocol'] = service_template.protocol
+            self.cleaned_data['ports'] = service_template.ports
+            if not self.cleaned_data['description']:
+                self.cleaned_data['description'] = service_template.description
+        elif not all(self.cleaned_data[f] for f in ('name', 'protocol', 'ports')):
+            raise forms.ValidationError("Must specify name, protocol, and port(s) if not using a service template.")

+ 1 - 1
netbox/ipam/urls.py

@@ -176,7 +176,7 @@ urlpatterns = [
 
     # Services
     path('services/', views.ServiceListView.as_view(), name='service_list'),
-    path('services/add/', views.ServiceEditView.as_view(), name='service_add'),
+    path('services/add/', views.ServiceCreateView.as_view(), name='service_add'),
     path('services/import/', views.ServiceBulkImportView.as_view(), name='service_import'),
     path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
     path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),

+ 6 - 0
netbox/ipam/views.py

@@ -1087,6 +1087,12 @@ class ServiceView(generic.ObjectView):
     queryset = Service.objects.prefetch_related('ipaddresses')
 
 
+class ServiceCreateView(generic.ObjectEditView):
+    queryset = Service.objects.all()
+    model_form = forms.ServiceCreateForm
+    template_name = 'ipam/service_create.html'
+
+
 class ServiceEditView(generic.ObjectEditView):
     queryset = Service.objects.prefetch_related('ipaddresses')
     model_form = forms.ServiceForm

+ 74 - 0
netbox/templates/ipam/service_create.html

@@ -0,0 +1,74 @@
+{% extends 'generic/object_edit.html' %}
+{% load form_helpers %}
+
+{% block form %}
+  <div class="field-group my-5">
+    <div class="row mb-2">
+      <h5 class="offset-sm-3">Service</h5>
+    </div>
+
+    {# Device/VM selection #}
+    <div class="row mb-2">
+      <div class="offset-sm-3">
+        <ul class="nav nav-pills" role="tablist">
+          <li role="presentation" class="nav-item">
+            <button role="tab" type="button" id="device_tab" data-bs-toggle="tab" aria-controls="device" data-bs-target="#device" class="nav-link {% if not form.initial.virtual_machine %}active{% endif %}">
+              Device
+            </button>
+          </li>
+          <li role="presentation" class="nav-item">
+            <button role="tab" type="button" id="vm_tab" data-bs-toggle="tab" aria-controls="vm" data-bs-target="#vm" class="nav-link {% if form.initial.virtual_machine %}active{% endif %}">
+              Virtual Machine
+            </button>
+          </li>
+        </ul>
+      </div>
+    </div>
+    <div class="tab-content p-0 border-0">
+      <div class="tab-pane {% if not form.initial.virtual_machine %}active{% endif %}" id="device" role="tabpanel" aria-labeled-by="device_tab">
+        {% render_field form.device %}
+      </div>
+      <div class="tab-pane {% if form.initial.virtual_machine %}active{% endif %}" id="vm" role="tabpanel" aria-labeled-by="vm_tab">
+        {% render_field form.virtual_machine %}
+      </div>
+    </div>
+
+    {# Template or custom #}
+    <div class="row mb-2">
+      <div class="offset-sm-3">
+        <ul class="nav nav-pills" role="tablist">
+          <li role="presentation" class="nav-item">
+            <button role="tab" type="button" id="template_tab" data-bs-toggle="tab" data-bs-target="#template" class="nav-link active">
+              From Template
+            </button>
+          </li>
+          <li role="presentation" class="nav-item">
+            <button role="tab" type="button" id="custom_tab" data-bs-toggle="tab" data-bs-target="#custom" class="nav-link">
+              Custom
+            </button>
+          </li>
+        </ul>
+      </div>
+    </div>
+    <div class="tab-content p-0 border-0">
+      <div class="tab-pane active" id="template" role="tabpanel" aria-labeled-by="template_tab">
+        {% render_field form.service_template %}
+      </div>
+      <div class="tab-pane" id="custom" role="tabpanel" aria-labeled-by="custom_tab">
+        {% render_field form.name %}
+        {% render_field form.protocol %}
+        {% render_field form.ports %}
+      </div>
+    </div>
+    {% render_field form.ipaddresses %}
+    {% render_field form.description %}
+    {% render_field form.tags %}
+  </div>
+
+  {% if form.custom_fields %}
+    <div class="row mb-2">
+      <h5 class="offset-sm-3">Custom Fields</h5>
+    </div>
+    {% render_custom_fields form %}
+  {% endif %}
+{% endblock %}