Skip to content

Data model

The whole shape on one page. Boxes are models, arrows are FKs.

Organization                                 [Tag] (global today; per-tenant Phase 5)
Tenant ──── hard isolation boundary ──────┐
  ├─ sites    : Site[]                    │
  ├─ vrfs     : VRF[]                     │
  ├─ vlans    : VLAN[]                    │
  ├─ device_types : DeviceType[]          │  every domain model
  ├─ devices  : Device[]                  │  carries  tenant FK
  ├─ prefixes : Prefix[]                  │
  ├─ ip_addresses : IPAddress[]           │
  └─ cables   : Cable[]                   │
VRF (tenant-scoped)                        │
  └─ prefixes : Prefix[]                   │
  └─ ip_addresses : IPAddress[]            │
  └─ sites    : Site[]   (M2M, docs only)  │
Site (tenant-scoped, location)             │
  ├─ name                                  │
  ├─ gateway_policy : first | last | none  │
  └─ vrfs   : VRF[]   (M2M, docs only)     │
Prefix (tenant + vrf scoped)               │
  ├─ cidr                                  │
  ├─ status : container | active | reserved | deprecated
  ├─ site → Site                           │
  ├─ vlan → VLAN                           │
  ├─ vrf  → VRF | NULL (Global)            │
  ├─ gateway : IP string                   │
  ├─ custom_fields : JSONB                 │
  ├─ tags : Tag[]   (via TaggedItem)       │
  └─ ip_addresses : IPAddress[]            │
IPAddress (tenant + vrf scoped)            │
  ├─ ip_address                            │
  ├─ status : available | assigned | reserved | dhcp_pool | floating
  ├─ role   : '' | gateway | loopback | vip | hsrp | vrrp | anycast | secondary
  ├─ prefix → Prefix                       │
  └─ vrf    → VRF | NULL                   ┘

Mixins, by which every domain model gets ...

class TimestampedModel(Model):
    created_at = DateTimeField(auto_now_add=True)
    updated_at = DateTimeField(auto_now=True)
    class Meta: abstract = True

class CustomFieldsMixin(Model):
    custom_fields = JSONField(default=dict, blank=True)
    class Meta: abstract = True

class TaggableMixin(Model):
    tags = TaggableManager(blank=True, through=TaggedItem)
    class Meta: abstract = True

Prefix, IPAddress, Site, DeviceType, Device, VLAN, Cable all multi-inherit these three.

Custom Tag with color

core.Tag subclasses taggit's TagBase to add color (hex string). The TaggedItem through-model uses GenericUUIDTaggedItemBase because all our content models have UUID PKs (the default IntegerField object_id overflows on UUID values).

Uniqueness constraints

Model Unique on
Tenant (org, slug) and (org, name)
Site (tenant, name)
VRF (tenant, name)
VLAN (tenant, vlan_id)
Prefix (tenant, vrf, cidr) with nulls_distinct=False ← critical
IPAddress (tenant, vrf, ip_address) with nulls_distinct=False
DeviceType (tenant, name)
Device (tenant, name)

Conventional VRF = NULL

We don't seed a "Global" VRF row. vrf=NULL is the Global VRF — that's why nulls_distinct=False is load-bearing. See Tenant + VRF for the full reasoning.