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

Merge branch 'develop' into feature

This commit is contained in:
Jeremy Stretch
2023-08-15 11:04:03 -04:00
20 changed files with 801 additions and 61 deletions

View File

@@ -1143,6 +1143,8 @@ class InterfaceSpeedChoices(ChoiceSet):
(25000000, '25 Gbps'),
(40000000, '40 Gbps'),
(100000000, '100 Gbps'),
(200000000, '200 Gbps'),
(400000000, '400 Gbps'),
]

View File

@@ -55,7 +55,10 @@ class ComponentCreateForm(forms.Form):
super().clean()
# Validate that all replication fields generate an equal number of values
pattern_count = len(self.cleaned_data[self.replication_fields[0]])
if not (patterns := self.cleaned_data.get(self.replication_fields[0])):
return
pattern_count = len(patterns)
for field_name in self.replication_fields:
value_count = len(self.cleaned_data[field_name])
if self.cleaned_data[field_name] and value_count != pattern_count:

View File

@@ -0,0 +1,62 @@
import json
import os
from django.conf import settings
from django.core.management.base import BaseCommand
from jinja2 import FileSystemLoader, Environment
from dcim.choices import *
TEMPLATE_FILENAME = 'devicetype_schema.jinja2'
OUTPUT_FILENAME = 'contrib/generated_schema.json'
CHOICES_MAP = {
'airflow_choices': DeviceAirflowChoices,
'weight_unit_choices': WeightUnitChoices,
'subdevice_role_choices': SubdeviceRoleChoices,
'console_port_type_choices': ConsolePortTypeChoices,
'console_server_port_type_choices': ConsolePortTypeChoices,
'power_port_type_choices': PowerPortTypeChoices,
'power_outlet_type_choices': PowerOutletTypeChoices,
'power_outlet_feedleg_choices': PowerOutletFeedLegChoices,
'interface_type_choices': InterfaceTypeChoices,
'interface_poe_mode_choices': InterfacePoEModeChoices,
'interface_poe_type_choices': InterfacePoETypeChoices,
'front_port_type_choices': PortTypeChoices,
'rear_port_type_choices': PortTypeChoices,
}
class Command(BaseCommand):
help = "Generate JSON schema for validating NetBox device type definitions"
def add_arguments(self, parser):
parser.add_argument(
'--write',
action='store_true',
help="Write the generated schema to file"
)
def handle(self, *args, **kwargs):
# Initialize template
template_loader = FileSystemLoader(searchpath=f'{settings.TEMPLATES_DIR}/extras/schema/')
template_env = Environment(loader=template_loader)
template = template_env.get_template(TEMPLATE_FILENAME)
# Render template
context = {
key: json.dumps(choices.values())
for key, choices in CHOICES_MAP.items()
}
rendered = template.render(**context)
if kwargs['write']:
# $root/contrib/generated_schema.json
filename = os.path.join(os.path.split(settings.BASE_DIR)[0], OUTPUT_FILENAME)
with open(filename, mode='w', encoding='UTF-8') as f:
f.write(json.dumps(json.loads(rendered), indent=4))
f.write('\n')
f.close()
self.stdout.write(self.style.SUCCESS(f"Schema written to {filename}."))
else:
self.stdout.write(rendered)

View File

@@ -1,7 +1,5 @@
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django.db.models import F
from django.db.models.functions import Round
from django.shortcuts import get_object_or_404
from django_pglocks import advisory_lock
from drf_spectacular.utils import extend_schema
@@ -16,6 +14,7 @@ from circuits.models import Provider
from dcim.models import Site
from ipam import filtersets
from ipam.models import *
from ipam.models import L2VPN, L2VPNTermination
from ipam.utils import get_next_available_prefix
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.api.viewsets.mixins import ObjectValidationMixin
@@ -24,7 +23,6 @@ from netbox.constants import ADVISORY_LOCK_KEYS
from utilities.api import get_serializer_for_model
from utilities.utils import count_related
from . import serializers
from ipam.models import L2VPN, L2VPNTermination
class IPAMRootView(APIRootView):
@@ -346,7 +344,11 @@ class AvailableASNsView(AvailableObjectsView):
def get(self, request, pk):
return super().get(request, pk)
@extend_schema(methods=["post"], responses={201: serializers.AvailableASNSerializer(many=True)})
@extend_schema(
methods=["post"],
responses={201: serializers.ASNSerializer(many=True)},
request=serializers.ASNSerializer(many=True),
)
def post(self, request, pk):
return super().post(request, pk)
@@ -395,7 +397,11 @@ class AvailablePrefixesView(AvailableObjectsView):
def get(self, request, pk):
return super().get(request, pk)
@extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=True)})
@extend_schema(
methods=["post"],
responses={201: serializers.PrefixSerializer(many=True)},
request=serializers.PrefixSerializer(many=True),
)
def post(self, request, pk):
return super().post(request, pk)
@@ -435,7 +441,11 @@ class AvailableIPAddressesView(AvailableObjectsView):
def get(self, request, pk):
return super().get(request, pk)
@extend_schema(methods=["post"], responses={201: serializers.IPAddressSerializer(many=True)})
@extend_schema(
methods=["post"],
responses={201: serializers.IPAddressSerializer(many=True)},
request=serializers.IPAddressSerializer(many=True),
)
def post(self, request, pk):
return super().post(request, pk)
@@ -482,6 +492,10 @@ class AvailableVLANsView(AvailableObjectsView):
def get(self, request, pk):
return super().get(request, pk)
@extend_schema(methods=["post"], responses={201: serializers.VLANSerializer(many=True)})
@extend_schema(
methods=["post"],
responses={201: serializers.VLANSerializer(many=True)},
request=serializers.VLANSerializer(many=True),
)
def post(self, request, pk):
return super().post(request, pk)

