mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-23 12:08:43 -06:00
Compare commits
1 Commits
19869-prov
...
20902-git-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eedefb2df |
43
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
43
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
@@ -1,43 +0,0 @@
|
||||
---
|
||||
name: 🏁 Performance
|
||||
type: Performance
|
||||
description: An opportunity to improve application performance
|
||||
labels: ["netbox", "type: performance", "status: needs triage"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Python Version
|
||||
description: What version of Python are you currently running?
|
||||
options:
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Area(s) of Concern
|
||||
description: Which application interface(s) are affected?
|
||||
options:
|
||||
- label: User Interface
|
||||
- label: REST API
|
||||
- label: GraphQL API
|
||||
- label: Python ORM
|
||||
- label: Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Details
|
||||
description: >
|
||||
Describe in detail the operations being performed and the indications of a performance issue.
|
||||
Include any relevant testing parameters, benchmarks, and expected results.
|
||||
validations:
|
||||
required: true
|
||||
@@ -102,7 +102,10 @@ class GitBackend(DataBackend):
|
||||
clone_args['pool_manager'] = ProxyPoolManager(self.socks_proxy)
|
||||
|
||||
if self.url_scheme in ('http', 'https'):
|
||||
if self.params.get('username'):
|
||||
# Only pass explicit credentials if URL doesn't already contain embedded username
|
||||
# to avoid credential conflicts
|
||||
parsed_url = urlparse(self.url)
|
||||
if not parsed_url.username and self.params.get('username'):
|
||||
clone_args.update(
|
||||
{
|
||||
"username": self.params.get('username'),
|
||||
|
||||
59
netbox/core/tests/test_data_backends.py
Normal file
59
netbox/core/tests/test_data_backends.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from core.data_backends import GitBackend
|
||||
|
||||
|
||||
class GitBackendCredentialTests(TestCase):
|
||||
|
||||
def _get_clone_kwargs(self, url, **params):
|
||||
backend = GitBackend(url=url, **params)
|
||||
|
||||
with patch('dulwich.porcelain.clone') as mock_clone, \
|
||||
patch('dulwich.porcelain.NoneStream'):
|
||||
try:
|
||||
with backend.fetch():
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if mock_clone.called:
|
||||
return mock_clone.call_args.kwargs
|
||||
return {}
|
||||
|
||||
def test_url_with_embedded_username_skips_explicit_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://myuser@bitbucket.org/workspace/repo.git',
|
||||
username='myuser',
|
||||
password='my-api-key'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), None)
|
||||
self.assertEqual(kwargs.get('password'), None)
|
||||
|
||||
def test_url_without_embedded_username_passes_explicit_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://bitbucket.org/workspace/repo.git',
|
||||
username='myuser',
|
||||
password='my-api-key'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), 'myuser')
|
||||
self.assertEqual(kwargs.get('password'), 'my-api-key')
|
||||
|
||||
def test_url_with_embedded_username_no_explicit_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://myuser@bitbucket.org/workspace/repo.git'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), None)
|
||||
self.assertEqual(kwargs.get('password'), None)
|
||||
|
||||
def test_public_repo_no_credentials(self):
|
||||
kwargs = self._get_clone_kwargs(
|
||||
url='https://github.com/public/repo.git'
|
||||
)
|
||||
|
||||
self.assertEqual(kwargs.get('username'), None)
|
||||
self.assertEqual(kwargs.get('password'), None)
|
||||
@@ -27,7 +27,6 @@ __all__ = (
|
||||
'DeviceTable',
|
||||
'FrontPortTable',
|
||||
'InterfaceTable',
|
||||
'InterfaceLAGMemberTable',
|
||||
'InventoryItemRoleTable',
|
||||
'InventoryItemTable',
|
||||
'MACAddressTable',
|
||||
@@ -690,33 +689,6 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
|
||||
class InterfaceLAGMemberTable(PathEndpointTable, NetBoxTable):
|
||||
parent = tables.Column(
|
||||
verbose_name=_('Parent'),
|
||||
accessor=Accessor('device'),
|
||||
linkify=True,
|
||||
)
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
linkify=True,
|
||||
order_by=('_name',),
|
||||
)
|
||||
connection = columns.TemplateColumn(
|
||||
accessor='connected_endpoints',
|
||||
template_code=INTERFACE_LAG_MEMBERS_LINKTERMINATION,
|
||||
verbose_name=_('Peer'),
|
||||
orderable=False,
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:interface_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = models.Interface
|
||||
fields = ('pk', 'parent', 'name', 'type', 'connection')
|
||||
default_columns = ('pk', 'parent', 'name', 'type', 'connection')
|
||||
|
||||
|
||||
class DeviceInterfaceTable(InterfaceTable):
|
||||
name = tables.TemplateColumn(
|
||||
verbose_name=_('Name'),
|
||||
|
||||
@@ -24,24 +24,6 @@ INTERFACE_LINKTERMINATION = """
|
||||
{% else %}""" + LINKTERMINATION + """{% endif %}
|
||||
"""
|
||||
|
||||
INTERFACE_LAG_MEMBERS_LINKTERMINATION = """
|
||||
{% for termination in value %}
|
||||
{% if termination.parent_object %}
|
||||
<a href="{{ termination.parent_object.get_absolute_url }}">{{ termination.parent_object }}</a>
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>
|
||||
{% if termination.lag %}
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
<a href="{{ termination.lag.get_absolute_url }}">{{ termination.lag }}</a>
|
||||
<span class="text-muted">(LAG)</span>
|
||||
{% endif %}
|
||||
{% if not forloop.last %}<br />{% endif %}
|
||||
{% empty %}
|
||||
{{ ''|placeholder }}
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
CABLE_LENGTH = """
|
||||
{% load helpers %}
|
||||
{% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %}
|
||||
|
||||
@@ -3135,14 +3135,6 @@ class InterfaceView(generic.ObjectView):
|
||||
)
|
||||
child_interfaces_table.configure(request)
|
||||
|
||||
# Get LAG interfaces
|
||||
lag_interfaces = Interface.objects.restrict(request.user, 'view').filter(lag=instance)
|
||||
lag_interfaces_table = tables.InterfaceLAGMemberTable(
|
||||
lag_interfaces,
|
||||
orderable=False
|
||||
)
|
||||
lag_interfaces_table.configure(request)
|
||||
|
||||
# Get assigned VLANs and annotate whether each is tagged or untagged
|
||||
vlans = []
|
||||
if instance.untagged_vlan is not None:
|
||||
@@ -3172,7 +3164,6 @@ class InterfaceView(generic.ObjectView):
|
||||
'bridge_interfaces': bridge_interfaces,
|
||||
'bridge_interfaces_table': bridge_interfaces_table,
|
||||
'child_interfaces_table': child_interfaces_table,
|
||||
'lag_interfaces_table': lag_interfaces_table,
|
||||
'vlan_table': vlan_table,
|
||||
'vlan_translation_table': vlan_translation_table,
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ IMAGEATTACHMENT_IMAGE = """
|
||||
<a href="{{ record.image.url }}" target="_blank" class="image-preview" data-bs-placement="top">
|
||||
<i class="mdi mdi-image"></i></a>
|
||||
{% endif %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.filename|truncate_middle:16 }}</a>
|
||||
<a href="{{ record.get_absolute_url }}">{{ record }}</a>
|
||||
"""
|
||||
|
||||
NOTIFICATION_ICON = """
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.forms import ValidationError
|
||||
from django.test import tag, TestCase
|
||||
|
||||
from core.models import AutoSyncRecord, DataSource, ObjectType
|
||||
from core.models import DataSource, ObjectType
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
|
||||
from extras.models import ConfigContext, ConfigContextProfile, ConfigTemplate, ImageAttachment, Tag, TaggedItem
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@@ -754,53 +754,3 @@ class ConfigTemplateTest(TestCase):
|
||||
@tag('regression')
|
||||
def test_config_template_with_data_source_nested_templates(self):
|
||||
self.assertEqual(self.BASE_TEMPLATE, self.main_config_template.render({}))
|
||||
|
||||
@tag('regression')
|
||||
def test_autosyncrecord_cleanup_on_detach(self):
|
||||
"""Test that AutoSyncRecord is deleted when detaching from DataSource."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
templates_dir = Path(temp_dir) / "templates"
|
||||
templates_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._create_template_file(templates_dir, 'test.j2', 'Test content')
|
||||
|
||||
data_source = DataSource(
|
||||
name="Test DataSource for Detach",
|
||||
type="local",
|
||||
source_url=str(templates_dir),
|
||||
)
|
||||
data_source.save()
|
||||
data_source.sync()
|
||||
|
||||
data_file = data_source.datafiles.filter(path__endswith='test.j2').first()
|
||||
|
||||
# Create a ConfigTemplate with data_file and auto_sync_enabled
|
||||
config_template = ConfigTemplate(
|
||||
name="TestTemplateForDetach",
|
||||
data_file=data_file,
|
||||
auto_sync_enabled=True
|
||||
)
|
||||
config_template.clean()
|
||||
config_template.save()
|
||||
|
||||
# Verify AutoSyncRecord was created
|
||||
object_type = ObjectType.objects.get_for_model(ConfigTemplate)
|
||||
autosync_records = AutoSyncRecord.objects.filter(
|
||||
object_type=object_type,
|
||||
object_id=config_template.pk
|
||||
)
|
||||
self.assertEqual(autosync_records.count(), 1, "AutoSyncRecord should be created")
|
||||
|
||||
# Detach from DataSource
|
||||
config_template.data_file = None
|
||||
config_template.data_source = None
|
||||
config_template.auto_sync_enabled = False
|
||||
config_template.clean()
|
||||
config_template.save()
|
||||
|
||||
# Verify AutoSyncRecord was deleted
|
||||
autosync_records = AutoSyncRecord.objects.filter(
|
||||
object_type=object_type,
|
||||
object_id=config_template.pk
|
||||
)
|
||||
self.assertEqual(autosync_records.count(), 0, "AutoSyncRecord should be deleted after detaching")
|
||||
|
||||
@@ -569,6 +569,7 @@ class SyncedDataMixin(models.Model):
|
||||
)
|
||||
else:
|
||||
AutoSyncRecord.objects.filter(
|
||||
datafile=self.data_file,
|
||||
object_type=object_type,
|
||||
object_id=self.pk
|
||||
).delete()
|
||||
@@ -581,6 +582,7 @@ class SyncedDataMixin(models.Model):
|
||||
# Delete AutoSyncRecord
|
||||
object_type = ObjectType.objects.get_for_model(self)
|
||||
AutoSyncRecord.objects.filter(
|
||||
datafile=self.data_file,
|
||||
object_type=object_type,
|
||||
object_id=self.pk
|
||||
).delete()
|
||||
|
||||
@@ -370,6 +370,33 @@
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if object.is_lag %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "LAG Members" %}</h2>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Parent" %}</th>
|
||||
<th>{% trans "Interface" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in object.member_interfaces.all %}
|
||||
<tr>
|
||||
<td>{{ member.device|linkify }}</td>
|
||||
<td>{{ member|linkify }}</td>
|
||||
<td>{{ member.get_type_display }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-muted">{% trans "No member interfaces" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'ipam/inc/panels/fhrp_groups.html' %}
|
||||
{% include 'dcim/inc/panels/inventory_items.html' %}
|
||||
{% plugin_right_page object %}
|
||||
@@ -414,13 +441,6 @@
|
||||
{% include 'inc/panel_table.html' with table=vlan_table heading="VLANs" %}
|
||||
</div>
|
||||
</div>
|
||||
{% if object.is_lag %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% include 'inc/panel_table.html' with table=lag_interfaces_table heading="LAG Members" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if object.vlan_translation_policy %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-22 05:07+0000\n"
|
||||
"POT-Creation-Date: 2026-01-21 05:07+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -12037,7 +12037,7 @@ msgstr ""
|
||||
msgid "date synced"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/netbox/models/features.py:621
|
||||
#: netbox/netbox/models/features.py:623
|
||||
#, python-brace-format
|
||||
msgid "{class_name} must implement a sync_data() method."
|
||||
msgstr ""
|
||||
@@ -13935,8 +13935,8 @@ msgid "No VLANs Assigned"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/inc/interface_vlans_table.html:44
|
||||
#: netbox/templates/ipam/inc/max_depth.html:11
|
||||
#: netbox/templates/ipam/inc/max_length.html:11
|
||||
#: netbox/templates/ipam/prefix_list.html:16
|
||||
#: netbox/templates/ipam/prefix_list.html:33
|
||||
msgid "Clear"
|
||||
msgstr ""
|
||||
|
||||
@@ -15053,8 +15053,8 @@ msgstr ""
|
||||
msgid "Date Added"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/aggregate/prefixes.html:10
|
||||
#: netbox/templates/ipam/prefix/prefixes.html:10
|
||||
#: netbox/templates/ipam/aggregate/prefixes.html:8
|
||||
#: netbox/templates/ipam/prefix/prefixes.html:8
|
||||
#: netbox/templates/ipam/role.html:10
|
||||
msgid "Add Prefix"
|
||||
msgstr ""
|
||||
@@ -15083,14 +15083,6 @@ msgstr ""
|
||||
msgid "Bulk Create"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/inc/max_depth.html:6
|
||||
msgid "Max Depth"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/inc/max_length.html:6
|
||||
msgid "Max Length"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/inc/panels/fhrp_groups.html:10
|
||||
msgid "Create Group"
|
||||
msgstr ""
|
||||
@@ -15192,6 +15184,14 @@ msgstr ""
|
||||
msgid "Hide Depth Indicators"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/prefix_list.html:11
|
||||
msgid "Max Depth"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/prefix_list.html:28
|
||||
msgid "Max Length"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/ipam/rir.html:10
|
||||
msgid "Add Aggregate"
|
||||
msgstr ""
|
||||
|
||||
@@ -252,16 +252,3 @@ def isodatetime(value, spec='seconds'):
|
||||
else:
|
||||
return ''
|
||||
return mark_safe(f'<span title="{naturaltime(value)}">{text}</span>')
|
||||
|
||||
|
||||
@register.filter
|
||||
def truncate_middle(value, length):
|
||||
if len(value) <= length:
|
||||
return value
|
||||
|
||||
# Calculate split points for the two parts
|
||||
half_len = (length - 1) // 2 # 1 for the ellipsis
|
||||
first_part = value[:half_len]
|
||||
second_part = value[len(value) - (length - 1 - half_len):]
|
||||
|
||||
return mark_safe(f"{first_part}…{second_part}")
|
||||
|
||||
Reference in New Issue
Block a user