Skip to content

22447 Add Cooling infrastructure modeling - #22517

Merged
jeremystretch merged 80 commits into
featurefrom
22447-cooling
Aug 4, 2026
Merged

22447 Add Cooling infrastructure modeling#22517
jeremystretch merged 80 commits into
featurefrom
22447-cooling

Conversation

@arthanson

@arthanson arthanson commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Closes: #22447

Adds cooling infrastructure modeling to DCIM, mirroring the shape of the power-distribution stack and reusing the standard NetBox model features (contacts and image attachments on the facility plant, plus change logging, custom fields, and tags). This lets operators model liquid- and hybrid-cooled environments (CDUs, manifolds, RDHx, facility plant) as a source of truth.

NetBox models design intent and physical topology, not live conditions — flow and capacity are design/rated specifications, not telemetry.

The topology parallels power, with two deliberate differences:

  1. Coolant connections are direct foreign keys, not cables. Hoses aren't structured cabling, so there's no Cable/CablePath machinery; tracing a loop is an FK walk.
  2. A feed is the whole loop. One CoolingFeed represents both the supply (cold) and return (warm) paths of a single loop, rather than modeling each direction as a separate feed.
POWER:    PowerPanel    → PowerFeed    →[cable]→ PowerPort      ↔ PowerOutlet
COOLING:  CoolingSource → CoolingFeed  (serves a rack)          CoolingIntake ↔(FK) CoolingOutflow

End-to-end picture

 CoolingSource   (chiller / cooling tower / dry cooler / CRAC / CRAH)
     │
     ▼
  CoolingFeed     (entire loop — supply + return; serves a rack)
     │
     ▼
  Rack            (devices derive their serving feed from the rack)
     │
     ├─ Device  (e.g. CDU)
     │     CoolingIntake    ◀── chilled water in (facility side)
     │       │  through the device
     │       ▼
     │     CoolingOutflow ──┐
     │                      │  coolant
     ├─ Device  (e.g. RDHx / row manifold)
     │     CoolingIntake ◀──┘
     │       │  through the device
     │       ▼
     │     CoolingOutflow ──┐
     │                      │  coolant
     └─ Device  (e.g. server)
           CoolingIntake ◀──┘   (cold-plate inlet)

Coolant connectivity is a chain of direct FKs stored on the downstream (consuming) side:

Field Points to Meaning
CoolingIntake.cooling_outflow one upstream CoolingOutflow the outflow feeding this intake
CoolingOutflow.cooling_intake parent CoolingIntake (same device) coolant enters via the intake, leaves via the outflow

A device's serving CoolingFeed is derived from its rack, not stored on each intake.

In-rack equipment (CDUs, manifolds, RDHx) is modeled generically as (typically zero-U) Devices carrying these components — the same way a PDU is a device with power ports/outlets. No bespoke CDU/manifold/RDHx models were added.

New models

  • CoolingSource — facility plant (chiller / cooling tower / dry cooler / CRAC / CRAH), scoped to a site/location; carries the loop fluid_type and a rated cooling capacity. Reuses contacts and image attachments.
  • CoolingFeed — a single coolant loop (supply + return) delivered from a source to a rack; rated cooling capacity and rated (design) flow rate.
  • CoolingIntake — a device coolant intake (CDU facility intake, server cold-plate inlet); connector type, diameter, rated maximum flow. Fed by one upstream CoolingOutflow.
  • CoolingOutflow — a device coolant supply point (CDU / manifold outlet); connector type, diameter, optionally linked to a parent CoolingIntake on the same device.
  • CoolingIntakeTemplate / CoolingOutflowTemplate — component templates, auto-instantiated when a device or module is created.

Changes to existing models

  • Device / DeviceType / ModuleType: new cooling_method (air / liquid / hybrid / immersion). A Device inherits the value from its DeviceType on creation and may override it (like airflow); DeviceType and ModuleType carry it as a product-level attribute (ModuleType mirrors airflow — type-level only, no per-instance override). On the edit and filter forms, cooling_method and airflow are grouped under a dedicated Cooling section. Each also gains cooling component counter caches (cooling_intake_count / cooling_outflow_count, or the _template_count variants on the types).
  • Rack / RackType: cooling_capability (air-only / hybrid / liquid-only) and cooling_capacity (kW) defined on the shared RackBase — set on the RackType and propagated to racks (like max_weight).
  • No Cable changes — coolant connectivity is FK-based (CoolingIntake.cooling_outflow, CoolingOutflow.cooling_intake); no new cable type or cable-termination registration.

Field conventions on the new cooling models

