Merge pull request #15423 from netbox-community/develop

Release v3.7.4
This commit is contained in:
Jeremy Stretch 2024-03-13 19:36:44 -04:00 committed by GitHub
commit c2cabe0273
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 147 additions and 77 deletions

View File

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

View File

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

View File

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

View File

@ -101,7 +101,7 @@ markdown-include
mkdocs-material mkdocs-material
# Introspection for embedded code # 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] mkdocstrings[python-legacy]
# Library for manipulating IP prefixes and addresses # Library for manipulating IP prefixes and addresses

View File

@ -384,7 +384,10 @@
"8gfc-sfpp", "8gfc-sfpp",
"16gfc-sfpp", "16gfc-sfpp",
"32gfc-sfp28", "32gfc-sfp28",
"32gfc-sfpp",
"64gfc-qsfpp", "64gfc-qsfpp",
"64gfc-sfpdd",
"64gfc-sfpp",
"128gfc-qsfp28", "128gfc-qsfp28",
"infiniband-sdr", "infiniband-sdr",
"infiniband-ddr", "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: Once PostgreSQL has been installed, start the service and enable it to run at boot:
```no-highlight ```no-highlight
sudo systemctl start postgresql sudo systemctl enable --now postgresql
sudo systemctl enable postgresql
``` ```
Before continuing, verify that you have installed PostgreSQL 12 or later: Before continuing, verify that you have installed PostgreSQL 12 or later:

View File

@ -14,8 +14,7 @@
```no-highlight ```no-highlight
sudo yum install -y redis sudo yum install -y redis
sudo systemctl start redis sudo systemctl enable --now redis
sudo systemctl enable redis
``` ```
Before continuing, verify that your installed version of Redis is at least v4.0: 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: Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
```no-highlight ```no-highlight
sudo systemctl start netbox netbox-rq sudo systemctl enable --now netbox netbox-rq
sudo systemctl enable netbox netbox-rq
``` ```
You can use the command `systemctl status netbox` to verify that the WSGI service is running: You can use the command `systemctl status netbox` to verify that the WSGI service is running:

View File

@ -1,5 +1,30 @@
# NetBox v3.7 # NetBox v3.7
## 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
---
## v3.7.3 (2024-02-21) ## v3.7.3 (2024-02-21)
### Enhancements ### Enhancements

View File

@ -414,11 +414,11 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
class NestedModuleBaySerializer(WritableNestedSerializer): class NestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail') 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: class Meta:
model = models.ModuleBay model = models.ModuleBay
fields = ['id', 'url', 'display', 'module', 'name'] fields = ['id', 'url', 'display', 'installed_module', 'name']
class NestedDeviceBaySerializer(WritableNestedSerializer): class NestedDeviceBaySerializer(WritableNestedSerializer):

View File

@ -326,8 +326,8 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
airflow = ChoiceField(choices=DeviceAirflowChoices, 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) weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
front_image = serializers.URLField(allow_null=True, required=False) front_image = serializers.ImageField(required=False, allow_null=True)
rear_image = serializers.URLField(allow_null=True, required=False) rear_image = serializers.ImageField(required=False, allow_null=True)
# Counter fields # Counter fields
console_port_template_count = serializers.IntegerField(read_only=True) console_port_template_count = serializers.IntegerField(read_only=True)
@ -1039,8 +1039,7 @@ class ModuleBaySerializer(NetBoxModelSerializer):
model = ModuleBay model = ModuleBay
fields = [ fields = [
'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', 'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags',
'custom_fields', 'custom_fields', 'created', 'last_updated',
'created', 'last_updated',
] ]

View File

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

View File

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

View File

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

View File

@ -1755,7 +1755,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
class ModuleBayTest(APIViewTestCases.APIViewTestCase): class ModuleBayTest(APIViewTestCases.APIViewTestCase):
model = ModuleBay model = ModuleBay
brief_fields = ['display', 'id', 'module', 'name', 'url'] brief_fields = ['display', 'id', 'installed_module', 'name', 'url']
bulk_update_data = { bulk_update_data = {
'description': 'New description', 'description': 'New description',
} }

View File

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

View File

