Closes #22447: Add Cooling infrastructure modeling (#22517)

This commit is contained in:
Arthur Hanson 2026-08-04 07:23:50 -07:00 committed by GitHub
parent a2c32f137b
commit da1db0055d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 7910 additions and 135 deletions

41
docs/features/cooling.md Normal file
View File

@ -0,0 +1,41 @@
# Cooling
As part of its DCIM feature set, NetBox supports modeling data center cooling infrastructure, from facility plant down to the coolant connections on individual devices. This is used to document liquid- and hybrid-cooled environments (chillers, cooling distribution units, manifolds, rear-door heat exchangers, and cold-plate servers) as a source of truth.
## Model Overview
Cooling infrastructure is modeled as a hierarchy running from facility plant down to individual devices:
**cooling source → cooling feed → device cooling intake / cooling outflow**
A few properties of the model are worth noting up front:
- **Connections are direct references, not cables.** Coolant hoses are not modeled as structured cabling; instead, an intake references the outflow that supplies it directly. Tracing a loop is a walk along these references.
- **A single feed represents the entire loop.** A cooling feed covers both the supply (cold) and return (warm) paths of a loop, rather than modeling each direction as a separate object.
- **Intakes and outflows both sit on the supply path.** Both device components describe the cold, coolant-distribution side of the loop: an intake receives coolant and an outflow passes it onward to downstream equipment. The warm return path is not modeled per-component — it is captured by the feed loop.
## Cooling Sources
A [cooling source](../models/dcim/coolingsource.md) is the furthest upstream cooling element modeled in NetBox, representing a chiller, cooling tower, dry cooler, or CRAC/CRAH unit. Each source is associated with a site, and may optionally be associated with a particular location within that site. A cooling source is not a device; it represents external facility plant, and records the coolant (fluid type) and total rated cooling capacity for the loops it originates.
## Cooling Feeds
A [cooling feed](../models/dcim/coolingfeed.md) represents a coolant loop running between a cooling source and a particular rack. Each feed records an operational status, a rated cooling capacity, and a rated (design) flow rate.
## Device Components
Devices participate in cooling through two component types, instantiated from templates defined on the device type:
- A [cooling intake](../models/dcim/coolingintake.md) is a coolant intake on a device, such as a server cold-plate inlet or a CDU facility intake. It records the connector type, diameter, and rated maximum flow, and optionally references the upstream [cooling outflow](../models/dcim/coolingoutflow.md) that supplies it.
- A [cooling outflow](../models/dcim/coolingoutflow.md) is a coolant supply point on a device, such as a CDU or manifold outlet. It optionally references a parent cooling intake on the same device — the device takes coolant in through its intake and passes it back out through its outflow.
!!! tip "In-rack cooling equipment is modeled as a device"
Coolant distribution units (CDUs), manifolds, and rear-door heat exchangers (RDHx) are modeled as ordinary (typically zero-U) [devices](../models/dcim/device.md) installed in the rack — exactly as a PDU is modeled as a device with power ports and outlets. The device's make and model come from its [device type](../models/dcim/devicetype.md), and its cooling connections are represented by cooling intake and outflow components. There is no dedicated CDU or RDHx model.
## Racks and Devices
Racks and devices carry lightweight cooling attributes independent of the feed/component topology:
- A [rack](../models/dcim/rack.md) records a **cooling capability** (air-only, hybrid, or liquid-only) and a **cooling capacity** in kilowatts, typically inherited from its rack type.
- A [device](../models/dcim/device.md) records a **cooling method** (air, liquid, hybrid, or immersion), inherited from its device type and overridable per device.

View File

@ -0,0 +1,37 @@
# Cooling Feed
A cooling feed represents a coolant loop delivered from a [cooling source](./coolingsource.md) to a particular rack or coolant distribution unit (CDU). The [cooling intakes](./coolingintake.md) a feed supplies are derived from the devices installed in the rack it serves, rather than referenced explicitly.
A single feed represents the entire loop, covering both the supply (cold) and return (warm) paths.
!!! tip
In-rack cooling equipment — coolant distribution units (CDUs), manifolds, and rear-door heat exchangers (RDHx) — is modeled as an ordinary (typically zero-U) [device](./device.md) installed in the rack. The device's make and model come from its [device type](./devicetype.md), and a [cooling intake](./coolingintake.md) component connects it to cooling. The feed serving such a device is derived from its rack.
## Fields
### Cooling Source
The [cooling source](./coolingsource.md) which supplies this feed.
### Rack
The [rack](./rack.md) which this feed serves (optional).
### Name
The feed's name or identifier. Must be unique to the assigned cooling source.
### Status
The feed's operational status.
!!! tip
Additional statuses may be defined by setting `CoolingFeed.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Cooling Capacity
The heat-removal capacity of the feed, in kilowatts (kW).
### Maximum Flow
The maximum rate of coolant flow supported by the feed, expressed as a numeric value with a selectable unit (liters per minute, cubic meters per hour, or gallons per minute). Must be a positive, non-zero value in the selected unit, or left blank.

View File

@ -0,0 +1,40 @@
# Cooling Intakes
A cooling intake is a device component which consumes coolant, such as a server cold-plate inlet or a coolant distribution unit (CDU) intake. It **receives** coolant from the cold, supply side of a loop (see [cooling](../../features/cooling.md) for the overall flow model). A cooling intake optionally references the upstream [cooling outflow](./coolingoutflow.md) which supplies it.
!!! tip
Like most device components, cooling intakes are instantiated automatically from [cooling intake templates](./coolingintaketemplate.md) assigned to the selected device type when a device is created.
## Fields
### Device
The device to which this cooling intake belongs.
### Module
The installed module within the assigned device to which this cooling intake belongs (optional).
### Name
The name of the cooling intake. Must be unique to the parent device.
### Label
An alternative physical label identifying the cooling intake.
### Connector Type
The physical coolant connector type (e.g. UQD, UQDB, QDC, camlock, or threaded NPT/BSP).
### Diameter
The connector diameter, expressed as a numeric value with a selectable unit (millimeters, centimeters, or inches). Must be a positive, non-zero value in the selected unit, or left blank.
### Maximum Flow
The maximum coolant flow rate this port supports, expressed as a numeric value with a selectable unit (liters per minute, cubic meters per hour, or gallons per minute). Must be a positive, non-zero value in the selected unit, or left blank.
### Cooling Outflow
The upstream [cooling outflow](./coolingoutflow.md) which supplies this intake (optional).

View File

@ -0,0 +1,3 @@
# Cooling Intake Templates
A template for a cooling intake that will be created on all instantiations of the parent device type. See the [cooling intake](./coolingintake.md) documentation for more detail.

View File

@ -0,0 +1,38 @@
# Cooling Outflows
A cooling outflow is a device component which delivers coolant to a downstream [cooling intake](./coolingintake.md), and generally represents an outlet on a coolant distribution unit (CDU) or manifold. A cooling outflow may optionally be associated with an upstream cooling intake on the same device for path tracing.
A cooling outflow is a **supply** point on the cold, coolant-distribution side of a loop: it passes coolant onward to downstream equipment. It does **not** represent the return of warmed coolant back to the cooling source. The return path is not modeled per-component; instead, a single [cooling feed](./coolingfeed.md) represents the entire loop, covering both the supply (cold) and return (warm) paths.
!!! tip
Like most device components, cooling outflows are instantiated automatically from [cooling outflow templates](./coolingoutflowtemplate.md) assigned to the selected device type when a device is created.
## Fields
### Device
The device to which this cooling outflow belongs.
### Module
The installed module within the assigned device to which this cooling outflow belongs (optional).
### Name
The name of the cooling outflow. Must be unique to the parent device.
### Label
An alternative physical label identifying the cooling outflow.
### Connector Type
The physical coolant connector type (e.g. UQD, UQDB, QDC, camlock, or threaded NPT/BSP).
### Diameter
The connector diameter, expressed as a numeric value with a selectable unit (millimeters, centimeters, or inches). Must be a positive, non-zero value in the selected unit, or left blank.
### Cooling Intake
The upstream [cooling intake](./coolingintake.md) on the same device which feeds this outlet (optional).

View File

@ -0,0 +1,3 @@
# Cooling Outflow Templates
A template for a cooling outflow that will be created on all instantiations of the parent device type. See the [cooling outflow](./coolingoutflow.md) documentation for more detail.

View File

@ -0,0 +1,36 @@
# Cooling Source
A cooling source represents a facility-level source of cooling, such as a chiller, cooling tower, or dry cooler. It serves as the upstream origin for one or more [cooling feeds](./coolingfeed.md) which distribute coolant to racks and devices. A cooling source is not modeled as a device; it represents external facility plant.
## Fields
### Site
The [site](./site.md) at which the cooling source is located.
### Location
The [location](./location.md) within the site where the cooling source resides (optional).
### Name
The cooling source's name or identifier. Must be unique to the assigned site.
### Type
The type of cooling plant (e.g. chiller, cooling tower, dry cooler, CRAC, or CRAH).
### Status
The operational status of the cooling source.
!!! tip
Additional statuses may be defined by setting `CoolingSource.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
### Fluid Type
The coolant used by the source (e.g. water, water/glycol, dielectric fluid, or refrigerant).
### Cooling Capacity
The total heat-removal capacity of the source, expressed in kilowatts (kW).

View File

@ -30,6 +30,10 @@ The hardware [device type](./devicetype.md) which defines the device's make & mo
The direction in which air circulates through the device chassis for cooling.
### Cooling Method
The cooling method employed by the device (air, liquid, hybrid, or immersion). If not set, this is inherited from the assigned [device type](./devicetype.md) when the device is created.
### Serial Number
The unique physical serial number assigned to this device by its manufacturer.

View File

@ -57,6 +57,10 @@ Indicates whether this is a parent type (capable of housing child devices), a ch
The default direction in which airflow circulates within the device chassis. This may be configured differently for instantiated devices (e.g. because of different fan modules).
### Cooling Method
The default cooling method employed by devices of this type (air, liquid, hybrid, or immersion). Instantiated devices inherit this value unless overridden.
### Weight
The numeric weight of the device, including a unit designation (e.g. 10 kilograms or 20 pounds).

View File