Several quantities are modeled as value + unit with a normalized field for correct cross-unit sorting (the same pattern as weight). All are design/rated specifications, not live readings:

  • Connector type — industry-standard set: UQD, UQDB, QDC, camlock, NPT, BSP, proprietary (no single-vendor names).
  • Diameter (intakes/outflows) — mm / cm / in, normalized to mm (DiameterMixin).
  • Flow rateCoolingFeed.rated_flow_rate and CoolingIntake.maximum_flow — L/min / m³/h / GPM, normalized to L/min (MaximumFlowMixin; the feed inlines the same value/unit/normalized pattern).
  • Fluid type — recorded once on the CoolingSource (water / water-glycol / dielectric / refrigerant / other); the whole loop shares it.
  • Capacity (cooling_capacity) stays single-unit kW — the universal standard for IT cooling load, so no unit selector.

No temperature fields: supply/return temperatures are operating conditions that go stale immediately, so they belong in a monitoring stack rather than the source of truth.

No pressure field: "operating pressure" has no meaning as a design value independent of live conditions, so it's omitted rather than inviting a stored telemetry reading. If a rated pressure spec is needed later, it can be added deliberately.

Design rationale

  • No live metrics. A source of truth shouldn't store point-in-time telemetry (temperature, pressure, instantaneous flow) — it goes stale immediately and belongs in a monitoring stack (Prometheus/BMS). Retained values are stable specs: rated flow, rated capacity, fluid type, connector geometry.
  • Coolant isn't cabling. Hoses lack structured-cabling semantics (no pass-through/CablePath), so connections are direct FKs; individual hoses aren't inventoried.
  • One feed per loop. A loop's supply and return are physically inseparable, so a single CoolingFeed represents the whole loop rather than splitting it into a supply feed and a return feed.
  • Feed derived, not denormalized. A CoolingIntake doesn't store its serving CoolingFeed; the feed is resolved from the device's rack. This avoids a denormalized FK that could drift out of sync when a device is moved.
  • Simple FKs, no through model. CoolingIntake → CoolingOutflow is many-to-one, so there's no M2M requiring a through; the intake row is already the per-connection record. A dedicated connection model would only be warranted for multi-source intakes (redundancy is modeled as multiple intakes, like dual-PSU) or physical-hose inventory.
  • In-rack equipment is a device. RDHx, CDUs, and manifolds are real appliances with a make/model, so they're modeled as ordinary (typically zero-U) Devices rather than rack booleans — mirroring how NetBox has no has_pdu/has_ups flag. Rack-level liquid readiness is still expressed via Rack.cooling_capability.

Deferred (non-schema) decisions

  • Vertical/rail-mounted manifolds and RDHx — model as zero-U (u_height=0) devices, reusing the vertical-PDU pattern. A generic mounting-type field, if wanted, is a DCIM-wide enhancement, not cooling-specific.
  • Cooling-path visualization — a graphical trace widget over the FK chain is a separable UI enhancement; the MVP is an FK-walk trace.

Data model (fields & relationships)

New models list all declared fields; existing models list only the fields added by this feature. FK → marks a foreign key; _abs_* are internal columns holding the normalized value (L/min, mm) for consistent sorting/filtering.

New models

CoolingSource — facility plant
  name
  site                     FK → Site
  location                 FK → Location          (optional)
  type                     chiller | cooling-tower | dry-cooler | crac | crah
  status                   offline | active | planned | failed
  fluid_type               water | water-glycol | dielectric | refrigerant | other
  cooling_capacity         rated kW
  (also: contacts, image attachments, description, comments, tags, custom fields)

CoolingFeed — coolant loop (source → rack); one feed = supply + return
  cooling_source           FK → CoolingSource
  rack                     FK → Rack              (optional)
  tenant                   FK → Tenant            (optional)
  name
  status                   offline | active | planned | failed
  cooling_capacity         rated kW
  rated_flow_rate          design  + rated_flow_rate_unit / _abs_rated_flow_rate   (L/min)

CoolingIntake — device coolant intake
  device                   FK → Device
  module                   FK → Module            (optional)
  name / label / description
  type                     connector: uqd | uqdb | qdc | camlock | npt | bsp | proprietary
  diameter                 + diameter_unit / _abs_diameter        (mm)
  maximum_flow             rated  + maximum_flow_unit / _abs_maximum_flow   (L/min)
  cooling_outflow          FK → CoolingOutflow    (optional, upstream — the feeding outflow)

