mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -06:00
commit
9dab3a0d79
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -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.5.3
|
placeholder: v3.5.4
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -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.5.3
|
placeholder: v3.5.4
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -1,5 +1,30 @@
|
|||||||
# NetBox v3.5
|
# NetBox v3.5
|
||||||
|
|
||||||
|
## v3.5.4 (2023-06-20)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#12828](https://github.com/netbox-community/netbox/issues/12828) - Define colors for staged change action choices
|
||||||
|
* [#12847](https://github.com/netbox-community/netbox/issues/12847) - Include "add" button on all device & virtual machine component list views
|
||||||
|
* [#12862](https://github.com/netbox-community/netbox/issues/12862) - Add menu navigation button to add wireless links directly
|
||||||
|
* [#12865](https://github.com/netbox-community/netbox/issues/12865) - Add "add" buttons for reports & scripts to navigation menu
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#12474](https://github.com/netbox-community/netbox/issues/12474) - Update cable terminations when assigning a location to a new site
|
||||||
|
* [#12622](https://github.com/netbox-community/netbox/issues/12622) - Permit the assignment of non-site VLANs to prefixes assigned to a site
|
||||||
|
* [#12682](https://github.com/netbox-community/netbox/issues/12682) - Correct OpenAPI schema for connected device API endpoint
|
||||||
|
* [#12687](https://github.com/netbox-community/netbox/issues/12687) - Allow the assignment of all /31 IP addresses to interfaces
|
||||||
|
* [#12818](https://github.com/netbox-community/netbox/issues/12818) - Fix permissions evaluation when queuing a data sync job
|
||||||
|
* [#12822](https://github.com/netbox-community/netbox/issues/12822) - Fix encoding of whitespace in custom link URLs
|
||||||
|
* [#12838](https://github.com/netbox-community/netbox/issues/12838) - Correct rounding of rack power utilization values
|
||||||
|
* [#12845](https://github.com/netbox-community/netbox/issues/12845) - Fix pagination of objects for related IP addresses table
|
||||||
|
* [#12850](https://github.com/netbox-community/netbox/issues/12850) - Fix table configuration modal for the contact assignments list
|
||||||
|
* [#12885](https://github.com/netbox-community/netbox/issues/12885) - Permit mounting of devices in rack unit 100
|
||||||
|
* [#12914](https://github.com/netbox-community/netbox/issues/12914) - Clear stored ordering from user config when cleared by request
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v3.5.3 (2023-06-02)
|
## v3.5.3 (2023-06-02)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -33,7 +33,7 @@ class DataSourceViewSet(NetBoxModelViewSet):
|
|||||||
"""
|
"""
|
||||||
Enqueue a job to synchronize the DataSource.
|
Enqueue a job to synchronize the DataSource.
|
||||||
"""
|
"""
|
||||||
if not request.user.has_perm('extras.sync_datasource'):
|
if not request.user.has_perm('core.sync_datasource'):
|
||||||
raise PermissionDenied("Syncing data sources requires the core.sync_datasource permission.")
|
raise PermissionDenied("Syncing data sources requires the core.sync_datasource permission.")
|
||||||
|
|
||||||
datasource = get_object_or_404(DataSource, pk=pk)
|
datasource = get_object_or_404(DataSource, pk=pk)
|
||||||
|
@ -646,7 +646,10 @@ class ConnectedDeviceViewSet(ViewSet):
|
|||||||
def get_view_name(self):
|
def get_view_name(self):
|
||||||
return "Connected Device Locator"
|
return "Connected Device Locator"
|
||||||
|
|
||||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
@extend_schema(
|
||||||
|
parameters=[_device_param, _interface_param],
|
||||||
|
responses={200: serializers.DeviceSerializer}
|
||||||
|
)
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
|
|
||||||
peer_device_name = request.query_params.get(self._device_param.name)
|
peer_device_name = request.query_params.get(self._device_param.name)
|
||||||
|
@ -11,6 +11,7 @@ DEVICETYPE_IMAGE_FORMATS = 'image/bmp,image/gif,image/jpeg,image/png,image/tiff,
|
|||||||
#
|
#
|
||||||
|
|
||||||
RACK_U_HEIGHT_DEFAULT = 42
|
RACK_U_HEIGHT_DEFAULT = 42
|
||||||
|
RACK_U_HEIGHT_MAX = 100
|
||||||
|
|
||||||
RACK_ELEVATION_BORDER_WIDTH = 2
|
RACK_ELEVATION_BORDER_WIDTH = 2
|
||||||
RACK_ELEVATION_DEFAULT_LEGEND_WIDTH = 30
|
RACK_ELEVATION_DEFAULT_LEGEND_WIDTH = 30
|
||||||
|
@ -18,6 +18,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='device',
|
model_name='device',
|
||||||
name='position',
|
name='position',
|
||||||
field=models.DecimalField(blank=True, decimal_places=1, max_digits=4, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(99.5)]),
|
field=models.DecimalField(blank=True, decimal_places=1, max_digits=4, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100.5)]),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -568,7 +568,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
|||||||
decimal_places=1,
|
decimal_places=1,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(99.5)],
|
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX + 0.5)],
|
||||||
verbose_name='Position (U)',
|
verbose_name='Position (U)',
|
||||||
help_text=_('The lowest-numbered unit occupied by the device')
|
help_text=_('The lowest-numbered unit occupied by the device')
|
||||||
)
|
)
|
||||||
|
@ -126,7 +126,7 @@ class Rack(PrimaryModel, WeightMixin):
|
|||||||
u_height = models.PositiveSmallIntegerField(
|
u_height = models.PositiveSmallIntegerField(
|
||||||
default=RACK_U_HEIGHT_DEFAULT,
|
default=RACK_U_HEIGHT_DEFAULT,
|
||||||
verbose_name='Height (U)',
|
verbose_name='Height (U)',
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(100)],
|
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)],
|
||||||
help_text=_('Height in rack units')
|
help_text=_('Height in rack units')
|
||||||
)
|
)
|
||||||
desc_units = models.BooleanField(
|
desc_units = models.BooleanField(
|
||||||
@ -466,7 +466,7 @@ class Rack(PrimaryModel, WeightMixin):
|
|||||||
powerport.get_power_draw()['allocated'] for powerport in powerports
|
powerport.get_power_draw()['allocated'] for powerport in powerports
|
||||||
])
|
])
|
||||||
|
|
||||||
return int(allocated_draw / available_power_total * 100)
|
return round(allocated_draw / available_power_total * 100, 1)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def total_weight(self):
|
def total_weight(self):
|
||||||
|
@ -27,6 +27,7 @@ def handle_location_site_change(instance, created, **kwargs):
|
|||||||
Rack.objects.filter(location__in=locations).update(site=instance.site)
|
Rack.objects.filter(location__in=locations).update(site=instance.site)
|
||||||
Device.objects.filter(location__in=locations).update(site=instance.site)
|
Device.objects.filter(location__in=locations).update(site=instance.site)
|
||||||
PowerPanel.objects.filter(location__in=locations).update(site=instance.site)
|
PowerPanel.objects.filter(location__in=locations).update(site=instance.site)
|
||||||
|
CableTermination.objects.filter(_location__in=locations).update(_site=instance.site)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Rack)
|
@receiver(post_save, sender=Rack)
|
||||||
|
@ -2193,7 +2193,6 @@ class ConsolePortListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ConsolePortFilterSet
|
filterset = filtersets.ConsolePortFilterSet
|
||||||
filterset_form = forms.ConsolePortFilterForm
|
filterset_form = forms.ConsolePortFilterForm
|
||||||
table = tables.ConsolePortTable
|
table = tables.ConsolePortTable
|
||||||
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConsolePort)
|
@register_model_view(ConsolePort)
|
||||||
@ -2257,7 +2256,6 @@ class ConsoleServerPortListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ConsoleServerPortFilterSet
|
filterset = filtersets.ConsoleServerPortFilterSet
|
||||||
filterset_form = forms.ConsoleServerPortFilterForm
|
filterset_form = forms.ConsoleServerPortFilterForm
|
||||||
table = tables.ConsoleServerPortTable
|
table = tables.ConsoleServerPortTable
|
||||||
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ConsoleServerPort)
|
@register_model_view(ConsoleServerPort)
|
||||||
@ -2321,7 +2319,6 @@ class PowerPortListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.PowerPortFilterSet
|
filterset = filtersets.PowerPortFilterSet
|
||||||
filterset_form = forms.PowerPortFilterForm
|
filterset_form = forms.PowerPortFilterForm
|
||||||
table = tables.PowerPortTable
|
table = tables.PowerPortTable
|
||||||
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(PowerPort)
|
@register_model_view(PowerPort)
|
||||||
@ -2385,7 +2382,6 @@ class PowerOutletListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.PowerOutletFilterSet
|
filterset = filtersets.PowerOutletFilterSet
|
||||||
filterset_form = forms.PowerOutletFilterForm
|
filterset_form = forms.PowerOutletFilterForm
|
||||||
table = tables.PowerOutletTable
|
table = tables.PowerOutletTable
|
||||||
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(PowerOutlet)
|
@register_model_view(PowerOutlet)
|
||||||
@ -2449,7 +2445,6 @@ class InterfaceListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.InterfaceFilterSet
|
filterset = filtersets.InterfaceFilterSet
|
||||||
filterset_form = forms.InterfaceFilterForm
|
filterset_form = forms.InterfaceFilterForm
|
||||||
table = tables.InterfaceTable
|
table = tables.InterfaceTable
|
||||||
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Interface)
|
@register_model_view(Interface)
|
||||||
@ -2559,7 +2554,6 @@ class FrontPortListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.FrontPortFilterSet
|
filterset = filtersets.FrontPortFilterSet
|
||||||
filterset_form = forms.FrontPortFilterForm
|
filterset_form = forms.FrontPortFilterForm
|
||||||
table = tables.FrontPortTable
|
table = tables.FrontPortTable
|
||||||
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(FrontPort)
|
@register_model_view(FrontPort)
|
||||||
@ -2623,7 +2617,6 @@ class RearPortListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.RearPortFilterSet
|
filterset = filtersets.RearPortFilterSet
|
||||||
filterset_form = forms.RearPortFilterForm
|
filterset_form = forms.RearPortFilterForm
|
||||||
table = tables.RearPortTable
|
table = tables.RearPortTable
|
||||||
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(RearPort)
|
@register_model_view(RearPort)
|
||||||
@ -2687,7 +2680,6 @@ class ModuleBayListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.ModuleBayFilterSet
|
filterset = filtersets.ModuleBayFilterSet
|
||||||
filterset_form = forms.ModuleBayFilterForm
|
filterset_form = forms.ModuleBayFilterForm
|
||||||
table = tables.ModuleBayTable
|
table = tables.ModuleBayTable
|
||||||
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(ModuleBay)
|
@register_model_view(ModuleBay)
|
||||||
@ -2743,7 +2735,6 @@ class DeviceBayListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.DeviceBayFilterSet
|
filterset = filtersets.DeviceBayFilterSet
|
||||||
filterset_form = forms.DeviceBayFilterForm
|
filterset_form = forms.DeviceBayFilterForm
|
||||||
table = tables.DeviceBayTable
|
table = tables.DeviceBayTable
|
||||||
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(DeviceBay)
|
@register_model_view(DeviceBay)
|
||||||
@ -2868,7 +2859,6 @@ class InventoryItemListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.InventoryItemFilterSet
|
filterset = filtersets.InventoryItemFilterSet
|
||||||
filterset_form = forms.InventoryItemFilterForm
|
filterset_form = forms.InventoryItemFilterForm
|
||||||
table = tables.InventoryItemTable
|
table = tables.InventoryItemTable
|
||||||
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(InventoryItem)
|
@register_model_view(InventoryItem)
|
||||||
|
@ -210,7 +210,7 @@ class ChangeActionChoices(ChoiceSet):
|
|||||||
ACTION_DELETE = 'delete'
|
ACTION_DELETE = 'delete'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(ACTION_CREATE, 'Create'),
|
(ACTION_CREATE, 'Create', 'green'),
|
||||||
(ACTION_UPDATE, 'Update'),
|
(ACTION_UPDATE, 'Update', 'blue'),
|
||||||
(ACTION_DELETE, 'Delete'),
|
(ACTION_DELETE, 'Delete', 'red'),
|
||||||
)
|
)
|
||||||
|
@ -285,7 +285,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
text = clean_html(text, allowed_schemes)
|
text = clean_html(text, allowed_schemes)
|
||||||
|
|
||||||
# Sanitize link
|
# Sanitize link
|
||||||
link = urllib.parse.quote_plus(link, safe='/:?&=%+[]@#')
|
link = urllib.parse.quote(link, safe='/:?&=%+[]@#')
|
||||||
|
|
||||||
# Verify link scheme is allowed
|
# Verify link scheme is allowed
|
||||||
result = urllib.parse.urlparse(link)
|
result = urllib.parse.urlparse(link)
|
||||||
|
@ -112,3 +112,6 @@ class StagedChange(ChangeLoggedModel):
|
|||||||
instance = self.model.objects.get(pk=self.object_id)
|
instance = self.model.objects.get(pk=self.object_id)
|
||||||
logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')
|
logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')
|
||||||
instance.delete()
|
instance.delete()
|
||||||
|
|
||||||
|
def get_action_color(self):
|
||||||
|
return ChangeActionChoices.colors.get(self.action)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Site
|
from dcim.models import Device, Interface, Site
|
||||||
@ -181,16 +182,31 @@ class PrefixImportForm(NetBoxModelImportForm):
|
|||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
super().__init__(data, *args, **kwargs)
|
super().__init__(data, *args, **kwargs)
|
||||||
|
|
||||||
if data:
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
# Limit VLAN queryset by assigned site and/or group (if specified)
|
site = data.get('site')
|
||||||
params = {}
|
vlan_group = data.get('vlan_group')
|
||||||
if data.get('site'):
|
|
||||||
params[f"site__{self.fields['site'].to_field_name}"] = data.get('site')
|
# Limit VLAN queryset by assigned site and/or group (if specified)
|
||||||
if data.get('vlan_group'):
|
query = Q()
|
||||||
params[f"group__{self.fields['vlan_group'].to_field_name}"] = data.get('vlan_group')
|
|
||||||
if params:
|
if site:
|
||||||
self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params)
|
query |= Q(**{
|
||||||
|
f"site__{self.fields['site'].to_field_name}": site
|
||||||
|
})
|
||||||
|
# Don't Forget to include VLANs without a site in the filter
|
||||||
|
query |= Q(**{
|
||||||
|
f"site__{self.fields['site'].to_field_name}__isnull": True
|
||||||
|
})
|
||||||
|
|
||||||
|
if vlan_group:
|
||||||
|
query &= Q(**{
|
||||||
|
f"group__{self.fields['vlan_group'].to_field_name}": vlan_group
|
||||||
|
})
|
||||||
|
|
||||||
|
queryset = self.fields['vlan'].queryset.filter(query)
|
||||||
|
self.fields['vlan'].queryset = queryset
|
||||||
|
|
||||||
|
|
||||||
class IPRangeImportForm(NetBoxModelImportForm):
|
class IPRangeImportForm(NetBoxModelImportForm):
|
||||||
|
@ -211,10 +211,8 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
|
|||||||
vlan = DynamicModelChoiceField(
|
vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
selector=True,
|
||||||
label=_('VLAN'),
|
label=_('VLAN'),
|
||||||
query_params={
|
|
||||||
'site_id': '$site',
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
@ -370,7 +368,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
|||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
if address.version == 6 and address.prefixlen not in (127, 128):
|
if address.version == 6 and address.prefixlen not in (127, 128):
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
if address.ip == address.broadcast:
|
if address.version == 4 and address.ip == address.broadcast and address.prefixlen not in (31, 32):
|
||||||
msg = f"{address} is a broadcast address, which may not be assigned to an interface."
|
msg = f"{address} is a broadcast address, which may not be assigned to an interface."
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
|
|
||||||
|
@ -495,6 +495,65 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
url = reverse('ipam:prefix_ipaddresses', kwargs={'pk': prefix.pk})
|
url = reverse('ipam:prefix_ipaddresses', kwargs={'pk': prefix.pk})
|
||||||
self.assertHttpStatus(self.client.get(url), 200)
|
self.assertHttpStatus(self.client.get(url), 200)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_prefix_import(self):
|
||||||
|
"""
|
||||||
|
Custom import test for YAML-based imports (versus CSV)
|
||||||
|
"""
|
||||||
|
IMPORT_DATA = """
|
||||||
|
prefix: 10.1.1.0/24
|
||||||
|
status: active
|
||||||
|
vlan: 101
|
||||||
|
site: Site 1
|
||||||
|
"""
|
||||||
|
# Note, a site is not tied to the VLAN to verify the fix for #12622
|
||||||
|
VLAN.objects.create(vid=101, name='VLAN101')
|
||||||
|
|
||||||
|
# Add all required permissions to the test user
|
||||||
|
self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
|
||||||
|
|
||||||
|
form_data = {
|
||||||
|
'data': IMPORT_DATA,
|
||||||
|
'format': 'yaml'
|
||||||
|
}
|
||||||
|
response = self.client.post(reverse('ipam:prefix_import'), data=form_data, follow=True)
|
||||||
|
self.assertHttpStatus(response, 200)
|
||||||
|
|
||||||
|
prefix = Prefix.objects.get(prefix='10.1.1.0/24')
|
||||||
|
self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_ACTIVE)
|
||||||
|
self.assertEqual(prefix.vlan.vid, 101)
|
||||||
|
self.assertEqual(prefix.site.name, "Site 1")
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_prefix_import_with_vlan_group(self):
|
||||||
|
"""
|
||||||
|
This test covers a unique import edge case where VLAN group is specified during the import.
|
||||||
|
"""
|
||||||
|
IMPORT_DATA = """
|
||||||
|
prefix: 10.1.2.0/24
|
||||||
|
status: active
|
||||||
|
vlan: 102
|
||||||
|
site: Site 1
|
||||||
|
vlan_group: Group 1
|
||||||
|
"""
|
||||||
|
vlan_group = VLANGroup.objects.create(name='Group 1', slug='group-1', scope=Site.objects.get(name="Site 1"))
|
||||||
|
VLAN.objects.create(vid=102, name='VLAN102', group=vlan_group)
|
||||||
|
|
||||||
|
# Add all required permissions to the test user
|
||||||
|
self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
|
||||||
|
|
||||||
|
form_data = {
|
||||||
|
'data': IMPORT_DATA,
|
||||||
|
'format': 'yaml'
|
||||||
|
}
|
||||||
|
response = self.client.post(reverse('ipam:prefix_import'), data=form_data, follow=True)
|
||||||
|
self.assertHttpStatus(response, 200)
|
||||||
|
|
||||||
|
prefix = Prefix.objects.get(prefix='10.1.2.0/24')
|
||||||
|
self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_ACTIVE)
|
||||||
|
self.assertEqual(prefix.vlan.vid, 102)
|
||||||
|
self.assertEqual(prefix.site.name, "Site 1")
|
||||||
|
|
||||||
|
|
||||||
class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = IPRange
|
model = IPRange
|
||||||
|
@ -102,7 +102,7 @@ CONNECTIONS_MENU = Menu(
|
|||||||
label=_('Connections'),
|
label=_('Connections'),
|
||||||
items=(
|
items=(
|
||||||
get_model_item('dcim', 'cable', _('Cables'), actions=['import']),
|
get_model_item('dcim', 'cable', _('Cables'), actions=['import']),
|
||||||
get_model_item('wireless', 'wirelesslink', _('Wireless Links'), actions=['import']),
|
get_model_item('wireless', 'wirelesslink', _('Wireless Links')),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
link='dcim:interface_connections_list',
|
link='dcim:interface_connections_list',
|
||||||
link_text=_('Interface Connections'),
|
link_text=_('Interface Connections'),
|
||||||
@ -301,12 +301,14 @@ CUSTOMIZATION_MENU = Menu(
|
|||||||
MenuItem(
|
MenuItem(
|
||||||
link='extras:report_list',
|
link='extras:report_list',
|
||||||
link_text=_('Reports'),
|
link_text=_('Reports'),
|
||||||
permissions=['extras.view_report']
|
permissions=['extras.view_report'],
|
||||||
|
buttons=get_model_buttons('extras', "reportmodule", actions=['add'])
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
link='extras:script_list',
|
link='extras:script_list',
|
||||||
link_text=_('Scripts'),
|
link_text=_('Scripts'),
|
||||||
permissions=['extras.view_script']
|
permissions=['extras.view_script'],
|
||||||
|
buttons=get_model_buttons('extras', "scriptmodule", actions=['add'])
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.5.3'
|
VERSION = '3.5.4'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -140,10 +140,14 @@ class BaseTable(tables.Table):
|
|||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
table_name = self.__class__.__name__
|
table_name = self.__class__.__name__
|
||||||
if self.prefixed_order_by_field in request.GET:
|
if self.prefixed_order_by_field in request.GET:
|
||||||
# If an ordering has been specified as a query parameter, save it as the
|
if request.GET[self.prefixed_order_by_field]:
|
||||||
# user's preferred ordering for this table.
|
# If an ordering has been specified as a query parameter, save it as the
|
||||||
ordering = request.GET.getlist(self.prefixed_order_by_field)
|
# user's preferred ordering for this table.
|
||||||
request.user.config.set(f'tables.{table_name}.ordering', ordering, commit=True)
|
ordering = request.GET.getlist(self.prefixed_order_by_field)
|
||||||
|
request.user.config.set(f'tables.{table_name}.ordering', ordering, commit=True)
|
||||||
|
else:
|
||||||
|
# If the ordering has been set to none (empty), clear any existing preference.
|
||||||
|
request.user.config.clear(f'tables.{table_name}.ordering', commit=True)
|
||||||
elif ordering := request.user.config.get(f'tables.{table_name}.ordering'):
|
elif ordering := request.user.config.get(f'tables.{table_name}.ordering'):
|
||||||
# If no ordering has been specified, set the preferred ordering (if any).
|
# If no ordering has been specified, set the preferred ordering (if any).
|
||||||
self.order_by = ordering
|
self.order_by = ordering
|
||||||
|
@ -2,18 +2,18 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
|
{% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="object_list">
|
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||||
{% include 'htmx/table.html' %}
|
{% include 'htmx/table.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modals %}
|
{% block modals %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% table_config_form table %}
|
{% table_config_form table %}
|
||||||
{% endblock modals %}
|
{% endblock modals %}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'inc/table_controls_htmx.html' with table_modal="ContactTable_config" %}
|
{% include 'inc/table_controls_htmx.html' with table_modal="ContactAssignmentTable_config" %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -178,7 +178,7 @@ def register_model_view(model, name='', path=None, kwargs=None):
|
|||||||
This decorator can be used to "attach" a view to any model in NetBox. This is typically used to inject
|
This decorator can be used to "attach" a view to any model in NetBox. This is typically used to inject
|
||||||
additional tabs within a model's detail view. For example, to add a custom tab to NetBox's dcim.Site model:
|
additional tabs within a model's detail view. For example, to add a custom tab to NetBox's dcim.Site model:
|
||||||
|
|
||||||
@netbox_model_view(Site, 'myview', path='my-custom-view')
|
@register_model_view(Site, 'myview', path='my-custom-view')
|
||||||
class MyView(ObjectView):
|
class MyView(ObjectView):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@ -415,7 +415,6 @@ class VMInterfaceListView(generic.ObjectListView):
|
|||||||
filterset = filtersets.VMInterfaceFilterSet
|
filterset = filtersets.VMInterfaceFilterSet
|
||||||
filterset_form = forms.VMInterfaceFilterForm
|
filterset_form = forms.VMInterfaceFilterForm
|
||||||
table = tables.VMInterfaceTable
|
table = tables.VMInterfaceTable
|
||||||
actions = ('import', 'export', 'bulk_edit', 'bulk_delete')
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VMInterface)
|
@register_model_view(VMInterface)
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
bleach==6.0.0
|
bleach==6.0.0
|
||||||
boto3==1.26.145
|
boto3==1.26.156
|
||||||
Django==4.1.9
|
Django==4.1.9
|
||||||
django-cors-headers==4.0.0
|
django-cors-headers==4.1.0
|
||||||
django-debug-toolbar==4.1.0
|
django-debug-toolbar==4.1.0
|
||||||
django-filter==23.2
|
django-filter==23.2
|
||||||
django-graphiql-debug-toolbar==0.2.0
|
django-graphiql-debug-toolbar==0.2.0
|
||||||
django-mptt==0.14
|
django-mptt==0.14
|
||||||
django-pglocks==1.0.4
|
django-pglocks==1.0.4
|
||||||
django-prometheus==2.3.1
|
django-prometheus==2.3.1
|
||||||
django-redis==5.2.0
|
django-redis==5.3.0
|
||||||
django-rich==1.5.0
|
django-rich==1.6.0
|
||||||
django-rq==2.8.1
|
django-rq==2.8.1
|
||||||
django-tables2==2.5.3
|
django-tables2==2.5.3
|
||||||
django-taggit==4.0.0
|
django-taggit==4.0.0
|
||||||
django-timezone-field==5.0
|
django-timezone-field==5.1
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
drf-spectacular==0.26.2
|
drf-spectacular==0.26.2
|
||||||
drf-spectacular-sidecar==2023.6.1
|
drf-spectacular-sidecar==2023.6.1
|
||||||
@ -23,15 +23,15 @@ graphene-django==3.0.0
|
|||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.3.7
|
Markdown==3.3.7
|
||||||
mkdocs-material==9.1.15
|
mkdocs-material==9.1.16
|
||||||
mkdocstrings[python-legacy]==0.22.0
|
mkdocstrings[python-legacy]==0.22.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.5.0
|
Pillow==9.5.0
|
||||||
psycopg2-binary==2.9.6
|
psycopg2-binary==2.9.6
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.25.0
|
sentry-sdk==1.25.1
|
||||||
social-auth-app-django==5.2.0
|
social-auth-app-django==5.2.0
|
||||||
social-auth-core[openidconnect]==4.4.2
|
social-auth-core[openidconnect]==4.4.2
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
tablib==3.4.0
|
tablib==3.5.0
|
||||||
tzdata==2023.3
|
tzdata==2023.3
|
||||||
|
Loading…
Reference in New Issue
Block a user