1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into develop-2.7

This commit is contained in:
Jeremy Stretch
2019-09-25 13:44:29 -04:00
24 changed files with 326 additions and 314 deletions

View File

@@ -1,7 +1,7 @@
from django import forms
from taggit.forms import TagField
from dcim.models import Site
from dcim.models import Region, Site
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.forms import TenancyFilterForm
@@ -268,7 +268,9 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Circuit
field_order = ['q', 'type', 'provider', 'status', 'site', 'tenant_group', 'tenant', 'commit_rate']
field_order = [
'q', 'type', 'provider', 'status', 'region', 'site', 'tenant_group', 'tenant', 'commit_rate',
]
q = forms.CharField(
required=False,
label='Search'
@@ -294,6 +296,15 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
required=False,
widget=StaticSelect2Multiple()
)
region = forms.ModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
)
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',

View File

@@ -2106,6 +2106,10 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
choices=add_blank_choice(POWERFEED_LEG_CHOICES),
required=False,
)
power_port = forms.ModelChoiceField(
queryset=PowerPort.objects.all(),
required=False
)
description = forms.CharField(
max_length=100,
required=False
@@ -2113,9 +2117,15 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
class Meta:
nullable_fields = [
'feed_leg', 'description',
'feed_leg', 'power_port', 'description',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit power_port queryset to PowerPorts which belong to the parent Device
self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent_obj)
class PowerOutletBulkRenameForm(BulkRenameForm):
pk = forms.ModelMultipleChoiceField(
@@ -2220,7 +2230,7 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
[(vlan.pk, vlan) for vlan in site_group_vlans]
))
self.fields['untagged_vlan'].choices = vlan_choices
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
self.fields['tagged_vlans'].choices = vlan_choices
@@ -2330,7 +2340,7 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
[(vlan.pk, vlan) for vlan in site_group_vlans]
))
self.fields['untagged_vlan'].choices = vlan_choices
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
self.fields['tagged_vlans'].choices = vlan_choices
@@ -2442,7 +2452,7 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
[(vlan.pk, vlan) for vlan in site_group_vlans]
))
self.fields['untagged_vlan'].choices = vlan_choices
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
self.fields['tagged_vlans'].choices = vlan_choices

View File

