Closes #8081: Allow creating services directly from navigation menu

This commit is contained in:
jeremystretch 2021-12-15 11:55:27 -05:00
parent f43ec7c05d
commit 8dbd3f332b
11 changed files with 51 additions and 63 deletions

View File

@ -7,6 +7,7 @@
* [#7665](https://github.com/netbox-community/netbox/issues/7665) - Add toggle to show only available child prefixes
* [#8057](https://github.com/netbox-community/netbox/issues/8057) - Dynamic object tables using HTMX
* [#8080](https://github.com/netbox-community/netbox/issues/8080) - Link to NAT IPs for device/VM primary IPs
* [#8081](https://github.com/netbox-community/netbox/issues/8081) - Allow creating services directly from navigation menu
### Bug Fixes

View File

@ -1,7 +1,6 @@
from django.urls import path
from extras.views import ObjectChangeLogView, ObjectJournalView
from ipam.views import ServiceEditView
from utilities.views import SlugRedirectView
from . import views
from .models import *
@ -233,7 +232,6 @@ urlpatterns = [
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
path('devices/<int:device>/services/assign/', ServiceEditView.as_view(), name='device_service_assign'),
# Console ports
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),

View File

@ -809,6 +809,14 @@ class VLANForm(TenancyForm, CustomFieldModelForm):
class ServiceForm(CustomFieldModelForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
required=False
)
virtual_machine = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all(),
required=False
)
ports = NumericArrayField(
base_field=forms.IntegerField(
min_value=SERVICE_PORT_MIN,
@ -816,6 +824,15 @@ class ServiceForm(CustomFieldModelForm):
),
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
)
ipaddresses = DynamicModelMultipleChoiceField(
queryset=IPAddress.objects.all(),
required=False,
label='IP Addresses',
query_params={
'device_id': '$device',
'virtual_machine_id': '$virtual_machine',
}
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
@ -824,7 +841,7 @@ class ServiceForm(CustomFieldModelForm):
class Meta:
model = Service
fields = [
'name', 'protocol', 'ports', 'ipaddresses', 'description', 'tags',
'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'tags',
]
help_texts = {
'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
@ -834,18 +851,3 @@ class ServiceForm(CustomFieldModelForm):
'protocol': StaticSelect(),
'ipaddresses': StaticSelectMultiple(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit IP address choices to those assigned to interfaces of the parent device/VM
if self.instance.device:
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
interface__in=self.instance.device.vc_interfaces().values_list('id', flat=True)
)
elif self.instance.virtual_machine:
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
vminterface__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True)
)
else:
self.fields['ipaddresses'].choices = []

View File

@ -562,18 +562,7 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
# TODO: Update base class to PrimaryObjectViewTestCase
# Blocked by absence of standard creation view
class ServiceTestCase(
ViewTestCases.GetObjectViewTestCase,
ViewTestCases.GetObjectChangelogViewTestCase,
ViewTestCases.EditObjectViewTestCase,
ViewTestCases.DeleteObjectViewTestCase,
ViewTestCases.ListObjectsViewTestCase,
ViewTestCases.BulkImportObjectsViewTestCase,
ViewTestCases.BulkEditObjectsViewTestCase,
ViewTestCases.BulkDeleteObjectsViewTestCase
):
class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Service
@classmethod

View File

@ -164,6 +164,7 @@ urlpatterns = [
# Services
path('services/', views.ServiceListView.as_view(), name='service_list'),
path('services/add/', views.ServiceEditView.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'),

View File

@ -1036,19 +1036,6 @@ class ServiceEditView(generic.ObjectEditView):
model_form = forms.ServiceForm
template_name = 'ipam/service_edit.html'
def alter_obj(self, obj, request, url_args, url_kwargs):
if 'device' in url_kwargs:
obj.device = get_object_or_404(
Device.objects.restrict(request.user),
pk=url_kwargs['device']
)
elif 'virtualmachine' in url_kwargs:
obj.virtual_machine = get_object_or_404(
VirtualMachine.objects.restrict(request.user),
pk=url_kwargs['virtualmachine']
)
return obj
class ServiceBulkImportView(generic.BulkImportView):
queryset = Service.objects.all()

View File

@ -260,7 +260,7 @@ IPAM_MENU = Menu(
label='Other',
items=(
get_model_item('ipam', 'fhrpgroup', 'FHRP Groups'),
get_model_item('ipam', 'service', 'Services', actions=['import']),
get_model_item('ipam', 'service', 'Services'),
),
),
),

View File

@ -290,7 +290,7 @@
</div>
{% if perms.ipam.add_service %}
<div class="card-footer text-end noprint">
<a href="{% url 'dcim:device_service_assign' device=object.pk %}" class="btn btn-sm btn-primary">
<a href="{% url 'ipam:service_add' %}?device={{ object.pk }}" class="btn btn-sm btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Assign Service
</a>
</div>

View File

@ -6,21 +6,33 @@
<div class="row mb-2">
<h5 class="offset-sm-3">Service</h5>
</div>
{% if obj.device %}
<div class="row mb-3">
<label class="col-sm-3 col-form-label text-lg-end">Device</label>
<div class="col">
<input class="form-control" value="{{ obj.device }}" disabled />
</div>
</div>
{% else %}
<div class="row mb-3">
<label class="col-sm-3 col-form-label text-lg-end">Virtual Machine</label>
<div class="col">
<input class="form-control" value="{{ obj.virtual_machine }}" disabled />
</div>
</div>
{% endif %}
<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">
{{ form.initial.device }}
{{ form.initial.virtual_machine }}
<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>
{% render_field form.name %}
<div class="row">
<label class="col-sm-3 col-form-label text-lg-end">Port(s)</label>

View File

@ -167,7 +167,7 @@
</div>
{% if perms.ipam.add_service %}
<div class="card-footer text-end noprint">
<a href="{% url 'virtualization:virtualmachine_service_assign' virtualmachine=object.pk %}" class="btn btn-sm btn-primary">
<a href="{% url 'ipam:service_add' %}?virtual_machine={{ object.pk }}" class="btn btn-sm btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Assign Service
</a>
</div>

View File

@ -1,7 +1,6 @@
from django.urls import path
from extras.views import ObjectChangeLogView, ObjectJournalView
from ipam.views import ServiceEditView
from . import views
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -59,7 +58,6 @@ urlpatterns = [
path('virtual-machines/<int:pk>/config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'),
path('virtual-machines/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}),
path('virtual-machines/<int:pk>/journal/', ObjectJournalView.as_view(), name='virtualmachine_journal', kwargs={'model': VirtualMachine}),
path('virtual-machines/<int:virtualmachine>/services/assign/', ServiceEditView.as_view(), name='virtualmachine_service_assign'),
# VM interfaces
path('interfaces/', views.VMInterfaceListView.as_view(), name='vminterface_list'),