Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8fd88b3
Introduce the Owner model
jeremystretch Oct 16, 2025
27ddccb
Add owner fields to applicable models
jeremystretch Oct 17, 2025
a2f8ddc
Introduce PrimaryModelSerializer & OrganizationalModelSerializer; add…
jeremystretch Oct 20, 2025
789139b
Add 'owner' field to bulk operation forms
jeremystretch Oct 20, 2025
800cf5c
Add owner filters
jeremystretch Oct 21, 2025
a4d52b4
NestedGroupModel should inherit from NetBoxModel
jeremystretch Oct 21, 2025
7d0f68c
Update GraphQL types to support owner assignment
jeremystretch Oct 21, 2025
6f1a845
Add missing filters
jeremystretch Oct 21, 2025
6746913
ComponentType should inherit from PrimaryObjectType
jeremystretch Oct 21, 2025
3a212cc
Misc fixes
jeremystretch Oct 21, 2025
1fdfff6
Split base form classes into separate modules under netbox.forms
jeremystretch Oct 21, 2025
e2163b9
Add owner field to all applicable model forms
jeremystretch Oct 21, 2025
ab092f2
Add owner field to all applicable bulk edit forms
jeremystretch Oct 21, 2025
a848d3b
Add owner field to all applicable bulk import forms
jeremystretch Oct 21, 2025
cd485a5
Add owner field to all applicable filterset forms
jeremystretch Oct 21, 2025
4dda968
Update forms for device & VM components
jeremystretch Oct 21, 2025
c3144dd
Fix base form classes
jeremystretch Oct 22, 2025
dd6c985
Rename bulk import form base classes
jeremystretch Oct 22, 2025
165c3f5
Fix device/VM component type definitions
jeremystretch Oct 22, 2025
912d2af
Correct filterset definitions
jeremystretch Oct 22, 2025
dbce384
Add base class tests for forms, filtersets, serializers, and GraphQL …
jeremystretch Oct 22, 2025
2f23bdc
Correct device/VM component filterset definitions
jeremystretch Oct 22, 2025
2477579
Correct device/VM component GraphQL type definitions
jeremystretch Oct 22, 2025
a4b8862
Add owner to base object template
jeremystretch Oct 22, 2025
77117a2
Show related objects under owner view
jeremystretch Oct 22, 2025
57daa9f
Fix owner filter form field
jeremystretch Oct 22, 2025
59082d0
Introduce base table classes with an 'owner' column for primary, orga…
jeremystretch Oct 22, 2025
9cbda4d
Fix owner filter
jeremystretch Oct 22, 2025
d154bac
Move owners under admin in nav menu
jeremystretch Oct 22, 2025
b9cc93a
Misc cleanup
jeremystretch Oct 22, 2025
3ca2a18
Introduce AdminModel base class to provide enhanced UI functionality …
jeremystretch Oct 22, 2025
1a6ea31
Introduce OwnerGroup model
jeremystretch Oct 22, 2025
3a7b4ac
Add documentation for owners & owner groups
jeremystretch Oct 23, 2025
c858d2a
Add tests for Owner & OwnerGroup
jeremystretch Oct 23, 2025
ef22251
Add "add owner" button to owner group detail view
jeremystretch Oct 23, 2025
f5da362
Add owners count to OwnerGroup list
jeremystretch Oct 23, 2025
c42e382
Make user_groups and users DynamicModelMultipleChoiceFields on OwnerForm
jeremystretch Oct 23, 2025
b9576ed
Misc cleanup
jeremystretch Oct 23, 2025
fc9a863
Add owner field to VirtualChassisCreateForm
jeremystretch Oct 24, 2025
ba03705
Merge branch 'feature' into 20304-object-owners
jeremystretch Oct 24, 2025
ee192e2
Reindex migrations
jeremystretch Oct 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12,471 changes: 11,697 additions & 774 deletions contrib/openapi.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions docs/features/resource-ownership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Resource Ownership

!!! info "This feature was introduced in NetBox v4.5."

Most objects in NetBox can be assigned an owner. An owner is a set of users and/or groups who are responsible for the administration of associated objects. For example, you might designate the operations team at a site as the owner for all prefixes and VLANs deployed at that site. The users and groups assigned to an owner are referred to as its members.

!!! note
Ownership of an object should not be confused with the concept of [tenancy](./tenancy.md), which indicates the dedication of an object to a specific tenant. For instance, a tenant might represent a customer served by the object, whereas an owner typically represents a set of internal users responsible for the management of the object.

Owners can be organized into groups for easier management.
30 changes: 23 additions & 7 deletions docs/features/tenancy.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Tenancy