@ -75,6 +75,10 @@ The numeric weight of the module, including a unit designation (e.g. 3 kilograms
The direction in which air circulates through the device chassis for cooling.
### Cooling Method
The cooling method employed by modules of this type (air, liquid, hybrid, or immersion). This is useful for liquid-cooled modules such as direct-to-chip accelerator (OAM) modules or liquid-cooled line cards.
### End of Life
The date after which this module type is no longer supported by its manufacturer. This can be used to identify modules approaching or past their support horizon to aid in hardware lifecycle planning.

View File

@ -54,6 +54,20 @@ The unique physical serial number assigned to this rack.
A unique, locally-administered label used to identify hardware resources.
### Cooling Capability
Describes how the rack is able to cool the equipment installed in it, which indicates what kind of equipment it can accommodate:
- **Air-only**: The rack is cooled by airflow only; no coolant is delivered to it. Only air-cooled equipment can be installed.
- **Hybrid**: Coolant can be delivered to the rack (e.g. via a [cooling feed](./coolingfeed.md)), but it can also house air-cooled equipment. Suitable for mixed or hybrid deployments.
- **Liquid-only**: The rack is intended exclusively for liquid-cooled equipment (such as direct-to-chip or immersion systems) and does not provide adequate air cooling on its own.
This attribute documents the rack's intended use so that incompatible equipment—such as high-density liquid-cooled hardware in an air-only rack—can be identified. When the rack is assigned a [rack type](./racktype.md), this value is inherited from the rack type.
### Cooling Capacity
The rack's cooling capacity, expressed in kilowatts (kW). When the rack is assigned a [rack type](./racktype.md), this value is inherited from the rack type.
## Physical Attributes
Several physical attributes may be defined on each rack, including its width, height, outer dimensions, mounting depth, and weight. These should generally be defined on the [rack type](./racktype.md) assigned to the rack rather than on the rack itself.

View File

@ -54,6 +54,14 @@ The numeric weight of the rack, including a unit designation (e.g. 10 kilograms
The maximum total weight capacity for all installed devices, inclusive of the rack itself.
### Cooling Capability
The rack design's coolant capability: air-only, hybrid, or liquid-only. Racks of this type inherit this value.
### Cooling Capacity
The rack design's cooling capacity, expressed in kilowatts (kW). Racks of this type inherit this value.
### Descending Units
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use ascending numbering, with unit 1 assigned to the bottommost position.)

View File

@ -71,6 +71,7 @@ nav:
- Facilities: 'features/facilities.md'
- Devices & Cabling: 'features/devices-cabling.md'
- Power Tracking: 'features/power-tracking.md'
- Cooling: 'features/cooling.md'
- IPAM: 'features/ipam.md'
- VLAN Management: 'features/vlan-management.md'
- L2VPN & Overlay: 'features/l2vpn-overlay.md'
@ -202,6 +203,12 @@ nav:
- ConsolePortTemplate: 'models/dcim/consoleporttemplate.md'
- ConsoleServerPort: 'models/dcim/consoleserverport.md'
- ConsoleServerPortTemplate: 'models/dcim/consoleserverporttemplate.md'
- CoolingFeed: 'models/dcim/coolingfeed.md'
- CoolingIntake: 'models/dcim/coolingintake.md'
- CoolingIntakeTemplate: 'models/dcim/coolingintaketemplate.md'
- CoolingOutflow: 'models/dcim/coolingoutflow.md'
- CoolingOutflowTemplate: 'models/dcim/coolingoutflowtemplate.md'
- CoolingSource: 'models/dcim/coolingsource.md'
- Device: 'models/dcim/device.md'
- DeviceBay: 'models/dcim/devicebay.md'
- DeviceBayTemplate: 'models/dcim/devicebaytemplate.md'

View File

@ -1,4 +1,5 @@
from .serializers_.cables import *
from .serializers_.cooling import *
from .serializers_.device_components import *
from .serializers_.devices import *
from .serializers_.devicetype_components import *

View File

@ -0,0 +1,83 @@
from dcim.choices import *
from dcim.models import CoolingFeed, CoolingSource
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import PrimaryModelSerializer
from netbox.choices import FlowRateUnitChoices
from tenancy.api.serializers_.tenants import TenantSerializer
from .racks import RackSerializer
from .sites import LocationSerializer, SiteSerializer
__all__ = (
'CoolingFeedSerializer',
'CoolingSourceSerializer',
)
class CoolingSourceSerializer(PrimaryModelSerializer):
site = SiteSerializer(nested=True)
location = LocationSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=CoolingSourceTypeChoices
)
status = ChoiceField(
choices=CoolingSourceStatusChoices,
default=lambda: CoolingSourceStatusChoices.STATUS_ACTIVE,
)
fluid_type = ChoiceField(
choices=FluidTypeChoices,
allow_blank=True,
required=False,
allow_null=True
)
# Related object counts
coolingfeed_count = RelatedObjectCountField('cooling_feeds')
class Meta:
model = CoolingSource
fields = [
'id', 'url', 'display_url', 'display', 'site', 'location', 'name', 'type', 'status', 'fluid_type',
'cooling_capacity', 'description', 'owner', 'comments', 'tags',
'custom_fields', 'coolingfeed_count', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'coolingfeed_count')
class CoolingFeedSerializer(PrimaryModelSerializer):
cooling_source = CoolingSourceSerializer(nested=True)
rack = RackSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
status = ChoiceField(
choices=CoolingFeedStatusChoices,
default=lambda: CoolingFeedStatusChoices.STATUS_ACTIVE,
)
max_flow_unit = ChoiceField(
choices=FlowRateUnitChoices,
allow_blank=True,
required=False,
allow_null=True
)
tenant = TenantSerializer(
nested=True,
required=False,
allow_null=True
)
class Meta:
model = CoolingFeed
fields = [
'id', 'url', 'display_url', 'display', 'cooling_source', 'rack', 'name', 'status',
'cooling_capacity', 'max_flow', 'max_flow_unit', 'description', 'tenant',
'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -8,6 +8,8 @@ from dcim.constants import *
from dcim.models import (
ConsolePort,
ConsoleServerPort,
CoolingIntake,
CoolingOutflow,
DeviceBay,
FrontPort,
Interface,
@ -25,6 +27,7 @@ from ipam.models import VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.gfk_fields import GFKSerializerField
from netbox.api.serializers import NetBoxModelSerializer
from netbox.choices import DiameterUnitChoices, FlowRateUnitChoices
from users.api.serializers_.mixins import OwnerMixin
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
@ -38,12 +41,14 @@ from .devices import DeviceSerializer, MACAddressSerializer, ModuleSerializer, V
from .devicetypes import ModuleBayTypeSerializer
from .manufacturers import ManufacturerSerializer
from .mixins import _UNSET, MACAddressShortcutMixin
from .nested import NestedInterfaceSerializer
from .nested import NestedCoolingOutflowSerializer, NestedInterfaceSerializer
from .roles import InventoryItemRoleSerializer
__all__ = (
'ConsolePortSerializer',
'ConsoleServerPortSerializer',
'CoolingIntakeSerializer',
'CoolingOutflowSerializer',
'DeviceBaySerializer',
'FrontPortSerializer',
'InterfaceSerializer',
@ -199,6 +204,83 @@ class PowerOutletSerializer(
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class CoolingIntakeSerializer(OwnerMixin, NetBoxModelSerializer):
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'device', 'module_bay'),
required=False,
allow_null=True
)
type = ChoiceField(
choices=CoolingConnectorTypeChoices,
allow_blank=True,
required=False,
allow_null=True
)
diameter_unit = ChoiceField(
choices=DiameterUnitChoices,
allow_blank=True,
required=False,
allow_null=True
)
max_flow_unit = ChoiceField(
choices=FlowRateUnitChoices,
allow_blank=True,
required=False,
allow_null=True
)
cooling_outflow = NestedCoolingOutflowSerializer(
required=False,
allow_null=True
)
class Meta:
model = CoolingIntake
fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type',
'diameter', 'diameter_unit', 'max_flow', 'max_flow_unit', 'cooling_outflow',
'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
class CoolingOutflowSerializer(OwnerMixin, NetBoxModelSerializer):
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'device', 'module_bay'),
required=False,
allow_null=True
)
type = ChoiceField(
choices=CoolingConnectorTypeChoices,
allow_blank=True,
required=False,
allow_null=True
)
diameter_unit = ChoiceField(
choices=DiameterUnitChoices,
allow_blank=True,
required=False,
allow_null=True
)
cooling_intake = CoolingIntakeSerializer(
nested=True,
required=False,
allow_null=True
)
class Meta:
model = CoolingOutflow
fields = [
'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type',
'diameter', 'diameter_unit', 'cooling_intake', 'description', 'owner', 'tags', 'custom_fields',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
class InterfaceSerializer(
MACAddressShortcutMixin,
OwnerMixin,

View File

@ -57,6 +57,7 @@ class DeviceSerializer(PrimaryModelSerializer):
)
status = ChoiceField(choices=DeviceStatusChoices, required=False)
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
cooling_method = ChoiceField(choices=CoolingMethodChoices, allow_blank=True, required=False, allow_null=True)
primary_ip = IPAddressSerializer(
nested=True,
read_only=True,
@ -105,11 +106,13 @@ class DeviceSerializer(PrimaryModelSerializer):
fields = [
'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
'vc_position', 'vc_priority', 'description', 'owner', 'comments', 'config_template', 'config_context',
'status', 'airflow', 'cooling_method', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster',
'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'owner', 'comments', 'config_template',
'config_context',
'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
'console_server_port_count', 'power_port_count', 'power_outlet_count', 'cooling_intake_count',
'cooling_outflow_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count',
'module_bay_count', 'inventory_item_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -6,6 +6,8 @@ from dcim.constants import *
from dcim.models import (
ConsolePortTemplate,
ConsoleServerPortTemplate,
CoolingIntakeTemplate,
CoolingOutflowTemplate,
DeviceBayTemplate,
FrontPortTemplate,
InterfaceTemplate,
@ -19,6 +21,7 @@ from dcim.models import (
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.gfk_fields import GFKSerializerField
from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
from netbox.choices import DiameterUnitChoices, FlowRateUnitChoices
from wireless.choices import *
from .base import PortSerializer
@ -30,6 +33,8 @@ from .roles import InventoryItemRoleSerializer
__all__ = (
'ConsolePortTemplateSerializer',
'ConsoleServerPortTemplateSerializer',
'CoolingIntakeTemplateSerializer',
'CoolingOutflowTemplateSerializer',
'DeviceBayTemplateSerializer',
'FrontPortTemplateSerializer',
'InterfaceTemplateSerializer',
@ -170,6 +175,88 @@ class PowerOutletTemplateSerializer(ComponentTemplateSerializer):
brief_fields = ('id', 'url', 'display', 'name', 'description')
class CoolingIntakeTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
module_type = ModuleTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=CoolingConnectorTypeChoices,
allow_blank=True,
required=False,
allow_null=True
)
diameter_unit = ChoiceField(
choices=DiameterUnitChoices,
allow_blank=True,
required=False,
allow_null=True
)
max_flow_unit = ChoiceField(
choices=FlowRateUnitChoices,
allow_blank=True,
required=False,
allow_null=True
)
class Meta:
model = CoolingIntakeTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type',
'diameter', 'diameter_unit', 'max_flow', 'max_flow_unit', 'description',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class CoolingOutflowTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
module_type = ModuleTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=CoolingConnectorTypeChoices,
allow_blank=True,
required=False,
allow_null=True
)
diameter_unit = ChoiceField(
choices=DiameterUnitChoices,
allow_blank=True,
required=False,
allow_null=True
)
cooling_intake = CoolingIntakeTemplateSerializer(
nested=True,
required=False,
allow_null=True
)
class Meta:
model = CoolingOutflowTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type',
'diameter', 'diameter_unit', 'cooling_intake', 'description', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class InterfaceTemplateSerializer(ComponentTemplateSerializer):
device_type = DeviceTypeSerializer(
nested=True,

View File

@ -32,6 +32,7 @@ class DeviceTypeSerializer(PrimaryModelSerializer):
)
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
cooling_method = ChoiceField(choices=CoolingMethodChoices, allow_blank=True, required=False, allow_null=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
front_image = serializers.ImageField(required=False, allow_null=True)
rear_image = serializers.ImageField(required=False, allow_null=True)
@ -41,6 +42,8 @@ class DeviceTypeSerializer(PrimaryModelSerializer):
console_server_port_template_count = serializers.IntegerField(read_only=True)
power_port_template_count = serializers.IntegerField(read_only=True)
power_outlet_template_count = serializers.IntegerField(read_only=True)
cooling_intake_template_count = serializers.IntegerField(read_only=True)
cooling_outflow_template_count = serializers.IntegerField(read_only=True)
interface_template_count = serializers.IntegerField(read_only=True)
front_port_template_count = serializers.IntegerField(read_only=True)
rear_port_template_count = serializers.IntegerField(read_only=True)
@ -53,13 +56,14 @@ class DeviceTypeSerializer(PrimaryModelSerializer):
model = DeviceType
fields = [
'id', 'url', 'display_url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number',
'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
'weight_unit', 'end_of_life', 'front_image', 'rear_image', 'description', 'owner',
'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'cooling_method',
'weight', 'weight_unit', 'end_of_life', 'front_image', 'rear_image', 'description', 'owner',
'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'device_count', 'console_port_template_count',
'console_server_port_template_count', 'power_port_template_count', 'power_outlet_template_count',
'interface_template_count', 'front_port_template_count', 'rear_port_template_count',
'device_bay_template_count', 'module_bay_template_count', 'inventory_item_template_count',
'cooling_intake_template_count', 'cooling_outflow_template_count', 'interface_template_count',
'front_port_template_count', 'rear_port_template_count', 'device_bay_template_count',
'module_bay_template_count', 'inventory_item_template_count',
]
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count')
@ -117,6 +121,7 @@ class ModuleTypeSerializer(PrimaryModelSerializer):
required=False,
allow_null=True
)
cooling_method = ChoiceField(choices=CoolingMethodChoices, allow_blank=True, required=False, allow_null=True)
attributes = AttributesField(
source='attribute_data',
required=False,
@ -129,6 +134,8 @@ class ModuleTypeSerializer(PrimaryModelSerializer):
console_server_port_template_count = serializers.IntegerField(read_only=True)
power_port_template_count = serializers.IntegerField(read_only=True)
power_outlet_template_count = serializers.IntegerField(read_only=True)
cooling_intake_template_count = serializers.IntegerField(read_only=True)
cooling_outflow_template_count = serializers.IntegerField(read_only=True)
interface_template_count = serializers.IntegerField(read_only=True)
front_port_template_count = serializers.IntegerField(read_only=True)
rear_port_template_count = serializers.IntegerField(read_only=True)
@ -138,11 +145,11 @@ class ModuleTypeSerializer(PrimaryModelSerializer):
model = ModuleType
fields = [
'id', 'url', 'display_url', 'display', 'profile', 'manufacturer', 'model', 'part_number', 'airflow',
'weight', 'weight_unit', 'end_of_life', 'description', 'attributes', 'module_bay_types', 'owner',
'comments', 'tags', 'custom_fields',
'cooling_method', 'weight', 'weight_unit', 'end_of_life', 'description', 'attributes', 'module_bay_types',
'owner', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'module_count', 'console_port_template_count',
'console_server_port_template_count', 'power_port_template_count', 'power_outlet_template_count',
'interface_template_count', 'front_port_template_count', 'rear_port_template_count',
'module_bay_template_count',
'cooling_intake_template_count', 'cooling_outflow_template_count', 'interface_template_count',
'front_port_template_count', 'rear_port_template_count', 'module_bay_template_count',
]
brief_fields = ('id', 'url', 'display', 'profile', 'manufacturer', 'model', 'description', 'module_count')

View File

@ -5,6 +5,7 @@ from dcim import models
from netbox.api.serializers import WritableNestedSerializer
__all__ = (
'NestedCoolingOutflowSerializer',
'NestedDeviceBaySerializer',
'NestedDeviceRoleSerializer',
'NestedDeviceSerializer',
@ -68,6 +69,14 @@ class NestedDeviceSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display_url', 'display', 'name']
class NestedCoolingOutflowSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
class Meta:
model = models.CoolingOutflow
fields = ['id', 'url', 'display_url', 'display', 'device', 'name']
class NestedInterfaceSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
_occupied = serializers.BooleanField(required=False, read_only=True)

View File

@ -77,6 +77,12 @@ class RackBaseSerializer(PrimaryModelSerializer):
required=False,
allow_null=True
)
cooling_capability = ChoiceField(
choices=RackCoolingCapabilityChoices,
allow_blank=True,
required=False,
allow_null=True
)
class RackTypeSerializer(RackBaseSerializer):
@ -88,8 +94,9 @@ class RackTypeSerializer(RackBaseSerializer):
fields = [
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor',
'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
'outer_unit', 'weight', 'max_weight', 'weight_unit', 'mounting_depth', 'description', 'owner', 'comments',
'tags', 'custom_fields', 'created', 'last_updated', 'rack_count',
'outer_unit', 'weight', 'max_weight', 'weight_unit', 'mounting_depth', 'cooling_capability',
'cooling_capacity', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'rack_count',
]
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'rack_count')
@ -152,8 +159,9 @@ class RackSerializer(RackBaseSerializer):
'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'group', 'tenant',
'status', 'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit',
'weight', 'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
'outer_unit', 'mounting_depth', 'airflow', 'description', 'owner', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'device_count', 'powerfeed_count',
'outer_unit', 'mounting_depth', 'airflow', 'cooling_capability', 'cooling_capacity',
'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
'powerfeed_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')

View File

@ -30,6 +30,8 @@ router.register('console-port-templates', views.ConsolePortTemplateViewSet)
router.register('console-server-port-templates', views.ConsoleServerPortTemplateViewSet)
router.register('power-port-templates', views.PowerPortTemplateViewSet)
router.register('power-outlet-templates', views.PowerOutletTemplateViewSet)
router.register('cooling-intake-templates', views.CoolingIntakeTemplateViewSet)
router.register('cooling-outflow-templates', views.CoolingOutflowTemplateViewSet)
router.register('interface-templates', views.InterfaceTemplateViewSet)
router.register('front-port-templates', views.FrontPortTemplateViewSet)
router.register('rear-port-templates', views.RearPortTemplateViewSet)
@ -49,6 +51,8 @@ router.register('console-ports', views.ConsolePortViewSet)
router.register('console-server-ports', views.ConsoleServerPortViewSet)
router.register('power-ports', views.PowerPortViewSet)
router.register('power-outlets', views.PowerOutletViewSet)
router.register('cooling-intakes', views.CoolingIntakeViewSet)
router.register('cooling-outflows', views.CoolingOutflowViewSet)
router.register('interfaces', views.InterfaceViewSet)
router.register('front-ports', views.FrontPortViewSet)
router.register('rear-ports', views.RearPortViewSet)
@ -74,6 +78,10 @@ router.register('virtual-chassis', views.VirtualChassisViewSet)
router.register('power-panels', views.PowerPanelViewSet)
router.register('power-feeds', views.PowerFeedViewSet)
# Cooling
router.register('cooling-sources', views.CoolingSourceViewSet)
router.register('cooling-feeds', views.CoolingFeedViewSet)
# Miscellaneous
router.register('connected-device', views.ConnectedDeviceViewSet, basename='connected-device')

View File

@ -332,6 +332,18 @@ class PowerOutletTemplateViewSet(NetBoxModelViewSet):
filterset_class = filtersets.PowerOutletTemplateFilterSet
class CoolingIntakeTemplateViewSet(NetBoxModelViewSet):
queryset = CoolingIntakeTemplate.objects.all()
serializer_class = serializers.CoolingIntakeTemplateSerializer
filterset_class = filtersets.CoolingIntakeTemplateFilterSet
class CoolingOutflowTemplateViewSet(NetBoxModelViewSet):
queryset = CoolingOutflowTemplate.objects.all()
serializer_class = serializers.CoolingOutflowTemplateSerializer
filterset_class = filtersets.CoolingOutflowTemplateFilterSet
class InterfaceTemplateViewSet(NetBoxModelViewSet):
queryset = InterfaceTemplate.objects.all()
serializer_class = serializers.InterfaceTemplateSerializer
@ -477,6 +489,18 @@ class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
filterset_class = filtersets.PowerOutletFilterSet
class CoolingIntakeViewSet(NetBoxModelViewSet):
queryset = CoolingIntake.objects.all()
serializer_class = serializers.CoolingIntakeSerializer
filterset_class = filtersets.CoolingIntakeFilterSet
class CoolingOutflowViewSet(NetBoxModelViewSet):
queryset = CoolingOutflow.objects.all()
serializer_class = serializers.CoolingOutflowSerializer
filterset_class = filtersets.CoolingOutflowFilterSet
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = Interface.objects.prefetch_related(
GenericPrefetch(
@ -621,6 +645,26 @@ class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
filterset_class = filtersets.PowerFeedFilterSet
#
# Cooling sources
#
class CoolingSourceViewSet(NetBoxModelViewSet):
queryset = CoolingSource.objects.all()
serializer_class = serializers.CoolingSourceSerializer
filterset_class = filtersets.CoolingSourceFilterSet
#
# Cooling feeds
#
class CoolingFeedViewSet(NetBoxModelViewSet):
queryset = CoolingFeed.objects.all()
serializer_class = serializers.CoolingFeedSerializer
filterset_class = filtersets.CoolingFeedFilterSet
#
# Miscellaneous
#

View File

@ -2116,6 +2116,120 @@ class PowerOutletStatusChoices(ChoiceSet):
]
#
# Cooling
#
class CoolingMethodChoices(ChoiceSet):
METHOD_AIR = 'air'
METHOD_LIQUID = 'liquid'
METHOD_HYBRID = 'hybrid'
METHOD_IMMERSION = 'immersion'
CHOICES = [
(METHOD_AIR, _('Air'), 'cyan'),
(METHOD_LIQUID, _('Liquid'), 'blue'),
(METHOD_HYBRID, _('Hybrid'), 'purple'),
(METHOD_IMMERSION, _('Immersion'), 'indigo'),
]
class CoolingSourceTypeChoices(ChoiceSet):
key = 'CoolingSource.type'
TYPE_CHILLER = 'chiller'
TYPE_COOLING_TOWER = 'cooling-tower'
TYPE_DRY_COOLER = 'dry-cooler'
TYPE_CRAC = 'crac'
TYPE_CRAH = 'crah'
CHOICES = [
(TYPE_CHILLER, _('Chiller')),
(TYPE_COOLING_TOWER, _('Cooling tower')),
(TYPE_DRY_COOLER, _('Dry cooler')),
(TYPE_CRAC, _('CRAC')),
(TYPE_CRAH, _('CRAH')),
]
class CoolingSourceStatusChoices(ChoiceSet):
key = 'CoolingSource.status'
STATUS_OFFLINE = 'offline'
STATUS_ACTIVE = 'active'
STATUS_PLANNED = 'planned'
STATUS_FAILED = 'failed'
CHOICES = [
(STATUS_OFFLINE, _('Offline'), 'gray'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_PLANNED, _('Planned'), 'blue'),
(STATUS_FAILED, _('Failed'), 'red'),
]
class CoolingFeedStatusChoices(ChoiceSet):
key = 'CoolingFeed.status'
STATUS_OFFLINE = 'offline'
STATUS_ACTIVE = 'active'
STATUS_PLANNED = 'planned'
STATUS_FAILED = 'failed'
CHOICES = [
(STATUS_OFFLINE, _('Offline'), 'gray'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_PLANNED, _('Planned'), 'blue'),
(STATUS_FAILED, _('Failed'), 'red'),
]
class FluidTypeChoices(ChoiceSet):
FLUID_WATER = 'water'
FLUID_WATER_GLYCOL = 'water-glycol'
FLUID_DIELECTRIC = 'dielectric'
FLUID_REFRIGERANT = 'refrigerant'
CHOICES = [
(FLUID_WATER, _('Water')),
(FLUID_WATER_GLYCOL, _('Water/glycol')),
(FLUID_DIELECTRIC, _('Dielectric')),
(FLUID_REFRIGERANT, _('Refrigerant')),
]
class RackCoolingCapabilityChoices(ChoiceSet):
CAPABILITY_AIR_ONLY = 'air-only'
CAPABILITY_HYBRID = 'hybrid'
CAPABILITY_LIQUID_ONLY = 'liquid-only'
CHOICES = [
(CAPABILITY_AIR_ONLY, _('Air only'), 'cyan'),
(CAPABILITY_HYBRID, _('Hybrid'), 'blue'),
(CAPABILITY_LIQUID_ONLY, _('Liquid only'), 'purple'),
]
class CoolingConnectorTypeChoices(ChoiceSet):
TYPE_UQD = 'uqd'
TYPE_UQDB = 'uqdb'
TYPE_QDC = 'qdc'
TYPE_CAMLOCK = 'camlock'
TYPE_NPT = 'npt'
TYPE_BSP = 'bsp'
TYPE_PROPRIETARY = 'proprietary'
CHOICES = [
(TYPE_UQD, _('UQD (Universal Quick Disconnect)')),
(TYPE_UQDB, _('UQDB (Universal Quick Disconnect, Blind-mate)')),
(TYPE_QDC, _('QDC (Quick Disconnect Coupling)')),
(TYPE_CAMLOCK, _('Camlock (cam-and-groove)')),
(TYPE_NPT, _('NPT (threaded)')),
(TYPE_BSP, _('BSP (threaded)')),
(TYPE_PROPRIETARY, _('Proprietary')),
]
#
# VDC
#

View File

@ -97,6 +97,8 @@ MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
model__in=(
'consoleporttemplate',
'consoleserverporttemplate',
'coolingintaketemplate',
'coolingoutflowtemplate',
'frontporttemplate',
'interfacetemplate',
'poweroutlettemplate',
@ -109,6 +111,8 @@ MODULAR_COMPONENT_MODELS = Q(
model__in=(
'consoleport',
'consoleserverport',
'coolingintake',
'coolingoutflow',
'frontport',
'interface',
'poweroutlet',

View File

@ -12,7 +12,7 @@ from extras.filtersets import LocalConfigContextFilterSet
from extras.models import ConfigTemplate
from ipam.filtersets import PrimaryIPFilterSet
from ipam.models import ASN, VRF, IPAddress, VLANTranslationPolicy
from netbox.choices import ColorChoices
from netbox.choices import ColorChoices, DiameterUnitChoices, FlowRateUnitChoices
from netbox.filtersets import (
AttributeFiltersMixin,
BaseFilterSet,
@ -57,6 +57,12 @@ __all__ = (
'ConsolePortTemplateFilterSet',
'ConsoleServerPortFilterSet',
'ConsoleServerPortTemplateFilterSet',
'CoolingFeedFilterSet',
'CoolingIntakeFilterSet',
'CoolingIntakeTemplateFilterSet',
'CoolingOutflowFilterSet',
'CoolingOutflowTemplateFilterSet',
'CoolingSourceFilterSet',
'DeviceBayFilterSet',
'DeviceBayTemplateFilterSet',
'DeviceFilterSet',
@ -355,12 +361,17 @@ class RackTypeFilterSet(PrimaryModelFilterSet):
choices=RackWidthChoices,
distinct=False,
)
cooling_capability = django_filters.MultipleChoiceFilter(
choices=RackCoolingCapabilityChoices,
distinct=False,
)
class Meta:
model = RackType
fields = (
'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'cooling_capacity',
'description',
# Counters
'rack_count',
@ -495,12 +506,17 @@ class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
lookup_expr='iexact'
)
cooling_capability = django_filters.MultipleChoiceFilter(
choices=RackCoolingCapabilityChoices,
distinct=False,
)
class Meta:
model = Rack
fields = (
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
'weight_unit', 'description',
'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'cooling_capacity',
'weight', 'max_weight', 'weight_unit', 'description',
)
def search(self, queryset, name, value):
@ -728,13 +744,15 @@ class DeviceTypeFilterSet(PrimaryModelFilterSet):
model = DeviceType
fields = (
'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'end_of_life', 'description',
'subdevice_role', 'airflow', 'cooling_method', 'weight', 'weight_unit', 'end_of_life', 'description',
# Counters
'console_port_template_count',
'console_server_port_template_count',
'power_port_template_count',
'power_outlet_template_count',
'cooling_intake_template_count',
'cooling_outflow_template_count',
'interface_template_count',
'front_port_template_count',
'rear_port_template_count',
@ -929,13 +947,16 @@ class ModuleTypeFilterSet(AttributeFiltersMixin, PrimaryModelFilterSet):
class Meta:
model = ModuleType
fields = (
'id', 'model', 'part_number', 'airflow', 'weight', 'weight_unit', 'end_of_life', 'description',
'id', 'model', 'part_number', 'airflow', 'cooling_method', 'weight', 'weight_unit', 'end_of_life',
'description',
# Counters
'console_port_template_count',
'console_server_port_template_count',
'power_port_template_count',
'power_outlet_template_count',
'cooling_intake_template_count',
'cooling_outflow_template_count',
'interface_template_count',
'front_port_template_count',
'rear_port_template_count',
@ -1051,6 +1072,55 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
fields = ('id', 'name', 'label', 'type', 'color', 'feed_leg', 'description')
@register_filterset
class CoolingIntakeTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=CoolingConnectorTypeChoices,
distinct=False,
null_value=None
)
diameter_unit = django_filters.MultipleChoiceFilter(
choices=DiameterUnitChoices,
distinct=False,
null_value=None
)
max_flow_unit = django_filters.MultipleChoiceFilter(
choices=FlowRateUnitChoices,
distinct=False,
null_value=None
)
class Meta:
model = CoolingIntakeTemplate
fields = (
'id', 'name', 'label', 'diameter', 'max_flow', 'description',
)
@register_filterset
class CoolingOutflowTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=CoolingConnectorTypeChoices,
distinct=False,
null_value=None
)
diameter_unit = django_filters.MultipleChoiceFilter(
choices=DiameterUnitChoices,
distinct=False,
null_value=None
)
cooling_intake_id = django_filters.ModelMultipleChoiceFilter(
field_name='cooling_intake',
queryset=CoolingIntakeTemplate.objects.all(),
distinct=False,
label=_('Cooling intake (ID)'),
)
class Meta:
model = CoolingOutflowTemplate
fields = ('id', 'name', 'label', 'diameter', 'description')
@register_filterset
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
@ -1542,14 +1612,16 @@ class DeviceFilterSet(
class Meta:
model = Device
fields = (
'id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority',
'description',
'id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'cooling_method', 'vc_position',
'vc_priority', 'description',
# Counters
'console_port_count',
'console_server_port_count',
'power_port_count',
'power_outlet_count',
'cooling_intake_count',
'cooling_outflow_count',
'interface_count',
'front_port_count',
'rear_port_count',
@ -2102,6 +2174,63 @@ class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
)
@register_filterset
class CoolingIntakeFilterSet(ModularDeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=CoolingConnectorTypeChoices,
distinct=False,
null_value=None
)
diameter_unit = django_filters.MultipleChoiceFilter(
choices=DiameterUnitChoices,
distinct=False,
null_value=None
)
max_flow_unit = django_filters.MultipleChoiceFilter(
choices=FlowRateUnitChoices,
distinct=False,
null_value=None
)
cooling_outflow_id = django_filters.ModelMultipleChoiceFilter(
queryset=CoolingOutflow.objects.all(),
field_name='cooling_outflow',
distinct=False,
label=_('Cooling outflow (ID)'),
)
class Meta:
model = CoolingIntake
fields = (
'id', 'name', 'label', 'diameter', 'max_flow', 'description',
)
@register_filterset
class CoolingOutflowFilterSet(ModularDeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=CoolingConnectorTypeChoices,
distinct=False,
null_value=None
)
diameter_unit = django_filters.MultipleChoiceFilter(
choices=DiameterUnitChoices,
distinct=False,
null_value=None
)
cooling_intake_id = django_filters.ModelMultipleChoiceFilter(
field_name='cooling_intake',
queryset=CoolingIntake.objects.all(),
distinct=False,
label=_('Cooling intake (ID)'),
)
class Meta:
model = CoolingOutflow
fields = (
'id', 'name', 'label', 'diameter', 'description',
)
@register_filterset
class MACAddressFilterSet(PrimaryModelFilterSet):
mac_address = MultiValueMACAddressFilter()
@ -3121,6 +3250,173 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpo
return queryset.filter(qs_filter)
@register_filterset
class CoolingSourceFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
distinct=False,
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='site__slug',
queryset=Site.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Site name (slug)'),
)
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='location',
lookup_expr='in',
label=_('Location (ID)'),
)
location = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='location',
lookup_expr='in',
to_field_name='slug',
label=_('Location (slug)'),
)
type = django_filters.MultipleChoiceFilter(
choices=CoolingSourceTypeChoices,
distinct=False,
null_value=None
)
status = django_filters.MultipleChoiceFilter(
choices=CoolingSourceStatusChoices,
distinct=False,
null_value=None
)
fluid_type = django_filters.MultipleChoiceFilter(
choices=FluidTypeChoices,
distinct=False,
null_value=None
)
class Meta:
model = CoolingSource
fields = (
'id', 'name', 'cooling_capacity', 'description',
)
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
)
return queryset.filter(qs_filter)
@register_filterset
class CoolingFeedFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='cooling_source__site__region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='cooling_source__site__region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='cooling_source__site__group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='cooling_source__site__group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='cooling_source__site',
queryset=Site.objects.all(),
distinct=False,
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='cooling_source__site__slug',
queryset=Site.objects.all(),
distinct=False,
to_field_name='slug',
label=_('Site name (slug)'),
)
cooling_source_id = django_filters.ModelMultipleChoiceFilter(
queryset=CoolingSource.objects.all(),
distinct=False,
label=_('Cooling source (ID)'),
)
rack_id = django_filters.ModelMultipleChoiceFilter(
field_name='rack',
queryset=Rack.objects.all(),
distinct=False,
label=_('Rack (ID)'),
)
status = django_filters.MultipleChoiceFilter(
choices=CoolingFeedStatusChoices,
distinct=False,
null_value=None
)
max_flow_unit = django_filters.MultipleChoiceFilter(
choices=FlowRateUnitChoices,
distinct=False,
null_value=None
)
class Meta:
model = CoolingFeed
fields = (
'id', 'name', 'cooling_capacity', 'max_flow', 'description',
)
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(cooling_source__name__icontains=value) |
Q(comments__icontains=value)
)
return queryset.filter(qs_filter)
#
# Connection filter sets
#

View File

@ -13,6 +13,8 @@ from .object_create import ComponentCreateForm
__all__ = (
'ConsolePortBulkCreateForm',
'ConsoleServerPortBulkCreateForm',
'CoolingIntakeBulkCreateForm',
'CoolingOutflowBulkCreateForm',
'DeviceBayBulkCreateForm',
# 'FrontPortBulkCreateForm',
'InterfaceBulkCreateForm',
@ -81,6 +83,33 @@ class PowerOutletBulkCreateForm(
)
class CoolingIntakeBulkCreateForm(
form_from_model(
CoolingIntake,
[
'type', 'diameter', 'diameter_unit', 'max_flow', 'max_flow_unit'
]
),
DeviceBulkAddComponentForm
):
model = CoolingIntake
field_order = (
'name', 'label', 'type', 'diameter', 'diameter_unit', 'max_flow', 'max_flow_unit',
'description', 'tags',
)
class CoolingOutflowBulkCreateForm(
form_from_model(CoolingOutflow, ['type', 'diameter', 'diameter_unit']),
DeviceBulkAddComponentForm
):
model = CoolingOutflow
field_order = (
'name', 'label', 'type', 'diameter', 'diameter_unit',
'description', 'tags',
)
class InterfaceBulkCreateForm(
form_from_model(Interface, [
'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'poe_mode', 'poe_type', 'rf_role'

View File

@ -1,3 +1,5 @@
from decimal import Decimal
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
@ -41,6 +43,12 @@ __all__ = (
'ConsolePortTemplateBulkEditForm',
'ConsoleServerPortBulkEditForm',
'ConsoleServerPortTemplateBulkEditForm',
'CoolingFeedBulkEditForm',
'CoolingIntakeBulkEditForm',
'CoolingIntakeTemplateBulkEditForm',
'CoolingOutflowBulkEditForm',
'CoolingOutflowTemplateBulkEditForm',
'CoolingSourceBulkEditForm',
'DeviceBayBulkEditForm',
'DeviceBayTemplateBulkEditForm',
'DeviceBulkEditForm',
@ -302,6 +310,16 @@ class RackTypeBulkEditForm(PrimaryModelBulkEditForm):
required=False,
initial=''
)
cooling_capability = forms.ChoiceField(
label=_('Cooling capability'),
choices=add_blank_choice(RackCoolingCapabilityChoices),
required=False
)
cooling_capacity = forms.DecimalField(
label=_('Cooling capacity'),
min_value=0,
required=False
)
model = RackType
fieldsets = (
@ -313,10 +331,11 @@ class RackTypeBulkEditForm(PrimaryModelBulkEditForm):
name=_('Dimensions')
),
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')),
)
nullable_fields = (
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'weight',
'max_weight', 'weight_unit', 'description', 'comments',
'max_weight', 'weight_unit', 'cooling_capability', 'cooling_capacity', 'description', 'comments',
)
@ -439,6 +458,16 @@ class RackBulkEditForm(PrimaryModelBulkEditForm):
choices=add_blank_choice(RackAirflowChoices),
required=False
)
cooling_capability = forms.ChoiceField(
label=_('Cooling capability'),
choices=add_blank_choice(RackCoolingCapabilityChoices),
required=False
)
cooling_capacity = forms.DecimalField(
label=_('Cooling capacity'),
min_value=0,
required=False
)
weight = forms.DecimalField(
label=_('Weight'),
min_value=0,
@ -464,11 +493,13 @@ class RackBulkEditForm(PrimaryModelBulkEditForm):
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
FieldSet('outer_width', 'outer_height', 'outer_depth', 'outer_unit', name=_('Outer Dimensions')),
FieldSet('form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'mounting_depth', name=_('Hardware')),
FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')),
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
)
nullable_fields = (
'location', 'group', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_height', 'outer_depth',
'outer_unit', 'weight', 'max_weight', 'weight_unit', 'description', 'comments',
'outer_unit', 'cooling_capability', 'cooling_capacity', 'weight', 'max_weight', 'weight_unit', 'description',
'comments',
)
@ -540,6 +571,11 @@ class DeviceTypeBulkEditForm(PrimaryModelBulkEditForm):
choices=add_blank_choice(DeviceAirflowChoices),
required=False
)
cooling_method = forms.ChoiceField(
label=_('Cooling method'),
choices=add_blank_choice(CoolingMethodChoices),
required=False
)
weight = forms.DecimalField(
label=_('Weight'),
min_value=0,
@ -561,12 +597,14 @@ class DeviceTypeBulkEditForm(PrimaryModelBulkEditForm):
fieldsets = (
FieldSet(
'manufacturer', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
'airflow', 'description', name=_('Device Type')
'airflow', 'cooling_method', 'description', name=_('Device Type')
),
FieldSet('weight', 'weight_unit', name=_('Weight')),
FieldSet('end_of_life', name=_('Lifecycle')),
)
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit', 'end_of_life', 'description', 'comments')
nullable_fields = (
'part_number', 'airflow', 'cooling_method', 'weight', 'weight_unit', 'end_of_life', 'description', 'comments',
)
class ModuleBayTypeBulkEditForm(PrimaryModelBulkEditForm):
@ -620,6 +658,11 @@ class ModuleTypeBulkEditForm(PrimaryModelBulkEditForm):
choices=add_blank_choice(ModuleAirflowChoices),
required=False
)
cooling_method = forms.ChoiceField(
label=_('Cooling method'),
choices=add_blank_choice(CoolingMethodChoices),
required=False
)
weight = forms.DecimalField(
label=_('Weight'),
min_value=0,
@ -652,14 +695,16 @@ class ModuleTypeBulkEditForm(PrimaryModelBulkEditForm):
fieldsets = (
FieldSet('profile', 'manufacturer', 'part_number', 'description', name=_('Module Type')),
FieldSet(
'airflow',
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
name=_('Chassis')
),
FieldSet('cooling_method', 'airflow', name=_('Cooling')),
FieldSet('add_module_bay_types', 'remove_module_bay_types', name=_('Bay Types')),
FieldSet('end_of_life', name=_('Lifecycle')),
)
nullable_fields = ('part_number', 'weight', 'weight_unit', 'profile', 'end_of_life', 'description', 'comments')
nullable_fields = (
'part_number', 'weight', 'weight_unit', 'profile', 'end_of_life', 'description', 'comments', 'cooling_method',
)
class DeviceRoleBulkEditForm(NestedGroupModelBulkEditForm):
@ -769,6 +814,11 @@ class DeviceBulkEditForm(PrimaryModelBulkEditForm):
choices=add_blank_choice(DeviceAirflowChoices),
required=False
)
cooling_method = forms.ChoiceField(
label=_('Cooling method'),
choices=add_blank_choice(CoolingMethodChoices),
required=False
)
serial = forms.CharField(
max_length=50,
required=False,
@ -792,12 +842,12 @@ class DeviceBulkEditForm(PrimaryModelBulkEditForm):
fieldsets = (
FieldSet('role', 'status', 'tenant', 'platform', 'description', name=_('Device')),
FieldSet('site', 'location', name=_('Location')),
FieldSet('manufacturer', 'device_type', 'airflow', 'serial', name=_('Hardware')),
FieldSet('manufacturer', 'device_type', 'airflow', 'cooling_method', 'serial', name=_('Hardware')),
FieldSet('config_template', name=_('Configuration')),
FieldSet('cluster', name=_('Virtualization')),
)
nullable_fields = (
'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'cluster', 'comments',
'location', 'tenant', 'platform', 'serial', 'airflow', 'cooling_method', 'description', 'cluster', 'comments',
)
@ -1036,6 +1086,130 @@ class PowerFeedBulkEditForm(PrimaryModelBulkEditForm):
nullable_fields = ('location', 'tenant', 'description', 'comments')
#
# Cooling
#
class CoolingSourceBulkEditForm(PrimaryModelBulkEditForm):
region = DynamicModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site_group = DynamicModelChoiceField(
label=_('Site group'),
queryset=SiteGroup.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region',
'group_id': '$site_group',
}
)
location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site'
}
)
type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(CoolingSourceTypeChoices),
required=False,
initial=''
)
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(CoolingSourceStatusChoices),
required=False,
initial=''
)
fluid_type = forms.ChoiceField(
label=_('Fluid type'),
choices=add_blank_choice(FluidTypeChoices),
required=False,
initial=''
)
cooling_capacity = forms.DecimalField(
label=_('Cooling capacity'),
min_value=0,
required=False
)
model = CoolingSource
fieldsets = (
FieldSet('region', 'site_group', 'site', 'location', 'type', 'status', 'description'),
FieldSet('fluid_type', 'cooling_capacity', name=_('Characteristics')),
)
nullable_fields = (
'location', 'fluid_type', 'cooling_capacity', 'description', 'comments',
)
class CoolingFeedBulkEditForm(PrimaryModelBulkEditForm):
cooling_source = DynamicModelChoiceField(
label=_('Cooling source'),
queryset=CoolingSource.objects.all(),
required=False
)
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
required=False,
)
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(CoolingFeedStatusChoices),
required=False,
initial=''
)
cooling_capacity = forms.DecimalField(
label=_('Cooling capacity'),
min_value=0,
required=False
)
max_flow = forms.DecimalField(
label=_('Max flow'),
min_value=Decimal('0.01'),
required=False
)
max_flow_unit = forms.ChoiceField(
label=_('Max flow unit'),
choices=add_blank_choice(FlowRateUnitChoices),
required=False,
initial=''
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False
)
model = CoolingFeed
fieldsets = (
FieldSet('cooling_source', 'rack', 'status', 'description', 'tenant'),
FieldSet(
'cooling_capacity', 'max_flow', 'max_flow_unit',
name=_('Characteristics')
),
)
nullable_fields = (
'rack', 'cooling_capacity', 'max_flow', 'max_flow_unit',
'tenant', 'description', 'comments',
)
#
# Device component templates
#
@ -1176,6 +1350,125 @@ class PowerOutletTemplateBulkEditForm(ComponentTemplateBulkEditForm):
self.fields['power_port'].widget.attrs['disabled'] = True
class CoolingIntakeTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=CoolingIntakeTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
)
label = forms.CharField(
label=_('Label'),
max_length=64,
required=False
)
type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(CoolingConnectorTypeChoices),
required=False
)
diameter = forms.DecimalField(
label=_('Diameter'),
min_value=Decimal('0.01'),
required=False
)
diameter_unit = forms.ChoiceField(
label=_('Diameter unit'),
choices=add_blank_choice(DiameterUnitChoices),
required=False
)
max_flow = forms.DecimalField(
label=_('Max flow'),
min_value=Decimal('0.01'),
required=False
)
max_flow_unit = forms.ChoiceField(
label=_('Max flow unit'),
choices=add_blank_choice(FlowRateUnitChoices),
required=False
)
description = forms.CharField(
label=_('Description'),
required=False
)
fieldsets = (
FieldSet(
'label', 'type',
InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
InlineFields('max_flow', 'max_flow_unit', label=_('Max flow')),
'description',
),
)
nullable_fields = (
'label', 'type', 'diameter', 'diameter_unit', 'max_flow', 'max_flow_unit',
'description',
)
class CoolingOutflowTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=CoolingOutflowTemplate.objects.all(),
widget=forms.MultipleHiddenInput()
)
device_type = forms.ModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),
required=False,
disabled=True,
widget=forms.HiddenInput()
)
label = forms.CharField(
label=_('Label'),
max_length=64,
required=False
)
type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(CoolingConnectorTypeChoices),
required=False
)
diameter = forms.DecimalField(
label=_('Diameter'),
min_value=Decimal('0.01'),
required=False
)
diameter_unit = forms.ChoiceField(
label=_('Diameter unit'),
choices=add_blank_choice(DiameterUnitChoices),
required=False
)
cooling_intake = forms.ModelChoiceField(
label=_('Cooling intake'),
queryset=CoolingIntakeTemplate.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
required=False
)
fieldsets = (
FieldSet(
'label', 'type',
InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
'cooling_intake', 'description',
),
)
nullable_fields = (
'label', 'type', 'diameter', 'diameter_unit', 'cooling_intake', 'description',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit cooling_intake queryset to CoolingIntakeTemplates which belong to the parent DeviceType
if 'device_type' in self.initial:
device_type = DeviceType.objects.filter(pk=self.initial['device_type']).first()
self.fields['cooling_intake'].queryset = CoolingIntakeTemplate.objects.filter(device_type=device_type)
else:
self.fields['cooling_intake'].choices = ()
self.fields['cooling_intake'].widget.attrs['disabled'] = True
class InterfaceTemplateBulkEditForm(ComponentTemplateBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=InterfaceTemplate.objects.all(),
@ -1482,6 +1775,71 @@ class PowerOutletBulkEditForm(
self.fields['power_port'].widget.attrs['disabled'] = True
class CoolingIntakeBulkEditForm(
ComponentBulkEditForm,
form_from_model(CoolingIntake, [
'label', 'type', 'diameter', 'diameter_unit', 'max_flow', 'max_flow_unit',
'description'
])
):
cooling_outflow = DynamicModelChoiceField(
label=_('Cooling outflow'),
queryset=CoolingOutflow.objects.all(),
required=False
)
model = CoolingIntake
fieldsets = (
FieldSet(
'module', 'type',
InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
'label', 'cooling_outflow', 'description',
),
FieldSet(
InlineFields('max_flow', 'max_flow_unit', label=_('Max flow')),
name=_('Characteristics')
),
)
nullable_fields = (
'module', 'label', 'type', 'diameter', 'diameter_unit', 'max_flow', 'max_flow_unit',
'cooling_outflow', 'description',
)
class CoolingOutflowBulkEditForm(
ComponentBulkEditForm,
form_from_model(
CoolingOutflow,
[
'label', 'type', 'diameter', 'diameter_unit', 'cooling_intake',
'description'
]
)
):
model = CoolingOutflow
fieldsets = (
FieldSet(
'module', 'type',
InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
'label', 'description', 'cooling_intake',
),
)
nullable_fields = (
'module', 'label', 'type', 'diameter', 'diameter_unit', 'cooling_intake', 'description',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit cooling_intake queryset to CoolingIntakes which belong to the parent Device
if self.device_id:
device = Device.objects.filter(pk=self.device_id).first()
self.fields['cooling_intake'].queryset = CoolingIntake.objects.filter(device=device)
else:
self.fields['cooling_intake'].choices = ()
self.fields['cooling_intake'].widget.attrs['disabled'] = True
class InterfaceBulkEditForm(
ComponentBulkEditForm,
form_from_model(Interface, [

View File

@ -40,6 +40,10 @@ __all__ = (
'CableImportForm',
'ConsolePortImportForm',
'ConsoleServerPortImportForm',
'CoolingFeedImportForm',
'CoolingIntakeImportForm',
'CoolingOutflowImportForm',
'CoolingSourceImportForm',
'DeviceBayImportForm',
'DeviceImportForm',
'DeviceRoleImportForm',
@ -241,13 +245,19 @@ class RackTypeImportForm(PrimaryModelImportForm):
required=False,
help_text=_('Unit for rack weights')
)
cooling_capability = CSVChoiceField(
label=_('Cooling capability'),
choices=RackCoolingCapabilityChoices,
required=False,
help_text=_('Cooling capability')
)
class Meta:
model = RackType
fields = (
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
'weight_unit', 'description', 'owner', 'comments', 'tags',
'weight_unit', 'cooling_capability', 'cooling_capacity', 'description', 'owner', 'comments', 'tags',
)
def __init__(self, data=None, *args, **kwargs):
@ -327,6 +337,12 @@ class RackImportForm(PrimaryModelImportForm):
required=False,
help_text=_('Airflow direction')
)
cooling_capability = CSVChoiceField(
label=_('Cooling capability'),
choices=RackCoolingCapabilityChoices,
required=False,
help_text=_('Cooling capability')
)
weight_unit = CSVChoiceField(
label=_('Weight unit'),
choices=WeightUnitChoices,
@ -339,8 +355,8 @@ class RackImportForm(PrimaryModelImportForm):
fields = (
'site', 'location', 'group', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor',
'serial', 'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'owner',
'comments', 'tags',
'outer_unit', 'mounting_depth', 'airflow', 'cooling_capability', 'cooling_capacity', 'weight',
'max_weight', 'weight_unit', 'description', 'owner', 'comments', 'tags',
)
def __init__(self, data=None, *args, **kwargs):
@ -460,8 +476,8 @@ class DeviceTypeImportForm(PrimaryModelImportForm):
model = DeviceType
fields = [
'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization',
'is_full_depth', 'subdevice_role', 'airflow', 'description', 'weight', 'weight_unit', 'end_of_life',
'owner', 'comments', 'tags',
'is_full_depth', 'subdevice_role', 'airflow', 'cooling_method', 'description', 'weight', 'weight_unit',
'end_of_life', 'owner', 'comments', 'tags',
]
@ -507,6 +523,12 @@ class ModuleTypeImportForm(PrimaryModelImportForm):
required=False,
help_text=_('Airflow direction')
)
cooling_method = CSVChoiceField(
label=_('Cooling method'),
choices=CoolingMethodChoices,
required=False,
help_text=_('Cooling method')
)
weight = forms.DecimalField(
label=_('Weight'),
required=False,
@ -527,8 +549,8 @@ class ModuleTypeImportForm(PrimaryModelImportForm):
class Meta:
model = ModuleType
fields = [
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'end_of_life',
'profile', 'attribute_data', 'owner', 'comments', 'tags',
'manufacturer', 'model', 'part_number', 'description', 'cooling_method', 'airflow', 'weight', 'weight_unit',
'end_of_life', 'profile', 'attribute_data', 'owner', 'comments', 'tags',
]
def clean(self):
@ -716,6 +738,12 @@ class DeviceImportForm(BaseDeviceImportForm):
required=False,
help_text=_('Airflow direction')
)
cooling_method = CSVChoiceField(
label=_('Cooling method'),
choices=CoolingMethodChoices,
required=False,
help_text=_('Cooling method')
)
config_template = CSVModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(),
@ -728,8 +756,8 @@ class DeviceImportForm(BaseDeviceImportForm):
fields = [
'name', 'role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent', 'device_bay', 'airflow',
'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'owner',
'comments', 'tags',
'cooling_method', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'description',
'config_template', 'owner', 'comments', 'tags',
]
def __init__(self, data=None, *args, **kwargs):
@ -966,6 +994,143 @@ class PowerOutletImportForm(OwnerCSVMixin, NetBoxModelImportForm):
self.fields['power_port'].queryset = PowerPort.objects.none()
class CoolingIntakeImportForm(OwnerCSVMixin, NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
type = CSVChoiceField(
label=_('Type'),
choices=CoolingConnectorTypeChoices,
required=False,
help_text=_('Physical connector type')
)
diameter_unit = CSVChoiceField(
label=_('Diameter unit'),
choices=DiameterUnitChoices,
required=False,
help_text=_('Diameter unit')
)
max_flow_unit = CSVChoiceField(
label=_('Max flow unit'),
choices=FlowRateUnitChoices,
required=False,
help_text=_('Unit for maximum flow')
)
cooling_outflow_device = CSVModelChoiceField(
label=_('Cooling outflow device'),
queryset=Device.objects.all(),
required=False,
to_field_name='name',
help_text=_('Device bearing the upstream cooling outflow (defaults to this intake\'s device)')
)
cooling_outflow = CSVModelChoiceField(
label=_('Cooling outflow'),
queryset=CoolingOutflow.objects.all(),
required=False,
to_field_name='name',
help_text=_('Upstream cooling outflow which feeds this intake')
)
class Meta:
model = CoolingIntake
fields = (
'device', 'name', 'label', 'type', 'diameter', 'diameter_unit', 'max_flow',
'max_flow_unit', 'cooling_outflow_device', 'cooling_outflow', 'description', 'owner', 'tags',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# The supplying outflow typically belongs to an upstream device (e.g. a CDU or manifold), so scope the
# CoolingOutflow choices to cooling_outflow_device where given, falling back to this intake's own device.
if self.is_bound and 'device' in self.data:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
device = None
else:
try:
device = self.instance.device
except Device.DoesNotExist:
device = None
outflow_device = None
if self.is_bound and self.data.get('cooling_outflow_device'):
try:
outflow_device = self.fields['cooling_outflow_device'].to_python(
self.data['cooling_outflow_device']
)
except forms.ValidationError:
outflow_device = None
if outflow_device:
self.fields['cooling_outflow'].queryset = CoolingOutflow.objects.filter(device=outflow_device)
elif device:
self.fields['cooling_outflow'].queryset = CoolingOutflow.objects.filter(
device__in=[device, device.get_vc_master()]
)
else:
self.fields['cooling_outflow'].queryset = CoolingOutflow.objects.none()
class CoolingOutflowImportForm(OwnerCSVMixin, NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
type = CSVChoiceField(
label=_('Type'),
choices=CoolingConnectorTypeChoices,
required=False,
help_text=_('Physical connector type')
)
diameter_unit = CSVChoiceField(
label=_('Diameter unit'),
choices=DiameterUnitChoices,
required=False,
help_text=_('Diameter unit')
)
cooling_intake = CSVModelChoiceField(
label=_('Cooling intake'),
queryset=CoolingIntake.objects.all(),
required=False,
to_field_name='name',
help_text=_('Local cooling intake which feeds this outflow')
)
class Meta:
model = CoolingOutflow
fields = (
'device', 'name', 'label', 'type', 'diameter', 'diameter_unit',
'cooling_intake', 'description', 'owner', 'tags',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit CoolingIntake choices to those belonging to this device (or VC master)
if self.is_bound and 'device' in self.data:
try:
device = self.fields['device'].to_python(self.data['device'])
except forms.ValidationError:
device = None
else:
try:
device = self.instance.device
except Device.DoesNotExist:
device = None
if device:
self.fields['cooling_intake'].queryset = CoolingIntake.objects.filter(
device__in=[device, device.get_vc_master()]
)
else:
self.fields['cooling_intake'].queryset = CoolingIntake.objects.none()
class InterfaceImportForm(OwnerCSVMixin, NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
@ -2027,6 +2192,127 @@ class PowerFeedImportForm(PrimaryModelImportForm):
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
class CoolingSourceImportForm(PrimaryModelImportForm):
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
to_field_name='name',
help_text=_('Name of parent site')
)
location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
to_field_name='name'
)
type = CSVChoiceField(
label=_('Type'),
choices=CoolingSourceTypeChoices,
help_text=_('Cooling source type')
)
status = CSVChoiceField(
label=_('Status'),
choices=CoolingSourceStatusChoices,
help_text=_('Operational status')
)
fluid_type = CSVChoiceField(
label=_('Fluid type'),
choices=FluidTypeChoices,
required=False,
help_text=_('Coolant fluid type')
)
class Meta:
model = CoolingSource
fields = (
'site', 'location', 'name', 'type', 'status', 'fluid_type', 'cooling_capacity', 'description', 'owner',
'comments', 'tags',
)
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
if data:
# Limit location queryset by assigned site
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
class CoolingFeedImportForm(PrimaryModelImportForm):
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
to_field_name='name',
help_text=_('Assigned site')
)
cooling_source = CSVModelChoiceField(
label=_('Cooling source'),
queryset=CoolingSource.objects.all(),
to_field_name='name',
help_text=_('Upstream cooling source')
)
location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
to_field_name='name',
required=False,
help_text=_("Rack's location (if any)")
)
rack = CSVModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
to_field_name='name',
required=False,
help_text=_('Rack')
)
tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
to_field_name='name',
required=False,
help_text=_('Assigned tenant')
)
status = CSVChoiceField(
label=_('Status'),
choices=CoolingFeedStatusChoices,
help_text=_('Operational status')
)
max_flow_unit = CSVChoiceField(
label=_('Max flow unit'),
choices=FlowRateUnitChoices,
required=False,
help_text=_('Unit for maximum flow')
)
class Meta:
model = CoolingFeed
fields = (
'site', 'cooling_source', 'location', 'rack', 'name', 'status',
'cooling_capacity', 'max_flow', 'max_flow_unit', 'tenant', 'description', 'owner',
'comments', 'tags',
)
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
if data:
# Limit cooling_source queryset by site
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
self.fields['cooling_source'].queryset = self.fields['cooling_source'].queryset.filter(**params)
# Limit location queryset by site
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
# Limit rack queryset by site and group
params = {
f"site__{self.fields['site'].to_field_name}": data.get('site'),
f"location__{self.fields['location'].to_field_name}": data.get('location'),
}
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
class VirtualDeviceContextImportForm(PrimaryModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),

View File

@ -19,7 +19,12 @@ from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from tenancy.models import Tenant
from users.models import User
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, PositiveBigIntegerField, TagFilterField
from utilities.forms.fields import (
ColorField,
DynamicModelMultipleChoiceField,
PositiveBigIntegerField,
TagFilterField,
)
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DatePicker, NumberWithOptions
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
@ -34,6 +39,12 @@ __all__ = (
'ConsolePortTemplateFilterForm',
'ConsoleServerPortFilterForm',
'ConsoleServerPortTemplateFilterForm',
'CoolingFeedFilterForm',
'CoolingIntakeFilterForm',
'CoolingIntakeTemplateFilterForm',
'CoolingOutflowFilterForm',
'CoolingOutflowTemplateFilterForm',
'CoolingSourceFilterForm',
'DeviceBayFilterForm',
'DeviceBayTemplateFilterForm',
'DeviceFilterForm',
@ -338,6 +349,16 @@ class RackBaseFilterForm(PrimaryModelFilterSetForm):
choices=add_blank_choice(WeightUnitChoices),
required=False
)
cooling_capability = forms.MultipleChoiceField(
label=_('Cooling capability'),
choices=RackCoolingCapabilityChoices,
required=False
)
cooling_capacity = forms.DecimalField(
label=_('Cooling capacity'),
required=False,
min_value=0
)
class RackTypeFilterForm(RackBaseFilterForm):
@ -347,6 +368,7 @@ class RackTypeFilterForm(RackBaseFilterForm):
FieldSet('manufacturer_id', 'form_factor', 'width', 'u_height', 'rack_count', name=_('Rack Type')),
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')),
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
)
selector_fields = ('filter_id', 'q', 'manufacturer_id')
@ -370,6 +392,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterFo
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'group_id', name=_('Location')),
FieldSet('status', 'role_id', 'manufacturer_id', 'rack_type_id', 'serial', 'asset_tag', name=_('Rack')),
FieldSet('form_factor', 'width', 'u_height', 'airflow', name=_('Hardware')),
FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')),
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
@ -562,8 +585,9 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
FieldSet('q', 'filter_id', 'tag'),
FieldSet(
'manufacturer_id', 'default_platform_id', 'part_number', 'device_count',
'subdevice_role', 'airflow', name=_('Hardware')
'subdevice_role', name=_('Hardware')
),
FieldSet('cooling_method', 'airflow', name=_('Cooling')),
FieldSet('has_front_image', 'has_rear_image', name=_('Images')),
FieldSet(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
@ -603,6 +627,11 @@ class DeviceTypeFilterForm(PrimaryModelFilterSetForm):
choices=add_blank_choice(DeviceAirflowChoices),
required=False
)
cooling_method = forms.MultipleChoiceField(
label=_('Cooling method'),
choices=add_blank_choice(CoolingMethodChoices),
required=False
)
has_front_image = forms.NullBooleanField(
required=False,
label=_('Has a front image'),
@ -738,8 +767,9 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
FieldSet('q', 'filter_id', 'tag'),
FieldSet(
'profile_id', 'manufacturer_id', 'part_number', 'module_count',
'airflow', 'module_bay_type_id', name=_('Hardware')
'module_bay_type_id', name=_('Hardware')
),
FieldSet('cooling_method', 'airflow', name=_('Cooling')),
FieldSet(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'module_bays', name=_('Components')
@ -830,6 +860,11 @@ class ModuleTypeFilterForm(PrimaryModelFilterSetForm):
choices=add_blank_choice(ModuleAirflowChoices),
required=False
)
cooling_method = forms.MultipleChoiceField(
label=_('Cooling method'),
choices=add_blank_choice(CoolingMethodChoices),
required=False
)
weight = forms.DecimalField(
label=_('Weight'),
required=False
@ -907,8 +942,9 @@ class DeviceFilterForm(
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
FieldSet('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_('Operation')),
FieldSet('status', 'role_id', 'serial', 'asset_tag', 'mac_address', name=_('Operation')),
FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_('Hardware')),
FieldSet('cooling_method', 'airflow', name=_('Cooling')),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
@ -996,6 +1032,11 @@ class DeviceFilterForm(
choices=add_blank_choice(DeviceAirflowChoices),
required=False
)
cooling_method = forms.MultipleChoiceField(
label=_('Cooling method'),
choices=add_blank_choice(CoolingMethodChoices),
required=False
)
serial = forms.CharField(
label=_('Serial'),
required=False
@ -1476,6 +1517,136 @@ class PowerFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
tag = TagFilterField(model)
class CoolingSourceFilterForm(ContactModelFilterForm, PrimaryModelFilterSetForm):
model = CoolingSource
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
FieldSet('type', 'status', 'fluid_type', name=_('Attributes')),
FieldSet('cooling_capacity', name=_('Characteristics')),
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
)
selector_fields = ('filter_id', 'q', 'site_id', 'location_id')
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id',
'group_id': '$site_group_id',
},
label=_('Site')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
null_option='None',
query_params={
'site_id': '$site_id'
},
label=_('Location')
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=CoolingSourceTypeChoices,
required=False
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=CoolingSourceStatusChoices,
required=False
)
fluid_type = forms.MultipleChoiceField(
label=_('Fluid type'),
choices=FluidTypeChoices,
required=False
)
cooling_capacity = forms.DecimalField(
label=_('Cooling capacity'),
required=False
)
tag = TagFilterField(model)
class CoolingFeedFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm):
model = CoolingFeed
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('region_id', 'site_group_id', 'site_id', 'cooling_source_id', 'rack_id', name=_('Location')),
FieldSet('status', name=_('Attributes')),
FieldSet(
'cooling_capacity', 'max_flow', 'max_flow_unit', name=_('Characteristics')
),
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
label=_('Site group')
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id'
},
label=_('Site')
)
cooling_source_id = DynamicModelMultipleChoiceField(
queryset=CoolingSource.objects.all(),
required=False,
null_option='None',
query_params={
'site_id': '$site_id'
},
label=_('Cooling source')
)
rack_id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
required=False,
null_option='None',
query_params={
'site_id': '$site_id'
},
label=_('Rack')
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=CoolingFeedStatusChoices,
required=False
)
cooling_capacity = forms.DecimalField(
label=_('Cooling capacity'),
required=False
)
max_flow = forms.DecimalField(
label=_('Max flow'),
required=False
)
max_flow_unit = forms.MultipleChoiceField(
label=_('Max flow unit'),
choices=FlowRateUnitChoices,
required=False
)
tag = TagFilterField(model)
#
# Device components
#
@ -1683,6 +1854,141 @@ class PowerOutletTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
)
class CoolingIntakeFilterForm(DeviceComponentFilterForm):
model = CoolingIntake
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet(
'name', 'label', 'type', 'diameter', 'diameter_unit', 'max_flow', 'max_flow_unit',
'cooling_outflow_id', name=_('Attributes')
),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
FieldSet(
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
name=_('Device')
),
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=CoolingConnectorTypeChoices,
required=False
)
diameter = forms.DecimalField(
label=_('Diameter'),
required=False
)
diameter_unit = forms.MultipleChoiceField(
label=_('Diameter unit'),
choices=DiameterUnitChoices,
required=False
)
max_flow = forms.DecimalField(
label=_('Max flow'),
required=False
)
max_flow_unit = forms.MultipleChoiceField(
label=_('Max flow unit'),
choices=FlowRateUnitChoices,
required=False
)
cooling_outflow_id = DynamicModelMultipleChoiceField(
label=_('Cooling outflow'),
queryset=CoolingOutflow.objects.all(),
required=False
)
tag = TagFilterField(model)
class CoolingIntakeTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
model = CoolingIntakeTemplate
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet(
'name', 'label', 'type', 'diameter', 'diameter_unit', 'max_flow', 'max_flow_unit',
name=_('Attributes')
),
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=CoolingConnectorTypeChoices,
required=False
)
diameter = forms.DecimalField(
label=_('Diameter'),
required=False
)
diameter_unit = forms.MultipleChoiceField(
label=_('Diameter unit'),
choices=DiameterUnitChoices,
required=False
)
max_flow = forms.DecimalField(
label=_('Max flow'),
required=False
)
max_flow_unit = forms.MultipleChoiceField(
label=_('Max flow unit'),
choices=FlowRateUnitChoices,
required=False
)
class CoolingOutflowFilterForm(DeviceComponentFilterForm):
model = CoolingOutflow
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet(
'name', 'label', 'type', 'diameter', 'diameter_unit', name=_('Attributes')
),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
FieldSet(
'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
name=_('Device')
),
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=CoolingConnectorTypeChoices,
required=False
)
diameter = forms.DecimalField(
label=_('Diameter'),
required=False
)
diameter_unit = forms.MultipleChoiceField(
label=_('Diameter unit'),
choices=DiameterUnitChoices,
required=False
)
tag = TagFilterField(model)
class CoolingOutflowTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
model = CoolingOutflowTemplate
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
FieldSet('name', 'label', 'type', 'diameter', 'diameter_unit', name=_('Attributes')),
FieldSet('device_type_id', 'module_type_id', name=_('Device')),
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=CoolingConnectorTypeChoices,
required=False
)
diameter = forms.DecimalField(
label=_('Diameter'),
required=False
)
diameter_unit = forms.MultipleChoiceField(
label=_('Diameter unit'),
choices=DiameterUnitChoices,
required=False
)
class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = Interface
fieldsets = (

View File

@ -52,6 +52,12 @@ __all__ = (
'ConsolePortTemplateForm',
'ConsoleServerPortForm',
'ConsoleServerPortTemplateForm',
'CoolingFeedForm',
'CoolingIntakeForm',
'CoolingIntakeTemplateForm',
'CoolingOutflowForm',
'CoolingOutflowTemplateForm',
'CoolingSourceForm',
'DeviceBayForm',
'DeviceBayTemplateForm',
'DeviceForm',
@ -310,6 +316,7 @@ class RackTypeForm(PrimaryModelForm):
'mounting_depth', name=_('Dimensions')
),
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')),
)
class Meta:
@ -317,7 +324,7 @@ class RackTypeForm(PrimaryModelForm):
fields = [
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
'weight_unit', 'description', 'owner', 'comments', 'tags',
'weight_unit', 'cooling_capability', 'cooling_capacity', 'description', 'owner', 'comments', 'tags',
]
@ -383,6 +390,7 @@ class RackForm(TenancyForm, PrimaryModelForm):
'site', 'location', 'group', 'name', 'status', 'role', 'rack_type', 'description', 'airflow', 'tags',
name=_('Rack')
),
FieldSet('cooling_capability', 'cooling_capacity', name=_('Cooling')),
FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
@ -392,8 +400,8 @@ class RackForm(TenancyForm, PrimaryModelForm):
fields = [
'site', 'location', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
'weight_unit', 'description', 'owner', 'comments', 'tags',
'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'cooling_capability',
'cooling_capacity', 'weight', 'max_weight', 'weight_unit', 'description', 'owner', 'comments', 'tags',
]
def __init__(self, *args, **kwargs):
@ -532,9 +540,10 @@ class DeviceTypeForm(PrimaryModelForm):
fieldsets = (
FieldSet('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags', name=_('Device Type')),
FieldSet(
'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow',
'u_height', 'exclude_from_utilization', 'is_full_depth', 'part_number', 'subdevice_role',
'weight', 'weight_unit', name=_('Chassis')
),
FieldSet('cooling_method', 'airflow', name=_('Cooling')),
FieldSet('end_of_life', name=_('Lifecycle')),
FieldSet('front_image', 'rear_image', name=_('Images')),
)
@ -543,7 +552,8 @@ class DeviceTypeForm(PrimaryModelForm):
model = DeviceType
fields = [
'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization',
'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'end_of_life', 'front_image',
'is_full_depth', 'subdevice_role', 'airflow', 'cooling_method', 'weight', 'weight_unit', 'end_of_life',
'front_image',
'rear_image', 'description', 'owner', 'comments', 'tags',
]
widgets = {
@ -627,7 +637,8 @@ class ModuleTypeForm(PrimaryModelForm):
def fieldsets(self):
return [
FieldSet('manufacturer', 'model', 'part_number', 'description', 'tags', name=_('Module Type')),
FieldSet('airflow', 'weight', 'weight_unit', name=_('Hardware')),
FieldSet('weight', 'weight_unit', name=_('Hardware')),
FieldSet('cooling_method', 'airflow', name=_('Cooling')),
FieldSet('module_bay_types', name=_('Bay Type Compatibility')),
FieldSet('end_of_life', name=_('Lifecycle')),
FieldSet('profile', *self.attr_fields, name=_('Profile & Attributes'), html_id='profile-attributes')
@ -636,8 +647,8 @@ class ModuleTypeForm(PrimaryModelForm):
class Meta:
model = ModuleType
fields = [
'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit',
'module_bay_types', 'end_of_life', 'owner', 'comments', 'tags',
'profile', 'manufacturer', 'model', 'part_number', 'description', 'cooling_method', 'airflow', 'weight',
'weight_unit', 'module_bay_types', 'end_of_life', 'owner', 'comments', 'tags',
]
widgets = {
'end_of_life': DatePicker(),
@ -878,7 +889,8 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
model = Device
fields = [
'name', 'role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster',
'latitude', 'longitude', 'status', 'airflow', 'cooling_method', 'platform', 'primary_ip4', 'primary_ip6',
'oob_ip', 'cluster',
'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
'owner', 'comments', 'tags', 'local_context_data',
]
@ -1153,6 +1165,74 @@ class PowerFeedForm(TenancyForm, PrimaryModelForm):
]
#
# Cooling
#
class CoolingSourceForm(PrimaryModelForm):
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
selector=True
)
location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
query_params={
'site_id': '$site'
}
)
fieldsets = (
FieldSet('site', 'location', 'name', 'type', 'status', 'description', 'tags', name=_('Cooling Source')),
FieldSet('fluid_type', 'cooling_capacity', name=_('Characteristics')),
)
class Meta:
model = CoolingSource
fields = [
'site', 'location', 'name', 'type', 'status', 'fluid_type', 'cooling_capacity', 'description', 'owner',
'comments', 'tags',
]
class CoolingFeedForm(TenancyForm, PrimaryModelForm):
cooling_source = DynamicModelChoiceField(
label=_('Cooling source'),
queryset=CoolingSource.objects.all(),
selector=True,
quick_add=True
)
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
required=False,
selector=True
)
fieldsets = (
FieldSet(
'cooling_source', 'rack', 'name', 'status', 'description', 'tags',
name=_('Cooling Feed')
),
FieldSet(
'cooling_capacity',
InlineFields('max_flow', 'max_flow_unit', label=_('Max flow')),
name=_('Characteristics')
),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
class Meta:
model = CoolingFeed
fields = [
'cooling_source', 'rack', 'name', 'status', 'cooling_capacity',
'max_flow', 'max_flow_unit', 'tenant_group', 'tenant', 'description', 'owner', 'comments',
'tags',
]
#
# Virtual chassis
#
@ -1394,6 +1474,58 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
]
class CoolingIntakeTemplateForm(ModularComponentTemplateForm):
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type',
InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
InlineFields('max_flow', 'max_flow_unit', label=_('Max flow')),
'description',
),
)
class Meta:
model = CoolingIntakeTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'diameter', 'diameter_unit',
'max_flow', 'max_flow_unit', 'description',
]
class CoolingOutflowTemplateForm(ModularComponentTemplateForm):
cooling_intake = DynamicModelChoiceField(
label=_('Cooling intake'),
queryset=CoolingIntakeTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type',
}
)
fieldsets = (
FieldSet(
TabbedGroups(
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type',
InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
'cooling_intake', 'description',
),
)
class Meta:
model = CoolingOutflowTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'diameter', 'diameter_unit',
'cooling_intake', 'description',
]
class InterfaceTemplateForm(ModularComponentTemplateForm):
type = ChoiceField(
label=_('Type'),
@ -1835,6 +1967,57 @@ class PowerOutletForm(ModularDeviceComponentForm):
]
class CoolingIntakeForm(ModularDeviceComponentForm):
cooling_outflow = DynamicModelChoiceField(
label=_('Cooling outflow'),
queryset=CoolingOutflow.objects.all(),
required=False,
selector=True
)
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type',
InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
InlineFields('max_flow', 'max_flow_unit', label=_('Max flow')),
'cooling_outflow', 'description', 'tags',
),
)
class Meta:
model = CoolingIntake
fields = [
'device', 'module', 'name', 'label', 'type', 'diameter', 'diameter_unit', 'max_flow',
'max_flow_unit', 'cooling_outflow', 'description', 'owner', 'tags',
]
class CoolingOutflowForm(ModularDeviceComponentForm):
cooling_intake = DynamicModelChoiceField(
label=_('Cooling intake'),
queryset=CoolingIntake.objects.all(),
required=False,
query_params={
'device_id': '$device',
}
)
fieldsets = (
FieldSet(
'device', 'module', 'name', 'label', 'type',
InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
'cooling_intake', 'description', 'tags',
),
)
class Meta:
model = CoolingOutflow
fields = [
'device', 'module', 'name', 'label', 'type', 'diameter', 'diameter_unit', 'cooling_intake', 'description',
'owner', 'tags',
]
class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
type = ChoiceField(
label=_('Type'),

View File

@ -21,6 +21,10 @@ __all__ = (
'ConsolePortTemplateCreateForm',
'ConsoleServerPortCreateForm',
'ConsoleServerPortTemplateCreateForm',
'CoolingIntakeCreateForm',
'CoolingIntakeTemplateCreateForm',
'CoolingOutflowCreateForm',
'CoolingOutflowTemplateCreateForm',
'DeviceBayCreateForm',
'DeviceBayTemplateCreateForm',
'FrontPortCreateForm',
@ -109,6 +113,18 @@ class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutlet
exclude = ('name', 'label')
class CoolingIntakeTemplateCreateForm(ComponentCreateForm, model_forms.CoolingIntakeTemplateForm):
class Meta(model_forms.CoolingIntakeTemplateForm.Meta):
exclude = ('name', 'label')
class CoolingOutflowTemplateCreateForm(ComponentCreateForm, model_forms.CoolingOutflowTemplateForm):
class Meta(model_forms.CoolingOutflowTemplateForm.Meta):
exclude = ('name', 'label')
class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemplateForm):
channel_id = ExpandableNumericField(
label=_('Channel ID'),
@ -206,6 +222,18 @@ class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
exclude = ('name', 'label')
class CoolingIntakeCreateForm(ComponentCreateForm, model_forms.CoolingIntakeForm):
class Meta(model_forms.CoolingIntakeForm.Meta):
exclude = ('name', 'label')
class CoolingOutflowCreateForm(ComponentCreateForm, model_forms.CoolingOutflowForm):
class Meta(model_forms.CoolingOutflowForm.Meta):
exclude = ('name', 'label')
class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
channel_id = ExpandableNumericField(
label=_('Channel ID'),

View File

@ -8,6 +8,8 @@ from wireless.choices import WirelessRoleChoices
__all__ = (
'ConsolePortTemplateImportForm',
'ConsoleServerPortTemplateImportForm',
'CoolingIntakeTemplateImportForm',
'CoolingOutflowTemplateImportForm',
'DeviceBayTemplateImportForm',
'FrontPortTemplateImportForm',
'InterfaceTemplateImportForm',
@ -80,6 +82,46 @@ class PowerOutletTemplateImportForm(forms.ModelForm):
return module_type
class CoolingIntakeTemplateImportForm(forms.ModelForm):
class Meta:
model = CoolingIntakeTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'diameter', 'diameter_unit',
'max_flow', 'max_flow_unit', 'description',
]
class CoolingOutflowTemplateImportForm(forms.ModelForm):
cooling_intake = forms.ModelChoiceField(
label=_('Cooling intake'),
queryset=CoolingIntakeTemplate.objects.all(),
to_field_name='name',
required=False
)
class Meta:
model = CoolingOutflowTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'diameter', 'diameter_unit',
'cooling_intake', 'description',
]
def clean_device_type(self):
if device_type := self.cleaned_data['device_type']:
cooling_intake = self.fields['cooling_intake']
cooling_intake.queryset = cooling_intake.queryset.filter(device_type=device_type)
return device_type
def clean_module_type(self):
if module_type := self.cleaned_data['module_type']:
cooling_intake = self.fields['cooling_intake']
cooling_intake.queryset = cooling_intake.queryset.filter(module_type=module_type)
return module_type
class InterfaceTemplateImportForm(forms.ModelForm):
type = forms.ChoiceField(
label=_('Type'),

View File

@ -1,6 +1,7 @@
import strawberry
from dcim.choices import *
from netbox.choices import DiameterUnitChoices, FlowRateUnitChoices
__all__ = (
'CableEndEnum',
@ -8,9 +9,17 @@ __all__ = (
'CableTypeEnum',
'ConsolePortSpeedEnum',
'ConsolePortTypeEnum',
'CoolingConnectorTypeEnum',
'CoolingFeedStatusEnum',
'CoolingMethodEnum',
'CoolingSourceStatusEnum',
'CoolingSourceTypeEnum',
'DeviceAirflowEnum',
'DeviceFaceEnum',
'DeviceStatusEnum',
'DiameterUnitEnum',
'FlowRateUnitEnum',
'FluidTypeEnum',
'InterfaceDuplexEnum',
'InterfaceKindEnum',
'InterfaceModeEnum',
@ -32,6 +41,7 @@ __all__ = (
'PowerOutletTypeEnum',
'PowerPortTypeEnum',
'RackAirflowEnum',
'RackCoolingCapabilityEnum',
'RackDimensionUnitEnum',
'RackFormFactorEnum',
'RackReservationStatusEnum',
@ -47,9 +57,17 @@ CableLengthUnitEnum = strawberry.enum(CableLengthUnitChoices.as_enum(prefix='uni
CableTypeEnum = strawberry.enum(CableTypeChoices.as_enum(prefix='type'))
ConsolePortSpeedEnum = strawberry.enum(ConsolePortSpeedChoices.as_enum(prefix='speed'))
ConsolePortTypeEnum = strawberry.enum(ConsolePortTypeChoices.as_enum(prefix='type'))
CoolingConnectorTypeEnum = strawberry.enum(CoolingConnectorTypeChoices.as_enum(prefix='type'))
CoolingFeedStatusEnum = strawberry.enum(CoolingFeedStatusChoices.as_enum(prefix='status'))
CoolingMethodEnum = strawberry.enum(CoolingMethodChoices.as_enum(prefix='method'))
CoolingSourceStatusEnum = strawberry.enum(CoolingSourceStatusChoices.as_enum(prefix='status'))
CoolingSourceTypeEnum = strawberry.enum(CoolingSourceTypeChoices.as_enum(prefix='type'))
DeviceAirflowEnum = strawberry.enum(DeviceAirflowChoices.as_enum(prefix='airflow'))
DeviceFaceEnum = strawberry.enum(DeviceFaceChoices.as_enum(prefix='face'))
DeviceStatusEnum = strawberry.enum(DeviceStatusChoices.as_enum(prefix='status'))
DiameterUnitEnum = strawberry.enum(DiameterUnitChoices.as_enum(prefix='unit'))
FlowRateUnitEnum = strawberry.enum(FlowRateUnitChoices.as_enum(prefix='unit'))
FluidTypeEnum = strawberry.enum(FluidTypeChoices.as_enum(prefix='fluid'))
InterfaceDuplexEnum = strawberry.enum(InterfaceDuplexChoices.as_enum(prefix='duplex'))
InterfaceKindEnum = strawberry.enum(InterfaceKindChoices.as_enum(prefix='kind'))
InterfaceModeEnum = strawberry.enum(InterfaceModeChoices.as_enum(prefix='mode'))
@ -71,6 +89,7 @@ PowerOutletStatusEnum = strawberry.enum(PowerOutletStatusChoices.as_enum(prefix=
PowerOutletTypeEnum = strawberry.enum(PowerOutletTypeChoices.as_enum(prefix='type'))
PowerPortTypeEnum = strawberry.enum(PowerPortTypeChoices.as_enum(prefix='type'))
RackAirflowEnum = strawberry.enum(RackAirflowChoices.as_enum())
RackCoolingCapabilityEnum = strawberry.enum(RackCoolingCapabilityChoices.as_enum(prefix='capability'))
RackDimensionUnitEnum = strawberry.enum(RackDimensionUnitChoices.as_enum(prefix='unit'))
RackFormFactorEnum = strawberry.enum(RackFormFactorChoices.as_enum(prefix='type'))
RackReservationStatusEnum = strawberry.enum(RackReservationStatusChoices.as_enum(prefix='status'))

View File

@ -14,7 +14,7 @@ if TYPE_CHECKING:
from dcim.graphql.filters import LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
from extras.graphql.filters import ConfigTemplateFilter
from ipam.graphql.filters import VLANFilter, VLANTranslationPolicyFilter
from netbox.graphql.filter_lookups import IntegerLookup
from netbox.graphql.filter_lookups import FloatLookup, IntegerLookup
from .filters import *
@ -177,3 +177,9 @@ class RackFilterMixin:
max_weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
cooling_capability: (
BaseFilterLookup[Annotated['RackCoolingCapabilityEnum', strawberry.lazy('dcim.graphql.enums')]] | None
) = strawberry_django.filter_field()
cooling_capacity: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)

View File

@ -77,6 +77,12 @@ __all__ = (
'ConsolePortTemplateFilter',
'ConsoleServerPortFilter',
'ConsoleServerPortTemplateFilter',
'CoolingFeedFilter',
'CoolingIntakeFilter',
'CoolingIntakeTemplateFilter',
'CoolingOutflowFilter',
'CoolingOutflowTemplateFilter',
'CoolingSourceFilter',
'DeviceBayFilter',
'DeviceBayTemplateFilter',
'DeviceFilter',
@ -256,6 +262,9 @@ class DeviceFilter(
airflow: BaseFilterLookup[Annotated['DeviceAirflowEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
cooling_method: BaseFilterLookup[Annotated['CoolingMethodEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
primary_ip4: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field()
)
@ -399,6 +408,9 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
airflow: BaseFilterLookup[Annotated['DeviceAirflowEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
cooling_method: BaseFilterLookup[Annotated['CoolingMethodEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
end_of_life: DateFilterLookup | None = strawberry_django.filter_field()
front_image: Annotated['ImageAttachmentFilter', strawberry.lazy('extras.graphql.filters')] | None = (
strawberry_django.filter_field()
@ -828,6 +840,9 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
airflow: BaseFilterLookup[Annotated['ModuleAirflowEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
cooling_method: BaseFilterLookup[Annotated['CoolingMethodEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
end_of_life: DateFilterLookup | None = strawberry_django.filter_field()
consoleporttemplates: Annotated['ConsolePortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='console_port_templates')
@ -979,6 +994,126 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedM
)
@register_filter(models.CoolingFeed, lookups=True)
class CoolingFeedFilter(TenancyFilterMixin, PrimaryModelFilter):
cooling_source: Annotated['CoolingSourceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
cooling_source_id: ID | None = strawberry_django.filter_field()
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
rack_id: ID | None = strawberry_django.filter_field()
name: StrFilterLookup | None = strawberry_django.filter_field()
status: BaseFilterLookup[Annotated['CoolingFeedStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
cooling_capacity: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
max_flow: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
max_flow_unit: (
BaseFilterLookup[Annotated['FlowRateUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None
) = strawberry_django.filter_field()
@register_filter(models.CoolingOutflow, lookups=True)
class CoolingOutflowFilter(ModularComponentFilterMixin, NetBoxModelFilter):
type: BaseFilterLookup[
Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
] | None = strawberry_django.filter_field()
diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
cooling_intake: Annotated['CoolingIntakeFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
cooling_intake_id: ID | None = strawberry_django.filter_field()
@register_filter(models.CoolingOutflowTemplate, lookups=True)
class CoolingOutflowTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
type: BaseFilterLookup[
Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
] | None = strawberry_django.filter_field()
diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
cooling_intake: Annotated['CoolingIntakeTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
cooling_intake_id: ID | None = strawberry_django.filter_field()
@register_filter(models.CoolingIntake, lookups=True)
class CoolingIntakeFilter(ModularComponentFilterMixin, NetBoxModelFilter):
type: BaseFilterLookup[
Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
] | None = strawberry_django.filter_field()
diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
max_flow: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
max_flow_unit: BaseFilterLookup[
Annotated['FlowRateUnitEnum', strawberry.lazy('dcim.graphql.enums')]
] | None = strawberry_django.filter_field()
@register_filter(models.CoolingIntakeTemplate, lookups=True)
class CoolingIntakeTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
type: BaseFilterLookup[
Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
] | None = strawberry_django.filter_field()
diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
max_flow: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
max_flow_unit: BaseFilterLookup[
Annotated['FlowRateUnitEnum', strawberry.lazy('dcim.graphql.enums')]
] | None = strawberry_django.filter_field()
@register_filter(models.CoolingSource, lookups=True)
class CoolingSourceFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilter):
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field()
location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field()
)
location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
name: StrFilterLookup | None = strawberry_django.filter_field()
type: BaseFilterLookup[Annotated['CoolingSourceTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
status: BaseFilterLookup[Annotated['CoolingSourceStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
fluid_type: BaseFilterLookup[Annotated['FluidTypeEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
strawberry_django.filter_field()
)
cooling_capacity: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field()
)
@register_filter(models.RackType, lookups=True)
class RackTypeFilter(ImageAttachmentFilterMixin, RackFilterMixin, WeightFilterMixin, PrimaryModelFilter):
form_factor: BaseFilterLookup[Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (

View File

@ -24,6 +24,24 @@ class DCIMQuery:
console_server_port_template: ConsoleServerPortTemplateType = strawberry_django.field()
console_server_port_template_list: list[ConsoleServerPortTemplateType] = strawberry_django.field()
cooling_feed: CoolingFeedType = strawberry_django.field()
cooling_feed_list: list[CoolingFeedType] = strawberry_django.field()
cooling_intake: CoolingIntakeType = strawberry_django.field()
cooling_intake_list: list[CoolingIntakeType] = strawberry_django.field()
cooling_intake_template: CoolingIntakeTemplateType = strawberry_django.field()
cooling_intake_template_list: list[CoolingIntakeTemplateType] = strawberry_django.field()
cooling_outflow: CoolingOutflowType = strawberry_django.field()
cooling_outflow_list: list[CoolingOutflowType] = strawberry_django.field()
cooling_outflow_template: CoolingOutflowTemplateType = strawberry_django.field()
cooling_outflow_template_list: list[CoolingOutflowTemplateType] = strawberry_django.field()
cooling_source: CoolingSourceType = strawberry_django.field()
cooling_source_list: list[CoolingSourceType] = strawberry_django.field()
device: DeviceType = strawberry_django.field()
device_list: list[DeviceType] = strawberry_django.field()

View File

@ -52,6 +52,12 @@ __all__ = (
'ConsolePortType',
'ConsoleServerPortTemplateType',
'ConsoleServerPortType',
'CoolingFeedType',
'CoolingIntakeTemplateType',
'CoolingIntakeType',
'CoolingOutflowTemplateType',
'CoolingOutflowType',
'CoolingSourceType',
'DeviceBayTemplateType',
'DeviceBayType',
'DeviceRoleType',
@ -258,6 +264,8 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, Prima
console_server_port_count: BigInt
power_port_count: BigInt
power_outlet_count: BigInt
cooling_intake_count: BigInt
cooling_outflow_count: BigInt
interface_count: BigInt
front_port_count: BigInt
rear_port_count: BigInt
@ -284,9 +292,11 @@ class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, Prima
rearports: list[Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]]
consoleports: list[Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')]]
powerports: list[Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')]]
coolingintakes: list[Annotated["CoolingIntakeType", strawberry.lazy('dcim.graphql.types')]]
cabletermination_set: list[Annotated["CableTerminationType", strawberry.lazy('dcim.graphql.types')]]
consoleserverports: list[Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')]]
poweroutlets: list[Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')]]
coolingoutflows: list[Annotated["CoolingOutflowType", strawberry.lazy('dcim.graphql.types')]]
frontports: list[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]
devicebays: list[Annotated["DeviceBayType", strawberry.lazy('dcim.graphql.types')]]
modulebays: list[Annotated["ModuleBayType", strawberry.lazy('dcim.graphql.types')]]
@ -378,6 +388,8 @@ class DeviceTypeType(PrimaryObjectType):
console_server_port_template_count: BigInt
power_port_template_count: BigInt
power_outlet_template_count: BigInt
cooling_intake_template_count: BigInt
cooling_outflow_template_count: BigInt
interface_template_count: BigInt
front_port_template_count: BigInt
rear_port_template_count: BigInt
@ -395,6 +407,8 @@ class DeviceTypeType(PrimaryObjectType):
instances: list[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
poweroutlettemplates: list[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
powerporttemplates: list[Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
coolingoutflowtemplates: list[Annotated["CoolingOutflowTemplateType", strawberry.lazy('dcim.graphql.types')]]
coolingintaketemplates: list[Annotated["CoolingIntakeTemplateType", strawberry.lazy('dcim.graphql.types')]]
inventoryitemtemplates: list[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]
rearporttemplates: list[Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
consoleserverporttemplates: list[Annotated["ConsoleServerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
@ -597,9 +611,11 @@ class ModuleType(PrimaryObjectType):
interfaces: list[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
powerports: list[Annotated["PowerPortType", strawberry.lazy('dcim.graphql.types')]]
coolingintakes: list[Annotated["CoolingIntakeType", strawberry.lazy('dcim.graphql.types')]]
consoleserverports: list[Annotated["ConsoleServerPortType", strawberry.lazy('dcim.graphql.types')]]
consoleports: list[Annotated["ConsolePortType", strawberry.lazy('dcim.graphql.types')]]
poweroutlets: list[Annotated["PowerOutletType", strawberry.lazy('dcim.graphql.types')]]
coolingoutflows: list[Annotated["CoolingOutflowType", strawberry.lazy('dcim.graphql.types')]]
rearports: list[Annotated["RearPortType", strawberry.lazy('dcim.graphql.types')]]
frontports: list[Annotated["FrontPortType", strawberry.lazy('dcim.graphql.types')]]
@ -668,6 +684,8 @@ class ModuleTypeType(PrimaryObjectType):
console_server_port_template_count: BigInt
power_port_template_count: BigInt
power_outlet_template_count: BigInt
cooling_intake_template_count: BigInt
cooling_outflow_template_count: BigInt
interface_template_count: BigInt
front_port_template_count: BigInt
rear_port_template_count: BigInt
@ -681,6 +699,8 @@ class ModuleTypeType(PrimaryObjectType):
interfacetemplates: list[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]
powerporttemplates: list[Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
poweroutlettemplates: list[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
coolingintaketemplates: list[Annotated["CoolingIntakeTemplateType", strawberry.lazy('dcim.graphql.types')]]
coolingoutflowtemplates: list[Annotated["CoolingOutflowTemplateType", strawberry.lazy('dcim.graphql.types')]]
rearporttemplates: list[Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
instances: list[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
consoleporttemplates: list[Annotated["ConsolePortTemplateType", strawberry.lazy('dcim.graphql.types')]]
@ -792,6 +812,73 @@ class PowerPortTemplateType(ModularComponentTemplateType):
poweroutlet_templates: list[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
@register_type(
models.CoolingFeed,
exclude=['_abs_max_flow'],
filters=CoolingFeedFilter,
pagination=True
)
class CoolingFeedType(PrimaryObjectType):
cooling_source: Annotated["CoolingSourceType", strawberry.lazy('dcim.graphql.types')]
rack: Annotated["RackType", strawberry.lazy('dcim.graphql.types')] | None
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
@register_type(
models.CoolingOutflow,
exclude=['_abs_diameter'],
filters=CoolingOutflowFilter,
pagination=True
)
class CoolingOutflowType(ModularComponentType):
cooling_intake: Annotated["CoolingIntakeType", strawberry.lazy('dcim.graphql.types')] | None
@register_type(
models.CoolingOutflowTemplate,
exclude=['_abs_diameter'],
filters=CoolingOutflowTemplateFilter,
pagination=True
)
class CoolingOutflowTemplateType(ModularComponentTemplateType):
cooling_intake: Annotated["CoolingIntakeTemplateType", strawberry.lazy('dcim.graphql.types')] | None
@register_type(
models.CoolingIntake,
exclude=['_abs_diameter', '_abs_max_flow'],
filters=CoolingIntakeFilter,
pagination=True
)
class CoolingIntakeType(ModularComponentType):
cooling_outflow: Annotated["CoolingOutflowType", strawberry.lazy('dcim.graphql.types')] | None
coolingoutflows: list[Annotated["CoolingOutflowType", strawberry.lazy('dcim.graphql.types')]]
@register_type(
models.CoolingIntakeTemplate,
exclude=['_abs_diameter', '_abs_max_flow'],
filters=CoolingIntakeTemplateFilter,
pagination=True
)
class CoolingIntakeTemplateType(ModularComponentTemplateType):
coolingoutflow_templates: list[Annotated["CoolingOutflowTemplateType", strawberry.lazy('dcim.graphql.types')]]
@register_type(
models.CoolingSource,
fields='__all__',
filters=CoolingSourceFilter,
pagination=True
)
class CoolingSourceType(ContactsMixin, PrimaryObjectType):
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
cooling_feeds: list[Annotated["CoolingFeedType", strawberry.lazy('dcim.graphql.types')]]
@register_type(
models.RackGroup,
fields='__all__',

View File

@ -0,0 +1,836 @@
from decimal import Decimal
import django.core.validators
import django.db.models.deletion
import taggit.managers
from django.db import migrations, models
import netbox.models.deletion
import utilities.fields
import utilities.json
import utilities.tracking
from utilities.migration import InstallDenormalizationTrigger
class Migration(migrations.Migration):
dependencies = [
("dcim", "0249_interface_channels"),
("extras", "0139_alter_customfieldchoiceset_extra_choices"),
("tenancy", "0025_ltree_paths"),
("users", "0016_default_ordering_indexes"),
]
operations = [
migrations.AddField(
model_name="device",
name="cooling_method",
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AddField(
model_name="device",
name="cooling_outflow_count",
field=utilities.fields.CounterCacheField(
default=0,
editable=False,
to_field="device",
to_model="dcim.CoolingOutflow",
),
),
migrations.AddField(
model_name="device",
name="cooling_intake_count",
field=utilities.fields.CounterCacheField(
default=0,
editable=False,
to_field="device",
to_model="dcim.CoolingIntake",
),
),
migrations.AddField(
model_name="devicetype",
name="cooling_method",
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AddField(
model_name="devicetype",
name="cooling_outflow_template_count",
field=utilities.fields.CounterCacheField(
default=0,
editable=False,
to_field="device_type",
to_model="dcim.CoolingOutflowTemplate",
),
),
migrations.AddField(
model_name="devicetype",
name="cooling_intake_template_count",
field=utilities.fields.CounterCacheField(
default=0,
editable=False,
to_field="device_type",
to_model="dcim.CoolingIntakeTemplate",
),
),
migrations.AddField(
model_name="moduletype",
name="cooling_method",
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AddField(
model_name="moduletype",
name="cooling_outflow_template_count",
field=utilities.fields.CounterCacheField(
default=0,
editable=False,
to_field="module_type",
to_model="dcim.CoolingOutflowTemplate",
),
),
migrations.AddField(
model_name="moduletype",
name="cooling_intake_template_count",
field=utilities.fields.CounterCacheField(
default=0,
editable=False,
to_field="module_type",
to_model="dcim.CoolingIntakeTemplate",
),
),
migrations.AddField(
model_name="rack",
name="cooling_capability",
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AddField(
model_name="rack",
name="cooling_capacity",
field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=10,
null=True,
validators=[django.core.validators.MinValueValidator(0)],
),
),
migrations.AddField(
model_name="racktype",
name="cooling_capability",
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AddField(
model_name="racktype",
name="cooling_capacity",
field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=10,
null=True,
validators=[django.core.validators.MinValueValidator(0)],
),
),
migrations.CreateModel(
name="CoolingIntake",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("created", models.DateTimeField(auto_now_add=True, null=True)),
("last_updated", models.DateTimeField(auto_now=True, null=True)),
(
"custom_field_data",
models.JSONField(
blank=True,
default=dict,
encoder=utilities.json.CustomFieldJSONEncoder,
),
),
(
"diameter",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=8,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('0.01'))],
),
),
(
"diameter_unit",
models.CharField(blank=True, max_length=50, null=True),
),
(
"_abs_diameter",
models.DecimalField(
blank=True, decimal_places=4, max_digits=13, null=True
),
),
(
"max_flow",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=8,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('0.01'))],
),
),
(
"max_flow_unit",
models.CharField(blank=True, max_length=50, null=True),
),
(
"_abs_max_flow",
models.DecimalField(
blank=True, decimal_places=4, max_digits=13, null=True
),
),
("name", models.CharField(db_collation="natural_sort", max_length=64)),
("label", models.CharField(blank=True, max_length=64)),
("description", models.CharField(blank=True, max_length=200)),
("type", models.CharField(blank=True, max_length=50, null=True)),
(
"_location",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="dcim.location",
),
),
(
"_rack",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="dcim.rack",
),
),
(
"_site",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="dcim.site",
),
),
(
"device",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="dcim.device",
),
),
(
"module",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="dcim.module",
),
),
(
"owner",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="users.owner",
),
),
(
"tags",
taggit.managers.TaggableManager(
through="extras.TaggedItem", to="extras.Tag"
),
),
],
options={
"verbose_name": "cooling intake",
"verbose_name_plural": "cooling intakes",
"ordering": ("device", "name"),
"abstract": False,
},
bases=(
netbox.models.deletion.DeleteMixin,
models.Model,
utilities.tracking.TrackingModelMixin,
),
),
migrations.CreateModel(
name="CoolingOutflow",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("created", models.DateTimeField(auto_now_add=True, null=True)),
("last_updated", models.DateTimeField(auto_now=True, null=True)),
(
"custom_field_data",
models.JSONField(
blank=True,
default=dict,
encoder=utilities.json.CustomFieldJSONEncoder,
),
),
(
"diameter",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=8,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('0.01'))],
),
),
(
"diameter_unit",
models.CharField(blank=True, max_length=50, null=True),
),
(
"_abs_diameter",
models.DecimalField(
blank=True, decimal_places=4, max_digits=13, null=True
),
),
("name", models.CharField(db_collation="natural_sort", max_length=64)),
("label", models.CharField(blank=True, max_length=64)),
("description", models.CharField(blank=True, max_length=200)),
("type", models.CharField(blank=True, max_length=50, null=True)),
(
"_location",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="dcim.location",
),
),
(
"_rack",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="dcim.rack",
),
),
(
"_site",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="dcim.site",
),
),
(
"device",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="dcim.device",
),
),
(
"module",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="dcim.module",
),
),
(
"owner",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="users.owner",
),
),
(
"tags",
taggit.managers.TaggableManager(
through="extras.TaggedItem", to="extras.Tag"
),
),
(
"cooling_intake",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="coolingoutflows",
to="dcim.coolingintake",
),
),
],
options={
"verbose_name": "cooling outflow",
"verbose_name_plural": "cooling outflows",
"ordering": ("device", "name"),
"abstract": False,
},
bases=(
netbox.models.deletion.DeleteMixin,
models.Model,
utilities.tracking.TrackingModelMixin,
),
),
migrations.CreateModel(
name="CoolingIntakeTemplate",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("created", models.DateTimeField(auto_now_add=True, null=True)),
("last_updated", models.DateTimeField(auto_now=True, null=True)),
(
"diameter",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=8,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('0.01'))],
),
),
(
"diameter_unit",
models.CharField(blank=True, max_length=50, null=True),
),
(
"_abs_diameter",
models.DecimalField(
blank=True, decimal_places=4, max_digits=13, null=True
),
),
(
"max_flow",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=8,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('0.01'))],
),
),
(
"max_flow_unit",
models.CharField(blank=True, max_length=50, null=True),
),
(
"_abs_max_flow",
models.DecimalField(
blank=True, decimal_places=4, max_digits=13, null=True
),
),
("name", models.CharField(db_collation="natural_sort", max_length=64)),
("label", models.CharField(blank=True, max_length=64)),
("description", models.CharField(blank=True, max_length=200)),
("type", models.CharField(blank=True, max_length=50, null=True)),
(
"device_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="dcim.devicetype",
),
),
(
"module_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="dcim.moduletype",
),
),
],
options={
"verbose_name": "cooling intake template",
"verbose_name_plural": "cooling intake templates",
"ordering": ("device_type", "module_type", "name"),
"abstract": False,
},
bases=(
netbox.models.deletion.DeleteMixin,
models.Model,
utilities.tracking.TrackingModelMixin,
),
),
migrations.CreateModel(
name="CoolingOutflowTemplate",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("created", models.DateTimeField(auto_now_add=True, null=True)),
("last_updated", models.DateTimeField(auto_now=True, null=True)),
(
"diameter",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=8,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('0.01'))],
),
),
(
"diameter_unit",
models.CharField(blank=True, max_length=50, null=True),
),
(
"_abs_diameter",
models.DecimalField(
blank=True, decimal_places=4, max_digits=13, null=True
),
),
("name", models.CharField(db_collation="natural_sort", max_length=64)),
("label", models.CharField(blank=True, max_length=64)),
("description", models.CharField(blank=True, max_length=200)),
("type", models.CharField(blank=True, max_length=50, null=True)),
(
"device_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="dcim.devicetype",
),
),
(
"module_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)ss",
to="dcim.moduletype",
),
),
(
"cooling_intake",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="coolingoutflow_templates",
to="dcim.coolingintaketemplate",
),
),
],
options={
"verbose_name": "cooling outflow template",
"verbose_name_plural": "cooling outflow templates",
"ordering": ("device_type", "module_type", "name"),
"abstract": False,
},
bases=(
netbox.models.deletion.DeleteMixin,
models.Model,
utilities.tracking.TrackingModelMixin,
),
),
migrations.CreateModel(
name="CoolingSource",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("created", models.DateTimeField(auto_now_add=True, null=True)),
("last_updated", models.DateTimeField(auto_now=True, null=True)),
(
"custom_field_data",
models.JSONField(
blank=True,
default=dict,
encoder=utilities.json.CustomFieldJSONEncoder,
),
),
("description", models.CharField(blank=True, max_length=200)),
("comments", models.TextField(blank=True)),
("name", models.CharField(db_collation="natural_sort", max_length=100)),
("type", models.CharField(max_length=50)),
("status", models.CharField(default="active", max_length=50)),
("fluid_type", models.CharField(blank=True, max_length=50, null=True)),
(
"cooling_capacity",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=10,
null=True,
validators=[django.core.validators.MinValueValidator(0)],
),
),
(
"location",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="dcim.location",
),
),
(
"owner",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="users.owner",
),
),
(
"site",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="dcim.site"
),
),
(
"tags",
taggit.managers.TaggableManager(
through="extras.TaggedItem", to="extras.Tag"
),
),
],
options={
"verbose_name": "cooling source",
"verbose_name_plural": "cooling sources",
"ordering": ["site", "name"],
},
bases=(netbox.models.deletion.DeleteMixin, models.Model),
),
migrations.CreateModel(
name="CoolingFeed",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("created", models.DateTimeField(auto_now_add=True, null=True)),
("last_updated", models.DateTimeField(auto_now=True, null=True)),
(
"custom_field_data",
models.JSONField(
blank=True,
default=dict,
encoder=utilities.json.CustomFieldJSONEncoder,
),
),
(
"max_flow",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=8,
null=True,
validators=[django.core.validators.MinValueValidator(Decimal('0.01'))],
),
),
(
"max_flow_unit",
models.CharField(blank=True, max_length=50, null=True),
),
(
"_abs_max_flow",
models.DecimalField(
blank=True, decimal_places=4, max_digits=13, null=True
),
),
("description", models.CharField(blank=True, max_length=200)),
("comments", models.TextField(blank=True)),
("name", models.CharField(db_collation="natural_sort", max_length=100)),
("status", models.CharField(default="active", max_length=50)),
(
"cooling_capacity",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=10,
null=True,
validators=[django.core.validators.MinValueValidator(0)],
),
),
(
"owner",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="users.owner",
),
),
(
"rack",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="cooling_feeds",
to="dcim.rack",
),
),
(
"tags",
taggit.managers.TaggableManager(
through="extras.TaggedItem", to="extras.Tag"
),
),
(
"tenant",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="cooling_feeds",
to="tenancy.tenant",
),
),
(
"cooling_source",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="cooling_feeds",
to="dcim.coolingsource",
),
),
],
options={
"verbose_name": "cooling feed",
"verbose_name_plural": "cooling feeds",
"ordering": ["cooling_source", "name"],
},
bases=(netbox.models.deletion.DeleteMixin, models.Model),
),
migrations.AddField(
model_name="coolingintake",
name="cooling_outflow",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="coolingintakes",
to="dcim.coolingoutflow",
),
),
migrations.AddConstraint(
model_name="coolingintake",
constraint=models.UniqueConstraint(
fields=("device", "name"), name="dcim_coolingintake_unique_device_name"
),
),
migrations.AddConstraint(
model_name="coolingoutflow",
constraint=models.UniqueConstraint(
fields=("device", "name"), name="dcim_coolingoutflow_unique_device_name"
),
),
migrations.AddIndex(
model_name="coolingintaketemplate",
index=models.Index(
fields=["device_type", "module_type", "name"],
name="dcim_coolin_device__ee88f9_idx",
),
),
migrations.AddConstraint(
model_name="coolingintaketemplate",
constraint=models.UniqueConstraint(
fields=("device_type", "name"),
name="dcim_coolingintaketemplate_unique_device_type_name",
),
),
migrations.AddConstraint(
model_name="coolingintaketemplate",
constraint=models.UniqueConstraint(
fields=("module_type", "name"),
name="dcim_coolingintaketemplate_unique_module_type_name",
),
),
migrations.AddIndex(
model_name="coolingoutflowtemplate",
index=models.Index(
fields=["device_type", "module_type", "name"],
name="dcim_coolin_device__4d0859_idx",
),
),
migrations.AddConstraint(
model_name="coolingoutflowtemplate",
constraint=models.UniqueConstraint(
fields=("device_type", "name"),
name="dcim_coolingoutflowtemplate_unique_device_type_name",
),
),
migrations.AddConstraint(
model_name="coolingoutflowtemplate",
constraint=models.UniqueConstraint(
fields=("module_type", "name"),
name="dcim_coolingoutflowtemplate_unique_module_type_name",
),
),
migrations.AddConstraint(
model_name="coolingsource",
constraint=models.UniqueConstraint(
fields=("site", "name"), name="dcim_coolingsource_unique_site_name"
),
),
migrations.AddConstraint(
model_name="coolingfeed",
constraint=models.UniqueConstraint(
fields=("cooling_source", "name"),
name="dcim_coolingfeed_unique_cooling_source_name",
),
),
# Install denormalized device → component triggers for the cooling device components,
# mirroring the triggers created for the other device components in migration 0243.
*[
InstallDenormalizationTrigger(
dependent_table=table,
source_table="dcim_device",
fk_column="device_id",
mappings={"_site_id": "site_id", "_location_id": "location_id", "_rack_id": "rack_id"},
)
for table in ("dcim_coolingintake", "dcim_coolingoutflow")
],
]

View File

@ -1,4 +1,5 @@
from .cables import *
from .cooling import *
from .device_component_templates import *
from .device_components import *
from .devices import *

View File

@ -0,0 +1,193 @@
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.models.mixins import MaxFlowMixin
from netbox.models import PrimaryModel
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
__all__ = (
'CoolingFeed',
'CoolingSource',
)
#
# Cooling
#
class CoolingSource(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
"""
A facility-level source of cooling; e.g. a chiller, cooling tower, or dry cooler.
"""
site = models.ForeignKey(
to='Site',
on_delete=models.PROTECT
)
location = models.ForeignKey(
to='dcim.Location',
on_delete=models.PROTECT,
blank=True,
null=True
)
name = models.CharField(
verbose_name=_('name'),
max_length=100,
db_collation="natural_sort"
)
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=CoolingSourceTypeChoices
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=CoolingSourceStatusChoices,
default=CoolingSourceStatusChoices.STATUS_ACTIVE
)
fluid_type = models.CharField(
verbose_name=_('fluid type'),
max_length=50,
choices=FluidTypeChoices,
blank=True,
null=True
)
cooling_capacity = models.DecimalField(
verbose_name=_('cooling capacity'),
max_digits=10,
decimal_places=2,
blank=True,
null=True,
validators=[MinValueValidator(0)],
help_text=_('Total rated cooling capacity (kW)')
)
clone_fields = (
'site', 'location', 'type', 'status', 'fluid_type', 'cooling_capacity',
)
prerequisite_models = (
'dcim.Site',
)
class Meta:
ordering = ['site', 'name']
constraints = (
models.UniqueConstraint(
fields=('site', 'name'),
name='%(app_label)s_%(class)s_unique_site_name'
),
)
verbose_name = _('cooling source')
verbose_name_plural = _('cooling sources')
def __str__(self):
return self.name
def get_status_color(self):
return CoolingSourceStatusChoices.colors.get(self.status)
def clean(self):
super().clean()
# Location must belong to assigned Site
if self.location and self.location.site != self.site:
raise ValidationError(
_("Location {location} ({location_site}) is in a different site than {site}").format(
location=self.location, location_site=self.location.site, site=self.site)
)
class CoolingFeed(MaxFlowMixin, PrimaryModel):
"""
A coolant loop delivered from a CoolingSource to a rack or CDU. A single feed represents the entire
loop (both the supply and return paths). A CoolingFeed serves a rack; the cooling intakes it supplies
are derived from the devices installed in that rack rather than referenced explicitly.
Maximum flow is a rated specification (the flow the loop supports), not live telemetry; runtime
readings belong in an external monitoring system.
"""
cooling_source = models.ForeignKey(
to='CoolingSource',
on_delete=models.PROTECT,
related_name='cooling_feeds'
)
rack = models.ForeignKey(
to='Rack',
on_delete=models.PROTECT,
related_name='cooling_feeds',
blank=True,
null=True
)
name = models.CharField(
verbose_name=_('name'),
max_length=100,
db_collation="natural_sort"
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=CoolingFeedStatusChoices,
default=CoolingFeedStatusChoices.STATUS_ACTIVE
)
cooling_capacity = models.DecimalField(
verbose_name=_('cooling capacity'),
max_digits=10,
decimal_places=2,
blank=True,
null=True,
validators=[MinValueValidator(0)],
help_text=_('Rated cooling capacity (kW)')
)
# max_flow, max_flow_unit, _abs_max_flow provided by MaxFlowMixin
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='cooling_feeds',
blank=True,
null=True
)
clone_fields = (
'cooling_source', 'rack', 'status', 'cooling_capacity', 'max_flow', 'max_flow_unit', 'tenant',
)
prerequisite_models = (
'dcim.CoolingSource',
)
class Meta:
ordering = ['cooling_source', 'name']
constraints = (
models.UniqueConstraint(
fields=('cooling_source', 'name'),
name='%(app_label)s_%(class)s_unique_cooling_source_name'
),
)
verbose_name = _('cooling feed')
verbose_name_plural = _('cooling feeds')
def __str__(self):
return self.name
def clean(self):
super().clean()
# Rack must belong to same Site as CoolingSource
if self.rack and self.rack.site != self.cooling_source.site:
raise ValidationError(_(
"Rack {rack} ({rack_site}) and cooling source {source} ({source_site}) are in different sites."
).format(
rack=self.rack,
rack_site=self.rack.site,
source=self.cooling_source,
source_site=self.cooling_source.site
))
@property
def parent_object(self):
return self.cooling_source
def get_status_color(self):
return CoolingFeedStatusChoices.colors.get(self.status)