CoolingOutflow — device coolant supply
  device                   FK → Device
  module                   FK → Module            (optional)
  name / label / description
  type                     connector type
  diameter                 + diameter_unit / _abs_diameter        (mm)
  cooling_intake           FK → CoolingIntake     (optional, parent; same device)

CoolingIntakeTemplate — instantiated as a CoolingIntake on device/module creation
  device_type              FK → DeviceType        (optional)
  module_type              FK → ModuleType        (optional)
  name / label / description
  type (connector), diameter (+unit, mm), maximum_flow (rated; +unit, L/min)

CoolingOutflowTemplate — instantiated as a CoolingOutflow on device/module creation
  device_type              FK → DeviceType        (optional)
  module_type              FK → ModuleType        (optional)
  name / label / description
  type (connector), diameter (+unit, mm)
  cooling_intake           FK → CoolingIntakeTemplate  (optional, parent)

Existing models — added fields only

Device       cooling_method (air|liquid|hybrid|immersion; inherited from DeviceType)
             cooling_intake_count, cooling_outflow_count            (counter caches)
DeviceType   cooling_method
             cooling_intake_template_count, cooling_outflow_template_count
ModuleType   cooling_method
             cooling_intake_template_count, cooling_outflow_template_count
Rack         cooling_capability (air-only|hybrid|liquid-only)
              cooling_capacity (kW)
RackType     cooling_capability, cooling_capacity (kW)

Relationships (all foreign keys — no new many-to-many)

Site           1 ──▶ N  CoolingSource
Location       1 ──▶ N  CoolingSource                                        (optional)
CoolingSource  1 ──▶ N  CoolingFeed
Rack           1 ──▶ N  CoolingFeed                                          (optional)
Tenant         1 ──▶ N  CoolingFeed                                          (optional)
CoolingOutflow 1 ──▶ N  CoolingIntake   via CoolingIntake.cooling_outflow    (an intake is fed by one outflow)
CoolingIntake  1 ──▶ N  CoolingOutflow  via CoolingOutflow.cooling_intake    (parent intake; same device)
Device         1 ──▶ N  CoolingIntake, CoolingOutflow
Module         1 ──▶ N  CoolingIntake, CoolingOutflow                        (optional)
DeviceType /
ModuleType     1 ──▶ N  CoolingIntakeTemplate, CoolingOutflowTemplate
CoolingIntakeTemplate  1 ──▶ N  CoolingOutflowTemplate                       (parent)

A device's serving CoolingFeed is not a stored FK; it is derived from the device's rack (Rack.cooling_feeds).

Scope

Full surface area for every new model: choice sets, filtersets, forms (model/bulk-edit/bulk-import/filter/create), tables, REST API (serializers, viewsets, routes), GraphQL (types, filters, queries), search indexes, navigation (new Cooling menu), UI panels/templates, and database migrations.

@netbox-community netbox-community deleted a comment from github-actions Bot Jun 25, 2026
@arthanson

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@arthanson

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@arthanson

This comment was marked as outdated.

@arthanson

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@arthanson

This comment was marked as outdated.

@arthanson
arthanson marked this pull request as ready for review June 25, 2026 23:24
@arthanson
arthanson requested review from a team and jeremystretch and removed request for a team June 25, 2026 23:24
jeremystretch

This comment was marked as outdated.

@adamboutcher

Copy link
Copy Markdown
Contributor

Historical related issue #6987 useful for searching etc.

Comment thread netbox/dcim/models/device_components.py Outdated
Comment thread netbox/dcim/models/cooling.py Outdated
Comment thread netbox/dcim/models/device_components.py Outdated
Comment thread netbox/dcim/models/mixins.py Outdated
Comment thread netbox/dcim/models/mixins.py Outdated
Comment thread netbox/dcim/models/racks.py Outdated
Comment thread netbox/dcim/models/device_components.py Outdated
Comment thread netbox/dcim/models/device_components.py Outdated
Comment thread netbox/dcim/models/device_component_templates.py Outdated
Comment thread netbox/dcim/models/cooling.py Outdated
@mrmrcoleman

Copy link
Copy Markdown
Collaborator

Overall a solid keeping the power-model shape. Most deviations from FR #22447 are reasonable simplifications (several requested in review). Notes below, kept short.

Decisions

  • Design supply/return temperatures — defer. Not telemetry (they're design setpoints, like rated capacity), but a clean two-way door: adding nullable °C fields later is a purely additive migration, no backfill. Fine to leave out of v1 and add as a fast-follow.
  • RDHx — defer, with notes below. Keep the current "model it as a zero-U device" direction; don't add a Rack.has_rdhx boolean now (it'd create a second source of truth that drifts from the device).

