Skip to content

Commit 8dbd3f3

Browse files
committed
Closes #8081: Allow creating services directly from navigation menu
1 parent f43ec7c commit 8dbd3f3

File tree

11 files changed

+51
-63
lines changed

11 files changed

+51
-63
lines changed

docs/release-notes/version-3.1.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* [#7665](https://github.com/netbox-community/netbox/issues/7665) - Add toggle to show only available child prefixes
88
* [#8057](https://github.com/netbox-community/netbox/issues/8057) - Dynamic object tables using HTMX
99
* [#8080](https://github.com/netbox-community/netbox/issues/8080) - Link to NAT IPs for device/VM primary IPs
10+
* [#8081](https://github.com/netbox-community/netbox/issues/8081) - Allow creating services directly from navigation menu
1011

1112
### Bug Fixes
1213

netbox/dcim/urls.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from django.urls import path
22

33
from extras.views import ObjectChangeLogView, ObjectJournalView
4-
from ipam.views import ServiceEditView
54
from utilities.views import SlugRedirectView
65
from . import views
76
from .models import *
@@ -233,7 +232,6 @@
233232
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
234233
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
235234
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
236-
path('devices/<int:device>/services/assign/', ServiceEditView.as_view(), name='device_service_assign'),
237235

238236
# Console ports
239237
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),

netbox/ipam/forms/models.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -809,13 +809,30 @@ class Meta:
809809

810810

811811
class ServiceForm(CustomFieldModelForm):
812+
device = DynamicModelChoiceField(
813+
queryset=Device.objects.all(),
814+
required=False
815+
)
816+
virtual_machine = DynamicModelChoiceField(
817+
queryset=VirtualMachine.objects.all(),
818+
required=False
819+
)
812820
ports = NumericArrayField(
813821
base_field=forms.IntegerField(
814822
min_value=SERVICE_PORT_MIN,
815823
max_value=SERVICE_PORT_MAX
816824
),
817825
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen."
818826
)
827+
ipaddresses = DynamicModelMultipleChoiceField(
828+
queryset=IPAddress.objects.all(),
829+
required=False,
830+
label='IP Addresses',
831+
query_params={
832+
'device_id': '$device',
833+
'virtual_machine_id': '$virtual_machine',
834+
}
835+
)
819836
tags = DynamicModelMultipleChoiceField(
820837
queryset=Tag.objects.all(),
821838
required=False
@@ -824,7 +841,7 @@ class ServiceForm(CustomFieldModelForm):
824841
class Meta:
825842
model = Service
826843
fields = [
827-
'name', 'protocol', 'ports', 'ipaddresses', 'description', 'tags',
844+
'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'tags',
828845
]
829846
help_texts = {
830847
'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
@@ -834,18 +851,3 @@ class Meta:
834851
'protocol': StaticSelect(),
835852
'ipaddresses': StaticSelectMultiple(),
836853
}
837-
838-
def __init__(self, *args, **kwargs):
839-
super().__init__(*args, **kwargs)
840-
841-
# Limit IP address choices to those assigned to interfaces of the parent device/VM
842-
if self.instance.device:
843-
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
844-
interface__in=self.instance.device.vc_interfaces().values_list('id', flat=True)
845-
)
846-
elif self.instance.virtual_machine:
847-
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
848-
vminterface__in=self.instance.virtual_machine.interfaces.values_list('id', flat=True)
849-
)
850-
else:
851-
self.fields['ipaddresses'].choices = []

netbox/ipam/tests/test_views.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -562,18 +562,7 @@ def setUpTestData(cls):
562562
}
563563

564564

565-
# TODO: Update base class to PrimaryObjectViewTestCase
566-
# Blocked by absence of standard creation view
567-
class ServiceTestCase(
568-
ViewTestCases.GetObjectViewTestCase,
569-
ViewTestCases.GetObjectChangelogViewTestCase,
570-
ViewTestCases.EditObjectViewTestCase,
571-
ViewTestCases.DeleteObjectViewTestCase,
572-
ViewTestCases.ListObjectsViewTestCase,
573-
ViewTestCases.BulkImportObjectsViewTestCase,
574-
ViewTestCases.BulkEditObjectsViewTestCase,
575-
ViewTestCases.BulkDeleteObjectsViewTestCase
576-
):
565+
class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
577566
model = Service
578567

579568
@classmethod

netbox/ipam/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164

165165
# Services
166166
path('services/', views.ServiceListView.as_view(), name='service_list'),
167+
path('services/add/', views.ServiceEditView.as_view(), name='service_add'),
167168
path('services/import/', views.ServiceBulkImportView.as_view(), name='service_import'),
168169
path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
169170
path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),

netbox/ipam/views.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,19 +1036,6 @@ class ServiceEditView(generic.ObjectEditView):
10361036
model_form = forms.ServiceForm
10371037
template_name = 'ipam/service_edit.html'
10381038