@@ -732,10 +732,21 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
def get_utilization(self):
"""
Determine the utilization rate of the rack and return it as a percentage.
Determine the utilization rate of the rack and return it as a percentage. Occupied and reserved units both count
as utilized.
"""
u_available = len(self.get_available_units())
return int(float(self.u_height - u_available) / self.u_height * 100)
# Determine unoccupied units
available_units = self.get_available_units()
# Remove reserved units
for u in self.get_reserved_units():
if u in available_units:
available_units.remove(u)
occupied_unit_count = self.u_height - len(available_units)
percentage = int(float(occupied_unit_count) / self.u_height * 100)
return percentage
def get_power_utilization(self):
"""
@@ -2785,6 +2796,20 @@ class Cable(ChangeLoggedModel):
type_a = self.termination_a_type.model
type_b = self.termination_b_type.model
# Validate interface types
if type_a == 'interface' and self.termination_a.type in NONCONNECTABLE_IFACE_TYPES:
raise ValidationError({
'termination_a_id': 'Cables cannot be terminated to {} interfaces'.format(
self.termination_a.get_type_display()
)
})
if type_b == 'interface' and self.termination_b.type in NONCONNECTABLE_IFACE_TYPES:
raise ValidationError({
'termination_b_id': 'Cables cannot be terminated to {} interfaces'.format(
self.termination_b.get_type_display()
)
})
# Check that termination types are compatible
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
raise ValidationError("Incompatible termination types: {} and {}".format(
@@ -2826,20 +2851,6 @@ class Cable(ChangeLoggedModel):
self.termination_b, self.termination_b.cable_id
))
# Virtual interfaces cannot be connected
endpoint_a, endpoint_b, _ = self.get_path_endpoints()
if (
(
isinstance(endpoint_a, Interface) and
endpoint_a.type == IFACE_TYPE_VIRTUAL
) or
(
isinstance(endpoint_b, Interface) and
endpoint_b.type == IFACE_TYPE_VIRTUAL
)
):
raise ValidationError("Cannot connect to a virtual interface")
# Validate length and length_unit
if self.length is not None and self.length_unit is None:
raise ValidationError("Must specify a unit when setting a cable length")

View File

@@ -74,7 +74,8 @@ RACKROLE_ACTIONS = """
RACK_ROLE = """
{% if record.role %}
<label class="label" style="background-color: #{{ record.role.color }}">{{ value }}</label>
{% load helpers %}
<label class="label" style="color: {{ record.role.color|fgcolor }}; background-color: #{{ record.role.color }}">{{ value }}</label>
{% else %}
&mdash;
{% endif %}

View File

@@ -343,7 +343,7 @@ class CableTestCase(TestCase):
def test_cable_validates_compatibale_types(self):
"""
The clean method should have a check to ensure only compatiable port types can be connected by a cable
The clean method should have a check to ensure only compatible port types can be connected by a cable
"""
# An interface cannot be connected to a power port
cable = Cable(termination_a=self.interface1, termination_b=self.power_port1)
@@ -360,30 +360,39 @@ class CableTestCase(TestCase):
def test_cable_front_port_cannot_connect_to_corresponding_rear_port(self):
"""
A cable cannot connect a front port to its sorresponding rear port
A cable cannot connect a front port to its corresponding rear port
"""
cable = Cable(termination_a=self.front_port, termination_b=self.rear_port)
with self.assertRaises(ValidationError):
cable.clean()
def test_cable_cannot_be_connected_to_an_existing_connection(self):
def test_cable_cannot_terminate_to_an_existing_connection(self):
"""
Either side of a cable cannot be terminated when that side aready has a connection
Either side of a cable cannot be terminated when that side already has a connection
"""
# Try to create a cable with the same interface terminations
cable = Cable(termination_a=self.interface2, termination_b=self.interface1)
with self.assertRaises(ValidationError):
cable.clean()
def test_cable_cannot_connect_to_a_virtual_inteface(self):
def test_cable_cannot_terminate_to_a_virtual_inteface(self):
"""
A cable connection cannot include a virtual interface
A cable cannot terminate to a virtual interface
"""
virtual_interface = Interface(device=self.device1, name="V1", type=0)
virtual_interface = Interface(device=self.device1, name="V1", type=IFACE_TYPE_VIRTUAL)
cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
with self.assertRaises(ValidationError):
cable.clean()
def test_cable_cannot_terminate_to_a_wireless_inteface(self):
"""
A cable cannot terminate to a wireless interface
"""
wireless_interface = Interface(device=self.device1, name="W1", type=IFACE_TYPE_80211A)
cable = Cable(termination_a=self.interface2, termination_b=wireless_interface)
with self.assertRaises(ValidationError):
cable.clean()
class CablePathTestCase(TestCase):

View File

@@ -222,8 +222,8 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
class Meta:
model = ObjectChange
fields = [
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object',
'object_data',
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
'changed_object', 'object_data',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)

View File

@@ -107,6 +107,7 @@ EXPORTTEMPLATE_MODELS = [
'dcim.device',
'dcim.devicetype',
'dcim.interface',
'dcim.inventoryitem',
'dcim.manufacturer',
'dcim.powerpanel',
'dcim.powerport',

View File

@@ -212,7 +212,9 @@ class ObjectChangeFilter(django_filters.FilterSet):
class Meta:
model = ObjectChange
fields = ['user', 'user_name', 'request_id', 'action', 'changed_object_type', 'object_repr']
fields = [
'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id', 'object_repr',
]
def search(self, queryset, name, value):
if not value.strip():

View File

@@ -432,14 +432,19 @@ class ExportTemplate(models.Model):
choices=TEMPLATE_LANGUAGE_CHOICES,
default=TEMPLATE_LANGUAGE_JINJA2
)
template_code = models.TextField()
template_code = models.TextField(
help_text='The list of objects being exported is passed as a context variable named <code>queryset</code>.'
)
mime_type = models.CharField(
max_length=50,
blank=True
blank=True,
verbose_name='MIME type',
help_text='Defaults to <code>text/plain</code>'
)
file_extension = models.CharField(
max_length=15,
blank=True
blank=True,
help_text='Extension to append to the rendered filename'
)
class Meta:

View File

@@ -382,7 +382,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
def to_csv(self):
return (
self.prefix,
self.vrf.rd if self.vrf else None,
self.vrf.name if self.vrf else None,
self.tenant.name if self.tenant else None,
self.site.name if self.site else None,
self.vlan.group.name if self.vlan and self.vlan.group else None,
@@ -674,7 +674,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
return (
self.address,
self.vrf.rd if self.vrf else None,
self.vrf.name if self.vrf else None,
self.tenant.name if self.tenant else None,
self.get_status_display(),
self.get_role_display(),

View File

@@ -133,116 +133,6 @@ input[name="pk"] {
margin-top: 0;
}
/* Color Selections */
.color-selection-aa1409 {
background-color: #aa1409;
color: #ffffff;
}
.color-selection-f44336 {
background-color: #f44336;
color: #ffffff;
}
.color-selection-e91e63 {
background-color: #e91e63;
color: #ffffff;
}
.color-selection-ffe4e1 {
background-color: #ffe4e1;
color: #000000;
}
.color-selection-ff66ff {
background-color: #ff66ff;
color: #ffffff;
}
.color-selection-9c27b0 {
background-color: #9c27b0;
color: #ffffff;
}
.color-selection-673ab7 {
background-color: #673ab7;
color: #ffffff;
}
.color-selection-3f51b5 {
background-color: #3f51b5;
color: #ffffff;
}
.color-selection-2196f3 {
background-color: #2196f3;
color: #ffffff;
}
.color-selection-03a9f4 {
background-color: #03a9f4;
color: #ffffff;
}
.color-selection-00bcd4 {
background-color: #00bcd4;
color: #ffffff;
}
.color-selection-009688 {
background-color: #009688;
color: #ffffff;
}
.color-selection-00ffff {
background-color: #00ffff;
color: #ffffff;
}
.color-selection-2f6a31 {
background-color: #2f6a31;
color: #ffffff;
}
.color-selection-4caf50 {
background-color: #4caf50;
color: #ffffff;
}
.color-selection-8bc34a {
background-color: #8bc34a;
color: #ffffff;
}
.color-selection-cddc39 {
background-color: #cddc39;
color: #000000;
}
.color-selection-ffeb3b {
background-color: #ffeb3b;
color: #000000;
}
.color-selection-ffc107 {
background-color: #ffc107;
color: #000000;
}
.color-selection-ff9800 {
background-color: #ff9800;
color: #ffffff;
}
.color-selection-ff5722 {
background-color: #ff5722;
color: #ffffff;
}
.color-selection-795548 {
background-color: #795548;
color: #ffffff;
}
.color-selection-c0c0c0 {
background-color: #c0c0c0;
color: #000000;
}
.color-selection-9e9e9e {
background-color: #9e9e9e;
color: #ffffff;
}
.color-selection-607d8b {
background-color: #607d8b;
color: #ffffff;
}
.color-selection-111111 {
background-color: #111111;
color: #ffffff;
}
.color-selection-ffffff {
background-color: #ffffff;
color: #000000;
}
/* Tables */
th.pk, td.pk {

View File

@@ -75,7 +75,7 @@ $(document).ready(function() {
var rendered_url = url;
var filter_field;
while (match = filter_regex.exec(url)) {
filter_field = $('#id_' + match[1]);untagged
filter_field = $('#id_' + match[1]);
var custom_attr = $('option:selected', filter_field).attr('api-value');
if (custom_attr) {
rendered_url = rendered_url.replace(match[0], custom_attr);
@@ -91,11 +91,8 @@ $(document).ready(function() {
// Assign color picker selection classes
function colorPickerClassCopy(data, container) {
if (data.element) {
// Remove any existing color-selection classes
$(container).attr('class', function(i, c) {
return c.replace(/(^|\s)color-selection-\S+/g, '');
});
$(container).addClass($(data.element).attr("class"));
// Swap the style
$(container).attr('style', $(data.element).attr("style"));
}
return data.text;
}
@@ -200,7 +197,7 @@ $(document).ready(function() {
$(element).children('option').attr('disabled', false);
var results = data.results;
results = results.reduce((results,record) => {
results = results.reduce((results,record,idx) => {
record.text = record[element.getAttribute('display-field')] || record.name;
record.id = record[element.getAttribute('value-field')] || record.id;
if(element.getAttribute('disabled-indicator') && record[element.getAttribute('disabled-indicator')]) {
@@ -225,7 +222,7 @@ $(document).ready(function() {
results['global'].children.push(record);
}
else {
results[record.id] = record
results[idx] = record
}
return results;

View File

@@ -1,66 +0,0 @@
from django.utils.text import slugify
from dcim.constants import *
from dcim.models import Device, DeviceRole, DeviceType, Site
from extras.scripts import *
class NewBranchScript(Script):
script_name = "New Branch"
script_description = "Provision a new branch site"
script_fields = ['site_name', 'switch_count', 'switch_model']
site_name = StringVar(
description="Name of the new site"
)
switch_count = IntegerVar(
description="Number of access switches to create"
)
switch_model = ObjectVar(
description="Access switch model",
queryset=DeviceType.objects.filter(
manufacturer__name='Cisco',
model__in=['Catalyst 3560X-48T', 'Catalyst 3750X-48T']
)
)
x = BooleanVar(
description="Check me out"
)
def run(self, data):
# Create the new site
site = Site(
name=data['site_name'],
slug=slugify(data['site_name']),
status=SITE_STATUS_PLANNED
)
site.save()
self.log_success("Created new site: {}".format(site))
# Create access switches
switch_role = DeviceRole.objects.get(name='Access Switch')
for i in range(1, data['switch_count'] + 1):
switch = Device(
device_type=data['switch_model'],
name='{}-switch{}'.format(site.slug, i),
site=site,
status=DEVICE_STATUS_PLANNED,
device_role=switch_role
)
switch.save()
self.log_success("Created new switch: {}".format(switch))
# Generate a CSV table of new devices
output = [
'name,make,model'
]
for switch in Device.objects.filter(site=site):
attrs = [
switch.name,
switch.device_type.manufacturer.name,
switch.device_type.model
]
output.append(','.join(attrs))
return '\n'.join(output)

View File

@@ -1,54 +0,0 @@
from dcim.models import Site
from extras.scripts import Script, BooleanVar, IntegerVar, ObjectVar, StringVar
class NoInputScript(Script):
description = "This script does not require any input"
def run(self, data):
self.log_debug("This a debug message.")
self.log_info("This an info message.")
self.log_success("This a success message.")
self.log_warning("This a warning message.")
self.log_failure("This a failure message.")
class DemoScript(Script):
name = "Script Demo"
description = "A quick demonstration of the available field types"
my_string1 = StringVar(
description="Input a string between 3 and 10 characters",
min_length=3,
max_length=10
)
my_string2 = StringVar(
description="This field enforces a regex: three letters followed by three numbers",
regex=r'[a-z]{3}\d{3}'
)
my_number = IntegerVar(
description="Pick a number between 1 and 255 (inclusive)",
min_value=1,
max_value=255
)
my_boolean = BooleanVar(
description="Use the checkbox to toggle true/false"
)
my_object = ObjectVar(
description="Select a NetBox site",
queryset=Site.objects.all()
)
def run(self, data):
self.log_info("Your string was {}".format(data['my_string1']))
self.log_info("Your second string was {}".format(data['my_string2']))
self.log_info("Your number was {}".format(data['my_number']))
if data['my_boolean']:
self.log_info("You ticked the checkbox")
else:
self.log_info("You did not tick the checkbox")
self.log_info("You chose the sites {}".format(data['my_object']))
return "Here's some output"

View File

@@ -135,6 +135,10 @@
<a href="{% url 'dcim:device_list' %}?rack_id={{ rack.id }}">{{ rack.devices.count }}</a>
</td>
</tr>
<tr>
<td>Utilization</td>
<td>{% utilization_graph rack.get_utilization %}</td>
</tr>
</table>
</div>
<div class="panel panel-default">

View File

@@ -11,20 +11,11 @@
{% render_field form.mtu %}
{% render_field form.description %}
{% render_field form.mode %}
{% render_field form.untagged_vlan %}
{% render_field form.tagged_vlans %}
{% render_field form.tags %}
</div>
</div>
{% if obj.mode %}
<div class="panel panel-default" id="vlans_panel">
<div class="panel-heading"><strong>802.1Q VLANs</strong></div>
{% include 'dcim/inc/interface_vlans_table.html' %}
<div class="panel-footer text-right">
<a href="{% url 'dcim:interface_assign_vlans' pk=obj.pk %}?return_url={% url 'virtualization:interface_edit' pk=obj.pk %}" class="btn btn-primary btn-xs{% if form.instance.mode == 100 and form.instance.untagged_vlan %} disabled{% endif %}">
<i class="glyphicon glyphicon-plus"></i> Add VLANs
</a>
</div>
</div>
{% endif %}
{% endblock %}
{% block buttons %}
@@ -36,19 +27,4 @@
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
{% endif %}
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
$('#clear_untagged_vlan').click(function () {
$('input[name="untagged_vlan"]').prop("checked", false);
return false;
});
$('#clear_tagged_vlans').click(function () {
$('input[name="tagged_vlans"]').prop("checked", false);
return false;
});
});
</script>
{% endblock %}
{% endblock %}

View File

@@ -1 +1,2 @@
<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %} class="color-selection-{{ widget.value }}">{{ widget.label }}</option>
{% load helpers %}
<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %} {% if widget.value %}style="color: {{ widget.value|fgcolor }}; background-color: #{{ widget.value }}"{% endif %}>{{ widget.label }}</option>

View File

@@ -2,11 +2,11 @@ from django import forms
from django.core.exceptions import ValidationError
from taggit.forms import TagField
from dcim.constants import IFACE_TYPE_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL
from dcim.constants import IFACE_TYPE_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL, IFACE_MODE_CHOICES
from dcim.forms import INTERFACE_MODE_HELP_TEXT
from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site
from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm
from ipam.models import IPAddress
from ipam.models import IPAddress, VLANGroup, VLAN
from tenancy.forms import TenancyForm
from tenancy.forms import TenancyFilterForm
from tenancy.models import Tenant
@@ -616,6 +616,24 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
#
class InterfaceForm(BootstrapMixin, forms.ModelForm):
untagged_vlan = forms.ModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)
tagged_vlans = forms.ModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)
tags = TagField(
required=False
)
@@ -638,6 +656,39 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
'mode': INTERFACE_MODE_HELP_TEXT,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
vlan_choices = []
global_vlans = VLAN.objects.filter(site=None, group=None)
vlan_choices.append(
('Global', [(vlan.pk, vlan) for vlan in global_vlans])
)
for group in VLANGroup.objects.filter(site=None):
global_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append(
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
)
site = getattr(self.instance.device, 'site', None)
if site is not None:
# Add non-grouped site VLANs
site_vlans = VLAN.objects.filter(site=site, group=None)
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
# Add grouped site VLANs
for group in VLANGroup.objects.filter(site=site):
site_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append((
'{} / {}'.format(group.site.name, group.name),
[(vlan.pk, vlan) for vlan in site_group_vlans]
))
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
self.fields['tagged_vlans'].choices = vlan_choices
def clean(self):
super().clean()
@@ -681,6 +732,29 @@ class InterfaceCreateForm(ComponentForm):
max_length=100,
required=False
)
mode = forms.ChoiceField(
choices=add_blank_choice(IFACE_MODE_CHOICES),
required=False,
widget=StaticSelect2(),
)
untagged_vlan = forms.ModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)
tagged_vlans = forms.ModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)
tags = TagField(
required=False
)
@@ -693,6 +767,36 @@ class InterfaceCreateForm(ComponentForm):
super().__init__(*args, **kwargs)
# Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
vlan_choices = []
global_vlans = VLAN.objects.filter(site=None, group=None)
vlan_choices.append(
('Global', [(vlan.pk, vlan) for vlan in global_vlans])
)
for group in VLANGroup.objects.filter(site=None):
global_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append(
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
)
site = getattr(self.parent.cluster, 'site', None)
if site is not None:
# Add non-grouped site VLANs
site_vlans = VLAN.objects.filter(site=site, group=None)
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
# Add grouped site VLANs
for group in VLANGroup.objects.filter(site=site):
site_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append((
'{} / {}'.format(group.site.name, group.name),
[(vlan.pk, vlan) for vlan in site_group_vlans]
))
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
self.fields['tagged_vlans'].choices = vlan_choices
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(
@@ -713,12 +817,68 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
max_length=100,
required=False
)
mode = forms.ChoiceField(
choices=add_blank_choice(IFACE_MODE_CHOICES),
required=False,
widget=StaticSelect2()
)
untagged_vlan = forms.ModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)
tagged_vlans = forms.ModelMultipleChoiceField(
queryset=VLAN.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/ipam/vlans/",
display_field='display_name',
full=True
)
)
class Meta:
nullable_fields = [
'mtu', 'description',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
vlan_choices = []
global_vlans = VLAN.objects.filter(site=None, group=None)
vlan_choices.append(
('Global', [(vlan.pk, vlan) for vlan in global_vlans])
)
for group in VLANGroup.objects.filter(site=None):
global_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append(
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
)
if self.parent_obj.cluster is not None:
site = getattr(self.parent_obj.cluster, 'site', None)
if site is not None:
# Add non-grouped site VLANs
site_vlans = VLAN.objects.filter(site=site, group=None)
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
# Add grouped site VLANs
for group in VLANGroup.objects.filter(site=site):
site_group_vlans = VLAN.objects.filter(group=group)
vlan_choices.append((
'{} / {}'.format(group.site.name, group.name),
[(vlan.pk, vlan) for vlan in site_group_vlans]
))
self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
self.fields['tagged_vlans'].choices = vlan_choices
#
# Bulk VirtualMachine component creation