RDHx notes (for whoever picks it up)

  • An RDHx is 1:1 with a rack — a water-cooled coil that replaces the rear door of one cabinet. It isn't shared or free-standing, and it doesn't occupy a U-slot, so a zero-U device is a slightly forced fit (no rear-door mount concept exists).
  • "Which racks have an RDHx?" is not free with the device approach. Verified: RackFilterSet has no devices__ traversal filter, and there's no built-in RDHx device role/type. Today you can only filter the Device list by role — that returns devices, not a rack list, and can't do the inverse ("racks missing one").
  • If we later add RDHx-as-device, plan to also add a small RackFilterSet filter traversing devices__role (needs distinct=True). Otherwise the FR's "RDHx tracking" reporting need is quietly dropped.
  • Also unaddressed (defer, but flag): partial-U-coverage RDHx and active-vs-passive (raised by @adamboutcher on the FR).

Endorsed simplifications (no change requested)

  • FK instead of cooling-hose cables — reasonable; handles 1-to-n splitters and dual-feed redundancy (multiple intakes), reviewer-endorsed.
  • One feed = whole loop (supply + return combined) — fine simplification; return is modeled implicitly.

Documentation suggestions

  • Add a short mapping from the new names/relationships back to power so existing users' mental model transfers: CoolingIntakePowerPort, CoolingOutflowPowerOutlet, FK chain ≈ cable.
  • Explicitly document the deliberate v1 limitations so they read as conscious cuts, not gaps: return path implicit, path tracing / impact-analysis deferred, no per-hose inventory, no design pressure/temperature yet.

Deferred but acceptable (noted, not blocking)

  • Design/rated pressure on the feed — likely fast-follow depending on feedback.
  • Independent supply/return modeling (asymmetric routing, separate return failure).
  • Path tracing + impact analysis ("if this loop fails, what loses cooling?") — an FR headline use case, deferred pending model approval; make sure it lands soon after.

@arthanson
arthanson requested a review from jeremystretch July 21, 2026 17:28
@arthanson

Copy link
Copy Markdown
Collaborator Author

@claude /review Only report on issues of potential concern; we don't need commentary on anything that doesn't require further consideration. Also see comments on previous review of Declined requested changes and reasoning.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @arthanson's task in 6m 25s —— View job


Code Review — 22447 Add Cooling Infrastructure Modeling

  • Read key changed files
  • Check all issues from previous review (2026-07-27)
  • Post review findings

All items from the 2026-07-27 review appear to have been addressed. Two remaining concerns follow.


1. CoolingConnectorTypeChoices missing key field

CoolingConnectorTypeChoices (dcim/choices.py:2218) has no key, so admins cannot extend it via FIELD_CHOICES. This was the concern raised in the previous review (the comment at line 2139 at the time pointed to the connector-type class). The parallel status/type classes that were flagged in the same review round (CoolingSourceTypeChoices, FluidTypeChoices) now have keys; the connector type class does not.

RackCoolingCapabilityChoices (line 2205) is in the same state — no key.

Fix this →


2. Module.cooling_method not validated against parent device

From the 2026-07-16 review round: ModuleType.cooling_method was added without validation against the device it is installed into. A module whose ModuleType.cooling_method is immersion can currently be installed into a device whose cooling_method is air with no complaint.

Module.clean() (models/modules.py:433) validates bay type compatibility and move legality but has no cooling-method check. Whether to add this validation was phrased as "we might want to" — flagging it here so it can be explicitly deferred or accepted.


@arthanson

Copy link
Copy Markdown
Collaborator Author

Per Claudes review:

  1. fixed
  2. There is no way to know like hybrid can have air or water, also they are extensible choices so no good way to restrict this.

@arthanson
arthanson requested a review from jeremystretch July 28, 2026 19:31
# Conflicts:
#	netbox/dcim/tests/test_filtersets.py
Comment thread netbox/dcim/models/device_components.py Outdated
Comment thread netbox/dcim/models/mixins.py Outdated
Comment thread netbox/dcim/models/mixins.py Outdated
Comment thread netbox/dcim/views.py Outdated
Comment thread netbox/dcim/graphql/filters.py Outdated
Comment thread netbox/dcim/forms/filtersets.py Outdated
@jeremystretch
jeremystretch self-requested a review August 4, 2026 12:40
@jeremystretch
jeremystretch merged commit da1db00 into feature Aug 4, 2026
19 checks passed
@jeremystretch
jeremystretch deleted the 22447-cooling branch August 4, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants