Merge branch 'feature' into 9856-strawberry-2

This commit is contained in:
Jeremy Stretch 2024-03-18 10:12:06 -04:00
commit 9c29f45c1a
57 changed files with 222 additions and 131 deletions

View File

@ -17,15 +17,16 @@ body:
How are you running NetBox? (For issues with the Docker image, please go to the
[netbox-docker](https://github.com/netbox-community/netbox-docker) repo.)
options:
- Self-hosted
- NetBox Cloud
- NetBox Enterprise
- Self-hosted
validations:
required: true
- type: input
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v3.7.3
placeholder: v3.7.4
validations:
required: true
- type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.7.3
placeholder: v3.7.4
validations:
required: true
- type: dropdown

View File

@ -84,4 +84,4 @@ jobs:
run: coverage run --source="netbox/" netbox/manage.py test netbox/ --parallel
- name: Show coverage report
run: coverage report --skip-covered --omit *migrations*
run: coverage report --skip-covered --omit '*/migrations/*,*/tests/*'

View File

@ -96,7 +96,7 @@ markdown-include
mkdocs-material
# Introspection for embedded code
# https://github.com/mkdocstrings/mkdocstrings/blob/master/CHANGELOG.md
# https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md
mkdocstrings[python-legacy]
# Library for manipulating IP prefixes and addresses

View File

@ -384,7 +384,10 @@
"8gfc-sfpp",
"16gfc-sfpp",
"32gfc-sfp28",
"32gfc-sfpp",
"64gfc-qsfpp",
"64gfc-sfpdd",
"64gfc-sfpp",
"128gfc-qsfp28",
"infiniband-sdr",
"infiniband-ddr",

View File

@ -31,8 +31,7 @@ This section entails the installation and configuration of a local PostgreSQL da
Once PostgreSQL has been installed, start the service and enable it to run at boot:
```no-highlight
sudo systemctl start postgresql
sudo systemctl enable postgresql
sudo systemctl enable --now postgresql
```
Before continuing, verify that you have installed PostgreSQL 12 or later:

View File

@ -14,8 +14,7 @@
```no-highlight
sudo yum install -y redis
sudo systemctl start redis
sudo systemctl enable redis
sudo systemctl enable --now redis
```
Before continuing, verify that your installed version of Redis is at least v4.0:

View File

@ -27,8 +27,7 @@ sudo systemctl daemon-reload
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
```no-highlight
sudo systemctl start netbox netbox-rq
sudo systemctl enable netbox netbox-rq
sudo systemctl enable --now netbox netbox-rq
```
You can use the command `systemctl status netbox` to verify that the WSGI service is running:

View File

@ -1,6 +1,31 @@
# NetBox v3.7
## v3.7.4 (FUTURE)
## v3.7.5 (FUTURE)
---
## v3.7.4 (2024-03-13)
### Enhancements
* [#14206](https://github.com/netbox-community/netbox/issues/14206) - Add additional FibreChannel SFP+ interface types
* [#14366](https://github.com/netbox-community/netbox/issues/14366) - Enable custom links for config contexts & templates
* [#15291](https://github.com/netbox-community/netbox/issues/15291) - Add tunnel termination buttons to VM interfaces table
* [#15297](https://github.com/netbox-community/netbox/issues/15297) - Linkify platform column in device & virtual machine tables
### Bug Fixes
* [#13722](https://github.com/netbox-community/netbox/issues/13722) - Fix range expansion for comma-separated numerical values
* [#14832](https://github.com/netbox-community/netbox/issues/14832) - Enable querying IP addresses for an FHRP group via GraphQL
* [#15220](https://github.com/netbox-community/netbox/issues/15220) - Fix validation check when bulk editing the mask length of IP addresses
* [#15232](https://github.com/netbox-community/netbox/issues/15232) - Permit user with sufficient permissions to assign an inventory item to a device type
* [#15241](https://github.com/netbox-community/netbox/issues/15241) - Restore missing `display` field on VirtualDisk serialization in REST API
* [#15243](https://github.com/netbox-community/netbox/issues/15243) - Correct representation of installed module when listing module bays using REST API brief mode
* [#15316](https://github.com/netbox-community/netbox/issues/15316) - Fix selection of 3DES encryption for IKE & IPSec proposals
* [#15322](https://github.com/netbox-community/netbox/issues/15322) - Add description field to YAML export for device & module types
* [#15336](https://github.com/netbox-community/netbox/issues/15336) - Correct label for recurring scheduled jobs
* [#15347](https://github.com/netbox-community/netbox/issues/15347) - Fix querying virtual machine contacts via GraphQL
* [#15356](https://github.com/netbox-community/netbox/issues/15356) - Fix assignment of front & rear images to device types via REST API
---

View File

@ -1,4 +1,5 @@
from typing import List
import strawberry
import strawberry_django

View File

@ -2,13 +2,12 @@ from typing import Annotated, List
import strawberry
import strawberry_django
from circuits import models
from dcim.graphql.mixins import CabledObjectMixin
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
from tenancy.graphql.types import TenantType
from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType
from tenancy.graphql.types import TenantType
from .filters import *
__all__ = (

View File

@ -1,7 +1,6 @@
import strawberry
import strawberry_django
from core import filtersets, models
from core import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (

View File

@ -1,4 +1,5 @@
from typing import List
import strawberry
import strawberry_django

View File

@ -309,6 +309,14 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name']
class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
class Meta:
model = models.Module
fields = ['id', 'url', 'display', 'serial']
class NestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = NestedDeviceSerializer(read_only=True)
@ -392,11 +400,11 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
class NestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
module = NestedModuleSerializer(required=False, read_only=True, allow_null=True)
installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
class Meta:
model = models.ModuleBay
fields = ['id', 'url', 'display', 'module', 'name']
fields = ['id', 'url', 'display', 'installed_module', 'name']
class NestedDeviceBaySerializer(WritableNestedSerializer):

View File

@ -28,8 +28,8 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
front_image = serializers.URLField(allow_null=True, required=False)
rear_image = serializers.URLField(allow_null=True, required=False)
front_image = serializers.ImageField(required=False, allow_null=True)
rear_image = serializers.ImageField(required=False, allow_null=True)
# Counter fields
console_port_template_count = serializers.IntegerField(read_only=True)

View File

@ -12,6 +12,7 @@ __all__ = (
class VirtualChassisSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
members = NestedDeviceSerializer(many=True, read_only=True)
# Counter fields
member_count = serializers.IntegerField(read_only=True)
@ -20,6 +21,6 @@ class VirtualChassisSerializer(NetBoxModelSerializer):
model = VirtualChassis
fields = [
'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'member_count',
'created', 'last_updated', 'member_count', 'members',
]
brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count')

View File

@ -511,7 +511,10 @@ class CableTerminationViewSet(NetBoxModelViewSet):
#
class VirtualChassisViewSet(NetBoxModelViewSet):
queryset = VirtualChassis.objects.all()
queryset = VirtualChassis.objects.prefetch_related(
# Prefetch related object for the display of unnamed devices
'master__virtual_chassis',
)
serializer_class = serializers.VirtualChassisSerializer
filterset_class = filtersets.VirtualChassisFilterSet

View File

@ -889,7 +889,10 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
TYPE_32GFC_SFP28 = '32gfc-sfp28'
TYPE_32GFC_SFP_PLUS = '32gfc-sfpp'
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
TYPE_64GFC_SFP_DD = '64gfc-sfpdd'
TYPE_64GFC_SFP_PLUS = '64gfc-sfpp'
TYPE_128GFC_QSFP28 = '128gfc-qsfp28'
# InfiniBand
@ -1058,7 +1061,10 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
(TYPE_32GFC_SFP_PLUS, 'SFP+ (32GFC)'),
(TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'),
(TYPE_64GFC_SFP_DD, 'SFP-DD (64GFC)'),
(TYPE_64GFC_SFP_PLUS, 'SFP+ (64GFC)'),
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
)
),

View File

@ -1,7 +1,6 @@
import strawberry
import strawberry_django
from dcim import filtersets, models
from dcim import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (

View File

@ -1,7 +1,3 @@
from typing import TYPE_CHECKING, Annotated, List, Union
import strawberry
import strawberry_django
from circuits.graphql.types import CircuitTerminationType, ProviderNetworkType
from circuits.models import CircuitTermination, ProviderNetwork
from dcim.graphql.types import (

View File

@ -1,7 +1,7 @@
from typing import Annotated, List, Union
import strawberry
import strawberry_django
from typing import TYPE_CHECKING, Annotated, List, Union
__all__ = (
'CabledObjectMixin',

View File

@ -1,4 +1,5 @@
from typing import List
import strawberry
import strawberry_django

View File

@ -2,6 +2,7 @@ from typing import Annotated, List, Union
import strawberry
import strawberry_django
from dcim import models
from extras.graphql.mixins import (
ChangelogMixin,
@ -12,14 +13,8 @@ from extras.graphql.mixins import (
TagsMixin,
)
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import (
BaseObjectType,
NetBoxObjectType,
OrganizationalObjectType,
)
from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType
from .filters import *
from .mixins import CabledObjectMixin, PathEndpointMixin

View File

@ -229,15 +229,16 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
'manufacturer': self.manufacturer.name,
'model': self.model,
'slug': self.slug,
'description': self.description,
'default_platform': self.default_platform.name if self.default_platform else None,
'part_number': self.part_number,
'u_height': float(self.u_height),
'is_full_depth': self.is_full_depth,
'subdevice_role': self.subdevice_role,
'airflow': self.airflow,
'comments': self.comments,
'weight': float(self.weight) if self.weight is not None else None,
'weight_unit': self.weight_unit,
'comments': self.comments,
}
# Component templates
@ -415,9 +416,10 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
'manufacturer': self.manufacturer.name,
'model': self.model,
'part_number': self.part_number,
'comments': self.comments,
'description': self.description,
'weight': float(self.weight) if self.weight is not None else None,
'weight_unit': self.weight_unit,
'comments': self.comments,
}
# Component templates

View File

@ -210,6 +210,10 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
linkify=True,
verbose_name=_('Type')
)
platform = tables.Column(
linkify=True,
verbose_name=_('Platform')
)
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip4', 'primary_ip6'),
@ -294,7 +298,7 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
model = models.Device
fields = (
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'role', 'manufacturer', 'device_type',
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'primary_ip', 'primary_ip4',
'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
'config_template', 'comments', 'contacts', 'tags', 'created', 'last_updated',

View File

@ -1079,7 +1079,7 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
tab = ViewTab(
label=_('Inventory Items'),
badge=lambda obj: obj.inventory_item_template_count,
permission='dcim.view_invenotryitemtemplate',
permission='dcim.view_inventoryitemtemplate',
weight=590,
hide_if_empty=True
)

View File

@ -1,7 +1,6 @@
import strawberry
import strawberry_django
from extras import filtersets, models
from extras import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (

View File

@ -1,7 +1,7 @@
import strawberry
import strawberry_django
from typing import TYPE_CHECKING, Annotated, List
import strawberry
import strawberry_django
from django.contrib.contenttypes.models import ContentType
from extras.models import ObjectChange
@ -9,6 +9,7 @@ from extras.models import ObjectChange
__all__ = (
'ChangelogMixin',
'ConfigContextMixin',
'ContactsMixin',
'CustomFieldsMixin',
'ImageAttachmentsMixin',
'JournalEntriesMixin',

View File

@ -1,4 +1,5 @@
from typing import List
import strawberry
import strawberry_django

View File

@ -3,10 +3,6 @@ from typing import Annotated, List
import strawberry
import strawberry_django
import strawberry
from strawberry import auto
import strawberry_django
from extras import models
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
from netbox.graphql.types import BaseObjectType, ContentTypeType, ObjectType, OrganizationalObjectType

View File

@ -11,7 +11,7 @@ from extras.querysets import ConfigContextQuerySet
from netbox.config import get_config
from netbox.registry import registry
from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
from utilities.jinja2 import ConfigTemplateLoader
from utilities.utils import deepmerge
@ -26,7 +26,7 @@ __all__ = (
# Config contexts
#
class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, ChangeLoggedModel):
"""
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
@ -210,7 +210,7 @@ class ConfigContextModel(models.Model):
# Config templates
#
class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
class ConfigTemplate(SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100

View File

@ -373,20 +373,6 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
'primary_for_parent', _("Only IP addresses assigned to an interface can be designated as primary IPs.")
)
# Do not allow assigning a network ID or broadcast address to an interface.
if interface and (address := self.cleaned_data.get('address')):
if address.ip == address.network:
msg = _("{ip} is a network ID, which may not be assigned to an interface.").format(ip=address.ip)
if address.version == 4 and address.prefixlen not in (31, 32):
raise ValidationError(msg)
if address.version == 6 and address.prefixlen not in (127, 128):
raise ValidationError(msg)
if address.version == 4 and address.ip == address.broadcast and address.prefixlen not in (31, 32):
msg = _("{ip} is a broadcast address, which may not be assigned to an interface.").format(
ip=address.ip
)
raise ValidationError(msg)
def save(self, *args, **kwargs):
ipaddress = super().save(*args, **kwargs)

View File

@ -1,10 +1,8 @@
import strawberry
import strawberry_django
from ipam import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'ASNFilter',
'ASNRangeFilter',

View File

@ -1,6 +1,7 @@
from typing import Annotated, List
import strawberry
import strawberry_django
from typing import TYPE_CHECKING, Annotated, List, Union
__all__ = (
'IPAddressesMixin',

View File

@ -1,4 +1,5 @@
from typing import List
import strawberry
import strawberry_django

View File

@ -1,19 +1,15 @@
from typing import TYPE_CHECKING, Annotated, List, Union
from typing import Annotated, List, Union
import strawberry
import strawberry_django
from circuits.graphql.types import ProviderType
from dcim.graphql.types import SiteType
from ipam import models
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import (
BaseObjectType,
NetBoxObjectType,
OrganizationalObjectType,
)
from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType
from .filters import *
from .mixins import IPAddressesMixin
__all__ = (
'ASNType',
@ -101,7 +97,7 @@ class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType):
fields='__all__',
filters=FHRPGroupFilter
)
class FHRPGroupType(NetBoxObjectType):
class FHRPGroupType(NetBoxObjectType, IPAddressesMixin):
@strawberry_django.field
def fhrpgroupassignment_set(self) -> List[Annotated["FHRPGroupAssignmentType", strawberry.lazy('ipam.graphql.types')]]:

View File

@ -844,6 +844,25 @@ class IPAddress(PrimaryModel):
'address': _("Cannot create IP address with /0 mask.")
})
# Do not allow assigning a network ID or broadcast address to an interface.
if self.assigned_object:
if self.address.ip == self.address.network:
msg = _("{ip} is a network ID, which may not be assigned to an interface.").format(
ip=self.address.ip
)
if self.address.version == 4 and self.address.prefixlen not in (31, 32):
raise ValidationError(msg)
if self.address.version == 6 and self.address.prefixlen not in (127, 128):
raise ValidationError(msg)
if (
self.address.version == 4 and self.address.ip == self.address.broadcast and
self.address.prefixlen not in (31, 32)
):
msg = _("{ip} is a broadcast address, which may not be assigned to an interface.").format(
ip=self.address.ip
)
raise ValidationError(msg)
# Enforce unique IP space (if applicable)
if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
duplicate_ips = self.get_duplicates()

View File

@ -1,6 +1,9 @@
from collections import namedtuple
from decimal import Decimal
from django.core.exceptions import FieldDoesNotExist
from django.db import models
from netaddr import IPAddress, IPNetwork
from ipam.fields import IPAddressField, IPNetworkField
from netbox.registry import registry
@ -56,6 +59,24 @@ class SearchIndex:
return FieldTypes.INTEGER
return FieldTypes.STRING
@staticmethod
def get_attr_type(instance, field_name):
"""
Return the data type of the specified object attribute.
"""
value = getattr(instance, field_name)
if type(value) is str:
return FieldTypes.STRING
if type(value) is int:
return FieldTypes.INTEGER
if type(value) in (float, Decimal):
return FieldTypes.FLOAT
if type(value) is IPNetwork:
return FieldTypes.CIDR
if type(value) is IPAddress:
return FieldTypes.INET
return FieldTypes.STRING
@staticmethod
def get_field_value(instance, field_name):
"""
@ -82,7 +103,11 @@ class SearchIndex:
# Capture built-in fields
for name, weight in cls.fields:
type_ = cls.get_field_type(instance, name)
try:
type_ = cls.get_field_type(instance, name)
except FieldDoesNotExist:
# Not a concrete field; handle as an object attribute
type_ = cls.get_attr_type(instance, name)
value = cls.get_field_value(instance, name)
if type_ and value:
values.append(

View File

@ -263,9 +263,11 @@ class SearchTable(tables.Table):
super().__init__(data, **kwargs)
def render_field(self, value, record):
if hasattr(record.object, value):
return title(record.object._meta.get_field(value).verbose_name)
return value
try:
model_field = record.object._meta.get_field(value)
return title(model_field.verbose_name)
except FieldDoesNotExist:
return value
def render_value(self, value):
if not self.highlight:

View File

@ -56,7 +56,7 @@
<td>
{{ object.scheduled|annotated_date|placeholder }}
{% if object.interval %}
({% blocktrans with interval=object.interval %}every {{ interval }} seconds{% endblocktrans %})
({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %})
{% endif %}
</td>
</tr>

View File

@ -1,8 +1,7 @@
import strawberry
import strawberry_django
from tenancy import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
from tenancy import filtersets, models
__all__ = (
'TenantFilter',

View File

@ -1,4 +1,5 @@
from typing import List
import strawberry
import strawberry_django

View File

@ -4,8 +4,8 @@ import strawberry
import strawberry_django
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
from tenancy import models
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
from tenancy import models
from .filters import *
__all__ = (

View File

@ -51,36 +51,43 @@ def parse_alphanumeric_range(string):
'0-3,a-d' => [0, 1, 2, 3, a, b, c, d]
"""
values = []
for dash_range in string.split(','):
for value in string.split(','):
if '-' not in value:
# Item is not a range
values.append(value)
continue
# Find the range's beginning & end values
try:
begin, end = dash_range.split('-')
begin, end = value.split('-')
vals = begin + end
# Break out of loop if there's an invalid pattern to return an error
if (not (vals.isdigit() or vals.isalpha())) or (vals.isalpha() and not (vals.isupper() or vals.islower())):
return []
except ValueError:
begin, end = dash_range, dash_range
raise forms.ValidationError(_('Range "{value}" is invalid.').format(value=value))
# Numeric range
if begin.isdigit() and end.isdigit():
if int(begin) >= int(end):
raise forms.ValidationError(_('Range "{value}" is invalid.').format(value=dash_range))
raise forms.ValidationError(
_('Invalid range: Ending value ({end}) must be greater than beginning value ({begin}).').format(
begin=begin, end=end
)
)
for n in list(range(int(begin), int(end) + 1)):
values.append(n)
# Alphanumeric range
else:
# Value-based
if begin == end:
values.append(begin)
# Range-based
else:
# Not a valid range (more than a single character)
if not len(begin) == len(end) == 1:
raise forms.ValidationError(_('Range "{value}" is invalid.').format(value=dash_range))
# Not a valid range (more than a single character)
if not len(begin) == len(end) == 1:
raise forms.ValidationError(_('Range "{value}" is invalid.').format(value=value))
if ord(begin) >= ord(end):
raise forms.ValidationError(_('Range "{value}" is invalid.').format(value=value))
for n in list(range(ord(begin), ord(end) + 1)):
values.append(chr(n))
if ord(begin) >= ord(end):
raise forms.ValidationError(_('Range "{value}" is invalid.').format(value=dash_range))
for n in list(range(ord(begin), ord(end) + 1)):
values.append(chr(n))
return values

View File

@ -191,7 +191,16 @@ class ExpandAlphanumeric(TestCase):
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
def test_set(self):
def test_set_numeric(self):
input = 'r[1,2]a'
output = sorted([
'r1a',
'r2a',
])
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output)
def test_set_alpha(self):
input = '[r,t]1a'
output = sorted([
'r1a',

View File

@ -1,9 +1,7 @@
import strawberry
import strawberry_django
from virtualization import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
from virtualization import filtersets, models
__all__ = (
'ClusterFilter',

View File

@ -1,4 +1,5 @@
from typing import List
import strawberry
import strawberry_django

View File

@ -3,7 +3,7 @@ from typing import Annotated, List
import strawberry
import strawberry_django
from extras.graphql.mixins import ConfigContextMixin
from extras.graphql.mixins import ConfigContextMixin, ContactsMixin
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType
@ -78,7 +78,7 @@ class ClusterTypeType(OrganizationalObjectType):
fields='__all__',
filters=VirtualMachineFilter
)
class VirtualMachineType(ConfigContextMixin, NetBoxObjectType):
class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType):
_name: str
interface_count: BigInt
virtual_disk_count: BigInt

View File

@ -33,6 +33,15 @@ VMINTERFACE_BUTTONS = """
</ul>
</span>
{% endif %}
{% if perms.vpn.add_tunnel and not record.tunnel_termination %}
<a href="{% url 'vpn:tunnel_add' %}?termination1_type=virtualization.virtualmachine&termination1_parent={{ record.virtual_machine.pk }}&termination1_termination={{ record.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" title="Create a tunnel" class="btn btn-success btn-sm">
<i class="mdi mdi-tunnel-outline" aria-hidden="true"></i>
</a>
{% elif perms.vpn.delete_tunneltermination and record.tunnel_termination %}
<a href="{% url 'vpn:tunneltermination_delete' pk=record.tunnel_termination.pk %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" title="Remove tunnel" class="btn btn-danger btn-sm">
<i class="mdi mdi-tunnel-outline" aria-hidden="true"></i>
</a>
{% endif %}
"""
@ -64,6 +73,10 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
role = columns.ColoredLabelColumn(
verbose_name=_('Role'),
)
platform = tables.Column(
linkify=True,
verbose_name=_('Platform')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
@ -97,9 +110,9 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
class Meta(NetBoxTable.Meta):
model = VirtualMachine
fields = (
'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'tenant_group', 'platform',
'vcpus', 'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'description', 'comments',
'config_template', 'contacts', 'tags', 'created', 'last_updated',
'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'tenant_group', 'vcpus',
'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'description', 'comments', 'config_template',
'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -124,7 +124,7 @@ class EncryptionAlgorithmChoices(ChoiceSet):
(ENCRYPTION_AES256_CBC, '256-bit AES (CBC)'),
(ENCRYPTION_AES256_GCM, '256-bit AES (GCM)'),
(ENCRYPTION_3DES, '3DES'),
(ENCRYPTION_3DES, 'DES'),
(ENCRYPTION_DES, 'DES'),
)

View File

@ -1,8 +1,7 @@
import strawberry
import strawberry_django
from vpn import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
from vpn import filtersets, models
__all__ = (
'TunnelGroupFilter',

View File

@ -1,4 +1,5 @@
from typing import List
import strawberry
import strawberry_django

View File

@ -1,8 +1,7 @@
import strawberry
import strawberry_django
from wireless import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
from wireless import filtersets, models
__all__ = (
'WirelessLANGroupFilter',

View File

@ -1,4 +1,5 @@
from typing import List
import strawberry
import strawberry_django

View File

@ -1,10 +1,10 @@
from typing import Annotated, List, Union
from typing import Annotated, List
import strawberry
import strawberry_django
from wireless import models
from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType
from wireless import models
from .filters import *
__all__ = (

View File

@ -1,9 +1,9 @@
Django==5.0.1
Django==5.0.3
django-cors-headers==4.3.1
django-debug-toolbar==4.3.0
django-filter==23.5
django-filter==24.1
django-graphiql-debug-toolbar==0.2.0
django-htmx==1.17.2
django-htmx==1.17.3
django-mptt==0.14.0
django-pglocks==1.0.4
django-prometheus==2.3.1
@ -15,13 +15,13 @@ django-tables2==2.7.0
django-timezone-field==6.1.0
djangorestframework==3.14.0
drf-spectacular==0.27.1
drf-spectacular-sidecar==2024.2.1
drf-spectacular-sidecar==2024.3.4
feedparser==6.0.11
gunicorn==21.2.0
Jinja2==3.1.3
Markdown==3.5.2
mkdocs-material==9.5.10
mkdocstrings[python-legacy]==0.24.0
mkdocs-material==9.5.13
mkdocstrings[python-legacy]==0.24.1
netaddr==1.2.1
nh3==0.2.15
Pillow==10.2.0