mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-21 03:27:21 -06:00
commit
f224ad2959
@ -120,7 +120,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
|
||||
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True)
|
||||
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||
type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
|
||||
@ -666,7 +666,7 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
# Provide a default value to satisfy UniqueTogetherValidator
|
||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
|
@ -1795,7 +1795,7 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
||||
# Compile VLAN choices
|
||||
vlan_choices = []
|
||||
|
||||
# Add global VLANs
|
||||
# Add non-grouped global VLANs
|
||||
global_vlans = VLAN.objects.filter(site=None, group=None).exclude(pk__in=assigned_vlans)
|
||||
vlan_choices.append((
|
||||
'Global', [(vlan.pk, vlan) for vlan in global_vlans])
|
||||
@ -1808,16 +1808,15 @@ class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm):
|
||||
(group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
|
||||
)
|
||||
|
||||
parent = self.instance.parent
|
||||
if parent is not None:
|
||||
site = getattr(self.instance.parent, 'site', None)
|
||||
if site is not None:
|
||||
|
||||
# Add site VLANs
|
||||
if parent.site:
|
||||
site_vlans = VLAN.objects.filter(site=parent.site, group=None).exclude(pk__in=assigned_vlans)
|
||||
vlan_choices.append((parent.site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
|
||||
# Add non-grouped site VLANs
|
||||
site_vlans = VLAN.objects.filter(site=site, group=None).exclude(pk__in=assigned_vlans)
|
||||
vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
|
||||
|
||||
# Add grouped site VLANs
|
||||
for group in VLANGroup.objects.filter(site=parent.site):
|
||||
for group in VLANGroup.objects.filter(site=site):
|
||||
site_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans)
|
||||
vlan_choices.append((
|
||||
'{} / {}'.format(group.site.name, group.name),
|
||||
|
@ -138,8 +138,11 @@ class ImageAttachmentViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class ConfigContextViewSet(ModelViewSet):
|
||||
queryset = ConfigContext.objects.prefetch_related('regions', 'sites', 'roles', 'platforms', 'tenants')
|
||||
queryset = ConfigContext.objects.prefetch_related(
|
||||
'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
||||
)
|
||||
serializer_class = serializers.ConfigContextSerializer
|
||||
filter_class = filters.ConfigContextFilter
|
||||
|
||||
|
||||
#
|
||||
|
@ -6,9 +6,10 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.models import Site
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
||||
from .models import CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
|
||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction
|
||||
|
||||
|
||||
class CustomFieldFilter(django_filters.Filter):
|
||||
@ -124,6 +125,92 @@ class TopologyMapFilter(django_filters.FilterSet):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class ConfigContextFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='regions',
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
)
|
||||
region = django_filters.ModelMultipleChoiceFilter(
|
||||
name='regions__slug',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='sites',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='sites__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='roles',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='roles__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platforms',
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform',
|
||||
)
|
||||
platform = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platforms__slug',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
tenant_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant_groups',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
label='Tenant group',
|
||||
)
|
||||
tenant_group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant_groups__slug',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant group (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenants',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenants__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = ['name', 'is_active']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(data__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ObjectChangeFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
@ -10,8 +10,12 @@ from mptt.forms import TreeNodeMultipleChoiceField
|
||||
from taggit.forms import TagField
|
||||
from taggit.models import Tag
|
||||
|
||||
from dcim.models import Region
|
||||
from utilities.forms import add_blank_choice, BootstrapMixin, BulkEditForm, LaxURLField, JSONField, SlugField
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField,
|
||||
JSONField, SlugField,
|
||||
)
|
||||
from .constants import (
|
||||
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||
OBJECTCHANGE_ACTION_CHOICES,
|
||||
@ -223,6 +227,37 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
]
|
||||
|
||||
|
||||
class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
region = FilterTreeNodeMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
platform = FilterChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
tenant_group = FilterChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Image attachments
|
||||
#
|
||||
|
@ -72,15 +72,10 @@ class ConfigContextTable(BaseTable):
|
||||
is_active = BooleanColumn(
|
||||
verbose_name='Active'
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=CONFIGCONTEXT_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConfigContext
|
||||
fields = ('pk', 'name', 'weight', 'is_active', 'description', 'actions')
|
||||
fields = ('pk', 'name', 'weight', 'is_active', 'description')
|
||||
|
||||
|
||||
class ObjectChangeTable(BaseTable):
|
||||
|
@ -14,7 +14,7 @@ from taggit.models import Tag
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||
from . import filters
|
||||
from .forms import ConfigContextForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
||||
from .forms import ConfigContextForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm
|
||||
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
||||
from .reports import get_report, get_reports
|
||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable
|
||||
@ -56,6 +56,8 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
||||
class ConfigContextListView(ObjectListView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
filter = filters.ConfigContextFilter
|
||||
filter_form = ConfigContextFilterForm
|
||||
table = ConfigContextTable
|
||||
template_name = 'extras/configcontext_list.html'
|
||||
|
||||
|
@ -140,10 +140,11 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
available_prefixes.remove(allocated_prefix)
|
||||
|
||||
# Initialize the serializer with a list or a single object depending on what was requested
|
||||
context = {'request': request}
|
||||
if isinstance(request.data, list):
|
||||
serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True)
|
||||
serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True, context=context)
|
||||
else:
|
||||
serializer = serializers.PrefixSerializer(data=requested_prefixes[0])
|
||||
serializer = serializers.PrefixSerializer(data=requested_prefixes[0], context=context)
|
||||
|
||||
# Create the new Prefix(es)
|
||||
if serializer.is_valid():
|
||||
@ -199,10 +200,11 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
||||
|
||||
# Initialize the serializer with a list or a single object depending on what was requested
|
||||
context = {'request': request}
|
||||
if isinstance(request.data, list):
|
||||
serializer = serializers.IPAddressSerializer(data=requested_ips, many=True)
|
||||
serializer = serializers.IPAddressSerializer(data=requested_ips, many=True, context=context)
|
||||
else:
|
||||
serializer = serializers.IPAddressSerializer(data=requested_ips[0])
|
||||
serializer = serializers.IPAddressSerializer(data=requested_ips[0], context=context)
|
||||
|
||||
# Create the new IP address(es)
|
||||
if serializer.is_valid():
|
||||
|
@ -494,7 +494,8 @@ class PrefixTest(APITestCase):
|
||||
|
||||
def test_create_single_available_prefix(self):
|
||||
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), is_pool=True)
|
||||
vrf = VRF.objects.create(name='Test VRF 1', rd='1234')
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=vrf, is_pool=True)
|
||||
url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk})
|
||||
|
||||
# Create four available prefixes with individual requests
|
||||
@ -512,6 +513,7 @@ class PrefixTest(APITestCase):
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data['prefix'], prefixes_to_be_created[i])
|
||||
self.assertEqual(response.data['vrf']['id'], vrf.pk)
|
||||
self.assertEqual(response.data['description'], data['description'])
|
||||
|
||||
# Try to create one more prefix
|
||||
@ -562,7 +564,8 @@ class PrefixTest(APITestCase):
|
||||
|
||||
def test_create_single_available_ip(self):
|
||||
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/30'), is_pool=True)
|
||||
vrf = VRF.objects.create(name='Test VRF 1', rd='1234')
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/30'), vrf=vrf, is_pool=True)
|
||||
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
|
||||
|
||||
# Create all four available IPs with individual requests
|
||||
@ -572,6 +575,7 @@ class PrefixTest(APITestCase):
|
||||
}
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data['vrf']['id'], vrf.pk)
|
||||
self.assertEqual(response.data['description'], data['description'])
|
||||
|
||||
# Try to create one more IP
|
||||
|
@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
VERSION = '2.4.2'
|
||||
VERSION = '2.4.3'
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
@ -573,7 +573,7 @@
|
||||
{% endif %}
|
||||
{% if cs_ports or device.device_type.is_console_server %}
|
||||
{% if perms.dcim.delete_consoleserverport %}
|
||||
<form method="post" action="{% url 'dcim:consoleserverport_bulk_delete' pk=device.pk %}">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
@ -606,12 +606,12 @@
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if cs_ports and perms.dcim.delete_consoleserverport %}
|
||||
<button type="submit" class="btn btn-danger btn-xs">
|
||||
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
@ -631,7 +631,7 @@
|
||||
{% endif %}
|
||||
{% if power_outlets or device.device_type.is_pdu %}
|
||||
{% if perms.dcim.delete_poweroutlet %}
|
||||
<form method="post" action="{% url 'dcim:poweroutlet_bulk_delete' pk=device.pk %}">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
@ -664,12 +664,12 @@
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
|
||||
</button>
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
|
||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if power_outlets and perms.dcim.delete_poweroutlet %}
|
||||
<button type="submit" class="btn btn-danger btn-xs">
|
||||
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -140,6 +140,20 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant Groups</td>
|
||||
<td>
|
||||
{% if configcontext.tenant_groups.all %}
|
||||
<ul>
|
||||
{% for tenant_group in configcontext.tenant_groups.all %}
|
||||
<li><a href="{{ tenant_group.get_absolute_url }}">{{ tenant_group }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenants</td>
|
||||
<td>
|
||||
|
@ -9,8 +9,11 @@
|
||||
</div>
|
||||
<h1>{% block title %}Config Contexts{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:configcontext_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -170,7 +170,9 @@ class WritableNestedSerializer(ModelSerializer):
|
||||
if data is None:
|
||||
return None
|
||||
try:
|
||||
return self.Meta.model.objects.get(pk=data)
|
||||
return self.Meta.model.objects.get(pk=int(data))
|
||||
except (TypeError, ValueError):
|
||||
raise ValidationError("Primary key must be an integer")
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError("Invalid ID")
|
||||
|
||||
|
@ -260,6 +260,22 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('virtualization:virtualmachine', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate primary IP addresses
|
||||
interfaces = self.interfaces.all()
|
||||
for field in ['primary_ip4', 'primary_ip6']:
|
||||
ip = getattr(self, field)
|
||||
if ip is not None:
|
||||
if ip.interface in interfaces:
|
||||
pass
|
||||
elif self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.interface in interfaces:
|
||||
pass
|
||||
else:
|
||||
raise ValidationError({
|
||||
field: "The specified IP address ({}) is not assigned to this VM.".format(ip),
|
||||
})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
self.name,
|
||||
|
Loading…
Reference in New Issue
Block a user