mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 21:16:27 -06:00
commit
0e14bc1e02
@ -2,7 +2,7 @@ The circuits component of NetBox deals with the management of long-haul Internet
|
||||
|
||||
# Providers
|
||||
|
||||
A provider is any entity which provides some form of connectivity. This obviously includes carriers which offer Internet and private transit service. However, it might also include Internet exchange (IX) points and even organizations with whom you peer directly.
|
||||
A provider is any entity which provides some form of connectivity. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly.
|
||||
|
||||
Each provider may be assigned an autonomous system number (ASN), an account number, and contact information.
|
||||
|
||||
@ -14,7 +14,7 @@ A circuit represents a single physical data link connecting two endpoints. Each
|
||||
|
||||
### Circuit Types
|
||||
|
||||
Circuits are classified by type. For example:
|
||||
Circuits are classified by type. For example, you might define circuit types for:
|
||||
|
||||
* Internet transit
|
||||
* Out-of-band connectivity
|
||||
@ -27,7 +27,7 @@ Circuit types are fully customizable.
|
||||
|
||||
A circuit may have one or two terminations, annotated as the "A" and "Z" sides of the circuit. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites.
|
||||
|
||||
Each circuit termination can be tied to a site, or to a specific device and interface within that site. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details.
|
||||
Each circuit termination is tied to a site, and optionally to a specific device and interface within that site. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details.
|
||||
|
||||
!!! note
|
||||
A circuit represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit.
|
||||
|
@ -2,61 +2,72 @@ Data center infrastructure management (DCIM) entails all physical assets: sites,
|
||||
|
||||
# Sites
|
||||
|
||||
How you define sites will depend on the nature of your organization, but typically a site will equate 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 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.
|
||||
|
||||
Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment.
|
||||
Sites can be assigned an optional facility ID to identify the actual facility housing colocated equipment, and an Autonomous System (AS) number.
|
||||
|
||||
### Regions
|
||||
|
||||
Sites can be arranged geographically using regions. A region might represent a continent, country, city, campus, or other area depending on your use case. Regions can be nested recursively to construct a hierarchy. For example, you might define several country regions, and within each of those several state or city regions to which sites are assigned.
|
||||
|
||||
---
|
||||
|
||||
# Racks
|
||||
|
||||
Within each site exist one or more racks. Each rack within NetBox represents a physical two- or four-post equipment rack in which equipment is mounted. Rack height is measured in *rack units* (U); most racks are between 42U and 48U, but NetBox allows you to define racks of any height. Each rack has two faces (front and rear) on which devices can be mounted.
|
||||
The rack model represents a physical two- or four-post equipment rack in which equipment is mounted. Each rack is assigned to a site. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U, but NetBox allows you to define racks of arbitrary height. Each rack has two faces (front and rear) on which devices can be mounted.
|
||||
|
||||
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." The facility ID can alternatively be used to store a rack's serial number.
|
||||
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." The facility ID can alternatively be used to store a rack's serial number.
|
||||
|
||||
The available rack types include 2- and 4-post frames, 4-post cabinet, and wall-mounted frame and cabinet. Rail-to-rail width may be 19 or 23 inches.
|
||||
|
||||
### 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 is a campus, each group might be a building. If each site is a building, each rack group might be a floor or room.
|
||||
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.
|
||||
|
||||
Each group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not supported.
|
||||
|
||||
### Rack Roles
|
||||
|
||||
Each rak can optionally be assigned to a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices.
|
||||
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.
|
||||
|
||||
### Rack Space 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).
|
||||
|
||||
---
|
||||
|
||||
# Device Types
|
||||
|
||||
A device type represents a particular manufacturer and model of equipment. Device types describe the physical attributes of a device (rack height and depth), its class (e.g. console server, PDU, etc.), and its individual components (console, power, and data).
|
||||
A device type represents a particular hardware model that exists in the real world. Device types describe the physical attributes of a device (rack height and depth), its class (e.g. console server, PDU, etc.), and its individual components (console, power, and data).
|
||||
|
||||
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.
|
||||
|
||||
### Manufacturers
|
||||
|
||||
Each device type belongs to one manufacturer; e.g. Cisco, Opengear, or APC. Manufacturers are used to group different models of device.
|
||||
Each device type belongs to one manufacturer; e.g. Cisco, Opengear, or APC. The model number of a device type must be unique to its manufacturer.
|
||||
|
||||
### Component Templates
|
||||
|
||||
Each device type is assigned a number of component templates which describe the console, power, and data ports a device has. These are:
|
||||
Each device type is assigned a number of component templates which define the physical interfaces a device has. These are:
|
||||
|
||||
* Console port templates
|
||||
* Console server port templates
|
||||
* Power port templates
|
||||
* Power outlet templates
|
||||
* Interface templates
|
||||
* Device bay templates
|
||||
* Console ports
|
||||
* Console server ports
|
||||
* Power ports
|
||||
* Power outlets
|
||||
* Interfaces
|
||||
* Device bays
|
||||
|
||||
Whenever a new device is created, it is automatically assigned console, power, and interface components per the templates assigned to its device type. For example, suppose your network employs Juniper EX4300-48T switches. You would create a device type with a model name "EX4300-48T" and assign it to the manufacturer "Juniper." You might then also create the following templates for it:
|
||||
Whenever a new device is created, it is automatically assigned components per the templates assigned to its device type. For example, a Juniper EX4300-48T device type might have the following component templates:
|
||||
|
||||
* One template for a console port ("Console")
|
||||
* Two templates for power ports ("PSU0" and "PSU1")
|
||||
* 48 templates for 1GE interfaces ("ge-0/0/0" through "ge-0/0/47")
|
||||
* Four templates for 10GE interfaces ("xe-0/2/0" through "xe-0/2/3")
|
||||
|
||||
Once you've done this, every new device that you create as an instance of this type will automatically be assigned each of the components listed above.
|
||||
Once component templates have been created, every new device that you create as an instance of this type will automatically be assigned each of the components listed above.
|
||||
|
||||
Note that assignment of components from templates occurs only at the time of device creation: If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components of existing devices individually.
|
||||
!!! note
|
||||
Assignment of components from templates occurs only at the time of device creation. If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components of existing devices individually.
|
||||
|
||||
---
|
||||
|
||||
@ -64,19 +75,19 @@ Note that assignment of components from templates occurs only at the time of dev
|
||||
|
||||
Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and depth. 0U devices which can be installed in a rack but don't consume vertical rack space (such as a vertically-mounted power distribution unit) can also be defined.
|
||||
|
||||
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 shows as being mounted in U8.
|
||||
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 shows as being 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 air flow.
|
||||
|
||||
### Roles
|
||||
|
||||
NetBox allows for the definition of arbitrary device roles by which devices can be organized. For example, you might create roles for core switches, distribution switches, and access switches. In the interest of simplicity, device can only belong to one device role.
|
||||
NetBox allows for the definition of arbitrary device roles by which devices can be organized. For example, you might create roles for core switches, distribution switches, and access switches. In the interest of simplicity, a device can belong to only one role.
|
||||
|
||||
### Platforms
|
||||
|
||||
A device's platform is used to denote the type of software running on it. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15.
|
||||
|
||||
The assignment of platforms to devices is an entirely optional feature, and may be disregarded if not desired.
|
||||
The assignment of platforms to devices is an optional feature, and may be disregarded if not desired.
|
||||
|
||||
### Modules
|
||||
|
||||
@ -93,10 +104,11 @@ There are six types of device components which comprise all of the interconnecti
|
||||
* Interfaces
|
||||
* Device bays
|
||||
|
||||
Console ports connect only to console server ports, and power ports connect only to power outlets. Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. (The relationship between two interfaces is actually represented in the database by an InterfaceConnection object, but this is transparent to the user.)
|
||||
Console ports connect only to console server ports, and power ports connect only to power outlets. Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. (The relationship between two interfaces is actually represented in the database by an InterfaceConnection object, but this is transparent to the user.) Each type of connection can be classified as either *planned* or *connected*. This allows for easily denoting connections which have not yet been installed.
|
||||
|
||||
Each type of connection can be classified as either *planned* or *connected*. This allows for easily denoting connections which have not yet been installed. In addition to a connecting peer, interfaces are also assigned a form factor and may be designated as management-only (for out-of-band management). Interfaces may also be assigned a short description.
|
||||
Each interface is a assigned a form factor denoting its physical properties. Two special form factors exist: the "virtual" form factor can be used to designate logical interfaces (such as SVIs), and the "LAG" form factor can be used to desinate link aggregation groups to which physical interfaces can be assigned. Each interface can also be designated as management-only (for out-of-band management) and assigned a short description.
|
||||
|
||||
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 on rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
|
||||
|
||||
Note that child devices differ from modules in that they are still treated as independent devices, with their own console/power/data components, modules, and IP addresses. Modules, on the other hand, are parts within a device, such as a hard disk or power supply.
|
||||
!!! note
|
||||
Child devices differ from modules in that they are still treated as independent devices, with their own console/power/data components, modules, and IP addresses. Modules, on the other hand, are parts within a device, such as a hard disk or power supply, which do not provide their own management plane.
|
||||
|
@ -2,7 +2,7 @@ This section entails features of NetBox which are not crucial to its primary fun
|
||||
|
||||
# Custom Fields
|
||||
|
||||
Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address` and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
|
||||
Each object in NetBox is represented in the database as a discrete table, and each attribute of an object exists as a column within its table. For example, sites are stored in the `dcim_site` table, which has columns named `name`, `facility`, `physical_address`, and so on. As new attributes are added to objects throughout the development of NetBox, tables are expanded to include new rows.
|
||||
|
||||
However, some users might want to associate with objects attributes that are somewhat esoteric in nature, and that would not make sense to include in the core NetBox database schema. For instance, suppose your organization needs to associate each device with a ticket number pointing to the support ticket that was opened to have it installed. This is certainly a legitimate use for NetBox, but it's perhaps not a common enough need to warrant expanding the internal data schema. Instead, you can create a custom field to hold this data.
|
||||
|
||||
@ -33,7 +33,15 @@ NetBox allows users to define custom templates that can be used when exporting o
|
||||
|
||||
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list.
|
||||
|
||||
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database is stored in the `queryset` variable. Typically, you'll want to iterate through this list using a for loop.
|
||||
Export templates are written in [Django's template language](https://docs.djangoproject.com/en/1.9/ref/templates/language/), which is very similar to Jinja2. The list of objects returned from the database is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example:
|
||||
|
||||
```
|
||||
{% for rack in queryset %}
|
||||
Rack: {{ rack.name }}
|
||||
Site: {{ rack.site.name }}
|
||||
Height: {{ rack.u_height }}U
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
|
||||
|
||||
@ -44,10 +52,10 @@ A MIME type and file extension can optionally be defined for each export templat
|
||||
Here's an example device export template that will generate a simple Nagios configuration from a list of devices.
|
||||
|
||||
```
|
||||
{% for d in queryset %}{% if d.status and d.primary_ip %}define host{
|
||||
{% for device in queryset %}{% if device.status and device.primary_ip %}define host{
|
||||
use generic-switch
|
||||
host_name {{ d.name }}
|
||||
address {{ d.primary_ip.address.ip }}
|
||||
host_name {{ device.name }}
|
||||
address {{ device.primary_ip.address.ip }}
|
||||
}
|
||||
{% endif %}{% endfor %}
|
||||
```
|
||||
@ -74,9 +82,9 @@ define host{
|
||||
|
||||
# Graphs
|
||||
|
||||
NetBox does not generate graphs itself. This feature allows you to embed contextual graphs from an external resources inside certain NetBox views. Each embedded graph must be defined with the following parameters:
|
||||
NetBox does not have the ability to generate graphs natively, but this feature allows you to embed contextual graphs from an external resources (such as a monitoring system) inside the site, provider, and interface views. Each embedded graph must be defined with the following parameters:
|
||||
|
||||
* **Type:** Interface, provider, or site. This determines where the graph will be displayed.
|
||||
* **Type:** Site, provider, or interface. This determines in which view the graph will be displayed.
|
||||
* **Weight:** Determines the order in which graphs are displayed (lower weights are displayed first). Graphs with equal weights will be ordered alphabetically by name.
|
||||
* **Name:** The title to display above the graph.
|
||||
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
|
||||
@ -86,7 +94,7 @@ NetBox does not generate graphs itself. This feature allows you to embed context
|
||||
|
||||
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
|
||||
|
||||
Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend connectivity).
|
||||
Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend infrastructure).
|
||||
|
||||
To define the scope of a topology map, decide which devices you want to include. The map will only include interface connections with both points terminated on an included device. Specify the devices to include in the **device patterns** field by entering a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) matching device names. For example, if you wanted to include "mgmt-switch1" through "mgmt-switch99", you might use the regex `mgmt-switch\d+`.
|
||||
|
||||
|
@ -6,11 +6,14 @@ A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain
|
||||
|
||||
Each VRF is assigned a name and a unique route distinguisher (RD). VRFs are an optional feature of NetBox: Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.
|
||||
|
||||
!!! note
|
||||
By default, NetBox allows for overlapping IP space both in the global table and within each VRF. Unique space enforcement can be toggled per-VRF as well as in the global table using the `ENFORCE_GLOBAL_UNIQUE` configuration setting.
|
||||
|
||||
---
|
||||
|
||||
# Aggregates
|
||||
|
||||
IPv4 address space is organized as a hierarchy, with more-specific (smaller) prefix arranged as child nodes under less-specific (larger) prefixes. For example:
|
||||
IP address space is organized as a hierarchy, with more-specific (smaller) prefixes arranged as child nodes under less-specific (larger) prefixes. For example:
|
||||
|
||||
* 10.0.0.0/8
|
||||
* 10.1.0.0/16
|
||||
@ -18,23 +21,23 @@ IPv4 address space is organized as a hierarchy, with more-specific (smaller) pre
|
||||
|
||||
The root of the IPv4 hierarchy is 0.0.0.0/0, which encompasses all possible IPv4 addresses (and similarly, ::/0 for IPv6). However, even the largest organizations use only a small fraction of the global address space. Therefore, it makes sense to track in NetBox only the address space which is of interest to your organization.
|
||||
|
||||
Aggregates serve as arbitrary top-level nodes in the IP space hierarchy. They allow you to easily construct your IP scheme without any clutter of unused address space. For instance, most organizations utilize some portion of the RFC 1918 private IPv4 space. So, you might define three aggregates for this space:
|
||||
Aggregates serve as arbitrary top-level nodes in the IP space hierarchy. They allow you to easily construct your IP scheme without any clutter of unused address space. For instance, most organizations utilize some portion of the private IPv4 space set aside in RFC 1918. So, you might define three aggregates for this space:
|
||||
|
||||
* 10.0.0.0/8
|
||||
* 172.16.0.0/12
|
||||
* 192.168.0.0/16
|
||||
|
||||
Additionally, you might define an aggregate for each large swath of public IPv4 space your organization uses. You'd also create aggregates for both globally routable and unique local IPv6 space.
|
||||
Additionally, you might define an aggregate for each large swath of public IPv4 space your organization uses. You'd also create aggregates for both globally routable and unique local IPv6 space. (Most organizations will not have a need to track IPv6 link local space.)
|
||||
|
||||
Any prefixes you create in NetBox (discussed below) will be automatically organized under their respective aggregates. Any space within an aggregate which is not covered by an existing prefix will be annotated as available for allocation.
|
||||
Prefixes you create in NetBox (discussed below) will be automatically organized under their respective aggregates. Any space within an aggregate which is not covered by an existing prefix will be annotated as available for allocation. Total utilization for each aggregate is displayed in the aggregates list.
|
||||
|
||||
Aggregates cannot overlap with one another; they can only exist in parallel. 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.
|
||||
Aggregates cannot overlap with one another; they can only exist in parallel. 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.
|
||||
|
||||
### RIRs
|
||||
|
||||
Regional Internet Registries (RIRs) are responsible for the allocation of global address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for private or internal use only, 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.
|
||||
|
||||
Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own).
|
||||
Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own). Each RIR can be annotated as representing only private space.
|
||||
|
||||
---
|
||||
|
||||
@ -44,7 +47,7 @@ A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 19
|
||||
|
||||
Each prefix may be assigned to one VRF; prefixes not assigned to a VRF are assigned to the "global" table. Prefixes are also organized under their respective aggregates, irrespective of VRF assignment.
|
||||
|
||||
A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefixes assigned to it. This can be helpful is replicating real-world IP assignments. Each prefix may also be assigned a short description.
|
||||
A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefixes assigned to it. Each prefix may also be assigned a short description.
|
||||
|
||||
### Statuses
|
||||
|
||||
@ -52,7 +55,7 @@ Each prefix is assigned an operational status. This is one of the following:
|
||||
|
||||
* Container - A summary of child prefixes
|
||||
* Active - Provisioned and in use
|
||||
* Reserved - Earmarked for future use
|
||||
* Reserved - Designated for future use
|
||||
* Deprecated - No longer in use
|
||||
|
||||
### Roles
|
||||
@ -65,30 +68,32 @@ Whereas a status describes a prefix's operational state, a role describes its fu
|
||||
* Lab
|
||||
* Out-of-band
|
||||
|
||||
Role assignment is optional and you are free to create as many as you'd like.
|
||||
Role assignment is optional and roles are fully customizable.
|
||||
|
||||
---
|
||||
|
||||
# IP Addresses
|
||||
|
||||
An IP address comprises a single address (either IPv4 or IPv6) and its mask. Its mask should match exactly how the IP address is configured on an interface in the real world.
|
||||
An IP address comprises a single 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 (or it will appear in the "global" table). IP addresses are automatically organized under parent prefixes within their respective VRFs. Each IP address can also be assigned a short description.
|
||||
|
||||
Each IP address can optionally be assigned to a device's interface; an interface may have multiple IP addresses assigned to it. Further, each device may have one of its interface IPs designated as its primary IP address.
|
||||
An IP address can be assigned to a device's interface; an interface may have multiple IP addresses assigned to it. Further, each device may have one of its interface IPs designated as its primary IP address (for both IPv4 and IPv6).
|
||||
|
||||
One IP address can be designated as the network address translation (NAT) IP address for exactly one other IP address. This is useful primarily is denoting the public address for a private internal IP. Tracking one-to-many NAT (or PAT) assignments is not currently supported.
|
||||
One IP address can be designated as the network address translation (NAT) IP address for exactly one other IP address. This is useful primarily to denote the public address for a private internal IP. Tracking one-to-many NAT (or PAT) assignments is not supported.
|
||||
|
||||
---
|
||||
|
||||
# VLANs
|
||||
|
||||
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094). Note that while it is good practice, neither VLAN names nor IDs must be unique within a site. This is to accommodate the fact that many real-world network use less-than-optimal VLAN allocations and may have overlapping VLAN ID assignments in practice.
|
||||
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. Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role, and may include a short description.
|
||||
|
||||
Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
|
||||
### VLAN Groups
|
||||
|
||||
VLAN groups can be employed for administrative organization within NetBox. 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 within a site.
|
||||
|
||||
---
|
||||
|
||||
# Services
|
||||
|
||||
A service represents a TCP or UDP service available on a device. Each service must be defined with a name, protocol, and port number; for example, SSH (TCP/22). A service may optionally be bound to one or more specific IP addresses belonging to a device. (If no IP addresses are bound, the service is assumed to be reachable via any IP address.)
|
||||
A service represents a TCP or UDP service available on a device. Each service must be defined with a name, protocol, and port number; for example, "SSH (TCP/22)." A service may optionally be bound to one or more specific IP addresses belonging to a device. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)
|
||||
|
@ -24,11 +24,11 @@ Roles are also used to control access to secrets. Each role is assigned an arbit
|
||||
|
||||
Each user within NetBox can associate his or her account with an RSA public key. If activated by an administrator, this user key will contain a unique, encrypted copy of the AES master key needed to retrieve secret data.
|
||||
|
||||
User keys may be created by users individually, however they are of no use until they have been activated by a user who already has access to retrieve secret data.
|
||||
User keys may be created by users individually, however they are of no use until they have been activated by a user who already possesses an active user key.
|
||||
|
||||
## Creating the First User Key
|
||||
|
||||
When NetBox is first installed, it contains no encryption keys. Before it can store secrets, a user (typically the super user) must create a user key. This can be done by navigating to Profile > User Key.
|
||||
When NetBox is first installed, it contains no encryption keys. Before it can store secrets, a user (typically the superuser) must create a user key. This can be done by navigating to Profile > User Key.
|
||||
|
||||
To create a user key, you can either generate a new RSA key pair, or upload the public key belonging to a pair you already have. If generating a new key pair, **you must save the private key** locally before saving your new user key. Once your user key has been created, its public key will be displayed under your profile.
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
NetBox supports the concept of individual tenants within its parent organization. Typically, these are used to represent individual customers or internal departments.
|
||||
NetBox supports the assignment of resources to tenant organizations. Typically, these are used to represent individual customers of or internal departments within the organization using NetBox.
|
||||
|
||||
# Tenants
|
||||
|
||||
A tenant represents a discrete organization. Certain resources within NetBox can be assigned to a tenant. This makes it very convenient to track which resources are assigned to which customers, for instance.
|
||||
|
||||
The following objects can be assigned to tenants:
|
||||
A tenant represents a discrete organization. The following objects can be assigned to tenants:
|
||||
|
||||
* Sites
|
||||
* Racks
|
||||
|
@ -27,6 +27,12 @@ If you followed the original installation guide to set up gunicorn, be sure to c
|
||||
# cp /opt/netbox-X.Y.Z/gunicorn_config.py /opt/netbox/gunicorn_config.py
|
||||
```
|
||||
|
||||
Copy the LDAP configuration if using LDAP:
|
||||
|
||||
```no-highlight
|
||||
# cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ldap_config.py
|
||||
```
|
||||
|
||||
## Option B: Clone the Git Repository (latest master release)
|
||||
|
||||
This guide assumes that NetBox is installed at `/opt/netbox`. Pull down the most recent iteration of the master branch:
|
||||
|
@ -11,8 +11,8 @@ from .models import Provider, Circuit, CircuitType
|
||||
|
||||
|
||||
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -31,7 +31,9 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
model = Provider
|
||||
fields = ['name', 'account', 'asn']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(account__icontains=value) |
|
||||
@ -40,8 +42,8 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -93,7 +95,9 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
model = Circuit
|
||||
fields = ['install_date']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(cid__icontains=value) |
|
||||
Q(terminations__xconnect_id__icontains=value) |
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
||||
from dcim.models import Site, Device, Interface, Rack, VIRTUAL_IFACE_TYPES
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
@ -64,6 +64,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Provider
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
|
||||
asn = forms.IntegerField(required=False, label='ASN')
|
||||
|
||||
|
||||
#
|
||||
@ -128,14 +129,23 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Circuit
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug')
|
||||
provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug')
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
||||
to_field_name='slug')
|
||||
type = FilterChoiceField(
|
||||
queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
provider = FilterChoiceField(
|
||||
queryset=Provider.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -143,19 +153,49 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
#
|
||||
|
||||
class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack',
|
||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
display_field='display_name', attrs={'filter-for': 'interface'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'rack'}
|
||||
)
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
label='Rack',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'interface'}
|
||||
)
|
||||
)
|
||||
livesearch = forms.CharField(
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=Livesearch(
|
||||
query_key='q',
|
||||
query_url='dcim-api:device_list',
|
||||
field_to_update='device'
|
||||
)
|
||||
)
|
||||
interface = forms.ModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
label='Interface',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
||||
disabled_indicator='is_connected'
|
||||
)
|
||||
)
|
||||
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical',
|
||||
disabled_indicator='is_connected'))
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
@ -197,14 +237,18 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
# Limit interface choices
|
||||
if self.is_bound and self.data.get('device'):
|
||||
interfaces = Interface.objects.filter(device=self.data['device'])\
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
||||
'connected_as_b')
|
||||
interfaces = Interface.objects.filter(device=self.data['device']).exclude(
|
||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||
).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
)
|
||||
self.fields['interface'].widget.attrs['initial'] = self.data.get('interface')
|
||||
elif self.initial.get('device'):
|
||||
interfaces = Interface.objects.filter(device=self.initial['device'])\
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit_termination', 'connected_as_a',
|
||||
'connected_as_b')
|
||||
interfaces = Interface.objects.filter(device=self.initial['device']).exclude(
|
||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||
).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
)
|
||||
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||
else:
|
||||
interfaces = []
|
||||
|
@ -31,7 +31,8 @@ class ProviderListView(ObjectListView):
|
||||
def provider(request, slug):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
circuits = Circuit.objects.filter(provider=provider)
|
||||
circuits = Circuit.objects.filter(provider=provider).select_related('type', 'tenant')\
|
||||
.prefetch_related('terminations__site')
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
@ -118,9 +119,17 @@ class CircuitListView(ObjectListView):
|
||||
|
||||
def circuit(request, pk):
|
||||
|
||||
circuit = get_object_or_404(Circuit, pk=pk)
|
||||
termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
|
||||
termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
|
||||
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
|
||||
termination_a = CircuitTermination.objects.select_related(
|
||||
'site__region', 'interface__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=TERM_SIDE_A
|
||||
).first()
|
||||
termination_z = CircuitTermination.objects.select_related(
|
||||
'site__region', 'interface__device'
|
||||
).filter(
|
||||
circuit=circuit, term_side=TERM_SIDE_Z
|
||||
).first()
|
||||
|
||||
return render(request, 'circuits/circuit.html', {
|
||||
'circuit': circuit,
|
||||
@ -224,9 +233,9 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
fields_initial = ['term_side']
|
||||
template_name = 'circuits/circuittermination_edit.html'
|
||||
|
||||
def alter_obj(self, obj, args, kwargs):
|
||||
if 'circuit' in kwargs:
|
||||
obj.circuit = get_object_or_404(Circuit, pk=kwargs['circuit'])
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
if 'circuit' in url_kwargs:
|
||||
obj.circuit = get_object_or_404(Circuit, pk=url_kwargs['circuit'])
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
|
@ -1,13 +1,24 @@
|
||||
from django.contrib import admin
|
||||
from django.db.models import Count
|
||||
|
||||
from mptt.admin import MPTTModelAdmin
|
||||
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
|
||||
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site,
|
||||
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region,
|
||||
Site,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Region)
|
||||
class RegionAdmin(MPTTModelAdmin):
|
||||
list_display = ['name', 'parent', 'slug']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(Site)
|
||||
class SiteAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'facility', 'asn']
|
||||
@ -37,6 +48,11 @@ class RackAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']
|
||||
|
||||
|
||||
@admin.register(RackReservation)
|
||||
class RackRackReservationAdmin(admin.ModelAdmin):
|
||||
list_display = ['rack', 'units', 'description', 'user', 'created']
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
|
@ -4,23 +4,42 @@ from ipam.models import IPAddress
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
|
||||
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
|
||||
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
|
||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_FRONT,
|
||||
RACK_FACE_REAR, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
|
||||
)
|
||||
from extras.api.serializers import CustomFieldSerializer
|
||||
from tenancy.api.serializers import TenantNestedSerializer
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
#
|
||||
|
||||
class RegionNestedSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class RegionSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ['id', 'name', 'slug', 'parent']
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
region = RegionNestedSerializer()
|
||||
tenant = TenantNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||
fields = ['id', 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||
'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes',
|
||||
'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
||||
|
||||
@ -70,6 +89,12 @@ class RackRoleNestedSerializer(RackRoleSerializer):
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackReservationNestedSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['id', 'units', 'created', 'user', 'description']
|
||||
|
||||
|
||||
class RackSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
site = SiteNestedSerializer()
|
||||
@ -92,10 +117,11 @@ class RackNestedSerializer(RackSerializer):
|
||||
class RackDetailSerializer(RackSerializer):
|
||||
front_units = serializers.SerializerMethodField()
|
||||
rear_units = serializers.SerializerMethodField()
|
||||
reservations = RackReservationNestedSerializer(many=True)
|
||||
|
||||
class Meta(RackSerializer.Meta):
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
|
||||
'u_height', 'desc_units', 'comments', 'custom_fields', 'front_units', 'rear_units']
|
||||
'u_height', 'desc_units', 'reservations', 'comments', 'custom_fields', 'front_units', 'rear_units']
|
||||
|
||||
def get_front_units(self, obj):
|
||||
units = obj.get_rack_units(face=RACK_FACE_FRONT)
|
||||
@ -110,6 +136,18 @@ class RackDetailSerializer(RackSerializer):
|
||||
return units
|
||||
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationSerializer(serializers.ModelSerializer):
|
||||
rack = RackNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
@ -256,6 +294,7 @@ class DeviceSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
device_role = DeviceRoleNestedSerializer()
|
||||
tenant = TenantNestedSerializer()
|
||||
platform = PlatformNestedSerializer()
|
||||
site = SiteNestedSerializer()
|
||||
rack = RackNestedSerializer()
|
||||
primary_ip = DeviceIPAddressNestedSerializer()
|
||||
primary_ip4 = DeviceIPAddressNestedSerializer()
|
||||
@ -264,9 +303,11 @@ class DeviceSerializer(CustomFieldSerializer, serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
|
||||
'asset_tag', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
|
||||
'primary_ip6', 'comments', 'custom_fields']
|
||||
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',
|
||||
'comments', 'custom_fields',
|
||||
]
|
||||
|
||||
def get_parent_device(self, obj):
|
||||
try:
|
||||
@ -368,13 +409,24 @@ class PowerPortNestedSerializer(PowerPortSerializer):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
class LAGInterfaceNestedSerializer(serializers.ModelSerializer):
|
||||
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected']
|
||||
fields = ['id', 'name', 'form_factor']
|
||||
|
||||
|
||||
class InterfaceSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
|
||||
lag = LAGInterfaceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'is_connected',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceNestedSerializer(InterfaceSerializer):
|
||||
@ -388,8 +440,10 @@ class InterfaceDetailSerializer(InterfaceSerializer):
|
||||
connected_interface = InterfaceSerializer()
|
||||
|
||||
class Meta(InterfaceSerializer.Meta):
|
||||
fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected',
|
||||
'connected_interface']
|
||||
fields = [
|
||||
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'is_connected',
|
||||
'connected_interface',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
|
@ -8,6 +8,10 @@ from .views import *
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# Regions
|
||||
url(r'^regions/$', RegionListView.as_view(), name='region_list'),
|
||||
url(r'^regions/(?P<pk>\d+)/$', RegionDetailView.as_view(), name='region_detail'),
|
||||
|
||||
# Sites
|
||||
url(r'^sites/$', SiteListView.as_view(), name='site_list'),
|
||||
url(r'^sites/(?P<pk>\d+)/$', SiteDetailView.as_view(), name='site_detail'),
|
||||
@ -27,6 +31,10 @@ urlpatterns = [
|
||||
url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),
|
||||
url(r'^racks/(?P<pk>\d+)/rack-units/$', RackUnitListView.as_view(), name='rack_units'),
|
||||
|
||||
# Rack reservations
|
||||
url(r'^rack-reservations/$', RackReservationListView.as_view(), name='rackreservation_list'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/$', RackReservationDetailView.as_view(), name='rackreservation_detail'),
|
||||
|
||||
# Manufacturers
|
||||
url(r'^manufacturers/$', ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
url(r'^manufacturers/(?P<pk>\d+)/$', ManufacturerDetailView.as_view(), name='manufacturer_detail'),
|
||||
|
@ -10,8 +10,9 @@ from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
|
||||
InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, Interface, InterfaceConnection,
|
||||
Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
||||
VIRTUAL_IFACE_TYPES,
|
||||
)
|
||||
from dcim import filters
|
||||
from extras.api.views import CustomFieldModelAPIView
|
||||
@ -21,6 +22,26 @@ from .exceptions import MissingFilterException
|
||||
from . import serializers
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
#
|
||||
|
||||
class RegionListView(generics.ListAPIView):
|
||||
"""
|
||||
List all regions
|
||||
"""
|
||||
queryset = Region.objects.all()
|
||||
serializer_class = serializers.RegionSerializer
|
||||
|
||||
|
||||
class RegionDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single region
|
||||
"""
|
||||
queryset = Region.objects.all()
|
||||
serializer_class = serializers.RegionSerializer
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
@ -134,6 +155,27 @@ class RackUnitListView(APIView):
|
||||
return Response(elevation)
|
||||
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationListView(generics.ListAPIView):
|
||||
"""
|
||||
List all rack reservation
|
||||
"""
|
||||
queryset = RackReservation.objects.all()
|
||||
serializer_class = serializers.RackReservationSerializer
|
||||
filter_class = filters.RackReservationFilter
|
||||
|
||||
|
||||
class RackReservationDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single rack reservation
|
||||
"""
|
||||
queryset = RackReservation.objects.all()
|
||||
serializer_class = serializers.RackReservationSerializer
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
@ -337,9 +379,9 @@ class InterfaceListView(generics.ListAPIView):
|
||||
# Filter by type (physical or virtual)
|
||||
iface_type = self.request.query_params.get('type')
|
||||
if iface_type == 'physical':
|
||||
queryset = queryset.exclude(form_factor=IFACE_FF_VIRTUAL)
|
||||
queryset = queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES)
|
||||
elif iface_type == 'virtual':
|
||||
queryset = queryset.filter(form_factor=IFACE_FF_VIRTUAL)
|
||||
queryset = queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES)
|
||||
elif iface_type is not None:
|
||||
queryset = queryset.empty()
|
||||
|
||||
|
@ -7,16 +7,28 @@ from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from .models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
|
||||
Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
||||
VIRTUAL_IFACE_TYPES,
|
||||
)
|
||||
|
||||
|
||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = NullableModelMultipleChoiceFilter(
|
||||
name='region',
|
||||
queryset=Region.objects.all(),
|
||||
label='Region (ID)',
|
||||
)
|
||||
region = NullableModelMultipleChoiceFilter(
|
||||
name='region',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
@ -33,9 +45,16 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
model = Site
|
||||
fields = ['q', 'name', 'facility', 'asn']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
|
||||
Q(shipping_address__icontains=value) | Q(comments__icontains=value)
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(facility__icontains=value) |
|
||||
Q(physical_address__icontains=value) |
|
||||
Q(shipping_address__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
try:
|
||||
qs_filter |= Q(asn=int(value.strip()))
|
||||
except ValueError:
|
||||
@ -58,11 +77,12 @@ class RackGroupFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -114,7 +134,9 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
model = Rack
|
||||
fields = ['u_height']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(facility_id__icontains=value) |
|
||||
@ -122,9 +144,21 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class RackReservationFilter(django_filters.FilterSet):
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack',
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['rack', 'user']
|
||||
|
||||
|
||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -141,10 +175,13 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device',
|
||||
'subdevice_role']
|
||||
fields = [
|
||||
'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
|
||||
]
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(manufacturer__name__icontains=value) |
|
||||
Q(model__icontains=value) |
|
||||
@ -154,21 +191,21 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
mac_address = django_filters.MethodFilter(
|
||||
action='_mac_address',
|
||||
mac_address = django_filters.CharFilter(
|
||||
method='_mac_address',
|
||||
label='MAC address',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site__slug',
|
||||
name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
@ -178,7 +215,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Rack group (ID)',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
rack_id = NullableModelMultipleChoiceFilter(
|
||||
name='rack',
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
@ -259,7 +296,9 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
model = Device
|
||||
fields = ['name', 'serial', 'asset_tag']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
@ -268,7 +307,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
Q(comments__icontains=value)
|
||||
).distinct()
|
||||
|
||||
def _mac_address(self, queryset, value):
|
||||
def _mac_address(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
@ -362,58 +401,72 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
type = django_filters.CharFilter(
|
||||
method='filter_type',
|
||||
label='Interface type',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['name']
|
||||
|
||||
def filter_type(self, queryset, name, value):
|
||||
value = value.strip().lower()
|
||||
if value == 'physical':
|
||||
return queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES)
|
||||
elif value == 'virtual':
|
||||
return queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES)
|
||||
elif value == 'lag':
|
||||
return queryset.filter(form_factor=IFACE_FF_LAG)
|
||||
return queryset
|
||||
|
||||
|
||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = []
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(cs_port__device__rack__site__slug=value)
|
||||
return queryset.filter(cs_port__device__site__slug=value)
|
||||
|
||||
|
||||
class PowerConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = []
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(power_outlet__device__rack__site__slug=value)
|
||||
return queryset.filter(power_outlet__device__site__slug=value)
|
||||
|
||||
|
||||
class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = []
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(interface_a__device__rack__site__slug=value) |
|
||||
Q(interface_b__device__rack__site__slug=value)
|
||||
Q(interface_a__device__site__slug=value) |
|
||||
Q(interface_b__device__site__slug=value)
|
||||
)
|
||||
|
@ -1915,6 +1915,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-edge1",
|
||||
"serial": "5555555555",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 1,
|
||||
"face": 0,
|
||||
@ -1935,6 +1936,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-core1",
|
||||
"serial": "",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 17,
|
||||
"face": 0,
|
||||
@ -1955,6 +1957,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-spine1",
|
||||
"serial": "",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 33,
|
||||
"face": 0,
|
||||
@ -1975,6 +1978,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-leaf1",
|
||||
"serial": "",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 34,
|
||||
"face": 0,
|
||||
@ -1995,6 +1999,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-leaf2",
|
||||
"serial": "9823478293748",
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 34,
|
||||
"face": 0,
|
||||
@ -2015,6 +2020,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-spine2",
|
||||
"serial": "45649818158",
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 33,
|
||||
"face": 0,
|
||||
@ -2035,6 +2041,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-edge2",
|
||||
"serial": "7567356345",
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 1,
|
||||
"face": 0,
|
||||
@ -2055,6 +2062,7 @@
|
||||
"platform": 1,
|
||||
"name": "test1-core2",
|
||||
"serial": "67856734534",
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": 17,
|
||||
"face": 0,
|
||||
@ -2075,6 +2083,7 @@
|
||||
"platform": 2,
|
||||
"name": "test1-oob1",
|
||||
"serial": "98273942938",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": 42,
|
||||
"face": 0,
|
||||
@ -2095,6 +2104,7 @@
|
||||
"platform": null,
|
||||
"name": "test1-pdu1",
|
||||
"serial": "",
|
||||
"site": 1,
|
||||
"rack": 1,
|
||||
"position": null,
|
||||
"face": null,
|
||||
@ -2115,6 +2125,7 @@
|
||||
"platform": null,
|
||||
"name": "test1-pdu2",
|
||||
"serial": "",
|
||||
"site": 1,
|
||||
"rack": 2,
|
||||
"position": null,
|
||||
"face": null,
|
||||
|
@ -1,6 +1,9 @@
|
||||
import re
|
||||
|
||||
from mptt.forms import TreeNodeChoiceField
|
||||
|
||||
from django import forms
|
||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count, Q
|
||||
|
||||
@ -8,18 +11,19 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField, CSVDataField,
|
||||
ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea,
|
||||
SlugField,
|
||||
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
|
||||
CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
|
||||
SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField,
|
||||
)
|
||||
|
||||
from .formfields import MACAddressFormField
|
||||
from .models import (
|
||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||
Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
||||
RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
|
||||
VIRTUAL_IFACE_TYPES
|
||||
)
|
||||
|
||||
|
||||
@ -52,18 +56,42 @@ def validate_connection_status(value):
|
||||
raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
|
||||
|
||||
|
||||
class DeviceComponentForm(BootstrapMixin, forms.Form):
|
||||
"""
|
||||
Allow inclusion of the parent device as context for limiting field choices.
|
||||
"""
|
||||
def __init__(self, device, *args, **kwargs):
|
||||
self.device = device
|
||||
super(DeviceComponentForm, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
#
|
||||
|
||||
class RegionForm(BootstrapMixin, forms.ModelForm):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
fields = ['parent', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteForm(BootstrapMixin, CustomFieldForm):
|
||||
region = TreeNodeChoiceField(queryset=Region.objects.all())
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name',
|
||||
'contact_phone', 'contact_email', 'comments']
|
||||
fields = [
|
||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||
'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
]
|
||||
widgets = {
|
||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||
'shipping_address': SmallTextarea(attrs={'rows': 3}),
|
||||
@ -78,12 +106,22 @@ class SiteForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
|
||||
class SiteFromCSVForm(forms.ModelForm):
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
region = forms.ModelChoiceField(
|
||||
Region.objects.all(), to_field_name='name', required=False, error_messages={
|
||||
'invalid_choice': 'Tenant not found.'
|
||||
}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
Tenant.objects.all(), to_field_name='name', required=False, error_messages={
|
||||
'invalid_choice': 'Tenant not found.'
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
|
||||
fields = [
|
||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
|
||||
]
|
||||
|
||||
|
||||
class SiteImportForm(BootstrapMixin, BulkImportForm):
|
||||
@ -92,18 +130,27 @@ class SiteImportForm(BootstrapMixin, BulkImportForm):
|
||||
|
||||
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
|
||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||
asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['tenant', 'asn']
|
||||
nullable_fields = ['region', 'tenant', 'asn']
|
||||
|
||||
|
||||
class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Site
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
region = FilterTreeNodeMultipleChoiceField(
|
||||
queryset=Region.objects.annotate(filter_count=Count('sites')),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.annotate(filter_count=Count('sites')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -234,13 +281,53 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Rack
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks')), to_field_name='slug')
|
||||
group_id = FilterChoiceField(queryset=RackGroup.objects.select_related('site')
|
||||
.annotate(filter_count=Count('racks')), label='Rack group', null_option=(0, 'None'))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
role = FilterChoiceField(queryset=RackRole.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.annotate(filter_count=Count('racks')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
group_id = FilterChoiceField(
|
||||
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')),
|
||||
label='Rack group',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.annotate(filter_count=Count('racks')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
queryset=RackRole.objects.annotate(filter_count=Count('racks')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationForm(BootstrapMixin, forms.ModelForm):
|
||||
units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10}))
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['units', 'description']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(RackReservationForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Populate rack unit choices
|
||||
self.fields['units'].widget.choices = self._get_unit_choices()
|
||||
|
||||
def _get_unit_choices(self):
|
||||
rack = self.instance.rack
|
||||
reserved_units = []
|
||||
for resv in rack.reservations.exclude(pk=self.instance.pk):
|
||||
for u in resv.units:
|
||||
reserved_units.append(u)
|
||||
unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units]
|
||||
return unit_choices
|
||||
|
||||
|
||||
#
|
||||
@ -284,8 +371,10 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = DeviceType
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||
to_field_name='slug')
|
||||
manufacturer = FilterChoiceField(
|
||||
queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -302,7 +391,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
class ConsolePortTemplateCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
@ -316,7 +405,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
class ConsoleServerPortTemplateCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
@ -330,7 +419,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
class PowerPortTemplateCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
@ -344,7 +433,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
class PowerOutletTemplateCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
@ -358,7 +447,7 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
class InterfaceTemplateCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||
@ -382,7 +471,7 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||
class DeviceBayTemplateCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
@ -416,7 +505,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), widget=APISelect(
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'position'}
|
||||
@ -453,7 +542,7 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
if self.instance.pk:
|
||||
|
||||
# Initialize helper selections
|
||||
self.initial['site'] = self.instance.rack.site
|
||||
self.initial['site'] = self.instance.site
|
||||
self.initial['manufacturer'] = self.instance.device_type.manufacturer
|
||||
|
||||
# Compile list of choices for primary IPv4 and IPv6 addresses
|
||||
@ -520,7 +609,7 @@ class DeviceForm(BootstrapMixin, CustomFieldForm):
|
||||
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
|
||||
self.fields['site'].disabled = True
|
||||
self.fields['rack'].disabled = True
|
||||
self.initial['site'] = self.instance.parent_bay.device.rack.site_id
|
||||
self.initial['site'] = self.instance.parent_bay.device.site_id
|
||||
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
||||
|
||||
|
||||
@ -556,7 +645,7 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
|
||||
'invalid_choice': 'Invalid site name.',
|
||||
})
|
||||
rack_name = forms.CharField()
|
||||
rack_name = forms.CharField(required=False)
|
||||
face = forms.CharField(required=False)
|
||||
|
||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||
@ -649,7 +738,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
)
|
||||
rack_group_id = FilterChoiceField(
|
||||
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')),
|
||||
label='Rack Group',
|
||||
label='Rack group',
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
|
||||
@ -714,14 +803,18 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ConsolePortCreateForm(BootstrapMixin, forms.Form):
|
||||
class ConsolePortCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class ConsoleConnectionCSVForm(forms.Form):
|
||||
console_server = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_console_server=True),
|
||||
to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Console server not found'})
|
||||
console_server = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.filter(device_type__is_console_server=True),
|
||||
to_field_name='name',
|
||||
error_messages={
|
||||
'invalid_choice': 'Console server not found',
|
||||
}
|
||||
)
|
||||
cs_port = forms.CharField()
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
@ -786,22 +879,49 @@ class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||
|
||||
|
||||
class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'console_server'}))
|
||||
console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'cs_port'}))
|
||||
livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='console_server')
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.HiddenInput(),
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'console_server', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
console_server = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Console Server',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}&is_console_server=True',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'cs_port'}
|
||||
)
|
||||
)
|
||||
livesearch = forms.CharField(
|
||||
required=False,
|
||||
label='Console Server',
|
||||
widget=Livesearch(
|
||||
query_key='q',
|
||||
query_url='dcim-api:device_list',
|
||||
field_to_update='console_server',
|
||||
)
|
||||
)
|
||||
cs_port = forms.ModelChoiceField(
|
||||
queryset=ConsoleServerPort.objects.all(),
|
||||
label='Port',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/{{console_server}}/console-server-ports/',
|
||||
disabled_indicator='connected_console',
|
||||
)
|
||||
)
|
||||
cs_port = forms.ModelChoiceField(queryset=ConsoleServerPort.objects.all(), label='Port',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{console_server}}/console-server-ports/',
|
||||
disabled_indicator='connected_console'))
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
|
||||
fields = ['site', 'rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
|
||||
labels = {
|
||||
'cs_port': 'Port',
|
||||
'connection_status': 'Status',
|
||||
@ -814,17 +934,22 @@ class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
if not self.instance.pk:
|
||||
raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
|
||||
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
|
||||
self.initial['site'] = self.instance.device.site
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.site)
|
||||
self.fields['cs_port'].required = True
|
||||
self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
|
||||
|
||||
# Initialize console server choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['console_server'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_console_server=True)
|
||||
self.fields['console_server'].queryset = Device.objects.filter(rack=self.data['rack'],
|
||||
device_type__is_console_server=True)
|
||||
elif self.initial.get('rack'):
|
||||
self.fields['console_server'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_console_server=True)
|
||||
self.fields['console_server'].queryset = Device.objects.filter(rack=self.initial['rack'],
|
||||
device_type__is_console_server=True)
|
||||
else:
|
||||
self.fields['console_server'].choices = []
|
||||
self.fields['console_server'].queryset = Device.objects.filter(site=self.instance.device.site,
|
||||
rack__isnull=True,
|
||||
device_type__is_console_server=True)
|
||||
|
||||
# Initialize CS port choices
|
||||
if self.is_bound:
|
||||
@ -849,27 +974,61 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form):
|
||||
class ConsoleServerPortCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
display_field='display_name', attrs={'filter-for': 'port'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.HiddenInput(),
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'port'}
|
||||
)
|
||||
)
|
||||
livesearch = forms.CharField(
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=Livesearch(
|
||||
query_key='q',
|
||||
query_url='dcim-api:device_list',
|
||||
field_to_update='device'
|
||||
)
|
||||
)
|
||||
port = forms.ModelChoiceField(
|
||||
queryset=ConsolePort.objects.all(),
|
||||
label='Port',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/{{device}}/console-ports/',
|
||||
disabled_indicator='cs_port'
|
||||
)
|
||||
)
|
||||
connection_status = forms.BooleanField(
|
||||
required=False,
|
||||
initial=CONNECTION_STATUS_CONNECTED,
|
||||
label='Status',
|
||||
widget=forms.Select(
|
||||
choices=CONNECTION_STATUS_CHOICES
|
||||
)
|
||||
)
|
||||
port = forms.ModelChoiceField(queryset=ConsolePort.objects.all(), label='Port',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/console-ports/',
|
||||
disabled_indicator='cs_port'))
|
||||
connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
|
||||
widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
|
||||
|
||||
class Meta:
|
||||
fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
|
||||
fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
|
||||
labels = {
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
@ -878,7 +1037,8 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
|
||||
|
||||
super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=consoleserverport.device.rack.site)
|
||||
self.initial['site'] = consoleserverport.device.site
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=consoleserverport.device.site)
|
||||
|
||||
# Initialize device choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
@ -886,7 +1046,8 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
|
||||
elif self.initial.get('rack', None):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
self.fields['device'].queryset = Device.objects.filter(site=consoleserverport.device.site,
|
||||
rack__isnull=True)
|
||||
|
||||
# Initialize port choices
|
||||
if self.is_bound:
|
||||
@ -911,13 +1072,18 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PowerPortCreateForm(BootstrapMixin, forms.Form):
|
||||
class PowerPortCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class PowerConnectionCSVForm(forms.Form):
|
||||
pdu = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_pdu=True), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'PDU not found.'})
|
||||
pdu = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.filter(device_type__is_pdu=True),
|
||||
to_field_name='name',
|
||||
error_messages={
|
||||
'invalid_choice': 'PDU not found.',
|
||||
}
|
||||
)
|
||||
power_outlet = forms.CharField()
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
@ -983,21 +1149,46 @@ class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||
|
||||
|
||||
class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'pdu'}))
|
||||
pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True',
|
||||
display_field='display_name', attrs={'filter-for': 'power_outlet'}))
|
||||
livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='pdu')
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.HiddenInput())
|
||||
rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'pdu', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
pdu = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='PDU',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}&is_pdu=True',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'power_outlet'}
|
||||
)
|
||||
)
|
||||
livesearch = forms.CharField(
|
||||
required=False,
|
||||
label='PDU',
|
||||
widget=Livesearch(
|
||||
query_key='q',
|
||||
query_url='dcim-api:device_list',
|
||||
field_to_update='pdu'
|
||||
)
|
||||
)
|
||||
power_outlet = forms.ModelChoiceField(
|
||||
queryset=PowerOutlet.objects.all(),
|
||||
label='Outlet',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/{{pdu}}/power-outlets/',
|
||||
disabled_indicator='connected_port'
|
||||
)
|
||||
)
|
||||
power_outlet = forms.ModelChoiceField(queryset=PowerOutlet.objects.all(), label='Outlet',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{pdu}}/power-outlets/',
|
||||
disabled_indicator='connected_port'))
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
|
||||
fields = ['site', 'rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
|
||||
labels = {
|
||||
'power_outlet': 'Outlet',
|
||||
'connection_status': 'Status',
|
||||
@ -1010,17 +1201,22 @@ class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
if not self.instance.pk:
|
||||
raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
|
||||
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
|
||||
self.initial['site'] = self.instance.device.site
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.site)
|
||||
self.fields['power_outlet'].required = True
|
||||
self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
|
||||
|
||||
# Initialize PDU choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['pdu'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_pdu=True)
|
||||
self.fields['pdu'].queryset = Device.objects.filter(rack=self.data['rack'],
|
||||
device_type__is_pdu=True)
|
||||
elif self.initial.get('rack', None):
|
||||
self.fields['pdu'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_pdu=True)
|
||||
self.fields['pdu'].queryset = Device.objects.filter(rack=self.initial['rack'],
|
||||
device_type__is_pdu=True)
|
||||
else:
|
||||
self.fields['pdu'].choices = []
|
||||
self.fields['pdu'].queryset = Device.objects.filter(site=self.instance.device.site,
|
||||
rack__isnull=True,
|
||||
device_type__is_pdu=True)
|
||||
|
||||
# Initialize power outlet choices
|
||||
if self.is_bound:
|
||||
@ -1045,27 +1241,61 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletCreateForm(BootstrapMixin, forms.Form):
|
||||
class PowerOutletCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
display_field='display_name', attrs={'filter-for': 'port'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'port'}
|
||||
)
|
||||
)
|
||||
livesearch = forms.CharField(
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=Livesearch(
|
||||
query_key='q',
|
||||
query_url='dcim-api:device_list',
|
||||
field_to_update='device'
|
||||
)
|
||||
)
|
||||
port = forms.ModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
label='Port',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/{{device}}/power-ports/',
|
||||
disabled_indicator='power_outlet'
|
||||
)
|
||||
)
|
||||
connection_status = forms.BooleanField(
|
||||
required=False,
|
||||
initial=CONNECTION_STATUS_CONNECTED,
|
||||
label='Status',
|
||||
widget=forms.Select(
|
||||
choices=CONNECTION_STATUS_CHOICES
|
||||
)
|
||||
)
|
||||
port = forms.ModelChoiceField(queryset=PowerPort.objects.all(), label='Port',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/power-ports/',
|
||||
disabled_indicator='power_outlet'))
|
||||
connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
|
||||
widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
|
||||
|
||||
class Meta:
|
||||
fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
|
||||
fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
|
||||
labels = {
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
@ -1074,7 +1304,8 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
|
||||
|
||||
super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=poweroutlet.device.rack.site)
|
||||
self.initial['site'] = poweroutlet.device.site
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=poweroutlet.device.site)
|
||||
|
||||
# Initialize device choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
@ -1082,7 +1313,8 @@ class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
|
||||
elif self.initial.get('rack', None):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
self.fields['device'].queryset = Device.objects.filter(site=poweroutlet.device.site,
|
||||
rack__isnull=True)
|
||||
|
||||
# Initialize port choices
|
||||
if self.is_bound:
|
||||
@ -1101,27 +1333,65 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
||||
fields = ['device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InterfaceForm, self).__init__(*args, **kwargs)
|
||||
|
||||
class InterfaceCreateForm(BootstrapMixin, forms.Form):
|
||||
# Limit LAG choices to interfaces belonging to this device
|
||||
if self.is_bound:
|
||||
self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
|
||||
device_id=self.data['device'], form_factor=IFACE_FF_LAG
|
||||
)
|
||||
else:
|
||||
self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
|
||||
device=self.instance.device, form_factor=IFACE_FF_LAG
|
||||
)
|
||||
|
||||
|
||||
class InterfaceCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
||||
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
||||
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InterfaceCreateForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Limit LAG choices to interfaces belonging to this device
|
||||
if self.device is not None:
|
||||
self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
|
||||
device=self.device, form_factor=IFACE_FF_LAG
|
||||
)
|
||||
else:
|
||||
self.fields['lag'].queryset = Interface.objects.none()
|
||||
|
||||
|
||||
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
|
||||
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
class Meta:
|
||||
nullable_fields = ['description']
|
||||
nullable_fields = ['lag', 'description']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Limit LAG choices to interfaces which belong to the parent device.
|
||||
if self.initial.get('device'):
|
||||
self.fields['lag'].queryset = Interface.objects.filter(
|
||||
device=self.initial['device'], form_factor=IFACE_FF_LAG
|
||||
)
|
||||
else:
|
||||
self.fields['lag'].choices = []
|
||||
|
||||
|
||||
#
|
||||
@ -1129,22 +1399,55 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
#
|
||||
|
||||
class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
interface_a = forms.ChoiceField(choices=[], widget=SelectWithDisabled, label='Interface')
|
||||
site_b = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'rack_b'}))
|
||||
rack_b = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site_b}}',
|
||||
attrs={'filter-for': 'device_b'}))
|
||||
device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'interface_b'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device_b')
|
||||
interface_a = forms.ChoiceField(
|
||||
choices=[],
|
||||
widget=SelectWithDisabled,
|
||||
label='Interface'
|
||||
)
|
||||
site_b = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'rack_b'}
|
||||
)
|
||||
)
|
||||
rack_b = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site_b}}',
|
||||
attrs={'filter-for': 'device_b', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device_b = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{site_b}}&rack_id={{rack_b}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'interface_b'}
|
||||
)
|
||||
)
|
||||
livesearch = forms.CharField(
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=Livesearch(
|
||||
query_key='q',
|
||||
query_url='dcim-api:device_list',
|
||||
field_to_update='device_b'
|
||||
)
|
||||
)
|
||||
interface_b = forms.ModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
label='Interface',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/{{device_b}}/interfaces/?type=physical',
|
||||
disabled_indicator='is_connected'
|
||||
)
|
||||
)
|
||||
interface_b = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device_b}}/interfaces/?type=physical',
|
||||
disabled_indicator='is_connected'))
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
@ -1155,8 +1458,11 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Initialize interface A choices
|
||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL)\
|
||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(
|
||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||
).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
)
|
||||
self.fields['interface_a'].choices = [
|
||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
|
||||
]
|
||||
@ -1169,23 +1475,31 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
else:
|
||||
self.fields['rack_b'].choices = []
|
||||
|
||||
# Initialize device_b choices if rack_b is set
|
||||
# Initialize device_b choices if rack_b or site_b is set
|
||||
if self.is_bound and self.data.get('rack_b'):
|
||||
self.fields['device_b'].queryset = Device.objects.filter(rack__pk=self.data['rack_b'])
|
||||
elif self.is_bound and self.data.get('site_b'):
|
||||
self.fields['device_b'].queryset = Device.objects.filter(site__pk=self.data['site_b'], rack__isnull=True)
|
||||
elif self.initial.get('rack_b'):
|
||||
self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
|
||||
elif self.initial.get('site_b'):
|
||||
self.fields['device_b'].queryset = Device.objects.filter(site=self.initial['site_b'], rack__isnull=True)
|
||||
else:
|
||||
self.fields['device_b'].choices = []
|
||||
|
||||
# Initialize interface_b choices if device_b is set
|
||||
if self.is_bound:
|
||||
device_b_interfaces = Interface.objects.filter(device=self.data['device_b'])\
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
||||
device_b_interfaces = Interface.objects.filter(device=self.data['device_b']).exclude(
|
||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||
).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
)
|
||||
elif self.initial.get('device_b'):
|
||||
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b'])\
|
||||
.exclude(form_factor=IFACE_FF_VIRTUAL)\
|
||||
.select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
|
||||
device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']).exclude(
|
||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||
).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
)
|
||||
else:
|
||||
device_b_interfaces = []
|
||||
self.fields['interface_b'].choices = [
|
||||
@ -1194,13 +1508,21 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class InterfaceConnectionCSVForm(forms.Form):
|
||||
device_a = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device A not found.'})
|
||||
device_a = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device A not found.'}
|
||||
)
|
||||
interface_a = forms.CharField()
|
||||
device_b = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device B not found.'})
|
||||
device_b = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device B not found.'}
|
||||
)
|
||||
interface_b = forms.CharField()
|
||||
status = forms.CharField(validators=[validate_connection_status])
|
||||
status = forms.CharField(
|
||||
validators=[validate_connection_status]
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
@ -1295,7 +1617,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayCreateForm(BootstrapMixin, forms.Form):
|
||||
class DeviceBayCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
|
33
netbox/dcim/migrations/0026_add_rack_reservations.py
Normal file
33
netbox/dcim/migrations/0026_add_rack_reservations.py
Normal file
@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 18:43
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('dcim', '0025_devicetype_add_interface_ordering'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RackReservation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('units', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(), size=None)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('description', models.CharField(max_length=100)),
|
||||
('rack', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='dcim.Rack')),
|
||||
('user', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created'],
|
||||
},
|
||||
),
|
||||
]
|
21
netbox/dcim/migrations/0027_device_add_site.py
Normal file
21
netbox/dcim/migrations/0027_device_add_site.py
Normal file
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 21:21
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0026_add_rack_reservations'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
|
||||
),
|
||||
]
|
23
netbox/dcim/migrations/0028_device_copy_rack_to_site.py
Normal file
23
netbox/dcim/migrations/0028_device_copy_rack_to_site.py
Normal file
@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 21:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def copy_site_from_rack(apps, schema_editor):
|
||||
Device = apps.get_model('dcim', 'Device')
|
||||
for device in Device.objects.all():
|
||||
device.site = device.rack.site
|
||||
device.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0027_device_add_site'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(copy_site_from_rack),
|
||||
]
|
26
netbox/dcim/migrations/0029_allow_rackless_devices.py
Normal file
26
netbox/dcim/migrations/0029_allow_rackless_devices.py
Normal file
@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-16 21:25
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0028_device_copy_rack_to_site'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='rack',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
|
||||
),
|
||||
]
|
31
netbox/dcim/migrations/0030_interface_add_lag.py
Normal file
31
netbox/dcim/migrations/0030_interface_add_lag.py
Normal file
@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-27 19:55
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0029_allow_rackless_devices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='lag',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name=b'Parent LAG'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200),
|
||||
),
|
||||
]
|
38
netbox/dcim/migrations/0031_regions.py
Normal file
38
netbox/dcim/migrations/0031_regions.py
Normal file
@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-28 17:14
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import mptt.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0030_interface_add_lag'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Region',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, unique=True)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('lft', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('level', models.PositiveIntegerField(db_index=True, editable=False)),
|
||||
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='dcim.Region')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='region',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='dcim.Region'),
|
||||
),
|
||||
]
|
21
netbox/dcim/migrations/0032_device_increase_name_length.py
Normal file
21
netbox/dcim/migrations/0032_device_increase_name_length.py
Normal file
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-03-02 15:09
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0031_regions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='name',
|
||||
field=utilities.fields.NullableCharField(blank=True, max_length=64, null=True, unique=True),
|
||||
),
|
||||
]
|
@ -1,8 +1,12 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
@ -66,6 +70,7 @@ IFACE_ORDERING_CHOICES = [
|
||||
|
||||
# Virtual
|
||||
IFACE_FF_VIRTUAL = 0
|
||||
IFACE_FF_LAG = 200
|
||||
# Ethernet
|
||||
IFACE_FF_100ME_FIXED = 800
|
||||
IFACE_FF_1GE_FIXED = 1000
|
||||
@ -104,6 +109,7 @@ IFACE_FF_CHOICES = [
|
||||
'Virtual interfaces',
|
||||
[
|
||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
@ -146,6 +152,7 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
|
||||
[IFACE_FF_T3, 'T3 (45 Mbps)'],
|
||||
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
||||
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
@ -165,6 +172,11 @@ IFACE_FF_CHOICES = [
|
||||
],
|
||||
]
|
||||
|
||||
VIRTUAL_IFACE_TYPES = [
|
||||
IFACE_FF_VIRTUAL,
|
||||
IFACE_FF_LAG,
|
||||
]
|
||||
|
||||
STATUS_ACTIVE = True
|
||||
STATUS_OFFLINE = False
|
||||
STATUS_CHOICES = [
|
||||
@ -190,6 +202,29 @@ RPC_CLIENT_CHOICES = [
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Region(MPTTModel):
|
||||
"""
|
||||
Sites can be grouped within geographic Regions.
|
||||
"""
|
||||
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?region={}".format(reverse('dcim:site_list'), self.slug)
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
@ -208,7 +243,8 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='sites', on_delete=models.PROTECT)
|
||||
region = models.ForeignKey('Region', related_name='sites', blank=True, null=True, on_delete=models.SET_NULL)
|
||||
tenant = models.ForeignKey(Tenant, related_name='sites', blank=True, null=True, on_delete=models.PROTECT)
|
||||
facility = models.CharField(max_length=50, blank=True)
|
||||
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
||||
physical_address = models.CharField(max_length=200, blank=True)
|
||||
@ -234,6 +270,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
return csv_format([
|
||||
self.name,
|
||||
self.slug,
|
||||
self.region.name if self.region else None,
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.facility,
|
||||
self.asn,
|
||||
@ -368,6 +405,19 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Record the original site assignment for this rack.
|
||||
_site_id = None
|
||||
if self.pk:
|
||||
_site_id = Rack.objects.get(pk=self.pk).site_id
|
||||
|
||||
super(Rack, self).save(*args, **kwargs)
|
||||
|
||||
# Update racked devices if the assigned Site has been changed.
|
||||
if _site_id is not None and self.site_id != _site_id:
|
||||
Device.objects.filter(rack=self).update(site_id=self.site.pk)
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
self.site.name,
|
||||
@ -478,6 +528,50 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
return int(float(self.u_height - u_available) / self.u_height * 100)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class RackReservation(models.Model):
|
||||
"""
|
||||
One or more reserved units within a Rack.
|
||||
"""
|
||||
rack = models.ForeignKey('Rack', related_name='reservations', editable=False, on_delete=models.CASCADE)
|
||||
units = ArrayField(models.PositiveSmallIntegerField())
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
user = models.ForeignKey(User, editable=False, on_delete=models.PROTECT)
|
||||
description = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
ordering = ['created']
|
||||
|
||||
def __str__(self):
|
||||
return u"Reservation for rack {}".format(self.rack)
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.units:
|
||||
|
||||
# Validate that all specified units exist in the Rack.
|
||||
invalid_units = [u for u in self.units if u not in self.rack.units]
|
||||
if invalid_units:
|
||||
raise ValidationError({
|
||||
'units': u"Invalid unit(s) for {}U rack: {}".format(
|
||||
self.rack.u_height,
|
||||
', '.join([str(u) for u in invalid_units]),
|
||||
),
|
||||
})
|
||||
|
||||
# Check that none of the units has already been reserved for this Rack.
|
||||
reserved_units = []
|
||||
for resv in self.rack.reservations.exclude(pk=self.pk):
|
||||
reserved_units += resv.units
|
||||
conflicting_units = [u for u in self.units if u in reserved_units]
|
||||
if conflicting_units:
|
||||
raise ValidationError({
|
||||
'units': 'The following units have already been reserved: {}'.format(
|
||||
', '.join([str(u) for u in conflicting_units]),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# Device Types
|
||||
#
|
||||
@ -821,11 +915,12 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
|
||||
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='devices', on_delete=models.PROTECT)
|
||||
platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
|
||||
name = NullableCharField(max_length=50, blank=True, null=True, unique=True)
|
||||
name = NullableCharField(max_length=64, blank=True, null=True, unique=True)
|
||||
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
|
||||
asset_tag = NullableCharField(max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
|
||||
help_text='A unique tag used to identify this device')
|
||||
rack = models.ForeignKey('Rack', related_name='devices', on_delete=models.PROTECT)
|
||||
site = models.ForeignKey('Site', related_name='devices', on_delete=models.PROTECT)
|
||||
rack = models.ForeignKey('Rack', related_name='devices', blank=True, null=True, on_delete=models.PROTECT)
|
||||
position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)],
|
||||
verbose_name='Position (U)',
|
||||
help_text='The lowest-numbered unit occupied by the device')
|
||||
@ -852,41 +947,59 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate site/rack combination
|
||||
if self.rack and self.site != self.rack.site:
|
||||
raise ValidationError({
|
||||
'rack': "Rack {} does not belong to site {}.".format(self.rack, self.site),
|
||||
})
|
||||
|
||||
if self.rack is None:
|
||||
if self.face is not None:
|
||||
raise ValidationError({
|
||||
'face': "Cannot select a rack face without assigning a rack.",
|
||||
})
|
||||
if self.position:
|
||||
raise ValidationError({
|
||||
'face': "Cannot select a rack position without assigning a rack.",
|
||||
})
|
||||
|
||||
# Validate position/face combination
|
||||
if self.position and self.face is None:
|
||||
raise ValidationError({
|
||||
'face': "Must specify rack face when defining rack position."
|
||||
'face': "Must specify rack face when defining rack position.",
|
||||
})
|
||||
|
||||
try:
|
||||
# Child devices cannot be assigned to a rack face/unit
|
||||
if self.device_type.is_child_device and self.face is not None:
|
||||
raise ValidationError({
|
||||
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent "
|
||||
"device."
|
||||
})
|
||||
if self.device_type.is_child_device and self.position:
|
||||
raise ValidationError({
|
||||
'position': "Child device types cannot be assigned to a rack position. This is an attribute of the "
|
||||
"parent device."
|
||||
})
|
||||
if self.rack:
|
||||
|
||||
# Validate rack space
|
||||
rack_face = self.face if not self.device_type.is_full_depth else None
|
||||
exclude_list = [self.pk] if self.pk else []
|
||||
try:
|
||||
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
||||
exclude=exclude_list)
|
||||
if self.position and self.position not in available_units:
|
||||
# Child devices cannot be assigned to a rack face/unit
|
||||
if self.device_type.is_child_device and self.face is not None:
|
||||
raise ValidationError({
|
||||
'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} "
|
||||
"({}U).".format(self.position, self.device_type, self.device_type.u_height)
|
||||
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent "
|
||||
"device."
|
||||
})
|
||||
if self.device_type.is_child_device and self.position:
|
||||
raise ValidationError({
|
||||
'position': "Child device types cannot be assigned to a rack position. This is an attribute of the "
|
||||
"parent device."
|
||||
})
|
||||
except Rack.DoesNotExist:
|
||||
pass
|
||||
|
||||
except DeviceType.DoesNotExist:
|
||||
pass
|
||||
# Validate rack space
|
||||
rack_face = self.face if not self.device_type.is_full_depth else None
|
||||
exclude_list = [self.pk] if self.pk else []
|
||||
try:
|
||||
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
|
||||
exclude=exclude_list)
|
||||
if self.position and self.position not in available_units:
|
||||
raise ValidationError({
|
||||
'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} "
|
||||
"({}U).".format(self.position, self.device_type, self.device_type.u_height)
|
||||
})
|
||||
except Rack.DoesNotExist:
|
||||
pass
|
||||
|
||||
except DeviceType.DoesNotExist:
|
||||
pass
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@ -934,8 +1047,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.platform.name if self.platform else None,
|
||||
self.serial,
|
||||
self.asset_tag,
|
||||
self.rack.site.name,
|
||||
self.rack.name,
|
||||
self.site.name,
|
||||
self.rack.name if self.rack else None,
|
||||
self.position,
|
||||
self.get_face_display(),
|
||||
])
|
||||
@ -984,6 +1097,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
return RPC_CLIENTS.get(self.platform.rpc_client)
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ConsolePort(models.Model):
|
||||
"""
|
||||
@ -1013,6 +1130,10 @@ class ConsolePort(models.Model):
|
||||
])
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
@ -1045,6 +1166,10 @@ class ConsoleServerPort(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class PowerPort(models.Model):
|
||||
"""
|
||||
@ -1064,8 +1189,8 @@ class PowerPort(models.Model):
|
||||
return self.name
|
||||
|
||||
# Used for connections export
|
||||
def csv_format(self):
|
||||
return ','.join([
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
self.power_outlet.device.identifier if self.power_outlet else None,
|
||||
self.power_outlet.name if self.power_outlet else None,
|
||||
self.device.identifier,
|
||||
@ -1074,6 +1199,10 @@ class PowerPort(models.Model):
|
||||
])
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
@ -1100,6 +1229,10 @@ class PowerOutlet(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Interface(models.Model):
|
||||
"""
|
||||
@ -1107,6 +1240,8 @@ class Interface(models.Model):
|
||||
of an InterfaceConnection.
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
||||
lag = models.ForeignKey('self', related_name='member_interfaces', null=True, blank=True, on_delete=models.SET_NULL,
|
||||
verbose_name='Parent LAG')
|
||||
name = models.CharField(max_length=30)
|
||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
||||
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
||||
@ -1125,15 +1260,42 @@ class Interface(models.Model):
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.form_factor == IFACE_FF_VIRTUAL and self.is_connected:
|
||||
# Virtual interfaces cannot be connected
|
||||
if self.form_factor in VIRTUAL_IFACE_TYPES and self.is_connected:
|
||||
raise ValidationError({
|
||||
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
|
||||
"interface or choose a physical form factor."
|
||||
})
|
||||
|
||||
# An interface's LAG must belong to the same device
|
||||
if self.lag and self.lag.device != self.device:
|
||||
raise ValidationError({
|
||||
'lag': u"The selected LAG interface ({}) belongs to a different device ({}).".format(
|
||||
self.lag.name, self.lag.device.name
|
||||
)
|
||||
})
|
||||
|
||||
# A virtual interface cannot have a parent LAG
|
||||
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None:
|
||||
raise ValidationError({
|
||||
'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
|
||||
})
|
||||
|
||||
# Only a LAG can have LAG members
|
||||
if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists():
|
||||
raise ValidationError({
|
||||
'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format(
|
||||
u", ".join([iface.name for iface in self.member_interfaces.all()])
|
||||
)
|
||||
})
|
||||
|
||||
@property
|
||||
def is_physical(self):
|
||||
return self.form_factor != IFACE_FF_VIRTUAL
|
||||
def is_virtual(self):
|
||||
return self.form_factor in VIRTUAL_IFACE_TYPES
|
||||
|
||||
@property
|
||||
def is_lag(self):
|
||||
return self.form_factor == IFACE_FF_LAG
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
@ -1197,6 +1359,10 @@ class InterfaceConnection(models.Model):
|
||||
])
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DeviceBay(models.Model):
|
||||
"""
|
||||
@ -1227,6 +1393,10 @@ class DeviceBay(models.Model):
|
||||
raise ValidationError("Cannot install a device into itself.")
|
||||
|
||||
|
||||
#
|
||||
# Modules
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Module(models.Model):
|
||||
"""
|
||||
|
@ -6,10 +6,28 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, Site,
|
||||
RackGroup, Region, Site,
|
||||
)
|
||||
|
||||
|
||||
REGION_LINK = """
|
||||
{% if record.get_children %}
|
||||
<span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i></a>
|
||||
{% else %}
|
||||
<span style="padding-left: {{ record.get_ancestors|length }}9px">
|
||||
{% endif %}
|
||||
{{ record.name }}
|
||||
</span>
|
||||
"""
|
||||
|
||||
SITE_REGION_LINK = """
|
||||
{% if record.region %}
|
||||
<a href="{% url 'dcim:site_list' %}?region={{ record.region.slug }}">{{ record.region }}</a>
|
||||
{% else %}
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
COLOR_LABEL = """
|
||||
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
|
||||
"""
|
||||
@ -20,6 +38,12 @@ DEVICE_LINK = """
|
||||
</a>
|
||||
"""
|
||||
|
||||
REGION_ACTIONS = """
|
||||
{% if perms.dcim.change_region %}
|
||||
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACKGROUP_ACTIONS = """
|
||||
{% if perms.dcim.change_rackgroup %}
|
||||
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@ -70,12 +94,38 @@ STATUS_ICON = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICE_PRIMARY_IP = """
|
||||
{{ record.primary_ip6.address.ip|default:"" }}
|
||||
{% if record.primary_ip6 and record.primary_ip4 %}<br />{% endif %}
|
||||
{{ record.primary_ip4.address.ip|default:"" }}
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph value %}
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
#
|
||||
|
||||
class RegionTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.TemplateColumn(template_code=REGION_LINK, orderable=False)
|
||||
site_count = tables.Column(verbose_name='Sites')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=REGION_ACTIONS,
|
||||
attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Region
|
||||
fields = ('pk', 'name', 'site_count', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
@ -84,6 +134,7 @@ class SiteTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
|
||||
facility = tables.Column(verbose_name='Facility')
|
||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK, verbose_name='Region')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
asn = tables.Column(verbose_name='ASN')
|
||||
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
|
||||
@ -94,8 +145,10 @@ class SiteTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Site
|
||||
fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
||||
'vlan_count', 'circuit_count')
|
||||
fields = (
|
||||
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
||||
'vlan_count', 'circuit_count',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -311,14 +364,13 @@ class DeviceTable(BaseTable):
|
||||
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
site = tables.LinkColumn('dcim:site', accessor=Accessor('rack.site'), args=[Accessor('rack.site.slug')],
|
||||
verbose_name='Site')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||
device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
text=lambda record: record.device_type.full_name)
|
||||
primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address',
|
||||
template_code="{{ record.primary_ip.address.ip }}")
|
||||
template_code=DEVICE_PRIMARY_IP)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
@ -328,8 +380,7 @@ class DeviceTable(BaseTable):
|
||||
class DeviceImportTable(BaseTable):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
site = tables.LinkColumn('dcim:site', accessor=Accessor('rack.site'), args=[Accessor('rack.site.slug')],
|
||||
verbose_name='Site')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||
position = tables.Column(verbose_name='Position')
|
||||
device_role = tables.Column(verbose_name='Role')
|
||||
|
@ -17,6 +17,7 @@ class SiteTest(APITestCase):
|
||||
'id',
|
||||
'name',
|
||||
'slug',
|
||||
'region',
|
||||
'tenant',
|
||||
'facility',
|
||||
'asn',
|
||||
@ -151,6 +152,7 @@ class RackTest(APITestCase):
|
||||
'width',
|
||||
'u_height',
|
||||
'desc_units',
|
||||
'reservations',
|
||||
'comments',
|
||||
'custom_fields',
|
||||
'front_units',
|
||||
@ -345,6 +347,7 @@ class DeviceTest(APITestCase):
|
||||
'platform',
|
||||
'serial',
|
||||
'asset_tag',
|
||||
'site',
|
||||
'rack',
|
||||
'position',
|
||||
'face',
|
||||
@ -416,6 +419,9 @@ class DeviceTest(APITestCase):
|
||||
'primary_ip4_family',
|
||||
'primary_ip4_id',
|
||||
'primary_ip6',
|
||||
'site_id',
|
||||
'site_name',
|
||||
'site_slug',
|
||||
'rack_display_name',
|
||||
'rack_facility_id',
|
||||
'rack_id',
|
||||
@ -571,6 +577,7 @@ class InterfaceTest(APITestCase):
|
||||
'device',
|
||||
'name',
|
||||
'form_factor',
|
||||
'lag',
|
||||
'mac_address',
|
||||
'mgmt_only',
|
||||
'description',
|
||||
@ -584,6 +591,7 @@ class InterfaceTest(APITestCase):
|
||||
'device',
|
||||
'name',
|
||||
'form_factor',
|
||||
'lag',
|
||||
'mac_address',
|
||||
'mgmt_only',
|
||||
'description',
|
||||
|
@ -6,14 +6,14 @@ class RackTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
site = Site.objects.create(
|
||||
self.site = Site.objects.create(
|
||||
name='TestSite1',
|
||||
slug='my-test-site'
|
||||
)
|
||||
self.rack = Rack.objects.create(
|
||||
name='TestRack1',
|
||||
facility_id='A101',
|
||||
site=site,
|
||||
site=self.site,
|
||||
u_height=42
|
||||
)
|
||||
self.manufacturer = Manufacturer.objects.create(
|
||||
@ -56,29 +56,29 @@ class RackTestCase(TestCase):
|
||||
|
||||
def test_mount_single_device(self):
|
||||
|
||||
rack1 = Rack.objects.get(name='TestRack1')
|
||||
device1 = Device(
|
||||
name='TestSwitch1',
|
||||
device_type=DeviceType.objects.get(manufacturer__slug='acme', slug='ff2048'),
|
||||
device_role=DeviceRole.objects.get(slug='switch'),
|
||||
rack=rack1,
|
||||
site=self.site,
|
||||
rack=self.rack,
|
||||
position=10,
|
||||
face=RACK_FACE_REAR,
|
||||
)
|
||||
device1.save()
|
||||
|
||||
# Validate rack height
|
||||
self.assertEqual(list(rack1.units), list(reversed(range(1, 43))))
|
||||
self.assertEqual(list(self.rack.units), list(reversed(range(1, 43))))
|
||||
|
||||
# Validate inventory (front face)
|
||||
rack1_inventory_front = rack1.get_front_elevation()
|
||||
rack1_inventory_front = self.rack.get_front_elevation()
|
||||
self.assertEqual(rack1_inventory_front[-10]['device'], device1)
|
||||
del(rack1_inventory_front[-10])
|
||||
for u in rack1_inventory_front:
|
||||
self.assertIsNone(u['device'])
|
||||
|
||||
# Validate inventory (rear face)
|
||||
rack1_inventory_rear = rack1.get_rear_elevation()
|
||||
rack1_inventory_rear = self.rack.get_rear_elevation()
|
||||
self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
|
||||
del(rack1_inventory_rear[-10])
|
||||
for u in rack1_inventory_rear:
|
||||
@ -89,6 +89,7 @@ class RackTestCase(TestCase):
|
||||
name='TestPDU',
|
||||
device_role=self.role.get('PDU'),
|
||||
device_type=self.device_type.get('cc5000'),
|
||||
site=self.site,
|
||||
rack=self.rack,
|
||||
position=None,
|
||||
face=None,
|
||||
|
@ -8,6 +8,12 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# Regions
|
||||
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
|
||||
url(r'^regions/add/$', views.RegionEditView.as_view(), name='region_add'),
|
||||
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
|
||||
|
||||
# Sites
|
||||
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
|
||||
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
|
||||
@ -29,6 +35,10 @@ urlpatterns = [
|
||||
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
|
||||
# Rack reservations
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
||||
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),
|
||||
@ -38,6 +48,7 @@ urlpatterns = [
|
||||
url(r'^racks/(?P<pk>\d+)/$', views.rack, name='rack'),
|
||||
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
||||
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
|
||||
|
||||
# Manufacturers
|
||||
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
|
@ -26,7 +26,7 @@ from .models import (
|
||||
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackRole, Site,
|
||||
RackReservation, RackRole, Region, Site,
|
||||
)
|
||||
|
||||
|
||||
@ -66,11 +66,12 @@ class ComponentCreateView(View):
|
||||
def get(self, request, pk):
|
||||
|
||||
parent = get_object_or_404(self.parent_model, pk=pk)
|
||||
form = self.form(parent, initial=request.GET)
|
||||
|
||||
return render(request, 'dcim/device_component_add.html', {
|
||||
'parent': parent,
|
||||
'component_type': self.model._meta.verbose_name,
|
||||
'form': self.form(initial=request.GET),
|
||||
'form': form,
|
||||
'return_url': parent.get_absolute_url(),
|
||||
})
|
||||
|
||||
@ -78,7 +79,7 @@ class ComponentCreateView(View):
|
||||
|
||||
parent = get_object_or_404(self.parent_model, pk=pk)
|
||||
|
||||
form = self.form(request.POST)
|
||||
form = self.form(parent, request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
new_components = []
|
||||
@ -128,12 +129,37 @@ class ComponentDeleteView(ObjectDeleteView):
|
||||
return obj.device.get_absolute_url()
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
#
|
||||
|
||||
class RegionListView(ObjectListView):
|
||||
queryset = Region.objects.annotate(site_count=Count('sites'))
|
||||
table = tables.RegionTable
|
||||
template_name = 'dcim/region_list.html'
|
||||
|
||||
|
||||
class RegionEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_region'
|
||||
model = Region
|
||||
form_class = forms.RegionForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
return reverse('dcim:region_list')
|
||||
|
||||
|
||||
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_region'
|
||||
cls = Region
|
||||
default_return_url = 'dcim:region_list'
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteListView(ObjectListView):
|
||||
queryset = Site.objects.select_related('tenant')
|
||||
queryset = Site.objects.select_related('region', 'tenant')
|
||||
filter = filters.SiteFilter
|
||||
filter_form = forms.SiteFilterForm
|
||||
table = tables.SiteTable
|
||||
@ -142,7 +168,7 @@ class SiteListView(ObjectListView):
|
||||
|
||||
def site(request, slug):
|
||||
|
||||
site = get_object_or_404(Site, slug=slug)
|
||||
site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
|
||||
stats = {
|
||||
'rack_count': Rack.objects.filter(site=site).count(),
|
||||
'device_count': Device.objects.filter(rack__site=site).count(),
|
||||
@ -262,15 +288,23 @@ class RackListView(ObjectListView):
|
||||
|
||||
def rack(request, pk):
|
||||
|
||||
rack = get_object_or_404(Rack, pk=pk)
|
||||
rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
|
||||
|
||||
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
|
||||
.select_related('device_type__manufacturer')
|
||||
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
|
||||
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
|
||||
|
||||
reservations = RackReservation.objects.filter(rack=rack)
|
||||
reserved_units = {}
|
||||
for r in reservations:
|
||||
for u in r.units:
|
||||
reserved_units[u] = r
|
||||
|
||||
return render(request, 'dcim/rack.html', {
|
||||
'rack': rack,
|
||||
'reservations': reservations,
|
||||
'reserved_units': reserved_units,
|
||||
'nonracked_devices': nonracked_devices,
|
||||
'next_rack': next_rack,
|
||||
'prev_rack': prev_rack,
|
||||
@ -317,6 +351,33 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
default_return_url = 'dcim:rack_list'
|
||||
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_rackreservation'
|
||||
model = RackReservation
|
||||
form_class = forms.RackReservationForm
|
||||
|
||||
def alter_obj(self, obj, request, args, kwargs):
|
||||
if not obj.pk:
|
||||
obj.rack = get_object_or_404(Rack, pk=kwargs['rack'])
|
||||
obj.user = request.user
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
return obj.rack.get_absolute_url()
|
||||
|
||||
|
||||
class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_rackreservation'
|
||||
model = RackReservation
|
||||
|
||||
def get_return_url(self, obj):
|
||||
return obj.rack.get_absolute_url()
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
@ -592,7 +653,7 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class DeviceListView(ObjectListView):
|
||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'rack__site',
|
||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack',
|
||||
'primary_ip4', 'primary_ip6')
|
||||
filter = filters.DeviceFilter
|
||||
filter_form = forms.DeviceFilterForm
|
||||
@ -602,7 +663,9 @@ class DeviceListView(ObjectListView):
|
||||
|
||||
def device(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
device = get_object_or_404(Device.objects.select_related(
|
||||
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
|
||||
), pk=pk)
|
||||
console_ports = natsorted(
|
||||
ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
|
||||
)
|
||||
@ -1376,7 +1439,7 @@ def interfaceconnection_add(request, pk):
|
||||
else:
|
||||
form = forms.InterfaceConnectionForm(device, initial={
|
||||
'interface_a': request.GET.get('interface_a', None),
|
||||
'site_b': request.GET.get('site_b', device.rack.site),
|
||||
'site_b': request.GET.get('site_b', device.site),
|
||||
'rack_b': request.GET.get('rack_b', None),
|
||||
'device_b': request.GET.get('device_b', None),
|
||||
'interface_b': request.GET.get('interface_b', None),
|
||||
@ -1517,9 +1580,9 @@ class ModuleEditView(PermissionRequiredMixin, ComponentEditView):
|
||||
model = Module
|
||||
form_class = forms.ModuleForm
|
||||
|
||||
def alter_obj(self, obj, args, kwargs):
|
||||
if 'device' in kwargs:
|
||||
obj.device = get_object_or_404(Device, pk=kwargs['device'])
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
if 'device' in url_kwargs:
|
||||
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||
return obj
|
||||
|
||||
|
||||
|
@ -6,7 +6,7 @@ from django.db import models
|
||||
from .formfields import IPFormField
|
||||
from .lookups import (
|
||||
EndsWith, IEndsWith, IRegex, IStartsWith, NetContained, NetContainedOrEqual, NetContains, NetContainsOrEquals,
|
||||
NetHost, Regex, StartsWith,
|
||||
NetHost, NetMaskLength, Regex, StartsWith,
|
||||
)
|
||||
|
||||
|
||||
@ -67,6 +67,7 @@ IPNetworkField.register_lookup(NetContainedOrEqual)
|
||||
IPNetworkField.register_lookup(NetContains)
|
||||
IPNetworkField.register_lookup(NetContainsOrEquals)
|
||||
IPNetworkField.register_lookup(NetHost)
|
||||
IPNetworkField.register_lookup(NetMaskLength)
|
||||
|
||||
|
||||
class IPAddressField(BaseIPField):
|
||||
@ -90,3 +91,4 @@ IPAddressField.register_lookup(NetContainedOrEqual)
|
||||
IPAddressField.register_lookup(NetContains)
|
||||
IPAddressField.register_lookup(NetContainsOrEquals)
|
||||
IPAddressField.register_lookup(NetHost)
|
||||
IPAddressField.register_lookup(NetMaskLength)
|
||||
|
@ -13,15 +13,10 @@ from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLAN
|
||||
|
||||
|
||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
name = django_filters.CharFilter(
|
||||
name='name',
|
||||
lookup_type='icontains',
|
||||
label='Name',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
@ -34,7 +29,9 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(rd__icontains=value) |
|
||||
@ -43,7 +40,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['rd']
|
||||
fields = ['name', 'rd']
|
||||
|
||||
|
||||
class RIRFilter(django_filters.FilterSet):
|
||||
@ -54,8 +51,8 @@ class RIRFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -74,7 +71,9 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
model = Aggregate
|
||||
fields = ['family', 'date_added']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(description__icontains=value)
|
||||
try:
|
||||
prefix = str(IPNetwork(value.strip()).cidr)
|
||||
@ -85,14 +84,18 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
parent = django_filters.MethodFilter(
|
||||
action='search_by_parent',
|
||||
parent = django_filters.CharFilter(
|
||||
method='search_by_parent',
|
||||
label='Parent prefix',
|
||||
)
|
||||
mask_length = django_filters.NumberFilter(
|
||||
method='filter_mask_length',
|
||||
label='Mask length',
|
||||
)
|
||||
vrf_id = NullableModelMultipleChoiceFilter(
|
||||
name='vrf_id',
|
||||
queryset=VRF.objects.all(),
|
||||
@ -151,7 +154,9 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
model = Prefix
|
||||
fields = ['family', 'status']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(description__icontains=value)
|
||||
try:
|
||||
prefix = str(IPNetwork(value.strip()).cidr)
|
||||
@ -160,7 +165,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def search_by_parent(self, queryset, value):
|
||||
def search_by_parent(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
@ -170,34 +175,25 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
def _tenant(self, queryset, value):
|
||||
if str(value) == '':
|
||||
def filter_mask_length(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(tenant__slug=value) |
|
||||
Q(tenant__isnull=True, vrf__tenant__slug=value)
|
||||
)
|
||||
|
||||
def _tenant_id(self, queryset, value):
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(tenant__pk=value) |
|
||||
Q(tenant__isnull=True, vrf__tenant__pk=value)
|
||||
)
|
||||
return queryset.filter(prefix__net_mask_length=value)
|
||||
|
||||
|
||||
class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
parent = django_filters.MethodFilter(
|
||||
action='search_by_parent',
|
||||
parent = django_filters.CharFilter(
|
||||
method='search_by_parent',
|
||||
label='Parent prefix',
|
||||
)
|
||||
mask_length = django_filters.NumberFilter(
|
||||
method='filter_mask_length',
|
||||
label='Mask length',
|
||||
)
|
||||
vrf_id = NullableModelMultipleChoiceFilter(
|
||||
name='vrf_id',
|
||||
queryset=VRF.objects.all(),
|
||||
@ -239,9 +235,11 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['q', 'family', 'status']
|
||||
fields = ['family', 'status']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(description__icontains=value)
|
||||
try:
|
||||
ipaddress = str(IPNetwork(value.strip()))
|
||||
@ -250,25 +248,30 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def search_by_parent(self, queryset, value):
|
||||
def search_by_parent(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
query = str(IPNetwork(value).cidr)
|
||||
query = str(IPNetwork(value.strip()).cidr)
|
||||
return queryset.filter(address__net_contained_or_equal=query)
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
def filter_mask_length(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(address__net_mask_length=value)
|
||||
|
||||
|
||||
class VLANGroupFilter(django_filters.FilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
site_id = NullableModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site__slug',
|
||||
site = NullableModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@ -276,20 +279,21 @@ class VLANGroupFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
site_id = NullableModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site__slug',
|
||||
site = NullableModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
@ -305,15 +309,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
name = django_filters.CharFilter(
|
||||
name='name',
|
||||
lookup_type='icontains',
|
||||
label='Name',
|
||||
)
|
||||
vid = django_filters.NumberFilter(
|
||||
name='vid',
|
||||
label='VLAN number (1-4095)',
|
||||
)
|
||||
tenant_id = NullableModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
@ -339,12 +334,14 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['status']
|
||||
fields = ['name', 'vid', 'status']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
||||
try:
|
||||
qs_filter |= Q(vid=int(value))
|
||||
qs_filter |= Q(vid=int(value.strip()))
|
||||
except ValueError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
@ -21,6 +21,12 @@ IP_FAMILY_CHOICES = [
|
||||
(6, 'IPv6'),
|
||||
]
|
||||
|
||||
PREFIX_MASK_LENGTH_CHOICES = [
|
||||
('', '---------'),
|
||||
] + [(i, i) for i in range(1, 128)]
|
||||
|
||||
IPADDRESS_MASK_LENGTH_CHOICES = PREFIX_MASK_LENGTH_CHOICES + [(128, 128)]
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
@ -131,8 +137,11 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Aggregate
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||
rir = FilterChoiceField(queryset=RIR.objects.annotate(filter_count=Count('aggregates')), to_field_name='slug',
|
||||
label='RIR')
|
||||
rir = FilterChoiceField(
|
||||
queryset=RIR.objects.annotate(filter_count=Count('aggregates')),
|
||||
to_field_name='slug',
|
||||
label='RIR'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -153,7 +162,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class PrefixForm(BootstrapMixin, CustomFieldForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
||||
widget=forms.Select(attrs={'filter-for': 'vlan'}))
|
||||
widget=forms.Select(attrs={'filter-for': 'vlan', 'nullable': 'true'}))
|
||||
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
|
||||
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
|
||||
display_field='display_name'))
|
||||
@ -173,7 +182,7 @@ class PrefixForm(BootstrapMixin, CustomFieldForm):
|
||||
elif self.initial.get('site'):
|
||||
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['vlan'].choices = []
|
||||
self.fields['vlan'].queryset = VLAN.objects.filter(site=None)
|
||||
|
||||
|
||||
class PrefixFromCSVForm(forms.ModelForm):
|
||||
@ -259,19 +268,33 @@ def prefix_status_choices():
|
||||
class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Prefix
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
parent = forms.CharField(required=False, label='Parent Prefix', widget=forms.TextInput(attrs={
|
||||
parent = forms.CharField(required=False, label='Parent prefix', widget=forms.TextInput(attrs={
|
||||
'placeholder': 'Prefix',
|
||||
}))
|
||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||
vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('prefixes')), to_field_name='rd',
|
||||
label='VRF', null_option=(0, 'Global'))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
|
||||
mask_length = forms.ChoiceField(required=False, choices=PREFIX_MASK_LENGTH_CHOICES, label='Mask length')
|
||||
vrf = FilterChoiceField(
|
||||
queryset=VRF.objects.annotate(filter_count=Count('prefixes')),
|
||||
to_field_name='rd',
|
||||
label='VRF',
|
||||
null_option=(0, 'Global')
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.annotate(filter_count=Count('prefixes')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.annotate(filter_count=Count('prefixes')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
queryset=Role.objects.annotate(filter_count=Count('prefixes')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
||||
|
||||
|
||||
@ -307,10 +330,10 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
nat_inside = self.instance.nat_inside
|
||||
# If the IP is assigned to an interface, populate site/device fields accordingly
|
||||
if self.instance.nat_inside.interface:
|
||||
self.initial['nat_site'] = self.instance.nat_inside.interface.device.rack.site.pk
|
||||
self.initial['nat_site'] = self.instance.nat_inside.interface.device.site.pk
|
||||
self.initial['nat_device'] = self.instance.nat_inside.interface.device.pk
|
||||
self.fields['nat_device'].queryset = Device.objects.filter(
|
||||
rack__site=nat_inside.interface.device.rack.site)
|
||||
rack__site=nat_inside.interface.device.site)
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
|
||||
interface__device=nat_inside.interface.device)
|
||||
else:
|
||||
@ -346,20 +369,54 @@ class IPAddressBulkAddForm(BootstrapMixin, forms.Form):
|
||||
|
||||
|
||||
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
|
||||
widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
display_field='display_name', attrs={'filter-for': 'device'}))
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
|
||||
widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
|
||||
display_field='display_name', attrs={'filter-for': 'interface'}))
|
||||
livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
|
||||
query_key='q', query_url='dcim-api:device_list', field_to_update='device')
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'rack'}
|
||||
)
|
||||
)
|
||||
rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'device', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
device = forms.ModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
label='Device',
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'interface'}
|
||||
)
|
||||
)
|
||||
livesearch = forms.CharField(
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=Livesearch(
|
||||
query_key='q',
|
||||
query_url='dcim-api:device_list',
|
||||
field_to_update='device'
|
||||
)
|
||||
)
|
||||
interface = forms.ModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
label='Interface',
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/devices/{{device}}/interfaces/'
|
||||
)
|
||||
)
|
||||
set_as_primary = forms.BooleanField(
|
||||
label='Set as primary IP for device',
|
||||
required=False
|
||||
)
|
||||
interface = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
|
||||
widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/'))
|
||||
set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@ -453,11 +510,19 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
parent = forms.CharField(required=False, label='Parent Prefix', widget=forms.TextInput(attrs={
|
||||
'placeholder': 'Prefix',
|
||||
}))
|
||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
|
||||
vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')), to_field_name='rd',
|
||||
label='VRF', null_option=(0, 'Global'))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
|
||||
to_field_name='slug', null_option=(0, 'None'))
|
||||
family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
|
||||
mask_length = forms.ChoiceField(required=False, choices=IPADDRESS_MASK_LENGTH_CHOICES, label='Mask length')
|
||||
vrf = FilterChoiceField(
|
||||
queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')),
|
||||
to_field_name='rd',
|
||||
label='VRF',
|
||||
null_option=(0, 'Global')
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
|
||||
|
||||
|
||||
@ -474,7 +539,11 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
|
||||
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug')
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'Global')
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@ -490,7 +559,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
||||
model = VLAN
|
||||
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||
help_texts = {
|
||||
'site': "The site at which this VLAN exists",
|
||||
'site': "Leave blank if this VLAN spans multiple sites",
|
||||
'group': "VLAN group (optional)",
|
||||
'vid': "Configured VLAN ID",
|
||||
'name': "Configured VLAN name",
|
||||
@ -498,7 +567,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
||||
'role': "The primary function of this VLAN",
|
||||
}
|
||||
widgets = {
|
||||
'site': forms.Select(attrs={'filter-for': 'group'}),
|
||||
'site': forms.Select(attrs={'filter-for': 'group', 'nullable': 'true'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -511,11 +580,11 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
||||
elif self.initial.get('site'):
|
||||
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['group'].choices = []
|
||||
self.fields['group'].queryset = VLANGroup.objects.filter(site=None)
|
||||
|
||||
|
||||
class VLANFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'VLAN group not found.'})
|
||||
@ -565,14 +634,27 @@ def vlan_status_choices():
|
||||
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = VLAN
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug')
|
||||
group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
|
||||
null_option=(0, 'None'))
|
||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.annotate(filter_count=Count('vlans')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'Global')
|
||||
)
|
||||
group_id = FilterChoiceField(
|
||||
queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')),
|
||||
label='VLAN group',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.annotate(filter_count=Count('vlans')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
|
||||
role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||
null_option=(0, 'None'))
|
||||
role = FilterChoiceField(
|
||||
queryset=Role.objects.annotate(filter_count=Count('vlans')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
@ -1,4 +1,4 @@
|
||||
from django.db.models import Lookup
|
||||
from django.db.models import Lookup, Transform, IntegerField
|
||||
from django.db.models.lookups import BuiltinLookup
|
||||
|
||||
|
||||
@ -87,3 +87,12 @@ class NetHost(Lookup):
|
||||
rhs_params[0] = rhs_params[0].split('/')[0]
|
||||
params = lhs_params + rhs_params
|
||||
return 'HOST(%s) = %s' % (lhs, rhs), params
|
||||
|
||||
|
||||
class NetMaskLength(Transform):
|
||||
lookup_name = 'net_mask_length'
|
||||
function = 'MASKLEN'
|
||||
|
||||
@property
|
||||
def output_field(self):
|
||||
return IntegerField()
|
||||
|
26
netbox/ipam/migrations/0015_global_vlans.py
Normal file
26
netbox/ipam/migrations/0015_global_vlans.py
Normal file
@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.4 on 2017-02-21 18:45
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0014_ipaddress_status_add_deprecated'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='vlan',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='dcim.Site'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vlangroup',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_groups', to='dcim.Site'),
|
||||
),
|
||||
]
|
@ -485,7 +485,7 @@ class VLANGroup(models.Model):
|
||||
"""
|
||||
name = models.CharField(max_length=50)
|
||||
slug = models.SlugField()
|
||||
site = models.ForeignKey('dcim.Site', related_name='vlan_groups')
|
||||
site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.PROTECT, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
@ -497,6 +497,8 @@ class VLANGroup(models.Model):
|
||||
verbose_name_plural = 'VLAN groups'
|
||||
|
||||
def __str__(self):
|
||||
if self.site is None:
|
||||
return self.name
|
||||
return u'{} - {}'.format(self.site.name, self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
@ -513,7 +515,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
|
||||
or more Prefixes assigned to it.
|
||||
"""
|
||||
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT)
|
||||
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT, blank=True, null=True)
|
||||
group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
|
||||
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
|
||||
MinValueValidator(1),
|
||||
@ -551,7 +553,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
def to_csv(self):
|
||||
return csv_format([
|
||||
self.site.name,
|
||||
self.site.name if self.site else None,
|
||||
self.group.name if self.group else None,
|
||||
self.vid,
|
||||
self.name,
|
||||
|
@ -297,9 +297,17 @@ def aggregate(request, pk):
|
||||
prefix_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
'add': request.user.has_perm('ipam.add_prefix'),
|
||||
'change': request.user.has_perm('ipam.change_prefix'),
|
||||
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||
}
|
||||
|
||||
return render(request, 'ipam/aggregate.html', {
|
||||
'aggregate': aggregate,
|
||||
'prefix_table': prefix_table,
|
||||
'permissions': permissions,
|
||||
})
|
||||
|
||||
|
||||
@ -385,7 +393,9 @@ class PrefixListView(ObjectListView):
|
||||
|
||||
def prefix(request, pk):
|
||||
|
||||
prefix = get_object_or_404(Prefix.objects.select_related('site', 'vlan', 'role'), pk=pk)
|
||||
prefix = get_object_or_404(Prefix.objects.select_related(
|
||||
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
|
||||
), pk=pk)
|
||||
|
||||
try:
|
||||
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
|
||||
@ -425,6 +435,13 @@ def prefix(request, pk):
|
||||
child_prefix_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
'add': request.user.has_perm('ipam.add_prefix'),
|
||||
'change': request.user.has_perm('ipam.change_prefix'),
|
||||
'delete': request.user.has_perm('ipam.delete_prefix'),
|
||||
}
|
||||
|
||||
return render(request, 'ipam/prefix.html', {
|
||||
'prefix': prefix,
|
||||
'aggregate': aggregate,
|
||||
@ -432,6 +449,7 @@ def prefix(request, pk):
|
||||
'parent_prefix_table': parent_prefix_table,
|
||||
'child_prefix_table': child_prefix_table,
|
||||
'duplicate_prefix_table': duplicate_prefix_table,
|
||||
'permissions': permissions,
|
||||
'return_url': prefix.get_absolute_url(),
|
||||
})
|
||||
|
||||
@ -490,9 +508,17 @@ def prefix_ipaddresses(request, pk):
|
||||
ip_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
'add': request.user.has_perm('ipam.add_ipaddress'),
|
||||
'change': request.user.has_perm('ipam.change_ipaddress'),
|
||||
'delete': request.user.has_perm('ipam.delete_ipaddress'),
|
||||
}
|
||||
|
||||
return render(request, 'ipam/prefix_ipaddresses.html', {
|
||||
'prefix': prefix,
|
||||
'ip_table': ip_table,
|
||||
'permissions': permissions,
|
||||
})
|
||||
|
||||
|
||||
@ -559,6 +585,8 @@ def ipaddress_assign(request, pk):
|
||||
device.save()
|
||||
|
||||
return redirect('ipam:ipaddress', pk=ipaddress.pk)
|
||||
else:
|
||||
assert False, form.errors
|
||||
|
||||
else:
|
||||
form = forms.IPAddressAssignForm()
|
||||
@ -705,7 +733,7 @@ class VLANListView(ObjectListView):
|
||||
|
||||
def vlan(request, pk):
|
||||
|
||||
vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
|
||||
vlan = get_object_or_404(VLAN.objects.select_related('site__region', 'tenant__group', 'role'), pk=pk)
|
||||
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
|
||||
prefix_table = tables.PrefixBriefTable(list(prefixes))
|
||||
prefix_table.exclude = ('vlan',)
|
||||
@ -764,9 +792,9 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
form_class = forms.ServiceForm
|
||||
template_name = 'ipam/service_edit.html'
|
||||
|
||||
def alter_obj(self, obj, args, kwargs):
|
||||
if 'device' in kwargs:
|
||||
obj.device = get_object_or_404(Device, pk=kwargs['device'])
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
if 'device' in url_kwargs:
|
||||
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
|
@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.8.4'
|
||||
VERSION = '1.9.0'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
@ -104,6 +104,7 @@ INSTALLED_APPS = (
|
||||
'django.contrib.humanize',
|
||||
'debug_toolbar',
|
||||
'django_tables2',
|
||||
'mptt',
|
||||
'rest_framework',
|
||||
'rest_framework_swagger',
|
||||
'circuits',
|
||||
|
@ -264,6 +264,15 @@ ul.rack_far_face li.blocked {
|
||||
#ffc7c7 14px
|
||||
);
|
||||
}
|
||||
ul.rack_near_face li.reserved {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
#f7f7f7,
|
||||
#f7f7f7 7px,
|
||||
#c7c7ff 7px,
|
||||
#c7c7ff 14px
|
||||
);
|
||||
}
|
||||
ul.rack_near_face {
|
||||
z-index: 200;
|
||||
}
|
||||
|
@ -68,37 +68,38 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
// API select widget
|
||||
$('select[filter-for]').change(function () {
|
||||
$('select[filter-for]').change(function() {
|
||||
|
||||
// Resolve child field by ID specified in parent
|
||||
var child_name = $(this).attr('filter-for');
|
||||
var child_field = $('#id_' + child_name);
|
||||
var child_selected = child_field.val();
|
||||
|
||||
// Wipe out any existing options within the child field
|
||||
// Wipe out any existing options within the child field and create a default option
|
||||
child_field.empty();
|
||||
child_field.append($("<option></option>").attr("value", "").text(""));
|
||||
|
||||
if ($(this).val()) {
|
||||
child_field.append($("<option></option>").attr("value", "").text("---------"));
|
||||
|
||||
if ($(this).val() || $(this).attr('nullable') == 'true') {
|
||||
var api_url = child_field.attr('api-url');
|
||||
var disabled_indicator = child_field.attr('disabled-indicator');
|
||||
var initial_value = child_field.attr('initial');
|
||||
var display_field = child_field.attr('display-field') || 'name';
|
||||
|
||||
// Gather the values of all other filter fields for this child
|
||||
$("select[filter-for='" + child_name + "']").each(function() {
|
||||
var filter_field = $(this);
|
||||
// Determine the filter fields needed to make an API call
|
||||
var filter_regex = /\{\{([a-z_]+)\}\}/g;
|
||||
var match;
|
||||
while (match = filter_regex.exec(api_url)) {
|
||||
var filter_field = $('#id_' + match[1]);
|
||||
if (filter_field.val()) {
|
||||
api_url = api_url.replace('{{' + filter_field.attr('name') + '}}', filter_field.val());
|
||||
} else {
|
||||
// Not all filters have been selected yet
|
||||
return false;
|
||||
api_url = api_url.replace(match[0], filter_field.val());
|
||||
} else if ($(this).attr('nullable') == 'true') {
|
||||
api_url = api_url.replace(match[0], '0');
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// If all URL variables have been replaced, make the API call
|
||||
if (api_url.search('{{') < 0) {
|
||||
console.log(child_name + ": Fetching " + api_url);
|
||||
$.ajax({
|
||||
url: api_url,
|
||||
dataType: 'json',
|
||||
@ -106,7 +107,9 @@ $(document).ready(function() {
|
||||
$.each(response, function (index, choice) {
|
||||
var option = $("<option></option>").attr("value", choice.id).text(choice[display_field]);
|
||||
if (disabled_indicator && choice[disabled_indicator] && choice.id != initial_value) {
|
||||
option.attr("disabled", "disabled")
|
||||
option.attr("disabled", "disabled");
|
||||
} else if (choice.id == child_selected) {
|
||||
option.attr("selected", "selected");
|
||||
}
|
||||
child_field.append(option);
|
||||
});
|
||||
|
@ -7,8 +7,8 @@ from dcim.models import Device
|
||||
|
||||
|
||||
class SecretFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -33,7 +33,9 @@ class SecretFilter(django_filters.FilterSet):
|
||||
model = Secret
|
||||
fields = ['name']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(device__name__icontains=value)
|
||||
|
@ -101,7 +101,10 @@ class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
||||
class SecretFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')
|
||||
role = FilterChoiceField(
|
||||
queryset=SecretRole.objects.annotate(filter_count=Count('secrets')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
@ -37,6 +37,11 @@
|
||||
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'dcim:region_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Regions</a></li>
|
||||
{% if perms.dcim.add_region %}
|
||||
<li><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Region</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li>
|
||||
{% if perms.tenancy.add_tenant %}
|
||||
<li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>
|
||||
|
@ -66,6 +66,10 @@
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
{% if circuit.tenant %}
|
||||
{% if circuit.tenant.group %}
|
||||
<a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group.name }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
|
@ -27,6 +27,10 @@
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>
|
||||
{% if termination.site.region %}
|
||||
<a href="{{ termination.site.region.get_absolute_url }}">{{ termination.site.region }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:site' slug=termination.site.slug %}">{{ termination.site }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
@ -34,7 +38,8 @@
|
||||
<td>Termination</td>
|
||||
<td>
|
||||
{% if termination.interface %}
|
||||
<span><a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a> {{ termination.interface }}</span>
|
||||
<a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a>
|
||||
<i class="fa fa-angle-right"></i> {{ termination.interface }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not defined</span>
|
||||
{% endif %}
|
||||
|
@ -48,7 +48,7 @@
|
||||
<h1>{{ provider }}</h1>
|
||||
{% include 'inc/created_updated.html' with obj=provider %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Provider</strong>
|
||||
@ -104,6 +104,12 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Circuits</td>
|
||||
<td>
|
||||
<a href="{% url 'circuits:circuit_list' %}?provider={{ provider.slug }}">{{ provider.circuits.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% with provider.get_custom_fields as custom_fields %}
|
||||
@ -122,12 +128,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Circuits</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<th>Circuit ID</th>
|
||||
<th>Type</th>
|
||||
<th>Tenant</th>
|
||||
<th>A Side</th>
|
||||
<th>Z Side</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for c in circuits %}
|
||||
<tr>
|
||||
<td>
|
||||
@ -136,6 +150,34 @@
|
||||
<td>
|
||||
<a href="{% url 'circuits:circuit_list' %}?type={{ c.type.slug }}">{{ c.type }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if c.tenant %}
|
||||
<a href="{% url 'tenants:tenant' slug=c.tenant.slug %}">{{ c.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.termination_a %}
|
||||
<a href="{% url 'dcim:site' slug=c.termination_a.site.slug %}">{{ c.termination_a.site }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.termination_z %}
|
||||
<a href="{% url 'dcim:site' slug=c.termination_z.site.slug %}">{{ c.termination_z.site }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.description %}
|
||||
{{ c.description }}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
|
@ -7,6 +7,9 @@
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
@ -29,6 +32,12 @@
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
<div class="tab-pane" id="select">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Site</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ consoleport.device.site }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.console_server %}
|
||||
</div>
|
||||
|
@ -6,7 +6,10 @@
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
@ -29,6 +32,12 @@
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
<div class="tab-pane" id="select">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Site</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ consoleserverport.device.site }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.device %}
|
||||
</div>
|
||||
|
@ -14,26 +14,28 @@
|
||||
<strong>Device</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
{% if device.tenant %}
|
||||
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>
|
||||
<a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a>
|
||||
{% if device.site.region %}
|
||||
<a href="{{ device.site.region.get_absolute_url }}">{{ device.site.region }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rack</td>
|
||||
<td>
|
||||
<span><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}</span>
|
||||
{% if device.rack %}
|
||||
{% if device.rack.group %}
|
||||
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group.name }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -44,15 +46,29 @@
|
||||
<span>U{{ parent.position }} / {{ parent.get_face_display }}
|
||||
(<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> - {{ device.parent_bay.name }})</span>
|
||||
{% endwith %}
|
||||
{% elif device.position %}
|
||||
{% elif device.rack and device.position %}
|
||||
<span>U{{ device.position }} / {{ device.get_face_display }}</span>
|
||||
{% elif device.device_type.u_height %}
|
||||
{% elif device.rack and device.device_type.u_height %}
|
||||
<span class="label label-warning">Not racked</span>
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
{% if device.tenant %}
|
||||
{% if device.tenant.group %}
|
||||
<a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group.name }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device Type</td>
|
||||
<td>
|
||||
@ -236,38 +252,44 @@
|
||||
{% for iface in mgmt_interfaces %}
|
||||
{% include 'dcim/inc/interface.html' with icon='wrench' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No management interfaces defined
|
||||
{% if perms.dcim.add_interface %}
|
||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if device.device_type.interface_templates.exists %}
|
||||
<tr>
|
||||
<td colspan="6" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No management interfaces defined
|
||||
{% if perms.dcim.add_interface %}
|
||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for cp in console_ports %}
|
||||
{% include 'dcim/inc/consoleport.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No console ports defined
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if device.device_type.console_port_templates.exists %}
|
||||
<tr>
|
||||
<td colspan="6" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No console ports defined
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for pp in power_ports %}
|
||||
{% include 'dcim/inc/powerport.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No power ports defined
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if device.device_type.power_port_templates.exists %}
|
||||
<tr>
|
||||
<td colspan="6" class="alert-warning">
|
||||
<i class="fa fa-fw fa-warning"></i> No power ports defined
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
||||
@ -314,7 +336,11 @@
|
||||
<a href="{% url 'dcim:device' pk=rd.pk %}">{{ rd }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'dcim:rack' pk=rd.rack.pk %}">Rack {{ rd.rack }}</a>
|
||||
{% if rd.rack %}
|
||||
<a href="{% url 'dcim:rack' pk=rd.rack.pk %}">Rack {{ rd.rack }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ rd.device_type.full_name }}</td>
|
||||
</tr>
|
||||
@ -379,9 +405,10 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if interfaces or device.device_type.is_network_device %}
|
||||
{% if perms.dcim.delete_interface %}
|
||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="device" value="{{ device.pk }}" />
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
|
||||
|
||||
{% block content %}{{ form.errors }}
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
|
@ -1,17 +1,17 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
{% if device.rack %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:site' slug=device.rack.site.slug %}">{{ device.rack.site }}</a></li>
|
||||
<li><a href="{% url 'dcim:rack_list' %}?site={{ device.rack.site.slug }}">Racks</a></li>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a></li>
|
||||
{% if device.rack %}
|
||||
<li><a href="{% url 'dcim:rack_list' %}?site={{ device.site.slug }}">Racks</a></li>
|
||||
<li><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a></li>
|
||||
{% if device.parent_bay %}
|
||||
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
|
||||
<li>{{ device.parent_bay.name }}</li>
|
||||
{% endif %}
|
||||
<li>{{ device }}</li>
|
||||
</ol>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if device.parent_bay %}
|
||||
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
|
||||
<li>{{ device.parent_bay.name }}</li>
|
||||
{% endif %}
|
||||
<li>{{ device }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-sm-4 col-md-3">
|
||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
||||
|
@ -6,14 +6,20 @@
|
||||
{% endif %}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
|
||||
{% if iface.lag %}
|
||||
<span class="label label-primary">{{ iface.lag.name }}</span>
|
||||
{% endif %}
|
||||
{% if iface.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
||||
{% endif %}
|
||||
{% if iface.is_lag %}
|
||||
<br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<small>{{ iface.mac_address|default:'' }}</small>
|
||||
</td>
|
||||
{% if not iface.is_physical %}
|
||||
{% if iface.is_virtual %}
|
||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||
{% elif iface.connection %}
|
||||
{% with iface.connected_interface as connected_iface %}
|
||||
@ -48,7 +54,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_interface %}
|
||||
{% if iface.is_physical %}
|
||||
{% if not iface.is_virtual %}
|
||||
{% if iface.connection %}
|
||||
{% if iface.connection.connection_status %}
|
||||
<a href="#" class="btn btn-warning btn-xs interface-toggle connected" data="{{ iface.connection.pk }}" title="Mark planned">
|
||||
|
@ -1,3 +1,5 @@
|
||||
{% load helpers %}
|
||||
|
||||
<ul class="rack_legend">
|
||||
{% for u in rack.units %}
|
||||
<li>{{ u }}</li>
|
||||
@ -35,9 +37,14 @@
|
||||
{% endifequal %}
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="available">
|
||||
<li class="available{% if u.id in reserved_units.keys %} reserved{% endif %}">
|
||||
{% if perms.dcim.add_device %}
|
||||
<a href="{% url 'dcim:device_add' %}?site={{ rack.site.pk }}&rack={{ rack.pk }}&face={{ face_id }}&position={{ u.id }}" class="add_device" >add device</a>
|
||||
<a href="{% url 'dcim:device_add' %}?site={{ rack.site.pk }}&rack={{ rack.pk }}&face={{ face_id }}&position={{ u.id }}" class="add_device"
|
||||
{% if u.id in reserved_units.keys %}{% with reserved_units|getkey:u.id as resv %}
|
||||
data-toggle="popover" data-trigger="hover" data-container="body" data-html="true"
|
||||
data-content="{{ resv.description }}<br/><small>{{ resv.user }} · {{ resv.created }}</small>"
|
||||
{% endwith %}{% endif %}
|
||||
>add device</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
@ -7,88 +7,88 @@
|
||||
{% block content %}
|
||||
<h1>Connect Interfaces</h1>
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading text-center">
|
||||
<strong>A Side</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading text-center">
|
||||
<strong>A Side</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Site</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device.rack.site }}</p>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Site</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device.site }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Rack</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device.rack }}</p>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Rack</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device.rack|default:"None" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device }}</p>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ device }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface_a %}
|
||||
</div>
|
||||
{% render_field form.interface_a %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-center" style="padding-top: 90px;">
|
||||
<i class="fa fa-exchange fa-4x"></i>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading text-center">
|
||||
<strong>B Side</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||
<li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="search">
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
<div class="tab-pane" id="select">
|
||||
{% render_field form.site_b %}
|
||||
{% render_field form.rack_b %}
|
||||
{% render_field form.device_b %}
|
||||
</div>
|
||||
<div class="col-md-2 text-center" style="padding-top: 90px;">
|
||||
<i class="fa fa-exchange fa-4x"></i>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading text-center">
|
||||
<strong>B Side</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
|
||||
<li role="presentation"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="search">
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
<div class="tab-pane" id="select">
|
||||
{% render_field form.site_b %}
|
||||
{% render_field form.rack_b %}
|
||||
{% render_field form.device_b %}
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.interface_b %}
|
||||
</div>
|
||||
{% render_field form.interface_b %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
{% render_field form.connection_status %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="form-group">
|
||||
<button type="submit" name="_create" class="btn btn-primary">Connect</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Connect and Add Another</button>
|
||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
{% render_field form.connection_status %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="form-group">
|
||||
<button type="submit" name="_create" class="btn btn-primary">Connect</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Connect and Add Another</button>
|
||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -6,7 +6,10 @@
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
@ -29,6 +32,12 @@
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
<div class="tab-pane" id="select">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Site</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ poweroutlet.device.site }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.device %}
|
||||
</div>
|
||||
|
@ -6,7 +6,10 @@
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% if form.non_field_errors %}
|
||||
@ -29,6 +32,12 @@
|
||||
{% render_field form.livesearch %}
|
||||
</div>
|
||||
<div class="tab-pane" id="select">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Site</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">{{ powerport.device.site }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% render_field form.rack %}
|
||||
{% render_field form.pdu %}
|
||||
</div>
|
||||
|
@ -64,6 +64,10 @@
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>
|
||||
{% if rack.site.region %}
|
||||
<a href="{{ rack.site.region.get_absolute_url }}">{{ rack.site.region }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:site' slug=rack.site.slug %}">{{ rack.site }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
@ -91,6 +95,10 @@
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
{% if rack.tenant %}
|
||||
{% if rack.tenant.group %}
|
||||
<a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group.name }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
@ -189,6 +197,51 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Reservations</strong>
|
||||
</div>
|
||||
{% if reservations %}
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<th>Units</th>
|
||||
<th>Description</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for resv in reservations %}
|
||||
<tr>
|
||||
<td>{{ resv.units|join:', ' }}</td>
|
||||
<td>
|
||||
{{ resv.description }}<br />
|
||||
<small>{{ resv.user }} · {{ resv.created }}</small>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if perms.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=resv.pk %}" class="btn btn-warning btn-xs" title="Edit reservation">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.delete_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_delete' pk=resv.pk %}" class="btn btn-danger btn-xs" title="Delete reservation">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">None</div>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_rackreservation %}
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'dcim:rack_add_reservation' rack=rack.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add a reservation
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row col-md-6">
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
|
21
netbox/templates/dcim/region_list.html
Normal file
21
netbox/templates/dcim/region_list.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Regions{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_region %}
|
||||
<a href="{% url 'dcim:region_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
Add a region
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ block.title }}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -9,7 +9,12 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
|
||||
{% if site.region %}
|
||||
{% for region in site.region.get_ancestors %}
|
||||
<li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
|
||||
{% endfor %}
|
||||
<li><a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ site }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
@ -55,10 +60,28 @@
|
||||
<strong>Site</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Region</td>
|
||||
<td>
|
||||
{% if site.region %}
|
||||
{% for region in site.region.get_ancestors %}
|
||||
<a href="{{ region.get_absolute_url }}">{{ region }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endfor %}
|
||||
<a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
{% if site.tenant %}
|
||||
{% if site.tenant.group %}
|
||||
<a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group.name }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
@ -85,6 +108,13 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Contact Info</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Physical Address</td>
|
||||
<td>
|
||||
|
@ -7,6 +7,7 @@
|
||||
<div class="panel-body">
|
||||
{% render_field form.name %}
|
||||
{% render_field form.slug %}
|
||||
{% render_field form.region %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.facility %}
|
||||
{% render_field form.asn %}
|
||||
|
@ -38,6 +38,11 @@
|
||||
<td>URL-friendly name</td>
|
||||
<td>ash4-south</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Region</td>
|
||||
<td>Name of region (optional)</td>
|
||||
<td>North America</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
@ -71,7 +76,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>ASH-4 South,ash4-south,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre>
|
||||
<pre>ASH-4 South,ash4-south,North America,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -30,8 +30,16 @@
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
{% if prefix.tenant %}
|
||||
{% if prefix.tenant.group %}
|
||||
<a href="{{ prefix.tenant.group.get_absolute_url }}">{{ prefix.tenant.group.name }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
|
||||
{% elif prefix.vrf.tenant %}
|
||||
{% if prefix.vrf.tenant.group %}
|
||||
<a href="{{ prefix.vrf.tenant.group.get_absolute_url }}">{{ prefix.vrf.tenant.group.name }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
|
||||
<label class="label label-info">Inherited</label>
|
||||
{% else %}
|
||||
@ -53,6 +61,10 @@
|
||||
<td>Site</td>
|
||||
<td>
|
||||
{% if prefix.site %}
|
||||
{% if prefix.site.region %}
|
||||
<a href="{{ prefix.site.region.get_absolute_url }}">{{ prefix.site.region }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:site' slug=prefix.site.slug %}">{{ prefix.site }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
@ -63,6 +75,10 @@
|
||||
<td>VLAN</td>
|
||||
<td>
|
||||
{% if prefix.vlan %}
|
||||
{% if prefix.vlan.group %}
|
||||
<a href="{{ prefix.vlan.group.get_absolute_url }}">{{ prefix.vlan.group.name }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
@ -79,7 +95,7 @@
|
||||
<td>Role</td>
|
||||
<td>
|
||||
{% if prefix.role %}
|
||||
<span>{{ prefix.role }}</span>
|
||||
<a href="{% url 'ipam:prefix_list' %}?role={{ prefix.role.slug }}">{{ prefix.role }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
|
@ -8,9 +8,11 @@
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
|
||||
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
|
||||
{% if vlan.site %}
|
||||
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
|
||||
{% endif %}
|
||||
{% if vlan.group %}
|
||||
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}&group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li>
|
||||
<li><a href="{% url 'ipam:vlan_list' %}?group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ vlan.name }} ({{ vlan.vid }})</li>
|
||||
</ol>
|
||||
@ -53,7 +55,17 @@
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td><a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a></td>
|
||||
<td>
|
||||
{% if vlan.site %}
|
||||
{% if vlan.site.region %}
|
||||
<a href="{{ vlan.site.region.get_absolute_url }}">{{ vlan.site.region }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
@ -77,6 +89,10 @@
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
{% if vlan.tenant %}
|
||||
{% if vlan.tenant.group %}
|
||||
<a href="{{ vlan.tenant.group.get_absolute_url }}">{{ vlan.tenant.group.name }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endif %}
|
||||
<a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
@ -93,7 +109,7 @@
|
||||
<td>Role</td>
|
||||
<td>
|
||||
{% if vlan.role %}
|
||||
<span>{{ vlan.role }}</span>
|
||||
<a href="{% url 'ipam:vlan_list' %}?role={{ vlan.role.slug }}">{{ vlan.role }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
|
@ -8,8 +8,8 @@ from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
group_id = NullableModelMultipleChoiceFilter(
|
||||
@ -26,9 +26,11 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = ['q', 'group_id', 'group', 'name']
|
||||
fields = ['name']
|
||||
|
||||
def search(self, queryset, value):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
|
@ -56,5 +56,8 @@ class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
model = Tenant
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
group = FilterChoiceField(queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')),
|
||||
to_field_name='slug', null_option=(0, 'None'))
|
||||
group = FilterChoiceField(
|
||||
queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')),
|
||||
to_field_name='slug',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
|
@ -2,6 +2,8 @@ import csv
|
||||
import itertools
|
||||
import re
|
||||
|
||||
from mptt.forms import TreeNodeMultipleChoiceField
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
@ -169,6 +171,27 @@ class SelectWithDisabled(forms.Select):
|
||||
force_text(option_label))
|
||||
|
||||
|
||||
class ArrayFieldSelectMultiple(SelectWithDisabled, forms.SelectMultiple):
|
||||
"""
|
||||
MultiSelect widgets for a SimpleArrayField. Choices must be populated on the widget.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.delimiter = kwargs.pop('delimiter', ',')
|
||||
super(ArrayFieldSelectMultiple, self).__init__(*args, **kwargs)
|
||||
|
||||
def render_options(self, selected_choices):
|
||||
# Split the delimited string of values into a list
|
||||
if selected_choices:
|
||||
selected_choices = selected_choices.split(self.delimiter)
|
||||
return super(ArrayFieldSelectMultiple, self).render_options(selected_choices)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
# Condense the list of selected choices into a delimited string
|
||||
data = super(ArrayFieldSelectMultiple, self).value_from_datadict(data, files, name)
|
||||
return self.delimiter.join(data)
|
||||
|
||||
|
||||
class APISelect(SelectWithDisabled):
|
||||
"""
|
||||
A select widget populated via an API call
|
||||
@ -344,7 +367,7 @@ class SlugField(forms.SlugField):
|
||||
self.widget.attrs['slug-source'] = slug_source
|
||||
|
||||
|
||||
class FilterChoiceField(forms.ModelMultipleChoiceField):
|
||||
class FilterChoiceFieldMixin(object):
|
||||
iterator = forms.models.ModelChoiceIterator
|
||||
|
||||
def __init__(self, null_option=None, *args, **kwargs):
|
||||
@ -353,12 +376,13 @@ class FilterChoiceField(forms.ModelMultipleChoiceField):
|
||||
kwargs['required'] = False
|
||||
if 'widget' not in kwargs:
|
||||
kwargs['widget'] = forms.SelectMultiple(attrs={'size': 6})
|
||||
super(FilterChoiceField, self).__init__(*args, **kwargs)
|
||||
super(FilterChoiceFieldMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
label = super(FilterChoiceFieldMixin, self).label_from_instance(obj)
|
||||
if hasattr(obj, 'filter_count'):
|
||||
return u'{} ({})'.format(obj, obj.filter_count)
|
||||
return force_text(obj)
|
||||
return u'{} ({})'.format(label, obj.filter_count)
|
||||
return label
|
||||
|
||||
def _get_choices(self):
|
||||
if hasattr(self, '_choices'):
|
||||
@ -370,6 +394,14 @@ class FilterChoiceField(forms.ModelMultipleChoiceField):
|
||||
choices = property(_get_choices, forms.ChoiceField._set_choices)
|
||||
|
||||
|
||||
class FilterChoiceField(FilterChoiceFieldMixin, forms.ModelMultipleChoiceField):
|
||||
pass
|
||||
|
||||
|
||||
class FilterTreeNodeMultipleChoiceField(FilterChoiceFieldMixin, TreeNodeMultipleChoiceField):
|
||||
pass
|
||||
|
||||
|
||||
class LaxURLField(forms.URLField):
|
||||
"""
|
||||
Custom URLField which allows any valid URL scheme
|
||||
|
@ -27,6 +27,14 @@ def getlist(value, arg):
|
||||
return value.getlist(arg)
|
||||
|
||||
|
||||
@register.filter
|
||||
def getkey(value, key):
|
||||
"""
|
||||
Return a dictionary item specified by key
|
||||
"""
|
||||
return value[key]
|
||||
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
def gfm(value):
|
||||
"""
|
||||
|
@ -145,9 +145,9 @@ class ObjectEditView(View):
|
||||
return get_object_or_404(self.model, pk=kwargs['pk'])
|
||||
return self.model()
|
||||
|
||||
def alter_obj(self, obj, args, kwargs):
|
||||
def alter_obj(self, obj, request, url_args, url_kwargs):
|
||||
# Allow views to add extra info to an object before it is processed. For example, a parent object can be defined
|
||||
# given some parameter from the request URI.
|
||||
# given some parameter from the request URL.
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
@ -159,7 +159,7 @@ class ObjectEditView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
obj = self.get_object(kwargs)
|
||||
obj = self.alter_obj(obj, args, kwargs)
|
||||
obj = self.alter_obj(obj, request, args, kwargs)
|
||||
initial_data = {k: request.GET[k] for k in self.fields_initial if k in request.GET}
|
||||
form = self.form_class(instance=obj, initial=initial_data)
|
||||
|
||||
@ -173,7 +173,7 @@ class ObjectEditView(View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
obj = self.get_object(kwargs)
|
||||
obj = self.alter_obj(obj, args, kwargs)
|
||||
obj = self.alter_obj(obj, request, args, kwargs)
|
||||
form = self.form_class(request.POST, instance=obj)
|
||||
|
||||
if form.is_valid():
|
||||
@ -307,11 +307,12 @@ class BulkAddView(View):
|
||||
if form.is_valid():
|
||||
|
||||
# The first field will be used as the pattern
|
||||
pattern_field = form.fields.keys()[0]
|
||||
field_names = list(form.fields.keys())
|
||||
pattern_field = field_names[0]
|
||||
pattern = form.cleaned_data[pattern_field]
|
||||
|
||||
# All other fields will be copied as object attributes
|
||||
kwargs = {k: form.cleaned_data[k] for k in form.fields.keys()[1:]}
|
||||
kwargs = {k: form.cleaned_data[k] for k in field_names[1:]}
|
||||
|
||||
new_objs = []
|
||||
try:
|
||||
@ -470,7 +471,9 @@ class BulkEditView(View):
|
||||
return redirect(return_url)
|
||||
|
||||
else:
|
||||
form = self.form(self.cls, initial={'pk': pk_list})
|
||||
initial_data = request.POST.copy()
|
||||
initial_data['pk'] = pk_list
|
||||
form = self.form(self.cls, initial=initial_data)
|
||||
|
||||
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
||||
if not selected_objects:
|
||||
|
@ -2,7 +2,8 @@ cffi>=1.8
|
||||
cryptography>=1.4
|
||||
Django>=1.10
|
||||
django-debug-toolbar>=1.6
|
||||
django-filter==0.15.3
|
||||
django-filter>=1.0.1
|
||||
django-mptt==0.8.7
|
||||
django-rest-swagger==0.3.10
|
||||
django-tables2>=1.2.5
|
||||
djangorestframework>=3.5.0
|
||||
|
Loading…
Reference in New Issue
Block a user