@ -7,6 +7,7 @@ from extras.models import ObjectChange
__all__ = ( __all__ = (
'ChangelogMixin', 'ChangelogMixin',
'ConfigContextMixin', 'ConfigContextMixin',
'ContactsMixin',
'CustomFieldsMixin', 'CustomFieldsMixin',
'ImageAttachmentsMixin', 'ImageAttachmentsMixin',
'JournalEntriesMixin', 'JournalEntriesMixin',

View File

@ -11,7 +11,7 @@ from extras.querysets import ConfigContextQuerySet
from netbox.config import get_config from netbox.config import get_config
from netbox.registry import registry from netbox.registry import registry
from netbox.models import ChangeLoggedModel 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.jinja2 import ConfigTemplateLoader
from utilities.utils import deepmerge from utilities.utils import deepmerge
@ -26,7 +26,7 @@ __all__ = (
# Config contexts # 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 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 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 # Config templates
# #
class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): class ConfigTemplate(SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
max_length=100 max_length=100

View File

@ -367,20 +367,6 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
'primary_for_parent', _("Only IP addresses assigned to an interface can be designated as primary IPs.") '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): def save(self, *args, **kwargs):
ipaddress = super().save(*args, **kwargs) ipaddress = super().save(*args, **kwargs)

View File

@ -1,6 +1,7 @@
import graphene import graphene
from ipam import filtersets, models from ipam import filtersets, models
from .mixins import IPAddressesMixin
from netbox.graphql.scalars import BigInt from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
@ -71,7 +72,7 @@ class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType):
filterset_class = filtersets.AggregateFilterSet filterset_class = filtersets.AggregateFilterSet
class FHRPGroupType(NetBoxObjectType): class FHRPGroupType(NetBoxObjectType, IPAddressesMixin):
class Meta: class Meta:
model = models.FHRPGroup model = models.FHRPGroup

View File

@ -844,6 +844,25 @@ class IPAddress(PrimaryModel):
'address': _("Cannot create IP address with /0 mask.") '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) # 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): if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique):
duplicate_ips = self.get_duplicates() duplicate_ips = self.get_duplicates()

View File

@ -28,7 +28,7 @@ from netbox.plugins import PluginConfig
# Environment setup # Environment setup
# #
VERSION = '3.7.3' VERSION = '3.7.4'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()

View File

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

View File

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

View File

@ -191,7 +191,16 @@ class ExpandAlphanumeric(TestCase):
self.assertEqual(sorted(expand_alphanumeric_pattern(input)), output) 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' input = '[r,t]1a'
output = sorted([ output = sorted([
'r1a', 'r1a',

View File

@ -172,6 +172,6 @@ class VirtualDiskSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = VirtualDisk model = VirtualDisk
fields = [ fields = [
'id', 'url', 'virtual_machine', 'name', 'description', 'size', 'tags', 'custom_fields', 'created', 'id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size', 'tags', 'custom_fields',
'last_updated', 'created', 'last_updated',
] ]

View File

@ -1,5 +1,5 @@
from dcim.graphql.types import ComponentObjectType from dcim.graphql.types import ComponentObjectType
from extras.graphql.mixins import ConfigContextMixin from extras.graphql.mixins import ConfigContextMixin, ContactsMixin
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType
from virtualization import filtersets, models from virtualization import filtersets, models
@ -38,7 +38,7 @@ class ClusterTypeType(OrganizationalObjectType):
filterset_class = filtersets.ClusterTypeFilterSet filterset_class = filtersets.ClusterTypeFilterSet
class VirtualMachineType(ConfigContextMixin, NetBoxObjectType): class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType):
class Meta: class Meta:
model = models.VirtualMachine model = models.VirtualMachine

View File

@ -33,6 +33,15 @@ VMINTERFACE_BUTTONS = """
</ul> </ul>
</span> </span>
{% endif %} {% 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( role = columns.ColoredLabelColumn(
verbose_name=_('Role'), verbose_name=_('Role'),
) )
platform = tables.Column(
linkify=True,
verbose_name=_('Platform')
)
comments = columns.MarkdownColumn( comments = columns.MarkdownColumn(
verbose_name=_('Comments'), verbose_name=_('Comments'),
) )
@ -97,9 +110,9 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = VirtualMachine model = VirtualMachine
fields = ( fields = (
'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'tenant_group', 'platform', 'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'tenant_group', 'vcpus',
'vcpus', 'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'description', 'comments', 'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'description', 'comments', 'config_template',
'config_template', 'contacts', 'tags', 'created', 'last_updated', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip', '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_CBC, '256-bit AES (CBC)'),
(ENCRYPTION_AES256_GCM, '256-bit AES (GCM)'), (ENCRYPTION_AES256_GCM, '256-bit AES (GCM)'),
(ENCRYPTION_3DES, '3DES'), (ENCRYPTION_3DES, '3DES'),
(ENCRYPTION_3DES, 'DES'), (ENCRYPTION_DES, 'DES'),
) )

View File

@ -1,8 +1,8 @@
bleach==6.1.0 bleach==6.1.0
Django==4.2.10 Django==4.2.11
django-cors-headers==4.3.1 django-cors-headers==4.3.1
django-debug-toolbar==4.3.0 django-debug-toolbar==4.3.0
django-filter==23.5 django-filter==24.1
django-graphiql-debug-toolbar==0.2.0 django-graphiql-debug-toolbar==0.2.0
django-mptt==0.14.0 django-mptt==0.14.0
django-pglocks==1.0.4 django-pglocks==1.0.4
@ -15,14 +15,14 @@ django-tables2==2.7.0
django-timezone-field==6.1.0 django-timezone-field==6.1.0
djangorestframework==3.14.0 djangorestframework==3.14.0
drf-spectacular==0.27.1 drf-spectacular==0.27.1
drf-spectacular-sidecar==2024.2.1 drf-spectacular-sidecar==2024.3.4
feedparser==6.0.11 feedparser==6.0.11
graphene-django==3.0.0 graphene-django==3.0.0
gunicorn==21.2.0 gunicorn==21.2.0
Jinja2==3.1.3 Jinja2==3.1.3
Markdown==3.5.2 Markdown==3.5.2
mkdocs-material==9.5.10 mkdocs-material==9.5.13
mkdocstrings[python-legacy]==0.24.0 mkdocstrings[python-legacy]==0.24.1
netaddr==1.2.1 netaddr==1.2.1
Pillow==10.2.0 Pillow==10.2.0
psycopg[binary,pool]==3.1.18 psycopg[binary,pool]==3.1.18