diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index f03f23a3e..fa2a1fd4a 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -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 diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index dd81ca2ba..51d91d7c9 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -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//status/', views.DeviceStatusView.as_view(), name='device_status'), path('devices//lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), path('devices//config/', views.DeviceConfigView.as_view(), name='device_config'), - path('devices//services/assign/', ServiceEditView.as_view(), name='device_service_assign'), # Console ports path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'), diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index 6185b9198..36b9219da 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -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 = [] diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 83de73bde..022ea13c3 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -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 diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index e9bba8fa1..a9f420253 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -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'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 50a40da72..cff845a7a 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -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() diff --git a/netbox/netbox/navigation_menu.py b/netbox/netbox/navigation_menu.py index 0bd29229f..488fa163d 100644 --- a/netbox/netbox/navigation_menu.py +++ b/netbox/netbox/navigation_menu.py @@ -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'), ), ), ), diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index abe9d4deb..b7750a640 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -290,7 +290,7 @@ {% if perms.ipam.add_service %} diff --git a/netbox/templates/ipam/service_edit.html b/netbox/templates/ipam/service_edit.html index ddeffee4a..069ebe933 100644 --- a/netbox/templates/ipam/service_edit.html +++ b/netbox/templates/ipam/service_edit.html @@ -6,21 +6,33 @@
Service
- {% if obj.device %} -
- -
- -
-
- {% else %} -
- -
- -
-
- {% endif %} + +
+
+ +
+
+
+ {{ form.initial.device }} + {{ form.initial.virtual_machine }} +
+ {% render_field form.device %} +
+
+ {% render_field form.virtual_machine %} +
+
{% render_field form.name %}
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 8df9a5002..140d1ab86 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -167,7 +167,7 @@
{% if perms.ipam.add_service %} diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py index d1f8e76e3..bfc5fe6c2 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -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//config-context/', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), path('virtual-machines//changelog/', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}), path('virtual-machines//journal/', ObjectJournalView.as_view(), name='virtualmachine_journal', kwargs={'model': VirtualMachine}), - path('virtual-machines//services/assign/', ServiceEditView.as_view(), name='virtualmachine_service_assign'), # VM interfaces path('interfaces/', views.VMInterfaceListView.as_view(), name='vminterface_list'),