Most core objects within NetBox's data model support _tenancy_. This is the association of an object with a particular tenant to convey ownership or dependency. For example, an enterprise might represent its internal business units as tenants, whereas a managed services provider might create a tenant in NetBox to represent each of its customers.
Most core objects within NetBox's data model support _tenancy_. This is the association of an object with a particular tenant to convey assignment or dependency. For example, an enterprise might represent its internal business units as tenants, whereas a managed services provider might create a tenant in NetBox to represent each of its customers.

```mermaid
flowchart TD
Expand All @@ -19,20 +19,36 @@ Tenants can be grouped by any logic that your use case demands, and groups can b

Typically, the tenant model is used to represent a customer or internal organization, however it can be used for whatever purpose meets your needs.

Most core objects within NetBox can be assigned to particular tenant, so this model provides a very convenient way to correlate ownership across object types. For example, each of your customers might have its own racks, devices, IP addresses, circuits and so on: These can all be easily tracked via tenant assignment.
Most core objects within NetBox can be assigned to a particular tenant, so this model provides a very convenient way to correlate resource allocation across object types. For example, each of your customers might have its own racks, devices, IP addresses, circuits and so on: These can all be easily tracked via tenant assignment.

The following objects can be assigned to tenants:

* Sites
* Circuits
* Circuit groups
* Virtual circuits
* Cables
* Devices
* Virtual device contexts
* Power feeds
* Racks
* Rack reservations
* Devices
* VRFs
* Sites
* Locations
* ASNs
* ASN ranges
* Aggregates
* Prefixes
* IP ranges
* IP addresses
* VLANs
* Circuits
* VLAN groups
* VRFs
* Route targets
* Clusters
* Virtual machines
* L2VPNs
* Tunnels
* Wireless LANs
* Wireless links

Tenant assignment is used to signify the ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so tenant assignment would not be appropriate.
Tenancy represents the dedication of an object to a specific tenant. As such, each object may only be assigned to a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so the assignment of a tenant would not be appropriate.
23 changes: 23 additions & 0 deletions docs/models/users/owner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Owner

An owner is a set of users and/or groups who are responsible for the administration of certain resources within NetBox. The users and groups assigned to an owner are referred to as its members. Owner assignments are useful for indicating which parties are responsible for the administration of a particular object.

Most objects within NetBox can be assigned an owner, although this is not required.

## Fields

### Name

The owner's name.

### Group

The [group](./ownergroup.md) to which the owner is assigned. The assignment of an owner to a group is optional.

### User Groups

Groups of users that are members of the owner.

### Users

Individual users that are members of the owner.
9 changes: 9 additions & 0 deletions docs/models/users/ownergroup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Owner Groups

Groups are used to correlate and organize [owners](./owner.md). The assignment of an owner to a group has no bearing on the relationship of owned objects to their owners.

## Fields

### Name

The name of the group.
4 changes: 4 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ nav:
- Wireless: 'features/wireless.md'
- Virtualization: 'features/virtualization.md'
- VPN Tunnels: 'features/vpn-tunnels.md'
- Resource Ownership: 'features/resource-ownership.md'
- Tenancy: 'features/tenancy.md'
- Contacts: 'features/contacts.md'
- Search: 'features/search.md'
Expand Down Expand Up @@ -273,6 +274,9 @@ nav:
- ContactRole: 'models/tenancy/contactrole.md'
- Tenant: 'models/tenancy/tenant.md'
- TenantGroup: 'models/tenancy/tenantgroup.md'
- Users:
- Owner: 'models/users/owner.md'
- OwnerGroup: 'models/users/ownergroup.md'
- Virtualization:
- Cluster: 'models/virtualization/cluster.md'
- ClusterGroup: 'models/virtualization/clustergroup.md'
Expand Down
30 changes: 16 additions & 14 deletions netbox/circuits/api/serializers_/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from dcim.api.serializers_.device_components import InterfaceSerializer
from dcim.api.serializers_.cables import CabledObjectSerializer
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from netbox.api.serializers import (
NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer,
)
from netbox.choices import DistanceUnitChoices
from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model
Expand All @@ -29,16 +31,16 @@
)


class CircuitTypeSerializer(NetBoxModelSerializer):
class CircuitTypeSerializer(OrganizationalModelSerializer):

# Related object counts
circuit_count = RelatedObjectCountField('circuits')

class Meta:
model = CircuitType
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'circuit_count',
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
'custom_fields', 'created', 'last_updated', 'circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')

Expand Down Expand Up @@ -71,15 +73,15 @@ def get_termination(self, obj):
return serializer(obj.termination, nested=True, context=context).data


class CircuitGroupSerializer(NetBoxModelSerializer):
class CircuitGroupSerializer(OrganizationalModelSerializer):
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
circuit_count = RelatedObjectCountField('assignments')

class Meta:
model = CircuitGroup
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant',
'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count'
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant', 'owner', 'tags',
'custom_fields', 'created', 'last_updated', 'circuit_count'
]
brief_fields = ('id', 'url', 'display', 'name')

Expand All @@ -99,7 +101,7 @@ class Meta:
brief_fields = ('id', 'url', 'display', 'group', 'priority')


class CircuitSerializer(NetBoxModelSerializer):
class CircuitSerializer(PrimaryModelSerializer):
provider = ProviderSerializer(nested=True)
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
status = ChoiceField(choices=CircuitStatusChoices, required=False)
Expand All @@ -115,7 +117,7 @@ class Meta:
fields = [
'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
'install_date', 'termination_date', 'commit_rate', 'description', 'distance', 'distance_unit',
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'termination_a', 'termination_z', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'assignments',
]
brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
Expand Down Expand Up @@ -176,21 +178,21 @@ def get_member(self, obj):
return serializer(obj.member, nested=True, context=context).data


class VirtualCircuitTypeSerializer(NetBoxModelSerializer):
class VirtualCircuitTypeSerializer(OrganizationalModelSerializer):

# Related object counts
virtual_circuit_count = RelatedObjectCountField('virtual_circuits')

class Meta:
model = VirtualCircuitType
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'virtual_circuit_count',
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
'custom_fields', 'created', 'last_updated', 'virtual_circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')


class VirtualCircuitSerializer(NetBoxModelSerializer):
class VirtualCircuitSerializer(PrimaryModelSerializer):
provider_network = ProviderNetworkSerializer(nested=True)
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None)
type = VirtualCircuitTypeSerializer(nested=True)
Expand All @@ -201,7 +203,7 @@ class Meta:
model = VirtualCircuit
fields = [
'id', 'url', 'display_url', 'display', 'cid', 'provider_network', 'provider_account', 'type', 'status',
'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'provider_network', 'cid', 'description')

Expand Down
18 changes: 9 additions & 9 deletions netbox/circuits/api/serializers_/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ipam.api.serializers_.asns import ASNSerializer
from ipam.models import ASN
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from netbox.api.serializers import PrimaryModelSerializer
from .nested import NestedProviderAccountSerializer

__all__ = (
Expand All @@ -14,7 +14,7 @@
)


class ProviderSerializer(NetBoxModelSerializer):
class ProviderSerializer(PrimaryModelSerializer):
accounts = SerializedPKRelatedField(
queryset=ProviderAccount.objects.all(),
serializer=NestedProviderAccountSerializer,
Expand All @@ -35,32 +35,32 @@ class ProviderSerializer(NetBoxModelSerializer):
class Meta:
model = Provider
fields = [
'id', 'url', 'display_url', 'display', 'name', 'slug', 'accounts', 'description', 'comments',
'id', 'url', 'display_url', 'display', 'name', 'slug', 'accounts', 'description', 'owner', 'comments',
'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')


class ProviderAccountSerializer(NetBoxModelSerializer):
class ProviderAccountSerializer(PrimaryModelSerializer):
provider = ProviderSerializer(nested=True)
name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='')

class Meta:
model = ProviderAccount
fields = [
'id', 'url', 'display_url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
'id', 'url', 'display_url', 'display', 'provider', 'name', 'account', 'description', 'owner', 'comments',
'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'account', 'description')


class ProviderNetworkSerializer(NetBoxModelSerializer):
class ProviderNetworkSerializer(PrimaryModelSerializer):
provider = ProviderSerializer(nested=True)

class Meta:
model = ProviderNetwork
fields = [
'id', 'url', 'display_url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
'id', 'url', 'display_url', 'display', 'provider', 'name', 'service_id', 'description', 'owner', 'comments',
'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
12 changes: 6 additions & 6 deletions netbox/circuits/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from dcim.filtersets import CabledObjectFilterSet
from dcim.models import Interface, Location, Region, Site, SiteGroup
from ipam.models import ASN
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
Expand All @@ -29,7 +29,7 @@
)


class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='circuits__terminations___region',
Expand Down Expand Up @@ -95,7 +95,7 @@ def search(self, queryset, name, value):
)


class ProviderAccountFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
Expand All @@ -122,7 +122,7 @@ def search(self, queryset, name, value):
).distinct()


class ProviderNetworkFilterSet(NetBoxModelFilterSet):
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
Expand Down Expand Up @@ -156,7 +156,7 @@ class Meta:
fields = ('id', 'name', 'slug', 'color', 'description')


class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
label=_('Provider (ID)'),
Expand Down Expand Up @@ -475,7 +475,7 @@ class Meta:
fields = ('id', 'name', 'slug', 'color', 'description')


class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='provider_network__provider',
queryset=Provider.objects.all(),
Expand Down
Loading