View File

@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
from dcim.models.base import PortMappingBase
from dcim.models.mixins import InterfaceValidationMixin
from dcim.models.mixins import DiameterMixin, InterfaceValidationMixin, MaxFlowMixin
from dcim.utils import get_module_bay_positions, resolve_module_placeholder
from netbox.models import ChangeLoggedModel
from netbox.models.features import ChangeLoggingMixin
@ -22,6 +22,8 @@ from wireless.choices import WirelessRoleChoices
from .device_components import (
ConsolePort,
ConsoleServerPort,
CoolingIntake,
CoolingOutflow,
DeviceBay,
FrontPort,
Interface,
@ -35,6 +37,8 @@ from .device_components import (
__all__ = (
'ConsolePortTemplate',
'ConsoleServerPortTemplate',
'CoolingIntakeTemplate',
'CoolingOutflowTemplate',
'DeviceBayTemplate',
'FrontPortTemplate',
'InterfaceTemplate',
@ -438,6 +442,132 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
}
class CoolingIntakeTemplate(DiameterMixin, MaxFlowMixin, ModularComponentTemplateModel):
"""
A template for a CoolingIntake to be created for a new Device.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=CoolingConnectorTypeChoices,
blank=True,
null=True
)
# diameter, diameter_unit, _abs_diameter provided by DiameterMixin
# max_flow, max_flow_unit, _abs_max_flow provided by MaxFlowMixin
component_model = CoolingIntake
class Meta(ModularComponentTemplateModel.Meta):
verbose_name = _('cooling intake template')
verbose_name_plural = _('cooling intake templates')
def instantiate(self, **kwargs):
component = self.component_model(
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
type=self.type,
diameter=self.diameter,
diameter_unit=self.diameter_unit,
max_flow=self.max_flow,
max_flow_unit=self.max_flow_unit,
**kwargs
)
# bulk_create bypasses save(), so populate the normalized _abs_* fields here
component.normalize_diameter()
component.normalize_max_flow()
return component
instantiate.do_not_call_in_templates = True
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'diameter': float(self.diameter) if self.diameter is not None else None,
'diameter_unit': self.diameter_unit,
'max_flow': float(self.max_flow) if self.max_flow is not None else None,
'max_flow_unit': self.max_flow_unit,
'label': self.label,
'description': self.description,
}
class CoolingOutflowTemplate(DiameterMixin, ModularComponentTemplateModel):
"""
A template for a CoolingOutflow to be created for a new Device.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=CoolingConnectorTypeChoices,
blank=True,
null=True
)
# diameter, diameter_unit, _abs_diameter provided by DiameterMixin
cooling_intake = models.ForeignKey(
to='dcim.CoolingIntakeTemplate',
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name='coolingoutflow_templates'
)
component_model = CoolingOutflow
class Meta(ModularComponentTemplateModel.Meta):
verbose_name = _('cooling outflow template')
verbose_name_plural = _('cooling outflow templates')
def clean(self):
super().clean()
# Validate cooling intake assignment
if self.cooling_intake:
if self.device_type and self.cooling_intake.device_type != self.device_type:
raise ValidationError(
_("Parent cooling intake ({cooling_intake}) must belong to the same device type").format(
cooling_intake=self.cooling_intake
)
)
if self.module_type and self.cooling_intake.module_type != self.module_type:
raise ValidationError(
_("Parent cooling intake ({cooling_intake}) must belong to the same module type").format(
cooling_intake=self.cooling_intake
)
)
def instantiate(self, **kwargs):
if self.cooling_intake:
cooling_intake_name = self.cooling_intake.resolve_name(kwargs.get('module'), kwargs.get('device'))
cooling_intake = CoolingIntake.objects.get(name=cooling_intake_name, **kwargs)
else:
cooling_intake = None
component = self.component_model(
name=self.resolve_name(kwargs.get('module'), kwargs.get('device')),
label=self.resolve_label(kwargs.get('module'), kwargs.get('device')),
type=self.type,
diameter=self.diameter,
diameter_unit=self.diameter_unit,
cooling_intake=cooling_intake,
**kwargs
)
# bulk_create bypasses save(), so populate the normalized _abs_diameter here
component.normalize_diameter()
return component
instantiate.do_not_call_in_templates = True
def to_yaml(self):
return {
'name': self.name,
'type': self.type,
'diameter': float(self.diameter) if self.diameter is not None else None,
'diameter_unit': self.diameter_unit,
'cooling_intake': self.cooling_intake.name if self.cooling_intake else None,
'label': self.label,
'description': self.description,
}
class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel):
"""
A template for a physical data interface on a new Device.

View File

@ -12,7 +12,12 @@ from dcim.choices import *
from dcim.constants import *
from dcim.fields import WWNField
from dcim.models.base import PortMappingBase
from dcim.models.mixins import InterfaceValidationMixin
from dcim.models.mixins import (
CoolingLoopValidationMixin,
DiameterMixin,
InterfaceValidationMixin,
MaxFlowMixin,
)
from netbox.choices import ColorChoices
from netbox.models import NetBoxModel, OrganizationalModel
from netbox.models.features import ChangeLoggingMixin
@ -30,6 +35,8 @@ __all__ = (
'CabledObjectModel',
'ConsolePort',
'ConsoleServerPort',
'CoolingIntake',
'CoolingOutflow',
'DeviceBay',
'FrontPort',
'Interface',
@ -684,6 +691,96 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
return PowerOutletStatusChoices.colors.get(self.status)
#
# Cooling components
#
class CoolingIntake(
CoolingLoopValidationMixin, DiameterMixin, MaxFlowMixin, ModularComponentModel, TrackingModelMixin
):
"""
A coolant intake port within a Device (e.g. a server cold-plate inlet or CDU intake). A
CoolingIntake is supplied by an upstream CoolingOutflow or CoolingFeed.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=CoolingConnectorTypeChoices,
blank=True,
null=True,
help_text=_('Physical connector type')
)
# diameter, diameter_unit, _abs_diameter provided by DiameterMixin
# max_flow, max_flow_unit, _abs_max_flow provided by MaxFlowMixin
cooling_outflow = models.ForeignKey(
to='dcim.CoolingOutflow',
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name='coolingintakes',
help_text=_('The upstream cooling outflow supplying this intake')
)
clone_fields = (
'device', 'module', 'type', 'diameter', 'diameter_unit', 'max_flow',
'max_flow_unit',
)
upstream_field = 'cooling_outflow'
class Meta(ModularComponentModel.Meta):
verbose_name = _('cooling intake')
verbose_name_plural = _('cooling intakes')
def clean(self):
super().clean()
# Prevent the intake/outflow chain from forming a loop
self.validate_cooling_loop()
class CoolingOutflow(CoolingLoopValidationMixin, DiameterMixin, ModularComponentModel, TrackingModelMixin):
"""
A coolant outlet within a Device (e.g. a CDU or manifold outlet) which supplies one or more
CoolingIntakes (referenced via CoolingIntake.cooling_outflow).
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=CoolingConnectorTypeChoices,
blank=True,
null=True,
help_text=_('Physical connector type')
)
# diameter, diameter_unit, _abs_diameter provided by DiameterMixin
cooling_intake = models.ForeignKey(
to='dcim.CoolingIntake',
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name='coolingoutflows'
)
clone_fields = ('device', 'module', 'type', 'diameter', 'diameter_unit', 'cooling_intake')
upstream_field = 'cooling_intake'
class Meta(ModularComponentModel.Meta):
verbose_name = _('cooling outflow')
verbose_name_plural = _('cooling outflows')
def clean(self):
super().clean()
# Validate cooling intake assignment
if self.cooling_intake and self.cooling_intake.device != self.device:
raise ValidationError(
_("Parent cooling intake ({cooling_intake}) must belong to the same device").format(
cooling_intake=self.cooling_intake)
)
# Prevent the intake/outflow chain from forming a loop
self.validate_cooling_loop()
#
# Interfaces
#

View File

@ -136,6 +136,13 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
blank=True,
null=True
)
cooling_method = models.CharField(
verbose_name=_('cooling method'),
max_length=50,
choices=CoolingMethodChoices,
blank=True,
null=True
)
end_of_life = models.DateField(
verbose_name=_('end of life'),
blank=True,
@ -168,6 +175,14 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
to_model='dcim.PowerOutletTemplate',
to_field='device_type'
)
cooling_intake_template_count = CounterCacheField(
to_model='dcim.CoolingIntakeTemplate',
to_field='device_type'
)
cooling_outflow_template_count = CounterCacheField(
to_model='dcim.CoolingOutflowTemplate',
to_field='device_type'
)
interface_template_count = CounterCacheField(
to_model='dcim.InterfaceTemplate',
to_field='device_type'
@ -198,8 +213,8 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
)
clone_fields = (
'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
'weight_unit',
'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
'cooling_method', 'weight', 'weight_unit',
)
prerequisite_models = (
'dcim.Manufacturer',
@ -237,6 +252,9 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
def full_name(self):
return f"{self.manufacturer} {self.model}"
def get_cooling_method_color(self):
return CoolingMethodChoices.colors.get(self.cooling_method)
def to_yaml(self):
data = {
'manufacturer': self.manufacturer.name,
@ -249,6 +267,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
'is_full_depth': self.is_full_depth,
'subdevice_role': self.subdevice_role,
'airflow': self.airflow,
'cooling_method': self.cooling_method,
'weight': float(self.weight) if self.weight is not None else None,
'weight_unit': self.weight_unit,
'end_of_life': self.end_of_life.isoformat() if self.end_of_life else None,
@ -272,6 +291,14 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
data['power-outlets'] = [
c.to_yaml() for c in self.poweroutlettemplates.all()
]
if self.coolingintaketemplates.exists():
data['cooling-intakes'] = [
c.to_yaml() for c in self.coolingintaketemplates.all()
]
if self.coolingoutflowtemplates.exists():
data['cooling-outflows'] = [
c.to_yaml() for c in self.coolingoutflowtemplates.all()
]
if self.interfacetemplates.exists():
data['interfaces'] = [
c.to_yaml() for c in self.interfacetemplates.all()
@ -602,6 +629,13 @@ class Device(
blank=True,
null=True
)
cooling_method = models.CharField(
verbose_name=_('cooling method'),
max_length=50,
choices=CoolingMethodChoices,
blank=True,
null=True
)
primary_ip4 = models.OneToOneField(
to='ipam.IPAddress',
on_delete=models.SET_NULL,
@ -701,6 +735,14 @@ class Device(
to_model='dcim.PowerOutlet',
to_field='device'
)
cooling_intake_count = CounterCacheField(
to_model='dcim.CoolingIntake',
to_field='device'
)
cooling_outflow_count = CounterCacheField(
to_model='dcim.CoolingOutflow',
to_field='device'
)
interface_count = CounterCacheField(
to_model='dcim.Interface',
to_field='device'
@ -730,7 +772,7 @@ class Device(
clone_fields = (
'device_type', 'role', 'tenant', 'platform', 'site', 'location', 'rack', 'face', 'status', 'airflow',
'cluster', 'virtual_chassis',
'cooling_method', 'cluster', 'virtual_chassis',
)
prerequisite_models = (
'dcim.Site',
@ -1044,6 +1086,10 @@ class Device(
if is_new and not self.airflow:
self.airflow = self.device_type.airflow
# Inherit cooling_method attribute from DeviceType if not set
if is_new and not self.cooling_method:
self.cooling_method = self.device_type.cooling_method
# Inherit default_platform from DeviceType if not set
if is_new and not self.platform:
self.platform = self.device_type.default_platform
@ -1060,6 +1106,8 @@ class Device(
self._instantiate_components(self.device_type.consoleserverporttemplates.all())
self._instantiate_components(self.device_type.powerporttemplates.all())
self._instantiate_components(self.device_type.poweroutlettemplates.all())
self._instantiate_components(self.device_type.coolingintaketemplates.all())
self._instantiate_components(self.device_type.coolingoutflowtemplates.all())
self._instantiate_components(self.device_type.interfacetemplates.all())
self._instantiate_components(self.device_type.rearporttemplates.all())
self._instantiate_components(self.device_type.frontporttemplates.all())
@ -1158,6 +1206,9 @@ class Device(
def get_status_color(self):
return DeviceStatusChoices.colors.get(self.status)
def get_cooling_method_color(self):
return CoolingMethodChoices.colors.get(self.cooling_method)
@cached_property
def total_weight(self):
total_weight = sum(

View File

@ -1,15 +1,26 @@
from decimal import Decimal
from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfaceTypeChoices
from dcim.constants import NONCONNECTABLE_IFACE_TYPES, VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES
from netbox.choices import *
from utilities.conversion import (
to_liters_per_minute,
to_millimeters,
)
__all__ = (
'CachedScopeMixin',
'CoolingLoopValidationMixin',
'DiameterMixin',
'InterfaceValidationMixin',
'MaxFlowMixin',
'RenderConfigMixin',
)
@ -229,3 +240,166 @@ class InterfaceValidationMixin:
# RF role may be set only for wireless interfaces
if self.rf_role and self.type not in WIRELESS_IFACE_TYPES:
raise ValidationError({'rf_role': _("Wireless role may be set only on wireless interfaces.")})
class CoolingLoopValidationMixin:
"""
Adds loop detection to the coolant chain formed by cooling intakes and outflows. A CoolingIntake is supplied
by an upstream CoolingOutflow (via `cooling_outflow`), which may in turn be supplied by an upstream
CoolingIntake on the same device (via `cooling_intake`), and so on; this chain must remain acyclic.
Each concrete model declares `upstream_field`, the name of its foreign key to the next component upstream.
The chain alternates between the two models, so the walk simply follows each visited component's own
upstream field in turn. (The field is resolved by name rather than referencing the models directly, as this
module is imported by the ones defining them.)
"""
upstream_field = None
@classmethod
def _get_upstream_field(cls):
return cls._meta.get_field(cls.upstream_field)
def validate_cooling_loop(self):
"""
Raise a ValidationError if this component's upstream assignment forms a loop.
Each hop resolves only the next foreign key ID (a single indexed column lookup) rather than loading
full related objects, and the `seen` set of (model, pk) pairs guarantees termination.
"""
seen = set()
if self.pk:
seen.add((type(self), self.pk))
# Seed the walk from this (possibly unsaved) component's in-memory foreign key
field = self._get_upstream_field()
model, pk = field.related_model, getattr(self, field.attname)
while pk is not None:
if (model, pk) in seen:
raise ValidationError(_("Cooling intake and outflow assignments cannot form a loop."))
seen.add((model, pk))
# Advance to the component upstream of the one just visited
field = model._get_upstream_field()
pk = model.objects.filter(pk=pk).values_list(field.attname, flat=True).first()
model = field.related_model
class DiameterMixin(models.Model):
diameter = models.DecimalField(
verbose_name=_('diameter'),
max_digits=8,
decimal_places=2,
blank=True,
null=True,
validators=[MinValueValidator(Decimal('0.01'))],
)
diameter_unit = models.CharField(
verbose_name=_('diameter unit'),
max_length=50,
choices=DiameterUnitChoices,
blank=True,
null=True,
)
# Stores the normalized diameter (in millimeters) for database ordering
_abs_diameter = models.DecimalField(
max_digits=13,
decimal_places=4,
blank=True,
null=True
)
class Meta:
abstract = True
@property
def abs_diameter(self):
# Public alias for _abs_diameter; Django templates cannot access underscore-prefixed attributes.
return self._abs_diameter
def normalize_diameter(self):
"""
Store the given diameter (if any) in millimeters for use in database ordering. Called by save(), and
directly by component instantiation, which bypasses save() via bulk_create().
"""
if self.diameter is not None and self.diameter_unit:
self._abs_diameter = to_millimeters(self.diameter, self.diameter_unit)
else:
self._abs_diameter = None
if self.diameter is None:
self.diameter_unit = None
normalize_diameter.alters_data = True
def save(self, *args, **kwargs):
self.normalize_diameter()
super().save(*args, **kwargs)
def clean(self):
super().clean()
# Validate diameter and diameter_unit
if self.diameter is not None and not self.diameter_unit:
raise ValidationError(_("Must specify a unit when setting a diameter"))
class MaxFlowMixin(models.Model):
"""
Adds the maximum rate of coolant flow supported by an object, held as a value plus its unit alongside a
normalized column (in liters per minute) so that ordering and filtering work across mixed units.
"""
max_flow = models.DecimalField(
verbose_name=_('max flow'),
max_digits=8,
decimal_places=2,
blank=True,
null=True,
validators=[MinValueValidator(Decimal('0.01'))],
)
max_flow_unit = models.CharField(
verbose_name=_('max flow unit'),
max_length=50,
choices=FlowRateUnitChoices,
blank=True,
null=True,
)
# Stores the normalized max flow (in liters per minute) for database ordering
_abs_max_flow = models.DecimalField(
max_digits=13,
decimal_places=4,
blank=True,
null=True
)
class Meta:
abstract = True
@property
def abs_max_flow(self):
# Public alias for _abs_max_flow; Django templates cannot access underscore-prefixed attributes.
return self._abs_max_flow
def normalize_max_flow(self):
"""
Store the given max flow (if any) in liters per minute for use in database ordering. Called by save(),
and directly by component instantiation, which bypasses save() via bulk_create().
"""
if self.max_flow is not None and self.max_flow_unit:
self._abs_max_flow = to_liters_per_minute(self.max_flow, self.max_flow_unit)
else:
self._abs_max_flow = None
if self.max_flow is None:
self.max_flow_unit = None
normalize_max_flow.alters_data = True
def save(self, *args, **kwargs):
self.normalize_max_flow()
super().save(*args, **kwargs)
def clean(self):
super().clean()
# Validate max_flow and max_flow_unit
if self.max_flow is not None and not self.max_flow_unit:
raise ValidationError(_("Must specify a unit when setting a maximum flow"))

View File

@ -146,6 +146,13 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
blank=True,
null=True
)
cooling_method = models.CharField(
verbose_name=_('cooling method'),
max_length=50,
choices=CoolingMethodChoices,
blank=True,
null=True
)
end_of_life = models.DateField(
verbose_name=_('end of life'),
blank=True,
@ -186,6 +193,14 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
to_model='dcim.PowerOutletTemplate',
to_field='module_type'
)
cooling_intake_template_count = CounterCacheField(
to_model='dcim.CoolingIntakeTemplate',
to_field='module_type'
)
cooling_outflow_template_count = CounterCacheField(
to_model='dcim.CoolingOutflowTemplate',
to_field='module_type'
)
interface_template_count = CounterCacheField(
to_model='dcim.InterfaceTemplate',
to_field='module_type'
@ -203,7 +218,7 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
to_field='module_type'
)
clone_fields = ('profile', 'manufacturer', 'weight', 'weight_unit', 'airflow')
clone_fields = ('profile', 'manufacturer', 'weight', 'weight_unit', 'airflow', 'cooling_method')
prerequisite_models = (
'dcim.Manufacturer',
)
@ -281,6 +296,9 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
else:
self.attribute_data = None
def get_cooling_method_color(self):
return CoolingMethodChoices.colors.get(self.cooling_method)
def to_yaml(self):
data = {
'profile': self.profile.name if self.profile else None,
@ -291,6 +309,7 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
'weight': float(self.weight) if self.weight is not None else None,
'weight_unit': self.weight_unit,
'airflow': self.airflow,
'cooling_method': self.cooling_method,
'end_of_life': self.end_of_life.isoformat() if self.end_of_life else None,
'attribute_data': self.attribute_data,
'comments': self.comments,
@ -313,6 +332,14 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
data['power-outlets'] = [
c.to_yaml() for c in self.poweroutlettemplates.all()
]
if self.coolingintaketemplates.exists():
data['cooling-intakes'] = [
c.to_yaml() for c in self.coolingintaketemplates.all()
]
if self.coolingoutflowtemplates.exists():
data['cooling-outflows'] = [
c.to_yaml() for c in self.coolingoutflowtemplates.all()
]
if self.interfacetemplates.exists():
data['interfaces'] = [
c.to_yaml() for c in self.interfacetemplates.all()
@ -503,6 +530,8 @@ class Module(TrackingModelMixin, PrimaryModel):
("interfacetemplates", "interfaces", Interface),
("powerporttemplates", "powerports", PowerPort),
("poweroutlettemplates", "poweroutlets", PowerOutlet),
("coolingintaketemplates", "coolingintakes", CoolingIntake),
("coolingoutflowtemplates", "coolingoutflows", CoolingOutflow),
("rearporttemplates", "rearports", RearPort),
("frontporttemplates", "frontports", FrontPort),
("modulebaytemplates", "modulebays", ModuleBay),

View File

@ -135,9 +135,30 @@ class RackBase(WeightMixin, PrimaryModel):
null=True
)
# Cooling
cooling_capability = models.CharField(
verbose_name=_('cooling capability'),
max_length=50,
choices=RackCoolingCapabilityChoices,
blank=True,
null=True
)
cooling_capacity = models.DecimalField(
verbose_name=_('cooling capacity'),
max_digits=10,
decimal_places=2,
blank=True,
null=True,
validators=[MinValueValidator(0)],
help_text=_('Cooling capacity (kW)')
)
class Meta:
abstract = True
def get_cooling_capability_color(self):
return RackCoolingCapabilityChoices.colors.get(self.cooling_capability)
#
# Rack Types
@ -174,7 +195,8 @@ class RackType(ImageAttachmentsMixin, RackBase):
clone_fields = (
'manufacturer', 'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'cooling_capability',
'cooling_capacity',
)
prerequisite_models = (
'dcim.Manufacturer',
@ -269,7 +291,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, TrackingModelMixin, RackBase):
# Fields which cannot be set locally if a RackType is assigned
RACKTYPE_FIELDS = (
'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'max_weight',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'max_weight', 'cooling_capability',
'cooling_capacity',
)
form_factor = models.CharField(
@ -369,9 +392,9 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, TrackingModelMixin, RackBase):
)
clone_fields = (
'site', 'location', 'tenant', 'status', 'role', 'form_factor', 'width', 'airflow', 'u_height', 'desc_units',
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
'weight_unit',
'site', 'location', 'tenant', 'status', 'role', 'form_factor', 'width', 'airflow', 'cooling_capability',
'cooling_capacity', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
)
prerequisite_models = (
'dcim.Site',

View File

@ -49,6 +49,50 @@ class ConsoleServerPortIndex(SearchIndex):
display_attrs = ('device', 'label', 'type', 'description')
@register_search
class CoolingFeedIndex(SearchIndex):
model = models.CoolingFeed
fields = (
('name', 100),
('description', 500),
('comments', 5000),
)
display_attrs = ('cooling_source', 'rack', 'status', 'description')
@register_search
class CoolingOutflowIndex(SearchIndex):
model = models.CoolingOutflow
fields = (
('name', 100),
('label', 200),
('description', 500),
)
display_attrs = ('device', 'label', 'type', 'description')
@register_search
class CoolingIntakeIndex(SearchIndex):
model = models.CoolingIntake
fields = (
('name', 100),
('label', 200),
('description', 500),
)
display_attrs = ('device', 'label', 'type', 'description')
@register_search
class CoolingSourceIndex(SearchIndex):
model = models.CoolingSource
fields = (
('name', 100),
('description', 500),
('comments', 5000),
)
display_attrs = ('site', 'location', 'type', 'status', 'description')
@register_search
class DeviceIndex(SearchIndex):
model = models.Device

View File

@ -1,5 +1,6 @@
from .cables import *
from .connections import *
from .cooling import *
from .devices import *
from .devicetypes import *
from .modules import *

View File

@ -0,0 +1,303 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from django_tables2.utils import Accessor
from dcim import models
from dcim.models import CoolingFeed, CoolingSource
from netbox.tables import PrimaryModelTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from .devices import DeviceComponentTable, ModularDeviceComponentTable
from .devicetypes import ComponentTemplateTable
from .template_code import DIAMETER, MAX_FLOW, MODULAR_COMPONENT_TEMPLATE_BUTTONS
__all__ = (
'CoolingFeedTable',
'CoolingIntakeTable',
'CoolingIntakeTemplateTable',
'CoolingOutflowTable',
'CoolingOutflowTemplateTable',
'CoolingSourceTable',
'DeviceCoolingIntakeTable',
'DeviceCoolingOutflowTable',
)
#
# Cooling sources
#
class CoolingSourceTable(ContactsColumnMixin, PrimaryModelTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
site = tables.Column(
verbose_name=_('Site'),
linkify=True
)
location = tables.Column(
verbose_name=_('Location'),
linkify=True
)
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
type = columns.ChoiceFieldColumn(
verbose_name=_('Type'),
)
fluid_type = columns.ChoiceFieldColumn(
verbose_name=_('Fluid Type'),
)
cooling_capacity = tables.Column(
verbose_name=_('Cooling Capacity (kW)')
)
coolingfeed_count = columns.LinkedCountColumn(
viewname='dcim:coolingfeed_list',
url_params={'cooling_source_id': 'pk'},
verbose_name=_('Cooling Feeds')
)
tags = columns.TagColumn(
url_name='dcim:coolingsource_list'
)
class Meta(PrimaryModelTable.Meta):
model = CoolingSource
fields = (
'pk', 'id', 'name', 'site', 'location', 'type', 'status', 'fluid_type', 'cooling_capacity',
'coolingfeed_count', 'contacts', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'site', 'location', 'type', 'status', 'cooling_capacity', 'coolingfeed_count',
)
#
# Cooling feeds
#
class CoolingFeedTable(TenancyColumnsMixin, PrimaryModelTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
cooling_source = tables.Column(
verbose_name=_('Cooling Source'),
linkify=True
)
rack = tables.Column(
verbose_name=_('Rack'),
linkify=True
)
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
cooling_capacity = tables.Column(
verbose_name=_('Cooling Capacity (kW)')
)
max_flow = columns.TemplateColumn(
verbose_name=_('Max Flow'),
template_code=MAX_FLOW,
order_by=('_abs_max_flow',),
)
tenant = tables.Column(
linkify=True,
verbose_name=_('Tenant')
)
site = tables.Column(
accessor='rack__site',
linkify=True,
verbose_name=_('Site'),
)
tags = columns.TagColumn(
url_name='dcim:coolingfeed_list'
)
class Meta(PrimaryModelTable.Meta):
model = CoolingFeed
fields = (
'pk', 'id', 'name', 'cooling_source', 'site', 'rack', 'status', 'cooling_capacity', 'max_flow', 'tenant',
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'cooling_source', 'rack', 'status', 'cooling_capacity',
'max_flow',
)
#
# Cooling ports
#
class CoolingIntakeTable(ModularDeviceComponentTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_coolingintakes',
'args': [Accessor('device_id')],
}
)
type = columns.ChoiceFieldColumn(
verbose_name=_('Type'),
)
diameter = columns.TemplateColumn(
verbose_name=_('Diameter'),
template_code=DIAMETER,
order_by=('_abs_diameter', 'diameter_unit')
)
max_flow = columns.TemplateColumn(
verbose_name=_('Max Flow'),
template_code=MAX_FLOW,
order_by=('_abs_max_flow',),
)
cooling_outflow = tables.Column(
verbose_name=_('Cooling Outflow'),
linkify=True
)
tags = columns.TagColumn(
url_name='dcim:coolingintake_list'
)
class Meta(DeviceComponentTable.Meta):
model = models.CoolingIntake
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'diameter', 'description',
'max_flow', 'cooling_outflow', 'inventory_items', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'device', 'label', 'type', 'diameter', 'max_flow',
'description',
)
#
# Cooling outlets
#
class CoolingOutflowTable(ModularDeviceComponentTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_coolingoutflows',
'args': [Accessor('device_id')],
}
)
type = columns.ChoiceFieldColumn(
verbose_name=_('Type'),
)
diameter = columns.TemplateColumn(
verbose_name=_('Diameter'),
template_code=DIAMETER,
order_by=('_abs_diameter', 'diameter_unit')
)
cooling_intake = tables.Column(
verbose_name=_('Cooling Intake'),
linkify=True
)
tags = columns.TagColumn(
url_name='dcim:coolingoutflow_list'
)
class Meta(DeviceComponentTable.Meta):
model = models.CoolingOutflow
fields = (
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'type', 'diameter',
'description', 'cooling_intake', 'inventory_items', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'device', 'label', 'type', 'diameter', 'cooling_intake',
'description',
)
#
# Cooling port templates
#
class CoolingIntakeTemplateTable(ComponentTemplateTable):
diameter = columns.TemplateColumn(
verbose_name=_('Diameter'),
template_code=DIAMETER,
order_by=('_abs_diameter', 'diameter_unit')
)
max_flow = columns.TemplateColumn(
verbose_name=_('Max Flow'),
template_code=MAX_FLOW,
order_by=('_abs_max_flow',),
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
)
class Meta(ComponentTemplateTable.Meta):
model = models.CoolingIntakeTemplate
fields = (
'pk', 'name', 'label', 'type', 'diameter', 'max_flow', 'description', 'actions',
)
empty_text = "None"
#
# Cooling outlet templates
#
class CoolingOutflowTemplateTable(ComponentTemplateTable):
diameter = columns.TemplateColumn(
verbose_name=_('Diameter'),
template_code=DIAMETER,
order_by=('_abs_diameter', 'diameter_unit')
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
)
class Meta(ComponentTemplateTable.Meta):
model = models.CoolingOutflowTemplate
fields = (
'pk', 'name', 'label', 'type', 'diameter', 'cooling_intake', 'description',
'actions',
)
empty_text = "None"
#
# Device cooling components
#
class DeviceCoolingIntakeTable(CoolingIntakeTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-snowflake"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
attrs={'td': {'class': 'text-nowrap'}}
)
class Meta(DeviceComponentTable.Meta):
model = models.CoolingIntake
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'diameter', 'max_flow',
'description', 'cooling_outflow', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'diameter', 'max_flow',
'description',
)
class DeviceCoolingOutflowTable(CoolingOutflowTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-snowflake"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
attrs={'td': {'class': 'text-nowrap'}}
)
class Meta(DeviceComponentTable.Meta):
model = models.CoolingOutflow
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'diameter',
'cooling_intake', 'description', 'tags', 'actions',
)
default_columns = (
'pk', 'name', 'label', 'type', 'diameter', 'cooling_intake', 'description',
)

View File

@ -254,6 +254,12 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
power_outlet_count = tables.Column(
verbose_name=_('Power outlets')
)
cooling_intake_count = tables.Column(
verbose_name=_('Cooling intakes')
)
cooling_outflow_count = tables.Column(
verbose_name=_('Cooling outflows')
)
interface_count = tables.Column(
verbose_name=_('Interfaces')
)
@ -272,13 +278,17 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
inventory_item_count = tables.Column(
verbose_name=_('Inventory items')
)
cooling_method = columns.ChoiceFieldColumn(
verbose_name=_('Cooling Method'),
)
class Meta(PrimaryModelTable.Meta):
model = models.Device
fields = (
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'role', 'manufacturer', 'device_type',
'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'primary_ip', 'primary_ip4',
'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'cooling_method',
'primary_ip', 'primary_ip4',
'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
'config_template', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)

View File

@ -128,6 +128,12 @@ class DeviceTypeTable(PrimaryModelTable):
power_outlet_template_count = tables.Column(
verbose_name=_('Power Outlets')
)
cooling_intake_template_count = tables.Column(
verbose_name=_('Cooling Intakes')
)
cooling_outflow_template_count = tables.Column(
verbose_name=_('Cooling Outflows')
)
interface_template_count = tables.Column(
verbose_name=_('Interfaces')
)
@ -146,12 +152,16 @@ class DeviceTypeTable(PrimaryModelTable):
inventory_item_template_count = tables.Column(
verbose_name=_('Inventory Items')
)
cooling_method = columns.ChoiceFieldColumn(
verbose_name=_('Cooling Method'),
)
class Meta(PrimaryModelTable.Meta):
model = models.DeviceType
fields = (
'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height',
'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'end_of_life',
'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'cooling_method', 'weight',
'end_of_life',
'description', 'comments', 'device_count', 'tags', 'created', 'last_updated',
)
default_columns = (

View File

@ -83,6 +83,9 @@ class ModuleTypeTable(PrimaryModelTable):
linkify=True,
verbose_name=_('Module Type')
)
cooling_method = columns.ChoiceFieldColumn(
verbose_name=_('Cooling Method'),
)
weight = columns.TemplateColumn(
verbose_name=_('Weight'),
template_code=WEIGHT,
@ -103,8 +106,8 @@ class ModuleTypeTable(PrimaryModelTable):
class Meta(PrimaryModelTable.Meta):
model = ModuleType
fields = (
'pk', 'id', 'model', 'profile', 'manufacturer', 'part_number', 'airflow', 'weight', 'end_of_life',
'module_bay_types',
'pk', 'id', 'model', 'profile', 'manufacturer', 'part_number', 'airflow', 'cooling_method', 'weight',
'end_of_life', 'module_bay_types',
'description', 'attributes', 'module_count', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (

View File

@ -106,6 +106,12 @@ class RackTypeTable(PrimaryModelTable):
url_params={'rack_type_id': 'pk'},
verbose_name=_('Rack Count'),
)
cooling_capability = columns.ChoiceFieldColumn(
verbose_name=_('Cooling Capability'),
)
cooling_capacity = tables.Column(
verbose_name=_('Cooling Capacity (kW)')
)
tags = columns.TagColumn(
url_name='dcim:rack_list'
)
@ -114,8 +120,8 @@ class RackTypeTable(PrimaryModelTable):
model = RackType
fields = (
'pk', 'id', 'model', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
'outer_height', 'outer_depth', 'mounting_depth', 'weight', 'max_weight', 'description', 'comments',
'rack_count', 'tags', 'created', 'last_updated',
'outer_height', 'outer_depth', 'mounting_depth', 'weight', 'max_weight', 'cooling_capability',
'cooling_capacity', 'description', 'comments', 'rack_count', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'model', 'manufacturer', 'type', 'u_height', 'description', 'rack_count',
@ -196,13 +202,20 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
template_code=WEIGHT,
order_by=('_abs_max_weight', 'weight_unit')
)
cooling_capability = columns.ChoiceFieldColumn(
verbose_name=_('Cooling Capability'),
)
cooling_capacity = tables.Column(
verbose_name=_('Cooling Capacity (kW)')
)
class Meta(PrimaryModelTable.Meta):
model = Rack
fields = (
'pk', 'id', 'name', 'site', 'location', 'group', 'status', 'facility_id', 'tenant', 'tenant_group', 'role',
'rack_type', 'serial', 'asset_tag', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
'outer_height', 'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'comments',
'outer_height', 'outer_depth', 'mounting_depth', 'airflow', 'cooling_capability',
'cooling_capacity', 'weight', 'max_weight', 'comments',
'device_count', 'get_utilization', 'get_power_utilization', 'description', 'contacts',
'tags', 'created', 'last_updated',
)

View File

@ -52,6 +52,16 @@ WEIGHT = """
{% display_weight record.weight record.weight_unit record.abs_weight %}
"""
DIAMETER = """
{% load helpers %}
{% display_diameter record.diameter record.diameter_unit record.abs_diameter %}
"""
MAX_FLOW = """
{% load helpers %}
{% display_flow_rate record.max_flow record.max_flow_unit record.abs_max_flow %}
"""
DEVICE_LINK = """
{{ record.label|default:'<span class="badge text-bg-info">Unnamed device</span>' }}
"""

View File

@ -11,6 +11,16 @@
"consoleserverport:api_list_objects": 14,
"consoleserverport:list_objects_with_permission": 21,
"consoleserverporttemplate:api_list_objects": 11,
"coolingfeed:api_list_objects": 15,
"coolingfeed:list_objects_with_permission": 22,
"coolingintake:api_list_objects": 14,
"coolingintake:list_objects_with_permission": 21,
"coolingintaketemplate:api_list_objects": 11,
"coolingoutflow:api_list_objects": 14,
"coolingoutflow:list_objects_with_permission": 22,
"coolingoutflowtemplate:api_list_objects": 11,
"coolingsource:api_list_objects": 15,
"coolingsource:list_objects_with_permission": 22,
"device:api_list_objects": 20,
"device:list_objects_with_permission": 25,
"devicebay:api_list_objects": 14,

View File

@ -679,6 +679,8 @@ class RackTypeTestCase(APIViewTestCases.APIViewTestCase):
brief_fields = ['description', 'display', 'id', 'manufacturer', 'model', 'rack_count', 'slug', 'url']
bulk_update_data = {
'description': 'new description',
'cooling_capability': RackCoolingCapabilityChoices.CAPABILITY_HYBRID,
'cooling_capacity': 50,
}
user_permissions = ('dcim.view_manufacturer',)
@ -718,12 +720,15 @@ class RackTypeTestCase(APIViewTestCases.APIViewTestCase):
'model': 'Rack Type 4',
'slug': 'rack-type-4',
'form_factor': RackFormFactorChoices.TYPE_CABINET,
'cooling_capability': RackCoolingCapabilityChoices.CAPABILITY_LIQUID_ONLY,
'cooling_capacity': 80,
},
{
'manufacturer': manufacturers[1].pk,
'model': 'Rack Type 5',
'slug': 'rack-type-5',
'form_factor': RackFormFactorChoices.TYPE_CABINET,
'cooling_capability': RackCoolingCapabilityChoices.CAPABILITY_HYBRID,
},
{
'manufacturer': manufacturers[1].pk,
@ -4238,6 +4243,326 @@ class PowerFeedTestCase(APIViewTestCases.APIViewTestCase):
]
class CoolingIntakeTemplateTestCase(APIViewTestCases.APIViewTestCase):
model = CoolingIntakeTemplate
brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
moduletype = ModuleType.objects.create(
manufacturer=manufacturer, model='Module Type 1'
)
cooling_intake_templates = (
CoolingIntakeTemplate(device_type=devicetype, name='Cooling Port Template 1'),
CoolingIntakeTemplate(device_type=devicetype, name='Cooling Port Template 2'),
CoolingIntakeTemplate(device_type=devicetype, name='Cooling Port Template 3'),
)
CoolingIntakeTemplate.objects.bulk_create(cooling_intake_templates)
cls.create_data = [
{
'device_type': devicetype.pk,
'name': 'Cooling Port Template 4',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
},
{
'device_type': devicetype.pk,
'name': 'Cooling Port Template 5',
},
{
'module_type': moduletype.pk,
'name': 'Cooling Port Template 6',
},
{
'module_type': moduletype.pk,
'name': 'Cooling Port Template 7',
},
]
class CoolingOutflowTemplateTestCase(APIViewTestCases.APIViewTestCase):
model = CoolingOutflowTemplate
brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_devicetype', )
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
moduletype = ModuleType.objects.create(
manufacturer=manufacturer, model='Module Type 1'
)
cooling_intake_templates = (
CoolingIntakeTemplate(device_type=devicetype, name='Cooling Port Template 1'),
CoolingIntakeTemplate(device_type=devicetype, name='Cooling Port Template 2'),
)
CoolingIntakeTemplate.objects.bulk_create(cooling_intake_templates)
cooling_outflow_templates = (
CoolingOutflowTemplate(device_type=devicetype, name='Cooling Outlet Template 1'),
CoolingOutflowTemplate(device_type=devicetype, name='Cooling Outlet Template 2'),
CoolingOutflowTemplate(device_type=devicetype, name='Cooling Outlet Template 3'),
)
CoolingOutflowTemplate.objects.bulk_create(cooling_outflow_templates)
cls.create_data = [
{
'device_type': devicetype.pk,
'name': 'Cooling Outlet Template 4',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'cooling_intake': cooling_intake_templates[0].pk,
},
{
'device_type': devicetype.pk,
'name': 'Cooling Outlet Template 5',
'cooling_intake': cooling_intake_templates[1].pk,
},
{
'device_type': devicetype.pk,
'name': 'Cooling Outlet Template 6',
'cooling_intake': None,
},
{
'module_type': moduletype.pk,
'name': 'Cooling Outlet Template 7',
},
{
'module_type': moduletype.pk,
'name': 'Cooling Outlet Template 8',
},
]
class CoolingIntakeTestCase(APIViewTestCases.APIViewTestCase):
model = CoolingIntake
brief_fields = ['description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
site = Site.objects.create(name='Site 1', slug='site-1')
role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
cooling_outflow = CoolingOutflow.objects.create(device=device, name='Cooling Outlet 1')
cooling_intakes = (
CoolingIntake(device=device, name='Cooling Port 1'),
CoolingIntake(device=device, name='Cooling Port 2'),
CoolingIntake(device=device, name='Cooling Port 3'),
)
CoolingIntake.objects.bulk_create(cooling_intakes)
cls.create_data = [
{
'device': device.pk,
'name': 'Cooling Port 4',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'cooling_outflow': cooling_outflow.pk,
},
{
'device': device.pk,
'name': 'Cooling Port 5',
'type': CoolingConnectorTypeChoices.TYPE_QDC,
},
{
'device': device.pk,
'name': 'Cooling Port 6',
},
]
class CoolingOutflowTestCase(APIViewTestCases.APIViewTestCase):
model = CoolingOutflow
brief_fields = ['description', 'device', 'display', 'id', 'name', 'url']
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
site = Site.objects.create(name='Site 1', slug='site-1')
role = DeviceRole.objects.create(name='Test Device Role 1', slug='test-device-role-1', color='ff0000')
device = Device.objects.create(device_type=devicetype, role=role, name='Device 1', site=site)
cooling_intakes = (
CoolingIntake(device=device, name='Cooling Port 1'),
CoolingIntake(device=device, name='Cooling Port 2'),
)
CoolingIntake.objects.bulk_create(cooling_intakes)
cooling_outflows = (
CoolingOutflow(device=device, name='Cooling Outlet 1'),
CoolingOutflow(device=device, name='Cooling Outlet 2'),
CoolingOutflow(device=device, name='Cooling Outlet 3'),
)
CoolingOutflow.objects.bulk_create(cooling_outflows)
cls.create_data = [
{
'device': device.pk,
'name': 'Cooling Outlet 4',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'cooling_intake': cooling_intakes[0].pk,
},
{
'device': device.pk,
'name': 'Cooling Outlet 5',
'cooling_intake': cooling_intakes[1].pk,
},
{
'device': device.pk,
'name': 'Cooling Outlet 6',
'cooling_intake': None,
},
]
class CoolingSourceTestCase(APIViewTestCases.APIViewTestCase):
model = CoolingSource
brief_fields = ['coolingfeed_count', 'description', 'display', 'id', 'name', 'url']
user_permissions = ('dcim.view_site', )
@classmethod
def setUpTestData(cls):
sites = (
Site.objects.create(name='Site 1', slug='site-1'),
Site.objects.create(name='Site 2', slug='site-2'),
)
locations = (
Location.objects.create(name='Location 1', slug='location-1', site=sites[0]),
Location.objects.create(name='Location 2', slug='location-2', site=sites[0]),
Location.objects.create(name='Location 3', slug='location-3', site=sites[0]),
Location.objects.create(name='Location 4', slug='location-3', site=sites[1]),
)
cooling_sources = (
CoolingSource(
site=sites[0], location=locations[0], name='Cooling Source 1',
type=CoolingSourceTypeChoices.TYPE_CHILLER
),
CoolingSource(
site=sites[0], location=locations[1], name='Cooling Source 2',
type=CoolingSourceTypeChoices.TYPE_COOLING_TOWER
),
CoolingSource(
site=sites[0], location=locations[2], name='Cooling Source 3',
type=CoolingSourceTypeChoices.TYPE_DRY_COOLER
),
)
CoolingSource.objects.bulk_create(cooling_sources)
cls.create_data = [
{
'name': 'Cooling Source 4',
'site': sites[0].pk,
'location': locations[0].pk,
'type': CoolingSourceTypeChoices.TYPE_CHILLER,
},
{
'name': 'Cooling Source 5',
'site': sites[0].pk,
'location': locations[1].pk,
'type': CoolingSourceTypeChoices.TYPE_CHILLER,
},
{
'name': 'Cooling Source 6',
'site': sites[0].pk,
'location': locations[2].pk,
'type': CoolingSourceTypeChoices.TYPE_CHILLER,
},
]
cls.bulk_update_data = {
'site': sites[1].pk,
'location': locations[3].pk
}
class CoolingFeedTestCase(APIViewTestCases.APIViewTestCase):
model = CoolingFeed
brief_fields = ['description', 'display', 'id', 'name', 'url']
bulk_update_data = {
'status': 'planned',
}
user_permissions = ('dcim.view_coolingsource', )
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 1', slug='site-1')
location = Location.objects.create(site=site, name='Location 1', slug='location-1')
rackrole = RackRole.objects.create(name='Rack Role 1', slug='rack-role-1', color='ff0000')
racks = (
Rack(site=site, location=location, role=rackrole, name='Rack 1'),
Rack(site=site, location=location, role=rackrole, name='Rack 2'),
Rack(site=site, location=location, role=rackrole, name='Rack 3'),
Rack(site=site, location=location, role=rackrole, name='Rack 4'),
)
Rack.objects.bulk_create(racks)
cooling_sources = (
CoolingSource(
site=site, location=location, name='Cooling Source 1', type=CoolingSourceTypeChoices.TYPE_CHILLER
),
CoolingSource(
site=site, location=location, name='Cooling Source 2', type=CoolingSourceTypeChoices.TYPE_CHILLER
),
)
CoolingSource.objects.bulk_create(cooling_sources)
cooling_feeds = (
CoolingFeed(cooling_source=cooling_sources[0], rack=racks[0], name='Cooling Feed 1A'),
CoolingFeed(cooling_source=cooling_sources[1], rack=racks[0], name='Cooling Feed 1B'),
CoolingFeed(cooling_source=cooling_sources[0], rack=racks[1], name='Cooling Feed 2A'),
CoolingFeed(cooling_source=cooling_sources[1], rack=racks[1], name='Cooling Feed 2B'),
CoolingFeed(cooling_source=cooling_sources[0], rack=racks[2], name='Cooling Feed 3A'),
CoolingFeed(cooling_source=cooling_sources[1], rack=racks[2], name='Cooling Feed 3B'),
)
CoolingFeed.objects.bulk_create(cooling_feeds)
cls.create_data = [
{
'name': 'Cooling Feed 4A',
'cooling_source': cooling_sources[0].pk,
'rack': racks[3].pk,
},
{
'name': 'Cooling Feed 4B',
'cooling_source': cooling_sources[1].pk,
'rack': racks[3].pk,
},
{
'name': 'Cooling Feed 4C',
'cooling_source': cooling_sources[0].pk,
'rack': racks[3].pk,
},
]
class VirtualDeviceContextTestCase(APIViewTestCases.APIViewTestCase):
model = VirtualDeviceContext
brief_fields = ['description', 'device', 'display', 'id', 'identifier', 'name', 'url']

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ from dcim.models import *
from extras.events import serialize_for_event
from extras.models import CustomField
from ipam.models import Prefix
from netbox.choices import WeightUnitChoices
from netbox.choices import DiameterUnitChoices, FlowRateUnitChoices, WeightUnitChoices
from tenancy.models import Tenant
from utilities.data import drange
from virtualization.models import Cluster, ClusterType
@ -243,6 +243,8 @@ class RackTypeTestCase(TestCase):
weight_unit=WeightUnitChoices.UNIT_POUND,
max_weight=7777,
mounting_depth=8,
cooling_capability=RackCoolingCapabilityChoices.CAPABILITY_LIQUID_ONLY,
cooling_capacity=80,
)
def test_rack_creation(self):
@ -262,7 +264,7 @@ class RackTypeTestCase(TestCase):
facility_id='A101',
site=sites[0],
location=locations[0],
rack_type=rack_type
rack_type=rack_type,
)
self.assertEqual(rack.width, rack_type.width)
self.assertEqual(rack.u_height, rack_type.u_height)
@ -275,6 +277,9 @@ class RackTypeTestCase(TestCase):
self.assertEqual(rack.weight_unit, rack_type.weight_unit)
self.assertEqual(rack.max_weight, rack_type.max_weight)
self.assertEqual(rack.mounting_depth, rack_type.mounting_depth)
# Cooling capability/capacity are inherited from the rack type
self.assertEqual(rack.cooling_capability, rack_type.cooling_capability)
self.assertEqual(rack.cooling_capacity, rack_type.cooling_capacity)
class RackTestCase(TestCase):
@ -3173,3 +3178,490 @@ class InventoryItemTemplateCycleTestCase(TestCase):
a.parent = a
with self.assertRaises(ValidationError):
a.full_clean()
class CoolingComponentTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.site = Site.objects.create(name='Site 1', slug='site-1')
cls.manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
cls.role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
def test_cooling_method_inherited_from_device_type(self):
"""
A new Device should inherit its cooling_method from the DeviceType when not explicitly set.
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer,
model='Device Type 1',
slug='device-type-1',
cooling_method=CoolingMethodChoices.METHOD_LIQUID
)
device = Device.objects.create(
site=self.site,
device_type=device_type,
role=self.role,
name='Device 1'
)
self.assertEqual(device.cooling_method, CoolingMethodChoices.METHOD_LIQUID)
def test_cooling_method_not_overridden_when_set(self):
"""
A new Device with an explicitly-set cooling_method should not be overridden by the DeviceType.
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer,
model='Device Type 2',
slug='device-type-2',
cooling_method=CoolingMethodChoices.METHOD_LIQUID
)
device = Device.objects.create(
site=self.site,
device_type=device_type,
role=self.role,
name='Device 2',
cooling_method=CoolingMethodChoices.METHOD_AIR
)
self.assertEqual(device.cooling_method, CoolingMethodChoices.METHOD_AIR)
def test_device_creation_instantiates_cooling_components(self):
"""
Creating a Device from a DeviceType with cooling component templates should auto-instantiate
matching CoolingIntake and CoolingOutflow components.
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer,
model='Device Type 3',
slug='device-type-3'
)
cooling_intake_template = CoolingIntakeTemplate.objects.create(
device_type=device_type,
name='Cooling Port 1',
type=CoolingConnectorTypeChoices.TYPE_UQD,
diameter=Decimal('25'),
diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
max_flow=100,
max_flow_unit=FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE
)
CoolingOutflowTemplate.objects.create(
device_type=device_type,
name='Cooling Outlet 1',
type=CoolingConnectorTypeChoices.TYPE_UQD,
diameter=Decimal('25'),
diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER
)
device = Device.objects.create(
site=self.site,
device_type=device_type,
role=self.role,
name='Device 3'
)
cooling_intake = CoolingIntake.objects.get(
device=device,
name='Cooling Port 1',
type=CoolingConnectorTypeChoices.TYPE_UQD,
diameter=Decimal('25'),
diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
max_flow=100,
max_flow_unit=FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE
)
self.assertEqual(cooling_intake_template.max_flow, cooling_intake.max_flow)
CoolingOutflow.objects.get(
device=device,
name='Cooling Outlet 1',
type=CoolingConnectorTypeChoices.TYPE_UQD,
diameter=Decimal('25'),
diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER
)
def test_cooling_choice_colors_resolve(self):
"""
ChoiceFieldColumn and ChoiceAttr both render a badge color by calling get_FOO_color() on the
instance, so every model exposing a colored cooling choice must implement the accessor.
"""
for model in (Device, DeviceType, ModuleType):
with self.subTest(model=model.__name__):
instance = model(cooling_method=CoolingMethodChoices.METHOD_LIQUID)
self.assertEqual(
instance.get_cooling_method_color(),
CoolingMethodChoices.colors[CoolingMethodChoices.METHOD_LIQUID]
)
# An unset value has no color, which the consumers fall back on
self.assertIsNone(model().get_cooling_method_color())
for model in (Rack, RackType):
with self.subTest(model=model.__name__):
instance = model(cooling_capability=RackCoolingCapabilityChoices.CAPABILITY_HYBRID)
self.assertEqual(
instance.get_cooling_capability_color(),
RackCoolingCapabilityChoices.colors[RackCoolingCapabilityChoices.CAPABILITY_HYBRID]
)
self.assertIsNone(model().get_cooling_capability_color())
def test_measurements_below_minimum_rejected(self):
"""
A populated diameter or flow rate must be positive and non-zero: anything below the smallest storable
value (0.01) should raise a ValidationError (via MinValueValidator) rather than a raw ValueError from
the unit conversion in save().
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer, model='Device Type 8', slug='device-type-8'
)
device = Device.objects.create(
site=self.site, device_type=device_type, role=self.role, name='Device F'
)
cooling_source = CoolingSource.objects.create(
site=self.site, name='Cooling Source F', type=CoolingSourceTypeChoices.TYPE_CHILLER
)
for value in (Decimal('-5'), Decimal('0')):
with self.subTest(diameter=value):
cooling_intake = CoolingIntake(
device=device,
name='Cooling Port 1',
diameter=value,
diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
)
with self.assertRaises(ValidationError):
cooling_intake.full_clean()
with self.subTest(max_flow=value):
cooling_intake = CoolingIntake(
device=device,
name='Cooling Port 1',
max_flow=value,
max_flow_unit=FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
)
with self.assertRaises(ValidationError):
cooling_intake.full_clean()
with self.subTest(feed_max_flow=value):
cooling_feed = CoolingFeed(
cooling_source=cooling_source,
name='Cooling Feed F',
max_flow=value,
max_flow_unit=FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
)
with self.assertRaises(ValidationError):
cooling_feed.full_clean()
# The minimum itself is permitted
CoolingIntake(
device=device,
name='Cooling Port 1',
diameter=Decimal('0.01'),
diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
max_flow=Decimal('0.01'),
max_flow_unit=FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
).full_clean()
# Sub-unit values are permitted: fractional-inch fittings (e.g. 1/2" NPT) and cold plates rated below
# one gallon per minute are both commonplace.
CoolingIntake(
device=device,
name='Cooling Port 1',
diameter=Decimal('0.5'),
diameter_unit=DiameterUnitChoices.UNIT_INCH,
max_flow=Decimal('0.75'),
max_flow_unit=FlowRateUnitChoices.UNIT_GALLONS_PER_MINUTE,
).full_clean()
CoolingFeed(
cooling_source=cooling_source,
name='Cooling Feed F',
max_flow=Decimal('0.5'),
max_flow_unit=FlowRateUnitChoices.UNIT_GALLONS_PER_MINUTE,
).full_clean()
def test_parent_intake_resolved_on_device_instantiation(self):
"""
A CoolingOutflowTemplate with a parent CoolingIntakeTemplate should resolve to the newly created
CoolingIntake on the same device. This depends on intakes being instantiated before outflows.
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer, model='Device Type 12', slug='device-type-12'
)
cooling_intake_template = CoolingIntakeTemplate.objects.create(
device_type=device_type, name='Cooling Port 1'
)
CoolingOutflowTemplate.objects.create(
device_type=device_type, name='Cooling Outlet 1', cooling_intake=cooling_intake_template
)
# A second outflow with no parent must remain unassigned
CoolingOutflowTemplate.objects.create(device_type=device_type, name='Cooling Outlet 2')
device = Device.objects.create(
site=self.site, device_type=device_type, role=self.role, name='Device J'
)
cooling_intake = CoolingIntake.objects.get(device=device, name='Cooling Port 1')
self.assertEqual(
CoolingOutflow.objects.get(device=device, name='Cooling Outlet 1').cooling_intake,
cooling_intake
)
self.assertIsNone(CoolingOutflow.objects.get(device=device, name='Cooling Outlet 2').cooling_intake)
def test_parent_intake_resolved_on_module_instantiation(self):
"""
The parent intake of a CoolingOutflowTemplate should likewise resolve when the components are
instantiated for a Module rather than a Device.
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer, model='Device Type 13', slug='device-type-13'
)
device = Device.objects.create(
site=self.site, device_type=device_type, role=self.role, name='Device K'
)
module_bay = ModuleBay.objects.create(device=device, name='Module Bay 1')
module_type = ModuleType.objects.create(manufacturer=self.manufacturer, model='Module Type 1')
cooling_intake_template = CoolingIntakeTemplate.objects.create(
module_type=module_type, name='Cooling Port 1'
)
CoolingOutflowTemplate.objects.create(
module_type=module_type, name='Cooling Outlet 1', cooling_intake=cooling_intake_template
)
module = Module.objects.create(device=device, module_bay=module_bay, module_type=module_type)
cooling_intake = CoolingIntake.objects.get(module=module, name='Cooling Port 1')
cooling_outflow = CoolingOutflow.objects.get(module=module, name='Cooling Outlet 1')
self.assertEqual(cooling_outflow.cooling_intake, cooling_intake)
self.assertEqual(cooling_outflow.device, device)
def test_measurements_normalized_on_save(self):
"""
Saving a component should populate the normalized _abs_* columns, converting from the selected
unit to the canonical unit (millimeters for diameter, liters per minute for flow).
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer, model='Device Type 9', slug='device-type-9'
)
device = Device.objects.create(
site=self.site, device_type=device_type, role=self.role, name='Device G'
)
cooling_intake = CoolingIntake.objects.create(
device=device,
name='Cooling Port 1',
diameter=Decimal('1'),
diameter_unit=DiameterUnitChoices.UNIT_INCH,
max_flow=Decimal('10'),
max_flow_unit=FlowRateUnitChoices.UNIT_GALLONS_PER_MINUTE,
)
cooling_intake.refresh_from_db()
self.assertEqual(cooling_intake._abs_diameter, Decimal('25.4'))
self.assertEqual(cooling_intake._abs_max_flow, Decimal('37.8541'))
# The public aliases used by templates should mirror the underscore-prefixed columns
self.assertEqual(cooling_intake.abs_diameter, cooling_intake._abs_diameter)
self.assertEqual(cooling_intake.abs_max_flow, cooling_intake._abs_max_flow)
# Clearing a value should null both its unit and its normalized column
cooling_intake.diameter = None
cooling_intake.max_flow = None
cooling_intake.save()
cooling_intake.refresh_from_db()
self.assertIsNone(cooling_intake.diameter_unit)
self.assertIsNone(cooling_intake._abs_diameter)
self.assertIsNone(cooling_intake.max_flow_unit)
self.assertIsNone(cooling_intake._abs_max_flow)
def test_measurements_normalized_on_component_instantiation(self):
"""
Components instantiated from templates are written via bulk_create, which bypasses save(); the
normalized _abs_* columns must still be populated.
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer, model='Device Type 10', slug='device-type-10'
)
CoolingIntakeTemplate.objects.create(
device_type=device_type,
name='Cooling Port 1',
diameter=Decimal('1'),
diameter_unit=DiameterUnitChoices.UNIT_INCH,
max_flow=Decimal('6'),
max_flow_unit=FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR,
)
CoolingOutflowTemplate.objects.create(
device_type=device_type,
name='Cooling Outlet 1',
diameter=Decimal('2.5'),
diameter_unit=DiameterUnitChoices.UNIT_CENTIMETER,
)
device = Device.objects.create(
site=self.site, device_type=device_type, role=self.role, name='Device H'
)
cooling_intake = CoolingIntake.objects.get(device=device, name='Cooling Port 1')
self.assertEqual(cooling_intake._abs_diameter, Decimal('25.4'))
self.assertEqual(cooling_intake._abs_max_flow, Decimal('100'))
cooling_outflow = CoolingOutflow.objects.get(device=device, name='Cooling Outlet 1')
self.assertEqual(cooling_outflow._abs_diameter, Decimal('25'))
def test_measurement_unit_required(self):
"""
Setting a measurement without its accompanying unit should raise a ValidationError.
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer, model='Device Type 11', slug='device-type-11'
)
device = Device.objects.create(
site=self.site, device_type=device_type, role=self.role, name='Device I'
)
with self.assertRaises(ValidationError):
CoolingIntake(device=device, name='Cooling Port 1', diameter=Decimal('25')).full_clean()
with self.assertRaises(ValidationError):
CoolingIntake(device=device, name='Cooling Port 2', max_flow=Decimal('100')).full_clean()
with self.assertRaises(ValidationError):
CoolingOutflow(device=device, name='Cooling Outlet 1', diameter=Decimal('25')).full_clean()
def test_cooling_feed_flow_normalized(self):
"""
CoolingFeed shares the flow-rate normalization applied to device components, and likewise requires
a unit whenever a flow rate is set.
"""
cooling_source = CoolingSource.objects.create(
site=self.site,
name='Cooling Source 1',
type=CoolingSourceTypeChoices.TYPE_CHILLER,
)
cooling_feed = CoolingFeed.objects.create(
cooling_source=cooling_source,
name='Cooling Feed 1',
max_flow=Decimal('6'),
max_flow_unit=FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR,
)
cooling_feed.refresh_from_db()
self.assertEqual(cooling_feed._abs_max_flow, Decimal('100'))
self.assertEqual(cooling_feed.abs_max_flow, cooling_feed._abs_max_flow)
with self.assertRaises(ValidationError):
CoolingFeed(
cooling_source=cooling_source, name='Cooling Feed 2', max_flow=Decimal('100')
).full_clean()
def test_cooling_outflow_clean_different_device(self):
"""
CoolingOutflow.clean() should raise a ValidationError when its cooling_intake belongs to a
different device.
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer,
model='Device Type 4',
slug='device-type-4'
)
device1 = Device.objects.create(
site=self.site, device_type=device_type, role=self.role, name='Device A'
)
device2 = Device.objects.create(
site=self.site, device_type=device_type, role=self.role, name='Device B'
)
cooling_intake = CoolingIntake.objects.create(device=device1, name='Cooling Port 1')
cooling_outflow = CoolingOutflow(device=device2, name='Cooling Outlet 1', cooling_intake=cooling_intake)
with self.assertRaises(ValidationError):
cooling_outflow.full_clean()
def test_cooling_source_location_site_mismatch(self):
"""
CoolingSource.clean() should raise a ValidationError when its location belongs to a different site.
"""
site2 = Site.objects.create(name='Site 2', slug='site-2')
location = Location.objects.create(name='Location 1', slug='location-1', site=site2)
cooling_source = CoolingSource(
site=self.site,
location=location,
name='Cooling Source 1',
type=CoolingSourceTypeChoices.TYPE_CHILLER,
status=CoolingSourceStatusChoices.STATUS_ACTIVE,
)
with self.assertRaises(ValidationError):
cooling_source.full_clean()
def test_cooling_feed_rack_site_mismatch(self):
"""
CoolingFeed.clean() should raise a ValidationError when its rack is in a different site than the
cooling source.
"""
site2 = Site.objects.create(name='Site 3', slug='site-3')
cooling_source = CoolingSource.objects.create(
site=self.site,
name='Cooling Source 3',
type=CoolingSourceTypeChoices.TYPE_CHILLER,
status=CoolingSourceStatusChoices.STATUS_ACTIVE,
)
rack = Rack.objects.create(name='Rack 1', site=site2, status=RackStatusChoices.STATUS_ACTIVE)
cooling_feed = CoolingFeed(
cooling_source=cooling_source,
rack=rack,
name='Cooling Feed 1',
status=CoolingFeedStatusChoices.STATUS_ACTIVE,
)
with self.assertRaises(ValidationError):
cooling_feed.full_clean()
def test_cooling_chain_valid(self):
"""
A non-looping intake/outflow assignment should pass validation.
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer, model='Device Type 5', slug='device-type-5'
)
device = Device.objects.create(
site=self.site, device_type=device_type, role=self.role, name='Device C'
)
cooling_outflow = CoolingOutflow.objects.create(device=device, name='Cooling Outlet 1')
cooling_intake = CoolingIntake(device=device, name='Cooling Port 1', cooling_outflow=cooling_outflow)
# Should not raise
cooling_intake.full_clean()
def test_cooling_intake_loop_rejected(self):
"""
Closing the intake/outflow chain into a loop from the intake side should raise a ValidationError.
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer, model='Device Type 6', slug='device-type-6'
)
device = Device.objects.create(
site=self.site, device_type=device_type, role=self.role, name='Device D'
)
cooling_intake = CoolingIntake.objects.create(device=device, name='Cooling Port 1')
cooling_outflow = CoolingOutflow.objects.create(
device=device, name='Cooling Outlet 1', cooling_intake=cooling_intake
)
# The outflow is fed by the intake; supplying that same intake from the outflow closes the loop
cooling_intake.cooling_outflow = cooling_outflow
with self.assertRaises(ValidationError):
cooling_intake.full_clean()
def test_cooling_outflow_loop_rejected(self):
"""
Closing the intake/outflow chain into a loop from the outflow side should raise a ValidationError.
"""
device_type = DeviceType.objects.create(
manufacturer=self.manufacturer, model='Device Type 7', slug='device-type-7'
)
device = Device.objects.create(
site=self.site, device_type=device_type, role=self.role, name='Device E'
)
cooling_outflow = CoolingOutflow.objects.create(device=device, name='Cooling Outlet 1')
cooling_intake = CoolingIntake.objects.create(
device=device, name='Cooling Port 1', cooling_outflow=cooling_outflow
)
# The intake is supplied by the outflow; feeding that same intake into the outflow closes the loop
cooling_outflow.cooling_intake = cooling_intake
with self.assertRaises(ValidationError):
cooling_outflow.full_clean()

View File

@ -195,6 +195,26 @@ class PowerFeedTableTestCase(TableTestCases.StandardTableTestCase):
table = PowerFeedTable
#
# Cooling
#
class CoolingSourceTableTestCase(TableTestCases.StandardTableTestCase):
table = CoolingSourceTable
class CoolingFeedTableTestCase(TableTestCases.StandardTableTestCase):
table = CoolingFeedTable
class CoolingIntakeTableTestCase(TableTestCases.StandardTableTestCase):
table = CoolingIntakeTable
class CoolingOutflowTableTestCase(TableTestCases.StandardTableTestCase):
table = CoolingOutflowTable
#
# Virtual chassis
#

View File

@ -19,7 +19,13 @@ from dcim.constants import *
from dcim.models import *
from extras.models import ConfigTemplate
from ipam.models import ASN, RIR, VLAN, VRF
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices
from netbox.choices import (
CSVDelimiterChoices,
DiameterUnitChoices,
FlowRateUnitChoices,
ImportFormatChoices,
WeightUnitChoices,
)
from tenancy.models import Tenant
from users.models import ObjectPermission, User
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
@ -930,6 +936,36 @@ power-outlets:
type: iec-60320-c13
power_port: Power Port 1
feed_leg: A
cooling-intakes:
- name: Cooling Intake 1
type: uqd
diameter: 1
diameter_unit: in
max_flow: 100
max_flow_unit: lpm
- name: Cooling Intake 2
type: uqd
diameter: 1
diameter_unit: in
- name: Cooling Intake 3
type: uqd
diameter: 1
diameter_unit: in
cooling-outflows:
- name: Cooling Outflow 1
type: uqdb
diameter: 25
diameter_unit: mm
cooling_intake: Cooling Intake 1
- name: Cooling Outflow 2
type: uqdb
diameter: 25
diameter_unit: mm
cooling_intake: Cooling Intake 1
- name: Cooling Outflow 3
type: uqdb
diameter: 25
diameter_unit: mm
interfaces:
- name: Interface 1
type: 1000base-t
@ -991,6 +1027,8 @@ inventory-items:
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_coolingintaketemplate',
'dcim.add_coolingoutflowtemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
@ -1041,6 +1079,26 @@ inventory-items:
self.assertEqual(po1.power_port, pp1)
self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
self.assertEqual(device_type.coolingintaketemplates.count(), 3)
ci1 = CoolingIntakeTemplate.objects.first()
self.assertEqual(ci1.name, 'Cooling Intake 1')
self.assertEqual(ci1.type, CoolingConnectorTypeChoices.TYPE_UQD)
self.assertEqual(ci1.diameter, 1)
self.assertEqual(ci1.diameter_unit, DiameterUnitChoices.UNIT_INCH)
self.assertEqual(ci1.max_flow, 100)
self.assertEqual(ci1.max_flow_unit, FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE)
# The normalized columns are populated on import
self.assertEqual(ci1._abs_diameter, Decimal('25.4'))
self.assertEqual(ci1._abs_max_flow, Decimal('100'))
self.assertEqual(device_type.coolingoutflowtemplates.count(), 3)
co1 = CoolingOutflowTemplate.objects.first()
self.assertEqual(co1.name, 'Cooling Outflow 1')
self.assertEqual(co1.type, CoolingConnectorTypeChoices.TYPE_UQDB)
self.assertEqual(co1.diameter, 25)
self.assertEqual(co1.diameter_unit, DiameterUnitChoices.UNIT_MILLIMETER)
self.assertEqual(co1.cooling_intake, ci1)
self.assertEqual(device_type.interfacetemplates.count(), 3)
iface1 = InterfaceTemplate.objects.first()
self.assertEqual(iface1.name, 'Interface 1')
@ -1087,6 +1145,8 @@ inventory-items:
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_coolingintaketemplate',
'dcim.add_coolingoutflowtemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
@ -1139,6 +1199,8 @@ module-bays:
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_coolingintaketemplate',
'dcim.add_coolingoutflowtemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
@ -1175,6 +1237,8 @@ console-ports: {value}
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_coolingintaketemplate',
'dcim.add_coolingoutflowtemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
@ -1299,6 +1363,8 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_coolingintaketemplate',
'dcim.add_coolingoutflowtemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
@ -1315,6 +1381,8 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_coolingintaketemplate',
'dcim.add_coolingoutflowtemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
@ -1331,6 +1399,8 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_coolingintaketemplate',
'dcim.add_coolingoutflowtemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
@ -1352,6 +1422,8 @@ class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_coolingintaketemplate',
'dcim.add_coolingoutflowtemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
@ -1524,6 +1596,26 @@ power-outlets:
type: iec-60320-c13
power_port: Power Port 1
feed_leg: A
cooling-intakes:
- name: Cooling Intake 1
type: uqd
diameter: 1
diameter_unit: in
max_flow: 6
max_flow_unit: m3ph
- name: Cooling Intake 2
type: uqd
- name: Cooling Intake 3
type: uqd
cooling-outflows:
- name: Cooling Outflow 1
type: uqdb
cooling_intake: Cooling Intake 1
- name: Cooling Outflow 2
type: uqdb
cooling_intake: Cooling Intake 1
- name: Cooling Outflow 3
type: uqdb
interfaces:
- name: Interface 1
type: 1000base-t
@ -1574,6 +1666,8 @@ module-bays:
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_coolingintaketemplate',
'dcim.add_coolingoutflowtemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
@ -1616,6 +1710,24 @@ module-bays:
self.assertEqual(po1.power_port, pp1)
self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)
self.assertEqual(module_type.coolingintaketemplates.count(), 3)
ci1 = CoolingIntakeTemplate.objects.first()
self.assertEqual(ci1.name, 'Cooling Intake 1')
self.assertEqual(ci1.type, CoolingConnectorTypeChoices.TYPE_UQD)
self.assertEqual(ci1.diameter, 1)
self.assertEqual(ci1.diameter_unit, DiameterUnitChoices.UNIT_INCH)
self.assertEqual(ci1.max_flow, 6)
self.assertEqual(ci1.max_flow_unit, FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR)
# The normalized columns are populated on import
self.assertEqual(ci1._abs_diameter, Decimal('25.4'))
self.assertEqual(ci1._abs_max_flow, Decimal('100'))
self.assertEqual(module_type.coolingoutflowtemplates.count(), 3)
co1 = CoolingOutflowTemplate.objects.first()
self.assertEqual(co1.name, 'Cooling Outflow 1')
self.assertEqual(co1.type, CoolingConnectorTypeChoices.TYPE_UQDB)
self.assertEqual(co1.cooling_intake, ci1)
self.assertEqual(module_type.interfacetemplates.count(), 3)
iface1 = InterfaceTemplate.objects.first()
self.assertEqual(iface1.name, 'Interface 1')
@ -4660,6 +4772,358 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
self.assertHttpStatus(response, 200)
class CoolingIntakeTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = CoolingIntakeTemplate
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
CoolingIntakeTemplate.objects.bulk_create((
CoolingIntakeTemplate(device_type=devicetype, name='Cooling Port Template 1'),
CoolingIntakeTemplate(device_type=devicetype, name='Cooling Port Template 2'),
CoolingIntakeTemplate(device_type=devicetype, name='Cooling Port Template 3'),
))
cls.form_data = {
'device_type': devicetype.pk,
'name': 'Cooling Port Template X',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'diameter': Decimal('25'),
'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
'max_flow': 100,
'max_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
}
cls.bulk_create_data = {
'device_type': devicetype.pk,
'name': 'Cooling Port Template [4-6]',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'diameter': Decimal('25'),
'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
'max_flow': 100,
'max_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
}
cls.bulk_edit_data = {
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'diameter': Decimal('25'),
'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
'max_flow': 100,
'max_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
}
class CoolingOutflowTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
model = CoolingOutflowTemplate
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
CoolingOutflowTemplate.objects.bulk_create((
CoolingOutflowTemplate(device_type=devicetype, name='Cooling Outlet Template 1'),
CoolingOutflowTemplate(device_type=devicetype, name='Cooling Outlet Template 2'),
CoolingOutflowTemplate(device_type=devicetype, name='Cooling Outlet Template 3'),
))
coolingintakes = (
CoolingIntakeTemplate(device_type=devicetype, name='Cooling Port Template 1'),
)
CoolingIntakeTemplate.objects.bulk_create(coolingintakes)
cls.form_data = {
'device_type': devicetype.pk,
'name': 'Cooling Outlet Template X',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'diameter': Decimal('25'),
'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
'cooling_intake': coolingintakes[0].pk,
}
cls.bulk_create_data = {
'device_type': devicetype.pk,
'name': 'Cooling Outlet Template [4-6]',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'diameter': Decimal('25'),
'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
'cooling_intake': coolingintakes[0].pk,
}
cls.bulk_edit_data = {
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'diameter': Decimal('25'),
'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
}
class CoolingIntakeTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = CoolingIntake
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
cooling_intakes = (
CoolingIntake(device=device, name='Cooling Port 1'),
CoolingIntake(device=device, name='Cooling Port 2'),
CoolingIntake(device=device, name='Cooling Port 3'),
)
CoolingIntake.objects.bulk_create(cooling_intakes)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'device': device.pk,
'name': 'Cooling Port X',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'diameter': Decimal('25'),
'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
'max_flow': 100,
'max_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
'description': 'A cooling port',
'tags': [t.pk for t in tags],
}
cls.bulk_create_data = {
'device': device.pk,
'name': 'Cooling Port [4-6]]',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'diameter': Decimal('25'),
'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
'max_flow': 100,
'max_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
'description': 'A cooling port',
'tags': [t.pk for t in tags],
}
cls.bulk_edit_data = {
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'diameter': Decimal('25'),
'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
'max_flow': 100,
'max_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
'description': 'New description',
}
cls.csv_data = (
"device,name",
"Device 1,Cooling Port 4",
"Device 1,Cooling Port 5",
"Device 1,Cooling Port 6",
)
cls.csv_update_data = (
"id,name,description",
f"{cooling_intakes[0].pk},Cooling Port 7,New description7",
f"{cooling_intakes[1].pk},Cooling Port 8,New description8",
f"{cooling_intakes[2].pk},Cooling Port 9,New description9",
)
class CoolingOutflowTestCase(ViewTestCases.DeviceComponentViewTestCase):
model = CoolingOutflow
validation_excluded_fields = ('name', 'label')
@classmethod
def setUpTestData(cls):
device = create_test_device('Device 1')
coolingintakes = (
CoolingIntake(device=device, name='Cooling Port 1'),
CoolingIntake(device=device, name='Cooling Port 2'),
)
CoolingIntake.objects.bulk_create(coolingintakes)
cooling_outflows = (
CoolingOutflow(device=device, name='Cooling Outlet 1', cooling_intake=coolingintakes[0]),
CoolingOutflow(device=device, name='Cooling Outlet 2', cooling_intake=coolingintakes[0]),
CoolingOutflow(device=device, name='Cooling Outlet 3', cooling_intake=coolingintakes[0]),
)
CoolingOutflow.objects.bulk_create(cooling_outflows)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'device': device.pk,
'name': 'Cooling Outlet X',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'diameter': Decimal('25'),
'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
'cooling_intake': coolingintakes[1].pk,
'description': 'A cooling outlet',
'tags': [t.pk for t in tags],
}
cls.bulk_create_data = {
'device': device.pk,
'name': 'Cooling Outlet [4-6]',
'type': CoolingConnectorTypeChoices.TYPE_UQD,
'diameter': Decimal('25'),
'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
'cooling_intake': coolingintakes[1].pk,
'description': 'A cooling outlet',
'tags': [t.pk for t in tags],
}
cls.bulk_edit_data = {
'cooling_intake': coolingintakes[1].pk,
'description': 'New description',
}
cls.csv_data = (
"device,name",
"Device 1,Cooling Outlet 4",
"Device 1,Cooling Outlet 5",
"Device 1,Cooling Outlet 6",
)
cls.csv_update_data = (
"id,name,description",
f"{cooling_outflows[0].pk},Cooling Outlet 7,New description7",
f"{cooling_outflows[1].pk},Cooling Outlet 8,New description8",
f"{cooling_outflows[2].pk},Cooling Outlet 9,New description9",
)
class CoolingSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = CoolingSource
@classmethod
def setUpTestData(cls):
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
)
Site.objects.bulk_create(sites)
locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
)
for location in locations:
location.save()
cooling_sources = (
CoolingSource(
site=sites[0], location=locations[0], name='Cooling Source 1',
type=CoolingSourceTypeChoices.TYPE_CHILLER
),
CoolingSource(
site=sites[0], location=locations[0], name='Cooling Source 2',
type=CoolingSourceTypeChoices.TYPE_CHILLER
),
CoolingSource(
site=sites[0], location=locations[0], name='Cooling Source 3',
type=CoolingSourceTypeChoices.TYPE_CHILLER
),
)
CoolingSource.objects.bulk_create(cooling_sources)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'site': sites[1].pk,
'location': locations[1].pk,
'name': 'Cooling Source X',
'type': CoolingSourceTypeChoices.TYPE_COOLING_TOWER,
'status': CoolingSourceStatusChoices.STATUS_ACTIVE,
'fluid_type': FluidTypeChoices.FLUID_WATER,
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"site,location,name,type,status",
"Site 1,Location 1,Cooling Source 4,chiller,active",
"Site 1,Location 1,Cooling Source 5,chiller,active",
"Site 1,Location 1,Cooling Source 6,chiller,active",
)
cls.csv_update_data = (
"id,name",
f"{cooling_sources[0].pk},Cooling Source 7",
f"{cooling_sources[1].pk},Cooling Source 8",
f"{cooling_sources[2].pk},Cooling Source 9",
)
cls.bulk_edit_data = {
'site': sites[1].pk,
'location': locations[1].pk,
}
class CoolingFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = CoolingFeed
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 1', slug='site-1')
cooling_sources = (
CoolingSource(site=site, name='Cooling Source 1', type=CoolingSourceTypeChoices.TYPE_CHILLER),
CoolingSource(site=site, name='Cooling Source 2', type=CoolingSourceTypeChoices.TYPE_CHILLER),
)
CoolingSource.objects.bulk_create(cooling_sources)
racks = (
Rack(site=site, name='Rack 1'),
Rack(site=site, name='Rack 2'),
)
Rack.objects.bulk_create(racks)
cooling_feeds = (
CoolingFeed(name='Cooling Feed 1', cooling_source=cooling_sources[0], rack=racks[0]),
CoolingFeed(name='Cooling Feed 2', cooling_source=cooling_sources[0], rack=racks[0]),
CoolingFeed(name='Cooling Feed 3', cooling_source=cooling_sources[0], rack=racks[0]),
)
CoolingFeed.objects.bulk_create(cooling_feeds)
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Cooling Feed X',
'cooling_source': cooling_sources[1].pk,
'rack': racks[1].pk,
'status': CoolingFeedStatusChoices.STATUS_PLANNED,
'cooling_capacity': 100,
'max_flow': 50,
'max_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
'comments': 'New comments',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"site,cooling_source,name,status",
"Site 1,Cooling Source 1,Cooling Feed 4,active",
"Site 1,Cooling Source 1,Cooling Feed 5,active",
"Site 1,Cooling Source 1,Cooling Feed 6,active",
)
cls.csv_update_data = (
"id,name,status",
f"{cooling_feeds[0].pk},Cooling Feed 7,{CoolingFeedStatusChoices.STATUS_PLANNED}",
f"{cooling_feeds[1].pk},Cooling Feed 8,{CoolingFeedStatusChoices.STATUS_PLANNED}",
f"{cooling_feeds[2].pk},Cooling Feed 9,{CoolingFeedStatusChoices.STATUS_PLANNED}",
)
cls.bulk_edit_data = {
'cooling_source': cooling_sources[1].pk,
'rack': racks[1].pk,
'status': CoolingFeedStatusChoices.STATUS_PLANNED,
'cooling_capacity': 100,
'max_flow': 50,
'max_flow_unit': FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE,
'comments': 'New comments',
}
class VirtualDeviceContextTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = VirtualDeviceContext

View File

@ -79,6 +79,8 @@ class RackPanel(panels.ObjectAttributesPanel):
serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
asset_tag = attrs.TextAttr('asset_tag', style='font-monospace', copy_button=True)
airflow = attrs.ChoiceAttr('airflow')
cooling_capability = attrs.ChoiceAttr('cooling_capability')
cooling_capacity = attrs.TextAttr('cooling_capacity', format_string=_('{} kW'))
space_utilization = attrs.UtilizationAttr('get_utilization')
power_utilization = attrs.UtilizationAttr('get_power_utilization')
@ -106,6 +108,8 @@ class RackTypePanel(panels.ObjectAttributesPanel):
manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
model = attrs.TextAttr('model')
description = attrs.TextAttr('description')
cooling_capability = attrs.ChoiceAttr('cooling_capability')
cooling_capacity = attrs.TextAttr('cooling_capacity', format_string=_('{} kW'))
class DevicePanel(panels.ObjectAttributesPanel):
@ -118,6 +122,7 @@ class DevicePanel(panels.ObjectAttributesPanel):
gps_coordinates = attrs.GPSCoordinatesAttr()
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
description = attrs.TextAttr('description')
cooling_method = attrs.ChoiceAttr('cooling_method')
airflow = attrs.ChoiceAttr('airflow')
serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
asset_tag = attrs.TextAttr('asset_tag', style='font-monospace', copy_button=True)
@ -181,6 +186,7 @@ class DeviceTypePanel(panels.ObjectAttributesPanel):
full_depth = attrs.BooleanAttr('is_full_depth')
weight = attrs.WeightAttr('weight')
subdevice_role = attrs.ChoiceAttr('subdevice_role', label=_('Parent/child'))
cooling_method = attrs.ChoiceAttr('cooling_method')
airflow = attrs.ChoiceAttr('airflow')
end_of_life = attrs.DateTimeAttr('end_of_life', spec='date')
front_image = attrs.ImageAttr('front_image')
@ -216,6 +222,7 @@ class ModuleTypePanel(panels.ObjectAttributesPanel):
model = attrs.TextAttr('model', label=_('Model name'))
part_number = attrs.TextAttr('part_number')
description = attrs.TextAttr('description')
cooling_method = attrs.ChoiceAttr('cooling_method')
airflow = attrs.ChoiceAttr('airflow')
weight = attrs.WeightAttr('weight')
end_of_life = attrs.DateTimeAttr('end_of_life', spec='date')
@ -273,6 +280,29 @@ class PowerOutletPanel(panels.ObjectAttributesPanel):
feed_leg = attrs.ChoiceAttr('feed_leg')
class CoolingIntakePanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
module = attrs.RelatedObjectAttr('module', linkify=True)
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
type = attrs.ChoiceAttr('type')
diameter = attrs.DiameterAttr('diameter')
max_flow = attrs.FlowRateAttr('max_flow')
cooling_outflow = attrs.RelatedObjectAttr('cooling_outflow', linkify=True)
description = attrs.TextAttr('description')
class CoolingOutflowPanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
module = attrs.RelatedObjectAttr('module', linkify=True)
name = attrs.TextAttr('name')
label = attrs.TextAttr('label')
type = attrs.ChoiceAttr('type')
diameter = attrs.DiameterAttr('diameter')
cooling_intake = attrs.RelatedObjectAttr('cooling_intake', linkify=True)
description = attrs.TextAttr('description')
class FrontPortPanel(panels.ObjectAttributesPanel):
device = attrs.RelatedObjectAttr('device', linkify=True)
module = attrs.RelatedObjectAttr('module', linkify=True)
@ -399,6 +429,31 @@ class PowerFeedElectricalPanel(panels.ObjectAttributesPanel):
max_utilization = attrs.TextAttr('max_utilization', format_string='{}%')
class CoolingSourcePanel(panels.ObjectAttributesPanel):
site = attrs.RelatedObjectAttr('site', linkify=True)
location = attrs.NestedObjectAttr('location', linkify=True)
type = attrs.ChoiceAttr('type')
status = attrs.ChoiceAttr('status')
fluid_type = attrs.ChoiceAttr('fluid_type')
cooling_capacity = attrs.TextAttr('cooling_capacity', format_string=_('{} kW'))
description = attrs.TextAttr('description')
class CoolingFeedPanel(panels.ObjectAttributesPanel):
cooling_source = attrs.RelatedObjectAttr('cooling_source', linkify=True)
rack = attrs.RelatedObjectAttr('rack', linkify=True)
status = attrs.ChoiceAttr('status')
description = attrs.TextAttr('description')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
class CoolingFeedCharacteristicsPanel(panels.ObjectAttributesPanel):
title = _('Cooling Characteristics')
cooling_capacity = attrs.TextAttr('cooling_capacity', format_string=_('{} kW'))
max_flow = attrs.FlowRateAttr('max_flow')
class VirtualDeviceContextPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
device = attrs.RelatedObjectAttr('device', linkify=True)

View File

@ -62,6 +62,12 @@ urlpatterns = [
path('power-outlet-templates/', include(get_model_urls('dcim', 'poweroutlettemplate', detail=False))),
path('power-outlet-templates/<int:pk>/', include(get_model_urls('dcim', 'poweroutlettemplate'))),
path('cooling-intake-templates/', include(get_model_urls('dcim', 'coolingintaketemplate', detail=False))),
path('cooling-intake-templates/<int:pk>/', include(get_model_urls('dcim', 'coolingintaketemplate'))),
path('cooling-outflow-templates/', include(get_model_urls('dcim', 'coolingoutflowtemplate', detail=False))),
path('cooling-outflow-templates/<int:pk>/', include(get_model_urls('dcim', 'coolingoutflowtemplate'))),
path('interface-templates/', include(get_model_urls('dcim', 'interfacetemplate', detail=False))),
path('interface-templates/<int:pk>/', include(get_model_urls('dcim', 'interfacetemplate'))),
@ -123,6 +129,22 @@ urlpatterns = [
name='device_bulk_add_poweroutlet'
),
path('cooling-intakes/', include(get_model_urls('dcim', 'coolingintake', detail=False))),
path('cooling-intakes/<int:pk>/', include(get_model_urls('dcim', 'coolingintake'))),
path(
'devices/cooling-intakes/add/',
views.DeviceBulkAddCoolingIntakeView.as_view(),
name='device_bulk_add_coolingintake'
),
path('cooling-outflows/', include(get_model_urls('dcim', 'coolingoutflow', detail=False))),
path('cooling-outflows/<int:pk>/', include(get_model_urls('dcim', 'coolingoutflow'))),
path(
'devices/cooling-outflows/add/',
views.DeviceBulkAddCoolingOutflowView.as_view(),
name='device_bulk_add_coolingoutflow'
),
path('interfaces/', include(get_model_urls('dcim', 'interface', detail=False))),
path('interfaces/<int:pk>/', include(get_model_urls('dcim', 'interface'))),
path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
@ -178,6 +200,12 @@ urlpatterns = [
path('power-feeds/', include(get_model_urls('dcim', 'powerfeed', detail=False))),
path('power-feeds/<int:pk>/', include(get_model_urls('dcim', 'powerfeed'))),
path('cooling-sources/', include(get_model_urls('dcim', 'coolingsource', detail=False))),
path('cooling-sources/<int:pk>/', include(get_model_urls('dcim', 'coolingsource'))),
path('cooling-feeds/', include(get_model_urls('dcim', 'coolingfeed', detail=False))),
path('cooling-feeds/<int:pk>/', include(get_model_urls('dcim', 'coolingfeed'))),
path('mac-addresses/', include(get_model_urls('dcim', 'macaddress', detail=False))),
path('mac-addresses/<int:pk>/', include(get_model_urls('dcim', 'macaddress'))),

View File

@ -1464,9 +1464,9 @@ class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView):
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance, omit=[
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate,
InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate,
RearPortTemplate,
ConsolePortTemplate, ConsoleServerPortTemplate, CoolingIntakeTemplate, CoolingOutflowTemplate,
DeviceBayTemplate, FrontPortTemplate, InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate,
PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
]),
}
@ -1543,6 +1543,36 @@ class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
)
@register_model_view(DeviceType, 'coolingintakes', path='cooling-intakes')
class DeviceTypeCoolingIntakesView(DeviceTypeComponentsView):
child_model = CoolingIntakeTemplate
table = tables.CoolingIntakeTemplateTable
filterset = filtersets.CoolingIntakeTemplateFilterSet
viewname = 'dcim:devicetype_coolingintakes'
tab = ViewTab(
label=_('Cooling Intakes'),
badge=lambda obj: obj.cooling_intake_template_count,
permission='dcim.view_coolingintaketemplate',
weight=590,
hide_if_empty=True
)
@register_model_view(DeviceType, 'coolingoutflows', path='cooling-outflows')
class DeviceTypeCoolingOutflowsView(DeviceTypeComponentsView):
child_model = CoolingOutflowTemplate
table = tables.CoolingOutflowTemplateTable
filterset = filtersets.CoolingOutflowTemplateFilterSet
viewname = 'dcim:devicetype_coolingoutflows'
tab = ViewTab(
label=_('Cooling Outflows'),
badge=lambda obj: obj.cooling_outflow_template_count,
permission='dcim.view_coolingoutflowtemplate',
weight=600,
hide_if_empty=True
)
@register_model_view(DeviceType, 'interfaces')
class DeviceTypeInterfacesView(DeviceTypeComponentsView):
child_model = InterfaceTemplate
@ -1628,7 +1658,7 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
label=_('Inventory Items'),
badge=lambda obj: obj.inventory_item_template_count,
permission='dcim.view_inventoryitemtemplate',
weight=590,
weight=610,
hide_if_empty=True
)
@ -1641,6 +1671,8 @@ class DeviceTypeImportView(generic.BulkImportView):
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_coolingintaketemplate',
'dcim.add_coolingoutflowtemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
@ -1655,6 +1687,8 @@ class DeviceTypeImportView(generic.BulkImportView):
'console-server-ports': forms.ConsoleServerPortTemplateImportForm,
'power-ports': forms.PowerPortTemplateImportForm,
'power-outlets': forms.PowerOutletTemplateImportForm,
'cooling-intakes': forms.CoolingIntakeTemplateImportForm,
'cooling-outflows': forms.CoolingOutflowTemplateImportForm,
'interfaces': forms.InterfaceTemplateImportForm,
'rear-ports': forms.RearPortTemplateImportForm,
'front-ports': forms.FrontPortTemplateImportForm,
@ -1889,9 +1923,9 @@ class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView):
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance, omit=[
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate,
InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate,
RearPortTemplate,
ConsolePortTemplate, ConsoleServerPortTemplate, CoolingIntakeTemplate, CoolingOutflowTemplate,
DeviceBayTemplate, FrontPortTemplate, InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate,
PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
]),
}
@ -1995,6 +2029,36 @@ class ModuleTypePowerOutletsView(ModuleTypeComponentsView):
)
@register_model_view(ModuleType, 'coolingintakes', path='cooling-intakes')
class ModuleTypeCoolingIntakesView(ModuleTypeComponentsView):
child_model = CoolingIntakeTemplate
table = tables.CoolingIntakeTemplateTable
filterset = filtersets.CoolingIntakeTemplateFilterSet
viewname = 'dcim:moduletype_coolingintakes'
tab = ViewTab(
label=_('Cooling Intakes'),
badge=lambda obj: obj.cooling_intake_template_count,
permission='dcim.view_coolingintaketemplate',
weight=570,
hide_if_empty=True
)
@register_model_view(ModuleType, 'coolingoutflows', path='cooling-outflows')
class ModuleTypeCoolingOutflowsView(ModuleTypeComponentsView):
child_model = CoolingOutflowTemplate
table = tables.CoolingOutflowTemplateTable
filterset = filtersets.CoolingOutflowTemplateFilterSet
viewname = 'dcim:moduletype_coolingoutflows'
tab = ViewTab(
label=_('Cooling Outflows'),
badge=lambda obj: obj.cooling_outflow_template_count,
permission='dcim.view_coolingoutflowtemplate',
weight=580,
hide_if_empty=True
)
@register_model_view(ModuleType, 'interfaces')
class ModuleTypeInterfacesView(ModuleTypeComponentsView):
child_model = InterfaceTemplate
@ -2050,7 +2114,7 @@ class ModuleTypeModuleBaysView(ModuleTypeComponentsView):
label=_('Module Bays'),
badge=lambda obj: obj.module_bay_template_count,
permission='dcim.view_modulebaytemplate',
weight=570,
weight=590,
hide_if_empty=True
)
@ -2063,6 +2127,8 @@ class ModuleTypeImportView(generic.BulkImportView):
'dcim.add_consoleserverporttemplate',
'dcim.add_powerporttemplate',
'dcim.add_poweroutlettemplate',
'dcim.add_coolingintaketemplate',
'dcim.add_coolingoutflowtemplate',
'dcim.add_interfacetemplate',
'dcim.add_frontporttemplate',
'dcim.add_rearporttemplate',
@ -2075,6 +2141,8 @@ class ModuleTypeImportView(generic.BulkImportView):
'console-server-ports': forms.ConsoleServerPortTemplateImportForm,
'power-ports': forms.PowerPortTemplateImportForm,
'power-outlets': forms.PowerOutletTemplateImportForm,
'cooling-intakes': forms.CoolingIntakeTemplateImportForm,
'cooling-outflows': forms.CoolingOutflowTemplateImportForm,
'interfaces': forms.InterfaceTemplateImportForm,
'rear-ports': forms.RearPortTemplateImportForm,
'front-ports': forms.FrontPortTemplateImportForm,
@ -2306,6 +2374,88 @@ class PowerOutletTemplateBulkDeleteView(generic.BulkDeleteView):
table = tables.PowerOutletTemplateTable
#
# Cooling port templates
#
@register_model_view(CoolingIntakeTemplate, 'add', detail=False)
class CoolingIntakeTemplateCreateView(generic.ComponentCreateView):
queryset = CoolingIntakeTemplate.objects.all()
form = forms.CoolingIntakeTemplateCreateForm
model_form = forms.CoolingIntakeTemplateForm
@register_model_view(CoolingIntakeTemplate, 'edit')
class CoolingIntakeTemplateEditView(generic.ObjectEditView):
queryset = CoolingIntakeTemplate.objects.all()
form = forms.CoolingIntakeTemplateForm
@register_model_view(CoolingIntakeTemplate, 'delete')
class CoolingIntakeTemplateDeleteView(generic.ObjectDeleteView):
queryset = CoolingIntakeTemplate.objects.all()
@register_model_view(CoolingIntakeTemplate, 'bulk_edit', path='edit', detail=False)
class CoolingIntakeTemplateBulkEditView(generic.BulkEditView):
queryset = CoolingIntakeTemplate.objects.all()
table = tables.CoolingIntakeTemplateTable
form = forms.CoolingIntakeTemplateBulkEditForm
@register_model_view(CoolingIntakeTemplate, 'bulk_rename', path='rename', detail=False)
class CoolingIntakeTemplateBulkRenameView(generic.BulkRenameView):
queryset = CoolingIntakeTemplate.objects.all()
rename_fields = ('name', 'label')
@register_model_view(CoolingIntakeTemplate, 'bulk_delete', path='delete', detail=False)
class CoolingIntakeTemplateBulkDeleteView(generic.BulkDeleteView):
queryset = CoolingIntakeTemplate.objects.all()
table = tables.CoolingIntakeTemplateTable
#
# Cooling outlet templates
#
@register_model_view(CoolingOutflowTemplate, 'add', detail=False)
class CoolingOutflowTemplateCreateView(generic.ComponentCreateView):
queryset = CoolingOutflowTemplate.objects.all()
form = forms.CoolingOutflowTemplateCreateForm
model_form = forms.CoolingOutflowTemplateForm
@register_model_view(CoolingOutflowTemplate, 'edit')
class CoolingOutflowTemplateEditView(generic.ObjectEditView):
queryset = CoolingOutflowTemplate.objects.all()
form = forms.CoolingOutflowTemplateForm
@register_model_view(CoolingOutflowTemplate, 'delete')
class CoolingOutflowTemplateDeleteView(generic.ObjectDeleteView):
queryset = CoolingOutflowTemplate.objects.all()
@register_model_view(CoolingOutflowTemplate, 'bulk_edit', path='edit', detail=False)
class CoolingOutflowTemplateBulkEditView(generic.BulkEditView):
queryset = CoolingOutflowTemplate.objects.all()
table = tables.CoolingOutflowTemplateTable
form = forms.CoolingOutflowTemplateBulkEditForm
@register_model_view(CoolingOutflowTemplate, 'bulk_rename', path='rename', detail=False)
class CoolingOutflowTemplateBulkRenameView(generic.BulkRenameView):
queryset = CoolingOutflowTemplate.objects.all()
rename_fields = ('name', 'label')
@register_model_view(CoolingOutflowTemplate, 'bulk_delete', path='delete', detail=False)
class CoolingOutflowTemplateBulkDeleteView(generic.BulkDeleteView):
queryset = CoolingOutflowTemplate.objects.all()
table = tables.CoolingOutflowTemplateTable
#
# Interface templates
#
@ -2918,6 +3068,38 @@ class DevicePowerOutletsView(DeviceComponentsView):
)
@register_model_view(Device, 'coolingintakes', path='cooling-intakes')
class DeviceCoolingIntakesView(DeviceComponentsView):
child_model = CoolingIntake
table = tables.DeviceCoolingIntakeTable
filterset = filtersets.CoolingIntakeFilterSet
filterset_form = forms.CoolingIntakeFilterForm
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
tab = ViewTab(
label=_('Cooling Intakes'),
badge=lambda obj: obj.cooling_intake_count,
permission='dcim.view_coolingintake',
weight=590,
hide_if_empty=True
)
@register_model_view(Device, 'coolingoutflows', path='cooling-outflows')
class DeviceCoolingOutflowsView(DeviceComponentsView):
child_model = CoolingOutflow
table = tables.DeviceCoolingOutflowTable
filterset = filtersets.CoolingOutflowFilterSet
filterset_form = forms.CoolingOutflowFilterForm
actions = (EditObject, DeleteObject, BulkEdit, BulkRename, BulkDelete)
tab = ViewTab(
label=_('Cooling Outflows'),
badge=lambda obj: obj.cooling_outflow_count,
permission='dcim.view_coolingoutflow',
weight=600,
hide_if_empty=True
)
@register_model_view(Device, 'interfaces')
class DeviceInterfacesView(DeviceComponentsView):
child_model = Interface
@ -3016,7 +3198,7 @@ class DeviceInventoryView(DeviceComponentsView):
label=_('Inventory Items'),
badge=lambda obj: obj.inventory_item_count,
permission='dcim.view_inventoryitem',
weight=590,
weight=610,
hide_if_empty=True
)
@ -3565,6 +3747,160 @@ class PowerOutletBulkDeleteView(generic.BulkDeleteView):
register_model_view(PowerOutlet, 'trace', kwargs={'model': PowerOutlet})(PathTraceView)
#
# Cooling ports
#
@register_model_view(CoolingIntake, 'list', path='', detail=False)
class CoolingIntakeListView(generic.ObjectListView):
queryset = CoolingIntake.objects.all()
filterset = filtersets.CoolingIntakeFilterSet
filterset_form = forms.CoolingIntakeFilterForm
table = tables.CoolingIntakeTable
@register_model_view(CoolingIntake)
class CoolingIntakeView(generic.ObjectView):
queryset = CoolingIntake.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
breadcrumbs=[
Breadcrumb('device', url=object_view_url('dcim:device_coolingintakes')),
],
left_panels=[
panels.CoolingIntakePanel(),
CustomFieldsPanel(),
TagsPanel(),
],
right_panels=[
panels.InventoryItemsPanel(),
],
)
@register_model_view(CoolingIntake, 'add', detail=False)
class CoolingIntakeCreateView(generic.ComponentCreateView):
queryset = CoolingIntake.objects.all()
form = forms.CoolingIntakeCreateForm
model_form = forms.CoolingIntakeForm
@register_model_view(CoolingIntake, 'edit')
class CoolingIntakeEditView(generic.ObjectEditView):
queryset = CoolingIntake.objects.all()
form = forms.CoolingIntakeForm
@register_model_view(CoolingIntake, 'delete')
class CoolingIntakeDeleteView(generic.ObjectDeleteView):
queryset = CoolingIntake.objects.all()
@register_model_view(CoolingIntake, 'bulk_import', path='import', detail=False)
class CoolingIntakeBulkImportView(generic.BulkImportView):
queryset = CoolingIntake.objects.all()
model_form = forms.CoolingIntakeImportForm
@register_model_view(CoolingIntake, 'bulk_edit', path='edit', detail=False)
class CoolingIntakeBulkEditView(generic.BulkEditView):
queryset = CoolingIntake.objects.all()
filterset = filtersets.CoolingIntakeFilterSet
table = tables.CoolingIntakeTable
form = forms.CoolingIntakeBulkEditForm
@register_model_view(CoolingIntake, 'bulk_rename', path='rename', detail=False)
class CoolingIntakeBulkRenameView(generic.BulkRenameView):
queryset = CoolingIntake.objects.all()
filterset = filtersets.CoolingIntakeFilterSet
rename_fields = ('name', 'label')
@register_model_view(CoolingIntake, 'bulk_delete', path='delete', detail=False)
class CoolingIntakeBulkDeleteView(generic.BulkDeleteView):
queryset = CoolingIntake.objects.all()
filterset = filtersets.CoolingIntakeFilterSet
table = tables.CoolingIntakeTable
#
# Cooling outlets
#
@register_model_view(CoolingOutflow, 'list', path='', detail=False)
class CoolingOutflowListView(generic.ObjectListView):
queryset = CoolingOutflow.objects.all()
filterset = filtersets.CoolingOutflowFilterSet
filterset_form = forms.CoolingOutflowFilterForm
table = tables.CoolingOutflowTable
@register_model_view(CoolingOutflow)
class CoolingOutflowView(generic.ObjectView):
queryset = CoolingOutflow.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
breadcrumbs=[
Breadcrumb('device', url=object_view_url('dcim:device_coolingoutflows')),
],
left_panels=[
panels.CoolingOutflowPanel(),
CustomFieldsPanel(),
TagsPanel(),
],
right_panels=[
panels.InventoryItemsPanel(),
],
)
@register_model_view(CoolingOutflow, 'add', detail=False)
class CoolingOutflowCreateView(generic.ComponentCreateView):
queryset = CoolingOutflow.objects.all()
form = forms.CoolingOutflowCreateForm
model_form = forms.CoolingOutflowForm
@register_model_view(CoolingOutflow, 'edit')
class CoolingOutflowEditView(generic.ObjectEditView):
queryset = CoolingOutflow.objects.all()
form = forms.CoolingOutflowForm
@register_model_view(CoolingOutflow, 'delete')
class CoolingOutflowDeleteView(generic.ObjectDeleteView):
queryset = CoolingOutflow.objects.all()
@register_model_view(CoolingOutflow, 'bulk_import', path='import', detail=False)
class CoolingOutflowBulkImportView(generic.BulkImportView):
queryset = CoolingOutflow.objects.all()
model_form = forms.CoolingOutflowImportForm
@register_model_view(CoolingOutflow, 'bulk_edit', path='edit', detail=False)
class CoolingOutflowBulkEditView(generic.BulkEditView):
queryset = CoolingOutflow.objects.all()
filterset = filtersets.CoolingOutflowFilterSet
table = tables.CoolingOutflowTable
form = forms.CoolingOutflowBulkEditForm
@register_model_view(CoolingOutflow, 'bulk_rename', path='rename', detail=False)
class CoolingOutflowBulkRenameView(generic.BulkRenameView):
queryset = CoolingOutflow.objects.all()
filterset = filtersets.CoolingOutflowFilterSet
rename_fields = ('name', 'label')
@register_model_view(CoolingOutflow, 'bulk_delete', path='delete', detail=False)
class CoolingOutflowBulkDeleteView(generic.BulkDeleteView):
queryset = CoolingOutflow.objects.all()
filterset = filtersets.CoolingOutflowFilterSet
table = tables.CoolingOutflowTable
#
# Interfaces
#
@ -4476,6 +4812,28 @@ class DeviceBulkAddPowerOutletView(generic.BulkComponentCreateView):
default_return_url = 'dcim:device_list'
class DeviceBulkAddCoolingIntakeView(generic.BulkComponentCreateView):
parent_model = Device
parent_field = 'device'
form = forms.CoolingIntakeBulkCreateForm
queryset = CoolingIntake.objects.all()
model_form = forms.CoolingIntakeForm
filterset = filtersets.DeviceFilterSet
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
class DeviceBulkAddCoolingOutflowView(generic.BulkComponentCreateView):
parent_model = Device
parent_field = 'device'
form = forms.CoolingOutflowBulkCreateForm
queryset = CoolingOutflow.objects.all()
model_form = forms.CoolingOutflowForm
filterset = filtersets.DeviceFilterSet
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
class DeviceBulkAddInterfaceView(generic.BulkComponentCreateView):
parent_model = Device
parent_field = 'device'
@ -5185,6 +5543,170 @@ class PowerFeedBulkDeleteView(generic.BulkDeleteView):
register_model_view(PowerFeed, 'trace', kwargs={'model': PowerFeed})(PathTraceView)
#
# Cooling sources
#
@register_model_view(CoolingSource, 'list', path='', detail=False)
class CoolingSourceListView(generic.ObjectListView):
queryset = CoolingSource.objects.annotate(
coolingfeed_count=count_related(CoolingFeed, 'cooling_source')
)
filterset = filtersets.CoolingSourceFilterSet
filterset_form = forms.CoolingSourceFilterForm
table = tables.CoolingSourceTable
@register_model_view(CoolingSource)
class CoolingSourceView(GetRelatedModelsMixin, generic.ObjectView):
queryset = CoolingSource.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
breadcrumbs=[
Breadcrumb('site', url=filtered_list_url('dcim:coolingsource_list', 'site_id')),
Breadcrumb('location'),
],
left_panels=[
panels.CoolingSourcePanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CustomFieldsPanel(),
ImageAttachmentsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='dcim.CoolingFeed',
filters={'cooling_source_id': lambda ctx: ctx['object'].pk},
actions=[
actions.AddObject('dcim.CoolingFeed', url_params={'cooling_source': lambda ctx: ctx['object'].pk}),
],
),
],
)
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
@register_model_view(CoolingSource, 'add', detail=False)
@register_model_view(CoolingSource, 'edit')
class CoolingSourceEditView(generic.ObjectEditView):
queryset = CoolingSource.objects.all()
form = forms.CoolingSourceForm
@register_model_view(CoolingSource, 'delete')
class CoolingSourceDeleteView(generic.ObjectDeleteView):
queryset = CoolingSource.objects.all()
@register_model_view(CoolingSource, 'bulk_import', path='import', detail=False)
class CoolingSourceBulkImportView(generic.BulkImportView):
queryset = CoolingSource.objects.all()
model_form = forms.CoolingSourceImportForm
@register_model_view(CoolingSource, 'bulk_edit', path='edit', detail=False)
class CoolingSourceBulkEditView(generic.BulkEditView):
queryset = CoolingSource.objects.all()
filterset = filtersets.CoolingSourceFilterSet
table = tables.CoolingSourceTable
form = forms.CoolingSourceBulkEditForm
@register_model_view(CoolingSource, 'bulk_rename', path='rename', detail=False)
class CoolingSourceBulkRenameView(generic.BulkRenameView):
queryset = CoolingSource.objects.all()
filterset = filtersets.CoolingSourceFilterSet
@register_model_view(CoolingSource, 'bulk_delete', path='delete', detail=False)
class CoolingSourceBulkDeleteView(generic.BulkDeleteView):
queryset = CoolingSource.objects.annotate(
coolingfeed_count=count_related(CoolingFeed, 'cooling_source')
)
filterset = filtersets.CoolingSourceFilterSet
table = tables.CoolingSourceTable
#
# Cooling feeds
#
@register_model_view(CoolingFeed, 'list', path='', detail=False)
class CoolingFeedListView(generic.ObjectListView):
queryset = CoolingFeed.objects.all()
filterset = filtersets.CoolingFeedFilterSet
filterset_form = forms.CoolingFeedFilterForm
table = tables.CoolingFeedTable
@register_model_view(CoolingFeed)
class CoolingFeedView(generic.ObjectView):
queryset = CoolingFeed.objects.all()
template_name = 'generic/object.html'
layout = layout.SimpleLayout(
breadcrumbs=[
Breadcrumb('cooling_source.site', url=filtered_list_url('dcim:coolingfeed_list', 'site_id')),
Breadcrumb('cooling_source', url=filtered_list_url('dcim:coolingfeed_list', 'cooling_source_id')),
Breadcrumb('rack', url=filtered_list_url('dcim:coolingfeed_list', 'rack_id')),
],
left_panels=[
panels.CoolingFeedPanel(),
panels.CoolingFeedCharacteristicsPanel(),
CustomFieldsPanel(),
TagsPanel(),
],
right_panels=[
CommentsPanel(),
],
)
@register_model_view(CoolingFeed, 'add', detail=False)
@register_model_view(CoolingFeed, 'edit')
class CoolingFeedEditView(generic.ObjectEditView):
queryset = CoolingFeed.objects.all()
form = forms.CoolingFeedForm
@register_model_view(CoolingFeed, 'delete')
class CoolingFeedDeleteView(generic.ObjectDeleteView):
queryset = CoolingFeed.objects.all()
@register_model_view(CoolingFeed, 'bulk_import', path='import', detail=False)
class CoolingFeedBulkImportView(generic.BulkImportView):
queryset = CoolingFeed.objects.all()
model_form = forms.CoolingFeedImportForm
@register_model_view(CoolingFeed, 'bulk_edit', path='edit', detail=False)
class CoolingFeedBulkEditView(generic.BulkEditView):
queryset = CoolingFeed.objects.all()
filterset = filtersets.CoolingFeedFilterSet
table = tables.CoolingFeedTable
form = forms.CoolingFeedBulkEditForm
@register_model_view(CoolingFeed, 'bulk_rename', path='rename', detail=False)
class CoolingFeedBulkRenameView(generic.BulkRenameView):
queryset = CoolingFeed.objects.all()
filterset = filtersets.CoolingFeedFilterSet
@register_model_view(CoolingFeed, 'bulk_delete', path='delete', detail=False)
class CoolingFeedBulkDeleteView(generic.BulkDeleteView):
queryset = CoolingFeed.objects.all()
filterset = filtersets.CoolingFeedFilterSet
table = tables.CoolingFeedTable
#
# Virtual device contexts
#

View File

@ -7,7 +7,9 @@ __all__ = (
'ButtonColorChoices',
'CSVDelimiterChoices',
'ColorChoices',
'DiameterUnitChoices',
'DistanceUnitChoices',
'FlowRateUnitChoices',
'ImportFormatChoices',
'ImportMethodChoices',
'WeightUnitChoices',
@ -197,3 +199,35 @@ class WeightUnitChoices(ChoiceSet):
Choice(UNIT_POUND, _('Pounds')),
Choice(UNIT_OUNCE, _('Ounces')),
)
class DiameterUnitChoices(ChoiceSet):
# Metric
UNIT_MILLIMETER = 'mm'
UNIT_CENTIMETER = 'cm'
# Imperial
UNIT_INCH = 'in'
CHOICES = (
Choice(UNIT_MILLIMETER, _('Millimeters')),
Choice(UNIT_CENTIMETER, _('Centimeters')),
Choice(UNIT_INCH, _('Inches')),
)
class FlowRateUnitChoices(ChoiceSet):
# Metric
UNIT_LITERS_PER_MINUTE = 'lpm'
UNIT_CUBIC_METERS_PER_HOUR = 'm3ph'
# Imperial
UNIT_GALLONS_PER_MINUTE = 'gpm'
CHOICES = (
Choice(UNIT_LITERS_PER_MINUTE, _('Liters per minute (L/min)')),
Choice(UNIT_CUBIC_METERS_PER_HOUR, _('Cubic meters per hour (m³/h)')),
Choice(UNIT_GALLONS_PER_MINUTE, _('Gallons per minute (GPM)')),
)

View File

@ -109,6 +109,8 @@ DEVICES_MENU = Menu(
get_model_item('dcim', 'consoleserverport', _('Console Server Ports')),
get_model_item('dcim', 'powerport', _('Power Ports')),
get_model_item('dcim', 'poweroutlet', _('Power Outlets')),
get_model_item('dcim', 'coolingintake', _('Cooling Intakes')),
get_model_item('dcim', 'coolingoutflow', _('Cooling Outflows')),
get_model_item('dcim', 'modulebay', _('Module Bays')),
get_model_item('dcim', 'devicebay', _('Device Bays')),
get_model_item('dcim', 'inventoryitem', _('Inventory Items')),
@ -335,6 +337,20 @@ POWER_MENU = Menu(
),
)
COOLING_MENU = Menu(
label=_('Cooling'),
icon_class='mdi mdi-snowflake',
groups=(
MenuGroup(
label=_('Cooling'),
items=(
get_model_item('dcim', 'coolingsource', _('Cooling Sources')),
get_model_item('dcim', 'coolingfeed', _('Cooling Feeds')),
),
),
),
)
PROVISIONING_MENU = Menu(
label=_('Provisioning'),
icon_class='mdi mdi-file-document-multiple-outline',
@ -482,6 +498,7 @@ def get_menus():
VIRTUALIZATION_MENU,
CIRCUITS_MENU,
POWER_MENU,
COOLING_MENU,
PROVISIONING_MENU,
CUSTOMIZATION_MENU,
OPERATIONS_MENU,

View File

@ -813,6 +813,172 @@ class DisplayDistanceTagTestCase(SimpleTestCase):
self.assertEqual(self._render(10, 'mi', 16093.44, system='imperial'), '10 mi')
class DiameterAttrTestCase(SimpleTestCase):
def _ctx(self, system=''):
return {'name': 'diameter', 'preferences': {'ui.measurement_system': system}}
def _obj(self, diameter, unit, abs_mm):
return SimpleNamespace(diameter=diameter, diameter_unit=unit, _abs_diameter=abs_mm)
def test_none_returns_placeholder(self):
attr = attrs.DiameterAttr('diameter')
obj = SimpleNamespace(diameter=None)
self.assertEqual(attr.render(obj, self._ctx()), attr.placeholder)
def test_inherit_shows_stored_value(self):
attr = attrs.DiameterAttr('diameter')
result = attr.render(self._obj(25, 'mm', 25), self._ctx(system=''))
self.assertIn('25', result)
self.assertIn('mm', result)
def test_metric_converts_in_to_mm(self):
# 1 in = 25.4 mm
attr = attrs.DiameterAttr('diameter')
result = attr.render(self._obj(1, 'in', 25.4), self._ctx(system='metric'))
self.assertIn('25.4', result)
self.assertIn('mm', result)
def test_imperial_converts_mm_to_in(self):
# 25 mm → 25 / 25.4 = 0.98 in
attr = attrs.DiameterAttr('diameter')
result = attr.render(self._obj(25, 'mm', 25), self._ctx(system='imperial'))
self.assertIn('0.98', result)
self.assertIn('in', result)
def test_metric_no_conversion_for_metric_unit(self):
attr = attrs.DiameterAttr('diameter')
result = attr.render(self._obj(2.5, 'cm', 25), self._ctx(system='metric'))
self.assertIn('2.5', result)
self.assertIn('cm', result)
def test_metric_no_conversion_when_abs_diameter_is_none(self):
attr = attrs.DiameterAttr('diameter')
result = attr.render(self._obj(1, 'in', None), self._ctx(system='metric'))
self.assertIn('1', result)
self.assertIn('in', result)
class FlowRateAttrTestCase(SimpleTestCase):
def _ctx(self, system=''):
return {'name': 'max_flow', 'preferences': {'ui.measurement_system': system}}
def _obj(self, max_flow, unit, abs_lpm):
return SimpleNamespace(max_flow=max_flow, max_flow_unit=unit, _abs_max_flow=abs_lpm)
def test_none_returns_placeholder(self):
attr = attrs.FlowRateAttr('max_flow')
obj = SimpleNamespace(max_flow=None)
self.assertEqual(attr.render(obj, self._ctx()), attr.placeholder)
def test_inherit_abbreviates_stored_unit(self):
attr = attrs.FlowRateAttr('max_flow')
result = attr.render(self._obj(100, 'lpm', 100), self._ctx(system=''))
self.assertIn('100', result)
self.assertIn('L/min', result)
def test_inherit_abbreviates_cubic_meters_per_hour(self):
attr = attrs.FlowRateAttr('max_flow')
result = attr.render(self._obj(6, 'm3ph', 100), self._ctx(system=''))
self.assertIn('6', result)
self.assertIn('m³/h', result)
def test_metric_converts_gpm_to_lpm(self):
# 10 GPM = 37.8541 L/min
attr = attrs.FlowRateAttr('max_flow')
result = attr.render(self._obj(10, 'gpm', 37.8541), self._ctx(system='metric'))
self.assertIn('37.85', result)
self.assertIn('L/min', result)
def test_imperial_converts_lpm_to_gpm(self):
# 100 L/min → 100 / 3.785411784 = 26.42 GPM
attr = attrs.FlowRateAttr('max_flow')
result = attr.render(self._obj(100, 'lpm', 100), self._ctx(system='imperial'))
self.assertIn('26.42', result)
self.assertIn('GPM', result)
def test_imperial_no_conversion_for_imperial_unit(self):
attr = attrs.FlowRateAttr('max_flow')
result = attr.render(self._obj(10, 'gpm', 37.8541), self._ctx(system='imperial'))
self.assertIn('10', result)
self.assertIn('GPM', result)
def test_metric_no_conversion_when_abs_flow_is_none(self):
attr = attrs.FlowRateAttr('max_flow')
result = attr.render(self._obj(10, 'gpm', None), self._ctx(system='metric'))
self.assertIn('10', result)
self.assertIn('GPM', result)
class DisplayDiameterTagTestCase(SimpleTestCase):
TEMPLATE = Template('{% load helpers %}{% display_diameter diameter diameter_unit abs_diameter %}')
def _render(self, diameter, diameter_unit, abs_diameter, system=''):
ctx = Context({
'preferences': {'ui.measurement_system': system},
'diameter': diameter,
'diameter_unit': diameter_unit,
'abs_diameter': abs_diameter,
})
return self.TEMPLATE.render(ctx).strip()
def test_none_diameter_returns_empty(self):
self.assertEqual(self._render(None, 'mm', None), '')
def test_zero_diameter_is_not_suppressed(self):
self.assertEqual(self._render(0, 'mm', 0), '0 mm')
def test_inherit_stores_mm(self):
# An unconverted Decimal keeps its stored scale, as with display_weight
self.assertEqual(self._render(Decimal('25.00'), 'mm', Decimal('25.0000')), '25.00 mm')
def test_metric_converts_in_to_mm(self):
self.assertEqual(self._render(1, 'in', Decimal('25.4000'), system='metric'), '25.4 mm')
def test_imperial_converts_cm_to_in(self):
# 2.5 cm = 25 mm → 25 / 25.4 = 0.98 in
self.assertEqual(self._render(Decimal('2.50'), 'cm', Decimal('25.0000'), system='imperial'), '0.98 in')
def test_imperial_no_conversion_for_imperial_unit(self):
self.assertEqual(self._render(1, 'in', Decimal('25.4000'), system='imperial'), '1 in')
class DisplayFlowRateTagTestCase(SimpleTestCase):
TEMPLATE = Template('{% load helpers %}{% display_flow_rate max_flow max_flow_unit abs_max_flow %}')
def _render(self, max_flow, max_flow_unit, abs_max_flow, system=''):
ctx = Context({
'preferences': {'ui.measurement_system': system},
'max_flow': max_flow,
'max_flow_unit': max_flow_unit,
'abs_max_flow': abs_max_flow,
})
return self.TEMPLATE.render(ctx).strip()
def test_none_flow_rate_returns_empty(self):
self.assertEqual(self._render(None, 'lpm', None), '')
def test_zero_flow_rate_is_not_suppressed(self):
self.assertEqual(self._render(0, 'lpm', 0), '0 L/min')
def test_inherit_abbreviates_stored_unit(self):
# An unconverted Decimal keeps its stored scale, as with display_weight
self.assertEqual(self._render(Decimal('100.00'), 'lpm', Decimal('100.0000')), '100.00 L/min')
self.assertEqual(self._render(Decimal('6.00'), 'm3ph', Decimal('100.0000')), '6.00 m³/h')
self.assertEqual(self._render(Decimal('10.00'), 'gpm', Decimal('37.8541')), '10.00 GPM')
def test_metric_converts_gpm_to_lpm(self):
self.assertEqual(self._render(10, 'gpm', Decimal('37.8541'), system='metric'), '37.85 L/min')
def test_imperial_converts_lpm_to_gpm(self):
# 100 L/min → 100 / 3.785411784 = 26.42 GPM
self.assertEqual(self._render(100, 'lpm', Decimal('100.0000'), system='imperial'), '26.42 GPM')
def test_imperial_no_conversion_for_imperial_unit(self):
self.assertEqual(self._render(10, 'gpm', Decimal('37.8541'), system='imperial'), '10 GPM')
class ObjectsTablePanelTestCase(TestCase):
"""
Verify that ObjectsTablePanel.should_render() hides the panel when

