mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
28f577738a
@ -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
|
||||
|
||||
|
@ -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 *
|
||||
|
||||
@ -231,7 +230,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'),
|
||||
|
@ -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 = []
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
|
@ -488,6 +488,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
|
||||
return {
|
||||
'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
|
||||
'active_tab': 'ip-ranges',
|
||||
'first_available_ip': instance.get_first_available_ip(),
|
||||
}
|
||||
|
||||
|
||||
@ -1035,19 +1036,6 @@ class ServiceEditView(generic.ObjectEditView):
|
||||
model_form = forms.ServiceForm
|
||||
template_name = 'ipam/service_edit.html'
|
||||
|
||||
def alter_object(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()
|
||||
|
@ -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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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>
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
{% if show_assigned or show_available is not None %}
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{{ request.path }}{% querystring request show_assigned='true' show_available='false' %}" class="btn btn-sm {% if show_assigned and not show_available %}btn-primary active{% else %}btn-outline-primary{% endif %}">
|
||||
<a href="{{ request.path }}{% querystring request show_assigned='true' show_available='false' %}" class="btn btn-sm {% if show_assigned and not show_available %}btn-secondary active{% else %}btn-outline-secondary{% endif %}">
|
||||
Show Assigned
|
||||
</a>
|
||||
<a href="{{ request.path }}{% querystring request show_assigned='false' show_available='true' %}" class="btn btn-sm {% if show_available and not show_assigned %}btn-primary active{% else %}btn-outline-primary{% endif %}">
|
||||
<a href="{{ request.path }}{% querystring request show_assigned='false' show_available='true' %}" class="btn btn-sm {% if show_available and not show_assigned %}btn-secondary active{% else %}btn-outline-secondary{% endif %}">
|
||||
Show Available
|
||||
</a>
|
||||
<a href="{{ request.path }}{% querystring request show_assigned='true' show_available='true' %}" class="btn btn-sm {% if show_available and show_assigned %}btn-primary active{% else %}btn-outline-primary{% endif %}">
|
||||
<a href="{{ request.path }}{% querystring request show_assigned='true' show_available='true' %}" class="btn btn-sm {% if show_available and show_assigned %}btn-secondary active{% else %}btn-outline-secondary{% endif %}">
|
||||
Show All
|
||||
</a>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.ipam.add_ipaddress and first_available_ip %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}&return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-sm btn-success">
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}&return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-sm btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add IP Address
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -1,6 +1,14 @@
|
||||
{% extends 'ipam/prefix/base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.ipam.add_iprange and first_available_ip %}
|
||||
<a href="{% url 'ipam:iprange_add' %}?start_address={{ first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}&return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-sm btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add IP Range
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
@ -4,7 +4,7 @@
|
||||
{% block extra_controls %}
|
||||
{% include 'ipam/inc/toggle_available.html' %}
|
||||
{% if perms.ipam.add_prefix and first_available_prefix %}
|
||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ object.vrf.pk }}&site={{ object.site.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-sm btn-success">
|
||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ first_available_prefix }}&vrf={{ object.vrf.pk }}&site={{ object.site.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-sm btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Prefix
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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'),
|
||||
|
@ -347,7 +347,7 @@ class VirtualMachineView(generic.ObjectView):
|
||||
class VirtualMachineInterfacesView(generic.ObjectChildrenView):
|
||||
queryset = VirtualMachine.objects.all()
|
||||
child_model = VMInterface
|
||||
table = tables.VMInterfaceTable
|
||||
table = tables.VirtualMachineVMInterfaceTable
|
||||
filterset = filtersets.VMInterfaceFilterSet
|
||||
template_name = 'virtualization/virtualmachine/interfaces.html'
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user