mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-07 15:00:04 +01:00
Compare commits
13 Commits
21468-copy
...
17654-asn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17d413cf29 | ||
|
|
c78972d4e9 | ||
|
|
37dab713cb | ||
|
|
9ca0424428 | ||
|
|
25ab954992 | ||
|
|
4b67f22c80 | ||
|
|
0072403677 | ||
|
|
37ce01a89f | ||
|
|
28468c32c2 | ||
|
|
6eafffb497 | ||
|
|
53ea48efa9 | ||
|
|
1a404f5c0f | ||
|
|
3320e07b70 |
@@ -31,6 +31,11 @@ The following data is available as context for Jinja2 templates:
|
||||
* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
|
||||
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
|
||||
|
||||
!!! warning "Deprecation of legacy fields"
|
||||
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
|
||||
|
||||
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.
|
||||
|
||||
### Default Request Body
|
||||
|
||||
If no body template is specified, the request body will be populated with a JSON object containing the context data. For example, a newly created site might appear as follows:
|
||||
|
||||
@@ -88,3 +88,8 @@ The following context variables are available in to the text and link templates.
|
||||
| `request_id` | The unique request ID |
|
||||
| `data` | A complete serialized representation of the object |
|
||||
| `snapshots` | Pre- and post-change snapshots of the object |
|
||||
|
||||
!!! warning "Deprecation of legacy fields"
|
||||
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
|
||||
|
||||
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.
|
||||
|
||||
@@ -14,6 +14,10 @@ The 16- or 32-bit AS number.
|
||||
|
||||
The [Regional Internet Registry](./rir.md) or similar authority responsible for the allocation of this particular ASN.
|
||||
|
||||
### Role
|
||||
|
||||
The user-defined functional [role](./role.md) assigned to this ASN.
|
||||
|
||||
### Sites
|
||||
|
||||
The [site(s)](../dcim/site.md) to which this ASN is assigned.
|
||||
|
||||
@@ -43,6 +43,11 @@ The resulting webhook payload will look like the following:
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning "Deprecation of legacy fields"
|
||||
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
|
||||
|
||||
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.
|
||||
|
||||
!!! note "Consider namespacing webhook data"
|
||||
The data returned from all webhook callbacks will be compiled into a single `context` dictionary. Any existing keys within this dictionary will be overwritten by subsequent callbacks which include those keys. To avoid collisions with webhook data provided by other plugins, consider namespacing your plugin's data within a nested dictionary as such:
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import warnings
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
|
||||
@@ -17,11 +18,12 @@ class Command(BaseCommand):
|
||||
help = "Perform nightly housekeeping tasks [DEPRECATED]"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(
|
||||
warnings.warn(
|
||||
"\n\nDEPRECATION WARNING\n"
|
||||
"Running this command is no longer necessary: All housekeeping tasks\n"
|
||||
"are addressed automatically via NetBox's built-in job scheduler. It\n"
|
||||
"will be removed in a future release.",
|
||||
self.style.WARNING
|
||||
"will be removed in a future release.\n",
|
||||
category=FutureWarning,
|
||||
)
|
||||
|
||||
config = Config()
|
||||
|
||||
@@ -6,6 +6,8 @@ from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
|
||||
from .roles import RoleSerializer
|
||||
|
||||
__all__ = (
|
||||
'ASNRangeSerializer',
|
||||
'ASNSerializer',
|
||||
@@ -56,6 +58,7 @@ class ASNSiteSerializer(PrimaryModelSerializer):
|
||||
|
||||
class ASNSerializer(PrimaryModelSerializer):
|
||||
rir = RIRSerializer(nested=True, required=False, allow_null=True)
|
||||
role = RoleSerializer(nested=True, required=False, allow_null=True)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
sites = SerializedPKRelatedField(
|
||||
queryset=Site.objects.all(),
|
||||
@@ -72,8 +75,8 @@ class ASNSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = ASN
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'asn', 'rir', 'tenant', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'site_count', 'provider_count', 'sites',
|
||||
'id', 'url', 'display_url', 'display', 'asn', 'rir', 'role', 'tenant', 'description', 'owner', 'comments',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'site_count', 'provider_count', 'sites',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'asn', 'description')
|
||||
|
||||
|
||||
@@ -12,11 +12,13 @@ class RoleSerializer(OrganizationalModelSerializer):
|
||||
# Related object counts
|
||||
prefix_count = RelatedObjectCountField('prefixes')
|
||||
vlan_count = RelatedObjectCountField('vlans')
|
||||
asn_count = RelatedObjectCountField('asns')
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'weight', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'prefix_count', 'vlan_count',
|
||||
'custom_fields', 'created', 'last_updated', 'prefix_count', 'vlan_count', 'asn_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count')
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count',
|
||||
'asn_count')
|
||||
|
||||
@@ -289,6 +289,18 @@ class ASNFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Role.objects.all(),
|
||||
distinct=False,
|
||||
label=_('Role (ID)'),
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='role__slug',
|
||||
queryset=Role.objects.all(),
|
||||
distinct=False,
|
||||
to_field_name='slug',
|
||||
label=_('Role (slug)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ASN
|
||||
|
||||
@@ -121,6 +121,11 @@ class ASNBulkEditForm(PrimaryModelBulkEditForm):
|
||||
required=False,
|
||||
label=_('RIR')
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
required=False,
|
||||
label=_('Role')
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
@@ -129,9 +134,9 @@ class ASNBulkEditForm(PrimaryModelBulkEditForm):
|
||||
|
||||
model = ASN
|
||||
fieldsets = (
|
||||
FieldSet('sites', 'rir', 'tenant', 'description'),
|
||||
FieldSet('sites', 'rir', 'role', 'tenant', 'description'),
|
||||
)
|
||||
nullable_fields = ('tenant', 'description', 'comments')
|
||||
nullable_fields = ('role', 'tenant', 'description', 'comments')
|
||||
|
||||
|
||||
class AggregateBulkEditForm(PrimaryModelBulkEditForm):
|
||||
|
||||
@@ -138,6 +138,13 @@ class ASNImportForm(PrimaryModelImportForm):
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned RIR')
|
||||
)
|
||||
role = CSVModelChoiceField(
|
||||
label=_('Role'),
|
||||
queryset=Role.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text=_('Functional role')
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
label=_('Tenant'),
|
||||
queryset=Tenant.objects.all(),
|
||||
@@ -148,7 +155,7 @@ class ASNImportForm(PrimaryModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ASN
|
||||
fields = ('asn', 'rir', 'tenant', 'description', 'owner', 'comments', 'tags')
|
||||
fields = ('asn', 'rir', 'role', 'tenant', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class RoleImportForm(OrganizationalModelImportForm):
|
||||
|
||||
@@ -151,7 +151,7 @@ class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = ASN
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('rir_id', 'site_group_id', 'site_id', name=_('Assignment')),
|
||||
FieldSet('rir_id', 'role_id', 'site_group_id', 'site_id', name=_('Assignment')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
)
|
||||
@@ -160,6 +160,11 @@ class ASNFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
required=False,
|
||||
label=_('RIR')
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
required=False,
|
||||
label=_('Role')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
|
||||
@@ -152,6 +152,12 @@ class ASNForm(TenancyForm, PrimaryModelForm):
|
||||
label=_('RIR'),
|
||||
quick_add=True
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
label=_('Role'),
|
||||
required=False,
|
||||
quick_add=True
|
||||
)
|
||||
sites = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label=_('Sites'),
|
||||
@@ -159,14 +165,14 @@ class ASNForm(TenancyForm, PrimaryModelForm):
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('asn', 'rir', 'sites', 'description', 'tags', name=_('ASN')),
|
||||
FieldSet('asn', 'rir', 'role', 'sites', 'description', 'tags', name=_('ASN')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ASN
|
||||
fields = [
|
||||
'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags'
|
||||
'asn', 'rir', 'role', 'sites', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags'
|
||||
]
|
||||
widgets = {
|
||||
'date_added': DatePicker(),
|
||||
|
||||
@@ -57,6 +57,8 @@ __all__ = (
|
||||
class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
role: Annotated['RoleFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
role_id: ID | None = strawberry_django.filter_field()
|
||||
asn: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
@@ -77,6 +77,7 @@ class BaseIPAddressFamilyType:
|
||||
class ASNType(ContactsMixin, PrimaryObjectType):
|
||||
asn: BigInt
|
||||
rir: Annotated["RIRType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
|
||||
sites: list[SiteType]
|
||||
|
||||
21
netbox/ipam/migrations/0087_add_asn_role.py
Normal file
21
netbox/ipam/migrations/0087_add_asn_role.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.11 on 2026-03-04 19:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0086_gfk_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='asn',
|
||||
name='role',
|
||||
field=models.ForeignKey(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asns', to='ipam.role'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -137,6 +137,15 @@ class ASN(ContactsMixin, PrimaryModel):
|
||||
verbose_name=_('ASN'),
|
||||
help_text=_('16- or 32-bit autonomous system number')
|
||||
)
|
||||
role = models.ForeignKey(
|
||||
to='ipam.Role',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='asns',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('role'),
|
||||
help_text=_("The primary function of this ASN")
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
|
||||
@@ -23,7 +23,7 @@ class ASNIndex(SearchIndex):
|
||||
('prefixed_name', 110),
|
||||
('description', 500),
|
||||
)
|
||||
display_attrs = ('rir', 'tenant', 'description')
|
||||
display_attrs = ('rir', 'role', 'tenant', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
|
||||
@@ -71,6 +71,10 @@ class ASNTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
url_params={'asn_id': 'pk'},
|
||||
verbose_name=_('Provider Count')
|
||||
)
|
||||
role = tables.Column(
|
||||
verbose_name=_('Role'),
|
||||
linkify=True
|
||||
)
|
||||
sites = columns.ManyToManyColumn(
|
||||
linkify_item=True,
|
||||
verbose_name=_('Sites')
|
||||
@@ -82,9 +86,9 @@ class ASNTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
class Meta(PrimaryModelTable.Meta):
|
||||
model = ASN
|
||||
fields = (
|
||||
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description',
|
||||
'contacts', 'comments', 'sites', 'tags', 'created', 'last_updated', 'actions',
|
||||
'pk', 'asn', 'asn_asdot', 'rir', 'role', 'site_count', 'provider_count', 'tenant', 'tenant_group',
|
||||
'description', 'contacts', 'comments', 'sites', 'tags', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant',
|
||||
'pk', 'asn', 'rir', 'role', 'site_count', 'provider_count', 'sites', 'description', 'tenant',
|
||||
)
|
||||
|
||||
@@ -120,6 +120,11 @@ class RoleTable(OrganizationalModelTable):
|
||||
url_params={'role_id': 'pk'},
|
||||
verbose_name=_('VLANs')
|
||||
)
|
||||
asn_count = columns.LinkedCountColumn(
|
||||
viewname='ipam:asn_list',
|
||||
url_params={'role_id': 'pk'},
|
||||
verbose_name=_('ASNs')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='ipam:role_list'
|
||||
)
|
||||
@@ -127,10 +132,10 @@ class RoleTable(OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = Role
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'slug', 'prefix_count', 'iprange_count', 'vlan_count', 'description', 'weight',
|
||||
'comments', 'tags', 'created', 'last_updated', 'actions',
|
||||
'pk', 'id', 'name', 'slug', 'prefix_count', 'iprange_count', 'vlan_count', 'asn_count', 'description',
|
||||
'weight', 'comments', 'tags', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'prefix_count', 'iprange_count', 'vlan_count', 'description')
|
||||
default_columns = ('pk', 'name', 'prefix_count', 'iprange_count', 'vlan_count', 'asn_count', 'description')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -151,6 +151,12 @@ class ASNTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
RIR.objects.bulk_create(rirs)
|
||||
|
||||
roles = (
|
||||
Role(name='Role 1', slug='role-1'),
|
||||
Role(name='Role 2', slug='role-2'),
|
||||
)
|
||||
Role.objects.bulk_create(roles)
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2')
|
||||
@@ -164,10 +170,10 @@ class ASNTest(APIViewTestCases.APIViewTestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
asns = (
|
||||
ASN(asn=65000, rir=rirs[0], tenant=tenants[0]),
|
||||
ASN(asn=65001, rir=rirs[0], tenant=tenants[1]),
|
||||
ASN(asn=4200000000, rir=rirs[1], tenant=tenants[0]),
|
||||
ASN(asn=4200000001, rir=rirs[1], tenant=tenants[1]),
|
||||
ASN(asn=65000, rir=rirs[0], role=roles[0], tenant=tenants[0]),
|
||||
ASN(asn=65001, rir=rirs[0], role=roles[0], tenant=tenants[1]),
|
||||
ASN(asn=4200000000, rir=rirs[1], role=roles[1], tenant=tenants[0]),
|
||||
ASN(asn=4200000001, rir=rirs[1], role=roles[1], tenant=tenants[1]),
|
||||
)
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
@@ -180,10 +186,12 @@ class ASNTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'asn': 64512,
|
||||
'rir': rirs[0].pk,
|
||||
'role': roles[0].pk,
|
||||
},
|
||||
{
|
||||
'asn': 65002,
|
||||
'rir': rirs[0].pk,
|
||||
'role': roles[1].pk,
|
||||
},
|
||||
{
|
||||
'asn': 4200000002,
|
||||
@@ -375,7 +383,7 @@ class AggregateTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class RoleTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Role
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'prefix_count', 'slug', 'url', 'vlan_count']
|
||||
brief_fields = ['asn_count', 'description', 'display', 'id', 'name', 'prefix_count', 'slug', 'url', 'vlan_count']
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Role 4',
|
||||
@@ -404,6 +412,17 @@ class RoleTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
Role.objects.bulk_create(roles)
|
||||
|
||||
rirs = (
|
||||
RIR(name='RIR 1', slug='rir-1', is_private=True),
|
||||
)
|
||||
RIR.objects.bulk_create(rirs)
|
||||
|
||||
asns = (
|
||||
ASN(asn=65000, rir=rirs[0], role=roles[0]),
|
||||
ASN(asn=65001, rir=rirs[0], role=roles[0]),
|
||||
)
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
|
||||
class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Prefix
|
||||
|
||||
@@ -114,6 +114,13 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
]
|
||||
RIR.objects.bulk_create(rirs)
|
||||
|
||||
roles = [
|
||||
Role(name='Role 1', slug='role-1'),
|
||||
Role(name='Role 2', slug='role-2'),
|
||||
Role(name='Role 3', slug='role-3'),
|
||||
]
|
||||
Role.objects.bulk_create(roles)
|
||||
|
||||
tenants = [
|
||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||
@@ -124,12 +131,12 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
asns = (
|
||||
ASN(asn=65001, rir=rirs[0], tenant=tenants[0], description='foobar1'),
|
||||
ASN(asn=65002, rir=rirs[1], tenant=tenants[1], description='foobar2'),
|
||||
ASN(asn=65003, rir=rirs[2], tenant=tenants[2], description='foobar3'),
|
||||
ASN(asn=4200000000, rir=rirs[0], tenant=tenants[0]),
|
||||
ASN(asn=4200000001, rir=rirs[1], tenant=tenants[1]),
|
||||
ASN(asn=4200000002, rir=rirs[2], tenant=tenants[2]),
|
||||
ASN(asn=65001, rir=rirs[0], role=roles[0], tenant=tenants[0], description='foobar1'),
|
||||
ASN(asn=65002, rir=rirs[1], role=roles[1], tenant=tenants[1], description='foobar2'),
|
||||
ASN(asn=65003, rir=rirs[2], role=roles[2], tenant=tenants[2], description='foobar3'),
|
||||
ASN(asn=4200000000, rir=rirs[0], role=roles[0], tenant=tenants[0]),
|
||||
ASN(asn=4200000001, rir=rirs[1], role=roles[1], tenant=tenants[1]),
|
||||
ASN(asn=4200000002, rir=rirs[2], role=roles[2], tenant=tenants[2]),
|
||||
)
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
@@ -186,6 +193,13 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'rir': [rirs[0].slug, rirs[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_role(self):
|
||||
roles = Role.objects.all()[:2]
|
||||
params = {'role_id': [roles[0].pk, roles[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
params = {'role': [roles[0].slug, roles[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_site_group(self):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
|
||||
@@ -84,6 +84,12 @@ class ASNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
]
|
||||
RIR.objects.bulk_create(rirs)
|
||||
|
||||
roles = (
|
||||
Role(name='Role 1', slug='role-1'),
|
||||
Role(name='Role 2', slug='role-2'),
|
||||
)
|
||||
Role.objects.bulk_create(roles)
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2')
|
||||
@@ -97,10 +103,10 @@ class ASNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
asns = (
|
||||
ASN(asn=65001, rir=rirs[0], tenant=tenants[0]),
|
||||
ASN(asn=65002, rir=rirs[1], tenant=tenants[1]),
|
||||
ASN(asn=4200000001, rir=rirs[0], tenant=tenants[0]),
|
||||
ASN(asn=4200000002, rir=rirs[1], tenant=tenants[1]),
|
||||
ASN(asn=65001, rir=rirs[0], role=roles[0], tenant=tenants[0]),
|
||||
ASN(asn=65002, rir=rirs[1], role=roles[1], tenant=tenants[1]),
|
||||
ASN(asn=4200000001, rir=rirs[0], role=roles[0], tenant=tenants[0]),
|
||||
ASN(asn=4200000002, rir=rirs[1], role=roles[1], tenant=tenants[1]),
|
||||
)
|
||||
ASN.objects.bulk_create(asns)
|
||||
|
||||
@@ -114,6 +120,7 @@ class ASNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
cls.form_data = {
|
||||
'asn': 65000,
|
||||
'rir': rirs[0].pk,
|
||||
'role': roles[0].pk,
|
||||
'tenant': tenants[0].pk,
|
||||
'site': sites[0].pk,
|
||||
'description': 'A new ASN',
|
||||
@@ -121,11 +128,11 @@ class ASNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"asn,rir",
|
||||
"65003,RIR 1",
|
||||
"65004,RIR 2",
|
||||
"4200000003,RIR 1",
|
||||
"4200000004,RIR 2",
|
||||
"asn,rir,role",
|
||||
f"65003,RIR 1,{roles[0].name}",
|
||||
f"65004,RIR 2,{roles[1].name}",
|
||||
f"4200000003,RIR 1,{roles[0].name}",
|
||||
f"4200000004,RIR 2,{roles[1].name}",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
@@ -137,6 +144,7 @@ class ASNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'rir': rirs[1].pk,
|
||||
'role': roles[1].pk,
|
||||
'description': 'Next description',
|
||||
}
|
||||
|
||||
|
||||
@@ -496,7 +496,8 @@ class RoleListView(generic.ObjectListView):
|
||||
queryset = Role.objects.annotate(
|
||||
prefix_count=count_related(Prefix, 'role'),
|
||||
iprange_count=count_related(IPRange, 'role'),
|
||||
vlan_count=count_related(VLAN, 'role')
|
||||
vlan_count=count_related(VLAN, 'role'),
|
||||
asn_count=count_related(ASN, 'role')
|
||||
)
|
||||
filterset = filtersets.RoleFilterSet
|
||||
filterset_form = forms.RoleFilterForm
|
||||
|
||||
@@ -29,6 +29,16 @@
|
||||
<a href="{% url 'ipam:asn_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>
|
||||
{% if object.role %}
|
||||
<a href="{% url 'ipam:asn_list' %}?role={{ object.role.slug }}">{{ object.role }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
|
||||
@@ -38,7 +38,6 @@ FILTER_TREENODE_NEGATION_LOOKUP_MAP = dict(
|
||||
# HTTP Request META safe copy
|
||||
#
|
||||
|
||||
# Non-HTTP_ META keys to include when copying a request (whitelist)
|
||||
HTTP_REQUEST_META_SAFE_COPY = [
|
||||
'CONTENT_LENGTH',
|
||||
'CONTENT_TYPE',
|
||||
@@ -62,13 +61,6 @@ HTTP_REQUEST_META_SAFE_COPY = [
|
||||
'SERVER_PORT',
|
||||
]
|
||||
|
||||
# HTTP_ META keys known to carry sensitive data; excluded when copying a request (denylist)
|
||||
HTTP_REQUEST_META_SENSITIVE = {
|
||||
'HTTP_AUTHORIZATION',
|
||||
'HTTP_COOKIE',
|
||||
'HTTP_PROXY_AUTHORIZATION',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# CSV-style format delimiters
|
||||
|
||||
@@ -8,7 +8,7 @@ from netaddr import AddrFormatError, IPAddress
|
||||
|
||||
from netbox.registry import registry
|
||||
|
||||
from .constants import HTTP_REQUEST_META_SAFE_COPY, HTTP_REQUEST_META_SENSITIVE
|
||||
from .constants import HTTP_REQUEST_META_SAFE_COPY
|
||||
|
||||
__all__ = (
|
||||
'NetBoxFakeRequest',
|
||||
@@ -45,14 +45,11 @@ def copy_safe_request(request, include_files=True):
|
||||
request: The original request object
|
||||
include_files: Whether to include request.FILES.
|
||||
"""
|
||||
meta = {}
|
||||
for k, v in request.META.items():
|
||||
if not isinstance(v, str):
|
||||
continue
|
||||
if k in HTTP_REQUEST_META_SAFE_COPY:
|
||||
meta[k] = v
|
||||
elif k.startswith('HTTP_') and k not in HTTP_REQUEST_META_SENSITIVE:
|
||||
meta[k] = v
|
||||
meta = {
|
||||
k: request.META[k]
|
||||
for k in HTTP_REQUEST_META_SAFE_COPY
|
||||
if k in request.META and isinstance(request.META[k], str)
|
||||
}
|
||||
data = {
|
||||
'META': meta,
|
||||
'COOKIES': request.COOKIES,
|
||||
|
||||
@@ -1,42 +1,7 @@
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.test import RequestFactory, TestCase
|
||||
from netaddr import IPAddress
|
||||
|
||||
from utilities.request import copy_safe_request, get_client_ip
|
||||
|
||||
|
||||
class CopySafeRequestTests(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def _make_request(self, **kwargs):
|
||||
request = self.factory.get('/', **kwargs)
|
||||
request.user = AnonymousUser()
|
||||
return request
|
||||
|
||||
def test_standard_meta_keys_copied(self):
|
||||
request = self._make_request(HTTP_USER_AGENT='TestAgent/1.0')
|
||||
fake = copy_safe_request(request)
|
||||
self.assertEqual(fake.META.get('HTTP_USER_AGENT'), 'TestAgent/1.0')
|
||||
|
||||
def test_arbitrary_http_headers_copied(self):
|
||||
"""Arbitrary HTTP_ headers (e.g. X-NetBox-*) should be included."""
|
||||
request = self._make_request(HTTP_X_NETBOX_BRANCH='my-branch')
|
||||
fake = copy_safe_request(request)
|
||||
self.assertEqual(fake.META.get('HTTP_X_NETBOX_BRANCH'), 'my-branch')
|
||||
|
||||
def test_sensitive_headers_excluded(self):
|
||||
"""Authorization and Cookie headers must not be copied."""
|
||||
request = self._make_request(HTTP_AUTHORIZATION='Bearer secret')
|
||||
fake = copy_safe_request(request)
|
||||
self.assertNotIn('HTTP_AUTHORIZATION', fake.META)
|
||||
|
||||
def test_non_string_meta_values_excluded(self):
|
||||
"""Non-string META values must not be copied."""
|
||||
request = self._make_request()
|
||||
request.META['HTTP_X_CUSTOM_INT'] = 42
|
||||
fake = copy_safe_request(request)
|
||||
self.assertNotIn('HTTP_X_CUSTOM_INT', fake.META)
|
||||
from utilities.request import get_client_ip
|
||||
|
||||
|
||||
class GetClientIPTests(TestCase):
|
||||
|
||||
Reference in New Issue
Block a user