1039-
def alter_obj(self, obj, request, url_args, url_kwargs):
1040-
if 'device' in url_kwargs:
1041-
obj.device = get_object_or_404(
1042-
Device.objects.restrict(request.user),
1043-
pk=url_kwargs['device']
1044-
)
1045-
elif 'virtualmachine' in url_kwargs:
1046-
obj.virtual_machine = get_object_or_404(
1047-
VirtualMachine.objects.restrict(request.user),
1048-
pk=url_kwargs['virtualmachine']
1049-
)
1050-
return obj
1051-
10521039

10531040
class ServiceBulkImportView(generic.BulkImportView):
10541041
queryset = Service.objects.all()

netbox/netbox/navigation_menu.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ def get_model_buttons(app_label, model_name, actions=('add', 'import')):
260260
label='Other',
261261
items=(
262262
get_model_item('ipam', 'fhrpgroup', 'FHRP Groups'),
263-
get_model_item('ipam', 'service', 'Services', actions=['import']),
263+
get_model_item('ipam', 'service', 'Services'),
264264
),
265265
),
266266
),

netbox/templates/dcim/device.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ <h5 class="card-header">
290290
</div>
291291
{% if perms.ipam.add_service %}
292292
<div class="card-footer text-end noprint">
293-
<a href="{% url 'dcim:device_service_assign' device=object.pk %}" class="btn btn-sm btn-primary">
293+
<a href="{% url 'ipam:service_add' %}?device={{ object.pk }}" class="btn btn-sm btn-primary">
294294
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Assign Service
295295
</a>
296296
</div>

netbox/templates/ipam/service_edit.html

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,33 @@
66
<div class="row mb-2">
77
<h5 class="offset-sm-3">Service</h5>
88
</div>
9-
{% if obj.device %}
10-
<div class="row mb-3">
11-
<label class="col-sm-3 col-form-label text-lg-end">Device</label>
12-
<div class="col">
13-
<input class="form-control" value="{{ obj.device }}" disabled />
14-
</div>
15-
</div>
16-
{% else %}
17-
<div class="row mb-3">
18-
<label class="col-sm-3 col-form-label text-lg-end">Virtual Machine</label>
19-
<div class="col">
20-
<input class="form-control" value="{{ obj.virtual_machine }}" disabled />
21-
</div>
22-
</div>
23-
{% endif %}
9+
10+
<div class="row mb-2">
11+
<div class="offset-sm-3">
12+
<ul class="nav nav-pills" role="tablist">
13+
<li role="presentation" class="nav-item">
14+
<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 %}">
15+
Device
16+
</button>
17+
</li>
18+
<li role="presentation" class="nav-item">
19+
<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 %}">
20+
Virtual Machine
21+
</button>
22+
</li>
23+
</ul>
24+
</div>
25+
</div>
26+
<div class="tab-content p-0 border-0">
27+
{{ form.initial.device }}
28+
{{ form.initial.virtual_machine }}
29+
<div class="tab-pane {% if not form.initial.virtual_machine %}active{% endif %}" id="device" role="tabpanel" aria-labeled-by="device_tab">
30+
{% render_field form.device %}
31+
</div>
32+
<div class="tab-pane {% if form.initial.virtual_machine %}active{% endif %}" id="vm" role="tabpanel" aria-labeled-by="vm_tab">
33+
{% render_field form.virtual_machine %}
34+
</div>
35+
</div>
2436
{% render_field form.name %}
2537
<div class="row">
2638
<label class="col-sm-3 col-form-label text-lg-end">Port(s)</label>

netbox/templates/virtualization/virtualmachine.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ <h5 class="card-header">
167167
</div>
168168
{% if perms.ipam.add_service %}
169169
<div class="card-footer text-end noprint">
170-
<a href="{% url 'virtualization:virtualmachine_service_assign' virtualmachine=object.pk %}" class="btn btn-sm btn-primary">
170+
<a href="{% url 'ipam:service_add' %}?virtual_machine={{ object.pk }}" class="btn btn-sm btn-primary">
171171
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Assign Service
172172
</a>
173173
</div>

netbox/virtualization/urls.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from django.urls import path
22

33
from extras.views import ObjectChangeLogView, ObjectJournalView
4-
from ipam.views import ServiceEditView
54
from . import views
65
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
76

@@ -59,7 +58,6 @@
5958
path('virtual-machines/<int:pk>/config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'),
6059
path('virtual-machines/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}),
6160
path('virtual-machines/<int:pk>/journal/', ObjectJournalView.as_view(), name='virtualmachine_journal', kwargs={'model': VirtualMachine}),
62-
path('virtual-machines/<int:virtualmachine>/services/assign/', ServiceEditView.as_view(), name='virtualmachine_service_assign'),
6361

6462
# VM interfaces
6563
path('interfaces/', views.VMInterfaceListView.as_view(), name='vminterface_list'),

0 commit comments

Comments
 (0)