View File

@@ -256,7 +256,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPRange
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Attriubtes'), ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')),
(_('Attributes'), ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
)
family = forms.ChoiceField(

View File

@@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
# Environment setup
#
VERSION = '3.6-beta1'
VERSION = '3.6-beta2'
# Hostname
HOSTNAME = platform.node()

View File

@@ -511,9 +511,9 @@ class CustomLinkColumn(tables.Column):
"""
def __init__(self, customlink, *args, **kwargs):
self.customlink = customlink
kwargs['accessor'] = Accessor('pk')
if 'verbose_name' not in kwargs:
kwargs['verbose_name'] = customlink.name
kwargs.setdefault('accessor', Accessor('pk'))
kwargs.setdefault('orderable', False)
kwargs.setdefault('verbose_name', customlink.name)
super().__init__(*args, **kwargs)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import { getElement, getElements, findFirstAdjacent } from '../util';
import { getElements, findFirstAdjacent } from '../util';
/**
* If any PK checkbox is checked, uncheck the select all table checkbox and the select all
@@ -63,29 +63,6 @@ function handleSelectAllToggle(event: Event): void {
}
}
/**
* Synchronize the select all confirmation checkbox state with the select all confirmation button
* disabled state. If the select all confirmation checkbox is checked, the buttons should be
* enabled. If not, the buttons should be disabled.
*
* @param event Change Event
*/
function handleSelectAll(event: Event): void {
const target = event.currentTarget as HTMLInputElement;
const selectAllBox = getElement<HTMLDivElement>('select-all-box');
if (selectAllBox !== null) {
for (const button of selectAllBox.querySelectorAll<HTMLButtonElement>(
'button[type="submit"]',
)) {
if (target.checked) {
button.disabled = false;
} else {
button.disabled = true;
}
}
}
}
/**
* Initialize table select all elements.
*/
@@ -98,9 +75,4 @@ export function initSelectAll(): void {
for (const element of getElements<HTMLInputElement>('input[type="checkbox"][name="pk"]')) {
element.addEventListener('change', handlePkCheck);
}
const selectAll = getElement<HTMLInputElement>('select-all');
if (selectAll !== null) {
selectAll.addEventListener('change', handleSelectAll);
}
}

View File

@@ -0,0 +1,4 @@
{% extends 'generic/object.html' %}
{% block tabs %}
{% endblock %}

View File

@@ -0,0 +1,93 @@
{
"type": "object",
"additionalProperties": false,
"definitions": {
"airflow": {
"type": "string",
"enum": {{ airflow_choices }}
},
"weight-unit": {
"type": "string",
"enum": {{ weight_unit_choices }}
},
"subdevice-role": {
"type": "string",
"enum": {{ subdevice_role_choices }}
},
"console-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ console_port_type_choices }}
}
}
},
"console-server-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ console_server_port_type_choices }}
}
}
},
"power-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ power_port_type_choices }}
}
}
},
"power-outlet": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ power_outlet_type_choices }}
},
"feed-leg": {
"type": "string",
"enum": {{ power_outlet_feedleg_choices }}
}
}
},
"interface": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ interface_type_choices }}
},
"poe_mode": {
"type": "string",
"enum": {{ interface_poe_mode_choices }}
},
"poe_type": {
"type": "string",
"enum": {{ interface_poe_type_choices }}
}
}
},
"front-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ front_port_type_choices }}
}
}
},
"rear-port": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": {{ rear_port_type_choices}}
}
}
}
}
}

View File

@@ -60,6 +60,9 @@ def parse_alphanumeric_range(string):
except ValueError:
begin, end = dash_range, dash_range
if begin.isdigit() and end.isdigit():
if int(begin) >= int(end):
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
for n in list(range(int(begin), int(end) + 1)):
values.append(n)
else:
@@ -71,6 +74,10 @@ def parse_alphanumeric_range(string):
# Not a valid range (more than a single character)
if not len(begin) == len(end) == 1:
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
if ord(begin) >= ord(end):
raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
for n in list(range(ord(begin), ord(end) + 1)):
values.append(chr(n))
return values

View File

@@ -264,8 +264,9 @@ class ExpandAlphanumeric(TestCase):
self.assertEqual(sorted(expand_alphanumeric_pattern('r[a-9]a')), [])
def test_invalid_range_bounds(self):
self.assertEqual(sorted(expand_alphanumeric_pattern('r[9-8]a')), [])
self.assertEqual(sorted(expand_alphanumeric_pattern('r[b-a]a')), [])
with self.assertRaises(forms.ValidationError):
sorted(expand_alphanumeric_pattern('r[9-8]a'))
sorted(expand_alphanumeric_pattern('r[b-a]a'))
def test_invalid_range_len(self):
with self.assertRaises(forms.ValidationError):