mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Merge branch 'feature' into 9856-strawberry-2
This commit is contained in:
commit
9c29f45c1a
5
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
5
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -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
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -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
|
||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -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/*'
|
||||
|
@ -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
|
||||
|
@ -384,7 +384,10 @@
|
||||
"8gfc-sfpp",
|
||||
"16gfc-sfpp",
|
||||
"32gfc-sfp28",
|
||||
"32gfc-sfpp",
|
||||
"64gfc-qsfpp",
|
||||
"64gfc-sfpdd",
|
||||
"64gfc-sfpp",
|
||||
"128gfc-qsfp28",
|
||||
"infiniband-sdr",
|
||||
"infiniband-ddr",
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
|
@ -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__ = (
|
||||
|
@ -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__ = (
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)'),
|
||||
)
|
||||
),
|
||||
|
@ -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__ = (
|
||||
|
@ -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 (
|
||||
|
@ -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',
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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__ = (
|
||||
|
@ -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',
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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',
|
||||
|
@ -1,6 +1,7 @@
|
||||
from typing import Annotated, List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from typing import TYPE_CHECKING, Annotated, List, Union
|
||||
|
||||
__all__ = (
|
||||
'IPAddressesMixin',
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
|
@ -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')]]:
|
||||
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
|
@ -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__ = (
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
@ -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'),
|
||||
)
|
||||
|
||||
|
||||
|
@ -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',
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
|
@ -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',
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import List
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
|
@ -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__ = (
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user