mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
commit
8cf8710130
22
CHANGELOG.md
22
CHANGELOG.md
@ -1,3 +1,25 @@
|
||||
v2.5.2 (2018-12-21)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#2561](https://github.com/digitalocean/netbox/issues/2561) - Add 200G and 400G interface types
|
||||
* [#2701](https://github.com/digitalocean/netbox/issues/2701) - Enable filtering of prefixes by exact prefix value
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2673](https://github.com/digitalocean/netbox/issues/2673) - Fix exception on LLDP neighbors view for device with a circuit connected
|
||||
* [#2691](https://github.com/digitalocean/netbox/issues/2691) - Cable trace should follow circuits
|
||||
* [#2698](https://github.com/digitalocean/netbox/issues/2698) - Remove pagination restriction on bulk component creation for devices/VMs
|
||||
* [#2704](https://github.com/digitalocean/netbox/issues/2704) - Fix form select widget population on parent with null value
|
||||
* [#2707](https://github.com/digitalocean/netbox/issues/2707) - Correct permission evaluation for circuit termination cabling
|
||||
* [#2712](https://github.com/digitalocean/netbox/issues/2712) - Preserve list filtering after editing objects in bulk
|
||||
* [#2717](https://github.com/digitalocean/netbox/issues/2717) - Fix bulk deletion of tags
|
||||
* [#2721](https://github.com/digitalocean/netbox/issues/2721) - Detect loops when tracing front/rear ports
|
||||
* [#2723](https://github.com/digitalocean/netbox/issues/2723) - Correct permission evaluation when bulk deleting tags
|
||||
* [#2724](https://github.com/digitalocean/netbox/issues/2724) - Limit rear port choices to current device when editing a front port
|
||||
|
||||
---
|
||||
|
||||
v2.5.1 (2018-12-13)
|
||||
|
||||
## Enhancements
|
||||
|
@ -44,7 +44,7 @@ class DeviceConnectionsReport(Report):
|
||||
|
||||
# Check that every console port for every active device has a connection defined.
|
||||
for console_port in ConsolePort.objects.select_related('device').filter(device__status=DEVICE_STATUS_ACTIVE):
|
||||
if console_port.cs_port is None:
|
||||
if console_port.connected_endpoint is None:
|
||||
self.log_failure(
|
||||
console_port.device,
|
||||
"No console connection defined for {}".format(console_port.name)
|
||||
@ -63,7 +63,7 @@ class DeviceConnectionsReport(Report):
|
||||
for device in Device.objects.filter(status=DEVICE_STATUS_ACTIVE):
|
||||
connected_ports = 0
|
||||
for power_port in PowerPort.objects.filter(device=device):
|
||||
if power_port.power_outlet is not None:
|
||||
if power_port.connected_endpoint is not None:
|
||||
connected_ports += 1
|
||||
if power_port.connection_status == CONNECTION_STATUS_PLANNED:
|
||||
self.log_warning(
|
||||
|
@ -95,6 +95,9 @@ AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
|
||||
# Define a group required to login.
|
||||
AUTH_LDAP_REQUIRE_GROUP = "CN=NETBOX_USERS,DC=example,DC=com"
|
||||
|
||||
# Mirror LDAP group assignments.
|
||||
AUTH_LDAP_MIRROR_GROUPS = True
|
||||
|
||||
# Define special user types using groups. Exercise great caution when assigning superuser status.
|
||||
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
|
||||
"is_active": "cn=active,ou=groups,dc=example,dc=com",
|
||||
@ -113,3 +116,21 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
|
||||
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
|
||||
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
|
||||
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
|
||||
|
||||
# Troubleshooting LDAP
|
||||
|
||||
`supervisorctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/supervisor/`.
|
||||
|
||||
For troubleshooting LDAP user/group queries, add the following lines to the start of `ldap_config.py` after `import ldap`.
|
||||
|
||||
```python
|
||||
import logging, logging.handlers
|
||||
logfile = "/opt/netbox/logs/django-ldap-debug.log"
|
||||
my_logger = logging.getLogger('django_auth_ldap')
|
||||
my_logger.setLevel(logging.DEBUG)
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
logfile, maxBytes=1024 * 500, backupCount=5)
|
||||
my_logger.addHandler(handler)
|
||||
```
|
||||
|
||||
Ensure the file and path specified in logfile exist and are writable and executable by the application service account. Restart the netbox service and attempt to log into the site to trigger log entries to this file.
|
||||
|
@ -60,7 +60,7 @@ class CableTraceMixin(object):
|
||||
# Initialize the path array
|
||||
path = []
|
||||
|
||||
for near_end, cable, far_end in obj.trace():
|
||||
for near_end, cable, far_end in obj.trace(follow_circuits=True):
|
||||
|
||||
# Serialize each object
|
||||
serializer_a = get_serializer_for_model(near_end, prefix='Nested')
|
||||
|
@ -82,6 +82,9 @@ IFACE_FF_100GE_CFP2 = 1510
|
||||
IFACE_FF_100GE_CFP4 = 1520
|
||||
IFACE_FF_100GE_CPAK = 1550
|
||||
IFACE_FF_100GE_QSFP28 = 1600
|
||||
IFACE_FF_200GE_CFP2 = 1650
|
||||
IFACE_FF_200GE_QSFP56 = 1700
|
||||
IFACE_FF_400GE_QSFP_DD = 1750
|
||||
# Wireless
|
||||
IFACE_FF_80211A = 2600
|
||||
IFACE_FF_80211G = 2610
|
||||
@ -153,9 +156,12 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
|
||||
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
|
||||
[IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
|
||||
[IFACE_FF_200GE_CFP2, 'CFP2 (200GE)'],
|
||||
[IFACE_FF_100GE_CFP4, 'CFP4 (100GE)'],
|
||||
[IFACE_FF_100GE_CPAK, 'Cisco CPAK (100GE)'],
|
||||
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
|
||||
[IFACE_FF_200GE_QSFP56, 'QSFP56 (200GE)'],
|
||||
[IFACE_FF_400GE_QSFP_DD, 'QSFP-DD (400GE)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
|
5
netbox/dcim/exceptions.py
Normal file
5
netbox/dcim/exceptions.py
Normal file
@ -0,0 +1,5 @@
|
||||
class LoopDetected(Exception):
|
||||
"""
|
||||
A loop has been detected while tracing a cable path.
|
||||
"""
|
||||
pass
|
@ -2098,6 +2098,15 @@ class FrontPortForm(BootstrapMixin, forms.ModelForm):
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Limit RearPort choices to the local device
|
||||
if hasattr(self.instance, 'device'):
|
||||
self.fields['rear_port'].queryset = self.fields['rear_port'].queryset.filter(
|
||||
device=self.instance.device
|
||||
)
|
||||
|
||||
|
||||
# TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
|
||||
class FrontPortCreateForm(ComponentForm):
|
||||
@ -2703,6 +2712,12 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form):
|
||||
to_field_name='slug',
|
||||
null_label='-- None --'
|
||||
)
|
||||
discovered = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
@ -21,6 +21,7 @@ from utilities.managers import NaturalOrderingManager
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object, to_meters
|
||||
from .constants import *
|
||||
from .exceptions import LoopDetected
|
||||
from .fields import ASNField, MACAddressField
|
||||
from .managers import DeviceComponentManager, InterfaceManager
|
||||
|
||||
@ -88,7 +89,7 @@ class CableTermination(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def trace(self, position=1, follow_circuits=False):
|
||||
def trace(self, position=1, follow_circuits=False, cable_history=None):
|
||||
"""
|
||||
Return a list representing a complete cable path, with each individual segment represented as a three-tuple:
|
||||
[
|
||||
@ -133,6 +134,13 @@ class CableTermination(models.Model):
|
||||
if not self.cable:
|
||||
return [(self, None, None)]
|
||||
|
||||
# Record cable history to detect loops
|
||||
if cable_history is None:
|
||||
cable_history = []
|
||||
elif self.cable in cable_history:
|
||||
raise LoopDetected()
|
||||
cable_history.append(self.cable)
|
||||
|
||||
far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a
|
||||
path = [(self, self.cable, far_end)]
|
||||
|
||||
@ -140,7 +148,11 @@ class CableTermination(models.Model):
|
||||
if peer_port is None:
|
||||
return path
|
||||
|
||||
next_segment = peer_port.trace(position)
|
||||
try:
|
||||
next_segment = peer_port.trace(position, follow_circuits, cable_history)
|
||||
except LoopDetected:
|
||||
return path
|
||||
|
||||
if next_segment is None:
|
||||
return path + [(peer_port, None, None)]
|
||||
|
||||
|
@ -29,7 +29,8 @@ SITE_REGION_LINK = """
|
||||
"""
|
||||
|
||||
COLOR_LABEL = """
|
||||
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
|
||||
{% load helpers %}
|
||||
<label class="label" style="color: {{ record.color|fgcolor }}; background-color: #{{ record.color }}">{{ record }}</label>
|
||||
"""
|
||||
|
||||
DEVICE_LINK = """
|
||||
|
@ -1530,6 +1530,7 @@ class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateV
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
model = ConsolePort
|
||||
model_form = forms.ConsolePortForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -1541,6 +1542,7 @@ class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentC
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
model = ConsoleServerPort
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -1552,6 +1554,7 @@ class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateVie
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
model = PowerPort
|
||||
model_form = forms.PowerPortForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -1563,6 +1566,7 @@ class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateV
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
model = PowerOutlet
|
||||
model_form = forms.PowerOutletForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -1574,6 +1578,7 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie
|
||||
form = forms.DeviceBulkAddInterfaceForm
|
||||
model = Interface
|
||||
model_form = forms.InterfaceForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@ -1585,6 +1590,7 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie
|
||||
form = forms.DeviceBulkAddComponentForm
|
||||
model = DeviceBay
|
||||
model_form = forms.DeviceBayForm
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
|
@ -7,19 +7,19 @@ urlpatterns = [
|
||||
|
||||
# Tags
|
||||
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
|
||||
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/$', views.TagView.as_view(), name='tag'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
|
||||
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
|
||||
# Config contexts
|
||||
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
|
||||
url(r'^config-contexts/edit/$', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
|
||||
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
||||
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
|
||||
|
||||
# Image attachments
|
||||
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||
|
@ -82,7 +82,7 @@ class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
|
||||
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuittype'
|
||||
permission_required = 'taggit.delete_tag'
|
||||
queryset = Tag.objects.annotate(
|
||||
items=Count('taggit_taggeditem_items')
|
||||
).order_by(
|
||||
|
@ -112,6 +112,10 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
prefix = django_filters.CharFilter(
|
||||
method='filter_prefix',
|
||||
label='Prefix',
|
||||
)
|
||||
within = django_filters.CharFilter(
|
||||
method='search_within',
|
||||
label='Within prefix',
|
||||
@ -197,6 +201,15 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_prefix(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
try:
|
||||
query = str(netaddr.IPNetwork(value).cidr)
|
||||
return queryset.filter(prefix=query)
|
||||
except ValidationError:
|
||||
return queryset.none()
|
||||
|
||||
def search_within(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
|
@ -22,7 +22,7 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.5.1'
|
||||
VERSION = '2.5.2'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
@ -100,7 +100,7 @@ $(document).ready(function() {
|
||||
} else if (filter_field.val()) {
|
||||
rendered_url = rendered_url.replace(match[0], filter_field.val());
|
||||
} else if (filter_field.attr('nullable') == 'true') {
|
||||
rendered_url = rendered_url.replace(match[0], '0');
|
||||
rendered_url = rendered_url.replace(match[0], 'null');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@
|
||||
<i class="fa fa-angle-right"></i> {{ termination.connected_endpoint }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if perms.circuits.add_cable %}
|
||||
{% if perms.dcim.add_cable %}
|
||||
<div class="pull-right">
|
||||
<a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk %}?return_url={{ circuit.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
|
||||
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i> Connect
|
||||
|
@ -22,13 +22,20 @@
|
||||
{% for iface in interfaces %}
|
||||
<tr id="{{ iface.name }}">
|
||||
<td>{{ iface }}</td>
|
||||
{% if iface.connected_endpoint %}
|
||||
{% if iface.connected_endpoint.device %}
|
||||
<td class="configured_device" data="{{ iface.connected_endpoint.device }}">
|
||||
<a href="{% url 'dcim:device' pk=iface.connected_endpoint.device.pk %}">{{ iface.connected_endpoint.device }}</a>
|
||||
</td>
|
||||
<td class="configured_interface" data="{{ iface.connected_endpoint }}">
|
||||
<span title="{{ iface.connected_endpoint.get_form_factor_display }}">{{ iface.connected_endpoint }}</span>
|
||||
</td>
|
||||
{% elif iface.connected_endpoint.circuit %}
|
||||
{% with circuit=iface.connected_endpoint.circuit %}
|
||||
<td colspan="2">
|
||||
<i class="fa fa-fw fa-globe" title="Circuit"></i>
|
||||
<a href="{{ circuit.get_absolute_url }}">{{ circuit.provider }} {{ circuit }}</a>
|
||||
</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td colspan="2">None</td>
|
||||
{% endif %}
|
||||
|
@ -2,7 +2,8 @@
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Add {{ component_name|title }}</h1>
|
||||
<h1>{% block title %}Add {{ model_name|title }}{% endblock %}</h1>
|
||||
<p>{{ table.rows|length }} {{ parent_model_name }} selected</p>
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if request.POST.return_url %}
|
||||
@ -27,7 +28,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>{{ component_name|title }} to Add</strong></div>
|
||||
<div class="panel-heading"><strong>{{ model_name|title }} to Add</strong></div>
|
||||
<div class="panel-body">
|
||||
{% for field in form.visible_fields %}
|
||||
{% render_field field %}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
from markdown import markdown
|
||||
|
||||
from utilities.forms import unpack_grouped_choices
|
||||
from utilities.utils import foreground_color
|
||||
|
||||
|
||||
register = template.Library()
|
||||
@ -152,6 +154,17 @@ def tzoffset(value):
|
||||
return datetime.datetime.now(value).strftime('%z')
|
||||
|
||||
|
||||
@register.filter()
|
||||
def fgcolor(value):
|
||||
"""
|
||||
Return black (#000000) or white (#ffffff) given an arbitrary background color in RRGGBB format.
|
||||
"""
|
||||
value = value.lower().strip('#')
|
||||
if not re.match('^[0-9a-f]{6}$', value):
|
||||
return ''
|
||||
return '#{}'.format(foreground_color(value))
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
|
@ -55,8 +55,9 @@ class GetReturnURLMixin(object):
|
||||
|
||||
def get_return_url(self, request, obj=None):
|
||||
|
||||
# First, see if `return_url` was specified as a query parameter. Use it only if it's considered safe.
|
||||
query_param = request.GET.get('return_url')
|
||||
# First, see if `return_url` was specified as a query parameter or form data. Use this URL only if it's
|
||||
# considered safe.
|
||||
query_param = request.GET.get('return_url') or request.POST.get('return_url')
|
||||
if query_param and is_safe_url(url=query_param, allowed_hosts=request.get_host()):
|
||||
return query_param
|
||||
|
||||
@ -789,9 +790,12 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
|
||||
|
||||
def post(self, request):
|
||||
|
||||
parent_model_name = self.parent_model._meta.verbose_name_plural
|
||||
model_name = self.model._meta.verbose_name_plural
|
||||
|
||||
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||
if request.POST.get('_all') and self.filter is not None:
|
||||
pk_list = [obj.pk for obj in self.filter(request.GET, self.model.objects.only('pk')).qs]
|
||||
pk_list = [obj.pk for obj in self.filter(request.GET, self.parent_model.objects.only('pk')).qs]
|
||||
else:
|
||||
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||
|
||||
@ -829,9 +833,9 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
|
||||
|
||||
messages.success(request, "Added {} {} to {} {}.".format(
|
||||
len(new_components),
|
||||
self.model._meta.verbose_name_plural,
|
||||
model_name,
|
||||
len(form.cleaned_data['pk']),
|
||||
self.parent_model._meta.verbose_name_plural
|
||||
parent_model_name
|
||||
))
|
||||
return redirect(self.get_return_url(request))
|
||||
|
||||
@ -840,7 +844,8 @@ class BulkComponentCreateView(GetReturnURLMixin, View):
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'component_name': self.model._meta.verbose_name_plural,
|
||||
'parent_model_name': parent_model_name,
|
||||
'model_name': model_name,
|
||||
'table': table,
|
||||
'return_url': self.get_return_url(request),
|
||||
})
|
||||
|
@ -369,5 +369,6 @@ class VirtualMachineBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentC
|
||||
form = forms.VirtualMachineBulkAddInterfaceForm
|
||||
model = Interface
|
||||
model_form = forms.InterfaceForm
|
||||
filter = filters.VirtualMachineFilter
|
||||
table = tables.VirtualMachineTable
|
||||
default_return_url = 'virtualization:virtualmachine_list'
|
||||
|
Loading…
Reference in New Issue
Block a user