diff --git a/.gitattributes b/.gitattributes
index dfdb8b771..9ad1ee25e 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,5 @@
*.sh text eol=lf
+# Treat minified or packed JS/CSS files as binary, as they're not meant to be human-readable
+*.min.* binary
+*.map binary
+*.pack.js binary
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 54dc5ca8c..5df769b94 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -7,11 +7,9 @@ about: Report a reproducible bug in the current release of NetBox
### Environment
-* Python version:
-* NetBox version:
+* Python version:
+* NetBox version:
|
+Each cable must have two endpoints defined. These endpoints are sometimes referenced as A and B for clarity, however cables are direction-agnostic and the order in which terminations are made has no meaning. Cables may be connected to the following objects:
- Device A Patch Panel A Patch Panel B Device B
-+-----------+ +-------------+ +-------------+ +-----------+
-| Interface | --- Cable --- | Front Port | | Front Port | --- Cable --- | Interface |
-+-----------+ +-------------+ +-------------+ +-----------+
- +-------------+ +-------------+
- | Rear Port | --- Cable --- | Rear Port |
- +-------------+ +-------------+
-```
+* Circuit terminations
+* Console ports
+* Console server ports
+* Interfaces
+* Pass-through ports (front and rear)
+* Power feeds
+* Power outlets
+* Power ports
-All connections between device components in NetBox are represented using cables. However, defining the actual cable plant is optional: Components can be be directly connected using cables with no type or other attributes assigned.
+Each cable may be assigned a type, label, length, and color. Each cable is also assigned one of three operational statuses:
-Cables are also used to associated ports and interfaces with circuit terminations. To do this, first create the circuit termination, then navigate the desired component and connect a cable between the two.
+* Active (default)
+* Planned
+* Decommissioning
+
+## Tracing Cables
+
+A cable may be traced from either of its endpoints by clicking the "trace" button. (A REST API endpoint also provides this functionality.) NetBox will follow the path of connected cables from this termination across the directly connected cable to the far-end termination. If the cable connects to a pass-through port, and the peer port has another cable connected, NetBox will continue following the cable path until it encounters a non-pass-through or unconnected termination point. The entire path will be displayed to the user.
+
+In the example below, three individual cables comprise a path between devices A and D:
+
+
+
+Traced from Interface 1 on Device A, NetBox will show the following path:
+
+* Cable 1: Interface 1 to Front Port 1
+* Cable 2: Rear Port 1 to Rear Port 2
+* Cable 3: Front Port 2 to Interface 2
diff --git a/docs/models/dcim/consoleport.md b/docs/models/dcim/consoleport.md
index 4d3a089c5..1a0782f25 100644
--- a/docs/models/dcim/consoleport.md
+++ b/docs/models/dcim/consoleport.md
@@ -1,5 +1,5 @@
## Console Ports
-A console port provides connectivity to the physical console of a device. Console ports are typically used for temporary access by someone who is physically near the device, or for remote out-of-band access via a console server.
+A console port provides connectivity to the physical console of a device. These are typically used for temporary access by someone who is physically near the device, or for remote out-of-band access provided via a networked console server. Each console port may be assigned a physical type.
-Console ports can be connected to console server ports.
+Cables can connect console ports to console server ports or pass-through ports.
diff --git a/docs/models/dcim/consoleporttemplate.md b/docs/models/dcim/consoleporttemplate.md
index 86281cb92..3462ff253 100644
--- a/docs/models/dcim/consoleporttemplate.md
+++ b/docs/models/dcim/consoleporttemplate.md
@@ -1,3 +1,3 @@
## Console Port Templates
-A template for a console port that will be created on all instantiations of the parent device type.
+A template for a console port that will be created on all instantiations of the parent device type. Each console port can be assigned a physical type.
diff --git a/docs/models/dcim/consoleserverport.md b/docs/models/dcim/consoleserverport.md
index 55aefd733..da1ee8986 100644
--- a/docs/models/dcim/consoleserverport.md
+++ b/docs/models/dcim/consoleserverport.md
@@ -1,5 +1,5 @@
## Console Server Ports
-A console server is a device which provides remote access to the local consoles of connected devices. This is typically done to provide remote out-of-band access to network devices.
+A console server is a device which provides remote access to the local consoles of connected devices. They are typically used to provide remote out-of-band access to network devices. Each console server port may be assigned a physical type.
-Console server ports can be connected to console ports.
+Cables can connect console server ports to console ports or pass-through ports.
diff --git a/docs/models/dcim/consoleserverporttemplate.md b/docs/models/dcim/consoleserverporttemplate.md
index ed99adb11..cc4e8bcd3 100644
--- a/docs/models/dcim/consoleserverporttemplate.md
+++ b/docs/models/dcim/consoleserverporttemplate.md
@@ -1,3 +1,3 @@
## Console Server Port Templates
-A template for a console server port that will be created on all instantiations of the parent device type.
+A template for a console server port that will be created on all instantiations of the parent device type. Each console server port can be assigned a physical type.
diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md
index 9ec2875da..df14c0e07 100644
--- a/docs/models/dcim/device.md
+++ b/docs/models/dcim/device.md
@@ -1,7 +1,15 @@
# Devices
-Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and can be half depth or full depth. A device may have a height of 0U: These devices do not consume vertical rack space and cannot be assigned to a particular rack unit. A common example of a 0U device is a vertically-mounted PDU.
+Every piece of hardware which is installed within a site or rack exists in NetBox as a device. Devices are measured in rack units (U) and can be half depth or full depth. A device may have a height of 0U: These devices do not consume vertical rack space and cannot be assigned to a particular rack unit. A common example of a 0U device is a vertically-mounted PDU.
When assigning a multi-U device to a rack, it is considered to be mounted in the lowest-numbered rack unit which it occupies. For example, a 3U device which occupies U8 through U10 is said to be mounted in U8. This logic applies to racks with both ascending and descending unit numbering.
-A device is said to be full depth if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede airflow.
+A device is said to be full-depth if its installation on one rack face prevents the installation of any other device on the opposite face within the same rack unit(s). This could be either because the device is physically too deep to allow a device behind it, or because the installation of an opposing device would impede airflow.
+
+Each device must be instantiated from a pre-created device type, and its default components (console ports, power ports, interfaces, etc.) will be created automatically. (The device type associated with a device may be changed after its creation, however its components will not be updated retroactively.)
+
+Each device must be assigned a site, device role, and operational status, and may optionally be assigned to a specific rack within a site. A platform, serial number, and asset tag may optionally be assigned to each device.
+
+Device names must be unique within a site, unless the device has been assigned to a tenant. Devices may also be unnamed.
+
+When a device has one or more interfaces with IP addresses assigned, a primary IP for the device can be designated, for both IPv4 and IPv6.
diff --git a/docs/models/dcim/devicebay.md b/docs/models/dcim/devicebay.md
index cdcd5657d..2aea14a7a 100644
--- a/docs/models/dcim/devicebay.md
+++ b/docs/models/dcim/devicebay.md
@@ -1,7 +1,8 @@
## Device Bays
-Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations or the "Non-Racked Devices" list within the rack view.
+Device bays represent a space or slot within a parent device in which a child device may be installed. For example, a 2U parent chassis might house four individual blade servers. The chassis would appear in the rack elevation as a 2U device with four device bays, and each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations or count as consuming rack units.
-Child devices are first-class Devices in their own right: that is, fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and interfaces. You cannot create a LAG between interfaces in different child devices.
+Child devices are first-class Devices in their own right: That is, they are fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and components. LAG interfaces may not group interfaces belonging to different child devices.
-Therefore, Device bays are **not** suitable for modeling chassis-based switches and routers. These should instead be modeled as a single Device, with the line cards as Inventory Items.
+!!! note
+ Device bays are **not** suitable for modeling line cards (such as those commonly found in chassis-based routers and switches), as these components depend on the control plane of the parent device to operate. Instead, line cards and similarly non-autonomous hardware should be modeled as inventory items within a device, with any associated interfaces or other components assigned directly to the device.
diff --git a/docs/models/dcim/devicerole.md b/docs/models/dcim/devicerole.md
index 315f81356..13b8f021e 100644
--- a/docs/models/dcim/devicerole.md
+++ b/docs/models/dcim/devicerole.md
@@ -1,3 +1,3 @@
# Device Roles
-Devices can be organized by functional roles. These roles are fully customizable. For example, you might create roles for core switches, distribution switches, and access switches.
+Devices can be organized by functional roles, which are fully customizable by the user. For example, you might create roles for core switches, distribution switches, and access switches within your network.
diff --git a/docs/models/dcim/devicetype.md b/docs/models/dcim/devicetype.md
index 1a10cee41..a7e00dbc6 100644
--- a/docs/models/dcim/devicetype.md
+++ b/docs/models/dcim/devicetype.md
@@ -1,18 +1,14 @@
# Device Types
-A device type represents a particular make and model of hardware that exists in the real world. Device types define the physical attributes of a device (rack height and depth) and its individual components (console, power, and network interfaces).
+A device type represents a particular make and model of hardware that exists in the real world. Device types define the physical attributes of a device (rack height and depth) and its individual components (console, power, network interfaces, and so on).
-Device types are instantiated as devices installed within racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple devices of this type named "switch1," "switch2," and so on. Each device will inherit the components (such as interfaces) of its device type at the time of creation. (However, changes made to a device type will **not** apply to instances of that device type retroactively.)
+Device types are instantiated as devices installed within sites and/or equipment racks. For example, you might define a device type to represent a Juniper EX4300-48T network switch with 48 Ethernet interfaces. You can then create multiple _instances_ of this type named "switch1," "switch2," and so on. Each device will automatically inherit the components (such as interfaces) of its device type at the time of creation. However, changes made to a device type will **not** apply to instances of that device type retroactively.
Some devices house child devices which share physical resources, like space and power, but which functional independently from one another. A common example of this is blade server chassis. Each device type is designated as one of the following:
* A parent device (which has device bays)
-* A child device (which must be installed in a device bay)
+* A child device (which must be installed within a device bay)
* Neither
!!! note
- This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane.
-
- For that application you should create a single Device for the chassis, and add Interfaces directly to it. Interfaces can be created in bulk using range patterns, e.g. "Gi1/[1-24]".
-
- Add Inventory Items if you want to record the line cards themselves as separate entities. There is no explicit relationship between each interface and its line card, but it may be implied by the naming (e.g. interfaces "Gi1/x" are on line card 1)
+ This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. Instead, line cards and similarly non-autonomous hardware should be modeled as inventory items within a device, with any associated interfaces or other components assigned directly to the device.
diff --git a/docs/models/dcim/frontport.md b/docs/models/dcim/frontport.md
index 12b9cfc16..0b753c012 100644
--- a/docs/models/dcim/frontport.md
+++ b/docs/models/dcim/frontport.md
@@ -1,5 +1,3 @@
## Front Ports
-Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports.
-
-Each front port is mapped to a specific rear port on the same device. A single rear port may be mapped to multiple rear ports.
\ No newline at end of file
+Front ports are pass-through ports used to represent physical cable connections that comprise part of a longer path. For example, the ports on the front face of a UTP patch panel would be modeled in NetBox as front ports. Each port is assigned a physical type, and must be mapped to a specific rear port on the same device. A single rear port may be mapped to multiple rear ports, using numeric positions to annotate the specific alignment of each.
diff --git a/docs/models/dcim/frontporttemplate.md b/docs/models/dcim/frontporttemplate.md
index b32349519..03de0eae4 100644
--- a/docs/models/dcim/frontporttemplate.md
+++ b/docs/models/dcim/frontporttemplate.md
@@ -1,3 +1,3 @@
## Front Port Templates
-A template for a front-facing pass-through port that will be created on all instantiations of the parent device type.
+A template for a front-facing pass-through port that will be created on all instantiations of the parent device type. Front ports may have a physical type assigned, and must be associated with a corresponding rear port and position. This association will be automatically replicated when the device type is instantiated.
diff --git a/docs/models/dcim/interface.md b/docs/models/dcim/interface.md
index cbccbec8d..756e320af 100644
--- a/docs/models/dcim/interface.md
+++ b/docs/models/dcim/interface.md
@@ -1,9 +1,12 @@
## Interfaces
-Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. Each type of connection can be classified as either *planned* or *connected*.
+Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management).
-Each interface is a assigned a type denoting its physical properties. Two special types exist: the "virtual" type can be used to designate logical interfaces (such as SVIs), and the "LAG" type can be used to desinate link aggregation groups to which physical interfaces can be assigned.
+Interfaces may be physical or virtual in nature, but only physical interfaces may be connected via cables. Cables can connect interfaces to pass-through ports, circuit terminations, or other interfaces.
-Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Fields are also provided to store an interface's MTU and MAC address.
+Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically.
-VLANs can be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
+IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
+
+!!! note
+ Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa.
diff --git a/docs/models/dcim/interfacetemplate.md b/docs/models/dcim/interfacetemplate.md
index 07fc3a65b..d9b30dd87 100644
--- a/docs/models/dcim/interfacetemplate.md
+++ b/docs/models/dcim/interfacetemplate.md
@@ -1,3 +1,3 @@
## Interface Templates
-A template for an interface that will be created on all instantiations of the parent device type.
+A template for a network interface that will be created on all instantiations of the parent device type. Each interface may be assigned a physical or virtual type, and may be designated as "management-only."
diff --git a/docs/models/dcim/inventoryitem.md b/docs/models/dcim/inventoryitem.md
index b113dce1e..237bad92c 100644
--- a/docs/models/dcim/inventoryitem.md
+++ b/docs/models/dcim/inventoryitem.md
@@ -1,3 +1,7 @@
# Inventory Items
-Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer.
+Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. Inventory items are distinct from other device components in that they cannot be templatized on a device type, and cannot be connected by cables. They are intended to be used primarily for inventory purposes.
+
+Each inventory item can be assigned a manufacturer, part ID, serial number, and asset tag (all optional). A boolean toggle is also provided to indicate whether each item was entered manually or discovered automatically (by some process outside of NetBox).
+
+Inventory items are hierarchical in nature, such that any individual item may be designated as the parent for other items. For example, an inventory item might be created to represent a line card which houses several SFP optics, each of which exists as a child item within the device.
diff --git a/docs/models/dcim/manufacturer.md b/docs/models/dcim/manufacturer.md
index cee89291d..df227ee17 100644
--- a/docs/models/dcim/manufacturer.md
+++ b/docs/models/dcim/manufacturer.md
@@ -1,3 +1,3 @@
# Manufacturers
-Each device type must be assigned to a manufacturer. The model number of a device type must be unique to its manufacturer.
+A manufacturer represents the "make" of a device; e.g. Cisco or Dell. Each device type must be assigned to a manufacturer. (Inventory items and platforms may also be associated with manufacturers.) Each manufacturer must have a unique name and may have a description assigned to it.
diff --git a/docs/models/dcim/platform.md b/docs/models/dcim/platform.md
index 19528da13..a860904b5 100644
--- a/docs/models/dcim/platform.md
+++ b/docs/models/dcim/platform.md
@@ -1,7 +1,9 @@
# Platforms
-A platform defines the type of software running on a device or virtual machine. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of the same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
+A platform defines the type of software running on a device or virtual machine. This can be helpful to model when it is necessary to distinguish between different versions or feature sets. Note that two devices of the same type may be assigned different platforms: For example, one Juniper MX240 might run Junos 14 while another runs Junos 15.
-The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform.
+Platforms may optionally be limited by manufacturer: If a platform is assigned to a particular manufacturer, it can only be assigned to devices with a type belonging to that manufacturer.
+
+The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver and any associated arguments NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform.
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
diff --git a/docs/models/dcim/powerfeed.md b/docs/models/dcim/powerfeed.md
index ab8621e14..48ad2a5dc 100644
--- a/docs/models/dcim/powerfeed.md
+++ b/docs/models/dcim/powerfeed.md
@@ -1,8 +1,21 @@
# Power Feed
-A power feed identifies the power outlet/drop that goes to a rack and is terminated to a power panel. Power feeds have a supply type (AC/DC), voltage, amperage, and phase type (single/three).
+A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power pot (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.
-Power feeds are optionally assigned to a rack. In addition, a power port â and only one â can connect to a power feed; in the context of a PDU, the power feed is analogous to the power outlet that a PDU's power port/inlet connects to.
+Each power feed is assigned an operational type (primary or redundant) and one of the following statuses:
+
+* Offline
+* Active
+* Planned
+* Failed
+
+Each power feed also defines the electrical characteristics of the circuit which it represents. These include the following:
+
+* Supply type (AC or DC)
+* Phase (single or three-phase)
+* Voltage
+* Amperage
+* Maximum utilization (percentage)
!!! info
- The power usage of a rack is calculated when a power feed (or multiple) is assigned to that rack and connected to a power port.
+ The power utilization of a rack is calculated when one or more power feeds are assigned to the rack and connected to devices that draw power.
diff --git a/docs/models/dcim/poweroutlet.md b/docs/models/dcim/poweroutlet.md
index 0ec93856e..e9ef307bd 100644
--- a/docs/models/dcim/poweroutlet.md
+++ b/docs/models/dcim/poweroutlet.md
@@ -1,3 +1,7 @@
## Power Outlets
-Power outlets represent the ports on a PDU that supply power to other devices. Power outlets are downstream-facing towards power ports. A power outlet can be associated with a power port on the same device and a feed leg (i.e. in a case of a three-phase supply). This indicates which power port supplies power to a power outlet.
+Power outlets represent the outlets on a power distribution unit (PDU) or other device that supply power to dependent devices. Each power port may be assigned a physical type, and may be associated with a specific feed leg (where three-phase power is used) and/or a specific upstream power port. This association can be used to model the distribution of power within a device.
+
+For example, imagine a PDU with one power port which draws from a three-phase feed and 48 power outlets arranged into three banks of 16 outlets each. Outlets 1-16 would be associated with leg A on the port, and outlets 17-32 and 33-48 would be associated with legs B and C, respectively.
+
+Cables can connect power outlets only to downstream power ports. (Pass-through ports cannot be used to model power distribution.)
diff --git a/docs/models/dcim/poweroutlettemplate.md b/docs/models/dcim/poweroutlettemplate.md
index e5b54af23..6f81891f1 100644
--- a/docs/models/dcim/poweroutlettemplate.md
+++ b/docs/models/dcim/poweroutlettemplate.md
@@ -1,3 +1,3 @@
## Power Outlet Templates
-A template for a power outlet that will be created on all instantiations of the parent device type.
+A template for a power outlet that will be created on all instantiations of the parent device type. Each power outlet can be assigned a physical type, and its power source may be mapped to a specific feed leg and power port template. This association will be automatically replicated when the device type is instantiated.
diff --git a/docs/models/dcim/powerpanel.md b/docs/models/dcim/powerpanel.md
index 3b05f8fad..3daecbacf 100644
--- a/docs/models/dcim/powerpanel.md
+++ b/docs/models/dcim/powerpanel.md
@@ -1,3 +1,8 @@
# Power Panel
-A power panel represents the distribution board where power circuits â and their circuit breakers â terminate on. If you have multiple power panels in your data center, you should model them as such in NetBox to assist you in determining the redundancy of your power allocation.
+A power panel represents the origin point in NetBox for electrical power being disseminated by one or more power feeds. In a data center environment, one power panel often serves a group of racks, with an individual power feed extending to each rack, though this is not always the case. It is common to have two sets of panels and feeds arranged in parallel to provide redundant power to each rack.
+
+Each power panel must be assigned to a site, and may optionally be assigned to a particular rack group.
+
+!!! note
+ NetBox does not model the mechanism by which power is delivered to a power panel. Power panels define the root level of the power distribution hierarchy in NetBox.
diff --git a/docs/models/dcim/powerport.md b/docs/models/dcim/powerport.md
index 6027fa98b..1948920d0 100644
--- a/docs/models/dcim/powerport.md
+++ b/docs/models/dcim/powerport.md
@@ -1,6 +1,8 @@
## Power Ports
-A power port is the inlet of a device where it draws its power. Power ports are upstream-facing towards power outlets. Alternatively, a power port can connect to a power feed â as mentioned in the power feed section â to indicate the power source of a PDU's inlet.
+A power port represents the inlet of a device where it draws its power, i.e. the connection port(s) on a device's power supply. Each power port may be assigned a physical type, as well as allocated and maximum draw values (in watts). These values can be used to calculate the overall utilization of an upstream power feed.
!!! info
- If the draw of a power port is left empty, it will be dynamically calculated based on the power outlets associated with that power port. This is usually the case on the power ports of devices that supply power, like a PDU.
+ When creating a power port on a device which supplies power to downstream devices, the allocated and maximum draw numbers should be left blank. Utilization will be calculated by taking the sum of all power ports of devices connected downstream.
+
+Cables can connect power ports only to power outlets or power feeds. (Pass-through ports cannot be used to model power distribution.)
diff --git a/docs/models/dcim/powerporttemplate.md b/docs/models/dcim/powerporttemplate.md
index b6e64be01..947f146ae 100644
--- a/docs/models/dcim/powerporttemplate.md
+++ b/docs/models/dcim/powerporttemplate.md
@@ -1,3 +1,3 @@
## Power Port Templates
-A template for a power port that will be created on all instantiations of the parent device type.
+A template for a power port that will be created on all instantiations of the parent device type. Each power port can be assigned a physical type, as well as a maximum and allocated draw in watts.
diff --git a/docs/models/dcim/rack.md b/docs/models/dcim/rack.md
index 39858b823..e5e52cc07 100644
--- a/docs/models/dcim/rack.md
+++ b/docs/models/dcim/rack.md
@@ -1,8 +1,10 @@
# Racks
-The rack model represents a physical two- or four-post equipment rack in which equipment is mounted. Each rack must be assigned to a site. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending or descending order.
+The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a rack group and/or tenant. Racks can also be organized by user-defined functional roles.
-Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." A unique serial number may also be associated with each rack.
+Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order.
+
+Each rack is assigned a name and (optionally) a separate facility ID. This is helpful when leasing space in a data center your organization does not own: The facility will often assign a seemingly arbitrary ID to a rack (for example, "M204.313") whereas internally you refer to is simply as "R113." A unique serial number and asset tag may also be associated with each rack.
A rack must be designated as one of the following types:
@@ -12,4 +14,12 @@ A rack must be designated as one of the following types:
* Wall-mounted frame
* Wall-mounted cabinet
-Each rack has two faces (front and rear) on which devices can be mounted. Rail-to-rail width may be 19 or 23 inches.
+Similarly, each rack must be assigned an operational status, which is one of the following:
+
+* Reserved
+* Available
+* Planned
+* Active
+* Deprecated
+
+Each rack has two faces (front and rear) on which devices can be mounted. Rail-to-rail width may be 10, 19, 21, or 23 inches. The outer width and depth of a rack or cabinet can also be annotated in millimeters or inches.
diff --git a/docs/models/dcim/rackgroup.md b/docs/models/dcim/rackgroup.md
index f5b2428e6..974285f71 100644
--- a/docs/models/dcim/rackgroup.md
+++ b/docs/models/dcim/rackgroup.md
@@ -1,7 +1,7 @@
# Rack Groups
-Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room.
+Racks can be organized into groups, which can be nested into themselves similar to regions. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room.
-Each rack group must be assigned to a parent site, and rack groups may optionally be nested to achieve a multi-level hierarchy.
+Each rack group must be assigned to a parent site, and rack groups may optionally be nested within a site to model a multi-level hierarchy. For example, you might have a tier of rooms beneath a tier of floors, all belonging to the same parent building (site).
The name and facility ID of each rack within a group must be unique. (Racks not assigned to the same rack group may have identical names and/or facility IDs.)
diff --git a/docs/models/dcim/rackreservation.md b/docs/models/dcim/rackreservation.md
index 09de55553..0ed9651a0 100644
--- a/docs/models/dcim/rackreservation.md
+++ b/docs/models/dcim/rackreservation.md
@@ -1,3 +1,3 @@
# Rack Reservations
-Users can reserve units within a rack for future use. Multiple non-contiguous rack units can be associated with a single reservation (but reservations cannot span multiple racks). A rack reservation may optionally designate a specific tenant.
+Users can reserve specific units within a rack for future use. An arbitrary set of units within a rack can be associated with a single reservation, but reservations cannot span multiple racks. A description is required for each reservation, reservations may optionally be associated with a specific tenant.
diff --git a/docs/models/dcim/rackrole.md b/docs/models/dcim/rackrole.md
index 63e9c1469..1375ce692 100644
--- a/docs/models/dcim/rackrole.md
+++ b/docs/models/dcim/rackrole.md
@@ -1,3 +1,3 @@
# Rack Roles
-Each rack can optionally be assigned a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. Rack roles are fully customizable.
+Each rack can optionally be assigned a user-defined functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. Rack roles are fully customizable and may be color-coded.
diff --git a/docs/models/dcim/rearport.md b/docs/models/dcim/rearport.md
index 8c8136338..41c5b3037 100644
--- a/docs/models/dcim/rearport.md
+++ b/docs/models/dcim/rearport.md
@@ -1,5 +1,6 @@
## Rear Ports
-Like front ports, rear ports are pass-through ports which represent the end of a particular cable segment in a path. Each rear port is defined with a number of positions: rear ports with more than one position can be mapped to multiple front ports. This can be useful for modeling instances where multiple paths share a common cable (for example, six different fiber connections sharing a 12-strand MPO cable).
+Like front ports, rear ports are pass-through ports which represent the continuation of a path from one cable to the next. Each rear port is defined with its physical type and a number of positions: Rear ports with more than one position can be mapped to multiple front ports. This can be useful for modeling instances where multiple paths share a common cable (for example, six discrete two-strand fiber connections sharing a 12-strand MPO cable).
-Note that front and rear ports need not necessarily reside on the actual front or rear device face. This terminology is used primarily to distinguish between the two components in a pass-through port pairing.
+!!! note
+ Front and rear ports need not necessarily reside on the actual front or rear device face. This terminology is used primarily to distinguish between the two components in a pass-through port pairing.
diff --git a/docs/models/dcim/rearporttemplate.md b/docs/models/dcim/rearporttemplate.md
index 448c0befd..01ba02ac0 100644
--- a/docs/models/dcim/rearporttemplate.md
+++ b/docs/models/dcim/rearporttemplate.md
@@ -1,3 +1,3 @@
## Rear Port Templates
-A template for a rear-facing pass-through port that will be created on all instantiations of the parent device type.
+A template for a rear-facing pass-through port that will be created on all instantiations of the parent device type. Each rear port may have a physical type and one or more front port templates assigned to it. The number of positions associated with a rear port determines how many front ports can be assigned to it (the maximum is 1024).
diff --git a/docs/models/dcim/site.md b/docs/models/dcim/site.md
index b13056a99..6617b950c 100644
--- a/docs/models/dcim/site.md
+++ b/docs/models/dcim/site.md
@@ -1,13 +1,15 @@
# Sites
-How you choose to use sites will depend on the nature of your organization, but typically a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities.
+How you choose to employ sites when modeling your network may vary depending on the nature of your organization, but generally a site will equate to a building or campus. For example, a chain of banks might create a site to represent each of its branches, a site for its corporate headquarters, and two additional sites for its presence in two colocation facilities.
-Each site must be assigned one of the following operational statuses:
+Each site must be assigned a unique name and may optionally be assigned to a region and/or tenant. The following operational statuses are available:
-* Active
* Planned
+* Staging
+* Active
+* Decommissioning
* Retired
-The site model provides a facility ID field which can be used to annotate a facility ID (such as a datacenter name) associated with the site. Each site may also have an autonomous system (AS) number and time zone associated with it. (Time zones are provided by the [pytz](https://pypi.org/project/pytz/) package.)
+The site model also provides a facility ID field which can be used to annotate a facility ID (such as a datacenter name) associated with the site. Each site may also have an autonomous system (AS) number and time zone associated with it. (Time zones are provided by the [pytz](https://pypi.org/project/pytz/) package.)
-The site model also includes several fields for storing contact and address information.
+The site model also includes several fields for storing contact and address information as well as geolocation data (GPS coordinates).
diff --git a/docs/models/dcim/virtualchassis.md b/docs/models/dcim/virtualchassis.md
index e1707918b..b2a7d3bc9 100644
--- a/docs/models/dcim/virtualchassis.md
+++ b/docs/models/dcim/virtualchassis.md
@@ -1,5 +1,8 @@
# Virtual Chassis
-A virtual chassis represents a set of devices which share a single control plane: a stack of switches which are managed as a single device, for example. Each device in the virtual chassis is assigned a position and (optionally) a priority. Exactly one device is designated the virtual chassis master: This device will typically be assigned a name, secrets, services, and other attributes related to its management.
+A virtual chassis represents a set of devices which share a common control plane. A common example of this is a stack of switches which are connected and configured to operate as a single device. A virtual chassis must be assigned a name and may be assigned a domain.
-It's important to recognize the distinction between a virtual chassis and a chassis-based device. For instance, a virtual chassis is not used to model a chassis switch with removable line cards such as the Juniper EX9208, as its line cards are _not_ physically separate devices capable of operating independently.
+Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. One of the devices may be designated as the VC master: This device will typically be assigned a name, secrets, services, and other attributes related to managing the VC.
+
+!!! note
+ It's important to recognize the distinction between a virtual chassis and a chassis-based device. A virtual chassis is **not** suitable for modeling a chassis-based switch with removable line cards (such as the Juniper EX9208), as its line cards are _not_ physically autonomous devices.
diff --git a/docs/models/extras/configcontext.md b/docs/models/extras/configcontext.md
index 380e631d8..af81cfbf9 100644
--- a/docs/models/extras/configcontext.md
+++ b/docs/models/extras/configcontext.md
@@ -1,5 +1,65 @@
# Configuration Contexts
-Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. For example, you might want to associate a set of syslog servers for all devices at a particular site. Context data enables the association of arbitrary data to devices and virtual machines grouped by region, site, role, platform, and/or tenant. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
+Sometimes it is desirable to associate additional data with a group of devices or virtual machines to aid in automated configuration. For example, you might want to associate a set of syslog servers for all devices within a particular region. Context data enables the association of extra user-defined data with devices and virtual machines grouped by one or more of the following assignments:
-Devices and Virtual Machines may also have a local config context defined. This local context will always overwrite the rendered config context objects for the Device/VM. This is useful in situations were the device requires a one-off value different from the rest of the environment.
+* Region
+* Site
+* Role
+* Platform
+* Cluster group
+* Cluster
+* Tenant group
+* Tenant
+* Tag
+
+## Hierarchical Rendering
+
+Context data is arranged hierarchically, so that data with a higher weight can be entered to override lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object.
+
+For example, suppose we want to specify a set of syslog and NTP servers for all devices within a region. We could create a config context instance with a weight of 1000 assigned to the region, with the following JSON data:
+
+```json
+{
+ "ntp-servers": [
+ "172.16.10.22",
+ "172.16.10.33"
+ ],
+ "syslog-servers": [
+ "172.16.9.100",
+ "172.16.9.101"
+ ]
+}
+```
+
+But suppose there's a problem at one particular site within this region preventing traffic from reaching the regional syslog server. Devices there need to use a local syslog server instead of the two defined above. We'll create a second config context assigned only to that site with a weight of 2000 and the following data:
+
+```json
+{
+ "syslog-servers": [
+ "192.168.43.107"
+ ]
+}
+```
+
+When the context data for a device at this site is rendered, the second, higher-weight data overwrite the first, resulting in the following:
+
+```json
+{
+ "ntp-servers": [
+ "172.16.10.22",
+ "172.16.10.33"
+ ],
+ "syslog-servers": [
+ "192.168.43.107"
+ ]
+}
+```
+
+Data from the higher-weight context overwrites conflicting data from the lower-weight context, while the non-conflicting portion of the lower-weight context (the list of NTP servers) is preserved.
+
+## Local Context Data
+
+Devices and virtual machines may also have a local config context defined. This local context will _always_ take precedence over any separate config context objects which apply to the device/VM. This is useful in situations where we need to call out a specific deviation in the data for a particular object.
+
+!!! warning
+ If you find that you're routinely defining local context data for many individual devices or virtual machines, custom fields may offer a more effective solution.
diff --git a/docs/models/extras/imageattachment.md b/docs/models/extras/imageattachment.md
new file mode 100644
index 000000000..da15462ab
--- /dev/null
+++ b/docs/models/extras/imageattachment.md
@@ -0,0 +1,3 @@
+# Image Attachments
+
+Certain objects in NetBox support the attachment of uploaded images. These will be saved to the NetBox server and made available whenever the object is viewed.
diff --git a/docs/models/extras/tag.md b/docs/models/extras/tag.md
index f94957616..29cc8b757 100644
--- a/docs/models/extras/tag.md
+++ b/docs/models/extras/tag.md
@@ -1,24 +1,20 @@
# Tags
-Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object (for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
+Tags are user-defined labels which can be applied to a variety of objects within NetBox. They can be used to establish dimensions of organization beyond the relationships built into NetBox. For example, you might create a tag to identify a particular ownership or condition across several types of objects.
-Each tag has a label and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters.
+Each tag has a label, color, and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters. Each tag can also be assigned a description indicating its purpose.
Objects can be filtered by the tags they have applied. For example, the following API request will retrieve all devices tagged as "monitored":
-```
+```no-highlight
GET /api/dcim/devices/?tag=monitored
```
-Tags are included in the API representation of an object as a list of plain strings:
+The `tag` filter can be specified multiple times to match only objects which have _all_ of the specified tags assigned:
+```no-highlight
+GET /api/dcim/devices/?tag=monitored&tag=deprecated
```
-{
- ...
- "tags": [
- "Core Switch",
- "Monitored"
- ],
- ...
-}
-```
+
+!!! note
+ Tags have changed substantially in NetBox v2.9. They are no longer created on-demand when editing an object, and their representation in the REST API now includes a complete depiction of the tag rather than only its label.
diff --git a/docs/models/ipam/aggregate.md b/docs/models/ipam/aggregate.md
index f43209619..ff5a50a39 100644
--- a/docs/models/ipam/aggregate.md
+++ b/docs/models/ipam/aggregate.md
@@ -1,6 +1,18 @@
# Aggregates
-The first step to documenting your IP space is to define its scope by creating aggregates. Aggregates establish the root of your IP address hierarchy by defining the top-level allocations that you're interested in managing. Most organizations will want to track some commonly-used private IP spaces, such as:
+IP addressing is by nature hierarchical. The first few levels of the IPv4 hierarchy, for example, look like this:
+
+* 0.0.0.0/0
+ * 0.0.0.0/1
+ * 0.0.0.0/2
+ * 64.0.0.0/2
+ * 128.0.0.0/1
+ * 128.0.0.0/2
+ * 192.0.0.0/2
+
+This hierarchy comprises 33 tiers of addressing, from /0 all the way down to individual /32 address (and much, much further to /128 for IPv6). Of course, most organizations are concerned with only relatively small portions of the total IP space, so tracking the uppermost of these tiers isn't necessary.
+
+NetBox allows us to specify the portions of IP space that are interesting to us by defining _aggregates_. Typically, an aggregate will correspond to either an allocation of public (globally routable) IP space granted by a regional authority, or a private (internally-routable) designation. Common private designations include:
* 10.0.0.0/8 (RFC 1918)
* 100.64.0.0/10 (RFC 6598)
@@ -8,8 +20,9 @@ The first step to documenting your IP space is to define its scope by creating a
* 192.168.0.0/16 (RFC 1918)
* One or more /48s within fd00::/8 (IPv6 unique local addressing)
-In addition to one or more of these, you'll want to create an aggregate for each globally-routable space your organization has been allocated. These aggregates should match the allocations recorded in public WHOIS databases.
+Each aggregate is assigned to a RIR. For "public" aggregates, this will be the real-world authority which has granted your organization permission to use the specified IP space on the public Internet. For "private" aggregates, this will be a statutory authority, such as RFC 1918. Each aggregate can also annotate that date on which it was allocated, where applicable.
-Each IP prefix will be automatically arranged under its parent aggregate if one exists. Note that it's advised to create aggregates only for IP ranges actually allocated to your organization (or marked for private use): There is no need to define aggregates for provider-assigned space which is only used on Internet circuits, for example.
+Prefixes are automatically arranged beneath their parent aggregates in NetBox. Typically you'll want to create aggregates only for the prefixes and IP addresses that your organization actually manages: There is no need to define aggregates for provider-assigned space which is only used on Internet circuits, for example.
-Aggregates cannot overlap with one another: They can only exist side-by-side. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix and automatically grouped under 10.0.0.0/8. Remember, the purpose of aggregates is to establish the root of your IP addressing hierarchy.
+!!! note
+ Because aggregates represent swaths of the global IP space, they cannot overlap with one another: They can only exist side-by-side. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a container prefix and automatically grouped under the 10.0.0.0/8 aggregate. Remember, the purpose of aggregates is to establish the root of your IP addressing hierarchy.
diff --git a/docs/models/ipam/ipaddress.md b/docs/models/ipam/ipaddress.md
index cbe12553d..1ea613997 100644
--- a/docs/models/ipam/ipaddress.md
+++ b/docs/models/ipam/ipaddress.md
@@ -2,16 +2,17 @@
An IP address comprises a single host address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world.
-Like prefixes, an IP address can optionally be assigned to a VRF (otherwise, it will appear in the "global" table). IP addresses are automatically organized under parent prefixes within their respective VRFs.
+Like a prefix, an IP address can optionally be assigned to a VRF (otherwise, it will appear in the "global" table). IP addresses are automatically arranged under parent prefixes within their respective VRFs according to the IP hierarchy.
-Also like prefixes, each IP address can be assigned a status and a role. Statuses are hard-coded in NetBox and include the following:
+Each IP address can also be assigned an operational status and a functional role. Statuses are hard-coded in NetBox and include the following:
* Active
* Reserved
* Deprecated
* DHCP
+* SLAAC (IPv6 Stateless Address Autoconfiguration)
-Each IP address can optionally be assigned a special role. Roles are used to indicate some special attribute of an IP address: for example, it is used as a loopback, or is a virtual IP maintained using VRRP. (Note that this differs in purpose from a _functional_ role, and thus cannot be customized.) Available roles include:
+Roles are used to indicate some special attribute of an IP address; for example, use as a loopback or as the the virtual IP for a VRRP group. (Note that functional roles are conceptual in nature, and thus cannot be customized by the user.) Available roles include:
* Loopback
* Secondary
@@ -21,7 +22,10 @@ Each IP address can optionally be assigned a special role. Roles are used to ind
* HSRP
* GLBP
-An IP address can be assigned to a device or virtual machine interface, and an interface may have multiple IP addresses assigned to it. Further, each device and virtual machine may have one of its interface IPs designated as its primary IP address (one for IPv4 and one for IPv6).
+An IP address can be assigned to any device or virtual machine interface, and an interface may have multiple IP addresses assigned to it. Further, each device and virtual machine may have one of its interface IPs designated as its primary IP per address family (one for IPv4 and one for IPv6).
+
+!!! note
+ When primary IPs are set for both IPv4 and IPv6, NetBox will prefer IPv6. This can be changed by setting the `PREFER_IPV4` configuration parameter.
## Network Address Translation (NAT)
diff --git a/docs/models/ipam/prefix.md b/docs/models/ipam/prefix.md
index 9ab5382a5..bd5e9695f 100644
--- a/docs/models/ipam/prefix.md
+++ b/docs/models/ipam/prefix.md
@@ -2,7 +2,7 @@
A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 192.0.2.0/24). A prefix entails only the "network portion" of an IP address: All bits in the address not covered by the mask must be zero. (In other words, a prefix cannot be a specific IP address.)
-Prefixes are automatically arranged by their parent aggregates. Additionally, each prefix can be assigned to a particular site and VRF (routing table). All prefixes not assigned to a VRF will appear in the "global" table.
+Prefixes are automatically organized by their parent aggregates. Additionally, each prefix can be assigned to a particular site and virtual routing and forwarding instance (VRF). Each VRF represents a separate IP space or routing table. All prefixes not assigned to a VRF are considered to be in the "global" table.
Each prefix can be assigned a status and a role. These terms are often used interchangeably so it's important to recognize the difference between them. The **status** defines a prefix's operational state. Statuses are hard-coded in NetBox and can be one of the following:
@@ -13,6 +13,6 @@ Each prefix can be assigned a status and a role. These terms are often used inte
On the other hand, a prefix's **role** defines its function. Role assignment is optional and roles are fully customizable. For example, you might create roles to differentiate between production and development infrastructure.
-A prefix may also be assigned to a VLAN. This association is helpful for identifying which prefixes are included when reviewing a list of VLANs.
+A prefix may also be assigned to a VLAN. This association is helpful for associating address space with layer two domains. A VLAN may have multiple prefixes assigned to it.
-The prefix model include a "pool" flag. If enabled, NetBox will treat this prefix as a range (such as a NAT pool) wherein every IP address is valid and assignable. This logic is used for identifying available IP addresses within a prefix. If this flag is disabled, NetBox will assume that the first and last (broadcast) address within the prefix are unusable.
+The prefix model include an "is pool" flag. If enabled, NetBox will treat this prefix as a range (such as a NAT pool) wherein every IP address is valid and assignable. This logic is used when identifying available IP addresses within a prefix. If this flag is disabled, NetBox will assume that the first and last (broadcast) address within an IPv4 prefix are unusable.
diff --git a/docs/models/ipam/rir.md b/docs/models/ipam/rir.md
index 69c34e72d..6904381ac 100644
--- a/docs/models/ipam/rir.md
+++ b/docs/models/ipam/rir.md
@@ -1,7 +1,7 @@
# Regional Internet Registries (RIRs)
-[Regional Internet registries](https://en.wikipedia.org/wiki/Regional_Internet_registry) are responsible for the allocation of globally-routable address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for internal use, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. There also exist lower-tier registries which serve a particular geographic area.
+[Regional Internet registries](https://en.wikipedia.org/wiki/Regional_Internet_registry) are responsible for the allocation of globally-routable address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for internal use, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space. There also exist lower-tier registries which serve particular geographic areas.
-Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own). The RIR model includes a boolean flag which indicates whether the RIR allocates only private IP space.
+Users can create whatever RIRs they like, but each aggregate must be assigned to one RIR. The RIR model includes a boolean flag which indicates whether the RIR allocates only private IP space.
-For example, suppose your organization has been allocated 104.131.0.0/16 by ARIN. It also makes use of RFC 1918 addressing internally. You would first create RIRs named ARIN and RFC 1918, then create an aggregate for each of these top-level prefixes, assigning it to its respective RIR.
+For example, suppose your organization has been allocated 104.131.0.0/16 by ARIN. It also makes use of RFC 1918 addressing internally. You would first create RIRs named "ARIN" and "RFC 1918," then create an aggregate for each of these top-level prefixes, assigning it to its respective RIR.
diff --git a/docs/models/ipam/routetarget.md b/docs/models/ipam/routetarget.md
new file mode 100644
index 000000000..b71e96904
--- /dev/null
+++ b/docs/models/ipam/routetarget.md
@@ -0,0 +1,5 @@
+# Route Targets
+
+A route target is a particular type of [extended BGP community](https://tools.ietf.org/html/rfc4360#section-4) used to control the redistribution of routes among VRF tables in a network. Route targets can be assigned to individual VRFs in NetBox as import or export targets (or both) to model this exchange in an L3VPN. Each route target must be given a unique name, which should be in a format prescribed by [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), similar to a VR route distinguisher.
+
+Each route target can optionally be assigned to a tenant, and may have tags assigned to it.
diff --git a/docs/models/ipam/vlan.md b/docs/models/ipam/vlan.md
index 48f24006c..f252204c5 100644
--- a/docs/models/ipam/vlan.md
+++ b/docs/models/ipam/vlan.md
@@ -1,6 +1,6 @@
# VLANs
-A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site and/or VLAN group.
+A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site, tenant, and/or VLAN group.
Each VLAN must be assigned one of the following operational statuses:
@@ -8,4 +8,4 @@ Each VLAN must be assigned one of the following operational statuses:
* Reserved
* Deprecated
-Each VLAN may also be assigned a functional role. Prefixes and VLANs share the same set of customizable roles.
+As with prefixes, each VLAN may also be assigned a functional role. Prefixes and VLANs share the same set of customizable roles.
diff --git a/docs/models/ipam/vlangroup.md b/docs/models/ipam/vlangroup.md
index 1fa31c522..7a0bb80ff 100644
--- a/docs/models/ipam/vlangroup.md
+++ b/docs/models/ipam/vlangroup.md
@@ -1,3 +1,5 @@
# VLAN Groups
-VLAN groups can be used to organize VLANs within NetBox. Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group.
+VLAN groups can be used to organize VLANs within NetBox. Each group may optionally be assigned to a specific site, but a group cannot belong to multiple sites.
+
+Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group.
diff --git a/docs/models/ipam/vrf.md b/docs/models/ipam/vrf.md
index c3d3390e4..392141fdd 100644
--- a/docs/models/ipam/vrf.md
+++ b/docs/models/ipam/vrf.md
@@ -1,12 +1,14 @@
# Virtual Routing and Forwarding (VRF)
-A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space).
+A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space). Each VRF may be assigned to a specific tenant to aid in organizing the available IP space by customer or internal user.
Each VRF is assigned a unique name and an optional route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
-Each prefix and IP address may be assigned to one (and only one) VRF. If you have a prefix or IP address which exists in multiple VRFs, you will need to create a separate instance of it in NetBox for each VRF. Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.
+Each prefix and IP address may be assigned to one (and only one) VRF. If you have a prefix or IP address which exists in multiple VRFs, you will need to create a separate instance of it in NetBox for each VRF. Any prefix or IP address not assigned to a VRF is said to belong to the "global" table.
-By default, NetBox will allow duplicate prefixes to be assigned to a VRF. This behavior can be disabled by setting the "enforce unique" flag on the VRF model.
+By default, NetBox will allow duplicate prefixes to be assigned to a VRF. This behavior can be toggled by setting the "enforce unique" flag on the VRF model.
!!! note
Enforcement of unique IP space can be toggled for global table (non-VRF prefixes) using the `ENFORCE_GLOBAL_UNIQUE` configuration setting.
+
+Each VRF may have one or more import and/or export route targets applied to it. Route targets are used to control the exchange of routes (prefixes) among VRFs in L3VPNs.
diff --git a/docs/models/secrets/secretrole.md b/docs/models/secrets/secretrole.md
index 8997ed52a..23f68912b 100644
--- a/docs/models/secrets/secretrole.md
+++ b/docs/models/secrets/secretrole.md
@@ -7,5 +7,3 @@ Each secret is assigned a functional role which indicates what it is used for. S
* RADIUS/TACACS+ keys
* IKE key strings
* Routing protocol shared secrets
-
-Roles are also used to control access to secrets. Each role is assigned an arbitrary number of groups and/or users. Only the users associated with a role have permission to decrypt the secrets assigned to that role. (A superuser has permission to decrypt all secrets, provided they have an active user key.)
diff --git a/docs/models/tenancy/tenant.md b/docs/models/tenancy/tenant.md
index f7cf68ab8..60a160b9e 100644
--- a/docs/models/tenancy/tenant.md
+++ b/docs/models/tenancy/tenant.md
@@ -1,6 +1,6 @@
# Tenants
-A tenant represents a discrete entity for administrative purposes. Typically, tenants are used to represent individual customers or internal departments within an organization. The following objects can be assigned to tenants:
+A tenant represents a discrete grouping of resources used for administrative purposes. Typically, tenants are used to represent individual customers or internal departments within an organization. The following objects can be assigned to tenants:
* Sites
* Racks
@@ -11,6 +11,7 @@ A tenant represents a discrete entity for administrative purposes. Typically, te
* IP addresses
* VLANs
* Circuits
+* Clusters
* Virtual machines
-Tenant assignment is used to signify ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so tenant assignment would not be appropriate.
+Tenant assignment is used to signify the ownership of an object in NetBox. As such, each object may only be owned by a single tenant. For example, if you have a firewall dedicated to a particular customer, you would assign it to the tenant which represents that customer. However, if the firewall serves multiple customers, it doesn't *belong* to any particular customer, so tenant assignment would not be appropriate.
diff --git a/docs/models/tenancy/tenantgroup.md b/docs/models/tenancy/tenantgroup.md
index a2ed7e324..078a71a72 100644
--- a/docs/models/tenancy/tenantgroup.md
+++ b/docs/models/tenancy/tenantgroup.md
@@ -1,5 +1,5 @@
# Tenant Groups
-Tenants can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional.
+Tenants can be organized by custom groups. For instance, you might create one group called "Customers" and one called "Departments." The assignment of a tenant to a group is optional.
-Tenant groups may be nested to achieve a multi-level hierarchy. For example, you might have a group called "Customers" containing subgroups of individual tenants grouped by product or account team.
+Tenant groups may be nested recursively to achieve a multi-level hierarchy. For example, you might have a group called "Customers" containing subgroups of individual tenants grouped by product or account team.
diff --git a/docs/models/users/objectpermission.md b/docs/models/users/objectpermission.md
new file mode 100644
index 000000000..48970dd05
--- /dev/null
+++ b/docs/models/users/objectpermission.md
@@ -0,0 +1,55 @@
+# Object Permissions
+
+A permission in NetBox represents a relationship shared by several components:
+
+* Object type(s) - One or more types of object in NetBox
+* User(s)/Group(s) - One or more users or groups of users
+* Action(s) - The action(s) that can be performed on an object
+* Constraints - An arbitrary filter used to limit the granted action(s) to a specific subset of objects
+
+At a minimum, a permission assignment must specify one object type, one user or group, and one action. The specification of constraints is optional: A permission without any constraints specified will apply to all instances of the selected model(s).
+
+## Actions
+
+There are four core actions that can be permitted for each type of object within NetBox, roughly analogous to the CRUD convention (create, read, update, and delete):
+
+* **View** - Retrieve an object from the database
+* **Add** - Create a new object
+* **Change** - Modify an existing object
+* **Delete** - Delete an existing object
+
+In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `napalm_read` permission on the device model allows a user to execute NAPALM queries on a device via NetBox's REST API. These can be specified when granting a permission in the "additional actions" field.
+
+!!! note
+ Internally, all actions granted by a permission (both built-in and custom) are stored as strings in an array field named `actions`.
+
+## Constraints
+
+Constraints are expressed as a JSON object or list representing a [Django query filter](https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups). This is the same syntax that you would pass to the QuerySet `filter()` method when performing a query using the Django ORM. As with query filters, double underscores can be used to traverse related objects or invoke lookup expressions. Some example queries and their corresponding definitions are shown below.
+
+All attributes defined within a single JSON object are applied with a logical AND. For example, suppose you assign a permission for the site model with the following constraints.
+
+```json
+{
+ "status": "active",
+ "region__name": "Americas"
+}
+```
+
+The permission will grant access only to sites which have a status of "active" **and** which are assigned to the "Americas" region.
+
+To achieve a logical OR with a different set of constraints, define multiple objects within a list. For example, if you want to constrain the permission to VLANs with an ID between 100 and 199 _or_ a status of "reserved," do the following:
+
+```json
+[
+ {
+ "vid__gte": 100,
+ "vid__lt": 200
+ },
+ {
+ "status": "reserved"
+ }
+]
+```
+
+Additionally, where multiple permissions have been assigned for an object type, their collective constraints will be merged using a logical "OR" operation.
diff --git a/docs/models/users/token.md b/docs/models/users/token.md
index bbeb2284b..d0e0f8609 100644
--- a/docs/models/users/token.md
+++ b/docs/models/users/token.md
@@ -1,12 +1,12 @@
## Tokens
-A token is a unique identifier that identifies a user to the API. Each user in NetBox may have one or more tokens which he or she can use to authenticate to the API. To create a token, navigate to the API tokens page at `/user/api-tokens/`.
+A token is a unique identifier mapped to a NetBox user account. Each user may have one or more tokens which he or she can use for authentication when making REST API requests. To create a token, navigate to the API tokens page under your user profile.
!!! note
The creation and modification of API tokens can be restricted per user by an administrator. If you don't see an option to create an API token, ask an administrator to grant you access.
Each token contains a 160-bit key represented as 40 hexadecimal characters. When creating a token, you'll typically leave the key field blank so that a random key will be automatically generated. However, NetBox allows you to specify a key in case you need to restore a previously deleted token to operation.
-By default, a token can be used for all operations available via the API. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only.
+By default, a token can be used to perform all actions via the API that a user would be permitted to do via the web UI. Deselecting the "write enabled" option will restrict API requests made with the token to read operations (e.g. GET) only.
Additionally, a token can be set to expire at a specific time. This can be useful if an external client needs to be granted temporary access to NetBox.
diff --git a/docs/models/virtualization/cluster.md b/docs/models/virtualization/cluster.md
index 6d8ce4214..3311ad42d 100644
--- a/docs/models/virtualization/cluster.md
+++ b/docs/models/virtualization/cluster.md
@@ -1,5 +1,5 @@
# Clusters
-A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type, and may optionally be assigned to a group and/or site.
+A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type (technological classification), and may optionally be assigned to a cluster group, site, and/or tenant.
-Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular VM may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
+Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
diff --git a/docs/models/virtualization/clustergroup.md b/docs/models/virtualization/clustergroup.md
index 9e1e17315..6dd0f9688 100644
--- a/docs/models/virtualization/clustergroup.md
+++ b/docs/models/virtualization/clustergroup.md
@@ -1,3 +1,3 @@
# Cluster Groups
-Cluster groups may be created for the purpose of organizing clusters. The assignment of clusters to groups is optional.
+Cluster groups may be created for the purpose of organizing clusters. The arrangement of clusters into groups is optional.
diff --git a/docs/models/virtualization/virtualmachine.md b/docs/models/virtualization/virtualmachine.md
index 5a82f8267..40e9ef2c0 100644
--- a/docs/models/virtualization/virtualmachine.md
+++ b/docs/models/virtualization/virtualmachine.md
@@ -1,11 +1,14 @@
# Virtual Machines
-A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be associated with exactly one cluster.
+A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be assigned to exactly one cluster.
-Like devices, each VM can be assigned a platform and have interfaces created on it. VM interfaces behave similarly to device interfaces, and can be assigned IP addresses, VLANs, and services. However, given their virtual nature, they cannot be connected to other interfaces. Unlike physical devices, VMs cannot be assigned console or power ports, device bays, or inventory items.
+Like devices, each VM can be assigned a platform and/or functional role, and must have one of the following operational statuses assigned to it:
-The following resources can be defined for each VM:
+* Active
+* Offline
+* Planned
+* Staged
+* Failed
+* Decommissioning
-* vCPU count
-* Memory (MB)
-* Disk space (GB)
+Additional fields are available for annotating the vCPU count, memory (GB), and disk (GB) allocated to each VM. Each VM may optionally be assigned to a tenant. Virtual machines may have virtual interfaces assigned to them, but do not support any physical component.
diff --git a/docs/models/virtualization/vminterface.md b/docs/models/virtualization/vminterface.md
new file mode 100644
index 000000000..6fac7ce36
--- /dev/null
+++ b/docs/models/virtualization/vminterface.md
@@ -0,0 +1,3 @@
+## Interfaces
+
+Virtual machine interfaces behave similarly to device interfaces, and can be assigned IP addresses, VLANs, and services. However, given their virtual nature, they lack properties pertaining to physical attributes. For example, VM interfaces do not have a physical type and cannot have cables attached to them.
diff --git a/docs/plugins/development.md b/docs/plugins/development.md
index ad7eef310..f008da2fb 100644
--- a/docs/plugins/development.md
+++ b/docs/plugins/development.md
@@ -12,6 +12,9 @@ Plugins can do a lot, including:
However, keep in mind that each piece of functionality is entirely optional. For example, if your plugin merely adds a piece of middleware or an API endpoint for existing data, there's no need to define any new models.
+!!! warning
+ While very powerful, the NetBox plugins API is necessarily limited in its scope. The plugins API is discussed here in its entirety: Any part of the NetBox code base not documented here is _not_ part of the supported plugins API, and should not be employed by a plugin. Internal elements of NetBox are subject to change at any time and without warning. Plugin authors are **strongly** encouraged to develop plugins using only the officially supported components discussed here and those provided by the underlying Django framework so as to avoid breaking changes in future releases.
+
## Initial Setup
## Plugin Structure
@@ -60,11 +63,15 @@ setup(
install_requires=[],
packages=find_packages(),
include_package_data=True,
+ zip_safe=False,
)
```
Many of these are self-explanatory, but for more information, see the [setuptools documentation](https://setuptools.readthedocs.io/en/latest/setuptools.html).
+!!! note
+ `zip_safe=False` is **required** as the current plugin iteration is not zip safe due to upstream python issue [issue19699](https://bugs.python.org/issue19699)
+
### Define a PluginConfig
The `PluginConfig` class is a NetBox-specific wrapper around Django's built-in [`AppConfig`](https://docs.djangoproject.com/en/stable/ref/applications/) class. It is used to declare NetBox plugin functionality within a Python package. Each plugin should provide its own subclass, defining its name, metadata, and default and required configuration parameters. An example is below:
@@ -110,6 +117,8 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
+All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.
+
### Install the Plugin for Development
To ease development, it is recommended to go ahead and install the plugin at this point using setuptools' `develop` mode. This will create symbolic links within your Python environment to the plugin development directory. Call `setup.py` from the plugin's root directory with the `develop` argument (instead of `install`):
@@ -196,26 +205,37 @@ class RandomAnimalView(View):
})
```
-This view retrieves a random animal from the database and and passes it as a context variable when rendering a template named `animal.html`, which doesn't exist yet. To create this template, first create a directory named `templates/netbox_animal_sounds/` within the plugin source directory. (We use the plugin's name as a subdirectory to guard against naming collisions with other plugins.) Then, create `animal.html`:
+This view retrieves a random animal from the database and and passes it as a context variable when rendering a template named `animal.html`, which doesn't exist yet. To create this template, first create a directory named `templates/netbox_animal_sounds/` within the plugin source directory. (We use the plugin's name as a subdirectory to guard against naming collisions with other plugins.) Then, create a template named `animal.html` as described below.
+
+### Extending the Base Template
+
+NetBox provides a base template to ensure a consistent user experience, which plugins can extend with their own content. This template includes four content blocks:
+
+* `title` - The page title
+* `header` - The upper portion of the page
+* `content` - The main page body
+* `javascript` - A section at the end of the page for including Javascript code
+
+For more information on how template blocks work, consult the [Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#block).
```jinja2
{% extends 'base.html' %}
{% block content %}
-{% with config=settings.PLUGINS_CONFIG.netbox_animal_sounds %}
-
- {% if animal %}
- The {{ animal.name|lower }} says
- {% if config.loud %}
- {{ animal.sound|upper }}!
- {% else %}
- {{ animal.sound }}
- {% endif %}
- {% else %}
- No animals have been created yet!
- {% endif %}
-
-{% endwith %}
+ {% with config=settings.PLUGINS_CONFIG.netbox_animal_sounds %}
+
+ {% if animal %}
+ The {{ animal.name|lower }} says
+ {% if config.loud %}
+ {{ animal.sound|upper }}!
+ {% else %}
+ {{ animal.sound }}
+ {% endif %}
+ {% else %}
+ No animals have been created yet!
+ {% endif %}
+
+ {% endwith %}
{% endblock %}
```
@@ -326,6 +346,9 @@ A `PluginMenuButton` has the following attributes:
* `color` - One of the choices provided by `ButtonColorChoices` (optional)
* `permissions` - A list of permissions required to display this button (optional)
+!!! note
+ Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons.
+
## Extending Core Templates
Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
diff --git a/docs/plugins/index.md b/docs/plugins/index.md
index 1f5587539..202e0a96b 100644
--- a/docs/plugins/index.md
+++ b/docs/plugins/index.md
@@ -64,6 +64,15 @@ PLUGINS_CONFIG = {
}
```
+### Run Database Migrations
+
+If the plugin introduces new database models, run the provided schema migrations:
+
+```no-highlight
+(venv) $ cd /opt/netbox/netbox/
+(venv) $ python3 manage.py migrate
+```
+
### Collect Static Files
Plugins may package static files to be served directly by the HTTP front end. Ensure that these are copied to the static root directory with the `collectstatic` management command:
diff --git a/docs/release-notes/index.md b/docs/release-notes/index.md
index 364b2cd9d..8990f83e0 120000
--- a/docs/release-notes/index.md
+++ b/docs/release-notes/index.md
@@ -1 +1 @@
-version-2.8.md
\ No newline at end of file
+version-2.10.md
\ No newline at end of file
diff --git a/docs/release-notes/version-2.1.md b/docs/release-notes/version-2.1.md
index 59f23c090..e5fa41d82 100644
--- a/docs/release-notes/version-2.1.md
+++ b/docs/release-notes/version-2.1.md
@@ -121,7 +121,7 @@ A new API endpoint has been added at `/api/ipam/prefixes//available-ips/`. A
#### NAPALM Integration ([#1348](https://github.com/netbox-community/netbox/issues/1348))
-The [NAPALM automation](https://napalm-automation.net/) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](http://netbox.readthedocs.io/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py.
+The [NAPALM automation](https://napalm-automation.net/) library provides an abstracted interface for pulling live data (e.g. uptime, software version, running config, LLDP neighbors, etc.) from network devices. The NetBox API has been extended to support executing read-only NAPALM methods on devices defined in NetBox. To enable this functionality, ensure that NAPALM has been installed (`pip install napalm`) and the `NETBOX_USERNAME` and `NETBOX_PASSWORD` [configuration parameters](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/#netbox_username) have been set in configuration.py.
### Enhancements
diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md
new file mode 100644
index 000000000..a3ab5968c
--- /dev/null
+++ b/docs/release-notes/version-2.10.md
@@ -0,0 +1,210 @@
+# NetBox v2.10
+
+## v2.10.3 (2021-01-05)
+
+### Bug Fixes
+
+* [#5049](https://github.com/netbox-community/netbox/issues/5049) - Add check for LLDP neighbor chassis name to lldp_neighbors
+* [#5301](https://github.com/netbox-community/netbox/issues/5301) - Fix misleading error when racking a device with invalid parameters
+* [#5311](https://github.com/netbox-community/netbox/issues/5311) - Update child objects when a rack group is moved to a new site
+* [#5518](https://github.com/netbox-community/netbox/issues/5518) - Fix persistent vertical scrollbar
+* [#5533](https://github.com/netbox-community/netbox/issues/5533) - Fix bulk editing of objects with required custom fields
+* [#5540](https://github.com/netbox-community/netbox/issues/5540) - Fix exception when viewing a provider with one or more tags assigned
+* [#5543](https://github.com/netbox-community/netbox/issues/5543) - Fix rendering of config contexts with cluster assignment for devices
+* [#5546](https://github.com/netbox-community/netbox/issues/5546) - Add custom field bulk edit support for cables, power panels, rack reservations, and virtual chassis
+* [#5547](https://github.com/netbox-community/netbox/issues/5547) - Add custom field bulk import support for cables, power panels, rack reservations, and virtual chassis
+* [#5551](https://github.com/netbox-community/netbox/issues/5551) - Restore missing import button on services list
+* [#5557](https://github.com/netbox-community/netbox/issues/5557) - Fix VRF route target assignment via REST API
+* [#5558](https://github.com/netbox-community/netbox/issues/5558) - Fix regex validation support for custom URL fields
+* [#5563](https://github.com/netbox-community/netbox/issues/5563) - Fix power feed cable trace link
+* [#5564](https://github.com/netbox-community/netbox/issues/5564) - Raise validation error if a power port template's `allocated_draw` exceeds its `maximum_draw`
+* [#5569](https://github.com/netbox-community/netbox/issues/5569) - Ensure consistent labeling of interface `mgmt_only` field
+* [#5573](https://github.com/netbox-community/netbox/issues/5573) - Report inconsistent values when migrating custom field data
+
+---
+
+## v2.10.2 (2020-12-21)
+
+### Enhancements
+
+* [#5489](https://github.com/netbox-community/netbox/issues/5489) - Add filters for type and width to racks list
+* [#5496](https://github.com/netbox-community/netbox/issues/5496) - Add form field to filter rack reservation by user
+
+### Bug Fixes
+
+* [#5254](https://github.com/netbox-community/netbox/issues/5254) - Require plugin authors to set zip_safe=False
+* [#5468](https://github.com/netbox-community/netbox/issues/5468) - Fix unlocking secrets from device/VM view
+* [#5473](https://github.com/netbox-community/netbox/issues/5473) - Fix alignment of rack names in elevations list
+* [#5478](https://github.com/netbox-community/netbox/issues/5478) - Fix display of route target description
+* [#5484](https://github.com/netbox-community/netbox/issues/5484) - Fix "tagged" indication in VLAN members list
+* [#5486](https://github.com/netbox-community/netbox/issues/5486) - Optimize retrieval of config context data for device/VM REST API views
+* [#5487](https://github.com/netbox-community/netbox/issues/5487) - Support filtering rack type/width with multiple values
+* [#5488](https://github.com/netbox-community/netbox/issues/5488) - Fix caching error when viewing cable trace after toggling cable status
+* [#5498](https://github.com/netbox-community/netbox/issues/5498) - Fix filtering rack reservations by username
+* [#5499](https://github.com/netbox-community/netbox/issues/5499) - Fix filtering of displayed device/VM interfaces by regex
+* [#5507](https://github.com/netbox-community/netbox/issues/5507) - Fix custom field data assignment via UI for IP addresses, secrets
+* [#5510](https://github.com/netbox-community/netbox/issues/5510) - Fix filtering by boolean custom fields
+
+---
+
+## v2.10.1 (2020-12-15)
+
+### Bug Fixes
+
+* [#5444](https://github.com/netbox-community/netbox/issues/5444) - Don't force overwriting of boolean fields when bulk editing interfaces
+* [#5450](https://github.com/netbox-community/netbox/issues/5450) - API serializer foreign count fields do not have a default value
+* [#5453](https://github.com/netbox-community/netbox/issues/5453) - Correct change log representation when creating a cable
+* [#5458](https://github.com/netbox-community/netbox/issues/5458) - Creating a component template throws an exception
+* [#5461](https://github.com/netbox-community/netbox/issues/5461) - Rack Elevations throw reverse match exception
+* [#5463](https://github.com/netbox-community/netbox/issues/5463) - Back-to-back Circuit Termination throws AttributeError exception
+* [#5465](https://github.com/netbox-community/netbox/issues/5465) - Correct return URL when disconnecting a cable from a device
+* [#5466](https://github.com/netbox-community/netbox/issues/5466) - Fix validation for required custom fields
+* [#5470](https://github.com/netbox-community/netbox/issues/5470) - Fix exception when making `OPTIONS` request for a REST API list endpoint
+
+---
+
+## v2.10.0 (2020-12-14)
+
+**NOTE:** This release completely removes support for embedded graphs.
+
+**NOTE:** The Django templating language (DTL) is no longer supported for export templates. Ensure that all export templates use Jinja2 before upgrading.
+
+### New Features
+
+#### Route Targets ([#259](https://github.com/netbox-community/netbox/issues/259))
+
+This release introduces support for modeling L3VPN route targets, which can be used to control the redistribution of advertised prefixes among VRFs. Each VRF may be assigned one or more route targets in the import and/or export direction. Like VRFs, route targets may be assigned to tenants and support tag assignment.
+
+#### REST API Bulk Deletion ([#3436](https://github.com/netbox-community/netbox/issues/3436))
+
+The REST API now supports the bulk deletion of objects of the same type in a single request. Send a `DELETE` HTTP request to the list to the model's list endpoint (e.g. `/api/dcim/sites/`) with a list of JSON objects specifying the numeric ID of each object to be deleted. For example, to delete sites with IDs 10, 11, and 12, issue the following request:
+
+```no-highlight
+curl -s -X DELETE \
+-H "Authorization: Token $TOKEN" \
+-H "Content-Type: application/json" \
+http://netbox/api/dcim/sites/ \
+--data '[{"id": 10}, {"id": 11}, {"id": 12}]'
+```
+
+#### REST API Bulk Update ([#4882](https://github.com/netbox-community/netbox/issues/4882))
+
+Similar to bulk deletion, the REST API also now supports bulk updates. Send a `PUT` or `PATCH` HTTP request to the list to the model's list endpoint (e.g. `/api/dcim/sites/`) with a list of JSON objects specifying the numeric ID of each object and the attribute(s) to be updated. For example, to set a description for sites with IDs 10 and 11, issue the following request:
+
+```no-highlight
+curl -s -X PATCH \
+-H "Authorization: Token $TOKEN" \
+-H "Content-Type: application/json" \
+http://netbox/api/dcim/sites/ \
+--data '[{"id": 10, "description": "Foo"}, {"id": 11, "description": "Bar"}]'
+```
+
+#### Reimplementation of Custom Fields ([#4878](https://github.com/netbox-community/netbox/issues/4878))
+
+NetBox v2.10 introduces a completely overhauled approach to custom fields. Whereas previous versions used CustomFieldValue instances to store values, custom field data is now stored directly on each model instance as JSON data and may be accessed using the `cf` property:
+
+```python
+>>> site = Site.objects.first()
+>>> site.cf
+{'site_code': 'US-RAL01'}
+>>> site.cf['foo'] = 'ABC'
+>>> site.full_clean()
+>>> site.save()
+>>> site = Site.objects.first()
+>>> site.cf
+{'foo': 'ABC', 'site_code': 'US-RAL01'}
+```
+
+Additionally, custom selection field choices are now defined on the CustomField model within the admin UI, which greatly simplifies working with choice values.
+
+#### Improved Cable Trace Performance ([#4900](https://github.com/netbox-community/netbox/issues/4900))
+
+All end-to-end cable paths are now cached using the new CablePath backend model. This allows NetBox to now immediately return the complete path originating from any endpoint directly from the database, rather than having to trace each cable recursively. It also resolves some systemic validation issues present in the original implementation.
+
+**Note:** As part of this change, cable traces will no longer traverse circuits: A circuit termination will be considered the origin or destination of an end-to-end path.
+
+### Enhancements
+
+* [#609](https://github.com/netbox-community/netbox/issues/609) - Add min/max value and regex validation for custom fields
+* [#1503](https://github.com/netbox-community/netbox/issues/1503) - Allow assigment of secrets to virtual machines
+* [#1692](https://github.com/netbox-community/netbox/issues/1692) - Allow assigment of inventory items to parent items in web UI
+* [#2179](https://github.com/netbox-community/netbox/issues/2179) - Support the use of multiple port numbers when defining a service
+* [#4897](https://github.com/netbox-community/netbox/issues/4897) - Allow filtering by content type identified as `.` string
+* [#4918](https://github.com/netbox-community/netbox/issues/4918) - Add a REST API endpoint (`/api/status/`) which returns NetBox's current operational status
+* [#4956](https://github.com/netbox-community/netbox/issues/4956) - Include inventory items on primary device view
+* [#4967](https://github.com/netbox-community/netbox/issues/4967) - Support tenant assignment for aggregates
+* [#5003](https://github.com/netbox-community/netbox/issues/5003) - CSV import now accepts slug values for choice fields
+* [#5146](https://github.com/netbox-community/netbox/issues/5146) - Add custom field support for cables, power panels, rack reservations, and virtual chassis
+* [#5154](https://github.com/netbox-community/netbox/issues/5154) - The web interface now consumes the entire browser window
+* [#5190](https://github.com/netbox-community/netbox/issues/5190) - Add a REST API endpoint for retrieving content types (`/api/extras/content-types/`)
+* [#5274](https://github.com/netbox-community/netbox/issues/5274) - Add REST API support for custom fields
+* [#5399](https://github.com/netbox-community/netbox/issues/5399) - Show options for cable endpoint types during bulk import
+* [#5411](https://github.com/netbox-community/netbox/issues/5411) - Include cable tags in trace view
+
+### Other Changes
+
+* [#1846](https://github.com/netbox-community/netbox/issues/1846) - Enable MPTT for InventoryItem hierarchy
+* [#2755](https://github.com/netbox-community/netbox/issues/2755) - Switched from Font Awesome/Glyphicons to Material Design icons
+* [#4349](https://github.com/netbox-community/netbox/issues/4349) - Dropped support for embedded graphs
+* [#4360](https://github.com/netbox-community/netbox/issues/4360) - Dropped support for the Django template language from export templates
+* [#4711](https://github.com/netbox-community/netbox/issues/4711) - Renamed Webhook `obj_type` to `content_types`
+* [#4941](https://github.com/netbox-community/netbox/issues/4941) - `commit` argument is now required argument in a custom script's `run()` method
+* [#5011](https://github.com/netbox-community/netbox/issues/5011) - Standardized name field lengths across all models
+* [#5139](https://github.com/netbox-community/netbox/issues/5139) - Omit utilization statistics from RIR list
+* [#5225](https://github.com/netbox-community/netbox/issues/5225) - Circuit termination port speed is now an optional field
+
+### REST API Changes
+
+* Added support for `PUT`, `PATCH`, and `DELETE` operations on list endpoints (bulk update and delete)
+* Added the `/extras/content-types/` endpoint for Django ContentTypes
+* Added the `/extras/custom-fields/` endpoint for custom fields
+* Removed the `/extras/_custom_field_choices/` endpoint (replaced by new custom fields endpoint)
+* Added the `/status/` endpoint to convey NetBox's current status
+* circuits.CircuitTermination:
+ * Added the `/trace/` endpoint
+ * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
+ * Added `cable_peer` and `cable_peer_type`
+ * `port_speed` may now be null
+* dcim.Cable: Added `custom_fields`
+* dcim.ConsolePort:
+ * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
+ * Added `cable_peer` and `cable_peer_type`
+ * Removed `connection_status` from nested serializer
+* dcim.ConsoleServerPort:
+ * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
+ * Added `cable_peer` and `cable_peer_type`
+ * Removed `connection_status` from nested serializer
+* dcim.FrontPort:
+ * Replaced the `/trace/` endpoint with `/paths/`, which returns a list of cable paths
+ * Added `cable_peer` and `cable_peer_type`
+* dcim.Interface:
+ * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
+ * Added `cable_peer` and `cable_peer_type`
+ * Removed `connection_status` from nested serializer
+* dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning
+* dcim.PowerFeed:
+ * Added the `/trace/` endpoint
+ * Added fields `connected_endpoint`, `connected_endpoint_type`, `connected_endpoint_reachable`, `cable_peer`, and `cable_peer_type`
+* dcim.PowerOutlet:
+ * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
+ * Added `cable_peer` and `cable_peer_type`
+ * Removed `connection_status` from nested serializer
+* dcim.PowerPanel: Added `custom_fields`
+* dcim.PowerPort
+ * Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
+ * Added `cable_peer` and `cable_peer_type`
+ * Removed `connection_status` from nested serializer
+* dcim.RackReservation: Added `custom_fields`
+* dcim.RearPort:
+ * Replaced the `/trace/` endpoint with `/paths/`, which returns a list of cable paths
+ * Added `cable_peer` and `cable_peer_type`
+* dcim.VirtualChassis: Added `custom_fields`
+* extras.ExportTemplate: The `template_language` field has been removed
+* extras.Graph: This API endpoint has been removed (see #4349)
+* extras.ImageAttachment: Filtering by `content_type` now takes a string in the form `.`
+* extras.ObjectChange: Filtering by `changed_object_type` now takes a string in the form `.`
+* ipam.Aggregate: Added `tenant` field
+* ipam.RouteTarget: New endpoint
+* ipam.Service: Renamed `port` to `ports`; now holds a list of one or more port numbers
+* ipam.VRF: Added `import_targets` and `export_targets` fields
+* secrets.Secret: Removed `device` field; replaced with `assigned_object` generic foreign key. This may represent either a device or a virtual machine. Assign an object by setting `assigned_object_type` and `assigned_object_id`.
diff --git a/docs/release-notes/version-2.2.md b/docs/release-notes/version-2.2.md
index 905b7a8d1..e13c4fe69 100644
--- a/docs/release-notes/version-2.2.md
+++ b/docs/release-notes/version-2.2.md
@@ -196,7 +196,7 @@ Our second-most popular feature request has arrived! NetBox now supports the cre
#### Custom Validation Reports ([#1511](https://github.com/netbox-community/netbox/issues/1511))
-Users can now create custom reports which are run to validate data in NetBox. Reports work very similar to Python unit tests: Each report inherits from NetBox's Report class and contains one or more test method. Reports can be run and retrieved via the web UI, API, or CLI. See [the docs](http://netbox.readthedocs.io/en/stable/miscellaneous/reports/) for more info.
+Users can now create custom reports which are run to validate data in NetBox. Reports work very similar to Python unit tests: Each report inherits from NetBox's Report class and contains one or more test method. Reports can be run and retrieved via the web UI, API, or CLI. See [the docs](https://netbox.readthedocs.io/en/stable/miscellaneous/reports/) for more info.
### Enhancements
diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md
index 5ca86217a..af758f928 100644
--- a/docs/release-notes/version-2.8.md
+++ b/docs/release-notes/version-2.8.md
@@ -1,5 +1,95 @@
# NetBox v2.8
+## v2.8.9 (2020-08-04)
+
+### Enhancements
+
+* [#4898](https://github.com/netbox-community/netbox/issues/4898) - Add MAC address search field to interfaces list
+* [#4899](https://github.com/netbox-community/netbox/issues/4899) - Add MAC address column to interfaces table
+
+### Bug Fixes
+
+* [#4455](https://github.com/netbox-community/netbox/issues/4455) - Fix ordering of prefixes beneath aggregate when available space is hidden
+* [#4875](https://github.com/netbox-community/netbox/issues/4875) - Fix documentation for image attachments
+* [#4876](https://github.com/netbox-community/netbox/issues/4876) - Fix labels for sites in staging or decommissioning status
+* [#4880](https://github.com/netbox-community/netbox/issues/4880) - Fix removal of tagged VLANs if not assigned in bulk interface editing
+* [#4887](https://github.com/netbox-community/netbox/issues/4887) - Don't disable NAPALM tabs when device has no primary IP
+* [#4894](https://github.com/netbox-community/netbox/issues/4894) - Fix display of device/VM counts on platforms list
+* [#4895](https://github.com/netbox-community/netbox/issues/4895) - Force UTF-8 encoding when embedding model documentation
+* [#4910](https://github.com/netbox-community/netbox/issues/4910) - Unpin redis dependency to fix exception in RQ worker
+* [#4926](https://github.com/netbox-community/netbox/issues/4926) - Fix ordering of VM interfaces in REST API endpoint
+* [#4927](https://github.com/netbox-community/netbox/issues/4927) - Fix validation error when updating an existing secret
+* [#4929](https://github.com/netbox-community/netbox/issues/4929) - Correct log message when creating a new object
+
+---
+
+## v2.8.8 (2020-07-21)
+
+### Enhancements
+
+* [#4805](https://github.com/netbox-community/netbox/issues/4805) - Improve handling of plugin loading errors
+* [#4829](https://github.com/netbox-community/netbox/issues/4829) - Add NEMA 15 power port and outlet types
+* [#4831](https://github.com/netbox-community/netbox/issues/4831) - Allow NAPALM to resolve device name when primary IP is not set
+* [#4854](https://github.com/netbox-community/netbox/issues/4854) - Add staging and decommissioning statuses for sites
+
+### Bug Fixes
+
+* [#3240](https://github.com/netbox-community/netbox/issues/3240) - Correct OpenAPI definition for available-prefixes endpoint
+* [#4595](https://github.com/netbox-community/netbox/issues/4595) - Ensure consistent display of non-racked and child devices on rack view
+* [#4803](https://github.com/netbox-community/netbox/issues/4803) - Return IP family (4 or 6) as integer rather than string
+* [#4821](https://github.com/netbox-community/netbox/issues/4821) - Restrict group options by selected site when bulk editing VLANs
+* [#4835](https://github.com/netbox-community/netbox/issues/4835) - Support passing multiple initial values for multiple choice fields
+* [#4838](https://github.com/netbox-community/netbox/issues/4838) - Fix rack power utilization display for racks without devices
+* [#4851](https://github.com/netbox-community/netbox/issues/4851) - Show locally connected peer on circuit terminations
+* [#4856](https://github.com/netbox-community/netbox/issues/4856) - Redirect user back to circuit after connecting a termination
+* [#4872](https://github.com/netbox-community/netbox/issues/4872) - Enable filtering virtual machine interfaces by tag
+
+---
+
+## v2.8.7 (2020-07-02)
+
+### Enhancements
+
+* [#4796](https://github.com/netbox-community/netbox/issues/4796) - Introduce configuration parameters for default rack elevation size
+* [#4802](https://github.com/netbox-community/netbox/issues/4802) - Allow changing page size when displaying only a single page of results
+
+### Bug Fixes
+
+* [#4695](https://github.com/netbox-community/netbox/issues/4695) - Expose cable termination type choices in OpenAPI spec
+* [#4708](https://github.com/netbox-community/netbox/issues/4708) - Relax connection constraints for multi-position rear ports
+* [#4766](https://github.com/netbox-community/netbox/issues/4766) - Fix redirect after login when `next` is not specified
+* [#4771](https://github.com/netbox-community/netbox/issues/4771) - Fix add/remove tag population when bulk editing objects
+* [#4772](https://github.com/netbox-community/netbox/issues/4772) - Fix "brief" format for the secrets REST API endpoint
+* [#4774](https://github.com/netbox-community/netbox/issues/4774) - Fix exception when deleting a device with device bays
+* [#4775](https://github.com/netbox-community/netbox/issues/4775) - Allow selecting an alternate device type when creating component templates
+
+---
+
+## v2.8.6 (2020-06-15)
+
+### Enhancements
+
+* [#4698](https://github.com/netbox-community/netbox/issues/4698) - Improve display of template code for object in admin UI
+* [#4717](https://github.com/netbox-community/netbox/issues/4717) - Introduce `ALLOWED_URL_SCHEMES` configuration parameter to mitigate dangerous hyperlinks
+* [#4744](https://github.com/netbox-community/netbox/issues/4744) - Hide "IP addresses" tab when viewing a container prefix
+* [#4755](https://github.com/netbox-community/netbox/issues/4755) - Enable creation of rack reservations directly from navigation menu
+* [#4761](https://github.com/netbox-community/netbox/issues/4761) - Enable tag assignment during bulk creation of IP addresses
+
+### Bug Fixes
+
+* [#4674](https://github.com/netbox-community/netbox/issues/4674) - Fix API definition for available prefix and IP address endpoints
+* [#4702](https://github.com/netbox-community/netbox/issues/4702) - Catch IntegrityError exception when adding a non-unique secret
+* [#4707](https://github.com/netbox-community/netbox/issues/4707) - Fix `prefix_count` population on VLAN API serializer
+* [#4710](https://github.com/netbox-community/netbox/issues/4710) - Fix merging of form fields among custom scripts
+* [#4725](https://github.com/netbox-community/netbox/issues/4725) - Fix "brief" rendering of various REST API endpoints
+* [#4736](https://github.com/netbox-community/netbox/issues/4736) - Add cable trace endpoints for pass-through ports
+* [#4737](https://github.com/netbox-community/netbox/issues/4737) - Fix display of role labels in virtual machines table
+* [#4743](https://github.com/netbox-community/netbox/issues/4743) - Allow users to create "next available" IPs without needing permission to create prefixes
+* [#4756](https://github.com/netbox-community/netbox/issues/4756) - Filter parent group by site when creating rack groups
+* [#4760](https://github.com/netbox-community/netbox/issues/4760) - Enable power port template assignment when bulk editing power outlet templates
+
+---
+
## v2.8.5 (2020-05-26)
**Note:** The minimum required version of PostgreSQL is now 9.6.
diff --git a/docs/release-notes/version-2.9.md b/docs/release-notes/version-2.9.md
new file mode 100644
index 000000000..4b359975c
--- /dev/null
+++ b/docs/release-notes/version-2.9.md
@@ -0,0 +1,335 @@
+# NetBox v2.9
+
+## v2.9.11 (2020-12-11)
+
+### Enhancements
+
+* [#5424](https://github.com/netbox-community/netbox/issues/5424) - Allow passing Python code to `nbshell` using `--command`
+* [#5439](https://github.com/netbox-community/netbox/issues/5439) - Add CS and SN fiber port types
+
+### Bug Fixes
+
+* [#5383](https://github.com/netbox-community/netbox/issues/5383) - Fix setting user password via REST API
+* [#5396](https://github.com/netbox-community/netbox/issues/5396) - Fix uniqueness constraint for virtual machine names
+* [#5387](https://github.com/netbox-community/netbox/issues/5387) - Fix error when rendering config contexts when objects have multiple tags assigned
+* [#5407](https://github.com/netbox-community/netbox/issues/5407) - Add direct link to secret on secrets list
+* [#5408](https://github.com/netbox-community/netbox/issues/5408) - Fix updating secrets without setting new plaintext
+* [#5410](https://github.com/netbox-community/netbox/issues/5410) - Restore tags field on cable connection forms
+* [#5433](https://github.com/netbox-community/netbox/issues/5433) - Exclude SVG files from front/rear image upload for device types (currently unsupported)
+* [#5436](https://github.com/netbox-community/netbox/issues/5436) - Show assigned IP addresses in interfaces list
+* [#5446](https://github.com/netbox-community/netbox/issues/5446) - Fix validation for plugin version and required settings
+
+---
+
+## v2.9.10 (2020-11-24)
+
+### Enhancements
+
+* [#5319](https://github.com/netbox-community/netbox/issues/5319) - Add USB types for power ports and outlets
+* [#5337](https://github.com/netbox-community/netbox/issues/5337) - Add "splice" type for pass-through ports
+
+### Bug Fixes
+
+* [#5235](https://github.com/netbox-community/netbox/issues/5235) - Fix exception when editing IP address with a NAT IP assigned to a non-racked device
+* [#5309](https://github.com/netbox-community/netbox/issues/5309) - Avoid extraneous database queries when manipulating objects
+* [#5345](https://github.com/netbox-community/netbox/issues/5345) - Fix non-deterministic ordering of prefixes and IP addresses
+* [#5350](https://github.com/netbox-community/netbox/issues/5350) - Filter available racks by selected group when creating a rack reservation
+* [#5355](https://github.com/netbox-community/netbox/issues/5355) - Limit rack groups by selected site when editing a rack
+* [#5356](https://github.com/netbox-community/netbox/issues/5356) - Populate manufacturer field when adding a device component template
+* [#5360](https://github.com/netbox-community/netbox/issues/5360) - Clear VLAN assignments when setting interface mode to none
+
+---
+
+## v2.9.9 (2020-11-09)
+
+### Enhancements
+
+* [#5304](https://github.com/netbox-community/netbox/issues/5304) - Return server error messages as JSON when handling REST API requests
+* [#5310](https://github.com/netbox-community/netbox/issues/5310) - Link to rack groups within rack list table
+* [#5327](https://github.com/netbox-community/netbox/issues/5327) - Be more strict when capturing anticipated ImportError exceptions
+
+### Bug Fixes
+
+* [#5271](https://github.com/netbox-community/netbox/issues/5271) - Fix auto-population of region field when editing a device
+* [#5314](https://github.com/netbox-community/netbox/issues/5314) - Fix config context rendering when multiple tags are assigned to an object
+* [#5316](https://github.com/netbox-community/netbox/issues/5316) - Dry running scripts should not trigger webhooks
+* [#5324](https://github.com/netbox-community/netbox/issues/5324) - Add missing template extension tags for plugins for VM interface view
+* [#5328](https://github.com/netbox-community/netbox/issues/5328) - Fix CreatedUpdatedFilterTest when running in non-UTC timezone
+* [#5331](https://github.com/netbox-community/netbox/issues/5331) - Fix filtering of sites by null region
+
+---
+
+## v2.9.8 (2020-10-30)
+
+### Enhancements
+
+* [#4559](https://github.com/netbox-community/netbox/issues/4559) - Improve device/VM context data rendering performance
+
+### Bug Fixes
+
+* [#3672](https://github.com/netbox-community/netbox/issues/3672) - Fix a caching issue causing incorrect related object counts in API responses
+* [#5113](https://github.com/netbox-community/netbox/issues/5113) - Fix incorrect caching of permission object assignments to user groups in the admin panel
+* [#5243](https://github.com/netbox-community/netbox/issues/5243) - Redirect user to appropriate tab after modifying device components
+* [#5273](https://github.com/netbox-community/netbox/issues/5273) - Fix exception when validating a new permission with no models selected
+* [#5282](https://github.com/netbox-community/netbox/issues/5282) - Fix high CPU load when LDAP authentication is enabled
+* [#5285](https://github.com/netbox-community/netbox/issues/5285) - Plugins no longer need to define `app_name` for API URLs to be included in the root view
+
+---
+
+## v2.9.7 (2020-10-12)
+
+### Bug Fixes
+
+* [#5231](https://github.com/netbox-community/netbox/issues/5231) - Fix KeyError exception when viewing object with custom link and debugging is disabled
+
+---
+
+## v2.9.6 (2020-10-09)
+
+### Bug Fixes
+
+* [#5229](https://github.com/netbox-community/netbox/issues/5229) - Fix AttributeError exception when LDAP authentication is enabled
+
+---
+
+## v2.9.5 (2020-10-09)
+
+### Enhancements
+
+* [#5202](https://github.com/netbox-community/netbox/issues/5202) - Extend the available context data when rendering custom links
+
+### Bug Fixes
+
+* [#4523](https://github.com/netbox-community/netbox/issues/4523) - Populate site vlan list when bulk editing interfaces under certain circumstances
+* [#5174](https://github.com/netbox-community/netbox/issues/5174) - Ensure consistent alignment of rack elevations
+* [#5175](https://github.com/netbox-community/netbox/issues/5175) - Fix toggling of rack elevation order
+* [#5184](https://github.com/netbox-community/netbox/issues/5184) - Fix missing Power Utilization
+* [#5197](https://github.com/netbox-community/netbox/issues/5197) - Limit duplicate IPs shown on IP address view
+* [#5199](https://github.com/netbox-community/netbox/issues/5199) - Change default LDAP logging to INFO
+* [#5201](https://github.com/netbox-community/netbox/issues/5201) - Fix missing querystring when bulk editing/deleting VLAN Group VLANs when selecting "select all x items matching query"
+* [#5206](https://github.com/netbox-community/netbox/issues/5206) - Apply user pagination preferences to all paginated object lists
+* [#5211](https://github.com/netbox-community/netbox/issues/5211) - Add missing `has_primary_ip` filter for virtual machines
+* [#5217](https://github.com/netbox-community/netbox/issues/5217) - Prevent erroneous removal of prefetched GenericForeignKey data from tables
+* [#5218](https://github.com/netbox-community/netbox/issues/5218) - Raise validation error if a power port's `allocated_draw` exceeds its `maximum_draw`
+* [#5220](https://github.com/netbox-community/netbox/issues/5220) - Fix API patch request against IP Address endpoint with null assigned_object_type
+* [#5221](https://github.com/netbox-community/netbox/issues/5221) - Fix bulk component creation for virtual machines
+* [#5224](https://github.com/netbox-community/netbox/issues/5224) - Don't allow a rear port to have fewer positions than the number of mapped front ports
+* [#5226](https://github.com/netbox-community/netbox/issues/5226) - Custom choice fields should be blank initially if no default choice has been designated
+
+---
+
+## v2.9.4 (2020-09-23)
+
+**NOTE:** This release removes support for the `DEFAULT_TIMEOUT` parameter under `REDIS` database configuration. Set `RQ_DEFAULT_TIMEOUT` as a global configuration parameter instead.
+
+**NOTE:** Any permissions referencing the legacy ReportResult model (e.g. `extras.view_reportresult`) should be updated to reference the Report model.
+
+### Enhancements
+
+* [#1755](https://github.com/netbox-community/netbox/issues/1755) - Toggle order in which rack elevations are displayed
+* [#5128](https://github.com/netbox-community/netbox/issues/5128) - Increase maximum rear port positions from 64 to 1024
+* [#5134](https://github.com/netbox-community/netbox/issues/5134) - Display full hierarchy in breadcrumbs for sites/racks
+* [#5149](https://github.com/netbox-community/netbox/issues/5149) - Add rack group field to device edit form
+* [#5164](https://github.com/netbox-community/netbox/issues/5164) - Show total rack count per rack group under site view
+* [#5171](https://github.com/netbox-community/netbox/issues/5171) - Introduce the `RQ_DEFAULT_TIMEOUT` configuration parameter
+
+### Bug Fixes
+
+* [#5050](https://github.com/netbox-community/netbox/issues/5050) - Fix potential failure on `0016_replicate_interfaces` schema migration from old release
+* [#5066](https://github.com/netbox-community/netbox/issues/5066) - Update `view_reportresult` to `view_report` permission
+* [#5075](https://github.com/netbox-community/netbox/issues/5075) - Include a VLAN membership view for VM interfaces
+* [#5105](https://github.com/netbox-community/netbox/issues/5105) - Validation should fail when reassigning a primary IP from device to VM
+* [#5109](https://github.com/netbox-community/netbox/issues/5109) - Fix representation of custom choice field values for webhook data
+* [#5108](https://github.com/netbox-community/netbox/issues/5108) - Fix execution of reports via CLI
+* [#5111](https://github.com/netbox-community/netbox/issues/5111) - Allow use of tuples when specifying ObjectVar `query_params`
+* [#5118](https://github.com/netbox-community/netbox/issues/5118) - Specifying an empty list of tags should clear assigned tags (REST API)
+* [#5133](https://github.com/netbox-community/netbox/issues/5133) - Fix disassociation of an IP address from a VM interface
+* [#5136](https://github.com/netbox-community/netbox/issues/5136) - Fix exception when bulk editing interface 802.1Q mode
+* [#5156](https://github.com/netbox-community/netbox/issues/5156) - Add missing "add" button to rack reservations list
+* [#5167](https://github.com/netbox-community/netbox/issues/5167) - Support filtering ObjectChanges by multiple users
+
+---
+
+## v2.9.3 (2020-09-04)
+
+### Enhancements
+
+* [#4977](https://github.com/netbox-community/netbox/issues/4977) - Redirect authenticated users from login view
+* [#5048](https://github.com/netbox-community/netbox/issues/5048) - Show the device/VM name when editing a component
+* [#5072](https://github.com/netbox-community/netbox/issues/5072) - Add REST API filters for image attachments
+* [#5080](https://github.com/netbox-community/netbox/issues/5080) - Add 8P6C, 8P4C, 8P2C port types
+
+### Bug Fixes
+
+* [#5046](https://github.com/netbox-community/netbox/issues/5046) - Disabled plugin menu items are no longer clickable
+* [#5063](https://github.com/netbox-community/netbox/issues/5063) - Fix "add device" link in rack elevations for opposite side of half-depth devices
+* [#5074](https://github.com/netbox-community/netbox/issues/5074) - Fix inclusion of VC member interfaces when viewing VC master
+* [#5078](https://github.com/netbox-community/netbox/issues/5078) - Fix assignment of existing IP addresses to interfaces via web UI
+* [#5081](https://github.com/netbox-community/netbox/issues/5081) - Fix exception during webhook processing with custom select field
+* [#5085](https://github.com/netbox-community/netbox/issues/5085) - Fix ordering by assignment in IP addresses table
+* [#5087](https://github.com/netbox-community/netbox/issues/5087) - Restore label field when editing console server ports, power ports, and power outlets
+* [#5089](https://github.com/netbox-community/netbox/issues/5089) - Redirect to device view after editing component
+* [#5090](https://github.com/netbox-community/netbox/issues/5090) - Fix status display for console/power/interface connections
+* [#5091](https://github.com/netbox-community/netbox/issues/5091) - Avoid KeyError when handling invalid table preferences
+* [#5095](https://github.com/netbox-community/netbox/issues/5095) - Show assigned prefixes in VLANs list
+
+---
+
+## v2.9.2 (2020-08-27)
+
+### Enhancements
+
+* [#5055](https://github.com/netbox-community/netbox/issues/5055) - Add tags column to device/VM component list tables
+* [#5056](https://github.com/netbox-community/netbox/issues/5056) - Add interface and parent columns to IP address list
+
+### Bug Fixes
+
+* [#4988](https://github.com/netbox-community/netbox/issues/4988) - Fix ordering of rack reservations with identical creation times
+* [#5002](https://github.com/netbox-community/netbox/issues/5002) - Correct OpenAPI definition for `available-prefixes` endpoint
+* [#5035](https://github.com/netbox-community/netbox/issues/5035) - Fix exception when modifying an IP address assigned to a VM
+* [#5038](https://github.com/netbox-community/netbox/issues/5038) - Fix validation of primary IPs assigned to virtual machines
+* [#5040](https://github.com/netbox-community/netbox/issues/5040) - Limit SLAAC status to IPv6 addresses
+* [#5041](https://github.com/netbox-community/netbox/issues/5041) - Fix form tabs when assigning an IP to a VM interface
+* [#5042](https://github.com/netbox-community/netbox/issues/5042) - Fix display of SLAAC label for IP addresses status
+* [#5045](https://github.com/netbox-community/netbox/issues/5045) - Allow assignment of interfaces to non-master VC peer LAG during import
+* [#5058](https://github.com/netbox-community/netbox/issues/5058) - Correct URL for front rack elevation images when using external storage
+* [#5059](https://github.com/netbox-community/netbox/issues/5059) - Fix inclusion of checkboxes for interfaces in virtual machine view
+* [#5060](https://github.com/netbox-community/netbox/issues/5060) - Fix validation when bulk-importing child devices
+* [#5061](https://github.com/netbox-community/netbox/issues/5061) - Allow adding/removing tags when bulk editing virtual machine interfaces
+
+---
+
+## v2.9.1 (2020-08-22)
+
+### Enhancements
+
+* [#4540](https://github.com/netbox-community/netbox/issues/4540) - Add IP address status type for SLAAC
+* [#4814](https://github.com/netbox-community/netbox/issues/4814) - Allow nested LAG interfaces
+* [#4991](https://github.com/netbox-community/netbox/issues/4991) - Add Python and NetBox versions to error page
+* [#5033](https://github.com/netbox-community/netbox/issues/5033) - Support backward compatibility for `REMOTE_AUTH_BACKEND` configuration parameter
+
+---
+
+## v2.9.0 (2020-08-21)
+
+**Note:** Redis 4.0 or later is required for this release.
+
+### New Features
+
+#### Object-Based Permissions ([#554](https://github.com/netbox-community/netbox/issues/554))
+
+NetBox v2.9 replaces Django's built-in permissions framework with one that supports object-based assignment of permissions using arbitrary constraints. When granting a user or group permission to perform a certain action on one or more types of objects, an administrator can optionally specify a set of constraints. The permission will apply only to objects which match the specified constraints. For example, assigning permission to modify devices with the constraint `{"tenant__group__name": "Customers"}` would allow the associated users/groups to perform an action only on devices assigned to a tenant belonging to the "Customers" group.
+
+#### Background Execution of Scripts & Reports ([#2006](https://github.com/netbox-community/netbox/issues/2006))
+
+When running a report or custom script, its execution is now queued for background processing and the user receives an immediate response indicating its status. This prevents long-running scripts from resulting in a timeout error. Once the execution has completed, the page will automatically refresh to display its results. Both scripts and reports now store their output in the new JobResult model. (The ReportResult model has been removed.)
+
+#### Named Virtual Chassis ([#2018](https://github.com/netbox-community/netbox/issues/2018))
+
+The VirtualChassis model now has a mandatory `name` field. Names are assigned to the virtual chassis itself rather than referencing the master VC member. Additionally, the designation of a master is now optional: a virtual chassis may have only non-master members.
+
+#### Changes to Tag Creation ([#3703](https://github.com/netbox-community/netbox/issues/3703))
+
+Tags are no longer created automatically: A tag must be created by a user before it can be applied to any object. Additionally, the REST API representation of assigned tags has been expanded to be consistent with other objects.
+
+#### Dedicated Model for VM Interfaces ([#4721](https://github.com/netbox-community/netbox/issues/4721))
+
+A new model has been introduced to represent virtual machine interfaces. Although this change is largely transparent to the end user, note that the IP address model no longer has a foreign key to the Interface model under the DCIM app. This has been replaced with a generic foreign key named `assigned_object`.
+
+#### REST API Endpoints for Users and Groups ([#4877](https://github.com/netbox-community/netbox/issues/4877))
+
+Two new REST API endpoints have been added to facilitate the retrieval and manipulation of users and groups:
+
+* `/api/users/groups/`
+* `/api/users/users/`
+
+### Enhancements
+
+* [#4615](https://github.com/netbox-community/netbox/issues/4615) - Add `label` field for all device components and component templates
+* [#4639](https://github.com/netbox-community/netbox/issues/4639) - Improve performance of web UI prefixes list
+* [#4742](https://github.com/netbox-community/netbox/issues/4742) - Add tagging for cables, power panels, and rack reservations
+* [#4788](https://github.com/netbox-community/netbox/issues/4788) - Add dedicated views for all device components
+* [#4792](https://github.com/netbox-community/netbox/issues/4792) - Add bulk rename capability for console and power ports
+* [#4793](https://github.com/netbox-community/netbox/issues/4793) - Add `description` field to device component templates
+* [#4795](https://github.com/netbox-community/netbox/issues/4795) - Add bulk disconnect capability for console and power ports
+* [#4806](https://github.com/netbox-community/netbox/issues/4806) - Add a `url` field to all API serializers
+* [#4807](https://github.com/netbox-community/netbox/issues/4807) - Add bulk edit ability for device bay templates
+* [#4817](https://github.com/netbox-community/netbox/issues/4817) - Standardize device/VM component `name` field to 64 characters
+* [#4837](https://github.com/netbox-community/netbox/issues/4837) - Use dynamic form widget for relationships to MPTT objects (e.g. regions)
+* [#4840](https://github.com/netbox-community/netbox/issues/4840) - Enable change logging for config contexts
+* [#4885](https://github.com/netbox-community/netbox/issues/4885) - Add MultiChoiceVar for custom scripts
+* [#4940](https://github.com/netbox-community/netbox/issues/4940) - Add an `occupied` field to rack unit representations for rack elevation views
+* [#4945](https://github.com/netbox-community/netbox/issues/4945) - Add a user-friendly 403 error page
+* [#4969](https://github.com/netbox-community/netbox/issues/4969) - Replace secret role user/group assignment with object permissions
+* [#4982](https://github.com/netbox-community/netbox/issues/4982) - Extended ObjectVar to allow filtering API query
+* [#4994](https://github.com/netbox-community/netbox/issues/4994) - Add `cable` attribute to PowerFeed API serializer
+* [#4997](https://github.com/netbox-community/netbox/issues/4997) - The browsable API now lists available endpoints alphabetically
+* [#5024](https://github.com/netbox-community/netbox/issues/5024) - List available options for choice fields within CSV import forms
+
+### Configuration Changes
+
+* If using NetBox's built-in remote authentication backend, update `REMOTE_AUTH_BACKEND` to `'netbox.authentication.RemoteUserBackend'`, as the authentication class has moved.
+* If using LDAP authentication, set `REMOTE_AUTH_BACKEND` to `'netbox.authentication.LDAPBackend'`. (LDAP configuration parameters in `ldap_config.py` remain unchanged.)
+* `REMOTE_AUTH_DEFAULT_PERMISSIONS` now takes a dictionary rather than a list. This is a mapping of permission names to a dictionary of constraining attributes, or `None`. For example, `['dcim.add_site', 'dcim.change_site']` would become `{'dcim.add_site': None, 'dcim.change_site': None}`.
+* Backward compatibility for the old `webhooks` Redis queue name has been dropped. Ensure that your `REDIS` configuration parameter specifies both the `tasks` and `caching` databases.
+
+### REST API Changes
+
+* Added new endpoints for users, groups, and permissions under `/api/users/`.
+* A `url` field is now included on all object representations, identifying the unique REST API URL for each object.
+* The `tags` field of an object now includes a more complete representation of each tag, rather than just its name.
+* The assignment of tags to an object is now achieved in the same manner as specifying any other related device. The `tags` field accepts a list of JSON objects each matching a desired tag. (Alternatively, a list of numeric primary keys corresponding to tags may be passed instead.) For example:
+
+```json
+"tags": [
+ {"name": "First Tag"},
+ {"name": "Second Tag"}
+]
+```
+
+* Legacy numeric values for choice fields are no longer conveyed or accepted.
+* circuits.CircuitTermination: Added `cable` field
+* dcim.Cable: Added `tags` field
+* dcim.ConsolePort: Added `label` field
+* dcim.ConsolePortTemplate: Added `description` and `label` fields
+* dcim.ConsoleServerPort: Added `label` field
+* dcim.ConsoleServerPortTemplate: Added `description` and `label` fields
+* dcim.DeviceBay: Added `label` field
+* dcim.DeviceBayTemplate: Added `description` and `label` fields
+* dcim.FrontPort: Added `label` field
+* dcim.FrontPortTemplate: Added `description` and `label` fields
+* dcim.Interface: Added `label` field
+* dcim.InterfaceTemplate: Added `description` and `label` fields
+* dcim.PowerFeed: Added `cable` field
+* dcim.PowerPanel: Added `tags` field
+* dcim.PowerPort: Added ``label` field
+* dcim.PowerPortTemplate: Added `description` and `label` fields
+* dcim.PowerOutlet: Added `label` field
+* dcim.PowerOutletTemplate: Added `description` and `label` fields
+* dcim.Rack: Added an `occupied` field to rack unit representations for rack elevation views
+* dcim.RackGroup: Added a `_depth` attribute indicating an object's position in the tree.
+* dcim.RackReservation: Added `tags` field
+* dcim.RearPort: Added `label` field
+* dcim.RearPortTemplate: Added `description` and `label` fields
+* dcim.Region: Added a `_depth` attribute indicating an object's position in the tree.
+* dcim.VirtualChassis: Added `name` field (required)
+* extras.ConfigContext: Added `created` and `last_updated` fields
+* extras.JobResult: Added the `/api/extras/job-results/` endpoint
+* extras.Report: The `failed` field has been removed. The `completed` (boolean) and `status` (string) fields have been introduced to convey the status of a report's most recent execution. Additionally, the `result` field now conveys the nested representation of a JobResult.
+* extras.Script: Added `module` and `result` fields. The `result` field now conveys the nested representation of a JobResult.
+* extras.Tag: The count of `tagged_items` is no longer included when viewing the tags list when `brief` is passed.
+* ipam.IPAddress: Removed `interface` field; replaced with `assigned_object` generic foreign key. This may represent either a device interface or a virtual machine interface. Assign an object by setting `assigned_object_type` and `assigned_object_id`.
+* ipam.VRF: Added `display_name`
+* tenancy.TenantGroup: Added a `_depth` attribute indicating an object's position in the tree.
+* users.ObjectPermissions: Added the `/api/users/permissions/` endpoint
+* virtualization.VMInterface: Removed `type` field (VM interfaces have no type)
+
+### Other Changes
+
+* A new model, `VMInterface` has been introduced to represent interfaces assigned to VirtualMachine instances. Previously, these interfaces utilized the DCIM model `Interface`. Instances will be replicated automatically upon upgrade, however any custom code which references or manipulates virtual machine interfaces will need to be updated accordingly.
+* The `secrets.activate_userkey` permission no longer exists. Instead, `secrets.change_userkey` is checked to determine whether a user has the ability to activate a UserKey.
+* The `users.delete_token` permission is no longer enforced. All users are permitted to delete their own API tokens.
+* Dropped backward compatibility for the `webhooks` Redis queue configuration (use `tasks` instead).
+* Dropped backward compatibility for the `/admin/webhook-backend-status` URL (moved to `/admin/background-tasks/`).
+* Virtual chassis are now created by navigating to `/dcim/virtual-chassis/add/` rather than via the devices list.
+* A name is required when creating a virtual chassis.
diff --git a/docs/rest-api/authentication.md b/docs/rest-api/authentication.md
new file mode 100644
index 000000000..5d5777483
--- /dev/null
+++ b/docs/rest-api/authentication.md
@@ -0,0 +1,30 @@
+# REST API Authentication
+
+The NetBox REST API primarily employs token-based authentication. For convenience, cookie-based authentication can also be used when navigating the browsable API.
+
+{!docs/models/users/token.md!}
+
+## Authenticating to the API
+
+An authentication token is attached to a request by setting the `Authorization` header to the string `Token` followed by a space and the user's token:
+
+```
+$ curl -H "Authorization: Token $TOKEN" \
+-H "Accept: application/json; indent=4" \
+http://netbox/api/dcim/sites/
+{
+ "count": 10,
+ "next": null,
+ "previous": null,
+ "results": [...]
+}
+```
+
+A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../../configuration/optional-settings/#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response:
+
+```
+$ curl http://netbox/api/dcim/sites/
+{
+ "detail": "Authentication credentials were not provided."
+}
+```
diff --git a/docs/rest-api/filtering.md b/docs/rest-api/filtering.md
new file mode 100644
index 000000000..b77513297
--- /dev/null
+++ b/docs/rest-api/filtering.md
@@ -0,0 +1,87 @@
+# REST API Filtering
+
+## Filtering Objects
+
+The objects returned by an API list endpoint can be filtered by attaching one or more query parameters to the request URL. For example, `GET /api/dcim/sites/?status=active` will return only sites with a status of "active."
+
+Multiple parameters can be joined to further narrow results. For example, `GET /api/dcim/sites/?status=active®ion=europe` will return only active sites within the Europe region.
+
+Generally, passing multiple values for a single parameter will result in a logical OR operation. For example, `GET /api/dcim/sites/?region=north-america®ion=south-america` will return sites in North America _or_ South America. However, a logical AND operation will be used in instances where a field may have multiple values, such as tags. For example, `GET /api/dcim/sites/?tag=foo&tag=bar` will return only sites which have both the "foo" _and_ "bar" tags applied.
+
+### Filtering by Choice Field
+
+Some models have fields which are limited to specific choices, such as the `status` field on the Prefix model. To find all available choices for this field, make an authenticated `OPTIONS` request to the model's list endpoint, and use `jq` to extract the relevant parameters:
+
+```no-highlight
+$ curl -s -X OPTIONS \
+-H "Authorization: Token $TOKEN" \
+-H "Content-Type: application/json" \
+http://netbox/api/ipam/prefixes/ | jq ".actions.POST.status.choices"
+[
+ {
+ "value": "container",
+ "display_name": "Container"
+ },
+ {
+ "value": "active",
+ "display_name": "Active"
+ },
+ {
+ "value": "reserved",
+ "display_name": "Reserved"
+ },
+ {
+ "value": "deprecated",
+ "display_name": "Deprecated"
+ }
+]
+```
+
+!!! note
+ The above works only if the API token used to authenticate the request has permission to make a `POST` request to this endpoint.
+
+### Filtering by Custom Field
+
+To filter results by a custom field value, prepend `cf_` to the custom field name. For example, the following query will return only sites where a custom field named `foo` is equal to 123:
+
+```no-highlight
+GET /api/dcim/sites/?cf_foo=123
+```
+
+Custom fields can be mixed with built-in fields to further narrow results. When creating a custom string field, the type of filtering selected (loose versus exact) determines whether partial or full matching is used.
+
+## Lookup Expressions
+
+Certain model fields also support filtering using additional lookup expressions. This allows
+for negation and other context-specific filtering.
+
+These lookup expressions can be applied by adding a suffix to the desired field's name, e.g. `mac_address__n`. In this case, the filter expression is for negation and it is separated by two underscores. Below are the lookup expressions that are supported across different field types.
+
+### Numeric Fields
+
+Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
+
+- `n` - not equal to (negation)
+- `lt` - less than
+- `lte` - less than or equal
+- `gt` - greater than
+- `gte` - greater than or equal
+
+### String Fields
+
+String based (char) fields (Name, Address, etc) support these lookup expressions:
+
+- `n` - not equal to (negation)
+- `ic` - case insensitive contains
+- `nic` - negated case insensitive contains
+- `isw` - case insensitive starts with
+- `nisw` - negated case insensitive starts with
+- `iew` - case insensitive ends with
+- `niew` - negated case insensitive ends with
+- `ie` - case insensitive exact match
+- `nie` - negated case insensitive exact match
+
+### Foreign Keys & Other Fields
+
+Certain other fields, namely foreign key relationships support just the negation
+expression: `n`.
diff --git a/docs/rest-api/overview.md b/docs/rest-api/overview.md
new file mode 100644
index 000000000..290343aa6
--- /dev/null
+++ b/docs/rest-api/overview.md
@@ -0,0 +1,563 @@
+# REST API Overview
+
+## What is a REST API?
+
+REST stands for [representational state transfer](https://en.wikipedia.org/wiki/Representational_state_transfer). It's a particular type of API which employs HTTP requests and [JavaScript Object Notation (JSON)](https://www.json.org/) to facilitate create, retrieve, update, and delete (CRUD) operations on objects within an application. Each type of operation is associated with a particular HTTP verb:
+
+* `GET`: Retrieve an object or list of objects
+* `POST`: Create an object
+* `PUT` / `PATCH`: Modify an existing object. `PUT` requires all mandatory fields to be specified, while `PATCH` only expects the field that is being modified to be specified.
+* `DELETE`: Delete an existing object
+
+Additionally, the `OPTIONS` verb can be used to inspect a particular REST API endpoint and return all supported actions and their available parameters.
+
+One of the primary benefits of a REST API is its human-friendliness. Because it utilizes HTTP and JSON, it's very easy to interact with NetBox data on the command line using common tools. For example, we can request an IP address from NetBox and output the JSON using `curl` and `jq`. The following command makes an HTTP `GET` request for information about a particular IP address, identified by its primary key, and uses `jq` to present the raw JSON data returned in a more human-friendly format. (Piping the output through `jq` isn't strictly required but makes it much easier to read.)
+
+```no-highlight
+curl -s http://netbox/api/ipam/ip-addresses/2954/ | jq '.'
+```
+
+```json
+{
+ "id": 2954,
+ "url": "http://netbox/api/ipam/ip-addresses/2954/",
+ "family": {
+ "value": 4,
+ "label": "IPv4"
+ },
+ "address": "192.168.0.42/26",
+ "vrf": null,
+ "tenant": null,
+ "status": {
+ "value": "active",
+ "label": "Active"
+ },
+ "role": null,
+ "assigned_object_type": "dcim.interface",
+ "assigned_object_id": 114771,
+ "assigned_object": {
+ "id": 114771,
+ "url": "http://netbox/api/dcim/interfaces/114771/",
+ "device": {
+ "id": 2230,
+ "url": "http://netbox/api/dcim/devices/2230/",
+ "name": "router1",
+ "display_name": "router1"
+ },
+ "name": "et-0/1/2",
+ "cable": null,
+ "connection_status": null
+ },
+ "nat_inside": null,
+ "nat_outside": null,
+ "dns_name": "",
+ "description": "Example IP address",
+ "tags": [],
+ "custom_fields": {},
+ "created": "2020-08-04",
+ "last_updated": "2020-08-04T14:12:39.666885Z"
+}
+```
+
+Each attribute of the IP address is expressed as an attribute of the JSON object. Fields may include their own nested objects, as in the case of the `assigned_object` field above. Every object includes a primary key named `id` which uniquely identifies it in the database.
+
+## Interactive Documentation
+
+Comprehensive, interactive documentation of all REST API endpoints is available on a running NetBox instance at `/api/docs/`. This interface provides a convenient sandbox for researching and experimenting with specific endpoints and request types. The API itself can also be explored using a web browser by navigating to its root at `/api/`.
+
+## Endpoint Hierarchy
+
+NetBox's entire REST API is housed under the API root at `https:///api/`. The URL structure is divided at the root level by application: circuits, DCIM, extras, IPAM, plugins, secrets, tenancy, users, and virtualization. Within each application exists a separate path for each model. For example, the provider and circuit objects are located under the "circuits" application:
+
+* `/api/circuits/providers/`
+* `/api/circuits/circuits/`
+
+Likewise, the site, rack, and device objects are located under the "DCIM" application:
+
+* `/api/dcim/sites/`
+* `/api/dcim/racks/`
+* `/api/dcim/devices/`
+
+The full hierarchy of available endpoints can be viewed by navigating to the API root in a web browser.
+
+Each model generally has two views associated with it: a list view and a detail view. The list view is used to retrieve a list of multiple objects and to create new objects. The detail view is used to retrieve, update, or delete an single existing object. All objects are referenced by their numeric primary key (`id`).
+
+* `/api/dcim/devices/` - List existing devices or create a new device
+* `/api/dcim/devices/123/` - Retrieve, update, or delete the device with ID 123
+
+Lists of objects can be filtered using a set of query parameters. For example, to find all interfaces belonging to the device with ID 123:
+
+```
+GET /api/dcim/interfaces/?device_id=123
+```
+
+See the [filtering documentation](filtering.md) for more details.
+
+## Serialization
+
+The REST API employs two types of serializers to represent model data: base serializers and nested serializers. The base serializer is used to present the complete view of a model. This includes all database table fields which comprise the model, and may include additional metadata. A base serializer includes relationships to parent objects, but **does not** include child objects. For example, the `VLANSerializer` includes a nested representation its parent VLANGroup (if any), but does not include any assigned Prefixes.
+
+```json
+{
+ "id": 1048,
+ "site": {
+ "id": 7,
+ "url": "http://netbox/api/dcim/sites/7/",
+ "name": "Corporate HQ",
+ "slug": "corporate-hq"
+ },
+ "group": {
+ "id": 4,
+ "url": "http://netbox/api/ipam/vlan-groups/4/",
+ "name": "Production",
+ "slug": "production"
+ },
+ "vid": 101,
+ "name": "Users-Floor1",
+ "tenant": null,
+ "status": {
+ "value": 1,
+ "label": "Active"
+ },
+ "role": {
+ "id": 9,
+ "url": "http://netbox/api/ipam/roles/9/",
+ "name": "User Access",
+ "slug": "user-access"
+ },
+ "description": "",
+ "display_name": "101 (Users-Floor1)",
+ "custom_fields": {}
+}
+```
+
+### Related Objects
+
+Related objects (e.g. `ForeignKey` fields) are represented using nested serializers. A nested serializer provides a minimal representation of an object, including only its direct URL and enough information to display the object to a user. When performing write API actions (`POST`, `PUT`, and `PATCH`), related objects may be specified by either numeric ID (primary key), or by a set of attributes sufficiently unique to return the desired object.
+
+For example, when creating a new device, its rack can be specified by NetBox ID (PK):
+
+```json
+{
+ "name": "MyNewDevice",
+ "rack": 123,
+ ...
+}
+```
+
+Or by a set of nested attributes which uniquely identify the rack:
+
+```json
+{
+ "name": "MyNewDevice",
+ "rack": {
+ "site": {
+ "name": "Equinix DC6"
+ },
+ "name": "R204"
+ },
+ ...
+}
+```
+
+Note that if the provided parameters do not return exactly one object, a validation error is raised.
+
+### Generic Relations
+
+Some objects within NetBox have attributes which can reference an object of multiple types, known as _generic relations_. For example, an IP address can be assigned to either a device interface _or_ a virtual machine interface. When making this assignment via the REST API, we must specify two attributes:
+
+* `assigned_object_type` - The content type of the assigned object, defined as `.`
+* `assigned_object_id` - The assigned object's unique numeric ID
+
+Together, these values identify a unique object in NetBox. The assigned object (if any) is represented by the `assigned_object` attribute on the IP address model.
+
+```no-highlight
+curl -X POST \
+-H "Authorization: Token $TOKEN" \
+-H "Content-Type: application/json" \
+-H "Accept: application/json; indent=4" \
+http://netbox/api/ipam/ip-addresses/ \
+--data '{
+ "address": "192.0.2.1/24",
+ "assigned_object_type": "dcim.interface",
+ "assigned_object_id": 69023
+}'
+```
+
+```json
+{
+ "id": 56296,
+ "url": "http://netbox/api/ipam/ip-addresses/56296/",
+ "assigned_object_type": "dcim.interface",
+ "assigned_object_id": 69000,
+ "assigned_object": {
+ "id": 69000,
+ "url": "http://netbox/api/dcim/interfaces/69023/",
+ "device": {
+ "id": 2174,
+ "url": "http://netbox/api/dcim/devices/2174/",
+ "name": "device105",
+ "display_name": "device105"
+ },
+ "name": "ge-0/0/0",
+ "cable": null,
+ "connection_status": null
+ },
+ ...
+}
+```
+
+If we wanted to assign this IP address to a virtual machine interface instead, we would have set `assigned_object_type` to `virtualization.vminterface` and updated the object ID appropriately.
+
+### Brief Format
+
+Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of available objects without any related data, such as when populating a drop-down list in a form. As an example, the default (complete) format of an IP address looks like this:
+
+```
+GET /api/ipam/prefixes/13980/
+
+{
+ "id": 13980,
+ "url": "http://netbox/api/ipam/prefixes/13980/",
+ "family": {
+ "value": 4,
+ "label": "IPv4"
+ },
+ "prefix": "192.0.2.0/24",
+ "site": {
+ "id": 3,
+ "url": "http://netbox/api/dcim/sites/17/",
+ "name": "Site 23A",
+ "slug": "site-23a"
+ },
+ "vrf": null,
+ "tenant": null,
+ "vlan": null,
+ "status": {
+ "value": "container",
+ "label": "Container"
+ },
+ "role": {
+ "id": 17,
+ "url": "http://netbox/api/ipam/roles/17/",
+ "name": "Staging",
+ "slug": "staging"
+ },
+ "is_pool": false,
+ "description": "Example prefix",
+ "tags": [],
+ "custom_fields": {},
+ "created": "2018-12-10",
+ "last_updated": "2019-03-01T20:02:46.173540Z"
+}
+```
+
+The brief format is much more terse:
+
+```
+GET /api/ipam/prefixes/13980/?brief=1
+
+{
+ "id": 13980,
+ "url": "http://netbox/api/ipam/prefixes/13980/",
+ "family": 4,
+ "prefix": "10.40.3.0/24"
+}
+```
+
+The brief format is supported for both lists and individual objects.
+
+### Excluding Config Contexts
+
+When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext/) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views.
+
+## Pagination
+
+API responses which contain a list of many objects will be paginated for efficiency. The root JSON object returned by a list endpoint contains the following attributes:
+
+* `count`: The total number of all objects matching the query
+* `next`: A hyperlink to the next page of results (if applicable)
+* `previous`: A hyperlink to the previous page of results (if applicable)
+* `results`: The list of objects on the current page
+
+Here is an example of a paginated response:
+
+```
+HTTP 200 OK
+Allow: GET, POST, OPTIONS
+Content-Type: application/json
+Vary: Accept
+
+{
+ "count": 2861,
+ "next": "http://netbox/api/dcim/devices/?limit=50&offset=50",
+ "previous": null,
+ "results": [
+ {
+ "id": 231,
+ "name": "Device1",
+ ...
+ },
+ {
+ "id": 232,
+ "name": "Device2",
+ ...
+ },
+ ...
+ ]
+}
+```
+
+The default page is determined by the [`PAGINATE_COUNT`](../../configuration/optional-settings/#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for:
+
+```
+http://netbox/api/dcim/devices/?limit=100
+```
+
+The response will return devices 1 through 100. The URL provided in the `next` attribute of the response will return devices 101 through 200:
+
+```json
+{
+ "count": 2861,
+ "next": "http://netbox/api/dcim/devices/?limit=100&offset=100",
+ "previous": null,
+ "results": [...]
+}
+```
+
+The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../../configuration/optional-settings/#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request.
+
+!!! warning
+ Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.
+
+## Interacting with Objects
+
+### Retrieving Multiple Objects
+
+To query NetBox for a list of objects, make a `GET` request to the model's _list_ endpoint. Objects are listed under the response object's `results` parameter.
+
+```no-highlight
+curl -s -X GET http://netbox/api/ipam/ip-addresses/ | jq '.'
+```
+
+```json
+{
+ "count": 42031,
+ "next": "http://netbox/api/ipam/ip-addresses/?limit=50&offset=50",
+ "previous": null,
+ "results": [
+ {
+ "id": 5618,
+ "address": "192.0.2.1/24",
+ ...
+ },
+ {
+ "id": 5619,
+ "address": "192.0.2.2/24",
+ ...
+ },
+ {
+ "id": 5620,
+ "address": "192.0.2.3/24",
+ ...
+ },
+ ...
+ ]
+}
+```
+
+### Retrieving a Single Object
+
+To query NetBox for a single object, make a `GET` request to the model's _detail_ endpoint specifying its unique numeric ID.
+
+!!! note
+ Note that the trailing slash is required. Omitting this will return a 302 redirect.
+
+```no-highlight
+curl -s -X GET http://netbox/api/ipam/ip-addresses/5618/ | jq '.'
+```
+
+```json
+{
+ "id": 5618,
+ "address": "192.0.2.1/24",
+ ...
+}
+```
+
+### Creating a New Object
+
+To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](../authentication/) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`.
+
+```no-highlight
+curl -s -X POST \
+-H "Authorization: Token $TOKEN" \
+-H "Content-Type: application/json" \
+http://netbox/api/ipam/prefixes/ \
+--data '{"prefix": "192.0.2.0/24", "site": 6}' | jq '.'
+```
+
+```json
+{
+ "id": 18691,
+ "url": "http://netbox/api/ipam/prefixes/18691/",
+ "family": {
+ "value": 4,
+ "label": "IPv4"
+ },
+ "prefix": "192.0.2.0/24",
+ "site": {
+ "id": 6,
+ "url": "http://netbox/api/dcim/sites/6/",
+ "name": "US-East 4",
+ "slug": "us-east-4"
+ },
+ "vrf": null,
+ "tenant": null,
+ "vlan": null,
+ "status": {
+ "value": "active",
+ "label": "Active"
+ },
+ "role": null,
+ "is_pool": false,
+ "description": "",
+ "tags": [],
+ "custom_fields": {},
+ "created": "2020-08-04",
+ "last_updated": "2020-08-04T20:08:39.007125Z"
+}
+```
+
+### Creating Multiple Objects
+
+To create multiple instances of a model using a single request, make a `POST` request to the model's _list_ endpoint with a list of JSON objects representing each instance to be created. If successful, the response will contain a list of the newly created instances. The example below illustrates the creation of three new sites.
+
+```no-highlight
+curl -X POST -H "Authorization: Token $TOKEN" \
+-H "Content-Type: application/json" \
+-H "Accept: application/json; indent=4" \
+http://netbox/api/dcim/sites/ \
+--data '[
+{"name": "Site 1", "slug": "site-1", "region": {"name": "United States"}},
+{"name": "Site 2", "slug": "site-2", "region": {"name": "United States"}},
+{"name": "Site 3", "slug": "site-3", "region": {"name": "United States"}}
+]'
+```
+
+```json
+[
+ {
+ "id": 21,
+ "url": "http://netbox/api/dcim/sites/21/",
+ "name": "Site 1",
+ ...
+ },
+ {
+ "id": 22,
+ "url": "http://netbox/api/dcim/sites/22/",
+ "name": "Site 2",
+ ...
+ },
+ {
+ "id": 23,
+ "url": "http://netbox/api/dcim/sites/23/",
+ "name": "Site 3",
+ ...
+ }
+]
+```
+
+### Updating an Object
+
+To modify an object which has already been created, make a `PATCH` request to the model's _detail_ endpoint specifying its unique numeric ID. Include any data which you wish to update on the object. As with object creation, the `Authorization` and `Content-Type` headers must also be specified.
+
+```no-highlight
+curl -s -X PATCH \
+-H "Authorization: Token $TOKEN" \
+-H "Content-Type: application/json" \
+http://netbox/api/ipam/prefixes/18691/ \
+--data '{"status": "reserved"}' | jq '.'
+```
+
+```json
+{
+ "id": 18691,
+ "url": "http://netbox/api/ipam/prefixes/18691/",
+ "family": {
+ "value": 4,
+ "label": "IPv4"
+ },
+ "prefix": "192.0.2.0/24",
+ "site": {
+ "id": 6,
+ "url": "http://netbox/api/dcim/sites/6/",
+ "name": "US-East 4",
+ "slug": "us-east-4"
+ },
+ "vrf": null,
+ "tenant": null,
+ "vlan": null,
+ "status": {
+ "value": "reserved",
+ "label": "Reserved"
+ },
+ "role": null,
+ "is_pool": false,
+ "description": "",
+ "tags": [],
+ "custom_fields": {},
+ "created": "2020-08-04",
+ "last_updated": "2020-08-04T20:14:55.709430Z"
+}
+```
+
+!!! note "PUT versus PATCH"
+ The NetBox REST API support the use of either `PUT` or `PATCH` to modify an existing object. The difference is that a `PUT` request requires the user to specify a _complete_ representation of the object being modified, whereas a `PATCH` request need include only the attributes that are being updated. For most purposes, using `PATCH` is recommended.
+
+### Updating Multiple Objects
+
+Multiple objects can be updated simultaneously by issuing a `PUT` or `PATCH` request to a model's list endpoint with a list of dictionaries specifying the numeric ID of each object to be deleted and the attributes to be updated. For example, to update sites with IDs 10 and 11 to a status of "active", issue the following request:
+
+```no-highlight
+curl -s -X PATCH \
+-H "Authorization: Token $TOKEN" \
+-H "Content-Type: application/json" \
+http://netbox/api/dcim/sites/ \
+--data '[{"id": 10, "status": "active"}, {"id": 11, "status": "active"}]'
+```
+
+Note that there is no requirement for the attributes to be identical among objects. For instance, it's possible to update the status of one site along with the name of another in the same request.
+
+!!! note
+ The bulk update of objects is an all-or-none operation, meaning that if NetBox fails to successfully update any of the specified objects (e.g. due a validation error), the entire operation will be aborted and none of the objects will be updated.
+
+### Deleting an Object
+
+To delete an object from NetBox, make a `DELETE` request to the model's _detail_ endpoint specifying its unique numeric ID. The `Authorization` header must be included to specify an authorization token, however this type of request does not support passing any data in the body.
+
+```no-highlight
+curl -s -X DELETE \
+-H "Authorization: Token $TOKEN" \
+http://netbox/api/ipam/prefixes/18691/
+```
+
+Note that `DELETE` requests do not return any data: If successful, the API will return a 204 (No Content) response.
+
+!!! note
+ You can run `curl` with the verbose (`-v`) flag to inspect the HTTP response codes.
+
+### Deleting Multiple Objects
+
+NetBox supports the simultaneous deletion of multiple objects of the same type by issuing a `DELETE` request to the model's list endpoint with a list of dictionaries specifying the numeric ID of each object to be deleted. For example, to delete sites with IDs 10, 11, and 12, issue the following request:
+
+```no-highlight
+curl -s -X DELETE \
+-H "Authorization: Token $TOKEN" \
+-H "Content-Type: application/json" \
+http://netbox/api/dcim/sites/ \
+--data '[{"id": 10}, {"id": 11}, {"id": 12}]'
+```
+
+!!! note
+ The bulk deletion of objects is an all-or-none operation, meaning that if NetBox fails to delete any of the specified objects (e.g. due a dependency by a related object), the entire operation will be aborted and none of the objects will be deleted.
diff --git a/docs/api/working-with-secrets.md b/docs/rest-api/working-with-secrets.md
similarity index 54%
rename from docs/api/working-with-secrets.md
rename to docs/rest-api/working-with-secrets.md
index 129bd0855..dafbb7239 100644
--- a/docs/api/working-with-secrets.md
+++ b/docs/rest-api/working-with-secrets.md
@@ -1,16 +1,19 @@
# Working with Secrets
-As with most other objects, the NetBox API can be used to create, modify, and delete secrets. However, additional steps are needed to encrypt or decrypt secret data.
+As with most other objects, the REST API can be used to view, create, modify, and delete secrets. However, additional steps are needed to encrypt or decrypt secret data.
## Generating a Session Key
In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../../core-functionality/secrets/#user-keys). The private key must be POSTed with the name `private_key`.
-```
-$ curl -X POST http://localhost:8000/api/secrets/get-session-key/ \
--H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
+```no-highlight
+$ curl -X POST http://netbox/api/secrets/get-session-key/ \
+-H "Authorization: Token $TOKEN" \
-H "Accept: application/json; indent=4" \
--data-urlencode "private_key@"
+```
+
+```json
{
"session_key": "dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
}
@@ -19,94 +22,106 @@ $ curl -X POST http://localhost:8000/api/secrets/get-session-key/ \
!!! note
To read the private key from a file, use the convention above. Alternatively, the private key can be read from an environment variable using `--data-urlencode "private_key=$PRIVATE_KEY"`.
-The request uses your private key to unlock your stored copy of the master key and generate a session key which can be attached in the `X-Session-Key` header of future API requests.
+The request uses the provided private key to unlock your stored copy of the master key and generate a temporary session key, which can be attached in the `X-Session-Key` header of future API requests.
## Retrieving Secrets
A session key is not needed to retrieve unencrypted secrets: The secret is returned like any normal object with its `plaintext` field set to null.
-```
-$ curl http://localhost:8000/api/secrets/secrets/2587/ \
--H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
+```no-highlight
+$ curl http://netbox/api/secrets/secrets/2587/ \
+-H "Authorization: Token $TOKEN" \
-H "Accept: application/json; indent=4"
+```
+
+```json
{
"id": 2587,
+ "url": "http://netbox/api/secrets/secrets/2587/",
"device": {
"id": 1827,
- "url": "http://localhost:8000/api/dcim/devices/1827/",
+ "url": "http://netbox/api/dcim/devices/1827/",
"name": "MyTestDevice",
"display_name": "MyTestDevice"
},
"role": {
"id": 1,
- "url": "http://localhost:8000/api/secrets/secret-roles/1/",
+ "url": "http://netbox/api/secrets/secret-roles/1/",
"name": "Login Credentials",
"slug": "login-creds"
},
"name": "admin",
"plaintext": null,
"hash": "pbkdf2_sha256$1000$G6mMFe4FetZQ$f+0itZbAoUqW5pd8+NH8W5rdp/2QNLIBb+LGdt4OSKA=",
+ "tags": [],
+ "custom_fields": {},
"created": "2017-03-21",
"last_updated": "2017-03-21T19:28:44.265582Z"
}
```
-To decrypt a secret, we must include our session key in the `X-Session-Key` header:
+To decrypt a secret, we must include our session key in the `X-Session-Key` header when sending the `GET` request:
-```
-$ curl http://localhost:8000/api/secrets/secrets/2587/ \
--H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
+```no-highlight
+$ curl http://netbox/api/secrets/secrets/2587/ \
+-H "Authorization: Token $TOKEN" \
-H "Accept: application/json; indent=4" \
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
+```
+
+```json
{
"id": 2587,
+ "url": "http://netbox/api/secrets/secrets/2587/",
"device": {
"id": 1827,
- "url": "http://localhost:8000/api/dcim/devices/1827/",
+ "url": "http://netbox/api/dcim/devices/1827/",
"name": "MyTestDevice",
"display_name": "MyTestDevice"
},
"role": {
"id": 1,
- "url": "http://localhost:8000/api/secrets/secret-roles/1/",
+ "url": "http://netbox/api/secrets/secret-roles/1/",
"name": "Login Credentials",
"slug": "login-creds"
},
"name": "admin",
"plaintext": "foobar",
"hash": "pbkdf2_sha256$1000$G6mMFe4FetZQ$f+0itZbAoUqW5pd8+NH8W5rdp/2QNLIBb+LGdt4OSKA=",
+ "tags": [],
+ "custom_fields": {},
"created": "2017-03-21",
"last_updated": "2017-03-21T19:28:44.265582Z"
}
```
-Lists of secrets can be decrypted in this manner as well:
+Multiple secrets within a list can be decrypted in this manner as well:
-```
-$ curl http://localhost:8000/api/secrets/secrets/?limit=3 \
--H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
+```no-highlight
+$ curl http://netbox/api/secrets/secrets/?limit=3 \
+-H "Authorization: Token $TOKEN" \
-H "Accept: application/json; indent=4" \
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk="
+```
+
+```json
{
"count": 3482,
- "next": "http://localhost:8000/api/secrets/secrets/?limit=3&offset=3",
+ "next": "http://netbox/api/secrets/secrets/?limit=3&offset=3",
"previous": null,
"results": [
{
"id": 2587,
- ...
"plaintext": "foobar",
...
},
{
"id": 2588,
- ...
"plaintext": "MyP@ssw0rd!",
...
},
{
"id": 2589,
- ...
"plaintext": "AnotherSecret!",
...
},
@@ -114,25 +129,44 @@ $ curl http://localhost:8000/api/secrets/secrets/?limit=3 \
}
```
-## Creating Secrets
+## Creating and Updating Secrets
-Session keys are also used to decrypt new or modified secrets. This is done by setting the `plaintext` field of the submitted object:
+Session keys are required when creating or modifying secrets. The secret's `plaintext` attribute is set to its non-encrypted value, and NetBox uses the session key to compute and store the encrypted value.
-```
-$ curl -X POST http://localhost:8000/api/secrets/secrets/ \
+```no-highlight
+$ curl -X POST http://netbox/api/secrets/secrets/ \
-H "Content-Type: application/json" \
--H "Authorization: Token c639d619ecbeb1f3055c4141ba6870e20572edd7" \
+-H "Authorization: Token $TOKEN" \
-H "Accept: application/json; indent=4" \
-H "X-Session-Key: dyEnxlc9lnGzaOAV1dV/xqYPV63njIbdZYOgnAlGPHk=" \
--data '{"device": 1827, "role": 1, "name": "backup", "plaintext": "Drowssap1"}'
+```
+
+```json
{
- "id": 2590,
- "device": 1827,
- "role": 1,
+ "id": 6194,
+ "url": "http://netbox/api/secrets/secrets/9194/",
+ "device": {
+ "id": 1827,
+ "url": "http://netbox/api/dcim/devices/1827/",
+ "name": "device43",
+ "display_name": "device43"
+ },
+ "role": {
+ "id": 1,
+ "url": "http://netbox/api/secrets/secret-roles/1/",
+ "name": "Login Credentials",
+ "slug": "login-creds"
+ },
"name": "backup",
- "plaintext": "Drowssap1"
+ "plaintext": "Drowssap1",
+ "hash": "pbkdf2_sha256$1000$J9db8sI5vBrd$IK6nFXnFl+K+nR5/KY8RSDxU1skYL8G69T5N3jZxM7c=",
+ "tags": [],
+ "custom_fields": {},
+ "created": "2020-08-05",
+ "last_updated": "2020-08-05T16:51:14.990506Z"
}
```
!!! note
- Don't forget to include the `Content-Type: application/json` header when making a POST request.
+ Don't forget to include the `Content-Type: application/json` header when making a POST or PATCH request.
diff --git a/mkdocs.yml b/mkdocs.yml
index b8633ea8f..092cb559a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -20,8 +20,9 @@ nav:
- 1. PostgreSQL: 'installation/1-postgresql.md'
- 2. Redis: 'installation/2-redis.md'
- 3. NetBox: 'installation/3-netbox.md'
- - 4. HTTP Daemon: 'installation/4-http-daemon.md'
- - 5. LDAP (Optional): 'installation/5-ldap.md'
+ - 4. Gunicorn: 'installation/4-gunicorn.md'
+ - 5. HTTP Server: 'installation/5-http-server.md'
+ - 6. LDAP (Optional): 'installation/6-ldap.md'
- Upgrading NetBox: 'installation/upgrading.md'
- Migrating to systemd: 'installation/migrating-to-systemd.md'
- Configuration:
@@ -39,42 +40,43 @@ nav:
- Circuits: 'core-functionality/circuits.md'
- Power Tracking: 'core-functionality/power.md'
- Secrets: 'core-functionality/secrets.md'
- - Tenancy Assignment: 'core-functionality/tenancy.md'
+ - Tenancy: 'core-functionality/tenancy.md'
- Additional Features:
- Caching: 'additional-features/caching.md'
- Change Logging: 'additional-features/change-logging.md'
- - Context Data: 'additional-features/context-data.md'
+ - Context Data: 'models/extras/configcontext.md'
- Custom Fields: 'additional-features/custom-fields.md'
- Custom Links: 'additional-features/custom-links.md'
- Custom Scripts: 'additional-features/custom-scripts.md'
- Export Templates: 'additional-features/export-templates.md'
- - Graphs: 'additional-features/graphs.md'
- NAPALM: 'additional-features/napalm.md'
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
- Reports: 'additional-features/reports.md'
- - Tags: 'additional-features/tags.md'
+ - Tags: 'models/extras/tag.md'
- Webhooks: 'additional-features/webhooks.md'
- Plugins:
- Using Plugins: 'plugins/index.md'
- Developing Plugins: 'plugins/development.md'
- Administration:
+ - Permissions: 'administration/permissions.md'
- Replicating NetBox: 'administration/replicating-netbox.md'
- NetBox Shell: 'administration/netbox-shell.md'
- - API:
- - Overview: 'api/overview.md'
- - Filtering: 'api/filtering.md'
- - Authentication: 'api/authentication.md'
- - Working with Secrets: 'api/working-with-secrets.md'
- - Examples: 'api/examples.md'
+ - REST API:
+ - Overview: 'rest-api/overview.md'
+ - Filtering: 'rest-api/filtering.md'
+ - Authentication: 'rest-api/authentication.md'
+ - Working with Secrets: 'rest-api/working-with-secrets.md'
- Development:
- Introduction: 'development/index.md'
+ - Getting Started: 'development/getting-started.md'
- Style Guide: 'development/style-guide.md'
- - Utility Views: 'development/utility-views.md'
- Extending Models: 'development/extending-models.md'
- Application Registry: 'development/application-registry.md'
- User Preferences: 'development/user-preferences.md'
- Release Checklist: 'development/release-checklist.md'
- Release Notes:
+ - Version 2.10: 'release-notes/version-2.10.md'
+ - Version 2.9: 'release-notes/version-2.9.md'
- Version 2.8: 'release-notes/version-2.8.md'
- Version 2.7: 'release-notes/version-2.7.md'
- Version 2.6: 'release-notes/version-2.6.md'
diff --git a/netbox/circuits/api/nested_serializers.py b/netbox/circuits/api/nested_serializers.py
index 067b82282..2d3457d2c 100644
--- a/netbox/circuits/api/nested_serializers.py
+++ b/netbox/circuits/api/nested_serializers.py
@@ -1,7 +1,7 @@
from rest_framework import serializers
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
-from utilities.api import WritableNestedSerializer
+from netbox.api import WritableNestedSerializer
__all__ = [
'NestedCircuitSerializer',
@@ -51,4 +51,4 @@ class NestedCircuitTerminationSerializer(WritableNestedSerializer):
class Meta:
model = CircuitTermination
- fields = ['id', 'url', 'circuit', 'term_side']
+ fields = ['id', 'url', 'circuit', 'term_side', 'cable']
diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py
index 6bac48a59..88890bf95 100644
--- a/netbox/circuits/api/serializers.py
+++ b/netbox/circuits/api/serializers.py
@@ -1,13 +1,13 @@
from rest_framework import serializers
-from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from circuits.choices import CircuitStatusChoices
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
-from dcim.api.serializers import ConnectedEndpointSerializer
+from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
from extras.api.customfields import CustomFieldModelSerializer
+from extras.api.serializers import TaggedObjectSerializer
+from netbox.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
-from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
from .nested_serializers import *
@@ -15,14 +15,14 @@ from .nested_serializers import *
# Providers
#
-class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
- tags = TagListSerializerField(required=False)
+class ProviderSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
circuit_count = serializers.IntegerField(read_only=True)
class Meta:
model = Provider
fields = [
- 'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
+ 'id', 'url', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
'custom_fields', 'created', 'last_updated', 'circuit_count',
]
@@ -32,11 +32,12 @@ class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
#
class CircuitTypeSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True)
class Meta:
model = CircuitType
- fields = ['id', 'name', 'slug', 'description', 'circuit_count']
+ fields = ['id', 'url', 'name', 'slug', 'description', 'circuit_count']
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
@@ -49,24 +50,25 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'site', 'connected_endpoint', 'port_speed', 'upstream_speed', 'xconnect_id']
-class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
+class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
provider = NestedProviderSerializer()
status = ChoiceField(choices=CircuitStatusChoices, required=False)
type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
termination_a = CircuitCircuitTerminationSerializer(read_only=True)
termination_z = CircuitCircuitTerminationSerializer(read_only=True)
- tags = TagListSerializerField(required=False)
class Meta:
model = Circuit
fields = [
- 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
+ 'id', 'url', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
-class CircuitTerminationSerializer(ConnectedEndpointSerializer):
+class CircuitTerminationSerializer(CableTerminationSerializer, ConnectedEndpointSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer()
site = NestedSiteSerializer()
cable = NestedCableSerializer(read_only=True)
@@ -74,6 +76,7 @@ class CircuitTerminationSerializer(ConnectedEndpointSerializer):
class Meta:
model = CircuitTermination
fields = [
- 'id', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
- 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
+ 'id', 'url', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
+ 'description', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
+ 'connected_endpoint_reachable'
]
diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py
index 01fbfb62c..b496796fe 100644
--- a/netbox/circuits/api/urls.py
+++ b/netbox/circuits/api/urls.py
@@ -1,18 +1,9 @@
-from rest_framework import routers
-
+from netbox.api import OrderedDefaultRouter
from . import views
-class CircuitsRootView(routers.APIRootView):
- """
- Circuits API root view
- """
- def get_view_name(self):
- return 'Circuits'
-
-
-router = routers.DefaultRouter()
-router.APIRootView = CircuitsRootView
+router = OrderedDefaultRouter()
+router.APIRootView = views.CircuitsRootView
# Providers
router.register('providers', views.ProviderViewSet)
diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py
index 363392a4d..736871a73 100644
--- a/netbox/circuits/api/views.py
+++ b/netbox/circuits/api/views.py
@@ -1,38 +1,34 @@
-from django.db.models import Count
-from django.shortcuts import get_object_or_404
-from rest_framework.decorators import action
-from rest_framework.response import Response
+from django.db.models import Prefetch
+from rest_framework.routers import APIRootView
from circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
-from extras.api.serializers import RenderedGraphSerializer
+from dcim.api.views import PathEndpointMixin
from extras.api.views import CustomFieldModelViewSet
-from extras.models import Graph
-from utilities.api import ModelViewSet
+from netbox.api.views import ModelViewSet
+from utilities.utils import count_related
from . import serializers
+class CircuitsRootView(APIRootView):
+ """
+ Circuits API root view
+ """
+ def get_view_name(self):
+ return 'Circuits'
+
+
#
# Providers
#
class ProviderViewSet(CustomFieldModelViewSet):
queryset = Provider.objects.prefetch_related('tags').annotate(
- circuit_count=Count('circuits')
+ circuit_count=count_related(Circuit, 'provider')
)
serializer_class = serializers.ProviderSerializer
filterset_class = filters.ProviderFilterSet
- @action(detail=True)
- def graphs(self, request, pk):
- """
- A convenience method for rendering graphs for a particular provider.
- """
- provider = get_object_or_404(Provider, pk=pk)
- queryset = Graph.objects.filter(type__model='provider')
- serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
- return Response(serializer.data)
-
#
# Circuit Types
@@ -40,7 +36,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
class CircuitTypeViewSet(ModelViewSet):
queryset = CircuitType.objects.annotate(
- circuit_count=Count('circuits')
+ circuit_count=count_related(Circuit, 'type')
)
serializer_class = serializers.CircuitTypeSerializer
filterset_class = filters.CircuitTypeFilterSet
@@ -52,7 +48,8 @@ class CircuitTypeViewSet(ModelViewSet):
class CircuitViewSet(CustomFieldModelViewSet):
queryset = Circuit.objects.prefetch_related(
- 'type', 'tenant', 'provider', 'terminations__site', 'terminations__connected_endpoint__device'
+ Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related('site')),
+ 'type', 'tenant', 'provider',
).prefetch_related('tags')
serializer_class = serializers.CircuitSerializer
filterset_class = filters.CircuitFilterSet
@@ -62,9 +59,10 @@ class CircuitViewSet(CustomFieldModelViewSet):
# Circuit Terminations
#
-class CircuitTerminationViewSet(ModelViewSet):
+class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
queryset = CircuitTermination.objects.prefetch_related(
- 'circuit', 'site', 'connected_endpoint__device', 'cable'
+ 'circuit', 'site', '_path__destination', 'cable'
)
serializer_class = serializers.CircuitTerminationSerializer
filterset_class = filters.CircuitTerminationFilterSet
+ brief_prefetch_fields = ['circuit']
diff --git a/netbox/circuits/choices.py b/netbox/circuits/choices.py
index 94a765d11..bbf536800 100644
--- a/netbox/circuits/choices.py
+++ b/netbox/circuits/choices.py
@@ -23,13 +23,13 @@ class CircuitStatusChoices(ChoiceSet):
(STATUS_DECOMMISSIONED, 'Decommissioned'),
)
- LEGACY_MAP = {
- STATUS_DEPROVISIONING: 0,
- STATUS_ACTIVE: 1,
- STATUS_PLANNED: 2,
- STATUS_PROVISIONING: 3,
- STATUS_OFFLINE: 4,
- STATUS_DECOMMISSIONED: 5,
+ CSS_CLASSES = {
+ STATUS_DEPROVISIONING: 'warning',
+ STATUS_ACTIVE: 'success',
+ STATUS_PLANNED: 'info',
+ STATUS_PROVISIONING: 'primary',
+ STATUS_OFFLINE: 'danger',
+ STATUS_DECOMMISSIONED: 'default',
}
diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py
index 206dcc305..fa563881c 100644
--- a/netbox/circuits/filters.py
+++ b/netbox/circuits/filters.py
@@ -1,8 +1,9 @@
import django_filters
from django.db.models import Q
+from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet
from dcim.models import Region, Site
-from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
+from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
from tenancy.filters import TenancyFilterSet
from utilities.filters import (
BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
@@ -18,7 +19,7 @@ __all__ = (
)
-class ProviderFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -72,7 +73,7 @@ class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug']
-class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
+class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -144,7 +145,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, Cr
).distinct()
-class CircuitTerminationFilterSet(BaseFilterSet):
+class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py
index 2185d1eab..4731c9adb 100644
--- a/netbox/circuits/forms.py
+++ b/netbox/circuits/forms.py
@@ -3,14 +3,14 @@ from django import forms
from dcim.models import Region, Site
from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
- TagField,
)
+from extras.models import Tag
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
- APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField,
- CSVModelForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField,
- StaticSelect2, StaticSelect2Multiple, TagFilterField,
+ add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DatePicker,
+ DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2,
+ StaticSelect2Multiple, TagFilterField,
)
from .choices import CircuitStatusChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -23,7 +23,8 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
class ProviderForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()
comments = CommentField()
- tags = TagField(
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
required=False
)
@@ -105,21 +106,15 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
- required=False,
- widget=APISelectMultiple(
- value_field="slug",
- filter_for={
- 'site': 'region'
- }
- )
+ required=False
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False,
- widget=APISelectMultiple(
- value_field="slug",
- )
+ query_params={
+ 'region': '$region'
+ }
)
asn = forms.IntegerField(
required=False,
@@ -165,7 +160,8 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
queryset=CircuitType.objects.all()
)
comments = CommentField()
- tags = TagField(
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
required=False
)
@@ -269,18 +265,12 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
type = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(),
to_field_name='slug',
- required=False,
- widget=APISelectMultiple(
- value_field="slug",
- )
+ required=False
)
provider = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
to_field_name='slug',
- required=False,
- widget=APISelectMultiple(
- value_field="slug",
- )
+ required=False
)
status = forms.MultipleChoiceField(
choices=CircuitStatusChoices,
@@ -290,21 +280,15 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
- required=False,
- widget=APISelectMultiple(
- value_field="slug",
- filter_for={
- 'site': 'region'
- }
- )
+ required=False
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False,
- widget=APISelectMultiple(
- value_field="slug",
- )
+ query_params={
+ 'region': '$region'
+ }
)
commit_rate = forms.IntegerField(
required=False,
@@ -319,14 +303,24 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
#
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
+ region = DynamicModelChoiceField(
+ queryset=Region.objects.all(),
+ required=False,
+ initial_params={
+ 'sites': '$site'
+ }
+ )
site = DynamicModelChoiceField(
- queryset=Site.objects.all()
+ queryset=Site.objects.all(),
+ query_params={
+ 'region_id': '$region'
+ }
)
class Meta:
model = CircuitTermination
fields = [
- 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
+ 'term_side', 'region', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description',
]
help_texts = {
'port_speed': "Physical circuit speed",
diff --git a/netbox/circuits/migrations/0019_nullbooleanfield_to_booleanfield.py b/netbox/circuits/migrations/0019_nullbooleanfield_to_booleanfield.py
new file mode 100644
index 000000000..c8e844284
--- /dev/null
+++ b/netbox/circuits/migrations/0019_nullbooleanfield_to_booleanfield.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1b1 on 2020-07-16 15:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('circuits', '0018_standardize_description'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='circuittermination',
+ name='connection_status',
+ field=models.BooleanField(blank=True, null=True),
+ ),
+ ]
diff --git a/netbox/circuits/migrations/0020_custom_field_data.py b/netbox/circuits/migrations/0020_custom_field_data.py
new file mode 100644
index 000000000..97da9962c
--- /dev/null
+++ b/netbox/circuits/migrations/0020_custom_field_data.py
@@ -0,0 +1,22 @@
+import django.core.serializers.json
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('circuits', '0019_nullbooleanfield_to_booleanfield'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='circuit',
+ name='custom_field_data',
+ field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
+ ),
+ migrations.AddField(
+ model_name='provider',
+ name='custom_field_data',
+ field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
+ ),
+ ]
diff --git a/netbox/circuits/migrations/0021_cache_cable_peer.py b/netbox/circuits/migrations/0021_cache_cable_peer.py
new file mode 100644
index 000000000..630c3b4ec
--- /dev/null
+++ b/netbox/circuits/migrations/0021_cache_cable_peer.py
@@ -0,0 +1,49 @@
+import sys
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+def cache_cable_peers(apps, schema_editor):
+ ContentType = apps.get_model('contenttypes', 'ContentType')
+ Cable = apps.get_model('dcim', 'Cable')
+ CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
+
+ if 'test' not in sys.argv:
+ print(f"\n Updating circuit termination cable peers...", flush=True)
+ ct = ContentType.objects.get_for_model(CircuitTermination)
+ for cable in Cable.objects.filter(termination_a_type=ct):
+ CircuitTermination.objects.filter(pk=cable.termination_a_id).update(
+ _cable_peer_type_id=cable.termination_b_type_id,
+ _cable_peer_id=cable.termination_b_id
+ )
+ for cable in Cable.objects.filter(termination_b_type=ct):
+ CircuitTermination.objects.filter(pk=cable.termination_b_id).update(
+ _cable_peer_type_id=cable.termination_a_type_id,
+ _cable_peer_id=cable.termination_a_id
+ )
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('circuits', '0020_custom_field_data'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='circuittermination',
+ name='_cable_peer_id',
+ field=models.PositiveIntegerField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='circuittermination',
+ name='_cable_peer_type',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
+ ),
+ migrations.RunPython(
+ code=cache_cable_peers,
+ reverse_code=migrations.RunPython.noop
+ ),
+ ]
diff --git a/netbox/circuits/migrations/0022_cablepath.py b/netbox/circuits/migrations/0022_cablepath.py
new file mode 100644
index 000000000..4a5b26efa
--- /dev/null
+++ b/netbox/circuits/migrations/0022_cablepath.py
@@ -0,0 +1,26 @@
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dcim', '0121_cablepath'),
+ ('circuits', '0021_cache_cable_peer'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='circuittermination',
+ name='_path',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'),
+ ),
+ migrations.RemoveField(
+ model_name='circuittermination',
+ name='connected_endpoint',
+ ),
+ migrations.RemoveField(
+ model_name='circuittermination',
+ name='connection_status',
+ ),
+ ]
diff --git a/netbox/circuits/migrations/0023_circuittermination_port_speed_optional.py b/netbox/circuits/migrations/0023_circuittermination_port_speed_optional.py
new file mode 100644
index 000000000..ea9190623
--- /dev/null
+++ b/netbox/circuits/migrations/0023_circuittermination_port_speed_optional.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1 on 2020-10-09 17:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('circuits', '0022_cablepath'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='circuittermination',
+ name='port_speed',
+ field=models.PositiveIntegerField(blank=True, null=True),
+ ),
+ ]
diff --git a/netbox/circuits/migrations/0024_standardize_name_length.py b/netbox/circuits/migrations/0024_standardize_name_length.py
new file mode 100644
index 000000000..8d0ae48e3
--- /dev/null
+++ b/netbox/circuits/migrations/0024_standardize_name_length.py
@@ -0,0 +1,38 @@
+# Generated by Django 3.1 on 2020-10-15 19:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('circuits', '0023_circuittermination_port_speed_optional'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='circuit',
+ name='cid',
+ field=models.CharField(max_length=100),
+ ),
+ migrations.AlterField(
+ model_name='circuittype',
+ name='name',
+ field=models.CharField(max_length=100, unique=True),
+ ),
+ migrations.AlterField(
+ model_name='circuittype',
+ name='slug',
+ field=models.SlugField(max_length=100, unique=True),
+ ),
+ migrations.AlterField(
+ model_name='provider',
+ name='name',
+ field=models.CharField(max_length=100, unique=True),
+ ),
+ migrations.AlterField(
+ model_name='provider',
+ name='slug',
+ field=models.SlugField(max_length=100, unique=True),
+ ),
+ ]
diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py
index 57d41a994..3d6d5d232 100644
--- a/netbox/circuits/models.py
+++ b/netbox/circuits/models.py
@@ -1,14 +1,12 @@
-from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
-from dcim.constants import CONNECTION_STATUS_CHOICES
from dcim.fields import ASNField
-from dcim.models import CableTermination
-from extras.models import CustomFieldModel, ObjectChange, TaggedItem
+from dcim.models import CableTermination, PathEndpoint
+from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.utils import extras_features
-from utilities.models import ChangeLoggedModel
+from utilities.querysets import RestrictedQuerySet
from utilities.utils import serialize_object
from .choices import *
from .querysets import CircuitQuerySet
@@ -22,17 +20,18 @@ __all__ = (
)
-@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Provider(ChangeLoggedModel, CustomFieldModel):
"""
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider.
"""
name = models.CharField(
- max_length=50,
+ max_length=100,
unique=True
)
slug = models.SlugField(
+ max_length=100,
unique=True
)
asn = ASNField(
@@ -61,14 +60,10 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
- custom_field_values = GenericRelation(
- to='extras.CustomFieldValue',
- content_type_field='obj_type',
- object_id_field='obj_id'
- )
-
tags = TaggableManager(through=TaggedItem)
+ objects = RestrictedQuerySet.as_manager()
+
csv_headers = [
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
]
@@ -104,10 +99,11 @@ class CircuitType(ChangeLoggedModel):
"Long Haul," "Metro," or "Out-of-Band".
"""
name = models.CharField(
- max_length=50,
+ max_length=100,
unique=True
)
slug = models.SlugField(
+ max_length=100,
unique=True
)
description = models.CharField(
@@ -115,6 +111,8 @@ class CircuitType(ChangeLoggedModel):
blank=True,
)
+ objects = RestrictedQuerySet.as_manager()
+
csv_headers = ['name', 'slug', 'description']
class Meta:
@@ -142,7 +140,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
in Kbps.
"""
cid = models.CharField(
- max_length=50,
+ max_length=100,
verbose_name='Circuit ID'
)
provider = models.ForeignKey(
@@ -183,11 +181,6 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
- custom_field_values = GenericRelation(
- to='extras.CustomFieldValue',
- content_type_field='obj_type',
- object_id_field='obj_id'
- )
objects = CircuitQuerySet.as_manager()
tags = TaggableManager(through=TaggedItem)
@@ -199,15 +192,6 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
]
- STATUS_CLASS_MAP = {
- CircuitStatusChoices.STATUS_DEPROVISIONING: 'warning',
- CircuitStatusChoices.STATUS_ACTIVE: 'success',
- CircuitStatusChoices.STATUS_PLANNED: 'info',
- CircuitStatusChoices.STATUS_PROVISIONING: 'primary',
- CircuitStatusChoices.STATUS_OFFLINE: 'danger',
- CircuitStatusChoices.STATUS_DECOMMISSIONED: 'default',
- }
-
class Meta:
ordering = ['provider', 'cid']
unique_together = ['provider', 'cid']
@@ -232,7 +216,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
)
def get_status_class(self):
- return self.STATUS_CLASS_MAP.get(self.status)
+ return CircuitStatusChoices.CSS_CLASSES.get(self.status)
def _get_termination(self, side):
for ct in self.terminations.all():
@@ -249,7 +233,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
return self._get_termination('Z')
-class CircuitTermination(CableTermination):
+class CircuitTermination(PathEndpoint, CableTermination):
circuit = models.ForeignKey(
to='circuits.Circuit',
on_delete=models.CASCADE,
@@ -265,20 +249,11 @@ class CircuitTermination(CableTermination):
on_delete=models.PROTECT,
related_name='circuit_terminations'
)
- connected_endpoint = models.OneToOneField(
- to='dcim.Interface',
- on_delete=models.SET_NULL,
- related_name='+',
+ port_speed = models.PositiveIntegerField(
+ verbose_name='Port speed (Kbps)',
blank=True,
null=True
)
- connection_status = models.NullBooleanField(
- choices=CONNECTION_STATUS_CHOICES,
- blank=True
- )
- port_speed = models.PositiveIntegerField(
- verbose_name='Port speed (Kbps)'
- )
upstream_speed = models.PositiveIntegerField(
blank=True,
null=True,
@@ -300,6 +275,8 @@ class CircuitTermination(CableTermination):
blank=True
)
+ objects = RestrictedQuerySet.as_manager()
+
class Meta:
ordering = ['circuit', 'term_side']
unique_together = ['circuit', 'term_side']
@@ -330,6 +307,9 @@ class CircuitTermination(CableTermination):
def get_peer_termination(self):
peer_side = 'Z' if self.term_side == 'A' else 'A'
try:
- return CircuitTermination.objects.prefetch_related('site').get(circuit=self.circuit, term_side=peer_side)
+ return CircuitTermination.objects.prefetch_related('site').get(
+ circuit=self.circuit,
+ term_side=peer_side
+ )
except CircuitTermination.DoesNotExist:
return None
diff --git a/netbox/circuits/querysets.py b/netbox/circuits/querysets.py
index 60956f32a..8a9bd50a4 100644
--- a/netbox/circuits/querysets.py
+++ b/netbox/circuits/querysets.py
@@ -1,7 +1,9 @@
-from django.db.models import OuterRef, QuerySet, Subquery
+from django.db.models import OuterRef, Subquery
+
+from utilities.querysets import RestrictedQuerySet
-class CircuitQuerySet(QuerySet):
+class CircuitQuerySet(RestrictedQuerySet):
def annotate_sites(self):
"""
diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py
index ea17031a1..782b02394 100644
--- a/netbox/circuits/tables.py
+++ b/netbox/circuits/tables.py
@@ -2,23 +2,9 @@ import django_tables2 as tables
from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT
-from utilities.tables import BaseTable, TagColumn, ToggleColumn
+from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn
from .models import Circuit, CircuitType, Provider
-CIRCUITTYPE_ACTIONS = """
-
-
-
-{% if perms.circuit.change_circuittype %}
-
-{% endif %}
-"""
-
-STATUS_LABEL = """
-{{ record.get_status_display }}
-"""
-
#
# Providers
@@ -53,11 +39,7 @@ class CircuitTypeTable(BaseTable):
circuit_count = tables.Column(
verbose_name='Circuits'
)
- actions = tables.TemplateColumn(
- template_code=CIRCUITTYPE_ACTIONS,
- attrs={'td': {'class': 'text-right noprint'}},
- verbose_name=''
- )
+ actions = ButtonsColumn(CircuitType, pk_field='slug')
class Meta(BaseTable.Meta):
model = CircuitType
@@ -76,11 +58,9 @@ class CircuitTable(BaseTable):
)
provider = tables.LinkColumn(
viewname='circuits:provider',
- args=[Accessor('provider.slug')]
- )
- status = tables.TemplateColumn(
- template_code=STATUS_LABEL
+ args=[Accessor('provider__slug')]
)
+ status = ChoiceFieldColumn()
tenant = tables.TemplateColumn(
template_code=COL_TENANT
)
diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py
index b5f8758e7..6df931553 100644
--- a/netbox/circuits/tests/test_api.py
+++ b/netbox/circuits/tests/test_api.py
@@ -1,443 +1,180 @@
-from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
-from rest_framework import status
from circuits.choices import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Site
-from extras.models import Graph
-from utilities.testing import APITestCase
+from utilities.testing import APITestCase, APIViewTestCases
class AppTest(APITestCase):
def test_root(self):
-
url = reverse('circuits-api:api-root')
response = self.client.get('{}?format=api'.format(url), **self.header)
self.assertEqual(response.status_code, 200)
-class ProviderTest(APITestCase):
+class ProviderTest(APIViewTestCases.APIViewTestCase):
+ model = Provider
+ brief_fields = ['circuit_count', 'id', 'name', 'slug', 'url']
+ create_data = [
+ {
+ 'name': 'Provider 4',
+ 'slug': 'provider-4',
+ },
+ {
+ 'name': 'Provider 5',
+ 'slug': 'provider-5',
+ },
+ {
+ 'name': 'Provider 6',
+ 'slug': 'provider-6',
+ },
+ ]
+ bulk_update_data = {
+ 'asn': 1234,
+ }
- def setUp(self):
+ @classmethod
+ def setUpTestData(cls):
- super().setUp()
-
- self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
- self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
- self.provider3 = Provider.objects.create(name='Test Provider 3', slug='test-provider-3')
-
- def test_get_provider(self):
-
- url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
- response = self.client.get(url, **self.header)
-
- self.assertEqual(response.data['name'], self.provider1.name)
-
- def test_get_provider_graphs(self):
-
- provider_ct = ContentType.objects.get(app_label='circuits', model='provider')
- self.graph1 = Graph.objects.create(
- type=provider_ct,
- name='Test Graph 1',
- source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'
+ providers = (
+ Provider(name='Provider 1', slug='provider-1'),
+ Provider(name='Provider 2', slug='provider-2'),
+ Provider(name='Provider 3', slug='provider-3'),
)
- self.graph2 = Graph.objects.create(
- type=provider_ct,
- name='Test Graph 2',
- source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2'
+ Provider.objects.bulk_create(providers)
+
+
+class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
+ model = CircuitType
+ brief_fields = ['circuit_count', 'id', 'name', 'slug', 'url']
+ create_data = (
+ {
+ 'name': 'Circuit Type 4',
+ 'slug': 'circuit-type-4',
+ },
+ {
+ 'name': 'Circuit Type 5',
+ 'slug': 'circuit-type-5',
+ },
+ {
+ 'name': 'Circuit Type 6',
+ 'slug': 'circuit-type-6',
+ },
+ )
+ bulk_update_data = {
+ 'description': 'New description',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+
+ circuit_types = (
+ CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
+ CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
+ CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
)
- self.graph3 = Graph.objects.create(
- type=provider_ct,
- name='Test Graph 3',
- source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3'
+ CircuitType.objects.bulk_create(circuit_types)
+
+
+class CircuitTest(APIViewTestCases.APIViewTestCase):
+ model = Circuit
+ brief_fields = ['cid', 'id', 'url']
+ bulk_update_data = {
+ 'status': 'planned',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+
+ providers = (
+ Provider(name='Provider 1', slug='provider-1'),
+ Provider(name='Provider 2', slug='provider-2'),
)
+ Provider.objects.bulk_create(providers)
- url = reverse('circuits-api:provider-graphs', kwargs={'pk': self.provider1.pk})
- response = self.client.get(url, **self.header)
-
- self.assertEqual(len(response.data), 3)
- self.assertEqual(response.data[0]['embed_url'], 'http://example.com/graphs.py?provider=test-provider-1&foo=1')
-
- def test_list_providers(self):
-
- url = reverse('circuits-api:provider-list')
- response = self.client.get(url, **self.header)
-
- self.assertEqual(response.data['count'], 3)
-
- def test_list_providers_brief(self):
-
- url = reverse('circuits-api:provider-list')
- response = self.client.get('{}?brief=1'.format(url), **self.header)
-
- self.assertEqual(
- sorted(response.data['results'][0]),
- ['circuit_count', 'id', 'name', 'slug', 'url']
+ circuit_types = (
+ CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
+ CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
)
+ CircuitType.objects.bulk_create(circuit_types)
- def test_create_provider(self):
+ circuits = (
+ Circuit(cid='Circuit 1', provider=providers[0], type=circuit_types[0]),
+ Circuit(cid='Circuit 2', provider=providers[0], type=circuit_types[0]),
+ Circuit(cid='Circuit 3', provider=providers[0], type=circuit_types[0]),
+ )
+ Circuit.objects.bulk_create(circuits)
- data = {
- 'name': 'Test Provider 4',
- 'slug': 'test-provider-4',
- }
-
- url = reverse('circuits-api:provider-list')
- response = self.client.post(url, data, format='json', **self.header)
-
- self.assertHttpStatus(response, status.HTTP_201_CREATED)
- self.assertEqual(Provider.objects.count(), 4)
- provider4 = Provider.objects.get(pk=response.data['id'])
- self.assertEqual(provider4.name, data['name'])
- self.assertEqual(provider4.slug, data['slug'])
-
- def test_create_provider_bulk(self):
-
- data = [
+ cls.create_data = [
{
- 'name': 'Test Provider 4',
- 'slug': 'test-provider-4',
+ 'cid': 'Circuit 4',
+ 'provider': providers[1].pk,
+ 'type': circuit_types[1].pk,
},
{
- 'name': 'Test Provider 5',
- 'slug': 'test-provider-5',
+ 'cid': 'Circuit 5',
+ 'provider': providers[1].pk,
+ 'type': circuit_types[1].pk,
},
{
- 'name': 'Test Provider 6',
- 'slug': 'test-provider-6',
+ 'cid': 'Circuit 6',
+ 'provider': providers[1].pk,
+ 'type': circuit_types[1].pk,
},
]
- url = reverse('circuits-api:provider-list')
- response = self.client.post(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_201_CREATED)
- self.assertEqual(Provider.objects.count(), 6)
- self.assertEqual(response.data[0]['name'], data[0]['name'])
- self.assertEqual(response.data[1]['name'], data[1]['name'])
- self.assertEqual(response.data[2]['name'], data[2]['name'])
+class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
+ model = CircuitTermination
+ brief_fields = ['cable', 'circuit', 'id', 'term_side', 'url']
- def test_update_provider(self):
+ @classmethod
+ def setUpTestData(cls):
+ SIDE_A = CircuitTerminationSideChoices.SIDE_A
+ SIDE_Z = CircuitTerminationSideChoices.SIDE_Z
- data = {
- 'name': 'Test Provider X',
- 'slug': 'test-provider-x',
- }
-
- url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
- response = self.client.put(url, data, format='json', **self.header)
-
- self.assertHttpStatus(response, status.HTTP_200_OK)
- self.assertEqual(Provider.objects.count(), 3)
- provider1 = Provider.objects.get(pk=response.data['id'])
- self.assertEqual(provider1.name, data['name'])
- self.assertEqual(provider1.slug, data['slug'])
-
- def test_delete_provider(self):
-
- url = reverse('circuits-api:provider-detail', kwargs={'pk': self.provider1.pk})
- response = self.client.delete(url, **self.header)
-
- self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
- self.assertEqual(Provider.objects.count(), 2)
-
-
-class CircuitTypeTest(APITestCase):
-
- def setUp(self):
-
- super().setUp()
-
- self.circuittype1 = CircuitType.objects.create(name='Test Circuit Type 1', slug='test-circuit-type-1')
- self.circuittype2 = CircuitType.objects.create(name='Test Circuit Type 2', slug='test-circuit-type-2')
- self.circuittype3 = CircuitType.objects.create(name='Test Circuit Type 3', slug='test-circuit-type-3')
-
- def test_get_circuittype(self):
-
- url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
- response = self.client.get(url, **self.header)
-
- self.assertEqual(response.data['name'], self.circuittype1.name)
-
- def test_list_circuittypes(self):
-
- url = reverse('circuits-api:circuittype-list')
- response = self.client.get(url, **self.header)
-
- self.assertEqual(response.data['count'], 3)
-
- def test_list_circuittypes_brief(self):
-
- url = reverse('circuits-api:circuittype-list')
- response = self.client.get('{}?brief=1'.format(url), **self.header)
-
- self.assertEqual(
- sorted(response.data['results'][0]),
- ['circuit_count', 'id', 'name', 'slug', 'url']
+ sites = (
+ Site(name='Site 1', slug='site-1'),
+ Site(name='Site 2', slug='site-2'),
)
+ Site.objects.bulk_create(sites)
- def test_create_circuittype(self):
+ provider = Provider.objects.create(name='Provider 1', slug='provider-1')
+ circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
- data = {
- 'name': 'Test Circuit Type 4',
- 'slug': 'test-circuit-type-4',
- }
-
- url = reverse('circuits-api:circuittype-list')
- response = self.client.post(url, data, format='json', **self.header)
-
- self.assertHttpStatus(response, status.HTTP_201_CREATED)
- self.assertEqual(CircuitType.objects.count(), 4)
- circuittype4 = CircuitType.objects.get(pk=response.data['id'])
- self.assertEqual(circuittype4.name, data['name'])
- self.assertEqual(circuittype4.slug, data['slug'])
-
- def test_update_circuittype(self):
-
- data = {
- 'name': 'Test Circuit Type X',
- 'slug': 'test-circuit-type-x',
- }
-
- url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
- response = self.client.put(url, data, format='json', **self.header)
-
- self.assertHttpStatus(response, status.HTTP_200_OK)
- self.assertEqual(CircuitType.objects.count(), 3)
- circuittype1 = CircuitType.objects.get(pk=response.data['id'])
- self.assertEqual(circuittype1.name, data['name'])
- self.assertEqual(circuittype1.slug, data['slug'])
-
- def test_delete_circuittype(self):
-
- url = reverse('circuits-api:circuittype-detail', kwargs={'pk': self.circuittype1.pk})
- response = self.client.delete(url, **self.header)
-
- self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
- self.assertEqual(CircuitType.objects.count(), 2)
-
-
-class CircuitTest(APITestCase):
-
- def setUp(self):
-
- super().setUp()
-
- self.provider1 = Provider.objects.create(name='Test Provider 1', slug='test-provider-1')
- self.provider2 = Provider.objects.create(name='Test Provider 2', slug='test-provider-2')
- self.circuittype1 = CircuitType.objects.create(name='Test Circuit Type 1', slug='test-circuit-type-1')
- self.circuittype2 = CircuitType.objects.create(name='Test Circuit Type 2', slug='test-circuit-type-2')
- self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=self.provider1, type=self.circuittype1)
- self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=self.provider1, type=self.circuittype1)
- self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=self.provider1, type=self.circuittype1)
-
- def test_get_circuit(self):
-
- url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
- response = self.client.get(url, **self.header)
-
- self.assertEqual(response.data['cid'], self.circuit1.cid)
-
- def test_list_circuits(self):
-
- url = reverse('circuits-api:circuit-list')
- response = self.client.get(url, **self.header)
-
- self.assertEqual(response.data['count'], 3)
-
- def test_list_circuits_brief(self):
-
- url = reverse('circuits-api:circuit-list')
- response = self.client.get('{}?brief=1'.format(url), **self.header)
-
- self.assertEqual(
- sorted(response.data['results'][0]),
- ['cid', 'id', 'url']
+ circuits = (
+ Circuit(cid='Circuit 1', provider=provider, type=circuit_type),
+ Circuit(cid='Circuit 2', provider=provider, type=circuit_type),
+ Circuit(cid='Circuit 3', provider=provider, type=circuit_type),
)
+ Circuit.objects.bulk_create(circuits)
- def test_create_circuit(self):
+ circuit_terminations = (
+ CircuitTermination(circuit=circuits[0], site=sites[0], term_side=SIDE_A),
+ CircuitTermination(circuit=circuits[0], site=sites[1], term_side=SIDE_Z),
+ CircuitTermination(circuit=circuits[1], site=sites[0], term_side=SIDE_A),
+ CircuitTermination(circuit=circuits[1], site=sites[1], term_side=SIDE_Z),
+ )
+ CircuitTermination.objects.bulk_create(circuit_terminations)
- data = {
- 'cid': 'TEST0004',
- 'provider': self.provider1.pk,
- 'type': self.circuittype1.pk,
- 'status': CircuitStatusChoices.STATUS_ACTIVE,
- }
-
- url = reverse('circuits-api:circuit-list')
- response = self.client.post(url, data, format='json', **self.header)
-
- self.assertHttpStatus(response, status.HTTP_201_CREATED)
- self.assertEqual(Circuit.objects.count(), 4)
- circuit4 = Circuit.objects.get(pk=response.data['id'])
- self.assertEqual(circuit4.cid, data['cid'])
- self.assertEqual(circuit4.provider_id, data['provider'])
- self.assertEqual(circuit4.type_id, data['type'])
-
- def test_create_circuit_bulk(self):
-
- data = [
+ cls.create_data = [
{
- 'cid': 'TEST0004',
- 'provider': self.provider1.pk,
- 'type': self.circuittype1.pk,
- 'status': CircuitStatusChoices.STATUS_ACTIVE,
+ 'circuit': circuits[2].pk,
+ 'term_side': SIDE_A,
+ 'site': sites[1].pk,
+ 'port_speed': 200000,
},
{
- 'cid': 'TEST0005',
- 'provider': self.provider1.pk,
- 'type': self.circuittype1.pk,
- 'status': CircuitStatusChoices.STATUS_ACTIVE,
- },
- {
- 'cid': 'TEST0006',
- 'provider': self.provider1.pk,
- 'type': self.circuittype1.pk,
- 'status': CircuitStatusChoices.STATUS_ACTIVE,
+ 'circuit': circuits[2].pk,
+ 'term_side': SIDE_Z,
+ 'site': sites[1].pk,
+ 'port_speed': 200000,
},
]
- url = reverse('circuits-api:circuit-list')
- response = self.client.post(url, data, format='json', **self.header)
-
- self.assertHttpStatus(response, status.HTTP_201_CREATED)
- self.assertEqual(Circuit.objects.count(), 6)
- self.assertEqual(response.data[0]['cid'], data[0]['cid'])
- self.assertEqual(response.data[1]['cid'], data[1]['cid'])
- self.assertEqual(response.data[2]['cid'], data[2]['cid'])
-
- def test_update_circuit(self):
-
- data = {
- 'cid': 'TEST000X',
- 'provider': self.provider2.pk,
- 'type': self.circuittype2.pk,
+ cls.bulk_update_data = {
+ 'port_speed': 123456
}
-
- url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
- response = self.client.put(url, data, format='json', **self.header)
-
- self.assertHttpStatus(response, status.HTTP_200_OK)
- self.assertEqual(Circuit.objects.count(), 3)
- circuit1 = Circuit.objects.get(pk=response.data['id'])
- self.assertEqual(circuit1.cid, data['cid'])
- self.assertEqual(circuit1.provider_id, data['provider'])
- self.assertEqual(circuit1.type_id, data['type'])
-
- def test_delete_circuit(self):
-
- url = reverse('circuits-api:circuit-detail', kwargs={'pk': self.circuit1.pk})
- response = self.client.delete(url, **self.header)
-
- self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
- self.assertEqual(Circuit.objects.count(), 2)
-
-
-class CircuitTerminationTest(APITestCase):
-
- def setUp(self):
-
- super().setUp()
-
- self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
- self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
- provider = Provider.objects.create(name='Test Provider', slug='test-provider')
- circuittype = CircuitType.objects.create(name='Test Circuit Type', slug='test-circuit-type')
- self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=provider, type=circuittype)
- self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
- self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
- self.circuittermination1 = CircuitTermination.objects.create(
- circuit=self.circuit1,
- term_side=CircuitTerminationSideChoices.SIDE_A,
- site=self.site1,
- port_speed=1000000
- )
- self.circuittermination2 = CircuitTermination.objects.create(
- circuit=self.circuit1,
- term_side=CircuitTerminationSideChoices.SIDE_Z,
- site=self.site2,
- port_speed=1000000
- )
- self.circuittermination3 = CircuitTermination.objects.create(
- circuit=self.circuit2,
- term_side=CircuitTerminationSideChoices.SIDE_A,
- site=self.site1,
- port_speed=1000000
- )
- self.circuittermination4 = CircuitTermination.objects.create(
- circuit=self.circuit2,
- term_side=CircuitTerminationSideChoices.SIDE_Z,
- site=self.site2,
- port_speed=1000000
- )
-
- def test_get_circuittermination(self):
-
- url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
- response = self.client.get(url, **self.header)
-
- self.assertEqual(response.data['id'], self.circuittermination1.pk)
-
- def test_list_circuitterminations(self):
-
- url = reverse('circuits-api:circuittermination-list')
- response = self.client.get(url, **self.header)
-
- self.assertEqual(response.data['count'], 4)
-
- def test_create_circuittermination(self):
-
- data = {
- 'circuit': self.circuit3.pk,
- 'term_side': CircuitTerminationSideChoices.SIDE_A,
- 'site': self.site1.pk,
- 'port_speed': 1000000,
- }
-
- url = reverse('circuits-api:circuittermination-list')
- response = self.client.post(url, data, format='json', **self.header)
-
- self.assertHttpStatus(response, status.HTTP_201_CREATED)
- self.assertEqual(CircuitTermination.objects.count(), 5)
- circuittermination4 = CircuitTermination.objects.get(pk=response.data['id'])
- self.assertEqual(circuittermination4.circuit_id, data['circuit'])
- self.assertEqual(circuittermination4.term_side, data['term_side'])
- self.assertEqual(circuittermination4.site_id, data['site'])
- self.assertEqual(circuittermination4.port_speed, data['port_speed'])
-
- def test_update_circuittermination(self):
-
- circuittermination5 = CircuitTermination.objects.create(
- circuit=self.circuit3,
- term_side=CircuitTerminationSideChoices.SIDE_A,
- site=self.site1,
- port_speed=1000000
- )
-
- data = {
- 'circuit': self.circuit3.pk,
- 'term_side': CircuitTerminationSideChoices.SIDE_Z,
- 'site': self.site2.pk,
- 'port_speed': 1000000,
- }
-
- url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': circuittermination5.pk})
- response = self.client.put(url, data, format='json', **self.header)
-
- self.assertHttpStatus(response, status.HTTP_200_OK)
- self.assertEqual(CircuitTermination.objects.count(), 5)
- circuittermination1 = CircuitTermination.objects.get(pk=response.data['id'])
- self.assertEqual(circuittermination1.term_side, data['term_side'])
- self.assertEqual(circuittermination1.site_id, data['site'])
- self.assertEqual(circuittermination1.port_speed, data['port_speed'])
-
- def test_delete_circuittermination(self):
-
- url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
- response = self.client.delete(url, **self.header)
-
- self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
- self.assertEqual(CircuitTermination.objects.count(), 3)
diff --git a/netbox/circuits/tests/test_filters.py b/netbox/circuits/tests/test_filters.py
index 9756c320b..9477bfbac 100644
--- a/netbox/circuits/tests/test_filters.py
+++ b/netbox/circuits/tests/test_filters.py
@@ -3,7 +3,7 @@ from django.test import TestCase
from circuits.choices import *
from circuits.filters import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
-from dcim.models import Region, Site
+from dcim.models import Cable, Region, Site
from tenancy.models import Tenant, TenantGroup
@@ -50,8 +50,8 @@ class ProviderTestCase(TestCase):
Circuit.objects.bulk_create(circuits)
CircuitTermination.objects.bulk_create((
- CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000),
- CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A', port_speed=1000),
+ CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
+ CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
))
def test_id(self):
@@ -176,9 +176,9 @@ class CircuitTestCase(TestCase):
Circuit.objects.bulk_create(circuits)
circuit_terminations = ((
- CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000),
- CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=1000),
- CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=1000),
+ CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
+ CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A'),
+ CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A'),
))
CircuitTermination.objects.bulk_create(circuit_terminations)
@@ -286,6 +286,8 @@ class CircuitTerminationTestCase(TestCase):
))
CircuitTermination.objects.bulk_create(circuit_terminations)
+ Cable(termination_a=circuit_terminations[0], termination_b=circuit_terminations[1]).save()
+
def test_term_side(self):
params = {'term_side': 'A'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
@@ -313,3 +315,13 @@ class CircuitTerminationTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+
+ def test_cabled(self):
+ params = {'cabled': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_connected(self):
+ params = {'connected': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'connected': False}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py
index 9cc7af6ae..3356fca8f 100644
--- a/netbox/circuits/tests/test_views.py
+++ b/netbox/circuits/tests/test_views.py
@@ -17,6 +17,8 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
Provider(name='Provider 3', slug='provider-3', asn=65003),
])
+ tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+
cls.form_data = {
'name': 'Provider X',
'slug': 'provider-x',
@@ -26,7 +28,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'noc_contact': 'noc@example.com',
'admin_contact': 'admin@example.com',
'comments': 'Another provider',
- 'tags': 'Alpha,Bravo,Charlie',
+ 'tags': [t.pk for t in tags],
}
cls.csv_data = (
@@ -96,6 +98,8 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]),
])
+ tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+
cls.form_data = {
'cid': 'Circuit X',
'provider': providers[1].pk,
@@ -106,7 +110,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'commit_rate': 1000,
'description': 'A new circuit',
'comments': 'Some comments',
- 'tags': 'Alpha,Bravo,Charlie',
+ 'tags': [t.pk for t in tags],
}
cls.csv_data = (
@@ -124,5 +128,4 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'commit_rate': 2000,
'description': 'New description',
'comments': 'New comments',
-
}
diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py
index 72d9720df..d757fd90d 100644
--- a/netbox/circuits/urls.py
+++ b/netbox/circuits/urls.py
@@ -1,6 +1,6 @@
from django.urls import path
-from dcim.views import CableCreateView, CableTraceView
+from dcim.views import CableCreateView, PathTraceView
from extras.views import ObjectChangeLogView
from . import views
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -10,7 +10,7 @@ urlpatterns = [
# Providers
path('providers/', views.ProviderListView.as_view(), name='provider_list'),
- path('providers/add/', views.ProviderCreateView.as_view(), name='provider_add'),
+ path('providers/add/', views.ProviderEditView.as_view(), name='provider_add'),
path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
@@ -21,15 +21,16 @@ urlpatterns = [
# Circuit types
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
- path('circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
+ path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
path('circuit-types//edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
+ path('circuit-types//delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
path('circuit-types//changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
# Circuits
path('circuits/', views.CircuitListView.as_view(), name='circuit_list'),
- path('circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'),
+ path('circuits/add/', views.CircuitEditView.as_view(), name='circuit_add'),
path('circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'),
path('circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
path('circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
@@ -37,14 +38,13 @@ urlpatterns = [
path('circuits//edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
path('circuits//delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
path('circuits//changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
- path('circuits//terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'),
+ path('circuits//terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'),
# Circuit terminations
-
- path('circuits//terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
+ path('circuits//terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
path('circuit-terminations//edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
path('circuit-terminations//delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
path('circuit-terminations//connect//', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
- path('circuit-terminations//trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
+ path('circuit-terminations//trace/', PathTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
]
diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py
index 709d2a726..9fea26652 100644
--- a/netbox/circuits/views.py
+++ b/netbox/circuits/views.py
@@ -1,19 +1,12 @@
-from django.conf import settings
from django.contrib import messages
-from django.contrib.auth.decorators import permission_required
-from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db import transaction
-from django.db.models import Count, OuterRef, Subquery
from django.shortcuts import get_object_or_404, redirect, render
-from django.views.generic import View
from django_tables2 import RequestConfig
-from extras.models import Graph
+from netbox.views import generic
from utilities.forms import ConfirmationForm
-from utilities.paginator import EnhancedPaginator
-from utilities.views import (
- BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
-)
+from utilities.paginator import EnhancedPaginator, get_paginate_count
+from utilities.utils import count_related
from . import filters, forms, tables
from .choices import CircuitTerminationSideChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -23,220 +16,221 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
# Providers
#
-class ProviderListView(PermissionRequiredMixin, ObjectListView):
- permission_required = 'circuits.view_provider'
- queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
+class ProviderListView(generic.ObjectListView):
+ queryset = Provider.objects.annotate(
+ count_circuits=count_related(Circuit, 'provider')
+ )
filterset = filters.ProviderFilterSet
filterset_form = forms.ProviderFilterForm
table = tables.ProviderTable
-class ProviderView(PermissionRequiredMixin, View):
- permission_required = 'circuits.view_provider'
+class ProviderView(generic.ObjectView):
+ queryset = Provider.objects.all()
- def get(self, request, slug):
-
- provider = get_object_or_404(Provider, slug=slug)
- circuits = Circuit.objects.filter(
- provider=provider
+ def get_extra_context(self, request, instance):
+ circuits = Circuit.objects.restrict(request.user, 'view').filter(
+ provider=instance
).prefetch_related(
'type', 'tenant', 'terminations__site'
).annotate_sites()
- show_graphs = Graph.objects.filter(type__model='provider').exists()
circuits_table = tables.CircuitTable(circuits)
circuits_table.columns.hide('provider')
paginate = {
'paginator_class': EnhancedPaginator,
- 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
+ 'per_page': get_paginate_count(request)
}
RequestConfig(request, paginate).configure(circuits_table)
- return render(request, 'circuits/provider.html', {
- 'provider': provider,
+ return {
'circuits_table': circuits_table,
- 'show_graphs': show_graphs,
- })
+ }
-class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
- permission_required = 'circuits.add_provider'
- model = Provider
+class ProviderEditView(generic.ObjectEditView):
+ queryset = Provider.objects.all()
model_form = forms.ProviderForm
template_name = 'circuits/provider_edit.html'
- default_return_url = 'circuits:provider_list'
-class ProviderEditView(ProviderCreateView):
- permission_required = 'circuits.change_provider'
+class ProviderDeleteView(generic.ObjectDeleteView):
+ queryset = Provider.objects.all()
-class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
- permission_required = 'circuits.delete_provider'
- model = Provider
- default_return_url = 'circuits:provider_list'
-
-
-class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
- permission_required = 'circuits.add_provider'
+class ProviderBulkImportView(generic.BulkImportView):
+ queryset = Provider.objects.all()
model_form = forms.ProviderCSVForm
table = tables.ProviderTable
- default_return_url = 'circuits:provider_list'
-class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
- permission_required = 'circuits.change_provider'
- queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
+class ProviderBulkEditView(generic.BulkEditView):
+ queryset = Provider.objects.annotate(
+ count_circuits=count_related(Circuit, 'provider')
+ )
filterset = filters.ProviderFilterSet
table = tables.ProviderTable
form = forms.ProviderBulkEditForm
- default_return_url = 'circuits:provider_list'
-class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
- permission_required = 'circuits.delete_provider'
- queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
+class ProviderBulkDeleteView(generic.BulkDeleteView):
+ queryset = Provider.objects.annotate(
+ count_circuits=count_related(Circuit, 'provider')
+ )
filterset = filters.ProviderFilterSet
table = tables.ProviderTable
- default_return_url = 'circuits:provider_list'
#
# Circuit Types
#
-class CircuitTypeListView(PermissionRequiredMixin, ObjectListView):
- permission_required = 'circuits.view_circuittype'
- queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
+class CircuitTypeListView(generic.ObjectListView):
+ queryset = CircuitType.objects.annotate(
+ circuit_count=count_related(Circuit, 'type')
+ )
table = tables.CircuitTypeTable
-class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
- permission_required = 'circuits.add_circuittype'
- model = CircuitType
+class CircuitTypeEditView(generic.ObjectEditView):
+ queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeForm
- default_return_url = 'circuits:circuittype_list'
-class CircuitTypeEditView(CircuitTypeCreateView):
- permission_required = 'circuits.change_circuittype'
+class CircuitTypeDeleteView(generic.ObjectDeleteView):
+ queryset = CircuitType.objects.all()
-class CircuitTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
- permission_required = 'circuits.add_circuittype'
+class CircuitTypeBulkImportView(generic.BulkImportView):
+ queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeCSVForm
table = tables.CircuitTypeTable
- default_return_url = 'circuits:circuittype_list'
-class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
- permission_required = 'circuits.delete_circuittype'
- queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
+class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
+ queryset = CircuitType.objects.annotate(
+ circuit_count=count_related(Circuit, 'type')
+ )
table = tables.CircuitTypeTable
- default_return_url = 'circuits:circuittype_list'
#
# Circuits
#
-class CircuitListView(PermissionRequiredMixin, ObjectListView):
- permission_required = 'circuits.view_circuit'
- _terminations = CircuitTermination.objects.filter(circuit=OuterRef('pk'))
+class CircuitListView(generic.ObjectListView):
queryset = Circuit.objects.prefetch_related(
- 'provider', 'type', 'tenant', 'terminations__site'
+ 'provider', 'type', 'tenant', 'terminations'
).annotate_sites()
filterset = filters.CircuitFilterSet
filterset_form = forms.CircuitFilterForm
table = tables.CircuitTable
-class CircuitView(PermissionRequiredMixin, View):
- permission_required = 'circuits.view_circuit'
+class CircuitView(generic.ObjectView):
+ queryset = Circuit.objects.all()
- def get(self, request, pk):
+ def get_extra_context(self, request, instance):
- circuit = get_object_or_404(Circuit.objects.prefetch_related('provider', 'type', 'tenant__group'), pk=pk)
- termination_a = CircuitTermination.objects.prefetch_related(
- 'site__region', 'connected_endpoint__device'
+ # A-side termination
+ termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
+ 'site__region'
).filter(
- circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
- ).first()
- termination_z = CircuitTermination.objects.prefetch_related(
- 'site__region', 'connected_endpoint__device'
- ).filter(
- circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
+ circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
).first()
+ if termination_a and termination_a.connected_endpoint and hasattr(termination_a.connected_endpoint, 'ip_addresses'):
+ termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view')
- return render(request, 'circuits/circuit.html', {
- 'circuit': circuit,
+ # Z-side termination
+ termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
+ 'site__region'
+ ).filter(
+ circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
+ ).first()
+ if termination_z and termination_z.connected_endpoint and hasattr(termination_z.connected_endpoint, 'ip_addresses'):
+ termination_z.ip_addresses = termination_z.connected_endpoint.ip_addresses.restrict(request.user, 'view')
+
+ return {
'termination_a': termination_a,
'termination_z': termination_z,
- })
+ }
-class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
- permission_required = 'circuits.add_circuit'
- model = Circuit
+class CircuitEditView(generic.ObjectEditView):
+ queryset = Circuit.objects.all()
model_form = forms.CircuitForm
template_name = 'circuits/circuit_edit.html'
- default_return_url = 'circuits:circuit_list'
-class CircuitEditView(CircuitCreateView):
- permission_required = 'circuits.change_circuit'
+class CircuitDeleteView(generic.ObjectDeleteView):
+ queryset = Circuit.objects.all()
-class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
- permission_required = 'circuits.delete_circuit'
- model = Circuit
- default_return_url = 'circuits:circuit_list'
-
-
-class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
- permission_required = 'circuits.add_circuit'
+class CircuitBulkImportView(generic.BulkImportView):
+ queryset = Circuit.objects.all()
model_form = forms.CircuitCSVForm
table = tables.CircuitTable
- default_return_url = 'circuits:circuit_list'
-class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
- permission_required = 'circuits.change_circuit'
- queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
+class CircuitBulkEditView(generic.BulkEditView):
+ queryset = Circuit.objects.prefetch_related(
+ 'provider', 'type', 'tenant', 'terminations'
+ )
filterset = filters.CircuitFilterSet
table = tables.CircuitTable
form = forms.CircuitBulkEditForm
- default_return_url = 'circuits:circuit_list'
-class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
- permission_required = 'circuits.delete_circuit'
- queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
+class CircuitBulkDeleteView(generic.BulkDeleteView):
+ queryset = Circuit.objects.prefetch_related(
+ 'provider', 'type', 'tenant', 'terminations'
+ )
filterset = filters.CircuitFilterSet
table = tables.CircuitTable
- default_return_url = 'circuits:circuit_list'
-@permission_required('circuits.change_circuittermination')
-def circuit_terminations_swap(request, pk):
+class CircuitSwapTerminations(generic.ObjectEditView):
+ """
+ Swap the A and Z terminations of a circuit.
+ """
+ queryset = Circuit.objects.all()
- circuit = get_object_or_404(Circuit, pk=pk)
- termination_a = CircuitTermination.objects.filter(
- circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
- ).first()
- termination_z = CircuitTermination.objects.filter(
- circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
- ).first()
- if not termination_a and not termination_z:
- messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
- return redirect('circuits:circuit', pk=circuit.pk)
+ def get(self, request, pk):
+ circuit = get_object_or_404(self.queryset, pk=pk)
+ form = ConfirmationForm()
- if request.method == 'POST':
+ # Circuit must have at least one termination to swap
+ if not circuit.termination_a and not circuit.termination_z:
+ messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
+ return redirect('circuits:circuit', pk=circuit.pk)
+
+ return render(request, 'circuits/circuit_terminations_swap.html', {
+ 'circuit': circuit,
+ 'termination_a': circuit.termination_a,
+ 'termination_z': circuit.termination_z,
+ 'form': form,
+ 'panel_class': 'default',
+ 'button_class': 'primary',
+ 'return_url': circuit.get_absolute_url(),
+ })
+
+ def post(self, request, pk):
+ circuit = get_object_or_404(self.queryset, pk=pk)
form = ConfirmationForm(request.POST)
+
if form.is_valid():
+
+ termination_a = CircuitTermination.objects.filter(
+ circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
+ ).first()
+ termination_z = CircuitTermination.objects.filter(
+ circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
+ ).first()
+
if termination_a and termination_z:
# Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint
+ print('swapping')
with transaction.atomic():
termination_a.term_side = '_'
termination_a.save()
@@ -250,30 +244,27 @@ def circuit_terminations_swap(request, pk):
else:
termination_z.term_side = 'A'
termination_z.save()
+
messages.success(request, "Swapped terminations for circuit {}.".format(circuit))
return redirect('circuits:circuit', pk=circuit.pk)
- else:
- form = ConfirmationForm()
-
- return render(request, 'circuits/circuit_terminations_swap.html', {
- 'circuit': circuit,
- 'termination_a': termination_a,
- 'termination_z': termination_z,
- 'form': form,
- 'panel_class': 'default',
- 'button_class': 'primary',
- 'return_url': circuit.get_absolute_url(),
- })
+ return render(request, 'circuits/circuit_terminations_swap.html', {
+ 'circuit': circuit,
+ 'termination_a': circuit.termination_a,
+ 'termination_z': circuit.termination_z,
+ 'form': form,
+ 'panel_class': 'default',
+ 'button_class': 'primary',
+ 'return_url': circuit.get_absolute_url(),
+ })
#
# Circuit terminations
#
-class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
- permission_required = 'circuits.add_circuittermination'
- model = CircuitTermination
+class CircuitTerminationEditView(generic.ObjectEditView):
+ queryset = CircuitTermination.objects.all()
model_form = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html'
@@ -286,10 +277,5 @@ class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
return obj.circuit.get_absolute_url()
-class CircuitTerminationEditView(CircuitTerminationCreateView):
- permission_required = 'circuits.change_circuittermination'
-
-
-class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
- permission_required = 'circuits.delete_circuittermination'
- model = CircuitTermination
+class CircuitTerminationDeleteView(generic.ObjectDeleteView):
+ queryset = CircuitTermination.objects.all()
diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py
index 5afca0664..d63d32d68 100644
--- a/netbox/dcim/api/nested_serializers.py
+++ b/netbox/dcim/api/nested_serializers.py
@@ -1,33 +1,34 @@
from rest_framework import serializers
-from dcim.choices import InterfaceTypeChoices
-from dcim.constants import CONNECTION_STATUS_CHOICES
-from dcim.models import (
- Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate,
- Interface, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerPanel, PowerPort, PowerPortTemplate, Rack,
- RackGroup, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
-)
-from utilities.api import ChoiceField, WritableNestedSerializer
+from dcim import models
+from netbox.api import WritableNestedSerializer
__all__ = [
'NestedCableSerializer',
'NestedConsolePortSerializer',
+ 'NestedConsolePortTemplateSerializer',
'NestedConsoleServerPortSerializer',
+ 'NestedConsoleServerPortTemplateSerializer',
'NestedDeviceBaySerializer',
+ 'NestedDeviceBayTemplateSerializer',
'NestedDeviceRoleSerializer',
'NestedDeviceSerializer',
'NestedDeviceTypeSerializer',
'NestedFrontPortSerializer',
'NestedFrontPortTemplateSerializer',
'NestedInterfaceSerializer',
+ 'NestedInterfaceTemplateSerializer',
+ 'NestedInventoryItemSerializer',
'NestedManufacturerSerializer',
'NestedPlatformSerializer',
'NestedPowerFeedSerializer',
'NestedPowerOutletSerializer',
+ 'NestedPowerOutletTemplateSerializer',
'NestedPowerPanelSerializer',
'NestedPowerPortSerializer',
'NestedPowerPortTemplateSerializer',
'NestedRackGroupSerializer',
+ 'NestedRackReservationSerializer',
'NestedRackRoleSerializer',
'NestedRackSerializer',
'NestedRearPortSerializer',
@@ -45,17 +46,18 @@ __all__ = [
class NestedRegionSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
site_count = serializers.IntegerField(read_only=True)
+ _depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
- model = Region
- fields = ['id', 'url', 'name', 'slug', 'site_count']
+ model = models.Region
+ fields = ['id', 'url', 'name', 'slug', 'site_count', '_depth']
class NestedSiteSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
class Meta:
- model = Site
+ model = models.Site
fields = ['id', 'url', 'name', 'slug']
@@ -66,10 +68,11 @@ class NestedSiteSerializer(WritableNestedSerializer):
class NestedRackGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
rack_count = serializers.IntegerField(read_only=True)
+ _depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
- model = RackGroup
- fields = ['id', 'url', 'name', 'slug', 'rack_count']
+ model = models.RackGroup
+ fields = ['id', 'url', 'name', 'slug', 'rack_count', '_depth']
class NestedRackRoleSerializer(WritableNestedSerializer):
@@ -77,7 +80,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
rack_count = serializers.IntegerField(read_only=True)
class Meta:
- model = RackRole
+ model = models.RackRole
fields = ['id', 'url', 'name', 'slug', 'rack_count']
@@ -86,10 +89,22 @@ class NestedRackSerializer(WritableNestedSerializer):
device_count = serializers.IntegerField(read_only=True)
class Meta:
- model = Rack
+ model = models.Rack
fields = ['id', 'url', 'name', 'display_name', 'device_count']
+class NestedRackReservationSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
+ user = serializers.SerializerMethodField(read_only=True)
+
+ class Meta:
+ model = models.RackReservation
+ fields = ['id', 'url', 'user', 'units']
+
+ def get_user(self, obj):
+ return obj.user.username
+
+
#
# Device types
#
@@ -99,7 +114,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
devicetype_count = serializers.IntegerField(read_only=True)
class Meta:
- model = Manufacturer
+ model = models.Manufacturer
fields = ['id', 'url', 'name', 'slug', 'devicetype_count']
@@ -109,15 +124,47 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
device_count = serializers.IntegerField(read_only=True)
class Meta:
- model = DeviceType
+ model = models.DeviceType
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
+class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
+
+ class Meta:
+ model = models.ConsolePortTemplate
+ fields = ['id', 'url', 'name']
+
+
+class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
+
+ class Meta:
+ model = models.ConsoleServerPortTemplate
+ fields = ['id', 'url', 'name']
+
+
class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
class Meta:
- model = PowerPortTemplate
+ model = models.PowerPortTemplate
+ fields = ['id', 'url', 'name']
+
+
+class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
+
+ class Meta:
+ model = models.PowerOutletTemplate
+ fields = ['id', 'url', 'name']
+
+
+class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
+
+ class Meta:
+ model = models.InterfaceTemplate
fields = ['id', 'url', 'name']
@@ -125,7 +172,7 @@ class NestedRearPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
class Meta:
- model = RearPortTemplate
+ model = models.RearPortTemplate
fields = ['id', 'url', 'name']
@@ -133,7 +180,15 @@ class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
class Meta:
- model = FrontPortTemplate
+ model = models.FrontPortTemplate
+ fields = ['id', 'url', 'name']
+
+
+class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
+
+ class Meta:
+ model = models.DeviceBayTemplate
fields = ['id', 'url', 'name']
@@ -147,7 +202,7 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
virtualmachine_count = serializers.IntegerField(read_only=True)
class Meta:
- model = DeviceRole
+ model = models.DeviceRole
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count']
@@ -157,7 +212,7 @@ class NestedPlatformSerializer(WritableNestedSerializer):
virtualmachine_count = serializers.IntegerField(read_only=True)
class Meta:
- model = Platform
+ model = models.Platform
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count']
@@ -165,59 +220,53 @@ class NestedDeviceSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
class Meta:
- model = Device
+ model = models.Device
fields = ['id', 'url', 'name', 'display_name']
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer(read_only=True)
- connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
- model = ConsoleServerPort
- fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
+ model = models.ConsoleServerPort
+ fields = ['id', 'url', 'device', 'name', 'cable']
class NestedConsolePortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer(read_only=True)
- connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
- model = ConsolePort
- fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
+ model = models.ConsolePort
+ fields = ['id', 'url', 'device', 'name', 'cable']
class NestedPowerOutletSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer(read_only=True)
- connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
- model = PowerOutlet
- fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
+ model = models.PowerOutlet
+ fields = ['id', 'url', 'device', 'name', 'cable']
class NestedPowerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer(read_only=True)
- connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta:
- model = PowerPort
- fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
+ model = models.PowerPort
+ fields = ['id', 'url', 'device', 'name', 'cable']
class NestedInterfaceSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
- connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
- type = ChoiceField(choices=InterfaceTypeChoices)
class Meta:
- model = Interface
- fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status', 'type']
+ model = models.Interface
+ fields = ['id', 'url', 'device', 'name', 'cable']
class NestedRearPortSerializer(WritableNestedSerializer):
@@ -225,7 +274,7 @@ class NestedRearPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
class Meta:
- model = RearPort
+ model = models.RearPort
fields = ['id', 'url', 'device', 'name', 'cable']
@@ -234,7 +283,7 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
class Meta:
- model = FrontPort
+ model = models.FrontPort
fields = ['id', 'url', 'device', 'name', 'cable']
@@ -243,10 +292,20 @@ class NestedDeviceBaySerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True)
class Meta:
- model = DeviceBay
+ model = models.DeviceBay
fields = ['id', 'url', 'device', 'name']
+class NestedInventoryItemSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
+ device = NestedDeviceSerializer(read_only=True)
+ _depth = serializers.IntegerField(source='level', read_only=True)
+
+ class Meta:
+ model = models.InventoryItem
+ fields = ['id', 'url', 'device', 'name', '_depth']
+
+
#
# Cables
#
@@ -255,7 +314,7 @@ class NestedCableSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
class Meta:
- model = Cable
+ model = models.Cable
fields = ['id', 'url', 'label']
@@ -269,8 +328,8 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
member_count = serializers.IntegerField(read_only=True)
class Meta:
- model = VirtualChassis
- fields = ['id', 'url', 'master', 'member_count']
+ model = models.VirtualChassis
+ fields = ['id', 'name', 'url', 'master', 'member_count']
#
@@ -282,7 +341,7 @@ class NestedPowerPanelSerializer(WritableNestedSerializer):
powerfeed_count = serializers.IntegerField(read_only=True)
class Meta:
- model = PowerPanel
+ model = models.PowerPanel
fields = ['id', 'url', 'name', 'powerfeed_count']
@@ -290,5 +349,5 @@ class NestedPowerFeedSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
class Meta:
- model = PowerFeed
- fields = ['id', 'url', 'name']
+ model = models.PowerFeed
+ fields = ['id', 'url', 'name', 'cable']
diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index 11c1f5051..0b7f2f1b2 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -1,42 +1,63 @@
+from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
-from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from dcim.choices import *
from dcim.constants import *
from dcim.models import (
- Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
+ Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
)
+from dcim.utils import decompile_path_node
from extras.api.customfields import CustomFieldModelSerializer
+from extras.api.serializers import TaggedObjectSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
from ipam.models import VLAN
+from netbox.api import (
+ ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer,
+ WritableNestedSerializer,
+)
from tenancy.api.nested_serializers import NestedTenantSerializer
from users.api.nested_serializers import NestedUserSerializer
-from utilities.api import (
- ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer,
- WritableNestedSerializer, get_serializer_for_model,
-)
+from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import NestedClusterSerializer
from .nested_serializers import *
+class CableTerminationSerializer(serializers.ModelSerializer):
+ cable_peer_type = serializers.SerializerMethodField(read_only=True)
+ cable_peer = serializers.SerializerMethodField(read_only=True)
+
+ def get_cable_peer_type(self, obj):
+ if obj._cable_peer is not None:
+ return f'{obj._cable_peer._meta.app_label}.{obj._cable_peer._meta.model_name}'
+ return None
+
+ @swagger_serializer_method(serializer_or_field=serializers.DictField)
+ def get_cable_peer(self, obj):
+ """
+ Return the appropriate serializer for the cable termination model.
+ """
+ if obj._cable_peer is not None:
+ serializer = get_serializer_for_model(obj._cable_peer, prefix='Nested')
+ context = {'request': self.context['request']}
+ return serializer(obj._cable_peer, context=context).data
+ return None
+
+
class ConnectedEndpointSerializer(ValidatedModelSerializer):
connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
connected_endpoint = serializers.SerializerMethodField(read_only=True)
- connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
+ connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
def get_connected_endpoint_type(self, obj):
- if hasattr(obj, 'connected_endpoint') and obj.connected_endpoint is not None:
- return '{}.{}'.format(
- obj.connected_endpoint._meta.app_label,
- obj.connected_endpoint._meta.model_name
- )
+ if obj._path is not None and obj._path.destination is not None:
+ return f'{obj._path.destination._meta.app_label}.{obj._path.destination._meta.model_name}'
return None
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@@ -44,14 +65,17 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer):
"""
Return the appropriate serializer for the type of connected object.
"""
- if getattr(obj, 'connected_endpoint', None) is None:
- return None
+ if obj._path is not None and obj._path.destination is not None:
+ serializer = get_serializer_for_model(obj._path.destination, prefix='Nested')
+ context = {'request': self.context['request']}
+ return serializer(obj._path.destination, context=context).data
+ return None
- serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested')
- context = {'request': self.context['request']}
- data = serializer(obj.connected_endpoint, context=context).data
-
- return data
+ @swagger_serializer_method(serializer_or_field=serializers.BooleanField)
+ def get_connected_endpoint_reachable(self, obj):
+ if obj._path is not None:
+ return obj._path.is_active
+ return None
#
@@ -59,20 +83,22 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer):
#
class RegionSerializer(CustomFieldModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
parent = NestedRegionSerializer(required=False, allow_null=True)
site_count = serializers.IntegerField(read_only=True)
+ _depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = Region
- fields = ['id', 'name', 'slug', 'parent', 'description', 'site_count', 'custom_fields']
+ fields = ['id', 'url', 'name', 'slug', 'parent', 'description', 'site_count', 'custom_fields', '_depth']
-class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
+class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
status = ChoiceField(choices=SiteStatusChoices, required=False)
region = NestedRegionSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneField(required=False)
- tags = TagListSerializerField(required=False)
circuit_count = serializers.IntegerField(read_only=True)
device_count = serializers.IntegerField(read_only=True)
prefix_count = serializers.IntegerField(read_only=True)
@@ -83,7 +109,7 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
class Meta:
model = Site
fields = [
- 'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
+ 'id', 'url', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
@@ -95,24 +121,28 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
#
class RackGroupSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
site = NestedSiteSerializer()
parent = NestedRackGroupSerializer(required=False, allow_null=True)
rack_count = serializers.IntegerField(read_only=True)
+ _depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = RackGroup
- fields = ['id', 'name', 'slug', 'site', 'parent', 'description', 'rack_count']
+ fields = ['id', 'url', 'name', 'slug', 'site', 'parent', 'description', 'rack_count', '_depth']
class RackRoleSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True)
class Meta:
model = RackRole
- fields = ['id', 'name', 'slug', 'color', 'description', 'rack_count']
+ fields = ['id', 'url', 'name', 'slug', 'color', 'description', 'rack_count']
-class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
+class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
site = NestedSiteSerializer()
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
tenant = NestedTenantSerializer(required=False, allow_null=True)
@@ -121,14 +151,13 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
width = ChoiceField(choices=RackWidthChoices, required=False)
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
- tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True)
powerfeed_count = serializers.IntegerField(read_only=True)
class Meta:
model = Rack
fields = [
- 'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial',
+ 'id', 'url', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
]
@@ -159,16 +188,18 @@ class RackUnitSerializer(serializers.Serializer):
name = serializers.CharField(read_only=True)
face = ChoiceField(choices=DeviceFaceChoices, read_only=True)
device = NestedDeviceSerializer(read_only=True)
+ occupied = serializers.BooleanField(read_only=True)
-class RackReservationSerializer(ValidatedModelSerializer):
+class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
rack = NestedRackSerializer()
user = NestedUserSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
class Meta:
model = RackReservation
- fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
+ fields = ['id', 'url', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags', 'custom_fields']
class RackElevationDetailFilterSerializer(serializers.Serializer):
@@ -185,10 +216,10 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
default=RackElevationDetailRenderChoices.RENDER_JSON
)
unit_width = serializers.IntegerField(
- default=RACK_ELEVATION_UNIT_WIDTH_DEFAULT
+ default=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH
)
unit_height = serializers.IntegerField(
- default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
+ default=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT
)
legend_width = serializers.IntegerField(
default=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
@@ -212,6 +243,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
#
class ManufacturerSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True)
inventoryitem_count = serializers.IntegerField(read_only=True)
platform_count = serializers.IntegerField(read_only=True)
@@ -219,26 +251,27 @@ class ManufacturerSerializer(ValidatedModelSerializer):
class Meta:
model = Manufacturer
fields = [
- 'id', 'name', 'slug', 'description', 'devicetype_count', 'inventoryitem_count', 'platform_count',
+ 'id', 'url', 'name', 'slug', 'description', 'devicetype_count', 'inventoryitem_count', 'platform_count',
]
-class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
+class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer()
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
- tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True)
class Meta:
model = DeviceType
fields = [
- 'id', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth',
+ 'id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth',
'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created',
'last_updated', 'device_count',
]
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
@@ -248,10 +281,11 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsolePortTemplate
- fields = ['id', 'device_type', 'name', 'type']
+ fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'description']
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
@@ -261,10 +295,11 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = ConsoleServerPortTemplate
- fields = ['id', 'device_type', 'name', 'type']
+ fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'description']
class PowerPortTemplateSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=PowerPortTypeChoices,
@@ -274,10 +309,11 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerPortTemplate
- fields = ['id', 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw']
+ fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description']
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=PowerOutletTypeChoices,
@@ -295,43 +331,47 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerOutletTemplate
- fields = ['id', 'device_type', 'name', 'type', 'power_port', 'feed_leg']
+ fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description']
class InterfaceTemplateSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=InterfaceTypeChoices)
class Meta:
model = InterfaceTemplate
- fields = ['id', 'device_type', 'name', 'type', 'mgmt_only']
+ fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description']
class RearPortTemplateSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=PortTypeChoices)
class Meta:
model = RearPortTemplate
- fields = ['id', 'device_type', 'name', 'type', 'positions']
+ fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'positions', 'description']
class FrontPortTemplateSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=PortTypeChoices)
rear_port = NestedRearPortTemplateSerializer()
class Meta:
model = FrontPortTemplate
- fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position']
+ fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description']
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
device_type = NestedDeviceTypeSerializer()
class Meta:
model = DeviceBayTemplate
- fields = ['id', 'device_type', 'name']
+ fields = ['id', 'url', 'device_type', 'name', 'label', 'description']
#
@@ -339,17 +379,19 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
#
class DeviceRoleSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
class Meta:
model = DeviceRole
fields = [
- 'id', 'name', 'slug', 'color', 'vm_role', 'description', 'device_count', 'virtualmachine_count',
+ 'id', 'url', 'name', 'slug', 'color', 'vm_role', 'description', 'device_count', 'virtualmachine_count',
]
class PlatformSerializer(ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
@@ -357,12 +399,13 @@ class PlatformSerializer(ValidatedModelSerializer):
class Meta:
model = Platform
fields = [
- 'id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'device_count',
+ 'id', 'url', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'device_count',
'virtualmachine_count',
]
-class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
+class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
device_type = NestedDeviceTypeSerializer()
device_role = NestedDeviceRoleSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
@@ -377,15 +420,14 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
parent_device = serializers.SerializerMethodField()
cluster = NestedClusterSerializer(required=False, allow_null=True)
virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True)
- tags = TagListSerializerField(required=False)
class Meta:
model = Device
fields = [
- 'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
- 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
- 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'tags',
- 'custom_fields', 'created', 'last_updated',
+ 'id', 'url', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
+ 'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
+ 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
+ 'tags', 'custom_fields', 'created', 'last_updated',
]
validators = []
@@ -418,10 +460,10 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
class Meta(DeviceSerializer.Meta):
fields = [
- 'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
- 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
- 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'tags',
- 'custom_fields', 'config_context', 'created', 'last_updated',
+ 'id', 'url', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
+ 'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
+ 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data',
+ 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
@@ -433,7 +475,8 @@ class DeviceNAPALMSerializer(serializers.Serializer):
method = serializers.DictField()
-class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
+class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
@@ -441,17 +484,17 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
required=False
)
cable = NestedCableSerializer(read_only=True)
- tags = TagListSerializerField(required=False)
class Meta:
model = ConsoleServerPort
fields = [
- 'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
- 'connection_status', 'cable', 'tags',
+ 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type',
+ 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
]
-class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
+class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
@@ -459,17 +502,17 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
required=False
)
cable = NestedCableSerializer(read_only=True)
- tags = TagListSerializerField(required=False)
class Meta:
model = ConsolePort
fields = [
- 'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
- 'connection_status', 'cable', 'tags',
+ 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type',
+ 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
]
-class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
+class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer()
type = ChoiceField(
choices=PowerOutletTypeChoices,
@@ -487,19 +530,18 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
cable = NestedCableSerializer(
read_only=True
)
- tags = TagListSerializerField(
- required=False
- )
class Meta:
model = PowerOutlet
fields = [
- 'id', 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
- 'connected_endpoint', 'connection_status', 'cable', 'tags',
+ 'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable',
+ 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
+ 'connected_endpoint_reachable', 'tags',
]
-class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
+class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer()
type = ChoiceField(
choices=PowerPortTypeChoices,
@@ -507,17 +549,18 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
required=False
)
cable = NestedCableSerializer(read_only=True)
- tags = TagListSerializerField(required=False)
class Meta:
model = PowerPort
fields = [
- 'id', 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
- 'connected_endpoint', 'connection_status', 'cable', 'tags',
+ 'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable',
+ 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
+ 'connected_endpoint_reachable', 'tags',
]
-class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
+class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer()
type = ChoiceField(choices=InterfaceTypeChoices)
lag = NestedInterfaceSerializer(required=False, allow_null=True)
@@ -530,47 +573,43 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
many=True
)
cable = NestedCableSerializer(read_only=True)
- tags = TagListSerializerField(required=False)
count_ipaddresses = serializers.IntegerField(read_only=True)
class Meta:
model = Interface
fields = [
- 'id', 'device', 'name', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
- 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan',
- 'tagged_vlans', 'tags', 'count_ipaddresses',
+ 'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
+ 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'cable', 'cable_peer', 'cable_peer_type',
+ 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
+ 'count_ipaddresses',
]
- # TODO: This validation should be handled by Interface.clean()
def validate(self, data):
- # All associated VLANs be global or assigned to the parent device's site.
+ # Validate many-to-many VLAN assignments
device = self.instance.device if self.instance else data.get('device')
- untagged_vlan = data.get('untagged_vlan')
- if untagged_vlan and untagged_vlan.site not in [device.site, None]:
- raise serializers.ValidationError({
- 'untagged_vlan': "VLAN {} must belong to the same site as the interface's parent device, or it must be "
- "global.".format(untagged_vlan)
- })
for vlan in data.get('tagged_vlans', []):
if vlan.site not in [device.site, None]:
raise serializers.ValidationError({
- 'tagged_vlans': "VLAN {} must belong to the same site as the interface's parent device, or it must "
- "be global.".format(vlan)
+ 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, or "
+ f"it must be global."
})
return super().validate(data)
-class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer):
+class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices)
cable = NestedCableSerializer(read_only=True)
- tags = TagListSerializerField(required=False)
class Meta:
model = RearPort
- fields = ['id', 'device', 'name', 'type', 'positions', 'description', 'cable', 'tags']
+ fields = [
+ 'id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer',
+ 'cable_peer_type', 'tags',
+ ]
class FrontPortRearPortSerializer(WritableNestedSerializer):
@@ -581,47 +620,51 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
class Meta:
model = RearPort
- fields = ['id', 'url', 'name']
+ fields = ['id', 'url', 'name', 'label']
-class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer):
+class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices)
rear_port = FrontPortRearPortSerializer()
cable = NestedCableSerializer(read_only=True)
- tags = TagListSerializerField(required=False)
class Meta:
model = FrontPort
- fields = ['id', 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags']
+ fields = [
+ 'id', 'url', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable',
+ 'cable_peer', 'cable_peer_type', 'tags',
+ ]
-class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
+class DeviceBaySerializer(TaggedObjectSerializer, ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = NestedDeviceSerializer()
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
- tags = TagListSerializerField(required=False)
class Meta:
model = DeviceBay
- fields = ['id', 'device', 'name', 'description', 'installed_device', 'tags']
+ fields = ['id', 'url', 'device', 'name', 'label', 'description', 'installed_device', 'tags']
#
# Inventory items
#
-class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
+class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
device = NestedDeviceSerializer()
# Provide a default value to satisfy UniqueTogetherValidator
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None)
- tags = TagListSerializerField(required=False)
+ _depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = InventoryItem
fields = [
- 'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
- 'description', 'tags',
+ 'id', 'url', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag',
+ 'discovered', 'description', 'tags', '_depth',
]
@@ -629,7 +672,8 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
# Cables
#
-class CableSerializer(ValidatedModelSerializer):
+class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
termination_a_type = ContentTypeField(
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
)
@@ -644,8 +688,9 @@ class CableSerializer(ValidatedModelSerializer):
class Meta:
model = Cable
fields = [
- 'id', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'termination_b_id',
- 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit',
+ 'id', 'url', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
+ 'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
+ 'custom_fields',
]
def _get_termination(self, obj, side):
@@ -685,6 +730,49 @@ class TracedCableSerializer(serializers.ModelSerializer):
]
+class CablePathSerializer(serializers.ModelSerializer):
+ origin_type = ContentTypeField(read_only=True)
+ origin = serializers.SerializerMethodField(read_only=True)
+ destination_type = ContentTypeField(read_only=True)
+ destination = serializers.SerializerMethodField(read_only=True)
+ path = serializers.SerializerMethodField(read_only=True)
+
+ class Meta:
+ model = CablePath
+ fields = [
+ 'id', 'origin_type', 'origin', 'destination_type', 'destination', 'path', 'is_active', 'is_split',
+ ]
+
+ @swagger_serializer_method(serializer_or_field=serializers.DictField)
+ def get_origin(self, obj):
+ """
+ Return the appropriate serializer for the origin.
+ """
+ serializer = get_serializer_for_model(obj.origin, prefix='Nested')
+ context = {'request': self.context['request']}
+ return serializer(obj.origin, context=context).data
+
+ @swagger_serializer_method(serializer_or_field=serializers.DictField)
+ def get_destination(self, obj):
+ """
+ Return the appropriate serializer for the destination, if any.
+ """
+ if obj.destination_id is not None:
+ serializer = get_serializer_for_model(obj.destination, prefix='Nested')
+ context = {'request': self.context['request']}
+ return serializer(obj.destination, context=context).data
+ return None
+
+ @swagger_serializer_method(serializer_or_field=serializers.ListField)
+ def get_path(self, obj):
+ ret = []
+ for node in obj.get_path():
+ serializer = get_serializer_for_model(node, prefix='Nested')
+ context = {'request': self.context['request']}
+ ret.append(serializer(node, context=context).data)
+ return ret
+
+
#
# Interface connections
#
@@ -692,37 +780,44 @@ class TracedCableSerializer(serializers.ModelSerializer):
class InterfaceConnectionSerializer(ValidatedModelSerializer):
interface_a = serializers.SerializerMethodField()
interface_b = NestedInterfaceSerializer(source='connected_endpoint')
- connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
+ connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Interface
- fields = ['interface_a', 'interface_b', 'connection_status']
+ fields = ['interface_a', 'interface_b', 'connected_endpoint_reachable']
@swagger_serializer_method(serializer_or_field=NestedInterfaceSerializer)
def get_interface_a(self, obj):
context = {'request': self.context['request']}
return NestedInterfaceSerializer(instance=obj, context=context).data
+ @swagger_serializer_method(serializer_or_field=serializers.BooleanField)
+ def get_connected_endpoint_reachable(self, obj):
+ if obj._path is not None:
+ return obj._path.is_active
+ return None
+
#
# Virtual chassis
#
-class VirtualChassisSerializer(TaggitSerializer, ValidatedModelSerializer):
- master = NestedDeviceSerializer()
- tags = TagListSerializerField(required=False)
+class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
+ master = NestedDeviceSerializer(required=False)
member_count = serializers.IntegerField(read_only=True)
class Meta:
model = VirtualChassis
- fields = ['id', 'master', 'domain', 'tags', 'member_count']
+ fields = ['id', 'url', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
#
# Power panels
#
-class PowerPanelSerializer(ValidatedModelSerializer):
+class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
site = NestedSiteSerializer()
rack_group = NestedRackGroupSerializer(
required=False,
@@ -733,10 +828,16 @@ class PowerPanelSerializer(ValidatedModelSerializer):
class Meta:
model = PowerPanel
- fields = ['id', 'site', 'rack_group', 'name', 'powerfeed_count']
+ fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'custom_fields', 'powerfeed_count']
-class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
+class PowerFeedSerializer(
+ TaggedObjectSerializer,
+ CableTerminationSerializer,
+ ConnectedEndpointSerializer,
+ CustomFieldModelSerializer
+):
+ url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
power_panel = NestedPowerPanelSerializer()
rack = NestedRackSerializer(
required=False,
@@ -759,13 +860,13 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
choices=PowerFeedPhaseChoices,
default=PowerFeedPhaseChoices.PHASE_SINGLE
)
- tags = TagListSerializerField(
- required=False
- )
+ cable = NestedCableSerializer(read_only=True)
class Meta:
model = PowerFeed
fields = [
- 'id', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
- 'max_utilization', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+ 'id', 'url', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
+ 'max_utilization', 'comments', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint',
+ 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
+ 'last_updated',
]
diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py
index f989d817c..689cb7aa1 100644
--- a/netbox/dcim/api/urls.py
+++ b/netbox/dcim/api/urls.py
@@ -1,18 +1,9 @@
-from rest_framework import routers
-
+from netbox.api import OrderedDefaultRouter
from . import views
-class DCIMRootView(routers.APIRootView):
- """
- DCIM API root view
- """
- def get_view_name(self):
- return 'DCIM'
-
-
-router = routers.DefaultRouter()
-router.APIRootView = DCIMRootView
+router = OrderedDefaultRouter()
+router.APIRootView = views.DCIMRootView
# Sites
router.register('regions', views.RegionViewSet)
diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py
index 10f31b1eb..379873ade 100644
--- a/netbox/dcim/api/views.py
+++ b/netbox/dcim/api/views.py
@@ -1,7 +1,8 @@
+import socket
from collections import OrderedDict
from django.conf import settings
-from django.db.models import Count, F
+from django.db.models import F
from django.http import HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404
from drf_yasg import openapi
@@ -10,45 +11,57 @@ from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.mixins import ListModelMixin
from rest_framework.response import Response
+from rest_framework.routers import APIRootView
from rest_framework.viewsets import GenericViewSet, ViewSet
from circuits.models import Circuit
from dcim import filters
from dcim.models import (
- Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
+ Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
)
-from extras.api.serializers import RenderedGraphSerializer
-from extras.api.views import CustomFieldModelViewSet
-from extras.models import Graph
+from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
from ipam.models import Prefix, VLAN
-from utilities.api import (
- get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable,
-)
-from utilities.utils import get_subquery
+from netbox.api.views import ModelViewSet
+from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
+from netbox.api.exceptions import ServiceUnavailable
+from netbox.api.metadata import ContentTypeMetadata
+from utilities.api import get_serializer_for_model
+from utilities.utils import count_related
from virtualization.models import VirtualMachine
from . import serializers
from .exceptions import MissingFilterException
+class DCIMRootView(APIRootView):
+ """
+ DCIM API root view
+ """
+ def get_view_name(self):
+ return 'DCIM'
+
+
# Mixins
-class CableTraceMixin(object):
+class PathEndpointMixin(object):
@action(detail=True, url_path='trace')
def trace(self, request, pk):
"""
Trace a complete cable path and return each segment as a three-tuple of (termination, cable, termination).
"""
- obj = get_object_or_404(self.queryset.model, pk=pk)
+ obj = get_object_or_404(self.queryset, pk=pk)
# Initialize the path array
path = []
- for near_end, cable, far_end in obj.trace()[0]:
+ for near_end, cable, far_end in obj.trace():
+ if near_end is None:
+ # Split paths
+ break
# Serialize each object
serializer_a = get_serializer_for_model(near_end, prefix='Nested')
@@ -68,13 +81,31 @@ class CableTraceMixin(object):
return Response(path)
+class PassThroughPortMixin(object):
+
+ @action(detail=True, url_path='paths')
+ def paths(self, request, pk):
+ """
+ Return all CablePaths which traverse a given pass-through port.
+ """
+ obj = get_object_or_404(self.queryset, pk=pk)
+ cablepaths = CablePath.objects.filter(path__contains=obj).prefetch_related('origin', 'destination')
+ serializer = serializers.CablePathSerializer(cablepaths, context={'request': request}, many=True)
+
+ return Response(serializer.data)
+
+
#
# Regions
#
class RegionViewSet(CustomFieldModelViewSet):
- queryset = Region.objects.annotate(
- site_count=Count('sites')
+ queryset = Region.objects.add_related_count(
+ Region.objects.all(),
+ Site,
+ 'region',
+ 'site_count',
+ cumulative=True
)
serializer_class = serializers.RegionSerializer
filterset_class = filters.RegionFilterSet
@@ -88,35 +119,29 @@ class SiteViewSet(CustomFieldModelViewSet):
queryset = Site.objects.prefetch_related(
'region', 'tenant', 'tags'
).annotate(
- device_count=get_subquery(Device, 'site'),
- rack_count=get_subquery(Rack, 'site'),
- prefix_count=get_subquery(Prefix, 'site'),
- vlan_count=get_subquery(VLAN, 'site'),
- circuit_count=get_subquery(Circuit, 'terminations__site'),
- virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'),
+ device_count=count_related(Device, 'site'),
+ rack_count=count_related(Rack, 'site'),
+ prefix_count=count_related(Prefix, 'site'),
+ vlan_count=count_related(VLAN, 'site'),
+ circuit_count=count_related(Circuit, 'terminations__site'),
+ virtualmachine_count=count_related(VirtualMachine, 'cluster__site')
)
serializer_class = serializers.SiteSerializer
filterset_class = filters.SiteFilterSet
- @action(detail=True)
- def graphs(self, request, pk):
- """
- A convenience method for rendering graphs for a particular site.
- """
- site = get_object_or_404(Site, pk=pk)
- queryset = Graph.objects.filter(type__model='site')
- serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
- return Response(serializer.data)
-
#
# Rack groups
#
class RackGroupViewSet(ModelViewSet):
- queryset = RackGroup.objects.prefetch_related('site').annotate(
- rack_count=Count('racks')
- )
+ queryset = RackGroup.objects.add_related_count(
+ RackGroup.objects.all(),
+ Rack,
+ 'group',
+ 'rack_count',
+ cumulative=True
+ ).prefetch_related('site')
serializer_class = serializers.RackGroupSerializer
filterset_class = filters.RackGroupFilterSet
@@ -127,7 +152,7 @@ class RackGroupViewSet(ModelViewSet):
class RackRoleViewSet(ModelViewSet):
queryset = RackRole.objects.annotate(
- rack_count=Count('racks')
+ rack_count=count_related(Rack, 'role')
)
serializer_class = serializers.RackRoleSerializer
filterset_class = filters.RackRoleFilterSet
@@ -141,8 +166,8 @@ class RackViewSet(CustomFieldModelViewSet):
queryset = Rack.objects.prefetch_related(
'site', 'group__site', 'role', 'tenant', 'tags'
).annotate(
- device_count=get_subquery(Device, 'rack'),
- powerfeed_count=get_subquery(PowerFeed, 'rack')
+ device_count=count_related(Device, 'rack'),
+ powerfeed_count=count_related(PowerFeed, 'rack')
)
serializer_class = serializers.RackSerializer
filterset_class = filters.RackFilterSet
@@ -156,7 +181,7 @@ class RackViewSet(CustomFieldModelViewSet):
"""
Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
"""
- rack = get_object_or_404(Rack, pk=pk)
+ rack = get_object_or_404(self.queryset, pk=pk)
serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
if not serializer.is_valid():
return Response(serializer.errors, 400)
@@ -166,6 +191,7 @@ class RackViewSet(CustomFieldModelViewSet):
# Render and return the elevation as an SVG drawing with the correct content type
drawing = rack.get_elevation_svg(
face=data['face'],
+ user=request.user,
unit_width=data['unit_width'],
unit_height=data['unit_height'],
legend_width=data['legend_width'],
@@ -178,6 +204,7 @@ class RackViewSet(CustomFieldModelViewSet):
# Return a JSON representation of the rack units in the elevation
elevation = rack.get_rack_units(
face=data['face'],
+ user=request.user,
exclude=data['exclude'],
expand_devices=data['expand_devices']
)
@@ -213,9 +240,9 @@ class RackReservationViewSet(ModelViewSet):
class ManufacturerViewSet(ModelViewSet):
queryset = Manufacturer.objects.annotate(
- devicetype_count=get_subquery(DeviceType, 'manufacturer'),
- inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'),
- platform_count=get_subquery(Platform, 'manufacturer')
+ devicetype_count=count_related(DeviceType, 'manufacturer'),
+ inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
+ platform_count=count_related(Platform, 'manufacturer')
)
serializer_class = serializers.ManufacturerSerializer
filterset_class = filters.ManufacturerFilterSet
@@ -226,11 +253,12 @@ class ManufacturerViewSet(ModelViewSet):
#
class DeviceTypeViewSet(CustomFieldModelViewSet):
- queryset = DeviceType.objects.prefetch_related('manufacturer').prefetch_related('tags').annotate(
- device_count=Count('instances')
+ queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
+ device_count=count_related(Device, 'device_type')
)
serializer_class = serializers.DeviceTypeSerializer
filterset_class = filters.DeviceTypeFilterSet
+ brief_prefetch_fields = ['manufacturer']
#
@@ -291,8 +319,8 @@ class DeviceBayTemplateViewSet(ModelViewSet):
class DeviceRoleViewSet(ModelViewSet):
queryset = DeviceRole.objects.annotate(
- device_count=get_subquery(Device, 'device_role'),
- virtualmachine_count=get_subquery(VirtualMachine, 'role')
+ device_count=count_related(Device, 'device_role'),
+ virtualmachine_count=count_related(VirtualMachine, 'role')
)
serializer_class = serializers.DeviceRoleSerializer
filterset_class = filters.DeviceRoleFilterSet
@@ -304,8 +332,8 @@ class DeviceRoleViewSet(ModelViewSet):
class PlatformViewSet(ModelViewSet):
queryset = Platform.objects.annotate(
- device_count=get_subquery(Device, 'platform'),
- virtualmachine_count=get_subquery(VirtualMachine, 'platform')
+ device_count=count_related(Device, 'platform'),
+ virtualmachine_count=count_related(VirtualMachine, 'platform')
)
serializer_class = serializers.PlatformSerializer
filterset_class = filters.PlatformFilterSet
@@ -315,7 +343,7 @@ class PlatformViewSet(ModelViewSet):
# Devices
#
-class DeviceViewSet(CustomFieldModelViewSet):
+class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
queryset = Device.objects.prefetch_related(
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
@@ -342,17 +370,6 @@ class DeviceViewSet(CustomFieldModelViewSet):
return serializers.DeviceWithConfigContextSerializer
- @action(detail=True)
- def graphs(self, request, pk):
- """
- A convenience method for rendering graphs for a particular Device.
- """
- device = get_object_or_404(Device, pk=pk)
- queryset = Graph.objects.filter(type__model='device')
- serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
-
- return Response(serializer.data)
-
@swagger_auto_schema(
manual_parameters=[
Parameter(
@@ -369,22 +386,40 @@ class DeviceViewSet(CustomFieldModelViewSet):
"""
Execute a NAPALM method on a Device
"""
- device = get_object_or_404(Device, pk=pk)
+ device = get_object_or_404(self.queryset, pk=pk)
if not device.primary_ip:
raise ServiceUnavailable("This device does not have a primary IP address configured.")
if device.platform is None:
raise ServiceUnavailable("No platform is configured for this device.")
if not device.platform.napalm_driver:
- raise ServiceUnavailable("No NAPALM driver is configured for this device's platform ().".format(
- device.platform
- ))
+ raise ServiceUnavailable(f"No NAPALM driver is configured for this device's platform: {device.platform}.")
+
+ # Check for primary IP address from NetBox object
+ if device.primary_ip:
+ host = str(device.primary_ip.address.ip)
+ else:
+ # Raise exception for no IP address and no Name if device.name does not exist
+ if not device.name:
+ raise ServiceUnavailable(
+ "This device does not have a primary IP address or device name to lookup configured."
+ )
+ try:
+ # Attempt to complete a DNS name resolution if no primary_ip is set
+ host = socket.gethostbyname(device.name)
+ except socket.gaierror:
+ # Name lookup failure
+ raise ServiceUnavailable(
+ f"Name lookup failure, unable to resolve IP address for {device.name}. Please set Primary IP or "
+ f"setup name resolution.")
# Check that NAPALM is installed
try:
import napalm
from napalm.base.exceptions import ModuleImportError
- except ImportError:
- raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
+ except ModuleNotFoundError as e:
+ if getattr(e, 'name') == 'napalm':
+ raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
+ raise e
# Validate the configured driver
try:
@@ -395,13 +430,11 @@ class DeviceViewSet(CustomFieldModelViewSet):
))
# Verify user permission
- if not request.user.has_perm('dcim.napalm_read'):
+ if not request.user.has_perm('dcim.napalm_read_device'):
return HttpResponseForbidden()
- # Connect to the device
napalm_methods = request.GET.getlist('method')
response = OrderedDict([(m, None) for m in napalm_methods])
- ip_address = str(device.primary_ip.address.ip)
username = settings.NAPALM_USERNAME
password = settings.NAPALM_PASSWORD
optional_args = settings.NAPALM_ARGS.copy()
@@ -421,8 +454,9 @@ class DeviceViewSet(CustomFieldModelViewSet):
elif key:
optional_args[key.lower()] = request.headers[header]
+ # Connect to the device
d = driver(
- hostname=ip_address,
+ hostname=host,
username=username,
password=password,
timeout=settings.NAPALM_TIMEOUT,
@@ -431,7 +465,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
try:
d.open()
except Exception as e:
- raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
+ raise ServiceUnavailable("Error connecting to the device at {}: {}".format(host, e))
# Validate and execute each specified NAPALM method
for method in napalm_methods:
@@ -456,74 +490,71 @@ class DeviceViewSet(CustomFieldModelViewSet):
# Device components
#
-class ConsolePortViewSet(CableTraceMixin, ModelViewSet):
- queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
+class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
+ queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
serializer_class = serializers.ConsolePortSerializer
filterset_class = filters.ConsolePortFilterSet
+ brief_prefetch_fields = ['device']
-class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet):
- queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
+class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
+ queryset = ConsoleServerPort.objects.prefetch_related(
+ 'device', '_path__destination', 'cable', '_cable_peer', 'tags'
+ )
serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filters.ConsoleServerPortFilterSet
+ brief_prefetch_fields = ['device']
-class PowerPortViewSet(CableTraceMixin, ModelViewSet):
- queryset = PowerPort.objects.prefetch_related(
- 'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags'
- )
+class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
+ queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerPortFilterSet
+ brief_prefetch_fields = ['device']
-class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
- queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
+class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
+ queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
serializer_class = serializers.PowerOutletSerializer
filterset_class = filters.PowerOutletFilterSet
+ brief_prefetch_fields = ['device']
-class InterfaceViewSet(CableTraceMixin, ModelViewSet):
+class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
queryset = Interface.objects.prefetch_related(
- 'device', '_connected_interface', '_connected_circuittermination', 'cable', 'ip_addresses', 'tags'
- ).filter(
- device__isnull=False
+ 'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
)
serializer_class = serializers.InterfaceSerializer
filterset_class = filters.InterfaceFilterSet
-
- @action(detail=True)
- def graphs(self, request, pk):
- """
- A convenience method for rendering graphs for a particular interface.
- """
- interface = get_object_or_404(Interface, pk=pk)
- queryset = Graph.objects.filter(type__model='interface')
- serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
- return Response(serializer.data)
+ brief_prefetch_fields = ['device']
-class FrontPortViewSet(ModelViewSet):
+class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
serializer_class = serializers.FrontPortSerializer
filterset_class = filters.FrontPortFilterSet
+ brief_prefetch_fields = ['device']
-class RearPortViewSet(ModelViewSet):
+class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
serializer_class = serializers.RearPortSerializer
filterset_class = filters.RearPortFilterSet
+ brief_prefetch_fields = ['device']
class DeviceBayViewSet(ModelViewSet):
queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags')
serializer_class = serializers.DeviceBaySerializer
filterset_class = filters.DeviceBayFilterSet
+ brief_prefetch_fields = ['device']
class InventoryItemViewSet(ModelViewSet):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags')
serializer_class = serializers.InventoryItemSerializer
filterset_class = filters.InventoryItemFilterSet
+ brief_prefetch_fields = ['device']
#
@@ -531,32 +562,26 @@ class InventoryItemViewSet(ModelViewSet):
#
class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
- queryset = ConsolePort.objects.prefetch_related(
- 'device', 'connected_endpoint__device'
- ).filter(
- connected_endpoint__isnull=False
+ queryset = ConsolePort.objects.prefetch_related('device', '_path').filter(
+ _path__destination_id__isnull=False
)
serializer_class = serializers.ConsolePortSerializer
filterset_class = filters.ConsoleConnectionFilterSet
class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
- queryset = PowerPort.objects.prefetch_related(
- 'device', 'connected_endpoint__device'
- ).filter(
- _connected_poweroutlet__isnull=False
+ queryset = PowerPort.objects.prefetch_related('device', '_path').filter(
+ _path__destination_id__isnull=False
)
serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerConnectionFilterSet
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
- queryset = Interface.objects.prefetch_related(
- 'device', '_connected_interface__device'
- ).filter(
+ queryset = Interface.objects.prefetch_related('device', '_path').filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair
- _connected_interface__isnull=False,
- pk__lt=F('_connected_interface')
+ _path__destination_id__isnull=False,
+ pk__lt=F('_path__destination_id')
)
serializer_class = serializers.InterfaceConnectionSerializer
filterset_class = filters.InterfaceConnectionFilterSet
@@ -567,6 +592,7 @@ class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
#
class CableViewSet(ModelViewSet):
+ metadata_class = ContentTypeMetadata
queryset = Cable.objects.prefetch_related(
'termination_a', 'termination_b'
)
@@ -580,10 +606,11 @@ class CableViewSet(ModelViewSet):
class VirtualChassisViewSet(ModelViewSet):
queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
- member_count=Count('members')
+ member_count=count_related(Device, 'virtual_chassis')
)
serializer_class = serializers.VirtualChassisSerializer
filterset_class = filters.VirtualChassisFilterSet
+ brief_prefetch_fields = ['master']
#
@@ -594,7 +621,7 @@ class PowerPanelViewSet(ModelViewSet):
queryset = PowerPanel.objects.prefetch_related(
'site', 'rack_group'
).annotate(
- powerfeed_count=Count('powerfeeds')
+ powerfeed_count=count_related(PowerFeed, 'power_panel')
)
serializer_class = serializers.PowerPanelSerializer
filterset_class = filters.PowerPanelFilterSet
@@ -604,8 +631,10 @@ class PowerPanelViewSet(ModelViewSet):
# Power feeds
#
-class PowerFeedViewSet(CustomFieldModelViewSet):
- queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', 'tags')
+class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
+ queryset = PowerFeed.objects.prefetch_related(
+ 'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags'
+ )
serializer_class = serializers.PowerFeedSerializer
filterset_class = filters.PowerFeedFilterSet
@@ -655,8 +684,12 @@ class ConnectedDeviceViewSet(ViewSet):
raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
# Determine local interface from peer interface's connection
- peer_interface = get_object_or_404(Interface, device__name=peer_device_name, name=peer_interface_name)
- local_interface = peer_interface._connected_interface
+ peer_interface = get_object_or_404(
+ Interface.objects.all(),
+ device__name=peer_device_name,
+ name=peer_interface_name
+ )
+ local_interface = peer_interface.connected_endpoint
if local_interface is None:
return Response()
diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py
index e79222449..436fb0a04 100644
--- a/netbox/dcim/choices.py
+++ b/netbox/dcim/choices.py
@@ -7,26 +7,30 @@ from utilities.choices import ChoiceSet
class SiteStatusChoices(ChoiceSet):
- STATUS_ACTIVE = 'active'
STATUS_PLANNED = 'planned'
+ STATUS_STAGING = 'staging'
+ STATUS_ACTIVE = 'active'
+ STATUS_DECOMMISSIONING = 'decommissioning'
STATUS_RETIRED = 'retired'
STATUS_CONSTRUCTION = 'construction'
STATUS_COMMISSIONING = 'commissioning'
CHOICES = (
- (STATUS_ACTIVE, 'Active'),
(STATUS_PLANNED, 'Planned'),
+ (STATUS_STAGING, 'Staging'),
+ (STATUS_ACTIVE, 'Active'),
+ (STATUS_DECOMMISSIONING, 'Decommissioning'),
(STATUS_RETIRED, 'Retired'),
(STATUS_CONSTRUCTION, 'Construction'),
(STATUS_COMMISSIONING, 'Commissioning'),
)
- LEGACY_MAP = {
- STATUS_ACTIVE: 1,
- STATUS_PLANNED: 2,
- STATUS_RETIRED: 4,
- STATUS_CONSTRUCTION: 90,
- STATUS_COMMISSIONING: 91,
+ CSS_CLASSES = {
+ STATUS_PLANNED: 'info',
+ STATUS_STAGING: 'primary',
+ STATUS_ACTIVE: 'success',
+ STATUS_DECOMMISSIONING: 'warning',
+ STATUS_RETIRED: 'danger',
}
@@ -50,14 +54,6 @@ class RackTypeChoices(ChoiceSet):
(TYPE_WALLCABINET, 'Wall-mounted cabinet'),
)
- LEGACY_MAP = {
- TYPE_2POST: 100,
- TYPE_4POST: 200,
- TYPE_CABINET: 300,
- TYPE_WALLFRAME: 1000,
- TYPE_WALLCABINET: 1100,
- }
-
class RackWidthChoices(ChoiceSet):
@@ -90,12 +86,12 @@ class RackStatusChoices(ChoiceSet):
(STATUS_DEPRECATED, 'Deprecated'),
)
- LEGACY_MAP = {
- STATUS_RESERVED: 0,
- STATUS_AVAILABLE: 1,
- STATUS_PLANNED: 2,
- STATUS_ACTIVE: 3,
- STATUS_DEPRECATED: 4,
+ CSS_CLASSES = {
+ STATUS_RESERVED: 'warning',
+ STATUS_AVAILABLE: 'success',
+ STATUS_PLANNED: 'info',
+ STATUS_ACTIVE: 'primary',
+ STATUS_DEPRECATED: 'danger',
}
@@ -109,11 +105,6 @@ class RackDimensionUnitChoices(ChoiceSet):
(UNIT_INCH, 'Inches'),
)
- LEGACY_MAP = {
- UNIT_MILLIMETER: 1000,
- UNIT_INCH: 2000,
- }
-
class RackElevationDetailRenderChoices(ChoiceSet):
@@ -140,11 +131,6 @@ class SubdeviceRoleChoices(ChoiceSet):
(ROLE_CHILD, 'Child'),
)
- LEGACY_MAP = {
- ROLE_PARENT: True,
- ROLE_CHILD: False,
- }
-
#
# Devices
@@ -160,11 +146,6 @@ class DeviceFaceChoices(ChoiceSet):
(FACE_REAR, 'Rear'),
)
- LEGACY_MAP = {
- FACE_FRONT: 0,
- FACE_REAR: 1,
- }
-
class DeviceStatusChoices(ChoiceSet):
@@ -186,14 +167,14 @@ class DeviceStatusChoices(ChoiceSet):
(STATUS_DECOMMISSIONING, 'Decommissioning'),
)
- LEGACY_MAP = {
- STATUS_OFFLINE: 0,
- STATUS_ACTIVE: 1,
- STATUS_PLANNED: 2,
- STATUS_STAGED: 3,
- STATUS_FAILED: 4,
- STATUS_INVENTORY: 5,
- STATUS_DECOMMISSIONING: 6,
+ CSS_CLASSES = {
+ STATUS_OFFLINE: 'warning',
+ STATUS_ACTIVE: 'success',
+ STATUS_PLANNED: 'info',
+ STATUS_STAGED: 'primary',
+ STATUS_FAILED: 'danger',
+ STATUS_INVENTORY: 'default',
+ STATUS_DECOMMISSIONING: 'warning',
}
@@ -266,6 +247,7 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
# NEMA non-locking
+ TYPE_NEMA_115P = 'nema-1-15p'
TYPE_NEMA_515P = 'nema-5-15p'
TYPE_NEMA_520P = 'nema-5-20p'
TYPE_NEMA_530P = 'nema-5-30p'
@@ -274,16 +256,36 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_NEMA_620P = 'nema-6-20p'
TYPE_NEMA_630P = 'nema-6-30p'
TYPE_NEMA_650P = 'nema-6-50p'
+ TYPE_NEMA_1030P = 'nema-10-30p'
+ TYPE_NEMA_1050P = 'nema-10-50p'
+ TYPE_NEMA_1420P = 'nema-14-20p'
+ TYPE_NEMA_1430P = 'nema-14-30p'
+ TYPE_NEMA_1450P = 'nema-14-50p'
+ TYPE_NEMA_1460P = 'nema-14-60p'
+ TYPE_NEMA_1515P = 'nema-15-15p'
+ TYPE_NEMA_1520P = 'nema-15-20p'
+ TYPE_NEMA_1530P = 'nema-15-30p'
+ TYPE_NEMA_1550P = 'nema-15-50p'
+ TYPE_NEMA_1560P = 'nema-15-60p'
# NEMA locking
+ TYPE_NEMA_L115P = 'nema-l1-15p'
TYPE_NEMA_L515P = 'nema-l5-15p'
TYPE_NEMA_L520P = 'nema-l5-20p'
TYPE_NEMA_L530P = 'nema-l5-30p'
- TYPE_NEMA_L615P = 'nema-l5-50p'
+ TYPE_NEMA_L550P = 'nema-l5-50p'
+ TYPE_NEMA_L615P = 'nema-l6-15p'
TYPE_NEMA_L620P = 'nema-l6-20p'
TYPE_NEMA_L630P = 'nema-l6-30p'
TYPE_NEMA_L650P = 'nema-l6-50p'
+ TYPE_NEMA_L1030P = 'nema-l10-30p'
TYPE_NEMA_L1420P = 'nema-l14-20p'
TYPE_NEMA_L1430P = 'nema-l14-30p'
+ TYPE_NEMA_L1450P = 'nema-l14-50p'
+ TYPE_NEMA_L1460P = 'nema-l14-60p'
+ TYPE_NEMA_L1520P = 'nema-l15-20p'
+ TYPE_NEMA_L1530P = 'nema-l15-30p'
+ TYPE_NEMA_L1550P = 'nema-l15-50p'
+ TYPE_NEMA_L1560P = 'nema-l15-60p'
TYPE_NEMA_L2120P = 'nema-l21-20p'
TYPE_NEMA_L2130P = 'nema-l21-30p'
# California style
@@ -306,6 +308,16 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_ITA_M = 'ita-m'
TYPE_ITA_N = 'ita-n'
TYPE_ITA_O = 'ita-o'
+ # USB
+ TYPE_USB_A = 'usb-a'
+ TYPE_USB_B = 'usb-b'
+ TYPE_USB_C = 'usb-c'
+ TYPE_USB_MINI_A = 'usb-mini-a'
+ TYPE_USB_MINI_B = 'usb-mini-b'
+ TYPE_USB_MICRO_A = 'usb-micro-a'
+ TYPE_USB_MICRO_B = 'usb-micro-b'
+ TYPE_USB_3_B = 'usb-3-b'
+ TYPE_USB_3_MICROB = 'usb-3-micro-b'
CHOICES = (
('IEC 60320', (
@@ -330,6 +342,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_IEC_3PNE9H, '3P+N+E 9H'),
)),
('NEMA (Non-locking)', (
+ (TYPE_NEMA_115P, 'NEMA 1-15P'),
(TYPE_NEMA_515P, 'NEMA 5-15P'),
(TYPE_NEMA_520P, 'NEMA 5-20P'),
(TYPE_NEMA_530P, 'NEMA 5-30P'),
@@ -338,17 +351,37 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_NEMA_620P, 'NEMA 6-20P'),
(TYPE_NEMA_630P, 'NEMA 6-30P'),
(TYPE_NEMA_650P, 'NEMA 6-50P'),
+ (TYPE_NEMA_1030P, 'NEMA 10-30P'),
+ (TYPE_NEMA_1050P, 'NEMA 10-50P'),
+ (TYPE_NEMA_1420P, 'NEMA 14-20P'),
+ (TYPE_NEMA_1430P, 'NEMA 14-30P'),
+ (TYPE_NEMA_1450P, 'NEMA 14-50P'),
+ (TYPE_NEMA_1460P, 'NEMA 14-60P'),
+ (TYPE_NEMA_1515P, 'NEMA 15-15P'),
+ (TYPE_NEMA_1520P, 'NEMA 15-20P'),
+ (TYPE_NEMA_1530P, 'NEMA 15-30P'),
+ (TYPE_NEMA_1550P, 'NEMA 15-50P'),
+ (TYPE_NEMA_1560P, 'NEMA 15-60P'),
)),
('NEMA (Locking)', (
+ (TYPE_NEMA_L115P, 'NEMA L1-15P'),
(TYPE_NEMA_L515P, 'NEMA L5-15P'),
(TYPE_NEMA_L520P, 'NEMA L5-20P'),
(TYPE_NEMA_L530P, 'NEMA L5-30P'),
+ (TYPE_NEMA_L550P, 'NEMA L5-50P'),
(TYPE_NEMA_L615P, 'NEMA L6-15P'),
(TYPE_NEMA_L620P, 'NEMA L6-20P'),
(TYPE_NEMA_L630P, 'NEMA L6-30P'),
(TYPE_NEMA_L650P, 'NEMA L6-50P'),
+ (TYPE_NEMA_L1030P, 'NEMA L10-30P'),
(TYPE_NEMA_L1420P, 'NEMA L14-20P'),
(TYPE_NEMA_L1430P, 'NEMA L14-30P'),
+ (TYPE_NEMA_L1450P, 'NEMA L14-50P'),
+ (TYPE_NEMA_L1460P, 'NEMA L14-60P'),
+ (TYPE_NEMA_L1520P, 'NEMA L15-20P'),
+ (TYPE_NEMA_L1530P, 'NEMA L15-30P'),
+ (TYPE_NEMA_L1550P, 'NEMA L15-50P'),
+ (TYPE_NEMA_L1560P, 'NEMA L15-60P'),
(TYPE_NEMA_L2120P, 'NEMA L21-20P'),
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
)),
@@ -374,6 +407,17 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_ITA_N, 'ITA Type N'),
(TYPE_ITA_O, 'ITA Type O'),
)),
+ ('USB', (
+ (TYPE_USB_A, 'USB Type A'),
+ (TYPE_USB_B, 'USB Type B'),
+ (TYPE_USB_C, 'USB Type C'),
+ (TYPE_USB_MINI_A, 'USB Mini A'),
+ (TYPE_USB_MINI_B, 'USB Mini B'),
+ (TYPE_USB_MICRO_A, 'USB Micro A'),
+ (TYPE_USB_MICRO_B, 'USB Micro B'),
+ (TYPE_USB_3_B, 'USB 3.0 Type B'),
+ (TYPE_USB_3_MICROB, 'USB 3.0 Micro B'),
+ )),
)
@@ -403,6 +447,7 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
# NEMA non-locking
+ TYPE_NEMA_115R = 'nema-1-15r'
TYPE_NEMA_515R = 'nema-5-15r'
TYPE_NEMA_520R = 'nema-5-20r'
TYPE_NEMA_530R = 'nema-5-30r'
@@ -411,16 +456,36 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_NEMA_620R = 'nema-6-20r'
TYPE_NEMA_630R = 'nema-6-30r'
TYPE_NEMA_650R = 'nema-6-50r'
+ TYPE_NEMA_1030R = 'nema-10-30r'
+ TYPE_NEMA_1050R = 'nema-10-50r'
+ TYPE_NEMA_1420R = 'nema-14-20r'
+ TYPE_NEMA_1430R = 'nema-14-30r'
+ TYPE_NEMA_1450R = 'nema-14-50r'
+ TYPE_NEMA_1460R = 'nema-14-60r'
+ TYPE_NEMA_1515R = 'nema-15-15r'
+ TYPE_NEMA_1520R = 'nema-15-20r'
+ TYPE_NEMA_1530R = 'nema-15-30r'
+ TYPE_NEMA_1550R = 'nema-15-50r'
+ TYPE_NEMA_1560R = 'nema-15-60r'
# NEMA locking
+ TYPE_NEMA_L115R = 'nema-l1-15r'
TYPE_NEMA_L515R = 'nema-l5-15r'
TYPE_NEMA_L520R = 'nema-l5-20r'
TYPE_NEMA_L530R = 'nema-l5-30r'
- TYPE_NEMA_L615R = 'nema-l5-50r'
+ TYPE_NEMA_L550R = 'nema-l5-50r'
+ TYPE_NEMA_L615R = 'nema-l6-15r'
TYPE_NEMA_L620R = 'nema-l6-20r'
TYPE_NEMA_L630R = 'nema-l6-30r'
TYPE_NEMA_L650R = 'nema-l6-50r'
+ TYPE_NEMA_L1030R = 'nema-l10-30r'
TYPE_NEMA_L1420R = 'nema-l14-20r'
TYPE_NEMA_L1430R = 'nema-l14-30r'
+ TYPE_NEMA_L1450R = 'nema-l14-50r'
+ TYPE_NEMA_L1460R = 'nema-l14-60r'
+ TYPE_NEMA_L1520R = 'nema-l15-20r'
+ TYPE_NEMA_L1530R = 'nema-l15-30r'
+ TYPE_NEMA_L1550R = 'nema-l15-50r'
+ TYPE_NEMA_L1560R = 'nema-l15-60r'
TYPE_NEMA_L2120R = 'nema-l21-20r'
TYPE_NEMA_L2130R = 'nema-l21-30r'
# California style
@@ -442,6 +507,10 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_ITA_M = 'ita-m'
TYPE_ITA_N = 'ita-n'
TYPE_ITA_O = 'ita-o'
+ # USB
+ TYPE_USB_A = 'usb-a'
+ TYPE_USB_MICROB = 'usb-micro-b'
+ TYPE_USB_C = 'usb-c'
# Proprietary
TYPE_HDOT_CX = 'hdot-cx'
@@ -468,6 +537,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_IEC_3PNE9H, '3P+N+E 9H'),
)),
('NEMA (Non-locking)', (
+ (TYPE_NEMA_115R, 'NEMA 1-15R'),
(TYPE_NEMA_515R, 'NEMA 5-15R'),
(TYPE_NEMA_520R, 'NEMA 5-20R'),
(TYPE_NEMA_530R, 'NEMA 5-30R'),
@@ -476,17 +546,37 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_NEMA_620R, 'NEMA 6-20R'),
(TYPE_NEMA_630R, 'NEMA 6-30R'),
(TYPE_NEMA_650R, 'NEMA 6-50R'),
+ (TYPE_NEMA_1030R, 'NEMA 10-30R'),
+ (TYPE_NEMA_1050R, 'NEMA 10-50R'),
+ (TYPE_NEMA_1420R, 'NEMA 14-20R'),
+ (TYPE_NEMA_1430R, 'NEMA 14-30R'),
+ (TYPE_NEMA_1450R, 'NEMA 14-50R'),
+ (TYPE_NEMA_1460R, 'NEMA 14-60R'),
+ (TYPE_NEMA_1515R, 'NEMA 15-15R'),
+ (TYPE_NEMA_1520R, 'NEMA 15-20R'),
+ (TYPE_NEMA_1530R, 'NEMA 15-30R'),
+ (TYPE_NEMA_1550R, 'NEMA 15-50R'),
+ (TYPE_NEMA_1560R, 'NEMA 15-60R'),
)),
('NEMA (Locking)', (
+ (TYPE_NEMA_L115R, 'NEMA L1-15R'),
(TYPE_NEMA_L515R, 'NEMA L5-15R'),
(TYPE_NEMA_L520R, 'NEMA L5-20R'),
(TYPE_NEMA_L530R, 'NEMA L5-30R'),
+ (TYPE_NEMA_L550R, 'NEMA L5-50R'),
(TYPE_NEMA_L615R, 'NEMA L6-15R'),
(TYPE_NEMA_L620R, 'NEMA L6-20R'),
(TYPE_NEMA_L630R, 'NEMA L6-30R'),
(TYPE_NEMA_L650R, 'NEMA L6-50R'),
+ (TYPE_NEMA_L1030R, 'NEMA L10-30R'),
(TYPE_NEMA_L1420R, 'NEMA L14-20R'),
(TYPE_NEMA_L1430R, 'NEMA L14-30R'),
+ (TYPE_NEMA_L1450R, 'NEMA L14-50R'),
+ (TYPE_NEMA_L1460R, 'NEMA L14-60R'),
+ (TYPE_NEMA_L1520R, 'NEMA L15-20R'),
+ (TYPE_NEMA_L1530R, 'NEMA L15-30R'),
+ (TYPE_NEMA_L1550R, 'NEMA L15-50R'),
+ (TYPE_NEMA_L1560R, 'NEMA L15-60R'),
(TYPE_NEMA_L2120R, 'NEMA L21-20R'),
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
)),
@@ -511,6 +601,11 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_ITA_N, 'ITA Type N'),
(TYPE_ITA_O, 'ITA Type O'),
)),
+ ('USB', (
+ (TYPE_USB_A, 'USB Type A'),
+ (TYPE_USB_MICROB, 'USB Micro B'),
+ (TYPE_USB_C, 'USB Type C'),
+ )),
('Proprietary', (
(TYPE_HDOT_CX, 'HDOT Cx'),
)),
@@ -529,12 +624,6 @@ class PowerOutletFeedLegChoices(ChoiceSet):
(FEED_LEG_C, 'C'),
)
- LEGACY_MAP = {
- FEED_LEG_A: 1,
- FEED_LEG_B: 2,
- FEED_LEG_C: 3,
- }
-
#
# Interfaces
@@ -766,81 +855,6 @@ class InterfaceTypeChoices(ChoiceSet):
),
)
- LEGACY_MAP = {
- TYPE_VIRTUAL: 0,
- TYPE_LAG: 200,
- TYPE_100ME_FIXED: 800,
- TYPE_1GE_FIXED: 1000,
- TYPE_1GE_GBIC: 1050,
- TYPE_1GE_SFP: 1100,
- TYPE_2GE_FIXED: 1120,
- TYPE_5GE_FIXED: 1130,
- TYPE_10GE_FIXED: 1150,
- TYPE_10GE_CX4: 1170,
- TYPE_10GE_SFP_PLUS: 1200,
- TYPE_10GE_XFP: 1300,
- TYPE_10GE_XENPAK: 1310,
- TYPE_10GE_X2: 1320,
- TYPE_25GE_SFP28: 1350,
- TYPE_40GE_QSFP_PLUS: 1400,
- TYPE_50GE_QSFP28: 1420,
- TYPE_100GE_CFP: 1500,
- TYPE_100GE_CFP2: 1510,
- TYPE_100GE_CFP4: 1520,
- TYPE_100GE_CPAK: 1550,
- TYPE_100GE_QSFP28: 1600,
- TYPE_200GE_CFP2: 1650,
- TYPE_200GE_QSFP56: 1700,
- TYPE_400GE_QSFP_DD: 1750,
- TYPE_400GE_OSFP: 1800,
- TYPE_80211A: 2600,
- TYPE_80211G: 2610,
- TYPE_80211N: 2620,
- TYPE_80211AC: 2630,
- TYPE_80211AD: 2640,
- TYPE_GSM: 2810,
- TYPE_CDMA: 2820,
- TYPE_LTE: 2830,
- TYPE_SONET_OC3: 6100,
- TYPE_SONET_OC12: 6200,
- TYPE_SONET_OC48: 6300,
- TYPE_SONET_OC192: 6400,
- TYPE_SONET_OC768: 6500,
- TYPE_SONET_OC1920: 6600,
- TYPE_SONET_OC3840: 6700,
- TYPE_1GFC_SFP: 3010,
- TYPE_2GFC_SFP: 3020,
- TYPE_4GFC_SFP: 3040,
- TYPE_8GFC_SFP_PLUS: 3080,
- TYPE_16GFC_SFP_PLUS: 3160,
- TYPE_32GFC_SFP28: 3320,
- TYPE_128GFC_QSFP28: 3400,
- TYPE_INFINIBAND_SDR: 7010,
- TYPE_INFINIBAND_DDR: 7020,
- TYPE_INFINIBAND_QDR: 7030,
- TYPE_INFINIBAND_FDR10: 7040,
- TYPE_INFINIBAND_FDR: 7050,
- TYPE_INFINIBAND_EDR: 7060,
- TYPE_INFINIBAND_HDR: 7070,
- TYPE_INFINIBAND_NDR: 7080,
- TYPE_INFINIBAND_XDR: 7090,
- TYPE_T1: 4000,
- TYPE_E1: 4010,
- TYPE_T3: 4040,
- TYPE_E3: 4050,
- TYPE_STACKWISE: 5000,
- TYPE_STACKWISE_PLUS: 5050,
- TYPE_FLEXSTACK: 5100,
- TYPE_FLEXSTACK_PLUS: 5150,
- TYPE_JUNIPER_VCP: 5200,
- TYPE_SUMMITSTACK: 5300,
- TYPE_SUMMITSTACK128: 5310,
- TYPE_SUMMITSTACK256: 5320,
- TYPE_SUMMITSTACK512: 5330,
- TYPE_OTHER: 32767,
- TYPE_KEYSTONE: 32766,
- }
-
class InterfaceModeChoices(ChoiceSet):
@@ -854,12 +868,6 @@ class InterfaceModeChoices(ChoiceSet):
(MODE_TAGGED_ALL, 'Tagged (All)'),
)
- LEGACY_MAP = {
- MODE_ACCESS: 100,
- MODE_TAGGED: 200,
- MODE_TAGGED_ALL: 300,
- }
-
#
# FrontPorts/RearPorts
@@ -868,6 +876,9 @@ class InterfaceModeChoices(ChoiceSet):
class PortTypeChoices(ChoiceSet):
TYPE_8P8C = '8p8c'
+ TYPE_8P6C = '8p6c'
+ TYPE_8P4C = '8p4c'
+ TYPE_8P2C = '8p2c'
TYPE_110_PUNCH = '110-punch'
TYPE_BNC = 'bnc'
TYPE_MRJ21 = 'mrj21'
@@ -881,12 +892,18 @@ class PortTypeChoices(ChoiceSet):
TYPE_MPO = 'mpo'
TYPE_LSH = 'lsh'
TYPE_LSH_APC = 'lsh-apc'
+ TYPE_SPLICE = 'splice'
+ TYPE_CS = 'cs'
+ TYPE_SN = 'sn'
CHOICES = (
(
'Copper',
(
(TYPE_8P8C, '8P8C'),
+ (TYPE_8P6C, '8P6C'),
+ (TYPE_8P4C, '8P4C'),
+ (TYPE_8P2C, '8P2C'),
(TYPE_110_PUNCH, '110 Punch'),
(TYPE_BNC, 'BNC'),
(TYPE_MRJ21, 'MRJ21'),
@@ -905,26 +922,13 @@ class PortTypeChoices(ChoiceSet):
(TYPE_SC, 'SC'),
(TYPE_SC_APC, 'SC/APC'),
(TYPE_ST, 'ST'),
+ (TYPE_CS, 'CS'),
+ (TYPE_SN, 'SN'),
+ (TYPE_SPLICE, 'Splice'),
)
)
)
- LEGACY_MAP = {
- TYPE_8P8C: 1000,
- TYPE_110_PUNCH: 1100,
- TYPE_BNC: 1200,
- TYPE_ST: 2000,
- TYPE_SC: 2100,
- TYPE_SC_APC: 2110,
- TYPE_FC: 2200,
- TYPE_LC: 2300,
- TYPE_LC_APC: 2310,
- TYPE_MTRJ: 2400,
- TYPE_MPO: 2500,
- TYPE_LSH: 2600,
- TYPE_LSH_APC: 2610,
- }
-
#
# Cables
@@ -984,28 +988,6 @@ class CableTypeChoices(ChoiceSet):
(TYPE_POWER, 'Power'),
)
- LEGACY_MAP = {
- TYPE_CAT3: 1300,
- TYPE_CAT5: 1500,
- TYPE_CAT5E: 1510,
- TYPE_CAT6: 1600,
- TYPE_CAT6A: 1610,
- TYPE_CAT7: 1700,
- TYPE_DAC_ACTIVE: 1800,
- TYPE_DAC_PASSIVE: 1810,
- TYPE_COAXIAL: 1900,
- TYPE_MMF: 3000,
- TYPE_MMF_OM1: 3010,
- TYPE_MMF_OM2: 3020,
- TYPE_MMF_OM3: 3030,
- TYPE_MMF_OM4: 3040,
- TYPE_SMF: 3500,
- TYPE_SMF_OS1: 3510,
- TYPE_SMF_OS2: 3520,
- TYPE_AOC: 3800,
- TYPE_POWER: 5000,
- }
-
class CableStatusChoices(ChoiceSet):
@@ -1019,9 +1001,10 @@ class CableStatusChoices(ChoiceSet):
(STATUS_DECOMMISSIONING, 'Decommissioning'),
)
- LEGACY_MAP = {
- STATUS_CONNECTED: True,
- STATUS_PLANNED: False,
+ CSS_CLASSES = {
+ STATUS_CONNECTED: 'success',
+ STATUS_PLANNED: 'info',
+ STATUS_DECOMMISSIONING: 'warning',
}
@@ -1039,13 +1022,6 @@ class CableLengthUnitChoices(ChoiceSet):
(UNIT_INCH, 'Inches'),
)
- LEGACY_MAP = {
- UNIT_METER: 1200,
- UNIT_CENTIMETER: 1100,
- UNIT_FOOT: 2100,
- UNIT_INCH: 2000,
- }
-
#
# PowerFeeds
@@ -1065,11 +1041,11 @@ class PowerFeedStatusChoices(ChoiceSet):
(STATUS_FAILED, 'Failed'),
)
- LEGACY_MAP = {
- STATUS_OFFLINE: 0,
- STATUS_ACTIVE: 1,
- STATUS_PLANNED: 2,
- STATUS_FAILED: 4,
+ CSS_CLASSES = {
+ STATUS_OFFLINE: 'warning',
+ STATUS_ACTIVE: 'success',
+ STATUS_PLANNED: 'info',
+ STATUS_FAILED: 'danger',
}
@@ -1083,9 +1059,9 @@ class PowerFeedTypeChoices(ChoiceSet):
(TYPE_REDUNDANT, 'Redundant'),
)
- LEGACY_MAP = {
- TYPE_PRIMARY: 1,
- TYPE_REDUNDANT: 2,
+ CSS_CLASSES = {
+ TYPE_PRIMARY: 'success',
+ TYPE_REDUNDANT: 'info',
}
@@ -1099,11 +1075,6 @@ class PowerFeedSupplyChoices(ChoiceSet):
(SUPPLY_DC, 'DC'),
)
- LEGACY_MAP = {
- SUPPLY_AC: 1,
- SUPPLY_DC: 2,
- }
-
class PowerFeedPhaseChoices(ChoiceSet):
@@ -1114,8 +1085,3 @@ class PowerFeedPhaseChoices(ChoiceSet):
(PHASE_SINGLE, 'Single phase'),
(PHASE_3PHASE, 'Three-phase'),
)
-
- LEGACY_MAP = {
- PHASE_SINGLE: 1,
- PHASE_3PHASE: 3,
- }
diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py
index f938b6f14..0fc69be3b 100644
--- a/netbox/dcim/constants.py
+++ b/netbox/dcim/constants.py
@@ -11,8 +11,6 @@ RACK_U_HEIGHT_DEFAULT = 42
RACK_ELEVATION_BORDER_WIDTH = 2
RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30
-RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 220
-RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 22
#
@@ -20,7 +18,7 @@ RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 22
#
REARPORT_POSITIONS_MIN = 1
-REARPORT_POSITIONS_MAX = 64
+REARPORT_POSITIONS_MAX = 1024
#
@@ -61,12 +59,6 @@ POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
# Cabling and connections
#
-# Console/power/interface connection statuses
-CONNECTION_STATUS_CHOICES = [
- [False, 'Not Connected'],
- [True, 'Connected'],
-]
-
# Cable endpoint types
CABLE_TERMINATION_MODELS = Q(
Q(app_label='circuits', model__in=(
@@ -85,12 +77,13 @@ CABLE_TERMINATION_MODELS = Q(
)
COMPATIBLE_TERMINATION_TYPES = {
+ 'circuittermination': ['interface', 'frontport', 'rearport', 'circuittermination'],
'consoleport': ['consoleserverport', 'frontport', 'rearport'],
'consoleserverport': ['consoleport', 'frontport', 'rearport'],
- 'powerport': ['poweroutlet', 'powerfeed'],
- 'poweroutlet': ['powerport'],
'interface': ['interface', 'circuittermination', 'frontport', 'rearport'],
'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
+ 'powerfeed': ['powerport'],
+ 'poweroutlet': ['powerport'],
+ 'powerport': ['poweroutlet', 'powerfeed'],
'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
- 'circuittermination': ['interface', 'frontport', 'rearport', 'circuittermination'],
}
diff --git a/netbox/dcim/elevations.py b/netbox/dcim/elevations.py
index ea780b2d9..93c44f087 100644
--- a/netbox/dcim/elevations.py
+++ b/netbox/dcim/elevations.py
@@ -14,10 +14,11 @@ class RackElevationSVG:
Use this class to render a rack elevation as an SVG image.
:param rack: A NetBox Rack instance
+ :param user: User instance. If specified, only devices viewable by this user will be fully displayed.
:param include_images: If true, the SVG document will embed front/rear device face images, where available
:param base_url: Base URL for links within the SVG document. If none, links will be relative.
"""
- def __init__(self, rack, include_images=True, base_url=None):
+ def __init__(self, rack, user=None, include_images=True, base_url=None):
self.rack = rack
self.include_images = include_images
if base_url is not None:
@@ -25,7 +26,14 @@ class RackElevationSVG:
else:
self.base_url = ''
- def _get_device_description(self, device):
+ # Determine the subset of devices within this rack that are viewable by the user, if any
+ permitted_devices = self.rack.devices
+ if user is not None:
+ permitted_devices = permitted_devices.restrict(user, 'view')
+ self.permitted_device_ids = permitted_devices.values_list('pk', flat=True)
+
+ @staticmethod
+ def _get_device_description(device):
return '{} ({}) â {} ({}U) {} {}'.format(
device.name,
device.device_role,
@@ -86,8 +94,12 @@ class RackElevationSVG:
# Embed front device type image if one exists
if self.include_images and device.device_type.front_image:
- url = '{}{}'.format(self.base_url, device.device_type.front_image.url)
- image = drawing.image(href=url, insert=start, size=end, class_='device-image')
+ image = drawing.image(
+ href=device.device_type.front_image.url,
+ insert=start,
+ size=end,
+ class_='device-image'
+ )
image.fit(scale='slice')
link.add(image)
@@ -99,8 +111,12 @@ class RackElevationSVG:
# Embed rear device type image if one exists
if self.include_images and device.device_type.rear_image:
- url = device.device_type.rear_image.url
- image = drawing.image(href=url, insert=start, size=end, class_='device-image')
+ image = drawing.image(
+ href=device.device_type.rear_image.url,
+ insert=start,
+ size=end,
+ class_='device-image'
+ )
image.fit(scale='slice')
drawing.add(image)
@@ -133,7 +149,7 @@ class RackElevationSVG:
unit_cursor = 0
for u in elevation:
o = other[unit_cursor]
- if not u['device'] and o['device']:
+ if not u['device'] and o['device'] and o['device'].device_type.is_full_depth:
u['device'] = o['device']
u['height'] = 1
unit_cursor += u.get('height', 1)
@@ -174,10 +190,13 @@ class RackElevationSVG:
text_cordinates = (x_offset + (unit_width / 2), y_offset + end_y / 2)
# Draw the device
- if device and device.face == face:
+ if device and device.face == face and device.pk in self.permitted_device_ids:
self._draw_device_front(drawing, device, start_cordinates, end_cordinates, text_cordinates)
- elif device and device.device_type.is_full_depth:
+ elif device and device.device_type.is_full_depth and device.pk in self.permitted_device_ids:
self._draw_device_rear(drawing, device, start_cordinates, end_cordinates, text_cordinates)
+ elif device:
+ # Devices which the user does not have permission to view are rendered only as unavailable space
+ drawing.add(drawing.rect(start_cordinates, end_cordinates, class_='blocked'))
else:
# Draw shallow devices, reservations, or empty units
class_ = 'slot'
diff --git a/netbox/dcim/exceptions.py b/netbox/dcim/exceptions.py
deleted file mode 100644
index 18e42318b..000000000
--- a/netbox/dcim/exceptions.py
+++ /dev/null
@@ -1,14 +0,0 @@
-class LoopDetected(Exception):
- """
- A loop has been detected while tracing a cable path.
- """
- pass
-
-
-class CableTraceSplit(Exception):
- """
- A cable trace cannot be completed because a RearPort maps to multiple FrontPorts and
- we don't know which one to follow.
- """
- def __init__(self, termination, *args, **kwargs):
- self.termination = termination
diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py
index 3acd0d4a1..21af2ed14 100644
--- a/netbox/dcim/fields.py
+++ b/netbox/dcim/fields.py
@@ -1,9 +1,11 @@
+from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from netaddr import AddrFormatError, EUI, mac_unix_expanded
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
+from .lookups import PathContains
class ASNField(models.BigIntegerField):
@@ -50,3 +52,15 @@ class MACAddressField(models.Field):
if not value:
return None
return str(self.to_python(value))
+
+
+class PathField(ArrayField):
+ """
+ An ArrayField which holds a set of objects, each identified by a (type, ID) tuple.
+ """
+ def __init__(self, **kwargs):
+ kwargs['base_field'] = models.CharField(max_length=40)
+ super().__init__(**kwargs)
+
+
+PathField.register_lookup(PathContains)
diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py
index 39d684d55..0fb4b7334 100644
--- a/netbox/dcim/filters.py
+++ b/netbox/dcim/filters.py
@@ -1,7 +1,8 @@
import django_filters
from django.contrib.auth.models import User
+from django.db.models import Count
-from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
+from extras.filters import CustomFieldModelFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
from tenancy.filters import TenancyFilterSet
from tenancy.models import Tenant
from utilities.choices import ColorChoices
@@ -23,6 +24,7 @@ from .models import (
__all__ = (
'CableFilterSet',
+ 'CableTerminationFilterSet',
'ConsoleConnectionFilterSet',
'ConsolePortFilterSet',
'ConsolePortTemplateFilterSet',
@@ -40,6 +42,7 @@ __all__ = (
'InterfaceTemplateFilterSet',
'InventoryItemFilterSet',
'ManufacturerFilterSet',
+ 'PathEndpointFilterSet',
'PlatformFilterSet',
'PowerConnectionFilterSet',
'PowerFeedFilterSet',
@@ -60,7 +63,7 @@ __all__ = (
)
-class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CustomFieldFilterSet):
+class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CustomFieldModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Parent region (ID)',
@@ -77,7 +80,7 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CustomFieldFilterS
fields = ['id', 'name', 'slug', 'description']
-class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -176,7 +179,7 @@ class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'color']
-class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -221,6 +224,12 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Creat
choices=RackStatusChoices,
null_value=None
)
+ type = django_filters.MultipleChoiceFilter(
+ choices=RackTypeChoices
+ )
+ width = django_filters.MultipleChoiceFilter(
+ choices=RackWidthChoices
+ )
role_id = django_filters.ModelMultipleChoiceFilter(
queryset=RackRole.objects.all(),
label='Role (ID)',
@@ -239,8 +248,8 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, Creat
class Meta:
model = Rack
fields = [
- 'id', 'name', 'facility_id', 'asset_tag', 'type', 'width', 'u_height', 'desc_units',
- 'outer_width', 'outer_depth', 'outer_unit',
+ 'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
+ 'outer_unit',
]
def search(self, queryset, name, value):
@@ -293,11 +302,12 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
label='User (ID)',
)
user = django_filters.ModelMultipleChoiceFilter(
- field_name='user',
+ field_name='user__username',
queryset=User.objects.all(),
to_field_name='username',
label='User (name)',
)
+ tag = TagFilter()
class Meta:
model = RackReservation
@@ -321,7 +331,7 @@ class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'description']
-class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -383,28 +393,28 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFil
)
def _console_ports(self, queryset, name, value):
- return queryset.exclude(consoleport_templates__isnull=value)
+ return queryset.exclude(consoleporttemplates__isnull=value)
def _console_server_ports(self, queryset, name, value):
- return queryset.exclude(consoleserverport_templates__isnull=value)
+ return queryset.exclude(consoleserverporttemplates__isnull=value)
def _power_ports(self, queryset, name, value):
- return queryset.exclude(powerport_templates__isnull=value)
+ return queryset.exclude(powerporttemplates__isnull=value)
def _power_outlets(self, queryset, name, value):
- return queryset.exclude(poweroutlet_templates__isnull=value)
+ return queryset.exclude(poweroutlettemplates__isnull=value)
def _interfaces(self, queryset, name, value):
- return queryset.exclude(interface_templates__isnull=value)
+ return queryset.exclude(interfacetemplates__isnull=value)
def _pass_through_ports(self, queryset, name, value):
return queryset.exclude(
- frontport_templates__isnull=value,
- rearport_templates__isnull=value
+ frontporttemplates__isnull=value,
+ rearporttemplates__isnull=value
)
def _device_bays(self, queryset, name, value):
- return queryset.exclude(device_bay_templates__isnull=value)
+ return queryset.exclude(devicebaytemplates__isnull=value)
class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
@@ -500,7 +510,7 @@ class DeviceFilterSet(
BaseFilterSet,
TenancyFilterSet,
LocalConfigContextFilterSet,
- CustomFieldFilterSet,
+ CustomFieldModelFilterSet,
CreatedUpdatedFilterSet
):
q = django_filters.CharFilter(
@@ -655,22 +665,16 @@ class DeviceFilterSet(
return queryset.filter(
Q(name__icontains=value) |
Q(serial__icontains=value.strip()) |
- Q(inventory_items__serial__icontains=value.strip()) |
+ Q(inventoryitems__serial__icontains=value.strip()) |
Q(asset_tag__icontains=value.strip()) |
Q(comments__icontains=value)
).distinct()
def _has_primary_ip(self, queryset, name, value):
+ params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)
if value:
- return queryset.filter(
- Q(primary_ip4__isnull=False) |
- Q(primary_ip6__isnull=False)
- )
- else:
- return queryset.exclude(
- Q(primary_ip4__isnull=False) |
- Q(primary_ip6__isnull=False)
- )
+ return queryset.filter(params)
+ return queryset.exclude(params)
def _virtual_chassis_member(self, queryset, name, value):
return queryset.exclude(virtual_chassis__isnull=value)
@@ -697,7 +701,7 @@ class DeviceFilterSet(
)
def _device_bays(self, queryset, name, value):
- return queryset.exclude(device_bays__isnull=value)
+ return queryset.exclude(devicebays__isnull=value)
class DeviceComponentFilterSet(django_filters.FilterSet):
@@ -746,75 +750,81 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
return queryset
return queryset.filter(
Q(name__icontains=value) |
+ Q(label__icontains=value) |
Q(description__icontains=value)
)
-class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
- type = django_filters.MultipleChoiceFilter(
- choices=ConsolePortTypeChoices,
- null_value=None
- )
+class CableTerminationFilterSet(django_filters.FilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
+
+class PathEndpointFilterSet(django_filters.FilterSet):
+ connected = django_filters.BooleanFilter(
+ method='filter_connected'
+ )
+
+ def filter_connected(self, queryset, name, value):
+ if value:
+ return queryset.filter(_path__is_active=True)
+ else:
+ return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
+
+
+class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
+ type = django_filters.MultipleChoiceFilter(
+ choices=ConsolePortTypeChoices,
+ null_value=None
+ )
+
class Meta:
model = ConsolePort
- fields = ['id', 'name', 'description', 'connection_status']
+ fields = ['id', 'name', 'description']
-class ConsoleServerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
+class ConsoleServerPortFilterSet(
+ BaseFilterSet,
+ DeviceComponentFilterSet,
+ CableTerminationFilterSet,
+ PathEndpointFilterSet
+):
type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices,
null_value=None
)
- cabled = django_filters.BooleanFilter(
- field_name='cable',
- lookup_expr='isnull',
- exclude=True
- )
class Meta:
model = ConsoleServerPort
- fields = ['id', 'name', 'description', 'connection_status']
+ fields = ['id', 'name', 'description']
-class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
+class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PowerPortTypeChoices,
null_value=None
)
- cabled = django_filters.BooleanFilter(
- field_name='cable',
- lookup_expr='isnull',
- exclude=True
- )
class Meta:
model = PowerPort
- fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status']
+ fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description']
-class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet):
+class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PowerOutletTypeChoices,
null_value=None
)
- cabled = django_filters.BooleanFilter(
- field_name='cable',
- lookup_expr='isnull',
- exclude=True
- )
class Meta:
model = PowerOutlet
- fields = ['id', 'name', 'feed_leg', 'description', 'connection_status']
+ fields = ['id', 'name', 'feed_leg', 'description']
-class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
+class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -831,11 +841,6 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
field_name='pk',
label='Device (ID)',
)
- cabled = django_filters.BooleanFilter(
- field_name='cable',
- lookup_expr='isnull',
- exclude=True
- )
kind = django_filters.CharFilter(
method='filter_kind',
label='Kind of interface',
@@ -862,7 +867,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
class Meta:
model = Interface
- fields = ['id', 'name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
+ fields = ['id', 'name', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
def filter_device(self, queryset, name, value):
try:
@@ -912,24 +917,14 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
}.get(value, queryset.none())
-class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
- cabled = django_filters.BooleanFilter(
- field_name='cable',
- lookup_expr='isnull',
- exclude=True
- )
+class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
class Meta:
model = FrontPort
fields = ['id', 'name', 'type', 'description']
-class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
- cabled = django_filters.BooleanFilter(
- field_name='cable',
- lookup_expr='isnull',
- exclude=True
- )
+class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
class Meta:
model = RearPort
@@ -1066,7 +1061,8 @@ class VirtualChassisFilterSet(BaseFilterSet):
if not value.strip():
return queryset
qs_filter = (
- Q(master__name__icontains=value) |
+ Q(name__icontains=value) |
+ Q(members__name__icontains=value) |
Q(domain__icontains=value)
)
return queryset.filter(qs_filter)
@@ -1117,6 +1113,7 @@ class CableFilterSet(BaseFilterSet):
method='filter_device',
field_name='device__tenant__slug'
)
+ tag = TagFilter()
class Meta:
model = Cable
@@ -1135,7 +1132,20 @@ class CableFilterSet(BaseFilterSet):
return queryset
-class ConsoleConnectionFilterSet(BaseFilterSet):
+class ConnectionFilterSet:
+
+ def filter_site(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(device__site__slug=value)
+
+ def filter_device(self, queryset, name, value):
+ if not value:
+ return queryset
+ return queryset.filter(device_id__in=value)
+
+
+class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
site = django_filters.CharFilter(
method='filter_site',
label='Site (slug)',
@@ -1150,23 +1160,10 @@ class ConsoleConnectionFilterSet(BaseFilterSet):
class Meta:
model = ConsolePort
- fields = ['name', 'connection_status']
-
- def filter_site(self, queryset, name, value):
- if not value.strip():
- return queryset
- return queryset.filter(connected_endpoint__device__site__slug=value)
-
- def filter_device(self, queryset, name, value):
- if not value:
- return queryset
- return queryset.filter(
- Q(**{'{}__in'.format(name): value}) |
- Q(**{'connected_endpoint__{}__in'.format(name): value})
- )
+ fields = ['name']
-class PowerConnectionFilterSet(BaseFilterSet):
+class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
site = django_filters.CharFilter(
method='filter_site',
label='Site (slug)',
@@ -1181,23 +1178,10 @@ class PowerConnectionFilterSet(BaseFilterSet):
class Meta:
model = PowerPort
- fields = ['name', 'connection_status']
-
- def filter_site(self, queryset, name, value):
- if not value.strip():
- return queryset
- return queryset.filter(_connected_poweroutlet__device__site__slug=value)
-
- def filter_device(self, queryset, name, value):
- if not value:
- return queryset
- return queryset.filter(
- Q(**{'{}__in'.format(name): value}) |
- Q(**{'_connected_poweroutlet__{}__in'.format(name): value})
- )
+ fields = ['name']
-class InterfaceConnectionFilterSet(BaseFilterSet):
+class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
site = django_filters.CharFilter(
method='filter_site',
label='Site (slug)',
@@ -1212,23 +1196,7 @@ class InterfaceConnectionFilterSet(BaseFilterSet):
class Meta:
model = Interface
- fields = ['connection_status']
-
- def filter_site(self, queryset, name, value):
- if not value.strip():
- return queryset
- return queryset.filter(
- Q(device__site__slug=value) |
- Q(_connected_interface__device__site__slug=value)
- )
-
- def filter_device(self, queryset, name, value):
- if not value:
- return queryset
- return queryset.filter(
- Q(**{'{}__in'.format(name): value}) |
- Q(**{'_connected_interface__{}__in'.format(name): value})
- )
+ fields = []
class PowerPanelFilterSet(BaseFilterSet):
@@ -1265,6 +1233,7 @@ class PowerPanelFilterSet(BaseFilterSet):
lookup_expr='in',
label='Rack group (ID)',
)
+ tag = TagFilter()
class Meta:
model = PowerPanel
@@ -1279,7 +1248,13 @@ class PowerPanelFilterSet(BaseFilterSet):
return queryset.filter(qs_filter)
-class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class PowerFeedFilterSet(
+ BaseFilterSet,
+ CableTerminationFilterSet,
+ PathEndpointFilterSet,
+ CustomFieldModelFilterSet,
+ CreatedUpdatedFilterSet
+):
q = django_filters.CharFilter(
method='search',
label='Search',
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index f421c2e81..fcc5a3ac2 100644
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -6,28 +6,28 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms.array import SimpleArrayField
from django.core.exceptions import ObjectDoesNotExist
from django.utils.safestring import mark_safe
-from mptt.forms import TreeNodeChoiceField
from netaddr import EUI
from netaddr.core import AddrFormatError
from timezone_field import TimeZoneFormField
-from circuits.models import Circuit, Provider
+from circuits.models import Circuit, CircuitTermination, Provider
from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, CustomFieldModelForm,
- LocalConfigContextFilterForm, TagField,
+ LocalConfigContextFilterForm,
)
+from extras.models import Tag
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
from ipam.models import IPAddress, VLAN
from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import (
- APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
- BulkEditNullBooleanSelect, ColorSelect, CommentField, ConfirmationForm, CSVChoiceField, CSVModelChoiceField,
- CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model,
- JSONField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
+ APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
+ ColorSelect, CommentField, CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelForm,
+ DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField,
+ NumericArrayField, SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
BOOLEAN_WITH_BLANK_CHOICES,
)
-from virtualization.models import Cluster, ClusterGroup, VirtualMachine
+from virtualization.models import Cluster, ClusterGroup
from .choices import *
from .constants import *
from .models import (
@@ -59,7 +59,6 @@ def get_device_by_name_or_pk(name):
class DeviceComponentFilterForm(BootstrapMixin, forms.Form):
-
field_order = [
'q', 'region', 'site'
]
@@ -70,39 +69,32 @@ class DeviceComponentFilterForm(BootstrapMixin, forms.Form):
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
- required=False,
- widget=APISelectMultiple(
- value_field='slug',
- filter_for={
- 'site': 'region'
- }
- )
+ required=False
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False,
- widget=APISelectMultiple(
- value_field="slug",
- filter_for={
- 'device_id': 'site',
- }
- )
+ query_params={
+ 'region': '$region'
+ }
)
device_id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
- label='Device'
+ label='Device',
+ query_params={
+ 'site': '$site'
+ }
)
-class InterfaceCommonForm:
+class InterfaceCommonForm(forms.Form):
def clean(self):
-
super().clean()
- # Validate VLAN assignments
+ parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine'
tagged_vlans = self.cleaned_data['tagged_vlans']
# Untagged interfaces cannot be assigned tagged VLANs
@@ -117,38 +109,42 @@ class InterfaceCommonForm:
# Validate tagged VLANs; must be a global VLAN or in the same site
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED:
- valid_sites = [None, self.cleaned_data['device'].site]
+ valid_sites = [None, self.cleaned_data[parent_field].site]
invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]
if invalid_vlans:
raise forms.ValidationError({
- 'tagged_vlans': "The tagged VLANs ({}) must belong to the same site as the interface's parent "
- "device/VM, or they must be global".format(', '.join(invalid_vlans))
+ 'tagged_vlans': f"The tagged VLANs ({', '.join(invalid_vlans)}) must belong to the same site as "
+ f"the interface's parent device/VM, or they must be global"
})
-class BulkRenameForm(forms.Form):
+class ComponentForm(BootstrapMixin, forms.Form):
"""
- An extendable form to be used for renaming device components in bulk.
+ Subclass this form when facilitating the creation of one or more device component or component templates based on
+ a name pattern.
"""
- find = forms.CharField()
- replace = forms.CharField()
- use_regex = forms.BooleanField(
+ name_pattern = ExpandableNameField(
+ label='Name'
+ )
+ label_pattern = ExpandableNameField(
+ label='Label',
required=False,
- initial=True,
- label='Use regular expressions'
+ help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
)
def clean(self):
+ super().clean()
- # Validate regular expression in "find" field
- if self.cleaned_data['use_regex']:
- try:
- re.compile(self.cleaned_data['find'])
- except re.error:
+ # Validate that the number of components being created from both the name_pattern and label_pattern are equal
+ if self.cleaned_data['label_pattern']:
+ name_pattern_count = len(self.cleaned_data['name_pattern'])
+ label_pattern_count = len(self.cleaned_data['label_pattern'])
+ if name_pattern_count != label_pattern_count:
raise forms.ValidationError({
- 'find': "Invalid regular expression"
- })
+ 'label_pattern': f'The provided name pattern will create {name_pattern_count} components, however '
+ f'{label_pattern_count} labels will be generated. These counts must match.'
+ }, code='label_pattern_mismatch')
#
@@ -178,10 +174,9 @@ class MACAddressField(forms.Field):
#
class RegionForm(BootstrapMixin, CustomFieldModelForm):
- parent = TreeNodeChoiceField(
+ parent = DynamicModelChoiceField(
queryset=Region.objects.all(),
- required=False,
- widget=StaticSelect2()
+ required=False
)
slug = SlugField()
@@ -218,14 +213,14 @@ class RegionFilterForm(BootstrapMixin, CustomFieldFilterForm):
#
class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
- region = TreeNodeChoiceField(
+ region = DynamicModelChoiceField(
queryset=Region.objects.all(),
- required=False,
- widget=StaticSelect2()
+ required=False
)
slug = SlugField()
comments = CommentField()
- tags = TagField(
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
required=False
)
@@ -303,10 +298,9 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
initial='',
widget=StaticSelect2()
)
- region = TreeNodeChoiceField(
+ region = DynamicModelChoiceField(
queryset=Region.objects.all(),
- required=False,
- widget=StaticSelect2()
+ required=False
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
@@ -349,10 +343,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
- required=False,
- widget=APISelectMultiple(
- value_field="slug",
- )
+ required=False
)
tag = TagFilterField(model)
@@ -362,19 +353,32 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
#
class RackGroupForm(BootstrapMixin, forms.ModelForm):
+ region = DynamicModelChoiceField(
+ queryset=Region.objects.all(),
+ required=False,
+ initial_params={
+ 'sites': '$site'
+ }
+ )
site = DynamicModelChoiceField(
- queryset=Site.objects.all()
+ queryset=Site.objects.all(),
+ query_params={
+ 'region_id': '$region'
+ }
)
parent = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
- required=False
+ required=False,
+ query_params={
+ 'site_id': '$site'
+ }
)
slug = SlugField()
class Meta:
model = RackGroup
fields = (
- 'site', 'parent', 'name', 'slug', 'description',
+ 'region', 'site', 'parent', 'name', 'slug', 'description',
)
@@ -403,34 +407,24 @@ class RackGroupFilterForm(BootstrapMixin, forms.Form):
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
- required=False,
- widget=APISelectMultiple(
- value_field="slug",
- filter_for={
- 'site': 'region',
- 'parent': 'region',
- }
- )
+ required=False
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False,
- widget=APISelectMultiple(
- value_field="slug",
- filter_for={
- 'parent': 'site',
- }
- )
+ query_params={
+ 'region': '$region'
+ }
)
parent = DynamicModelMultipleChoiceField(
queryset=RackGroup.objects.all(),
to_field_name='slug',
required=False,
- widget=APISelectMultiple(
- api_url="/api/dcim/rack-groups/",
- value_field="slug",
- )
+ query_params={
+ 'region': '$region',
+ 'site': '$site',
+ }
)
@@ -464,32 +458,42 @@ class RackRoleCSVForm(CSVModelForm):
#
class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
+ region = DynamicModelChoiceField(
+ queryset=Region.objects.all(),
+ required=False,
+ initial_params={
+ 'sites': '$site'
+ }
+ )
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
- widget=APISelect(
- filter_for={
- 'group': 'site_id',
- }
- )
+ query_params={
+ 'region_id': '$region'
+ }
)
group = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
- required=False
+ required=False,
+ query_params={
+ 'site_id': '$site'
+ }
)
role = DynamicModelChoiceField(
queryset=RackRole.objects.all(),
required=False
)
comments = CommentField()
- tags = TagField(
+ tags = DynamicModelMultipleChoiceField(
+ queryset=Tag.objects.all(),
required=False
)
class Meta:
model = Rack
fields = [
- 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'asset_tag',
- 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'tags',
+ 'region', 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
+ 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
+ 'comments', 'tags',
]
help_texts = {
'site': "The site at which the rack exists",
@@ -566,18 +570,26 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
queryset=Rack.objects.all(),
widget=forms.MultipleHiddenInput
)
+ region = DynamicModelChoiceField(
+ queryset=Region.objects.all(),
+ required=False,
+ initial_params={
+ 'sites': '$site'
+ }
+ )
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False,
- widget=APISelect(
- filter_for={
- 'group': 'site_id',
- }
- )
+ query_params={
+ 'region_id': '$region'
+ }
)
group = DynamicModelChoiceField(
queryset=RackGroup.objects.all(),
- required=False
+ required=False,
+ query_params={
+ 'site_id': '$site'
+ }
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
@@ -655,48 +667,45 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
- required=False,
- widget=APISelectMultiple(
- value_field="slug",
- filter_for={
- 'site': 'region'
- }
- )
+ required=False
)
site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
required=False,
- widget=APISelectMultiple(
- value_field="slug",
- filter_for={
- 'group_id': 'site'
- }
- )
+ query_params={
+ 'region': '$region'
+ }
)
group_id = DynamicModelMultipleChoiceField(
- queryset=RackGroup.objects.prefetch_related(
- 'site'
- ),
+ queryset=RackGroup.objects.all(),
required=False,
label='Rack group',
- widget=APISelectMultiple(
- null_option=True
- )
+ null_option='None',
+ query_params={
+ 'site': '$site'
+ }
)
status = forms.MultipleChoiceField(
choices=RackStatusChoices,
required=False,
widget=StaticSelect2Multiple()
)
+ type = forms.MultipleChoiceField(
+ choices=RackTypeChoices,
+ required=False,
+ widget=StaticSelect2Multiple()
+ )
+ width = forms.MultipleChoiceField(
+ choices=RackWidthChoices,
+ required=False,
+ widget=StaticSelect2Multiple()
+ )
role = DynamicModelMultipleChoiceField(
queryset=RackRole.objects.all(),
to_field_name='slug',
required=False,
- widget=APISelectMultiple(
- value_field="slug",
- null_option=True,
- )
+ null_option='None'
)
tag = TagFilterField(model)
@@ -711,38 +720,51 @@ class RackElevationFilterForm(RackFilterForm):
queryset=Rack.objects.all(),
label='Rack',
required=False,
- widget=APISelectMultiple(
- display_field='display_name',
- )
+ display_field='display_name',
+ query_params={
+ 'site': '$site',
+ 'group_id': '$group_id',
+ }
)
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- # Filter the rack field based on the site and group
- self.fields['site'].widget.add_filter_for('id', 'site')
- self.fields['group_id'].widget.add_filter_for('id', 'group_id')
-
#
# Rack reservations
#
-class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
- rack = forms.ModelChoiceField(
- queryset=Rack.objects.all(),
+class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
+ region = DynamicModelChoiceField(
+ queryset=Region.objects.all(),
required=False,
- widget=forms.HiddenInput()
+ initial_params={
+ 'sites': '$site'
+ }
)
- # TODO: Change this to an API-backed form field. We can't do this currently because we want to retain
- # the multi-line