View File

@ -14,8 +14,10 @@ __all__ = (
'ChoiceAttr',
'ColorAttr',
'DateTimeAttr',
'DiameterAttr',
'DistanceAttr',
'DurationAttr',
'FlowRateAttr',
'GPSCoordinatesAttr',
'GenericForeignKeyAttr',
'ImageAttr',
@ -605,6 +607,17 @@ IMPERIAL_WEIGHT = {'lb', 'oz'}
METRIC_WEIGHT = {'kg', 'g'}
IMPERIAL_DISTANCE = {'mi', 'ft'}
METRIC_DISTANCE = {'km', 'm'}
IMPERIAL_DIAMETER = {'in'}
METRIC_DIAMETER = {'mm', 'cm'}
IMPERIAL_FLOW_RATE = {'gpm'}
METRIC_FLOW_RATE = {'lpm', 'm3ph'}
# Abbreviations for the flow rate units, whose stored values are not themselves presentable
FLOW_RATE_ABBREVIATIONS = {
'lpm': 'L/min',
'm3ph': 'm³/h',
'gpm': 'GPM',
}
def compute_weight_display(weight, weight_unit, abs_weight, system):
@ -623,6 +636,30 @@ def compute_weight_display(weight, weight_unit, abs_weight, system):
return weight, weight_unit
def compute_diameter_display(diameter, diameter_unit, abs_diameter, system):
"""
Return (display_value, display_unit) for a diameter, respecting the user's measurement system.
abs_diameter is in millimeters (from DiameterMixin._abs_diameter).
"""
if system == 'metric' and diameter_unit in IMPERIAL_DIAMETER and abs_diameter is not None:
return round(float(abs_diameter), 2), 'mm'
if system == 'imperial' and diameter_unit in METRIC_DIAMETER and abs_diameter is not None:
return round(float(abs_diameter) / 25.4, 2), 'in'
return diameter, diameter_unit
def compute_flow_rate_display(flow_rate, flow_rate_unit, abs_flow_rate, system):
"""
Return (display_value, display_unit) for a flow rate, respecting the user's measurement system.
abs_flow_rate is in liters per minute (from MaxFlowMixin._abs_max_flow).
"""
if system == 'metric' and flow_rate_unit in IMPERIAL_FLOW_RATE and abs_flow_rate is not None:
return round(float(abs_flow_rate), 2), FLOW_RATE_ABBREVIATIONS['lpm']
if system == 'imperial' and flow_rate_unit in METRIC_FLOW_RATE and abs_flow_rate is not None:
return round(float(abs_flow_rate) / 3.785411784, 2), FLOW_RATE_ABBREVIATIONS['gpm']
return flow_rate, FLOW_RATE_ABBREVIATIONS.get(flow_rate_unit, flow_rate_unit)
def compute_distance_display(distance, distance_unit, abs_distance, system):
"""
Return (display_value, display_unit) for a distance, respecting the user's measurement system.
@ -706,3 +743,69 @@ class DistanceAttr(ObjectAttribute):
'value': display_value,
'unit': display_unit,
})
class DiameterAttr(ObjectAttribute):
"""
A diameter attribute that converts to the user's preferred measurement system.
Parameters:
unit_attr (str): Name of the field holding the diameter unit (default: 'diameter_unit')
abs_attr (str): The internal _abs_diameter field name on DiameterMixin (stored in millimeters).
Accessed via Python not subject to Django's template underscore restriction.
"""
template_name = 'ui/attrs/measurement.html'
def __init__(self, *args, unit_attr='diameter_unit', abs_attr='_abs_diameter', **kwargs):
super().__init__(*args, **kwargs)
self.unit_attr = unit_attr
self.abs_attr = abs_attr
def render(self, obj, context):
diameter = resolve_attr_path(obj, self.accessor)
if diameter is None:
return self.placeholder
system = (context.get('preferences') or {}).get('ui.measurement_system') or ''
unit = resolve_attr_path(obj, self.unit_attr)
abs_diameter = resolve_attr_path(obj, self.abs_attr)
display_value, display_unit = compute_diameter_display(diameter, unit, abs_diameter, system)
return render_to_string(self.template_name, {
'name': context['name'],
'value': display_value,
'unit': display_unit,
})
class FlowRateAttr(ObjectAttribute):
"""
A flow rate attribute that converts to the user's preferred measurement system.
Parameters:
unit_attr (str): Name of the field holding the flow rate unit (default: 'max_flow_unit')
abs_attr (str): The internal _abs_max_flow field name on MaxFlowMixin (stored in liters per
minute). Accessed via Python not subject to Django's template underscore restriction.
"""
template_name = 'ui/attrs/measurement.html'
def __init__(self, *args, unit_attr='max_flow_unit', abs_attr='_abs_max_flow', **kwargs):
super().__init__(*args, **kwargs)
self.unit_attr = unit_attr
self.abs_attr = abs_attr
def render(self, obj, context):
flow_rate = resolve_attr_path(obj, self.accessor)
if flow_rate is None:
return self.placeholder
system = (context.get('preferences') or {}).get('ui.measurement_system') or ''
unit = resolve_attr_path(obj, self.unit_attr)
abs_flow_rate = resolve_attr_path(obj, self.abs_attr)
display_value, display_unit = compute_flow_rate_display(flow_rate, unit, abs_flow_rate, system)
return render_to_string(self.template_name, {
'name': context['name'],
'value': display_value,
'unit': display_unit,
})

View File

@ -32,6 +32,20 @@
</button>
</li>
{% endif %}
{% if perms.dcim.add_coolingintake %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_coolingintake' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Cooling Intakes" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_coolingoutflow %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_coolingoutflow' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">
{% trans "Cooling Outflows" %}
</button>
</li>
{% endif %}
{% if perms.dcim.add_interface %}
<li>
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_interface' %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" class="dropdown-item">

View File

@ -33,6 +33,12 @@
{% if perms.dcim.add_poweroutlet %}
<li><a class="dropdown-item" href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}">{% trans "Power Outlets" %}</a></li>
{% endif %}
{% if perms.dcim.add_coolingintake %}
<li><a class="dropdown-item" href="{% url 'dcim:coolingintake_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_coolingintakes' pk=object.pk %}">{% trans "Cooling Intakes" %}</a></li>
{% endif %}
{% if perms.dcim.add_coolingoutflow %}
<li><a class="dropdown-item" href="{% url 'dcim:coolingoutflow_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_coolingoutflows' pk=object.pk %}">{% trans "Cooling Outflows" %}</a></li>
{% endif %}
{% if perms.dcim.add_interface %}
<li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">{% trans "Interfaces" %}</a></li>
{% endif %}

View File

@ -22,11 +22,18 @@
<h2 class="col-9 offset-3">{% trans "Hardware" %}</h2>
</div>
{% render_field form.device_type %}
{% render_field form.airflow %}
{% render_field form.serial %}
{% render_field form.asset_tag %}
</div>
<div class="field-group my-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Cooling" %}</h2>
</div>
{% render_field form.cooling_method %}
{% render_field form.airflow %}
</div>
<div class="field-group my-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Location" %}</h2>

View File

@ -25,6 +25,12 @@
{% if perms.dcim.add_poweroutlettemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:poweroutlettemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_poweroutlets' pk=object.pk %}">{% trans "Power Outlets" %}</a></li>
{% endif %}
{% if perms.dcim.add_coolingintaketemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:coolingintaketemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_coolingintakes' pk=object.pk %}">{% trans "Cooling Intakes" %}</a></li>
{% endif %}
{% if perms.dcim.add_coolingoutflowtemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:coolingoutflowtemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_coolingoutflows' pk=object.pk %}">{% trans "Cooling Outflows" %}</a></li>
{% endif %}
{% if perms.dcim.add_interfacetemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:interfacetemplate_add' %}?device_type={{ object.pk }}&return_url={% url 'dcim:devicetype_interfaces' pk=object.pk %}">{% trans "Interfaces" %}</a></li>
{% endif %}

View File

@ -0,0 +1,8 @@
{# As numeric.html, but renders the unit verbatim. Attributes which resolve their own unit abbreviation #}
{# (e.g. DiameterAttr, FlowRateAttr) already supply correct casing, which "L/min" and "GPM" depend on. #}
<span>
<span{% if name %} id="attr_{{ name }}"{% endif %}>{{ value }}</span>
{% if unit %}
{{ unit }}
{% endif %}
</span>

View File

@ -3,66 +3,91 @@ from decimal import Decimal, InvalidOperation
from django.utils.translation import gettext as _
from dcim.choices import CableLengthUnitChoices
from netbox.choices import WeightUnitChoices
from netbox.choices import (
DiameterUnitChoices,
FlowRateUnitChoices,
WeightUnitChoices,
)
__all__ = (
'to_grams',
'to_liters_per_minute',
'to_meters',
'to_millimeters',
)
def _normalized_measurement(value, unit, converters, quantity, precision=4) -> Decimal:
"""
Shared implementation for the unit-normalization helpers below. Coerce `value` to a non-negative
Decimal, apply the matching per-unit converter, and round the result to `precision` decimal places.
`converters` maps each valid unit to a callable receiving the Decimal value; `quantity` labels the
value in error messages. Pass `precision=None` to return the unrounded result.
"""
try:
value = Decimal(value)
except (InvalidOperation, TypeError, ValueError):
raise TypeError(
_("Invalid value '{value}' for {quantity} (must be a number)").format(value=value, quantity=quantity)
)
if value < 0:
raise ValueError(_("Invalid value for {quantity}: must be a positive number").format(quantity=quantity))
if unit not in converters:
raise ValueError(
_("Unknown unit {unit}. Must be one of the following: {valid_units}").format(
unit=unit,
valid_units=', '.join(converters)
)
)
result = converters[unit](value)
return result if precision is None else round(result, precision)
def to_grams(weight, unit) -> int:
"""
Convert the given weight to integer grams.
"""
try:
if weight < 0:
raise ValueError(_("Weight must be a positive number"))
except TypeError:
raise TypeError(_("Invalid value '{weight}' for weight (must be a number)").format(weight=weight))
if unit == WeightUnitChoices.UNIT_KILOGRAM:
return int(weight * 1000)
if unit == WeightUnitChoices.UNIT_GRAM:
return int(weight)
if unit == WeightUnitChoices.UNIT_POUND:
return int(weight * Decimal(453.592))
if unit == WeightUnitChoices.UNIT_OUNCE:
return int(weight * Decimal(28.3495))
raise ValueError(
_("Unknown unit {unit}. Must be one of the following: {valid_units}").format(
unit=unit,
valid_units=', '.join(WeightUnitChoices.values())
)
)
# Rounding is suppressed so that the result is truncated exactly as the caller's int() expects; rounding
# first could nudge a value across an integer boundary.
return int(_normalized_measurement(weight, unit, {
WeightUnitChoices.UNIT_KILOGRAM: lambda v: v * 1000,
WeightUnitChoices.UNIT_GRAM: lambda v: v,
WeightUnitChoices.UNIT_POUND: lambda v: v * Decimal(453.592),
WeightUnitChoices.UNIT_OUNCE: lambda v: v * Decimal(28.3495),
}, _('weight'), precision=None))
def to_meters(length, unit) -> Decimal:
"""
Convert the given length to meters, returning a Decimal value.
"""
try:
length = Decimal(length)
except InvalidOperation:
raise TypeError(_("Invalid value '{length}' for length (must be a number)").format(length=length))
if length < 0:
raise ValueError(_("Length must be a positive number"))
return _normalized_measurement(length, unit, {
CableLengthUnitChoices.UNIT_KILOMETER: lambda v: v * 1000,
CableLengthUnitChoices.UNIT_METER: lambda v: v,
CableLengthUnitChoices.UNIT_CENTIMETER: lambda v: v / 100,
CableLengthUnitChoices.UNIT_MILE: lambda v: v * Decimal(1609.344),
CableLengthUnitChoices.UNIT_FOOT: lambda v: v * Decimal(0.3048),
CableLengthUnitChoices.UNIT_INCH: lambda v: v * Decimal(0.0254),
}, _('length'))
if unit == CableLengthUnitChoices.UNIT_KILOMETER:
return round(Decimal(length * 1000), 4)
if unit == CableLengthUnitChoices.UNIT_METER:
return round(Decimal(length), 4)
if unit == CableLengthUnitChoices.UNIT_CENTIMETER:
return round(Decimal(length / 100), 4)
if unit == CableLengthUnitChoices.UNIT_MILE:
return round(length * Decimal(1609.344), 4)
if unit == CableLengthUnitChoices.UNIT_FOOT:
return round(length * Decimal(0.3048), 4)
if unit == CableLengthUnitChoices.UNIT_INCH:
return round(length * Decimal(0.0254), 4)
raise ValueError(
_("Unknown unit {unit}. Must be one of the following: {valid_units}").format(
unit=unit,
valid_units=', '.join(CableLengthUnitChoices.values())
)
)
def to_millimeters(diameter, unit) -> Decimal:
"""
Convert the given diameter to millimeters, returning a Decimal value.
"""
return _normalized_measurement(diameter, unit, {
DiameterUnitChoices.UNIT_MILLIMETER: lambda v: v,
DiameterUnitChoices.UNIT_CENTIMETER: lambda v: v * 10,
DiameterUnitChoices.UNIT_INCH: lambda v: v * Decimal('25.4'),
}, _('diameter'))
def to_liters_per_minute(flow_rate, unit) -> Decimal:
"""
Convert the given flow rate to liters per minute, returning a Decimal value.
"""
return _normalized_measurement(flow_rate, unit, {
FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE: lambda v: v,
FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR: lambda v: v * Decimal(1000) / Decimal(60),
FlowRateUnitChoices.UNIT_GALLONS_PER_MINUTE: lambda v: v * Decimal('3.785411784'),
}, _('flow rate'))

View File

@ -10,7 +10,9 @@ from django.utils.translation import gettext_lazy as _
from core.models import ObjectType
from netbox.settings import DISK_BASE_UNIT, RAM_BASE_UNIT
from netbox.ui.attrs import (
compute_diameter_display,
compute_distance_display,
compute_flow_rate_display,
compute_weight_display,
)
from utilities.forms import TableConfigForm, get_selected_values
@ -21,7 +23,9 @@ __all__ = (
'action_url',
'applied_filters',
'as_range',
'display_diameter',
'display_distance',
'display_flow_rate',
'display_weight',
'divide',
'get_item',
@ -360,6 +364,30 @@ def display_distance(context, distance, distance_unit, abs_distance):
return f'{value:g} {unit}'
@register.simple_tag(takes_context=True)
def display_diameter(context, diameter, diameter_unit, abs_diameter):
"""
Render a diameter value respecting the user's ui.measurement_system preference.
"""
if diameter is None:
return ''
system = (context.get('preferences') or {}).get('ui.measurement_system') or ''
value, unit = compute_diameter_display(diameter, diameter_unit, abs_diameter, system)
return f'{value:g} {unit}'
@register.simple_tag(takes_context=True)
def display_flow_rate(context, flow_rate, flow_rate_unit, abs_flow_rate):
"""
Render a flow rate value respecting the user's ui.measurement_system preference.
"""
if flow_rate is None:
return ''
system = (context.get('preferences') or {}).get('ui.measurement_system') or ''
value, unit = compute_flow_rate_display(flow_rate, flow_rate_unit, abs_flow_rate, system)
return f'{value:g} {unit}'
@register.filter("startswith")
def startswith(text: str, starts: str) -> bool:
"""

View File

@ -1,8 +1,17 @@
from decimal import Decimal
from dcim.choices import CableLengthUnitChoices
from netbox.choices import WeightUnitChoices
from utilities.conversion import to_grams, to_meters
from netbox.choices import (
DiameterUnitChoices,
FlowRateUnitChoices,
WeightUnitChoices,
)
from utilities.conversion import (
to_grams,
to_liters_per_minute,
to_meters,
to_millimeters,
)
from utilities.testing.base import TestCase
@ -25,6 +34,34 @@ class ConversionsTestCase(TestCase):
to_grams(1, WeightUnitChoices.UNIT_OUNCE),
28
)
# The result is truncated, not rounded
self.assertEqual(
to_grams(Decimal('1.9999'), WeightUnitChoices.UNIT_GRAM),
1
)
with self.assertRaises(ValueError):
to_grams(1, 'invalid')
with self.assertRaises(ValueError):
to_grams(-1, WeightUnitChoices.UNIT_GRAM)
with self.assertRaises(TypeError):
to_grams('abc', WeightUnitChoices.UNIT_GRAM)
with self.assertRaises(TypeError):
to_grams(None, WeightUnitChoices.UNIT_GRAM)
def test_invalid_values(self):
# A non-numeric value is reported as a TypeError rather than surfacing a raw Decimal error
for converter, unit in (
(to_meters, CableLengthUnitChoices.UNIT_METER),
(to_millimeters, DiameterUnitChoices.UNIT_MILLIMETER),
(to_liters_per_minute, FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE),
):
with self.subTest(converter=converter.__name__):
with self.assertRaises(TypeError):
converter(None, unit)
with self.assertRaises(TypeError):
converter('abc', unit)
with self.assertRaises(ValueError):
converter(-1, unit)
def test_to_meters(self):
self.assertEqual(
@ -51,3 +88,37 @@ class ConversionsTestCase(TestCase):
to_meters(1, CableLengthUnitChoices.UNIT_INCH),
Decimal('0.0254')
)
def test_to_millimeters(self):
self.assertEqual(
to_millimeters(1, DiameterUnitChoices.UNIT_MILLIMETER),
Decimal('1')
)
self.assertEqual(
to_millimeters(1, DiameterUnitChoices.UNIT_CENTIMETER),
Decimal('10')
)
self.assertEqual(
to_millimeters(1, DiameterUnitChoices.UNIT_INCH),
Decimal('25.4')
)
with self.assertRaises(ValueError):
to_millimeters(1, 'invalid')
def test_to_liters_per_minute(self):
self.assertEqual(
to_liters_per_minute(10, FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE),
Decimal('10')
)
self.assertAlmostEqual(
to_liters_per_minute(6, FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR),
Decimal('100'),
places=4
)
self.assertAlmostEqual(
to_liters_per_minute(10, FlowRateUnitChoices.UNIT_GALLONS_PER_MINUTE),
Decimal('37.8541'),
places=4
)
with self.assertRaises(ValueError):
to_liters_per_minute(10, 'invalid')