Initial push to public repo
This commit is contained in:
commit
27b289ee3b
|
@ -0,0 +1,6 @@
|
|||
*.pyc
|
||||
configuration.py
|
||||
.idea
|
||||
*.sh
|
||||
fabfile.py
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
|
@ -0,0 +1,51 @@
|
|||
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers.
|
||||
|
||||
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/) Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a complete list of requirements, see `requirements.txt`.
|
||||
|
||||
# Components
|
||||
|
||||
NetBox understands all of the physical and logical building blocks that comprise network infrastructure, and the manners in which they are all related.
|
||||
|
||||
## DCIM
|
||||
|
||||
DCIM comprises all the physical installations and connections which comprise a network. NetBox tracks where devices are installed, as well as their individual power, console, and network connections.
|
||||
|
||||
**Site:** A physical location (typically a building) where network devices are installed. Devices in different sites cannot be directly connected to one another.
|
||||
|
||||
**Rack:** An equipment rack into which devices are installed. Each rack belongs to a site.
|
||||
|
||||
**Device:** Any type of rack-mounted device. For example, routers, switches, servers, console servers, PDUs, etc. 0U (non-rack-mounted) devices are supported.
|
||||
|
||||
## IPAM
|
||||
|
||||
IPAM deals with the IP addressing and VLANs in use on a network. NetBox makes a distinction between IP prefixes (networks) and individual IP addresses.
|
||||
|
||||
Because NetBox is a combined DCIM/IPAM system, IP addresses can be assigned to device interfaces in the application just as they are in the real world.
|
||||
|
||||
**Aggregate:** A top-level aggregate of IP address space; for example, 10.0.0.0/8 or 2001:db8::/32. Each aggregate belongs to a regional Internet registry (RIR) like ARIN or RIPE, or to an authoritative standard such as RFC 1918.
|
||||
|
||||
**VRF:** A virtual routing table. VRF support is currently still under development.
|
||||
|
||||
**Prefix:** An IPv4 or IPv6 network. A prefix can be assigned to a VRF; if not, it is considered to belong to the global table. Prefixes are grouped by aggregates automatically and can optionally be assigned to sites.
|
||||
|
||||
**IP Address:** An individual IPv4 or IPv6 address (with CIDR mask). IP address can be assigned to device interfaces.
|
||||
|
||||
**VLAN:** VLANs are assigned to sites, and can optionally have one or more IP prefixes assigned to them. VLAN IDs are unique only within the scope of a site.
|
||||
|
||||
## Circuits
|
||||
|
||||
Long-distance data connections are typically referred to as _circuits_. NetBox provides a method for managing circuits and their providers. Individual circuits can be terminated to device interfaces.
|
||||
|
||||
**Provider:** An entity to which a network connects to. This can be a transit provider, peer, or some other organization.
|
||||
|
||||
**Circuit:** A data circuit which connects to a provider. The local end of a circuit can be assigned to a device interface.
|
||||
|
||||
## Secrets
|
||||
|
||||
NetBox provides encrypted storage of sensitive data it calls _secrets_. Each user may be issued an encryption key with which stored secrets can be retrieved.
|
||||
|
||||
Note that NetBox does not merely hash secrets, a function which is only useful for validation. It employs fully reversible AES-256 encryption so that secret data can be retrieved and consumed by other services.
|
||||
|
||||
**Secrets** Any piece of confidential data which must be retrievable. For example: passwords, SNMP communities, RADIUS shared secrets, etc.
|
||||
|
||||
**User Key:** An individual user's encrypted copy of the master key, which can be used to retrieve secret data.
|
|
@ -0,0 +1,30 @@
|
|||
Circuits are communication links which connect two endpoints, typically over long distances. For example, a circuit might connect an enterprise to its Internet service provider. NetBox can track circuits and their providers.
|
||||
|
||||
# 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.
|
||||
|
||||
Each provider may be assigned an autonomous system number (ASN) for reference. Each provider can also be assigned account and contact information, as well as miscellaneous comments.
|
||||
|
||||
# Circuits
|
||||
|
||||
A circuit represents a single physical data link connecting two endpoints. Each circuit belongs to a provider and must be assigned circuit ID which is unique to that provider. Each circuit must also be assigned to a site, and may optionally be connected to a specific interface on a specific device within that site.
|
||||
|
||||
NetBox also tracks miscellaneous circuit attributes (most of which are optional), including:
|
||||
|
||||
* Date of installation
|
||||
* Port speed
|
||||
* Commit rate
|
||||
* Cross-connect ID
|
||||
* Patch panel information
|
||||
|
||||
## Circuit Type
|
||||
|
||||
Circuits can be classified by type. For example:
|
||||
|
||||
* Internet transit
|
||||
* Out-of-band connectivity
|
||||
* Peering
|
||||
* Private backhaul
|
||||
|
||||
Each circuit must be assigned exactly one circuit type.
|
|
@ -0,0 +1,84 @@
|
|||
The data center infrastructure management (DCIM) component of NetBox assists in the management of physical assets within a network: equipment racks, the gear in them, and the cabling that connects it all.
|
||||
|
||||
# Sites
|
||||
|
||||
A site is a geographic location at which network equipment is housed. How you choose to define sites will depend on the nature of your organization, but typically a site will be 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.
|
||||
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
Each group is assigned to a parent sire for easy navigation. Hierarchical recursion of rack groups is not currently supported.
|
||||
|
||||
# Devices
|
||||
|
||||
Every piece of hardware which is installed within a rack exists in NetBox as a device. Devices are measured in rack units (U) and whether they are full 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.
|
||||
|
||||
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.
|
||||
|
||||
Each device has a physical device type (make and model), which is discussed below.
|
||||
|
||||
## Device 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.
|
||||
|
||||
## Platform
|
||||
|
||||
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.
|
||||
|
||||
## Modules
|
||||
|
||||
A device can be assigned modules which represent internal components. Currently, these are used merely for inventory tracking, although future development might see their functionality expand.
|
||||
|
||||
## Device Components
|
||||
|
||||
There are five types of device components which comprise all of the interconnection logic with NetBox:
|
||||
|
||||
* Console ports
|
||||
* Console server ports
|
||||
* Power ports
|
||||
* Power outlets
|
||||
* Interfaces
|
||||
|
||||
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 defined 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.
|
||||
|
||||
# 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).
|
||||
|
||||
## Manufacturers
|
||||
|
||||
Each device type belongs to one manufacturer; e.g. Cisco, Opengear, or APC. Manufacturers are used to group different models of device.
|
||||
|
||||
## Device 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:
|
||||
|
||||
* Console port templates
|
||||
* Console server port templates
|
||||
* Power port templates
|
||||
* Power outlet templates
|
||||
* Interface templates
|
||||
|
||||
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:
|
||||
|
||||
* 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.
|
||||
|
||||
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.
|
|
@ -0,0 +1,80 @@
|
|||
IP address management (IPAM) entails the allocation of IP networks and addresses. Within NetBox, at least, IPAM also includes the management of VLAN assignments.
|
||||
|
||||
# VRF
|
||||
|
||||
A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain within a network. Each VRF is essentially a separate routing table: the same IP prefix or address can exist in multiple VRFs. VRFs are commonly used to isolate customers or organizations from one another within a network.
|
||||
|
||||
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.
|
||||
|
||||
# Aggregate
|
||||
|
||||
IPv4 address space is organized as a hierarchy, with more-specific (smaller) prefix arranged as child nodes under less-specific (larger) prefixes. For example:
|
||||
|
||||
* 10.0.0.0/8
|
||||
* 10.1.0.0/16
|
||||
* 10.1.2.0/24
|
||||
|
||||
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:
|
||||
|
||||
* 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.
|
||||
|
||||
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.
|
||||
|
||||
## 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. NetBox by default will be populated with the RIRs listed above, however you are free to remove these and/or create your own if you choose.
|
||||
|
||||
# Prefixes
|
||||
|
||||
A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 192.0.2.0/24). A prefix entails only the "network portion" of an IP address; all bits in the address not covered by the mask must be zero.
|
||||
|
||||
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.
|
||||
|
||||
## Status
|
||||
|
||||
Each prefix is assigned an operational status. A status describes very generally the state of a prefix within the network; for example, statuses might include:
|
||||
|
||||
* Active (provisioned)
|
||||
* Reserved (for future use)
|
||||
* Deprecated (no longer in use)
|
||||
* Container (a summary of child prefixes)
|
||||
|
||||
NetBox provides several statuses by default, but you are free to change them to suit the needs of your organization.
|
||||
|
||||
## Role
|
||||
|
||||
Whereas a status describes a prefix's operational state, a role describes its function. For example, roles might include:
|
||||
|
||||
* Access segment
|
||||
* Infrastructure
|
||||
* NAT
|
||||
* Lab
|
||||
* Out-of-band
|
||||
|
||||
Role assignment is optional. And like statuses, you are free to create your own.
|
||||
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
# VLAN
|
||||
|
||||
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.
|
||||
|
||||
Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
|
|
@ -0,0 +1,23 @@
|
|||
"Secrets" are small amounts of data that must be kept confidential; for example, passwords and SNMP community strings. NetBox provides encrypted storage of secret data.
|
||||
|
||||
# Secret
|
||||
|
||||
A secret represents a single credential or other string which must be stored securely. Each secret is assigned to a parent object with NetBox, such as a device. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext.
|
||||
|
||||
Each secret can also store an optional name parameter, which is not encrypted. This may be useful for storing user names.
|
||||
|
||||
## Secret Roles
|
||||
|
||||
Each secret is assigned a functional role which indicates what it is used for. Typical roles might include:
|
||||
|
||||
* Login credentials
|
||||
* SNMP community strings
|
||||
* RADIUS/TACACS+ keys
|
||||
* IKE key strings
|
||||
* Routing protocol shared secrets
|
||||
|
||||
# User Keys
|
||||
|
||||
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.
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,30 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import Provider, CircuitType, Circuit
|
||||
|
||||
|
||||
@admin.register(Provider)
|
||||
class ProviderAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'asn']
|
||||
|
||||
|
||||
@admin.register(CircuitType)
|
||||
class CircuitTypeAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug']
|
||||
|
||||
|
||||
@admin.register(Circuit)
|
||||
class CircuitAdmin(admin.ModelAdmin):
|
||||
list_display = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id']
|
||||
list_filter = ['provider']
|
||||
exclude = ['interface']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(CircuitAdmin, self).get_queryset(request)
|
||||
return qs.select_related('provider', 'type', 'site')
|
|
@ -0,0 +1,60 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import Provider, CircuitType, Circuit
|
||||
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||
|
||||
|
||||
class ProviderNestedSerializer(ProviderSerializer):
|
||||
|
||||
class Meta(ProviderSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Circuit types
|
||||
#
|
||||
|
||||
class CircuitTypeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class CircuitTypeNestedSerializer(CircuitTypeSerializer):
|
||||
|
||||
class Meta(CircuitTypeSerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitSerializer(serializers.ModelSerializer):
|
||||
provider = ProviderNestedSerializer()
|
||||
type = CircuitTypeNestedSerializer()
|
||||
site = SiteNestedSerializer()
|
||||
interface = InterfaceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'cid', 'provider', 'type', 'site', 'interface', 'install_date', 'port_speed', 'commit_rate',
|
||||
'xconnect_id', 'comments']
|
||||
|
||||
|
||||
class CircuitNestedSerializer(CircuitSerializer):
|
||||
|
||||
class Meta(CircuitSerializer.Meta):
|
||||
fields = ['id', 'cid']
|
|
@ -0,0 +1,24 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from extras.models import GRAPH_TYPE_PROVIDER
|
||||
from extras.api.views import GraphListView
|
||||
|
||||
from .views import *
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# Providers
|
||||
url(r'^providers/$', ProviderListView.as_view(), name='provider_list'),
|
||||
url(r'^providers/(?P<pk>\d+)/$', ProviderDetailView.as_view(), name='provider_detail'),
|
||||
url(r'^providers/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_PROVIDER}, name='provider_graphs'),
|
||||
|
||||
# Circuit types
|
||||
url(r'^circuit-types/$', CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
url(r'^circuit-types/(?P<pk>\d+)/$', CircuitTypeDetailView.as_view(), name='circuittype_detail'),
|
||||
|
||||
# Circuits
|
||||
url(r'^circuits/$', CircuitListView.as_view(), name='circuit_list'),
|
||||
url(r'^circuits/(?P<pk>\d+)/$', CircuitDetailView.as_view(), name='circuit_detail'),
|
||||
|
||||
]
|
|
@ -0,0 +1,54 @@
|
|||
from rest_framework import generics
|
||||
|
||||
from circuits.models import Provider, CircuitType, Circuit
|
||||
from circuits.filters import CircuitFilter
|
||||
from .serializers import ProviderSerializer, CircuitTypeSerializer, CircuitSerializer
|
||||
|
||||
|
||||
class ProviderListView(generics.ListAPIView):
|
||||
"""
|
||||
List all providers
|
||||
"""
|
||||
queryset = Provider.objects.all()
|
||||
serializer_class = ProviderSerializer
|
||||
|
||||
|
||||
class ProviderDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single provider
|
||||
"""
|
||||
queryset = Provider.objects.all()
|
||||
serializer_class = ProviderSerializer
|
||||
|
||||
|
||||
class CircuitTypeListView(generics.ListAPIView):
|
||||
"""
|
||||
List all circuit types
|
||||
"""
|
||||
queryset = CircuitType.objects.all()
|
||||
serializer_class = CircuitTypeSerializer
|
||||
|
||||
|
||||
class CircuitTypeDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single circuit type
|
||||
"""
|
||||
queryset = CircuitType.objects.all()
|
||||
serializer_class = CircuitTypeSerializer
|
||||
|
||||
|
||||
class CircuitListView(generics.ListAPIView):
|
||||
"""
|
||||
List circuits (filterable)
|
||||
"""
|
||||
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
|
||||
serializer_class = CircuitSerializer
|
||||
filter_class = CircuitFilter
|
||||
|
||||
|
||||
class CircuitDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single circuit
|
||||
"""
|
||||
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
|
||||
serializer_class = CircuitSerializer
|
|
@ -0,0 +1,52 @@
|
|||
import django_filters
|
||||
|
||||
from dcim.models import Site
|
||||
from circuits.models import Provider, Circuit, CircuitType
|
||||
|
||||
|
||||
class CircuitFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='provider',
|
||||
queryset=Provider.objects.all(),
|
||||
label='Provider (ID)',
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
name='provider',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Provider (slug)',
|
||||
)
|
||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='type',
|
||||
queryset=CircuitType.objects.all(),
|
||||
label='Circuit type (ID)',
|
||||
)
|
||||
type = django_filters.ModelMultipleChoiceFilter(
|
||||
name='type',
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Circuit type (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date']
|
||||
|
||||
def search(self, queryset, value):
|
||||
value = value.strip()
|
||||
return queryset.filter(cid__icontains=value)
|
|
@ -0,0 +1,191 @@
|
|||
from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
||||
from utilities.forms import BootstrapMixin, SmallTextarea, ConfirmationForm, APISelect, Livesearch
|
||||
|
||||
from .models import PORT_SPEED_CHOICES, Circuit, Provider, CircuitType
|
||||
from utilities.forms import CommentField, CSVDataField, BulkImportForm
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderForm(forms.ModelForm, BootstrapMixin):
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||
widgets = {
|
||||
'noc_contact': SmallTextarea(attrs={'rows': 5}),
|
||||
'admin_contact': SmallTextarea(attrs={'rows': 5}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': "Full name of the provider",
|
||||
'slug': "URL-friendly unique shorthand (e.g. 'decix' for DE-CIX)",
|
||||
'asn': "BGP autonomous system number (if applicable)",
|
||||
'portal_url': "URL of the provider's customer support portal",
|
||||
'noc_contact': "NOC email address and phone number",
|
||||
'admin_contact': "Administrative contact email address and phone number",
|
||||
}
|
||||
|
||||
|
||||
class ProviderFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url']
|
||||
|
||||
|
||||
class ProviderImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=ProviderFromCSVForm)
|
||||
|
||||
|
||||
class ProviderBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
asn = forms.IntegerField(required=False, label='ASN')
|
||||
account = forms.CharField(max_length=30, required=False, label='Account number')
|
||||
portal_url = forms.URLField(required=False, label='Portal')
|
||||
noc_contact = forms.CharField(required=False, widget=SmallTextarea, label='NOC contact')
|
||||
admin_contact = forms.CharField(required=False, widget=SmallTextarea, label='Admin contact')
|
||||
comments = CommentField()
|
||||
|
||||
|
||||
class ProviderBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitForm(forms.ModelForm, BootstrapMixin):
|
||||
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}}',
|
||||
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/?type=physical',
|
||||
disabled_indicator='is_connected'))
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
|
||||
'port_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
|
||||
]
|
||||
help_texts = {
|
||||
'cid': "Unique circuit ID",
|
||||
'install_date': "Format: YYYY-MM-DD",
|
||||
'port_speed': "Physical circuit speed",
|
||||
'commit_rate': "Commited rate (in Mbps)",
|
||||
'xconnect_id': "ID of the local cross-connect",
|
||||
'pp_info': "Patch panel ID and port number(s)"
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(CircuitForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# If this circuit has been assigned to an interface, initialize rack and device
|
||||
if self.instance.interface:
|
||||
self.initial['rack'] = self.instance.interface.device.rack
|
||||
self.initial['device'] = self.instance.interface.device
|
||||
|
||||
# Limit rack choices
|
||||
if self.is_bound:
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Limit device choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
|
||||
elif self.initial.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
|
||||
# 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', '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', 'connected_as_a', 'connected_as_b')
|
||||
self.fields['interface'].widget.attrs['initial'] = self.initial.get('interface')
|
||||
else:
|
||||
interfaces = []
|
||||
self.fields['interface'].choices = [
|
||||
(iface.id, {
|
||||
'label': iface.name,
|
||||
'disabled': iface.is_connected and iface.id != self.fields['interface'].widget.attrs.get('initial'),
|
||||
}) for iface in interfaces
|
||||
]
|
||||
|
||||
|
||||
class CircuitFromCSVForm(forms.ModelForm):
|
||||
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Provider not found.'})
|
||||
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
||||
site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id',
|
||||
'pp_info']
|
||||
|
||||
|
||||
class CircuitImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
||||
|
||||
|
||||
class CircuitBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
|
||||
provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
|
||||
port_speed = forms.ChoiceField(choices=[(None, '---------')] + PORT_SPEED_CHOICES, required=False,
|
||||
label='Port speed')
|
||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Mbps)')
|
||||
comments = CommentField()
|
||||
|
||||
|
||||
class CircuitBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def circuit_type_choices():
|
||||
type_choices = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||
return [(t.slug, '{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
|
||||
|
||||
|
||||
def circuit_provider_choices():
|
||||
provider_choices = Provider.objects.annotate(circuit_count=Count('circuits'))
|
||||
return [(p.slug, '{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
|
||||
|
||||
|
||||
def circuit_site_choices():
|
||||
site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
|
||||
|
||||
|
||||
class CircuitFilterForm(forms.Form, BootstrapMixin):
|
||||
type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
|
||||
provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
@ -0,0 +1,82 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.1 on 2016-02-27 02:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('dcim', '__first__'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Circuit',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('cid', models.CharField(max_length=50, verbose_name=b'Circuit ID')),
|
||||
('install_date', models.DateField(blank=True, null=True, verbose_name=b'Date installed')),
|
||||
('port_speed', models.PositiveSmallIntegerField(choices=[[100, b'100 Mbps'], [1000, b'1 Gbps'], [10000, b'10 Gbps'], [25000, b'25 Gbps'], [40000, b'40 Gbps'], [50000, b'50 Gbps'], [100000, b'100 Gbps']], verbose_name=b'Port speed')),
|
||||
('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'Commit rate (Mbps)')),
|
||||
('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')),
|
||||
('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='circuit', to='dcim.Interface')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['provider', 'cid'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CircuitType',
|
||||
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)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Provider',
|
||||
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)),
|
||||
('asn', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'ASN')),
|
||||
('account', models.CharField(blank=True, max_length=30, verbose_name=b'Account number')),
|
||||
('portal_url', models.URLField(blank=True, verbose_name=b'Portal')),
|
||||
('noc_contact', models.TextField(blank=True, verbose_name=b'NOC Contact')),
|
||||
('admin_contact', models.TextField(blank=True, verbose_name=b'Admin Contact')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='provider',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.Provider'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='dcim.Site'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.CircuitType'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='circuit',
|
||||
unique_together=set([('provider', 'cid')]),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,86 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
|
||||
from dcim.models import Site, Interface
|
||||
|
||||
|
||||
PORT_SPEED_100M = 100
|
||||
PORT_SPEED_1G = 1000
|
||||
PORT_SPEED_10G = 10000
|
||||
PORT_SPEED_25G = 25000
|
||||
PORT_SPEED_40G = 40000
|
||||
PORT_SPEED_50G = 50000
|
||||
PORT_SPEED_100G = 100000
|
||||
PORT_SPEED_CHOICES = [
|
||||
[PORT_SPEED_100M, '100 Mbps'],
|
||||
[PORT_SPEED_1G, '1 Gbps'],
|
||||
[PORT_SPEED_10G, '10 Gbps'],
|
||||
[PORT_SPEED_25G, '25 Gbps'],
|
||||
[PORT_SPEED_40G, '40 Gbps'],
|
||||
[PORT_SPEED_50G, '50 Gbps'],
|
||||
[PORT_SPEED_100G, '100 Gbps'],
|
||||
]
|
||||
|
||||
|
||||
class Provider(models.Model):
|
||||
"""
|
||||
A transit provider, IX, or direct peer
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
|
||||
account = models.CharField(max_length=30, blank=True, verbose_name='Account number')
|
||||
portal_url = models.URLField(blank=True, verbose_name='Portal')
|
||||
noc_contact = models.TextField(blank=True, verbose_name='NOC Contact')
|
||||
admin_contact = models.TextField(blank=True, verbose_name='Admin Contact')
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provider', args=[self.slug])
|
||||
|
||||
|
||||
class CircuitType(models.Model):
|
||||
"""
|
||||
A type of circuit
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Circuit(models.Model):
|
||||
"""
|
||||
A data circuit from a site to a provider (includes IX connections)
|
||||
"""
|
||||
cid = models.CharField(max_length=50, verbose_name='Circuit ID')
|
||||
provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
|
||||
type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
|
||||
site = models.ForeignKey(Site, related_name='circuits', on_delete=models.PROTECT)
|
||||
interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True)
|
||||
install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
|
||||
port_speed = models.PositiveSmallIntegerField(choices=PORT_SPEED_CHOICES, verbose_name='Port speed')
|
||||
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Mbps)')
|
||||
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
|
||||
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider', 'cid']
|
||||
unique_together = ['provider', 'cid']
|
||||
|
||||
def __unicode__(self):
|
||||
return "{0} {1}".format(self.provider, self.cid)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
|
@ -0,0 +1,59 @@
|
|||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from .models import Circuit, Provider
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
class ProviderTable(tables.Table):
|
||||
name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
|
||||
asn = tables.Column(verbose_name='ASN')
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ('name', 'asn', 'circuit_count')
|
||||
empty_text = "No providers found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class ProviderBulkEditTable(ProviderTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(ProviderTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'name', 'asn', 'circuit_count')
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
class CircuitTable(tables.Table):
|
||||
cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
|
||||
type = tables.Column(verbose_name='Type')
|
||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
port_speed = tables.Column(verbose_name='Port Speed')
|
||||
commit_rate = tables.Column(verbose_name='Commit (Mbps)')
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ('cid', 'type', 'provider', 'site', 'port_speed', 'commit_rate')
|
||||
empty_text = "No circuits found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class CircuitBulkEditTable(CircuitTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(CircuitTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'cid', 'type', 'provider', 'site', 'port_speed', 'commit_rate')
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,23 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^circuits/$', views.circuit_list, name='circuit_list'),
|
||||
url(r'^circuits/add/$', views.circuit_add, name='circuit_add'),
|
||||
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
||||
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
||||
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
||||
url(r'^circuits/(?P<pk>\d+)/$', views.circuit, name='circuit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/edit/$', views.circuit_edit, name='circuit_edit'),
|
||||
url(r'^circuits/(?P<pk>\d+)/delete/$', views.circuit_delete, name='circuit_delete'),
|
||||
|
||||
url(r'^providers/$', views.provider_list, name='provider_list'),
|
||||
url(r'^providers/add/$', views.provider_add, name='provider_add'),
|
||||
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
||||
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/$', views.provider, name='provider'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.provider_edit, name='provider_edit'),
|
||||
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.provider_delete, name='provider_delete'),
|
||||
]
|
|
@ -0,0 +1,309 @@
|
|||
from django_tables2 import RequestConfig
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Count, ProtectedError
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from extras.models import ExportTemplate
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import BulkImportView, BulkEditView, BulkDeleteView
|
||||
|
||||
from .filters import CircuitFilter
|
||||
from .forms import CircuitForm, CircuitImportForm, CircuitBulkEditForm, CircuitBulkDeleteForm, CircuitFilterForm, \
|
||||
ProviderForm, ProviderImportForm, ProviderBulkEditForm, ProviderBulkDeleteForm
|
||||
from .models import Circuit, Provider
|
||||
from .tables import CircuitTable, CircuitBulkEditTable, ProviderTable, ProviderBulkEditTable
|
||||
|
||||
|
||||
#
|
||||
# Providers
|
||||
#
|
||||
|
||||
def provider_list(request):
|
||||
|
||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||
|
||||
# Export
|
||||
if 'export' in request.GET:
|
||||
et = get_object_or_404(ExportTemplate, content_type__model='provider', name=request.GET.get('export'))
|
||||
response = et.to_response(context_dict={'queryset': queryset}, filename='netbox_providers')
|
||||
return response
|
||||
|
||||
if request.user.has_perm('circuits.change_provider') or request.user.has_perm('circuits.delete_provider'):
|
||||
provider_table = ProviderBulkEditTable(queryset)
|
||||
else:
|
||||
provider_table = ProviderTable(queryset)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator}).configure(provider_table)
|
||||
|
||||
export_templates = ExportTemplate.objects.filter(content_type__model='provider')
|
||||
|
||||
return render(request, 'circuits/provider_list.html', {
|
||||
'provider_table': provider_table,
|
||||
'export_templates': export_templates,
|
||||
})
|
||||
|
||||
|
||||
def provider(request, slug):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device')
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
'provider': provider,
|
||||
'circuits': circuits,
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.add_provider')
|
||||
def provider_add(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ProviderForm(request.POST)
|
||||
if form.is_valid():
|
||||
provider = form.save()
|
||||
messages.success(request, "Added new provider: {0}".format(provider))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('circuits:provider_add')
|
||||
else:
|
||||
return redirect('circuits:provider', slug=provider.slug)
|
||||
|
||||
else:
|
||||
form = ProviderForm()
|
||||
|
||||
return render(request, 'circuits/provider_edit.html', {
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:provider_list'),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.change_provider')
|
||||
def provider_edit(request, slug):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ProviderForm(request.POST, instance=provider)
|
||||
if form.is_valid():
|
||||
provider = form.save()
|
||||
messages.success(request, "Modified provider {0}".format(provider))
|
||||
return redirect('circuits:provider', slug=provider.slug)
|
||||
|
||||
else:
|
||||
form = ProviderForm(instance=provider)
|
||||
|
||||
return render(request, 'circuits/provider_edit.html', {
|
||||
'provider': provider,
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:provider', kwargs={'slug': provider.slug}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.delete_provider')
|
||||
def provider_delete(request, slug):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
provider.delete()
|
||||
messages.success(request, "Provider {0} has been deleted".format(provider))
|
||||
return redirect('circuits:provider_list')
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(provider, request, e)
|
||||
return redirect('circuits:provider', slug=provider.slug)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'circuits/provider_delete.html', {
|
||||
'provider': provider,
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:provider', kwargs={'slug': provider.slug})
|
||||
})
|
||||
|
||||
|
||||
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'circuits.add_provider'
|
||||
form = ProviderImportForm
|
||||
table = ProviderTable
|
||||
template_name = 'circuits/provider_import.html'
|
||||
obj_list_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'circuits.change_provider'
|
||||
cls = Provider
|
||||
form = ProviderBulkEditForm
|
||||
template_name = 'circuits/provider_bulk_edit.html'
|
||||
redirect_url = 'circuits:provider_list'
|
||||
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} providers".format(updated_count))
|
||||
|
||||
|
||||
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_provider'
|
||||
cls = Provider
|
||||
form = ProviderBulkDeleteForm
|
||||
template_name = 'circuits/provider_bulk_delete.html'
|
||||
redirect_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
||||
def circuit_list(request):
|
||||
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'site')
|
||||
queryset = CircuitFilter(request.GET, queryset).qs
|
||||
|
||||
# Export
|
||||
if 'export' in request.GET:
|
||||
et = get_object_or_404(ExportTemplate, content_type__model='circuit', name=request.GET.get('export'))
|
||||
response = et.to_response(context_dict={'queryset': queryset}, filename='netbox_circuits')
|
||||
return response
|
||||
|
||||
if request.user.has_perm('circuits.change_circuit') or request.user.has_perm('circuits.delete_circuit'):
|
||||
circuit_table = CircuitBulkEditTable(queryset)
|
||||
else:
|
||||
circuit_table = CircuitTable(queryset)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator}).configure(circuit_table)
|
||||
|
||||
export_templates = ExportTemplate.objects.filter(content_type__model='circuit')
|
||||
|
||||
return render(request, 'circuits/circuit_list.html', {
|
||||
'circuit_table': circuit_table,
|
||||
'export_templates': export_templates,
|
||||
'filter_form': CircuitFilterForm(request.GET, label_suffix=''),
|
||||
})
|
||||
|
||||
|
||||
def circuit(request, pk):
|
||||
|
||||
circuit = get_object_or_404(Circuit, pk=pk)
|
||||
|
||||
return render(request, 'circuits/circuit.html', {
|
||||
'circuit': circuit,
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.add_circuit')
|
||||
def circuit_add(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CircuitForm(request.POST)
|
||||
if form.is_valid():
|
||||
circuit = form.save()
|
||||
messages.success(request, "Added new circuit: {0}".format(circuit))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('circuits:circuit_add')
|
||||
else:
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
else:
|
||||
form = CircuitForm(initial={
|
||||
'site': request.GET.get('site'),
|
||||
})
|
||||
|
||||
return render(request, 'circuits/circuit_edit.html', {
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:circuit_list'),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.change_circuit')
|
||||
def circuit_edit(request, pk):
|
||||
|
||||
circuit = get_object_or_404(Circuit, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CircuitForm(request.POST, instance=circuit)
|
||||
if form.is_valid():
|
||||
circuit = form.save()
|
||||
messages.success(request, "Modified circuit {0}".format(circuit))
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
else:
|
||||
form = CircuitForm(instance=circuit)
|
||||
|
||||
return render(request, 'circuits/circuit_edit.html', {
|
||||
'circuit': circuit,
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:circuit', kwargs={'pk': circuit.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('circuits.delete_circuit')
|
||||
def circuit_delete(request, pk):
|
||||
|
||||
circuit = get_object_or_404(Circuit, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
circuit.delete()
|
||||
messages.success(request, "Circuit {0} has been deleted".format(circuit))
|
||||
return redirect('circuits:circuit_list')
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(circuit, request, e)
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'circuits/circuit_delete.html', {
|
||||
'circuit': circuit,
|
||||
'form': form,
|
||||
'cancel_url': reverse('circuits:circuit', kwargs={'pk': circuit.pk})
|
||||
})
|
||||
|
||||
|
||||
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'circuits.add_circuit'
|
||||
form = CircuitImportForm
|
||||
table = CircuitTable
|
||||
template_name = 'circuits/circuit_import.html'
|
||||
obj_list_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
cls = Circuit
|
||||
form = CircuitBulkEditForm
|
||||
template_name = 'circuits/circuit_bulk_edit.html'
|
||||
redirect_url = 'circuits:circuit_list'
|
||||
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} circuits".format(updated_count))
|
||||
|
||||
|
||||
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuit'
|
||||
cls = Circuit
|
||||
form = CircuitBulkDeleteForm
|
||||
template_name = 'circuits/circuit_bulk_delete.html'
|
||||
redirect_url = 'circuits:circuit_list'
|
|
@ -0,0 +1 @@
|
|||
default_app_config = 'dcim.apps.IPAMConfig'
|
|
@ -0,0 +1,161 @@
|
|||
from django.contrib import admin
|
||||
from django.db.models import Count
|
||||
|
||||
from .models import *
|
||||
|
||||
|
||||
@admin.register(Site)
|
||||
class SiteAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'facility', 'asn']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(RackGroup)
|
||||
class RackGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'site']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(Rack)
|
||||
class RackAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'facility_id', 'site', 'u_height']
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
|
||||
@admin.register(Manufacturer)
|
||||
class ManufacturerAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug']
|
||||
|
||||
|
||||
class ConsolePortTemplateAdmin(admin.TabularInline):
|
||||
model = ConsolePortTemplate
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateAdmin(admin.TabularInline):
|
||||
model = ConsoleServerPortTemplate
|
||||
|
||||
|
||||
class PowerPortTemplateAdmin(admin.TabularInline):
|
||||
model = PowerPortTemplate
|
||||
|
||||
|
||||
class PowerOutletTemplateAdmin(admin.TabularInline):
|
||||
model = PowerOutletTemplate
|
||||
|
||||
|
||||
class InterfaceTemplateAdmin(admin.TabularInline):
|
||||
model = InterfaceTemplate
|
||||
|
||||
|
||||
@admin.register(DeviceType)
|
||||
class DeviceTypeAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['model'],
|
||||
}
|
||||
inlines = [
|
||||
ConsolePortTemplateAdmin,
|
||||
ConsoleServerPortTemplateAdmin,
|
||||
PowerPortTemplateAdmin,
|
||||
PowerOutletTemplateAdmin,
|
||||
InterfaceTemplateAdmin,
|
||||
]
|
||||
list_display = ['model', 'manufacturer', 'slug', 'u_height', 'console_ports', 'console_server_ports', 'power_ports',
|
||||
'power_outlets', 'interfaces']
|
||||
list_filter = ['manufacturer']
|
||||
|
||||
def get_queryset(self, request):
|
||||
return DeviceType.objects.annotate(
|
||||
console_port_count=Count('console_port_templates', distinct=True),
|
||||
cs_port_count=Count('cs_port_templates', distinct=True),
|
||||
power_port_count=Count('power_port_templates', distinct=True),
|
||||
power_outlet_count=Count('power_outlet_templates', distinct=True),
|
||||
interface_count=Count('interface_templates', distinct=True),
|
||||
)
|
||||
|
||||
def console_ports(self, instance):
|
||||
return instance.console_port_count
|
||||
|
||||
def console_server_ports(self, instance):
|
||||
return instance.cs_port_count
|
||||
|
||||
def power_ports(self, instance):
|
||||
return instance.power_port_count
|
||||
|
||||
def power_outlets(self, instance):
|
||||
return instance.power_outlet_count
|
||||
|
||||
def interfaces(self, instance):
|
||||
return instance.interface_count
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
@admin.register(DeviceRole)
|
||||
class DeviceRoleAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'color']
|
||||
|
||||
|
||||
@admin.register(Platform)
|
||||
class PlatformAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'rpc_client']
|
||||
|
||||
|
||||
class ConsolePortAdmin(admin.TabularInline):
|
||||
model = ConsolePort
|
||||
readonly_fields = ['cs_port']
|
||||
|
||||
|
||||
class ConsoleServerPortAdmin(admin.TabularInline):
|
||||
model = ConsoleServerPort
|
||||
|
||||
|
||||
class PowerPortAdmin(admin.TabularInline):
|
||||
model = PowerPort
|
||||
readonly_fields = ['power_outlet']
|
||||
|
||||
|
||||
class PowerOutletAdmin(admin.TabularInline):
|
||||
model = PowerOutlet
|
||||
|
||||
|
||||
class InterfaceAdmin(admin.TabularInline):
|
||||
model = Interface
|
||||
|
||||
|
||||
class ModuleAdmin(admin.TabularInline):
|
||||
model = Module
|
||||
|
||||
@admin.register(Device)
|
||||
class DeviceAdmin(admin.ModelAdmin):
|
||||
inlines = [
|
||||
ConsolePortAdmin,
|
||||
ConsoleServerPortAdmin,
|
||||
PowerPortAdmin,
|
||||
PowerOutletAdmin,
|
||||
InterfaceAdmin,
|
||||
ModuleAdmin,
|
||||
]
|
||||
list_display = ['display_name', 'device_type', 'device_role', 'primary_ip', 'rack', 'position', 'serial']
|
||||
list_filter = ['device_role']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(DeviceAdmin, self).get_queryset(request)
|
||||
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip', 'rack')
|
|
@ -0,0 +1,6 @@
|
|||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
class MissingFilterException(APIException):
|
||||
status_code = 400
|
||||
default_detail = "One or more required filters is missing from the request."
|
|
@ -0,0 +1,300 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from ipam.models import IPAddress
|
||||
from dcim.models import Site, Rack, RackGroup, Manufacturer, DeviceType, DeviceRole, Platform, Device, ConsolePort,\
|
||||
ConsoleServerPort, PowerPort, PowerOutlet, Interface, InterfaceConnection, RACK_FACE_FRONT, RACK_FACE_REAR
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['id', 'name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
|
||||
'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
||||
|
||||
|
||||
class SiteNestedSerializer(SiteSerializer):
|
||||
|
||||
class Meta(SiteSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
class RackGroupSerializer(serializers.ModelSerializer):
|
||||
site = SiteNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['id', 'name', 'slug', 'site']
|
||||
|
||||
|
||||
class RackGroupNestedSerializer(SiteSerializer):
|
||||
|
||||
class Meta(SiteSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
|
||||
class RackSerializer(serializers.ModelSerializer):
|
||||
display_name = serializers.SerializerMethodField()
|
||||
site = SiteNestedSerializer()
|
||||
group = RackGroupNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments']
|
||||
|
||||
def get_display_name(self, obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
class RackNestedSerializer(RackSerializer):
|
||||
|
||||
class Meta(RackSerializer.Meta):
|
||||
fields = ['id', 'name', 'facility_id', 'display_name']
|
||||
|
||||
|
||||
class RackDetailSerializer(RackSerializer):
|
||||
front_units = serializers.SerializerMethodField()
|
||||
rear_units = serializers.SerializerMethodField()
|
||||
|
||||
class Meta(RackSerializer.Meta):
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments', 'front_units',
|
||||
'rear_units']
|
||||
|
||||
def get_front_units(self, obj):
|
||||
units = obj.get_rack_units(face=RACK_FACE_FRONT)
|
||||
for u in units:
|
||||
u['device'] = DeviceNestedSerializer(u['device']).data if u['device'] else None
|
||||
return units
|
||||
|
||||
def get_rear_units(self, obj):
|
||||
units = obj.get_rack_units(face=RACK_FACE_REAR)
|
||||
for u in units:
|
||||
u['device'] = DeviceNestedSerializer(u['device']).data if u['device'] else None
|
||||
return units
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class ManufacturerNestedSerializer(ManufacturerSerializer):
|
||||
|
||||
class Meta(ManufacturerSerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
|
||||
class DeviceTypeSerializer(serializers.ModelSerializer):
|
||||
manufacturer = ManufacturerNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['id', 'manufacturer', 'model', 'slug', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device']
|
||||
|
||||
|
||||
class DeviceTypeNestedSerializer(DeviceTypeSerializer):
|
||||
|
||||
class Meta(DeviceTypeSerializer.Meta):
|
||||
fields = ['id', 'manufacturer', 'model', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
|
||||
class DeviceRoleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
|
||||
class DeviceRoleNestedSerializer(DeviceRoleSerializer):
|
||||
|
||||
class Meta(DeviceRoleSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Platforms
|
||||
#
|
||||
|
||||
class PlatformSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['id', 'name', 'slug', 'rpc_client']
|
||||
|
||||
|
||||
class PlatformNestedSerializer(PlatformSerializer):
|
||||
|
||||
class Meta(PlatformSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
# Cannot import ipam.api.IPAddressNestedSerializer due to circular dependency
|
||||
class DeviceIPAddressNestedSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'family', 'address']
|
||||
|
||||
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
device_type = DeviceTypeNestedSerializer()
|
||||
device_role = DeviceRoleNestedSerializer()
|
||||
platform = PlatformNestedSerializer()
|
||||
rack = RackNestedSerializer()
|
||||
primary_ip = DeviceIPAddressNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
|
||||
'face', 'status', 'primary_ip', 'ro_snmp', 'comments']
|
||||
|
||||
|
||||
class DeviceNestedSerializer(DeviceSerializer):
|
||||
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
model = Device
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'device', 'name', 'connected_console']
|
||||
|
||||
|
||||
class ConsoleServerPortNestedSerializer(ConsoleServerPortSerializer):
|
||||
|
||||
class Meta(ConsoleServerPortSerializer.Meta):
|
||||
fields = ['id', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
class ConsolePortSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
cs_port = ConsoleServerPortNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
|
||||
|
||||
|
||||
class ConsolePortNestedSerializer(ConsolePortSerializer):
|
||||
|
||||
class Meta(ConsolePortSerializer.Meta):
|
||||
fields = ['id', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['id', 'device', 'name', 'connected_port']
|
||||
|
||||
|
||||
class PowerOutletNestedSerializer(PowerOutletSerializer):
|
||||
|
||||
class Meta(PowerOutletSerializer.Meta):
|
||||
fields = ['id', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
#
|
||||
|
||||
class PowerPortSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
power_outlet = PowerOutletNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
|
||||
|
||||
|
||||
class PowerPortNestedSerializer(PowerPortSerializer):
|
||||
|
||||
class Meta(PowerPortSerializer.Meta):
|
||||
fields = ['id', 'device', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['id', 'device', 'name', 'form_factor', 'mgmt_only', 'description', 'is_connected']
|
||||
|
||||
|
||||
class InterfaceNestedSerializer(InterfaceSerializer):
|
||||
form_factor = serializers.ReadOnlyField(source='get_form_factor_display')
|
||||
|
||||
class Meta(InterfaceSerializer.Meta):
|
||||
fields = ['id', 'device', 'name']
|
||||
|
||||
|
||||
class InterfaceDetailSerializer(InterfaceSerializer):
|
||||
connected_interface = InterfaceSerializer(source='get_connected_interface')
|
||||
|
||||
class Meta(InterfaceSerializer.Meta):
|
||||
fields = ['id', 'device', 'name', 'form_factor', 'mgmt_only', 'description', 'is_connected',
|
||||
'connected_interface']
|
||||
|
||||
|
||||
#
|
||||
# Interface connections
|
||||
#
|
||||
|
||||
class InterfaceConnectionSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
|
|
@ -0,0 +1,581 @@
|
|||
import json
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
|
||||
class SiteTest(APITestCase):
|
||||
|
||||
fixtures = [
|
||||
'dcim',
|
||||
'ipam',
|
||||
'extras',
|
||||
]
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'slug',
|
||||
'facility',
|
||||
'asn',
|
||||
'physical_address',
|
||||
'shipping_address',
|
||||
'comments',
|
||||
'count_prefixes',
|
||||
'count_vlans',
|
||||
'count_racks',
|
||||
'count_devices',
|
||||
'count_circuits'
|
||||
]
|
||||
|
||||
nested_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'slug'
|
||||
]
|
||||
|
||||
rack_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'facility_id',
|
||||
'display_name',
|
||||
'site',
|
||||
'group',
|
||||
'u_height',
|
||||
'comments'
|
||||
]
|
||||
|
||||
graph_fields = [
|
||||
'name',
|
||||
'embed_url',
|
||||
'link',
|
||||
]
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/sites/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/sites/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_get_site_list_rack(self, endpoint='/api/dcim/sites/1/racks/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in json.loads(response.content):
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.rack_fields),
|
||||
)
|
||||
# Check Nested Serializer.
|
||||
self.assertEqual(
|
||||
sorted(i.get('site').keys()),
|
||||
sorted(self.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_site_list_graphs(self, endpoint='/api/dcim/sites/1/graphs/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in json.loads(response.content):
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.graph_fields),
|
||||
)
|
||||
|
||||
|
||||
class RackTest(APITestCase):
|
||||
fixtures = [
|
||||
'dcim',
|
||||
'ipam'
|
||||
]
|
||||
|
||||
nested_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'facility_id',
|
||||
'display_name'
|
||||
]
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'facility_id',
|
||||
'display_name',
|
||||
'site',
|
||||
'group',
|
||||
'u_height',
|
||||
'comments'
|
||||
]
|
||||
|
||||
detail_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'facility_id',
|
||||
'display_name',
|
||||
'site',
|
||||
'group',
|
||||
'u_height',
|
||||
'comments',
|
||||
'front_units',
|
||||
'rear_units'
|
||||
]
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/racks/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(i.get('site').keys()),
|
||||
sorted(SiteTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/racks/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.detail_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(content.get('site').keys()),
|
||||
sorted(SiteTest.nested_fields),
|
||||
)
|
||||
|
||||
|
||||
class ManufacturersTest(APITestCase):
|
||||
|
||||
fixtures = [
|
||||
'dcim',
|
||||
'ipam'
|
||||
]
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'slug',
|
||||
]
|
||||
|
||||
nested_fields = standard_fields
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/manufacturers/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/manufacturers/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'manufacturer',
|
||||
'model',
|
||||
'slug',
|
||||
'u_height',
|
||||
'is_console_server',
|
||||
'is_pdu',
|
||||
'is_network_device'
|
||||
]
|
||||
|
||||
nested_fields = [
|
||||
'id',
|
||||
'manufacturer',
|
||||
'model',
|
||||
'slug'
|
||||
]
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/device-types/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_detail_list(self, endpoint='/api/dcim/device-types/1/'):
|
||||
# TODO: details returns list view.
|
||||
# response = self.client.get(endpoint)
|
||||
# content = json.loads(response.content)
|
||||
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# self.assertEqual(
|
||||
# sorted(content.keys()),
|
||||
# sorted(self.standard_fields),
|
||||
# )
|
||||
# self.assertEqual(
|
||||
# sorted(content.get('manufacturer').keys()),
|
||||
# sorted(ManufacturersTest.nested_fields),
|
||||
# )
|
||||
pass
|
||||
|
||||
|
||||
class DeviceRolesTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
nested_fields = ['id', 'name', 'slug']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/device-roles/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/device-roles/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
|
||||
class PlatformsTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'name', 'slug', 'rpc_client']
|
||||
|
||||
nested_fields = ['id', 'name', 'slug']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/platforms/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/platforms/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
|
||||
class DeviceTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'display_name',
|
||||
'device_type',
|
||||
'device_role',
|
||||
'platform',
|
||||
'serial',
|
||||
'rack',
|
||||
'position',
|
||||
'face',
|
||||
'status',
|
||||
'primary_ip',
|
||||
'ro_snmp',
|
||||
'comments',
|
||||
]
|
||||
|
||||
nested_fields = ['id', 'name']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for device in content:
|
||||
self.assertEqual(
|
||||
sorted(device.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(device.get('device_type')),
|
||||
sorted(DeviceTypeTest.nested_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(device.get('device_role')),
|
||||
sorted(DeviceRolesTest.nested_fields),
|
||||
)
|
||||
if device.get('platform'):
|
||||
self.assertEqual(
|
||||
sorted(device.get('platform')),
|
||||
sorted(PlatformsTest.nested_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(device.get('rack')),
|
||||
sorted(RackTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/devices/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
|
||||
|
||||
class ConsoleServerPortsTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'device', 'name', 'connected_console']
|
||||
|
||||
nested_fields = ['id', 'device', 'name']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/9/console-server-ports/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for console_port in content:
|
||||
self.assertEqual(
|
||||
sorted(console_port.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(console_port.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortsTest(APITestCase):
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
|
||||
|
||||
nested_fields = ['id', 'device', 'name']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/1/console-ports/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for console_port in content:
|
||||
self.assertEqual(
|
||||
sorted(console_port.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(console_port.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(console_port.get('cs_port')),
|
||||
sorted(ConsoleServerPortsTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/console-ports/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(content.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
|
||||
class PowerPortsTest(APITestCase):
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
|
||||
|
||||
nested_fields = ['id', 'device', 'name']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/1/power-ports/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(i.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/power-ports/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(content.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletsTest(APITestCase):
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = ['id', 'device', 'name', 'connected_port']
|
||||
|
||||
nested_fields = ['id', 'device', 'name']
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/11/power-outlets/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(i.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTest(APITestCase):
|
||||
fixtures = ['dcim', 'ipam', 'extras']
|
||||
|
||||
standard_fields = [
|
||||
'id',
|
||||
'device',
|
||||
'name',
|
||||
'form_factor',
|
||||
'mgmt_only',
|
||||
'description',
|
||||
'is_connected'
|
||||
]
|
||||
|
||||
nested_fields = ['id', 'device', 'name']
|
||||
|
||||
detail_fields = [
|
||||
'id',
|
||||
'device',
|
||||
'name',
|
||||
'form_factor',
|
||||
'mgmt_only',
|
||||
'description',
|
||||
'is_connected',
|
||||
'connected_interface'
|
||||
]
|
||||
|
||||
connection_fields = [
|
||||
'id',
|
||||
'interface_a',
|
||||
'interface_b',
|
||||
'connection_status',
|
||||
]
|
||||
|
||||
def test_get_list(self, endpoint='/api/dcim/devices/1/interfaces/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(i.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_detail(self, endpoint='/api/dcim/interfaces/1/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.detail_fields),
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(content.get('device')),
|
||||
sorted(DeviceTest.nested_fields),
|
||||
)
|
||||
|
||||
def test_get_graph_list(self, endpoint='/api/dcim/interfaces/1/graphs/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
for i in content:
|
||||
self.assertEqual(
|
||||
sorted(i.keys()),
|
||||
sorted(SiteTest.graph_fields),
|
||||
)
|
||||
|
||||
def test_get_interface_connections(self, endpoint='/api/dcim/interface-connections/4/'):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.connection_fields),
|
||||
)
|
||||
|
||||
|
||||
class RelatedConnectionsTest(APITestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
standard_fields = [
|
||||
'device',
|
||||
'console-ports',
|
||||
'power-ports',
|
||||
'interfaces',
|
||||
]
|
||||
|
||||
def test_get_list(self, endpoint=(
|
||||
'/api/dcim/related-connections/'
|
||||
'?peer-device=test1-edge1&peer-interface=xe-0/0/3')):
|
||||
response = self.client.get(endpoint)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
sorted(content.keys()),
|
||||
sorted(self.standard_fields),
|
||||
)
|
|
@ -0,0 +1,66 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from extras.api.views import GraphListView
|
||||
|
||||
from .views import *
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# Sites
|
||||
url(r'^sites/$', SiteListView.as_view(), name='site_list'),
|
||||
url(r'^sites/(?P<pk>\d+)/$', SiteDetailView.as_view(), name='site_detail'),
|
||||
url(r'^sites/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_SITE}, name='site_graphs'),
|
||||
url(r'^sites/(?P<site>\d+)/racks/$', RackListView.as_view(), name='site_racks'),
|
||||
|
||||
# Rack groups
|
||||
url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'),
|
||||
url(r'^rack-groups/(?P<pk>\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'),
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', RackListView.as_view(), name='rack_list'),
|
||||
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'),
|
||||
|
||||
# Manufacturers
|
||||
url(r'^manufacturers/$', ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
url(r'^manufacturers/(?P<pk>\d+)/$', ManufacturerDetailView.as_view(), name='manufacturer_detail'),
|
||||
|
||||
# Device types
|
||||
url(r'^device-types/$', DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
url(r'^device-types/(?P<pk>\d+)/$', DeviceTypeDetailView.as_view(), name='devicetype_detail'),
|
||||
|
||||
# Device roles
|
||||
url(r'^device-roles/$', DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||
url(r'^device-roles/(?P<pk>\d+)/$', DeviceRoleDetailView.as_view(), name='devicerole_detail'),
|
||||
|
||||
# Platforms
|
||||
url(r'^platforms/$', PlatformListView.as_view(), name='platform_list'),
|
||||
url(r'^platforms/(?P<pk>\d+)/$', PlatformDetailView.as_view(), name='platform_detail'),
|
||||
|
||||
# Devices
|
||||
url(r'^devices/$', DeviceListView.as_view(), name='device_list'),
|
||||
url(r'^devices/(?P<pk>\d+)/$', DeviceDetailView.as_view(), name='device_detail'),
|
||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', LLDPNeighborsView.as_view(), name='device_lldp-neighbors'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/$', ConsolePortListView.as_view(), name='device_consoleports'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/$', ConsoleServerPortListView.as_view(), name='device_consoleserverports'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/$', PowerPortListView.as_view(), name='device_powerports'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/$', PowerOutletListView.as_view(), name='device_poweroutlets'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/$', InterfaceListView.as_view(), name='device_interfaces'),
|
||||
|
||||
# Console ports
|
||||
url(r'^console-ports/(?P<pk>\d+)/$', ConsolePortView.as_view(), name='consoleport'),
|
||||
|
||||
# Power ports
|
||||
url(r'^power-ports/(?P<pk>\d+)/$', PowerPortView.as_view(), name='powerport'),
|
||||
|
||||
# Interfaces
|
||||
url(r'^interfaces/(?P<pk>\d+)/$', InterfaceDetailView.as_view(), name='interface_detail'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE}, name='interface_graphs'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection'),
|
||||
|
||||
# Miscellaneous
|
||||
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),
|
||||
|
||||
]
|
|
@ -0,0 +1,438 @@
|
|||
from rest_framework import generics
|
||||
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from dcim.models import Site, Rack, RackGroup, Manufacturer, DeviceType, DeviceRole, Platform, Device, ConsolePort, \
|
||||
ConsoleServerPort, PowerPort, PowerOutlet, Interface, InterfaceConnection, IFACE_FF_VIRTUAL
|
||||
from dcim.filters import RackGroupFilter, RackFilter, DeviceTypeFilter, DeviceFilter, InterfaceFilter
|
||||
from .exceptions import MissingFilterException
|
||||
from .serializers import SiteSerializer, RackGroupSerializer, RackSerializer, RackDetailSerializer, \
|
||||
ManufacturerSerializer, DeviceTypeSerializer, DeviceRoleSerializer, PlatformSerializer, DeviceSerializer, \
|
||||
DeviceNestedSerializer, ConsolePortSerializer, ConsoleServerPortSerializer, PowerPortSerializer, \
|
||||
PowerOutletSerializer, InterfaceSerializer, InterfaceDetailSerializer, InterfaceConnectionSerializer
|
||||
from extras.api.renderers import BINDZoneRenderer
|
||||
from utilities.api import ServiceUnavailable
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteListView(generics.ListAPIView):
|
||||
"""
|
||||
List all sites
|
||||
"""
|
||||
queryset = Site.objects.all()
|
||||
serializer_class = SiteSerializer
|
||||
|
||||
|
||||
class SiteDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single site
|
||||
"""
|
||||
queryset = Site.objects.all()
|
||||
serializer_class = SiteSerializer
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
class RackGroupListView(generics.ListAPIView):
|
||||
"""
|
||||
List all rack groups
|
||||
"""
|
||||
queryset = RackGroup.objects.all()
|
||||
serializer_class = RackGroupSerializer
|
||||
filter_class = RackGroupFilter
|
||||
|
||||
|
||||
class RackGroupDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single rack group
|
||||
"""
|
||||
queryset = RackGroup.objects.all()
|
||||
serializer_class = RackGroupSerializer
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackListView(generics.ListAPIView):
|
||||
"""
|
||||
List racks (filterable)
|
||||
"""
|
||||
queryset = Rack.objects.select_related('site')
|
||||
serializer_class = RackSerializer
|
||||
filter_class = RackFilter
|
||||
|
||||
|
||||
class RackDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single rack
|
||||
"""
|
||||
queryset = Rack.objects.select_related('site')
|
||||
serializer_class = RackDetailSerializer
|
||||
|
||||
|
||||
#
|
||||
# Rack units
|
||||
#
|
||||
|
||||
class RackUnitListView(APIView):
|
||||
"""
|
||||
List rack units (by rack)
|
||||
"""
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
rack = get_object_or_404(Rack, pk=pk)
|
||||
face = request.GET.get('face', 0)
|
||||
elevation = rack.get_rack_units(face)
|
||||
|
||||
# Serialize Devices within the rack elevation
|
||||
for u in elevation:
|
||||
if u['device']:
|
||||
u['device'] = DeviceNestedSerializer(instance=u['device']).data
|
||||
|
||||
return Response(elevation)
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerListView(generics.ListAPIView):
|
||||
"""
|
||||
List all hardware manufacturers
|
||||
"""
|
||||
queryset = Manufacturer.objects.all()
|
||||
serializer_class = ManufacturerSerializer
|
||||
|
||||
|
||||
class ManufacturerDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single hardware manufacturers
|
||||
"""
|
||||
queryset = Manufacturer.objects.all()
|
||||
serializer_class = ManufacturerSerializer
|
||||
|
||||
|
||||
#
|
||||
# Device Types
|
||||
#
|
||||
|
||||
class DeviceTypeListView(generics.ListAPIView):
|
||||
"""
|
||||
List device types (filterable)
|
||||
"""
|
||||
queryset = DeviceType.objects.select_related('manufacturer')
|
||||
serializer_class = DeviceTypeSerializer
|
||||
filter_class = DeviceTypeFilter
|
||||
|
||||
|
||||
class DeviceTypeDetailView(generics.ListAPIView):
|
||||
"""
|
||||
Retrieve a single device type
|
||||
"""
|
||||
queryset = DeviceType.objects.select_related('manufacturer')
|
||||
serializer_class = DeviceTypeSerializer
|
||||
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
|
||||
class DeviceRoleListView(generics.ListAPIView):
|
||||
"""
|
||||
List all device roles
|
||||
"""
|
||||
queryset = DeviceRole.objects.all()
|
||||
serializer_class = DeviceRoleSerializer
|
||||
|
||||
|
||||
class DeviceRoleDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single device role
|
||||
"""
|
||||
queryset = DeviceRole.objects.all()
|
||||
serializer_class = DeviceRoleSerializer
|
||||
|
||||
|
||||
#
|
||||
# Platforms
|
||||
#
|
||||
|
||||
class PlatformListView(generics.ListAPIView):
|
||||
"""
|
||||
List all platforms
|
||||
"""
|
||||
queryset = Platform.objects.all()
|
||||
serializer_class = PlatformSerializer
|
||||
|
||||
|
||||
class PlatformDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single platform
|
||||
"""
|
||||
queryset = Platform.objects.all()
|
||||
serializer_class = PlatformSerializer
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceListView(generics.ListAPIView):
|
||||
"""
|
||||
List devices (filterable)
|
||||
"""
|
||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'platform', 'rack__site')\
|
||||
.prefetch_related('primary_ip__nat_outside')
|
||||
serializer_class = DeviceSerializer
|
||||
filter_class = DeviceFilter
|
||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer]
|
||||
|
||||
|
||||
class DeviceDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single device
|
||||
"""
|
||||
queryset = Device.objects.all()
|
||||
serializer_class = DeviceSerializer
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
class ConsolePortListView(generics.ListAPIView):
|
||||
"""
|
||||
List console ports (by device)
|
||||
"""
|
||||
serializer_class = ConsolePortSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return ConsolePort.objects.filter(device=device).select_related('cs_port')
|
||||
|
||||
|
||||
class ConsolePortView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
|
||||
serializer_class = ConsolePortSerializer
|
||||
queryset = ConsolePort.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortListView(generics.ListAPIView):
|
||||
"""
|
||||
List console server ports (by device)
|
||||
"""
|
||||
serializer_class = ConsoleServerPortSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return ConsoleServerPort.objects.filter(device=device).select_related('connected_console')
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
#
|
||||
|
||||
class PowerPortListView(generics.ListAPIView):
|
||||
"""
|
||||
List power ports (by device)
|
||||
"""
|
||||
serializer_class = PowerPortSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return PowerPort.objects.filter(device=device).select_related('power_outlet')
|
||||
|
||||
|
||||
class PowerPortView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
|
||||
serializer_class = PowerPortSerializer
|
||||
queryset = PowerPort.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletListView(generics.ListAPIView):
|
||||
"""
|
||||
List power outlets (by device)
|
||||
"""
|
||||
serializer_class = PowerOutletSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return PowerOutlet.objects.filter(device=device).select_related('connected_port')
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceListView(generics.ListAPIView):
|
||||
"""
|
||||
List interfaces (by device)
|
||||
"""
|
||||
serializer_class = InterfaceSerializer
|
||||
filter_class = InterfaceFilter
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
queryset = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b')
|
||||
|
||||
# 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)
|
||||
elif iface_type == 'virtual':
|
||||
queryset = queryset.filter(form_factor=IFACE_FF_VIRTUAL)
|
||||
elif iface_type is not None:
|
||||
queryset = queryset.empty()
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class InterfaceDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single interface
|
||||
"""
|
||||
queryset = Interface.objects.select_related('device')
|
||||
serializer_class = InterfaceDetailSerializer
|
||||
|
||||
|
||||
class InterfaceConnectionView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
|
||||
serializer_class = InterfaceConnectionSerializer
|
||||
queryset = InterfaceConnection.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Live queries
|
||||
#
|
||||
|
||||
class LLDPNeighborsView(APIView):
|
||||
"""
|
||||
Retrieve live LLDP neighbors of a device
|
||||
"""
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
if not device.primary_ip:
|
||||
raise ServiceUnavailable(detail="No IP configured for this device.")
|
||||
hostname = str(device.primary_ip.address.ip)
|
||||
|
||||
RPC = device.get_rpc_client()
|
||||
if not RPC:
|
||||
raise ServiceUnavailable(detail="No RPC client available for this platform ({}).".format(device.platform))
|
||||
|
||||
# Connect to device and retrieve inventory info
|
||||
try:
|
||||
with RPC(device, username=settings.NETBOX_USERNAME, password=settings.NETBOX_PASSWORD) as rpc_client:
|
||||
lldp_neighbors = rpc_client.get_lldp_neighbors()
|
||||
except:
|
||||
raise ServiceUnavailable(detail="Error connecting to the remote device.")
|
||||
|
||||
return Response(lldp_neighbors)
|
||||
|
||||
|
||||
#
|
||||
# Miscellaneous
|
||||
#
|
||||
|
||||
class RelatedConnectionsView(APIView):
|
||||
"""
|
||||
Retrieve all connections related to a given console/power/interface connection
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
|
||||
peer_device = request.GET.get('peer-device')
|
||||
peer_interface = request.GET.get('peer-interface')
|
||||
|
||||
# Search by interface
|
||||
if peer_device and peer_interface:
|
||||
|
||||
# Determine local interface from peer interface's connection
|
||||
try:
|
||||
peer_iface = Interface.objects.get(device__name=peer_device, name=peer_interface)
|
||||
except Interface.DoesNotExist:
|
||||
raise Http404()
|
||||
local_iface = peer_iface.get_connected_interface()
|
||||
if local_iface:
|
||||
device = local_iface.device
|
||||
else:
|
||||
return Response()
|
||||
|
||||
else:
|
||||
raise MissingFilterException(detail='Must specify search parameters (peer-device and peer-interface).')
|
||||
|
||||
# Initialize response skeleton
|
||||
response = dict()
|
||||
response['device'] = DeviceSerializer(device).data
|
||||
response['console-ports'] = []
|
||||
response['power-ports'] = []
|
||||
response['interfaces'] = []
|
||||
|
||||
# Build console connections
|
||||
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
|
||||
for cp in console_ports:
|
||||
cp_info = dict()
|
||||
cp_info['name'] = cp.name
|
||||
if cp.cs_port:
|
||||
cp_info['console-server'] = cp.cs_port.device.name
|
||||
cp_info['port'] = cp.cs_port.name
|
||||
else:
|
||||
cp_info['console-server'] = None
|
||||
cp_info['port'] = None
|
||||
response['console-ports'].append(cp_info)
|
||||
|
||||
# Build power connections
|
||||
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
|
||||
for pp in power_ports:
|
||||
pp_info = dict()
|
||||
pp_info['name'] = pp.name
|
||||
if pp.power_outlet:
|
||||
pp_info['pdu'] = pp.power_outlet.device.name
|
||||
pp_info['outlet'] = pp.power_outlet.name
|
||||
else:
|
||||
pp_info['pdu'] = None
|
||||
pp_info['outlet'] = None
|
||||
response['power-ports'].append(pp_info)
|
||||
|
||||
# Built interface connections
|
||||
interfaces = Interface.objects.filter(device=device)
|
||||
for iface in interfaces:
|
||||
iface_info = dict()
|
||||
iface_info['name'] = iface.name
|
||||
peer_interface = iface.get_connected_interface()
|
||||
if peer_interface:
|
||||
iface_info['device'] = peer_interface.device.name
|
||||
iface_info['interface'] = peer_interface.name
|
||||
else:
|
||||
iface_info['device'] = None
|
||||
iface_info['interface'] = None
|
||||
response['interfaces'].append(iface_info)
|
||||
|
||||
return Response(response)
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class IPAMConfig(AppConfig):
|
||||
name = "dcim"
|
||||
verbose_name = "DCIM"
|
|
@ -0,0 +1,317 @@
|
|||
import django_filters
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from .models import Site, RackGroup, Rack, Manufacturer, DeviceType, DeviceRole, Device, ConsolePort, \
|
||||
ConsoleServerPort, Platform, PowerPort, PowerOutlet, Interface, InterfaceConnection
|
||||
|
||||
|
||||
class RackGroupFilter(django_filters.FilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
fields = ['site_id', 'site']
|
||||
|
||||
|
||||
class RackFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
name='group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['q', 'site_id', 'site', 'u_height']
|
||||
|
||||
def search(self, queryset, value):
|
||||
value = value.strip()
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(facility_id__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeFilter(django_filters.FilterSet):
|
||||
manufacturer_id = django_filters.ModelChoiceFilter(
|
||||
name='manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelChoiceFilter(
|
||||
name='manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['manufacturer_id', 'manufacturer', 'model', 'u_height', 'is_console_server', 'is_pdu',
|
||||
'is_network_device']
|
||||
|
||||
|
||||
class DeviceFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site name (slug)',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack',
|
||||
queryset=Rack.objects.all(),
|
||||
label='Rack (ID)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_role',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_role',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
device_type = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type',
|
||||
queryset=DeviceType.objects.all(),
|
||||
label='Device type (ID)',
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label='Manufacturer (ID)',
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Manufacturer (slug)',
|
||||
)
|
||||
model = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type',
|
||||
queryset=DeviceType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Device model (slug)',
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platform',
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform (ID)',
|
||||
)
|
||||
platform = django_filters.ModelMultipleChoiceFilter(
|
||||
name='platform',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
is_console_server = django_filters.BooleanFilter(
|
||||
name='device_type__is_console_server',
|
||||
label='Is a console server',
|
||||
)
|
||||
is_pdu = django_filters.BooleanFilter(
|
||||
name='device_type__is_pdu',
|
||||
label='Is a PDU',
|
||||
)
|
||||
is_network_device = django_filters.BooleanFilter(
|
||||
name='device_type__is_network_device',
|
||||
label='Is a network device',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['q', 'name', 'site_id', 'site', 'rack_id', 'role_id', 'role', 'device_type', 'manufacturer_id',
|
||||
'manufacturer', 'model', 'platform_id', 'platform', 'is_console_server', 'is_pdu',
|
||||
'is_network_device']
|
||||
|
||||
def search(self, queryset, value):
|
||||
value = value.strip()
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(serial__icontains=value) |
|
||||
Q(modules__serial__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
class ConsolePortFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['device_id', 'device', 'name']
|
||||
|
||||
|
||||
class ConsoleServerPortFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['device_id', 'device', 'name']
|
||||
|
||||
|
||||
class PowerPortFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['device_id', 'device', 'name']
|
||||
|
||||
|
||||
class PowerOutletFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['device_id', 'device', 'name']
|
||||
|
||||
|
||||
class InterfaceFilter(django_filters.FilterSet):
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['device_id', 'device', 'name']
|
||||
|
||||
|
||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(cs_port__device__rack__site__slug=value)
|
||||
|
||||
|
||||
class PowerConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(power_outlet__device__rack__site__slug=value)
|
||||
|
||||
|
||||
class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.MethodFilter(
|
||||
action='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
|
||||
def filter_site(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(interface_a__device__rack__site__slug=value) |
|
||||
Q(interface_b__device__rack__site__slug=value)
|
||||
)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,953 @@
|
|||
import re
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Count, Q
|
||||
|
||||
from ipam.models import IPAddress
|
||||
from utilities.forms import BootstrapMixin, SmallTextarea, SelectWithDisabled, ConfirmationForm, APISelect, \
|
||||
Livesearch, CSVDataField, CommentField, BulkImportForm, FlexibleModelChoiceField, ExpandableNameField
|
||||
|
||||
from .models import Site, Rack, RackGroup, Device, Manufacturer, DeviceType, DeviceRole, Platform, ConsolePort, \
|
||||
ConsoleServerPort, PowerPort, PowerOutlet, Interface, InterfaceConnection, CONNECTION_STATUS_CHOICES, \
|
||||
CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, IFACE_FF_VIRTUAL, STATUS_CHOICES
|
||||
|
||||
|
||||
BULK_STATUS_CHOICES = [
|
||||
['', '---------'],
|
||||
]
|
||||
BULK_STATUS_CHOICES += STATUS_CHOICES
|
||||
|
||||
DEVICE_BY_PK_RE = '{\d+\}'
|
||||
|
||||
|
||||
def get_device_by_name_or_pk(name):
|
||||
"""
|
||||
Attempt to retrieve a device by either its name or primary key ('{pk}').
|
||||
"""
|
||||
if re.match(DEVICE_BY_PK_RE, name):
|
||||
pk = name.strip('{}')
|
||||
device = Device.objects.get(pk=pk)
|
||||
else:
|
||||
device = Device.objects.get(name=name)
|
||||
return device
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteForm(forms.ModelForm, BootstrapMixin):
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
|
||||
widgets = {
|
||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||
'shipping_address': SmallTextarea(attrs={'rows': 3}),
|
||||
}
|
||||
help_texts = {
|
||||
'name': "Full name of the site",
|
||||
'slug': "URL-friendly unique shorthand (e.g. 'nyc3' for NYC3)",
|
||||
'facility': "Data center provider and facility (e.g. Equinix NY7)",
|
||||
'asn': "BGP autonomous system number",
|
||||
'physical_address': "Physical location of the building (e.g. for GPS)",
|
||||
'shipping_address': "If different from the physical address"
|
||||
}
|
||||
|
||||
|
||||
class SiteFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['name', 'slug', 'facility', 'asn']
|
||||
|
||||
|
||||
class SiteImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=SiteFromCSVForm)
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackForm(forms.ModelForm, BootstrapMixin):
|
||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group', widget=APISelect(
|
||||
api_url='/api/dcim/rack-groups/?site_id={{site}}',
|
||||
))
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'u_height', 'comments']
|
||||
help_texts = {
|
||||
'site': "The site at which the rack exists",
|
||||
'name': "Organizational rack name",
|
||||
'facility_id': "The unique rack ID assigned by the facility",
|
||||
'u_height': "Height in rack units",
|
||||
}
|
||||
widgets = {
|
||||
'site': forms.Select(attrs={'filter-for': 'group'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(RackForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Limit rack group choices
|
||||
if self.is_bound and self.data.get('site'):
|
||||
self.fields['group'].queryset = RackGroup.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['group'].queryset = RackGroup.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['group'].choices = []
|
||||
|
||||
|
||||
class RackFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Group not found.'})
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'u_height']
|
||||
|
||||
def clean(self):
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
group = self.cleaned_data.get('group')
|
||||
|
||||
# Validate device type
|
||||
if site and group:
|
||||
try:
|
||||
self.instance.group = RackGroup.objects.get(site=site, name=group)
|
||||
except RackGroup.DoesNotExist:
|
||||
self.add_error('group', "Invalid rack group ({})".format(group))
|
||||
|
||||
|
||||
class RackImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=RackFromCSVForm)
|
||||
|
||||
|
||||
class RackBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False)
|
||||
u_height = forms.IntegerField(required=False, label='Height (U)')
|
||||
comments = CommentField()
|
||||
|
||||
|
||||
class RackBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def rack_site_choices():
|
||||
site_choices = Site.objects.annotate(rack_count=Count('racks'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
||||
|
||||
|
||||
def rack_group_choices():
|
||||
group_choices = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
|
||||
return [(g.slug, '{} > {} ({})'.format(g.site.name, g.name, g.rack_count)) for g in group_choices]
|
||||
|
||||
|
||||
class RackFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
group = forms.MultipleChoiceField(required=False, choices=rack_group_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceForm(forms.ModelForm, BootstrapMixin):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
|
||||
rack = forms.ModelChoiceField(queryset=Rack.objects.all(), widget=APISelect(
|
||||
api_url='/api/dcim/racks/?site_id={{site}}',
|
||||
display_field='display_name',
|
||||
attrs={'filter-for': 'position'}
|
||||
))
|
||||
position = forms.TypedChoiceField(required=False, empty_value=None, widget=APISelect(
|
||||
api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}',
|
||||
disabled_indicator='device',
|
||||
))
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
|
||||
widget=forms.Select(attrs={'filter-for': 'device_type'}))
|
||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Model', widget=APISelect(
|
||||
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
|
||||
display_field='model'
|
||||
))
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
|
||||
'platform', 'primary_ip', 'ro_snmp', 'comments']
|
||||
help_texts = {
|
||||
'device_role': "The function this device serves",
|
||||
'serial': "Chassis serial number",
|
||||
'ro_snmp': "Read-only SNMP string",
|
||||
}
|
||||
widgets = {
|
||||
'face': forms.Select(attrs={'filter-for': 'position'}),
|
||||
'manufacturer': forms.Select(attrs={'filter-for': 'device_type'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(DeviceForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if self.instance.pk:
|
||||
|
||||
# Initialize helper selections
|
||||
self.initial['site'] = self.instance.rack.site
|
||||
self.initial['manufacturer'] = self.instance.device_type.manufacturer
|
||||
|
||||
# Compile list of IPs assigned to this device
|
||||
primary_ip_choices = []
|
||||
interface_ips = IPAddress.objects.filter(interface__device=self.instance)
|
||||
primary_ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||
nat_ips = IPAddress.objects.filter(nat_inside__interface__device=self.instance)\
|
||||
.select_related('nat_inside__interface')
|
||||
primary_ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
||||
self.fields['primary_ip'].choices = [(None, '---------')] + primary_ip_choices
|
||||
|
||||
else:
|
||||
|
||||
# An object that doesn't exist yet can't have any IPs assigned to it
|
||||
self.fields['primary_ip'].choices = []
|
||||
self.fields['primary_ip'].widget.attrs['readonly'] = True
|
||||
|
||||
# Limit rack choices
|
||||
if self.is_bound:
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Rack position
|
||||
try:
|
||||
if self.is_bound and self.data.get('rack') and self.data.get('face') is not None:
|
||||
position_choices = Rack.objects.get(pk=self.data['rack']).get_rack_units(face=self.data.get('face'))
|
||||
elif self.initial.get('rack') and self.initial.get('face') is not None:
|
||||
position_choices = Rack.objects.get(pk=self.initial['rack']).get_rack_units(face=self.initial.get('face'))
|
||||
else:
|
||||
position_choices = []
|
||||
except Rack.DoesNotExist:
|
||||
position_choices = []
|
||||
self.fields['position'].choices = [('', '---------')] + [
|
||||
(p['id'], {
|
||||
'label': p['name'],
|
||||
'disabled': bool(p['device'] and p['id'] != self.initial.get('position')),
|
||||
}) for p in position_choices
|
||||
]
|
||||
|
||||
# Limit device_type choices
|
||||
if self.is_bound:
|
||||
self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer__pk=self.data['manufacturer'])\
|
||||
.select_related('manufacturer')
|
||||
elif self.initial.get('manufacturer'):
|
||||
self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer=self.initial['manufacturer'])\
|
||||
.select_related('manufacturer')
|
||||
else:
|
||||
self.fields['device_type'].choices = []
|
||||
|
||||
|
||||
class DeviceFromCSVForm(forms.ModelForm):
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid device role.'})
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid manufacturer.'})
|
||||
model_name = forms.CharField()
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid platform.'})
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
|
||||
'invalid_choice': 'Invalid site name.',
|
||||
})
|
||||
rack_name = forms.CharField()
|
||||
face = forms.ChoiceField(choices=[('front', 'Front'), ('rear', 'Rear')])
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
|
||||
'position', 'face']
|
||||
|
||||
def clean(self):
|
||||
|
||||
manufacturer = self.cleaned_data.get('manufacturer')
|
||||
model_name = self.cleaned_data.get('model_name')
|
||||
site = self.cleaned_data.get('site')
|
||||
rack_name = self.cleaned_data.get('rack_name')
|
||||
|
||||
# Validate device type
|
||||
if manufacturer and model_name:
|
||||
try:
|
||||
self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
|
||||
except DeviceType.DoesNotExist:
|
||||
self.add_error('model_name', "Invalid device type ({})".format(model_name))
|
||||
|
||||
# Validate rack
|
||||
if site and rack_name:
|
||||
try:
|
||||
self.instance.rack = Rack.objects.get(site=site, name=rack_name)
|
||||
except Rack.DoesNotExist:
|
||||
self.add_error('rack_name', "Invalid rack ({})".format(rack_name))
|
||||
|
||||
def clean_face(self):
|
||||
face = self.cleaned_data['face']
|
||||
if face.lower() == 'front':
|
||||
return 0
|
||||
if face.lower() == 'rear':
|
||||
return 1
|
||||
raise forms.ValidationError("Invalid rack face ({})".format(face))
|
||||
|
||||
|
||||
class DeviceImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=DeviceFromCSVForm)
|
||||
|
||||
|
||||
class DeviceBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, label='Platform')
|
||||
platform_delete = forms.BooleanField(required=False, label='Set platform to "none"')
|
||||
status = forms.ChoiceField(choices=BULK_STATUS_CHOICES, required=False, initial='', label='Status')
|
||||
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
||||
ro_snmp = forms.CharField(max_length=50, required=False, label='SNMP (RO)')
|
||||
|
||||
|
||||
class DeviceBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def device_site_choices():
|
||||
site_choices = Site.objects.annotate(device_count=Count('racks__devices'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
|
||||
|
||||
|
||||
def device_role_choices():
|
||||
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices]
|
||||
|
||||
|
||||
def device_type_choices():
|
||||
type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
|
||||
return [(t.slug, '{} ({})'.format(t, t.device_count)) for t in type_choices]
|
||||
|
||||
|
||||
def device_platform_choices():
|
||||
platform_choices = Platform.objects.annotate(device_count=Count('devices'))
|
||||
return [(p.slug, '{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
|
||||
|
||||
|
||||
class DeviceFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
type = forms.MultipleChoiceField(required=False, choices=device_type_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
|
||||
|
||||
|
||||
#
|
||||
# Console ports
|
||||
#
|
||||
|
||||
class ConsolePortForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['device', 'name']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class ConsolePortCreateForm(forms.Form, BootstrapMixin):
|
||||
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'})
|
||||
cs_port = forms.CharField()
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
console_port = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate console server port
|
||||
if self.cleaned_data.get('console_server'):
|
||||
try:
|
||||
cs_port = ConsoleServerPort.objects.get(device=self.cleaned_data['console_server'],
|
||||
name=self.cleaned_data['cs_port'])
|
||||
if ConsolePort.objects.filter(cs_port=cs_port):
|
||||
raise forms.ValidationError("Console server port is already occupied (by {} {})"
|
||||
.format(cs_port.connected_console.device, cs_port.connected_console))
|
||||
except ConsoleServerPort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid console server port ({} {})"
|
||||
.format(self.cleaned_data['console_server'], self.cleaned_data['cs_port']))
|
||||
|
||||
# Validate console port
|
||||
if self.cleaned_data.get('device'):
|
||||
try:
|
||||
console_port = ConsolePort.objects.get(device=self.cleaned_data['device'],
|
||||
name=self.cleaned_data['console_port'])
|
||||
if console_port.cs_port:
|
||||
raise forms.ValidationError("Console port is already connected (to {} {})"
|
||||
.format(console_port.cs_port.device, console_port.cs_port))
|
||||
except ConsolePort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid console port ({} {})"
|
||||
.format(self.cleaned_data['device'], self.cleaned_data['console_port']))
|
||||
|
||||
|
||||
class ConsoleConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
|
||||
|
||||
def clean(self):
|
||||
records = self.cleaned_data.get('csv')
|
||||
if not records:
|
||||
return
|
||||
|
||||
connection_list = []
|
||||
|
||||
for i, record in enumerate(records, start=1):
|
||||
form = self.fields['csv'].csv_form(data=record)
|
||||
if form.is_valid():
|
||||
console_port = ConsolePort.objects.get(device=form.cleaned_data['device'],
|
||||
name=form.cleaned_data['console_port'])
|
||||
console_port.cs_port = ConsoleServerPort.objects.get(device=form.cleaned_data['console_server'],
|
||||
name=form.cleaned_data['cs_port'])
|
||||
if form.cleaned_data['status'] == 'planned':
|
||||
console_port.connection_status = CONNECTION_STATUS_PLANNED
|
||||
else:
|
||||
console_port.connection_status = CONNECTION_STATUS_CONNECTED
|
||||
connection_list.append(console_port)
|
||||
else:
|
||||
for field, errors in form.errors.items():
|
||||
for e in errors:
|
||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
||||
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class ConsolePortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
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',
|
||||
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'))
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
|
||||
labels = {
|
||||
'cs_port': 'Port',
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(ConsolePortConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
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.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)
|
||||
elif self.initial.get('rack'):
|
||||
self.fields['console_server'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_console_server=True)
|
||||
else:
|
||||
self.fields['console_server'].choices = []
|
||||
|
||||
# Initialize CS port choices
|
||||
if self.is_bound:
|
||||
self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(device__pk=self.data['console_server'])
|
||||
elif self.initial.get('console_server', None):
|
||||
self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(device__pk=self.initial['console_server'])
|
||||
else:
|
||||
self.fields['cs_port'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
class ConsoleServerPortForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['device', 'name']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortCreateForm(forms.Form, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class ConsoleServerPortConnectionForm(forms.Form, BootstrapMixin):
|
||||
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}}',
|
||||
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))
|
||||
|
||||
class Meta:
|
||||
fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
|
||||
labels = {
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
|
||||
def __init__(self, consoleserverport, *args, **kwargs):
|
||||
|
||||
super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=consoleserverport.device.rack.site)
|
||||
|
||||
# Initialize device choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
|
||||
elif self.initial.get('rack', None):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
|
||||
# Initialize port choices
|
||||
if self.is_bound:
|
||||
self.fields['port'].queryset = ConsolePort.objects.filter(device__pk=self.data['device'])
|
||||
elif self.initial.get('device', None):
|
||||
self.fields['port'].queryset = ConsolePort.objects.filter(device_pk=self.initial['device'])
|
||||
else:
|
||||
self.fields['port'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
#
|
||||
|
||||
class PowerPortForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['device', 'name']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class PowerPortCreateForm(forms.Form, BootstrapMixin):
|
||||
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.'})
|
||||
power_outlet = forms.CharField()
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
power_port = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate power outlet
|
||||
if self.cleaned_data.get('pdu'):
|
||||
try:
|
||||
power_outlet = PowerOutlet.objects.get(device=self.cleaned_data['pdu'],
|
||||
name=self.cleaned_data['power_outlet'])
|
||||
if PowerPort.objects.filter(power_outlet=power_outlet):
|
||||
raise forms.ValidationError("Power outlet is already occupied (by {} {})"
|
||||
.format(power_outlet.connected_console.device,
|
||||
power_outlet.connected_console))
|
||||
except PowerOutlet.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid PDU port ({} {})"
|
||||
.format(self.cleaned_data['pdu'], self.cleaned_data['power_outlet']))
|
||||
|
||||
# Validate power port
|
||||
if self.cleaned_data.get('device'):
|
||||
try:
|
||||
power_port = PowerPort.objects.get(device=self.cleaned_data['device'],
|
||||
name=self.cleaned_data['power_port'])
|
||||
if power_port.power_outlet:
|
||||
raise forms.ValidationError("Power port is already connected (to {} {})"
|
||||
.format(power_port.power_outlet.device, power_port.power_outlet))
|
||||
except PowerPort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid power port ({} {})"
|
||||
.format(self.cleaned_data['device'], self.cleaned_data['power_port']))
|
||||
|
||||
|
||||
class PowerConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=PowerConnectionCSVForm)
|
||||
|
||||
def clean(self):
|
||||
records = self.cleaned_data.get('csv')
|
||||
if not records:
|
||||
return
|
||||
|
||||
connection_list = []
|
||||
|
||||
for i, record in enumerate(records, start=1):
|
||||
form = self.fields['csv'].csv_form(data=record)
|
||||
if form.is_valid():
|
||||
power_port = PowerPort.objects.get(device=form.cleaned_data['device'],
|
||||
name=form.cleaned_data['power_port'])
|
||||
power_port.cs_port = PowerOutlet.objects.get(device=form.cleaned_data['pdu'],
|
||||
name=form.cleaned_data['power_outlet'])
|
||||
if form.cleaned_data['status'] == 'planned':
|
||||
power_port.connection_status = CONNECTION_STATUS_PLANNED
|
||||
else:
|
||||
power_port.connection_status = CONNECTION_STATUS_CONNECTED
|
||||
connection_list.append(power_port)
|
||||
else:
|
||||
for field, errors in form.errors.items():
|
||||
for e in errors:
|
||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
||||
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class PowerPortConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
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',
|
||||
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'))
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
|
||||
labels = {
|
||||
'power_outlet': 'Outlet',
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super(PowerPortConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
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.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)
|
||||
elif self.initial.get('rack', None):
|
||||
self.fields['pdu'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_pdu=True)
|
||||
else:
|
||||
self.fields['pdu'].choices = []
|
||||
|
||||
# Initialize power outlet choices
|
||||
if self.is_bound:
|
||||
self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device__pk=self.data['pdu'])
|
||||
elif self.initial.get('pdu', None):
|
||||
self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device__pk=self.initial['pdu'])
|
||||
else:
|
||||
self.fields['power_outlet'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
class PowerOutletForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = ['device', 'name']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletCreateForm(forms.Form, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class PowerOutletConnectionForm(forms.Form, BootstrapMixin):
|
||||
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}}',
|
||||
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))
|
||||
|
||||
class Meta:
|
||||
fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
|
||||
labels = {
|
||||
'connection_status': 'Status',
|
||||
}
|
||||
|
||||
def __init__(self, poweroutlet, *args, **kwargs):
|
||||
|
||||
super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack'].queryset = Rack.objects.filter(site=poweroutlet.device.rack.site)
|
||||
|
||||
# Initialize device choices
|
||||
if self.is_bound and self.data.get('rack'):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
|
||||
elif self.initial.get('rack', None):
|
||||
self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
|
||||
else:
|
||||
self.fields['device'].choices = []
|
||||
|
||||
# Initialize port choices
|
||||
if self.is_bound:
|
||||
self.fields['port'].queryset = PowerPort.objects.filter(device__pk=self.data['device'])
|
||||
elif self.initial.get('device', None):
|
||||
self.fields['port'].queryset = PowerPort.objects.filter(device_pk=self.initial['device'])
|
||||
else:
|
||||
self.fields['port'].choices = []
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
class InterfaceForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['device', 'name', 'form_factor', 'mgmt_only', 'description']
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class InterfaceCreateForm(forms.ModelForm, BootstrapMixin):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['name_pattern', 'form_factor', 'mgmt_only', 'description']
|
||||
|
||||
|
||||
class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Interface connections
|
||||
#
|
||||
|
||||
class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
|
||||
interface_a = forms.ChoiceField(choices=[], widget=SelectWithDisabled, label='Interface')
|
||||
rack_b = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
|
||||
widget=forms.Select(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}}',
|
||||
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'))
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['interface_a', 'rack_b', 'device_b', 'interface_b', 'livesearch', 'connection_status']
|
||||
|
||||
def __init__(self, device_a, *args, **kwargs):
|
||||
|
||||
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['rack_b'].queryset = Rack.objects.filter(site=device_a.rack.site)
|
||||
|
||||
# Initialize interface A choices
|
||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \
|
||||
.select_related('circuit', '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
|
||||
]
|
||||
|
||||
# Initialize device_b choices if rack_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.initial.get('rack_b'):
|
||||
self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
|
||||
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', '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', 'connected_as_a', 'connected_as_b')
|
||||
else:
|
||||
device_b_interfaces = []
|
||||
self.fields['interface_b'].choices = [
|
||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_b_interfaces
|
||||
]
|
||||
|
||||
|
||||
class InterfaceConnectionCSVForm(forms.Form):
|
||||
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.'})
|
||||
interface_b = forms.CharField()
|
||||
status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate interface A
|
||||
if self.cleaned_data.get('device_a'):
|
||||
try:
|
||||
interface_a = Interface.objects.get(device=self.cleaned_data['device_a'],
|
||||
name=self.cleaned_data['interface_a'])
|
||||
except Interface.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid interface ({} {})"
|
||||
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
|
||||
try:
|
||||
InterfaceConnection.objects.get(Q(interface_a=interface_a) | Q(interface_b=interface_a))
|
||||
raise forms.ValidationError("{} {} is already connected"
|
||||
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
|
||||
except InterfaceConnection.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Validate interface B
|
||||
if self.cleaned_data.get('device_b'):
|
||||
try:
|
||||
interface_b = Interface.objects.get(device=self.cleaned_data['device_b'],
|
||||
name=self.cleaned_data['interface_b'])
|
||||
except Interface.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid interface ({} {})"
|
||||
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
|
||||
try:
|
||||
InterfaceConnection.objects.get(Q(interface_a=interface_b) | Q(interface_b=interface_b))
|
||||
raise forms.ValidationError("{} {} is already connected"
|
||||
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
|
||||
except InterfaceConnection.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class InterfaceConnectionImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
|
||||
|
||||
def clean(self):
|
||||
records = self.cleaned_data.get('csv')
|
||||
if not records:
|
||||
return
|
||||
|
||||
connection_list = []
|
||||
|
||||
for i, record in enumerate(records, start=1):
|
||||
form = self.fields['csv'].csv_form(data=record)
|
||||
if form.is_valid():
|
||||
interface_a = Interface.objects.get(device=form.cleaned_data['device_a'],
|
||||
name=form.cleaned_data['interface_a'])
|
||||
interface_b = Interface.objects.get(device=form.cleaned_data['device_b'],
|
||||
name=form.cleaned_data['interface_b'])
|
||||
connection = InterfaceConnection(interface_a=interface_a, interface_b=interface_b)
|
||||
if form.cleaned_data['status'] == 'planned':
|
||||
connection.connection_status = CONNECTION_STATUS_PLANNED
|
||||
else:
|
||||
connection.connection_status = CONNECTION_STATUS_CONNECTED
|
||||
connection_list.append(connection)
|
||||
else:
|
||||
for field, errors in form.errors.items():
|
||||
for e in errors:
|
||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
||||
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin):
|
||||
confirm = forms.BooleanField(required=True)
|
||||
# Used for HTTP redirect upon successful deletion
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
|
||||
|
||||
|
||||
#
|
||||
# Connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
|
||||
|
||||
class PowerConnectionFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
|
||||
|
||||
class InterfaceConnectionFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
|
||||
|
||||
#
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
||||
set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'interface', 'set_as_primary']
|
||||
help_texts = {
|
||||
'address': 'IPv4 or IPv6 address (with mask)'
|
||||
}
|
||||
|
||||
def __init__(self, device, *args, **kwargs):
|
||||
|
||||
super(IPAddressForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
self.fields['interface'].queryset = device.interfaces.all()
|
||||
self.fields['interface'].required = True
|
||||
|
||||
# If this device does not have any IP addresses assigned, default to setting the first IP as its primary
|
||||
if not IPAddress.objects.filter(interface__device=device).count():
|
||||
self.fields['set_as_primary'].initial = True
|
|
@ -0,0 +1,291 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.1 on 2016-02-27 02:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utilities.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ConsolePort',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('connection_status', models.NullBooleanField(choices=[[False, b'Planned'], [True, b'Connected']], default=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConsolePortTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConsoleServerPort',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConsoleServerPortTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Device',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', utilities.fields.NullableCharField(blank=True, max_length=50, null=True, unique=True)),
|
||||
('serial', models.CharField(blank=True, max_length=50, verbose_name=b'Serial number')),
|
||||
('position', models.PositiveSmallIntegerField(blank=True, help_text=b'Number of the lowest U position occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Position (U)')),
|
||||
('face', models.PositiveSmallIntegerField(blank=True, choices=[[0, b'Front'], [1, b'Rear']], null=True, verbose_name=b'Rack face')),
|
||||
('status', models.BooleanField(choices=[[True, b'Active'], [False, b'Offline']], default=True, verbose_name=b'Status')),
|
||||
('ro_snmp', models.CharField(blank=True, max_length=50, verbose_name=b'SNMP (RO)')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceRole',
|
||||
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)),
|
||||
('color', models.CharField(choices=[[b'teal', b'Teal'], [b'green', b'Green'], [b'blue', b'Blue'], [b'purple', b'Purple'], [b'yellow', b'Yellow'], [b'orange', b'Orange'], [b'red', b'Red'], [b'light_gray', b'Light Gray'], [b'medium_gray', b'Medium Gray'], [b'dark_gray', b'Dark Gray']], max_length=30)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceType',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('model', models.CharField(max_length=50)),
|
||||
('slug', models.SlugField()),
|
||||
('u_height', models.PositiveSmallIntegerField(default=1, verbose_name=b'Height (U)')),
|
||||
('is_full_depth', models.BooleanField(default=True, help_text=b'Device consumes both front and rear rack faces', verbose_name=b'Is full depth')),
|
||||
('is_console_server', models.BooleanField(default=False, help_text=b'Include this type of device in lists of console servers', verbose_name=b'Is a console server')),
|
||||
('is_pdu', models.BooleanField(default=False, help_text=b'Include this type of device in lists of PDUs', verbose_name=b'Is a PDU')),
|
||||
('is_network_device', models.BooleanField(default=True, help_text=b'This is a network device (e.g. switch, router, etc.)', verbose_name=b'Is a network device')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['manufacturer', 'model'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Interface',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('form_factor', models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (Copper)'], [1000, b'1GE (Copper)'], [1100, b'1GE (SFP)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200)),
|
||||
('mgmt_only', models.BooleanField(default=False, verbose_name=b'OOB Management')),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InterfaceConnection',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('connection_status', models.BooleanField(choices=[[False, b'Planned'], [True, b'Connected']], default=True, verbose_name=b'Status')),
|
||||
('interface_a', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='connected_as_a', to='dcim.Interface')),
|
||||
('interface_b', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='connected_as_b', to='dcim.Interface')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InterfaceTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('form_factor', models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (Copper)'], [1000, b'1GE (Copper)'], [1100, b'1GE (SFP)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200)),
|
||||
('mgmt_only', models.BooleanField(default=False, verbose_name=b'Management only')),
|
||||
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interface_templates', to='dcim.DeviceType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Manufacturer',
|
||||
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)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Module',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50, verbose_name=b'Name')),
|
||||
('part_id', models.CharField(blank=True, max_length=50, verbose_name=b'Part ID')),
|
||||
('serial', models.CharField(blank=True, max_length=50, verbose_name=b'Serial number')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', to='dcim.Device')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Platform',
|
||||
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)),
|
||||
('rpc_client', models.CharField(blank=True, choices=[[b'juniper-junos', b'Juniper Junos (NETCONF)'], [b'cisco-ios', b'Cisco IOS (SSH)'], [b'opengear', b'Opengear (SSH)']], max_length=30, verbose_name=b'RPC client')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerOutlet',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_outlets', to='dcim.Device')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerOutletTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_outlet_templates', to='dcim.DeviceType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerPort',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('connection_status', models.NullBooleanField(choices=[[False, b'Planned'], [True, b'Connected']], default=True)),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_ports', to='dcim.Device')),
|
||||
('power_outlet', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_port', to='dcim.PowerOutlet')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerPortTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='power_port_templates', to='dcim.DeviceType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['device_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Rack',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('facility_id', utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name=b'Facility ID')),
|
||||
('u_height', models.PositiveSmallIntegerField(default=42, verbose_name=b'Height (U)')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['site', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RackGroup',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('slug', models.SlugField()),
|
||||
],
|
||||
options={
|
||||
'ordering': ['site', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Site',
|
||||
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)),
|
||||
('facility', models.CharField(blank=True, max_length=50)),
|
||||
('asn', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'ASN')),
|
||||
('physical_address', models.CharField(blank=True, max_length=200)),
|
||||
('shipping_address', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackgroup',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rack_groups', to='dcim.Site'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='racks', to='dcim.RackGroup'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='site',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.Site'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='device_types', to='dcim.Manufacturer'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='device_role',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.DeviceRole'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='instances', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='platform',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='dcim.Platform'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,114 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.1 on 2016-02-27 02:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0001_initial'),
|
||||
('ipam', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='primary_ip',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_for', to='ipam.IPAddress', verbose_name=b'Primary IP'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='rack',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_port_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='device',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_ports', to='dcim.Device'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='device_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_port_templates', to='dcim.DeviceType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='cs_port',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name=b'Console server port'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='device',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_ports', to='dcim.Device'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rackgroup',
|
||||
unique_together=set([('site', 'name'), ('site', 'slug')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rack',
|
||||
unique_together=set([('site', 'facility_id'), ('site', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerporttemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='powerport',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='poweroutlettemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='poweroutlet',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='module',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='interfacetemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='interface',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='devicetype',
|
||||
unique_together=set([('manufacturer', 'slug'), ('manufacturer', 'model')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='device',
|
||||
unique_together=set([('rack', 'position', 'face')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleserverporttemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleserverport',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleporttemplate',
|
||||
unique_together=set([('device_type', 'name')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='consoleport',
|
||||
unique_together=set([('device', 'name')]),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,686 @@
|
|||
from collections import OrderedDict
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q, ObjectDoesNotExist
|
||||
|
||||
from extras.rpc import RPC_CLIENTS
|
||||
from secrets.models import Secret
|
||||
from utilities.fields import NullableCharField
|
||||
|
||||
|
||||
RACK_FACE_FRONT = 0
|
||||
RACK_FACE_REAR = 1
|
||||
RACK_FACE_CHOICES = [
|
||||
[RACK_FACE_FRONT, 'Front'],
|
||||
[RACK_FACE_REAR, 'Rear'],
|
||||
]
|
||||
|
||||
COLOR_TEAL = 'teal'
|
||||
COLOR_GREEN = 'green'
|
||||
COLOR_BLUE = 'blue'
|
||||
COLOR_PURPLE = 'purple'
|
||||
COLOR_YELLOW = 'yellow'
|
||||
COLOR_ORANGE = 'orange'
|
||||
COLOR_RED = 'red'
|
||||
COLOR_GRAY1 = 'light_gray'
|
||||
COLOR_GRAY2 = 'medium_gray'
|
||||
COLOR_GRAY3 = 'dark_gray'
|
||||
DEVICE_ROLE_COLOR_CHOICES = [
|
||||
[COLOR_TEAL, 'Teal'],
|
||||
[COLOR_GREEN, 'Green'],
|
||||
[COLOR_BLUE, 'Blue'],
|
||||
[COLOR_PURPLE, 'Purple'],
|
||||
[COLOR_YELLOW, 'Yellow'],
|
||||
[COLOR_ORANGE, 'Orange'],
|
||||
[COLOR_RED, 'Red'],
|
||||
[COLOR_GRAY1, 'Light Gray'],
|
||||
[COLOR_GRAY2, 'Medium Gray'],
|
||||
[COLOR_GRAY3, 'Dark Gray'],
|
||||
]
|
||||
|
||||
IFACE_FF_VIRTUAL = 0
|
||||
IFACE_FF_100M_COPPER = 800
|
||||
IFACE_FF_1GE_COPPER = 1000
|
||||
IFACE_FF_SFP = 1100
|
||||
IFACE_FF_SFP_PLUS = 1200
|
||||
IFACE_FF_XFP = 1300
|
||||
IFACE_FF_QSFP_PLUS = 1400
|
||||
IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||
[IFACE_FF_100M_COPPER, '10/100M (Copper)'],
|
||||
[IFACE_FF_1GE_COPPER, '1GE (Copper)'],
|
||||
[IFACE_FF_SFP, '1GE (SFP)'],
|
||||
[IFACE_FF_SFP_PLUS, '10GE (SFP+)'],
|
||||
[IFACE_FF_XFP, '10GE (XFP)'],
|
||||
[IFACE_FF_QSFP_PLUS, '40GE (QSFP+)'],
|
||||
]
|
||||
|
||||
STATUS_ACTIVE = True
|
||||
STATUS_OFFLINE = False
|
||||
STATUS_CHOICES = [
|
||||
[STATUS_ACTIVE, 'Active'],
|
||||
[STATUS_OFFLINE, 'Offline'],
|
||||
]
|
||||
|
||||
CONNECTION_STATUS_PLANNED = False
|
||||
CONNECTION_STATUS_CONNECTED = True
|
||||
CONNECTION_STATUS_CHOICES = [
|
||||
[CONNECTION_STATUS_PLANNED, 'Planned'],
|
||||
[CONNECTION_STATUS_CONNECTED, 'Connected'],
|
||||
]
|
||||
|
||||
# For mapping platform -> NC client
|
||||
RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos'
|
||||
RPC_CLIENT_CISCO_IOS = 'cisco-ios'
|
||||
RPC_CLIENT_OPENGEAR = 'opengear'
|
||||
RPC_CLIENT_CHOICES = [
|
||||
[RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'],
|
||||
[RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'],
|
||||
[RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'],
|
||||
]
|
||||
|
||||
|
||||
class Site(models.Model):
|
||||
"""
|
||||
A physical site
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
facility = models.CharField(max_length=50, blank=True)
|
||||
asn = models.PositiveIntegerField(blank=True, null=True, verbose_name='ASN')
|
||||
physical_address = models.CharField(max_length=200, blank=True)
|
||||
shipping_address = models.CharField(max_length=200, blank=True)
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:site', args=[self.slug])
|
||||
|
||||
@property
|
||||
def count_prefixes(self):
|
||||
return self.prefixes.count()
|
||||
|
||||
@property
|
||||
def count_vlans(self):
|
||||
return self.vlans.count()
|
||||
|
||||
@property
|
||||
def count_racks(self):
|
||||
return Rack.objects.filter(site=self).count()
|
||||
|
||||
@property
|
||||
def count_devices(self):
|
||||
return Device.objects.filter(rack__site=self).count()
|
||||
|
||||
@property
|
||||
def count_circuits(self):
|
||||
return self.circuits.count()
|
||||
|
||||
|
||||
class RackGroup(models.Model):
|
||||
"""
|
||||
An arbitrary grouping of Racks; e.g. a building or room.
|
||||
"""
|
||||
name = models.CharField(max_length=50)
|
||||
slug = models.SlugField()
|
||||
site = models.ForeignKey('Site', related_name='rack_groups')
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
['site', 'name'],
|
||||
['site', 'slug'],
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Rack(models.Model):
|
||||
"""
|
||||
An equipment rack within a site (e.g. a 48U rack)
|
||||
"""
|
||||
name = models.CharField(max_length=50)
|
||||
facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID')
|
||||
site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
|
||||
group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
|
||||
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
['site', 'name'],
|
||||
['site', 'facility_id'],
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
if self.facility_id:
|
||||
return "{} ({})".format(self.name, self.facility_id)
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:rack', args=[self.pk])
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return reversed(range(1, self.u_height + 1))
|
||||
|
||||
def get_rack_units(self, face=RACK_FACE_FRONT, remove_redundant=False):
|
||||
"""
|
||||
Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'}
|
||||
Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy.
|
||||
|
||||
:param face: Rack face (front or rear)
|
||||
:param remove_redundant: If True, rack units occupied by a device already listed will be omitted
|
||||
"""
|
||||
|
||||
elevation = OrderedDict()
|
||||
for u in reversed(range(1, self.u_height + 1)):
|
||||
elevation[u] = {'id': u, 'name': 'U{}'.format(u), 'face': face, 'device': None}
|
||||
|
||||
# Add devices to rack units list
|
||||
if self.pk:
|
||||
for device in Device.objects.select_related('device_type__manufacturer', 'device_role')\
|
||||
.filter(rack=self, position__gt=0).filter(Q(face=face) | Q(device_type__is_full_depth=True)):
|
||||
if remove_redundant:
|
||||
elevation[device.position]['device'] = device
|
||||
for u in range(device.position + 1, device.position + device.device_type.u_height):
|
||||
elevation.pop(u, None)
|
||||
else:
|
||||
for u in range(device.position, device.position + device.device_type.u_height):
|
||||
elevation[u]['device'] = device
|
||||
|
||||
return [u for u in elevation.values()]
|
||||
|
||||
def get_front_elevation(self):
|
||||
return self.get_rack_units(face=RACK_FACE_FRONT, remove_redundant=True)
|
||||
|
||||
def get_rear_elevation(self):
|
||||
return self.get_rack_units(face=RACK_FACE_REAR, remove_redundant=True)
|
||||
|
||||
def get_available_units(self, u_height=1, rack_face=None, exclude=list()):
|
||||
"""
|
||||
Return a list of units within the rack available to accommodate a device of a given U height (default 1).
|
||||
Optionally exclude one or more devices when calculating empty units (needed when moving a device from one
|
||||
position to another within a rack).
|
||||
|
||||
:param u_height: Minimum number of contiguous free units required
|
||||
:param rack_face: The face of the rack (front or rear) required; 'None' if device is full depth
|
||||
:param exclude: List of devices IDs to exclude (useful when moving a device within a rack)
|
||||
"""
|
||||
|
||||
# Gather all devices which consume U space within the rack
|
||||
devices = self.devices.select_related().filter(position__gte=1).exclude(pk__in=exclude)
|
||||
|
||||
# Initialize the rack unit skeleton
|
||||
units = range(1, self.u_height + 1)
|
||||
|
||||
# Remove units consumed by installed devices
|
||||
for d in devices:
|
||||
if rack_face is None or d.face == rack_face or d.device_type.is_full_depth:
|
||||
for u in range(d.position, d.position + d.device_type.u_height):
|
||||
try:
|
||||
units.remove(u)
|
||||
except ValueError:
|
||||
# Found overlapping devices in the rack!
|
||||
pass
|
||||
|
||||
# Remove units without enough space above them to accommodate a device of the specified height
|
||||
available_units = []
|
||||
for u in units:
|
||||
if set(range(u, u + u_height)).issubset(units):
|
||||
available_units.append(u)
|
||||
|
||||
return list(reversed(available_units))
|
||||
|
||||
def get_0u_devices(self):
|
||||
return self.devices.filter(position=0)
|
||||
|
||||
|
||||
#
|
||||
# Device Types
|
||||
#
|
||||
|
||||
class Manufacturer(models.Model):
|
||||
"""
|
||||
A hardware manufacturer
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class DeviceType(models.Model):
|
||||
"""
|
||||
A unique hardware type; manufacturer and model number (e.g. Juniper EX4300-48T)
|
||||
"""
|
||||
manufacturer = models.ForeignKey('Manufacturer', related_name='device_types', on_delete=models.PROTECT)
|
||||
model = models.CharField(max_length=50)
|
||||
slug = models.SlugField()
|
||||
u_height = models.PositiveSmallIntegerField(verbose_name='Height (U)', default=1)
|
||||
is_full_depth = models.BooleanField(default=True, verbose_name="Is full depth",
|
||||
help_text="Device consumes both front and rear rack faces")
|
||||
is_console_server = models.BooleanField(default=False, verbose_name='Is a console server',
|
||||
help_text="Include this type of device in lists of console servers")
|
||||
is_pdu = models.BooleanField(default=False, verbose_name='Is a PDU',
|
||||
help_text="Include this type of device in lists of PDUs")
|
||||
is_network_device = models.BooleanField(default=True, verbose_name='Is a network device',
|
||||
help_text="This is a network device (e.g. switch, router, etc.)")
|
||||
|
||||
class Meta:
|
||||
ordering = ['manufacturer', 'model']
|
||||
unique_together = [
|
||||
['manufacturer', 'model'],
|
||||
['manufacturer', 'slug'],
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
return "{0} {1}".format(self.manufacturer, self.model)
|
||||
|
||||
|
||||
class ConsolePortTemplate(models.Model):
|
||||
"""
|
||||
A template for a ConsolePort to be created for a new device
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='console_port_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ConsoleServerPortTemplate(models.Model):
|
||||
"""
|
||||
A template for a ConsoleServerPort to be created for a new device
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='cs_port_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PowerPortTemplate(models.Model):
|
||||
"""
|
||||
A template for a PowerPort to be created for a new device
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='power_port_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PowerOutletTemplate(models.Model):
|
||||
"""
|
||||
A template for a PowerOutlet to be created for a new device
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='power_outlet_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class InterfaceTemplate(models.Model):
|
||||
"""
|
||||
A template for a physical data interface on a new device
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='interface_templates', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS)
|
||||
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceRole(models.Model):
|
||||
"""
|
||||
The functional role of a device (e.g. router, switch, console server, etc.)
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
color = models.CharField(max_length=30, choices=DEVICE_ROLE_COLOR_CHOICES)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Platform(models.Model):
|
||||
"""
|
||||
A class of software running on a hardware device (e.g. Juniper Junos or Cisco IOS)
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True, verbose_name='RPC client')
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Device(models.Model):
|
||||
"""
|
||||
A physical piece of equipment mounted within a rack
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
|
||||
device_role = models.ForeignKey('DeviceRole', 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)
|
||||
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
|
||||
rack = models.ForeignKey('Rack', related_name='devices', on_delete=models.PROTECT)
|
||||
position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(1)], verbose_name='Position (U)', help_text='Number of the lowest U position occupied by the device')
|
||||
face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
|
||||
status = models.BooleanField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
|
||||
primary_ip = models.OneToOneField('ipam.IPAddress', related_name='primary_for', on_delete=models.SET_NULL, blank=True, null=True, verbose_name='Primary IP')
|
||||
ro_snmp = models.CharField(max_length=50, blank=True, verbose_name='SNMP (RO)')
|
||||
comments = models.TextField(blank=True)
|
||||
secrets = GenericRelation(Secret)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = ['rack', 'position', 'face']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.display_name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:device', args=[self.pk])
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
elif self.position:
|
||||
return "{} ({} U{})".format(self.device_type, self.rack, self.position)
|
||||
else:
|
||||
return "{} ({})".format(self.device_type, self.rack)
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Validate position/face combination
|
||||
if self.position and self.face is None:
|
||||
raise ValidationError("Must specify rack face with rack position.")
|
||||
|
||||
# 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("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
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
is_new = not bool(self.pk)
|
||||
|
||||
super(Device, self).save(*args, **kwargs)
|
||||
|
||||
# If this is a new Device, instantiate all of the related components per the DeviceType definition
|
||||
if is_new:
|
||||
ConsolePort.objects.bulk_create(
|
||||
[ConsolePort(device=self, name=template.name) for template in self.device_type.console_port_templates.all()]
|
||||
)
|
||||
ConsoleServerPort.objects.bulk_create(
|
||||
[ConsoleServerPort(device=self, name=template.name) for template in self.device_type.cs_port_templates.all()]
|
||||
)
|
||||
PowerPort.objects.bulk_create(
|
||||
[PowerPort(device=self, name=template.name) for template in self.device_type.power_port_templates.all()]
|
||||
)
|
||||
PowerOutlet.objects.bulk_create(
|
||||
[PowerOutlet(device=self, name=template.name) for template in self.device_type.power_outlet_templates.all()]
|
||||
)
|
||||
Interface.objects.bulk_create(
|
||||
[Interface(device=self, name=template.name, form_factor=template.form_factor, mgmt_only=template.mgmt_only) for template in self.device_type.interface_templates.all()]
|
||||
)
|
||||
|
||||
def get_rpc_client(self):
|
||||
"""
|
||||
Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined.
|
||||
"""
|
||||
if not self.platform:
|
||||
return None
|
||||
return RPC_CLIENTS.get(self.platform.rpc_client)
|
||||
|
||||
|
||||
class ConsolePort(models.Model):
|
||||
"""
|
||||
A physical console port on a device
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='console_ports', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
cs_port = models.OneToOneField('ConsoleServerPort', related_name='connected_console', on_delete=models.SET_NULL, verbose_name='Console server port', blank=True, null=True)
|
||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ConsoleServerPortManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Include the trailing numeric portion of each port name to allow for proper ordering.
|
||||
For example:
|
||||
Port 1, Port 2, Port 3 ... Port 9, Port 10, Port 11 ...
|
||||
Instead of:
|
||||
Port 1, Port 10, Port 11 ... Port 19, Port 2, Port 20 ...
|
||||
"""
|
||||
return super(ConsoleServerPortManager, self).get_queryset().extra(select={
|
||||
'name_as_integer': "CAST(substring(dcim_consoleserverport.name FROM '[0-9]+$') AS INTEGER)",
|
||||
}).order_by('device', 'name_as_integer')
|
||||
|
||||
|
||||
class ConsoleServerPort(models.Model):
|
||||
"""
|
||||
A physical port on a console server
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='cs_ports', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
objects = ConsoleServerPortManager()
|
||||
|
||||
class Meta:
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PowerPort(models.Model):
|
||||
"""
|
||||
A physical power supply (intake) port on a device
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='power_ports', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
power_outlet = models.OneToOneField('PowerOutlet', related_name='connected_port', on_delete=models.SET_NULL, blank=True, null=True)
|
||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PowerOutletManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
return super(PowerOutletManager, self).get_queryset().extra(select={
|
||||
'name_padded': "CONCAT(SUBSTRING(dcim_poweroutlet.name FROM '^[^0-9]+'), LPAD(SUBSTRING(dcim_poweroutlet.name FROM '[0-9\/]+$'), 8, '0'))",
|
||||
}).order_by('device', 'name_padded')
|
||||
|
||||
|
||||
class PowerOutlet(models.Model):
|
||||
"""
|
||||
A physical power outlet (output) port on a device
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='power_outlets', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
|
||||
objects = PowerOutletManager()
|
||||
|
||||
class Meta:
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class InterfaceManager(models.Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Cast up to three interface slot/position IDs as independent integers and order appropriately. This ensures that
|
||||
interfaces are ordered numerically without regard to type. For example:
|
||||
xe-0/0/0, xe-0/0/1, xe-0/0/2 ... et-0/0/47, et-0/0/48, et-0/0/49 ...
|
||||
instead of:
|
||||
et-0/0/48, et-0/0/49, et-0/0/50 ... et-0/0/53, xe-0/0/0, xe-0/0/1 ...
|
||||
"""
|
||||
return super(InterfaceManager, self).get_queryset().extra(select={
|
||||
'_id1': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)\/([0-9]+)\/([0-9]+)$') AS integer)",
|
||||
'_id2': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)\/([0-9]+)$') AS integer)",
|
||||
'_id3': "CAST(SUBSTRING(dcim_interface.name FROM '([0-9]+)$') AS integer)",
|
||||
}).order_by('device', '_id1', '_id2', '_id3')
|
||||
|
||||
def virtual(self):
|
||||
return self.get_queryset().filter(form_factor=IFACE_FF_VIRTUAL)
|
||||
|
||||
def physical(self):
|
||||
return self.get_queryset().exclude(form_factor=IFACE_FF_VIRTUAL)
|
||||
|
||||
|
||||
class Interface(models.Model):
|
||||
"""
|
||||
A physical data interface on a device
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=30)
|
||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_SFP_PLUS)
|
||||
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management')
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
|
||||
objects = InterfaceManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def is_physical(self):
|
||||
return self.form_factor != IFACE_FF_VIRTUAL
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
try:
|
||||
return bool(self.circuit)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
return bool(self.connection)
|
||||
|
||||
@property
|
||||
def connection(self):
|
||||
try:
|
||||
return self.connected_as_a
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
try:
|
||||
return self.connected_as_b
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_connected_interface(self):
|
||||
try:
|
||||
connection = InterfaceConnection.objects.select_related().get(Q(interface_a=self) | Q(interface_b=self))
|
||||
if connection.interface_a == self:
|
||||
return connection.interface_b
|
||||
else:
|
||||
return connection.interface_a
|
||||
except InterfaceConnection.DoesNotExist:
|
||||
return None
|
||||
except InterfaceConnection.MultipleObjectsReturned as e:
|
||||
raise e("Multiple connections found for {0} interface {1}!".format(self.device, self))
|
||||
|
||||
|
||||
class InterfaceConnection(models.Model):
|
||||
"""
|
||||
A symmetrical, one-to-one connection between two device interfaces
|
||||
"""
|
||||
interface_a = models.OneToOneField('Interface', related_name='connected_as_a', on_delete=models.CASCADE)
|
||||
interface_b = models.OneToOneField('Interface', related_name='connected_as_b', on_delete=models.CASCADE)
|
||||
connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED, verbose_name='Status')
|
||||
|
||||
|
||||
class Module(models.Model):
|
||||
"""
|
||||
A hardware module belonging to a device. Used for inventory purposes only.
|
||||
"""
|
||||
device = models.ForeignKey('Device', related_name='modules', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=50, verbose_name='Name')
|
||||
part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True)
|
||||
serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
|
@ -0,0 +1,165 @@
|
|||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from .models import Site, Rack, Device, ConsolePort, PowerPort
|
||||
|
||||
|
||||
PREFIXES_PER_VLAN = """
|
||||
{% for p in record.prefix_set.all %}
|
||||
<a href="{% url 'ipam:prefix' pk=p.pk %}">{{ p }}</a>
|
||||
{% if not forloop.last %}<br />{% endif %}
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
<span class="label label-{{ record.status.get_bootstrap_class_display|lower }}">
|
||||
{{ record.status.name }}
|
||||
</span>
|
||||
"""
|
||||
|
||||
DEVICE_LINK = """
|
||||
<a href="{% url 'dcim:device' pk=record.pk %}">{{ record.name|default:'<span class="label label-info">Unnamed device</span>' }}</a>
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteTable(tables.Table):
|
||||
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
|
||||
facility = tables.Column(verbose_name='Facility')
|
||||
asn = tables.Column(verbose_name='ASN')
|
||||
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
|
||||
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
|
||||
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
||||
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ('name', 'facility', 'asn', 'rack_count', 'device_count', 'prefix_count', 'vlan_count', 'circuit_count')
|
||||
empty_text = "No sites have been defined."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackTable(tables.Table):
|
||||
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
group = tables.Column(verbose_name='Group')
|
||||
facility_id = tables.Column(verbose_name='Facility ID')
|
||||
u_height = tables.Column(verbose_name='Height (U)')
|
||||
devices = tables.Column(accessor=Accessor('device_count'), orderable=False, verbose_name='Devices')
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ('name', 'site', 'group', 'facility_id', 'u_height')
|
||||
empty_text = "No racks were found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class RackBulkEditTable(RackTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(RackTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'u_height')
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
|
||||
class DeviceTable(tables.Table):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
|
||||
device_role = tables.Column(verbose_name='Role')
|
||||
device_type = tables.Column(verbose_name='Type')
|
||||
primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address', template_code="{{ record.primary_ip.address.ip }}")
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ('name', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||
empty_text = "No devices were found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class DeviceBulkEditTable(DeviceTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(DeviceTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'name', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||
|
||||
|
||||
class DeviceImportTable(tables.Table):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
site = tables.Column(accessor=Accessor('rack.site'), 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')
|
||||
device_type = tables.Column(verbose_name='Type')
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ('name', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Device connections
|
||||
#
|
||||
|
||||
class ConsoleConnectionTable(tables.Table):
|
||||
console_server = tables.LinkColumn('dcim:device', accessor=Accessor('cs_port.device'), args=[Accessor('cs_port.device.pk')], verbose_name='Console server')
|
||||
cs_port = tables.Column(verbose_name='Port')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
name = tables.Column(verbose_name='Console port')
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ('console_server', 'cs_port', 'device', 'name')
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class PowerConnectionTable(tables.Table):
|
||||
pdu = tables.LinkColumn('dcim:device', accessor=Accessor('power_outlet.device'), args=[Accessor('power_outlet.device.pk')], verbose_name='PDU')
|
||||
power_outlet = tables.Column(verbose_name='Outlet')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
|
||||
name = tables.Column(verbose_name='Console port')
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ('pdu', 'power_outlet', 'device', 'name')
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class InterfaceConnectionTable(tables.Table):
|
||||
device_a = tables.LinkColumn('dcim:device', accessor=Accessor('interface_a.device'), args=[Accessor('interface_a.device.pk')], verbose_name='Device A')
|
||||
interface_a = tables.Column(verbose_name='Interface A')
|
||||
device_b = tables.LinkColumn('dcim:device', accessor=Accessor('interface_b.device'), args=[Accessor('interface_b.device.pk')], verbose_name='Device B')
|
||||
interface_b = tables.Column(verbose_name='Interface B')
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
from django.test import TestCase
|
||||
from dcim.forms import *
|
||||
from dcim.models import *
|
||||
|
||||
|
||||
def get_id(model, slug):
|
||||
return model.objects.get(slug=slug).id
|
||||
|
||||
|
||||
class DeviceTestCase(TestCase):
|
||||
|
||||
fixtures = ['dcim', 'ipam']
|
||||
|
||||
def test_racked_device(self):
|
||||
test = DeviceForm(data={
|
||||
'device_role': get_id(DeviceRole, 'leaf-switch'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'face': RACK_FACE_FRONT,
|
||||
'platform': get_id(Platform, 'juniper-junos'),
|
||||
'device_type': get_id(DeviceType, 'qfx5100-48s'),
|
||||
'position': 41,
|
||||
'rack': '1',
|
||||
'manufacturer': get_id(Manufacturer, 'juniper'),
|
||||
})
|
||||
self.assertTrue(test.is_valid(), test.fields['position'].choices)
|
||||
self.assertTrue(test.save())
|
||||
|
||||
def test_racked_device_occupied(self):
|
||||
test = DeviceForm(data={
|
||||
'device_role': get_id(DeviceRole, 'leaf-switch'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'face': RACK_FACE_FRONT,
|
||||
'platform': get_id(Platform, 'juniper-junos'),
|
||||
'device_type': get_id(DeviceType, 'qfx5100-48s'),
|
||||
'position': 1,
|
||||
'rack': '1',
|
||||
'manufacturer': get_id(Manufacturer, 'juniper'),
|
||||
})
|
||||
self.assertFalse(test.is_valid())
|
||||
|
||||
def test_non_racked_device(self):
|
||||
test = DeviceForm(data={
|
||||
'device_role': get_id(DeviceRole, 'pdu'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'face': None,
|
||||
'platform': None,
|
||||
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
|
||||
'position': None,
|
||||
'rack': '1',
|
||||
'manufacturer': get_id(Manufacturer, 'servertech'),
|
||||
})
|
||||
self.assertTrue(test.is_valid())
|
||||
self.assertTrue(test.save())
|
||||
|
||||
def test_non_racked_device_with_face(self):
|
||||
test = DeviceForm(data={
|
||||
'device_role': get_id(DeviceRole, 'pdu'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'face': RACK_FACE_REAR,
|
||||
'platform': None,
|
||||
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
|
||||
'position': None,
|
||||
'rack': '1',
|
||||
'manufacturer': get_id(Manufacturer, 'servertech'),
|
||||
})
|
||||
self.assertTrue(test.is_valid())
|
||||
self.assertTrue(test.save())
|
|
@ -0,0 +1,96 @@
|
|||
from django.test import TestCase
|
||||
from dcim.models import *
|
||||
|
||||
|
||||
class RackTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
site = Site.objects.create(
|
||||
name='TestSite1',
|
||||
slug='my-test-site'
|
||||
)
|
||||
self.rack = Rack.objects.create(
|
||||
name='TestRack1',
|
||||
facility_id='A101',
|
||||
site=site,
|
||||
u_height=42
|
||||
)
|
||||
self.manufacturer = Manufacturer.objects.create(
|
||||
name='Acme',
|
||||
slug='acme'
|
||||
)
|
||||
|
||||
self.device_type = {
|
||||
'ff2048': DeviceType.objects.create(
|
||||
manufacturer=self.manufacturer,
|
||||
model='FrameForwarder 2048',
|
||||
slug='ff2048'
|
||||
),
|
||||
'cc5000': DeviceType.objects.create(
|
||||
manufacturer=self.manufacturer,
|
||||
model='CurrentCatapult 5000',
|
||||
slug='cc5000',
|
||||
u_height=0
|
||||
),
|
||||
}
|
||||
self.role = {
|
||||
'Server': DeviceRole.objects.create(
|
||||
name='Server',
|
||||
slug='server',
|
||||
),
|
||||
'Switch': DeviceRole.objects.create(
|
||||
name='Switch',
|
||||
slug='switch',
|
||||
),
|
||||
'Console Server': DeviceRole.objects.create(
|
||||
name='Console Server',
|
||||
slug='console-server',
|
||||
),
|
||||
'PDU': DeviceRole.objects.create(
|
||||
name='PDU',
|
||||
slug='pdu',
|
||||
),
|
||||
|
||||
}
|
||||
|
||||
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,
|
||||
position=10,
|
||||
face=RACK_FACE_REAR,
|
||||
)
|
||||
device1.save()
|
||||
|
||||
# Validate rack height
|
||||
self.assertEqual(list(rack1.units), list(reversed(range(1, 43))))
|
||||
|
||||
# Validate inventory (front face)
|
||||
rack1_inventory_front = rack1.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()
|
||||
self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
|
||||
del(rack1_inventory_rear[-10])
|
||||
for u in rack1_inventory_rear:
|
||||
self.assertIsNone(u['device'])
|
||||
|
||||
def test_mount_zero_ru(self):
|
||||
pdu = Device.objects.create(
|
||||
name='TestPDU',
|
||||
device_role=self.role.get('PDU'),
|
||||
device_type=self.device_type.get('cc5000'),
|
||||
rack=self.rack,
|
||||
position=None,
|
||||
face=None,
|
||||
)
|
||||
self.assertTrue(pdu)
|
|
@ -0,0 +1,86 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from secrets.views import secret_add
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# Sites
|
||||
url(r'^sites/$', views.site_list, name='site_list'),
|
||||
url(r'^sites/add/$', views.site_add, name='site_add'),
|
||||
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.site_edit, name='site_edit'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.site_delete, name='site_delete'),
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', views.rack_list, name='rack_list'),
|
||||
url(r'^racks/add/$', views.rack_add, name='rack_add'),
|
||||
url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'),
|
||||
url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
|
||||
url(r'^racks/delete/$', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
||||
url(r'^racks/(?P<pk>\d+)/$', views.rack, name='rack'),
|
||||
url(r'^racks/(?P<pk>\d+)/edit/$', views.rack_edit, name='rack_edit'),
|
||||
url(r'^racks/(?P<pk>\d+)/delete/$', views.rack_delete, name='rack_delete'),
|
||||
|
||||
# Devices
|
||||
url(r'^devices/$', views.device_list, name='device_list'),
|
||||
url(r'^devices/add/$', views.device_add, name='device_add'),
|
||||
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
|
||||
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
||||
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'),
|
||||
url(r'^devices/(?P<pk>\d+)/edit/$', views.device_edit, name='device_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/delete/$', views.device_delete, name='device_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'),
|
||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'),
|
||||
url(r'^devices/(?P<pk>\d+)/ip-addresses/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
|
||||
url(r'^devices/(?P<parent_pk>\d+)/add-secret/$', secret_add, {'parent_model': 'dcim.Device'},
|
||||
name='device_addsecret'),
|
||||
|
||||
# Console ports
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.consoleport_edit, name='consoleport_edit'),
|
||||
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.consoleport_delete, name='consoleport_delete'),
|
||||
|
||||
# Console server ports
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.consoleserverport_add, name='consoleserverport_add'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.consoleserverport_edit, name='consoleserverport_edit'),
|
||||
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.consoleserverport_delete, name='consoleserverport_delete'),
|
||||
|
||||
# Power ports
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.powerport_add, name='powerport_add'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.powerport_edit, name='powerport_edit'),
|
||||
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.powerport_delete, name='powerport_delete'),
|
||||
|
||||
# Power outlets
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.poweroutlet_add, name='poweroutlet_add'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.poweroutlet_edit, name='poweroutlet_edit'),
|
||||
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.poweroutlet_delete, name='poweroutlet_delete'),
|
||||
|
||||
# Console/power/interface connections
|
||||
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
||||
url(r'^console-connections/import/$', views.ConsoleConnectionsBulkImportView.as_view(), name='console_connections_import'),
|
||||
url(r'^power-connections/$', views.PowerConnectionsListView.as_view(), name='power_connections_list'),
|
||||
url(r'^power-connections/import/$', views.PowerConnectionsBulkImportView.as_view(), name='power_connections_import'),
|
||||
url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
|
||||
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
||||
|
||||
# Interfaces
|
||||
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_bulk_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.interface_edit, name='interface_edit'),
|
||||
url(r'^interfaces/(?P<pk>\d+)/delete/$', views.interface_delete, name='interface_delete'),
|
||||
|
||||
]
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import Graph, ExportTemplate
|
||||
|
||||
|
||||
@admin.register(Graph)
|
||||
class GraphAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'type', 'weight', 'source']
|
||||
|
||||
|
||||
@admin.register(ExportTemplate)
|
||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ['content_type', 'name', 'mime_type', 'file_extension']
|
|
@ -0,0 +1,31 @@
|
|||
from rest_framework import renderers
|
||||
|
||||
|
||||
# IP address family designations
|
||||
AF = {
|
||||
4: 'A',
|
||||
6: 'AAAA',
|
||||
}
|
||||
|
||||
|
||||
class BINDZoneRenderer(renderers.BaseRenderer):
|
||||
"""
|
||||
Generate a BIND zone file from a list of DNS records.
|
||||
Required fields: `name`, `primary_ip`
|
||||
"""
|
||||
media_type = 'text/plain'
|
||||
format = 'bind-zone'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
records = []
|
||||
for record in data:
|
||||
if record.get('name') and record.get('primary_ip'):
|
||||
try:
|
||||
records.append("{} IN {} {}".format(
|
||||
record['name'],
|
||||
AF[record['primary_ip']['family']],
|
||||
record['primary_ip']['address'].split('/')[0],
|
||||
))
|
||||
except KeyError:
|
||||
pass
|
||||
return '\n'.join(records)
|
|
@ -0,0 +1,14 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from extras.models import Graph
|
||||
|
||||
|
||||
class GraphSerializer(serializers.ModelSerializer):
|
||||
embed_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Graph
|
||||
fields = ['name', 'embed_url', 'link']
|
||||
|
||||
def get_embed_url(self, obj):
|
||||
return obj.embed_url(self.context['graphed_object'])
|
|
@ -0,0 +1,33 @@
|
|||
from rest_framework import generics
|
||||
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from circuits.models import Provider
|
||||
from dcim.models import Site, Interface
|
||||
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_PROVIDER, GRAPH_TYPE_SITE
|
||||
from .serializers import GraphSerializer
|
||||
|
||||
|
||||
class GraphListView(generics.ListAPIView):
|
||||
"""
|
||||
Returns a list of relevant graphs
|
||||
"""
|
||||
serializer_class = GraphSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
cls = {
|
||||
GRAPH_TYPE_INTERFACE: Interface,
|
||||
GRAPH_TYPE_PROVIDER: Provider,
|
||||
GRAPH_TYPE_SITE: Site,
|
||||
}
|
||||
context = super(GraphListView, self).get_serializer_context()
|
||||
context.update({'graphed_object': get_object_or_404(cls[self.kwargs.get('type')], pk=self.kwargs['pk'])})
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
graph_type = self.kwargs.get('type', None)
|
||||
if not graph_type:
|
||||
raise Http404()
|
||||
queryset = Graph.objects.filter(type=graph_type)
|
||||
return queryset
|
|
@ -0,0 +1,12 @@
|
|||
- model: extras.graph
|
||||
pk: 1
|
||||
fields: {type: 300, weight: 1000, name: Site Test Graph, source: 'http://localhost/na.png',
|
||||
link: ''}
|
||||
- model: extras.graph
|
||||
pk: 2
|
||||
fields: {type: 200, weight: 1000, name: Provider Test Graph, source: 'http://localhost/provider_graph.png',
|
||||
link: ''}
|
||||
- model: extras.graph
|
||||
pk: 3
|
||||
fields: {type: 100, weight: 1000, name: Interface Test Graph, source: 'http://localhost/interface_graph.png',
|
||||
link: ''}
|
|
@ -0,0 +1,117 @@
|
|||
from Exscript.protocols.Exception import LoginFailure
|
||||
from getpass import getpass
|
||||
from ncclient.transport.errors import AuthenticationError
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
from dcim.models import Device, Module, Site
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Update inventory information for specified devices"
|
||||
username = settings.NETBOX_USERNAME
|
||||
password = settings.NETBOX_PASSWORD
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('-u', '--username', dest='username', help="Specify the username to use")
|
||||
parser.add_argument('-p', '--password', action='store_true', default=False, help="Prompt for password to use")
|
||||
parser.add_argument('-s', '--site', dest='site', action='append', help="Filter devices by site (include argument once per site)")
|
||||
parser.add_argument('-n', '--name', dest='name', help="Filter devices by name (regular expression)")
|
||||
parser.add_argument('--full', action='store_true', default=False, help="For inventory update for all devices")
|
||||
parser.add_argument('--fake', action='store_true', default=False, help="Do not actually update database")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
# Credentials
|
||||
if options['username']:
|
||||
self.username = options['username']
|
||||
if options['password']:
|
||||
self.password = getpass("Password: ")
|
||||
|
||||
device_list = Device.objects.filter()
|
||||
|
||||
# --site: Include only devices belonging to specified site(s)
|
||||
if options['site']:
|
||||
sites = Site.objects.filter(slug__in=options['site'])
|
||||
if sites:
|
||||
site_names = [s.name for s in sites]
|
||||
self.stdout.write("Running inventory for these sites: {}".format(', '.join(site_names)))
|
||||
else:
|
||||
raise CommandError("One or more sites specified but none found.")
|
||||
device_list = device_list.filter(rack__site__in=sites)
|
||||
|
||||
# --name: Filter devices by name matching a regex
|
||||
if options['name']:
|
||||
device_list = device_list.filter(name__iregex=options['name'])
|
||||
|
||||
# --full: Gather inventory data for *all* devices
|
||||
if options['full']:
|
||||
self.stdout.write("WARNING: Running inventory for all devices! Prior data will be overwritten. (--full)")
|
||||
|
||||
# --fake: Gathering data but not updating the database
|
||||
if options['fake']:
|
||||
self.stdout.write("WARNING: Inventory data will not be saved! (--fake)")
|
||||
|
||||
device_count = device_list.count()
|
||||
self.stdout.write("** Found {} devices...".format(device_count))
|
||||
|
||||
for i, device in enumerate(device_list, start=1):
|
||||
|
||||
self.stdout.write("[{}/{}] {}: ".format(i, device_count, device.name), ending='')
|
||||
|
||||
# Skip inactive devices
|
||||
if not device.status:
|
||||
self.stdout.write("Skipped (inactive)")
|
||||
continue
|
||||
|
||||
# Skip devices without primary_ip set
|
||||
if not device.primary_ip:
|
||||
self.stdout.write("Skipped (no primary IP set)")
|
||||
continue
|
||||
|
||||
# Skip devices which have already been inventoried if not doing a full update
|
||||
if device.serial and not options['full']:
|
||||
self.stdout.write("Skipped (Serial: {})".format(device.serial))
|
||||
continue
|
||||
|
||||
RPC = device.get_rpc_client()
|
||||
if not RPC:
|
||||
self.stdout.write("Skipped (no RPC client available for platform {})".format(device.platform))
|
||||
continue
|
||||
|
||||
# Connect to device and retrieve inventory info
|
||||
try:
|
||||
with RPC(device, self.username, self.password) as rpc_client:
|
||||
inventory = rpc_client.get_inventory()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except (AuthenticationError, LoginFailure):
|
||||
self.stdout.write("Authentication error!")
|
||||
continue
|
||||
except Exception as e:
|
||||
self.stdout.write("Error for {} ({}): {}".format(device, device.primary_ip.address.ip, e))
|
||||
continue
|
||||
|
||||
self.stdout.write("")
|
||||
self.stdout.write("\tSerial: {}".format(inventory['chassis']['serial']))
|
||||
self.stdout.write("\tDescription: {}".format(inventory['chassis']['description']))
|
||||
for module in inventory['modules']:
|
||||
self.stdout.write("\tModule: {} / {} ({})".format(module['name'], module['part_id'], module['serial']))
|
||||
|
||||
if not options['fake']:
|
||||
with transaction.atomic():
|
||||
if inventory['chassis']['serial']:
|
||||
device.serial = inventory['chassis']['serial']
|
||||
device.save()
|
||||
Module.objects.filter(device=device).delete()
|
||||
modules = []
|
||||
for module in inventory['modules']:
|
||||
modules.append(Module(device=device,
|
||||
name=module['name'],
|
||||
part_id=module['part_id'],
|
||||
serial=module['serial']))
|
||||
Module.objects.bulk_create(modules)
|
||||
|
||||
self.stdout.write("Finished!")
|
|
@ -0,0 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.1 on 2016-02-27 02:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ExportTemplate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('template_code', models.TextField()),
|
||||
('mime_type', models.CharField(blank=True, max_length=15)),
|
||||
('file_extension', models.CharField(blank=True, max_length=15)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['content_type', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Graph',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(100, b'Interface'), (200, b'Provider'), (300, b'Site')])),
|
||||
('weight', models.PositiveSmallIntegerField(default=1000)),
|
||||
('name', models.CharField(max_length=100, verbose_name=b'Name')),
|
||||
('source', models.CharField(max_length=500, verbose_name=b'Source URL')),
|
||||
('link', models.URLField(blank=True, verbose_name=b'Link URL')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['type', 'weight', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='exporttemplate',
|
||||
unique_together=set([('content_type', 'name')]),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,70 @@
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context
|
||||
|
||||
|
||||
GRAPH_TYPE_INTERFACE = 100
|
||||
GRAPH_TYPE_PROVIDER = 200
|
||||
GRAPH_TYPE_SITE = 300
|
||||
GRAPH_TYPE_CHOICES = (
|
||||
(GRAPH_TYPE_INTERFACE, 'Interface'),
|
||||
(GRAPH_TYPE_PROVIDER, 'Provider'),
|
||||
(GRAPH_TYPE_SITE, 'Site'),
|
||||
)
|
||||
|
||||
EXPORTTEMPLATE_MODELS = [
|
||||
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection',
|
||||
'aggregate', 'prefix', 'ipaddress', 'vlan',
|
||||
'provider', 'circuit'
|
||||
]
|
||||
|
||||
|
||||
class Graph(models.Model):
|
||||
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
|
||||
weight = models.PositiveSmallIntegerField(default=1000)
|
||||
name = models.CharField(max_length=100, verbose_name='Name')
|
||||
source = models.CharField(max_length=500, verbose_name='Source URL')
|
||||
link = models.URLField(verbose_name='Link URL', blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['type', 'weight', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def embed_url(self, obj):
|
||||
template = Template(self.source)
|
||||
return template.render(Context({'obj': obj}))
|
||||
|
||||
|
||||
class ExportTemplate(models.Model):
|
||||
content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS})
|
||||
name = models.CharField(max_length=200)
|
||||
template_code = models.TextField()
|
||||
mime_type = models.CharField(max_length=15, blank=True)
|
||||
file_extension = models.CharField(max_length=15, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['content_type', 'name']
|
||||
unique_together = [
|
||||
['content_type', 'name']
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
return "{}: {}".format(self.content_type, self.name)
|
||||
|
||||
def to_response(self, context_dict, filename):
|
||||
"""
|
||||
Render the template to an HTTP response, delivered as a named file attachment
|
||||
"""
|
||||
template = Template(self.template_code)
|
||||
mime_type = 'text/plain' if not self.mime_type else self.mime_type
|
||||
response = HttpResponse(
|
||||
template.render(Context(context_dict)),
|
||||
content_type=mime_type
|
||||
)
|
||||
if self.file_extension:
|
||||
filename += '.{}'.format(self.file_extension)
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
return response
|
|
@ -0,0 +1,247 @@
|
|||
from Exscript import Account
|
||||
from Exscript.protocols import SSH2
|
||||
from ncclient import manager
|
||||
import paramiko
|
||||
import re
|
||||
import xmltodict
|
||||
|
||||
|
||||
CONNECT_TIMEOUT = 5 # seconds
|
||||
|
||||
|
||||
class RPCClient(object):
|
||||
|
||||
def __init__(self, device, username='', password=''):
|
||||
self.username = username
|
||||
self.password = password
|
||||
try:
|
||||
self.host = str(device.primary_ip.address.ip)
|
||||
except AttributeError:
|
||||
raise Exception("Specified device ({}) does not have a primary IP defined.".format(device))
|
||||
|
||||
def get_lldp_neighbors(self):
|
||||
"""
|
||||
Returns a list of dictionaries, each representing an LLDP neighbor adjacency.
|
||||
|
||||
{
|
||||
'local-interface': <str>,
|
||||
'name': <str>,
|
||||
'remote-interface': <str>,
|
||||
'chassis-id': <str>,
|
||||
}
|
||||
"""
|
||||
raise NotImplementedError("Feature not implemented for this platform.")
|
||||
|
||||
def get_inventory(self):
|
||||
"""
|
||||
Returns a dictionary representing the device chassis and installed modules.
|
||||
|
||||
{
|
||||
'chassis': {
|
||||
'serial': <str>,
|
||||
'description': <str>,
|
||||
}
|
||||
'modules': [
|
||||
{
|
||||
'name': <str>,
|
||||
'part_id': <str>,
|
||||
'serial': <str>,
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
raise NotImplementedError("Feature not implemented for this platform.")
|
||||
|
||||
|
||||
class JunosNC(RPCClient):
|
||||
"""
|
||||
NETCONF client for Juniper Junos devices
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
# Initiate a connection to the device
|
||||
self.manager = manager.connect(host=self.host, username=self.username, password=self.password,
|
||||
hostkey_verify=False, timeout=CONNECT_TIMEOUT)
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
|
||||
# Close the connection to the device
|
||||
self.manager.close_session()
|
||||
|
||||
def get_lldp_neighbors(self):
|
||||
|
||||
rpc_reply = self.manager.dispatch('get-lldp-neighbors-information')
|
||||
lldp_neighbors_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['lldp-neighbors-information']['lldp-neighbor-information']
|
||||
|
||||
result = []
|
||||
for neighbor_raw in lldp_neighbors_raw:
|
||||
neighbor = dict()
|
||||
neighbor['local-interface'] = neighbor_raw.get('lldp-local-port-id')
|
||||
neighbor['name'] = neighbor_raw.get('lldp-remote-system-name')
|
||||
neighbor['name'] = neighbor['name'].split('.')[0] # Split hostname from domain if one is present
|
||||
try:
|
||||
neighbor['remote-interface'] = neighbor_raw['lldp-remote-port-description']
|
||||
except KeyError:
|
||||
# Older versions of Junos report on interface ID instead of description
|
||||
neighbor['remote-interface'] = neighbor_raw.get('lldp-remote-port-id')
|
||||
neighbor['chassis-id'] = neighbor_raw.get('lldp-remote-chassis-id')
|
||||
result.append(neighbor)
|
||||
|
||||
return result
|
||||
|
||||
def get_inventory(self):
|
||||
|
||||
rpc_reply = self.manager.dispatch('get-chassis-inventory')
|
||||
inventory_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['chassis-inventory']['chassis']
|
||||
|
||||
result = dict()
|
||||
|
||||
# Gather chassis data
|
||||
result['chassis'] = {
|
||||
'serial': inventory_raw['serial-number'],
|
||||
'description': inventory_raw['description'],
|
||||
}
|
||||
|
||||
# Gather modules
|
||||
result['modules'] = []
|
||||
for module in inventory_raw['chassis-module']:
|
||||
try:
|
||||
# Skip built-in modules
|
||||
if module['name'] and module['serial-number'] != inventory_raw['serial-number']:
|
||||
result['modules'].append({
|
||||
'name': module['name'],
|
||||
'part_id': module['model-number'] or '',
|
||||
'serial': module['serial-number'] or '',
|
||||
})
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class IOSSSH(RPCClient):
|
||||
"""
|
||||
SSH client for Cisco IOS devices
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
# Initiate a connection to the device
|
||||
self.ssh = SSH2(connect_timeout=CONNECT_TIMEOUT)
|
||||
self.ssh.connect(self.host)
|
||||
self.ssh.login(Account(self.username, self.password))
|
||||
|
||||
# Disable terminal paging
|
||||
self.ssh.execute("terminal length 0")
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
|
||||
# Close the connection to the device
|
||||
self.ssh.send("exit\r")
|
||||
self.ssh.close()
|
||||
|
||||
def get_inventory(self):
|
||||
|
||||
result = dict()
|
||||
|
||||
# Gather chassis data
|
||||
try:
|
||||
self.ssh.execute("show version")
|
||||
show_version = self.ssh.response
|
||||
serial = re.search("Processor board ID ([^\s]+)", show_version).groups()[0]
|
||||
description = re.search("\r\n\r\ncisco ([^\s]+)", show_version).groups()[0]
|
||||
except:
|
||||
raise RuntimeError("Failed to glean chassis info from device.")
|
||||
result['chassis'] = {
|
||||
'serial': serial,
|
||||
'description': description,
|
||||
}
|
||||
|
||||
# Gather modules
|
||||
result['modules'] = []
|
||||
try:
|
||||
self.ssh.execute("show inventory")
|
||||
show_inventory = self.ssh.response
|
||||
# Split modules on double line
|
||||
modules_raw = show_inventory.strip().split('\r\n\r\n')
|
||||
for module_raw in modules_raw:
|
||||
try:
|
||||
m_name = re.search('NAME: "([^"]+)"', module_raw).group(1)
|
||||
m_pid = re.search('PID: ([^\s]+)', module_raw).group(1)
|
||||
m_serial = re.search('SN: ([^\s]+)', module_raw).group(1)
|
||||
# Omit built-in modules and those with no PID
|
||||
if m_serial != result['chassis']['serial'] and m_pid.lower() != 'unspecified':
|
||||
result['modules'].append({
|
||||
'name': m_name,
|
||||
'part_id': m_pid,
|
||||
'serial': m_serial,
|
||||
})
|
||||
except AttributeError:
|
||||
continue
|
||||
except:
|
||||
raise RuntimeError("Failed to glean module info from device.")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class OpengearSSH(RPCClient):
|
||||
"""
|
||||
SSH client for Opengear devices
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
# Initiate a connection to the device
|
||||
self.ssh = paramiko.SSHClient()
|
||||
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
self.ssh.connect(self.host, username=self.username, password=self.password, timeout=CONNECT_TIMEOUT)
|
||||
except paramiko.AuthenticationException:
|
||||
# Try default Opengear credentials if the configured creds don't work
|
||||
self.ssh.connect(self.host, username='root', password='default')
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
|
||||
# Close the connection to the device
|
||||
self.ssh.close()
|
||||
|
||||
def get_inventory(self):
|
||||
|
||||
try:
|
||||
stdin, stdout, stderr = self.ssh.exec_command("showserial")
|
||||
serial = stdout.readlines()[0].strip()
|
||||
except:
|
||||
raise RuntimeError("Failed to glean chassis serial from device.")
|
||||
# Older models don't provide serial info
|
||||
if serial == "No serial number information available":
|
||||
serial = ''
|
||||
|
||||
try:
|
||||
stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model")
|
||||
description = stdout.readlines()[0].split(' ', 1)[1].strip()
|
||||
except:
|
||||
raise RuntimeError("Failed to glean chassis description from device.")
|
||||
|
||||
return {
|
||||
'chassis': {
|
||||
'serial': serial,
|
||||
'description': description,
|
||||
},
|
||||
'modules': [],
|
||||
}
|
||||
|
||||
|
||||
# For mapping platform -> NC client
|
||||
RPC_CLIENTS = {
|
||||
'juniper-junos': JunosNC,
|
||||
'cisco-ios': IOSSSH,
|
||||
'opengear': OpengearSSH,
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
|
@ -0,0 +1,2 @@
|
|||
default_app_config = 'ipam.apps.IPAMConfig'
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import *
|
||||
|
||||
|
||||
@admin.register(VRF)
|
||||
class VRFAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'rd']
|
||||
|
||||
|
||||
@admin.register(Status)
|
||||
class StatusAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'weight', 'bootstrap_class']
|
||||
|
||||
|
||||
@admin.register(Role)
|
||||
class RoleAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug', 'weight']
|
||||
|
||||
|
||||
@admin.register(RIR)
|
||||
class RIRAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
list_display = ['name', 'slug']
|
||||
|
||||
|
||||
@admin.register(Aggregate)
|
||||
class AggregateAdmin(admin.ModelAdmin):
|
||||
list_display = ['prefix', 'rir', 'date_added']
|
||||
list_filter = ['family', 'rir']
|
||||
search_fields = ['prefix']
|
||||
|
||||
|
||||
@admin.register(Prefix)
|
||||
class PrefixAdmin(admin.ModelAdmin):
|
||||
list_display = ['prefix', 'vrf', 'site', 'status', 'role', 'vlan']
|
||||
list_filter = ['family', 'site', 'status', 'role']
|
||||
search_fields = ['prefix']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(PrefixAdmin, self).get_queryset(request)
|
||||
return qs.select_related('vrf', 'site', 'status', 'role', 'vlan')
|
||||
|
||||
|
||||
@admin.register(IPAddress)
|
||||
class IPAddressAdmin(admin.ModelAdmin):
|
||||
list_display = ['address', 'vrf', 'nat_inside']
|
||||
list_filter = ['family']
|
||||
fields = ['address', 'vrf', 'device', 'interface', 'nat_inside']
|
||||
readonly_fields = ['interface', 'device', 'nat_inside']
|
||||
search_fields = ['address']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(IPAddressAdmin, self).get_queryset(request)
|
||||
return qs.select_related('vrf', 'nat_inside')
|
||||
|
||||
|
||||
@admin.register(VLAN)
|
||||
class VLANAdmin(admin.ModelAdmin):
|
||||
list_display = ['site', 'vid', 'name', 'status', 'role']
|
||||
list_filter = ['site', 'status', 'role']
|
||||
search_fields = ['vid', 'name']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(VLANAdmin, self).get_queryset(request)
|
||||
return qs.select_related('site', 'status', 'role')
|
|
@ -0,0 +1,158 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||
from ipam.models import VRF, Status, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
|
||||
class VRFSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['id', 'name', 'rd', 'description']
|
||||
|
||||
|
||||
class VRFNestedSerializer(VRFSerializer):
|
||||
|
||||
class Meta(VRFSerializer.Meta):
|
||||
fields = ['id', 'name', 'rd']
|
||||
|
||||
|
||||
#
|
||||
# Statuses
|
||||
#
|
||||
|
||||
class StatusSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Status
|
||||
fields = ['id', 'name', 'slug', 'weight', 'bootstrap_class']
|
||||
|
||||
|
||||
class StatusNestedSerializer(StatusSerializer):
|
||||
|
||||
class Meta(StatusSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Roles
|
||||
#
|
||||
|
||||
class RoleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ['id', 'name', 'slug', 'weight']
|
||||
|
||||
|
||||
class RoleNestedSerializer(RoleSerializer):
|
||||
|
||||
class Meta(RoleSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# RIRs
|
||||
#
|
||||
|
||||
class RIRSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class RIRNestedSerializer(RIRSerializer):
|
||||
|
||||
class Meta(RIRSerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# Aggregates
|
||||
#
|
||||
|
||||
class AggregateSerializer(serializers.ModelSerializer):
|
||||
rir = RIRNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description']
|
||||
|
||||
|
||||
class AggregateNestedSerializer(AggregateSerializer):
|
||||
|
||||
class Meta(AggregateSerializer.Meta):
|
||||
fields = ['id', 'family', 'prefix']
|
||||
|
||||
|
||||
#
|
||||
# VLANs
|
||||
#
|
||||
|
||||
class VLANSerializer(serializers.ModelSerializer):
|
||||
display_name = serializers.SerializerMethodField()
|
||||
site = SiteNestedSerializer()
|
||||
status = StatusNestedSerializer()
|
||||
role = RoleNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['id', 'site', 'vid', 'name', 'status', 'role', 'display_name']
|
||||
|
||||
def get_display_name(self, obj):
|
||||
return "{} ({})".format(obj.vid, obj.name)
|
||||
|
||||
|
||||
class VLANNestedSerializer(VLANSerializer):
|
||||
|
||||
class Meta(VLANSerializer.Meta):
|
||||
fields = ['id', 'vid', 'name', 'display_name']
|
||||
|
||||
|
||||
#
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
class PrefixSerializer(serializers.ModelSerializer):
|
||||
site = SiteNestedSerializer()
|
||||
vrf = VRFNestedSerializer()
|
||||
vlan = VLANNestedSerializer()
|
||||
status = StatusNestedSerializer()
|
||||
role = RoleNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'vlan', 'status', 'role', 'description']
|
||||
|
||||
|
||||
class PrefixNestedSerializer(PrefixSerializer):
|
||||
|
||||
class Meta(PrefixSerializer.Meta):
|
||||
fields = ['id', 'family', 'prefix']
|
||||
|
||||
|
||||
#
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressSerializer(serializers.ModelSerializer):
|
||||
vrf = VRFNestedSerializer()
|
||||
interface = InterfaceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'family', 'address', 'vrf', 'interface', 'description', 'nat_inside', 'nat_outside']
|
||||
|
||||
|
||||
class IPAddressNestedSerializer(IPAddressSerializer):
|
||||
|
||||
class Meta(IPAddressSerializer.Meta):
|
||||
fields = ['id', 'family', 'address']
|
||||
|
||||
IPAddressSerializer._declared_fields['nat_inside'] = IPAddressNestedSerializer()
|
||||
IPAddressSerializer._declared_fields['nat_outside'] = IPAddressNestedSerializer()
|
|
@ -0,0 +1,40 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from .views import *
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# VRFs
|
||||
url(r'^vrfs/$', VRFListView.as_view(), name='vrf_list'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/$', VRFDetailView.as_view(), name='vrf_detail'),
|
||||
|
||||
# Statuses
|
||||
url(r'^statuses/$', StatusListView.as_view(), name='status_list'),
|
||||
url(r'^statuses/(?P<pk>\d+)/$', StatusDetailView.as_view(), name='status_detail'),
|
||||
|
||||
# Roles
|
||||
url(r'^roles/$', RoleListView.as_view(), name='role_list'),
|
||||
url(r'^roles/(?P<pk>\d+)/$', RoleDetailView.as_view(), name='role_detail'),
|
||||
|
||||
# RIRs
|
||||
url(r'^rirs/$', RIRListView.as_view(), name='rir_list'),
|
||||
url(r'^rirs/(?P<pk>\d+)/$', RIRDetailView.as_view(), name='rir_detail'),
|
||||
|
||||
# Aggregates
|
||||
url(r'^aggregates/$', AggregateListView.as_view(), name='aggregate_list'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/$', AggregateDetailView.as_view(), name='aggregate_detail'),
|
||||
|
||||
# Prefixes
|
||||
url(r'^prefixes/$', PrefixListView.as_view(), name='prefix_list'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/$', PrefixDetailView.as_view(), name='prefix_detail'),
|
||||
|
||||
# IP addresses
|
||||
url(r'^ip-addresses/$', IPAddressListView.as_view(), name='ipaddress_list'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/$', IPAddressDetailView.as_view(), name='ipaddress_detail'),
|
||||
|
||||
# VLANs
|
||||
url(r'^vlans/$', VLANListView.as_view(), name='vlan_list'),
|
||||
url(r'^vlans/(?P<pk>\d+)/$', VLANDetailView.as_view(), name='vlan_detail'),
|
||||
|
||||
]
|
|
@ -0,0 +1,140 @@
|
|||
from rest_framework import generics
|
||||
|
||||
from ipam.models import VRF, Status, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
|
||||
from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter
|
||||
from .serializers import VRFSerializer, StatusSerializer, RoleSerializer, RIRSerializer, AggregateSerializer, \
|
||||
PrefixSerializer, IPAddressSerializer, VLANSerializer
|
||||
|
||||
|
||||
class VRFListView(generics.ListAPIView):
|
||||
"""
|
||||
List all VRFs
|
||||
"""
|
||||
queryset = VRF.objects.all()
|
||||
serializer_class = VRFSerializer
|
||||
|
||||
|
||||
class VRFDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single VRF
|
||||
"""
|
||||
queryset = VRF.objects.all()
|
||||
serializer_class = VRFSerializer
|
||||
|
||||
|
||||
class StatusListView(generics.ListAPIView):
|
||||
"""
|
||||
List all statuses
|
||||
"""
|
||||
queryset = Status.objects.all()
|
||||
serializer_class = StatusSerializer
|
||||
|
||||
|
||||
class StatusDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single status
|
||||
"""
|
||||
queryset = Status.objects.all()
|
||||
serializer_class = StatusSerializer
|
||||
|
||||
|
||||
class RoleListView(generics.ListAPIView):
|
||||
"""
|
||||
List all roles
|
||||
"""
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
|
||||
|
||||
class RoleDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single role
|
||||
"""
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
|
||||
|
||||
class RIRListView(generics.ListAPIView):
|
||||
"""
|
||||
List all RIRs
|
||||
"""
|
||||
queryset = RIR.objects.all()
|
||||
serializer_class = RIRSerializer
|
||||
|
||||
|
||||
class RIRDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single RIR
|
||||
"""
|
||||
queryset = RIR.objects.all()
|
||||
serializer_class = RIRSerializer
|
||||
|
||||
|
||||
class AggregateListView(generics.ListAPIView):
|
||||
"""
|
||||
List aggregates (filterable)
|
||||
"""
|
||||
queryset = Aggregate.objects.select_related('rir')
|
||||
serializer_class = AggregateSerializer
|
||||
filter_class = AggregateFilter
|
||||
|
||||
|
||||
class AggregateDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single aggregate
|
||||
"""
|
||||
queryset = Aggregate.objects.select_related('rir')
|
||||
serializer_class = AggregateSerializer
|
||||
|
||||
|
||||
class PrefixListView(generics.ListAPIView):
|
||||
"""
|
||||
List prefixes (filterable)
|
||||
"""
|
||||
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'status', 'role')
|
||||
serializer_class = PrefixSerializer
|
||||
filter_class = PrefixFilter
|
||||
|
||||
|
||||
class PrefixDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single prefix
|
||||
"""
|
||||
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'status', 'role')
|
||||
serializer_class = PrefixSerializer
|
||||
|
||||
|
||||
class IPAddressListView(generics.ListAPIView):
|
||||
"""
|
||||
List IP addresses (filterable)
|
||||
"""
|
||||
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
|
||||
.prefetch_related('nat_outside')
|
||||
serializer_class = IPAddressSerializer
|
||||
filter_class = IPAddressFilter
|
||||
|
||||
|
||||
class IPAddressDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single IP address
|
||||
"""
|
||||
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
|
||||
.prefetch_related('nat_outside')
|
||||
serializer_class = IPAddressSerializer
|
||||
|
||||
|
||||
class VLANListView(generics.ListAPIView):
|
||||
"""
|
||||
List VLANs (filterable)
|
||||
"""
|
||||
queryset = VLAN.objects.select_related('site', 'status', 'role')
|
||||
serializer_class = VLANSerializer
|
||||
filter_class = VLANFilter
|
||||
|
||||
|
||||
class VLANDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single VLAN
|
||||
"""
|
||||
queryset = VLAN.objects.select_related('site', 'status', 'role')
|
||||
serializer_class = VLANSerializer
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class IPAMConfig(AppConfig):
|
||||
name = "ipam"
|
||||
verbose_name = "IPAM"
|
|
@ -0,0 +1,82 @@
|
|||
from netaddr import IPNetwork
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.six import with_metaclass
|
||||
|
||||
from .formfields import IPFormField
|
||||
from .lookups import EndsWith, IEndsWith, StartsWith, IStartsWith, Regex, IRegex, NetContained, NetContainedOrEqual, \
|
||||
NetContains, NetContainsOrEquals, NetHost
|
||||
|
||||
|
||||
class _BaseIPField(models.Field):
|
||||
|
||||
def python_type(self):
|
||||
return IPNetwork
|
||||
|
||||
def to_python(self, value):
|
||||
if not value:
|
||||
return value
|
||||
try:
|
||||
return IPNetwork(value)
|
||||
except ValueError as e:
|
||||
raise ValidationError(e)
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return str(self.to_python(value))
|
||||
|
||||
def form_class(self):
|
||||
return IPFormField
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': self.form_class()}
|
||||
defaults.update(kwargs)
|
||||
return super(_BaseIPField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class IPNetworkField(with_metaclass(models.SubfieldBase, _BaseIPField)):
|
||||
"""
|
||||
IP prefix (network and mask)
|
||||
"""
|
||||
description = "PostgreSQL CIDR field"
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'cidr'
|
||||
|
||||
|
||||
IPNetworkField.register_lookup(EndsWith)
|
||||
IPNetworkField.register_lookup(IEndsWith)
|
||||
IPNetworkField.register_lookup(StartsWith)
|
||||
IPNetworkField.register_lookup(IStartsWith)
|
||||
IPNetworkField.register_lookup(Regex)
|
||||
IPNetworkField.register_lookup(IRegex)
|
||||
IPNetworkField.register_lookup(NetContained)
|
||||
IPNetworkField.register_lookup(NetContainedOrEqual)
|
||||
IPNetworkField.register_lookup(NetContains)
|
||||
IPNetworkField.register_lookup(NetContainsOrEquals)
|
||||
IPNetworkField.register_lookup(NetHost)
|
||||
|
||||
|
||||
class IPAddressField(with_metaclass(models.SubfieldBase, _BaseIPField)):
|
||||
"""
|
||||
IP address (host address and mask)
|
||||
"""
|
||||
description = "PostgreSQL INET field"
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'inet'
|
||||
|
||||
|
||||
IPAddressField.register_lookup(EndsWith)
|
||||
IPAddressField.register_lookup(IEndsWith)
|
||||
IPAddressField.register_lookup(StartsWith)
|
||||
IPAddressField.register_lookup(IStartsWith)
|
||||
IPAddressField.register_lookup(Regex)
|
||||
IPAddressField.register_lookup(IRegex)
|
||||
IPAddressField.register_lookup(NetContained)
|
||||
IPAddressField.register_lookup(NetContainedOrEqual)
|
||||
IPAddressField.register_lookup(NetContains)
|
||||
IPAddressField.register_lookup(NetContainsOrEquals)
|
||||
IPAddressField.register_lookup(NetHost)
|
|
@ -0,0 +1,216 @@
|
|||
import django_filters
|
||||
from netaddr import IPNetwork
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from dcim.models import Site, Device, Interface
|
||||
from ipam.models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, Status, Role
|
||||
|
||||
|
||||
class VRFFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(
|
||||
name='name',
|
||||
lookup_type='icontains',
|
||||
label='Name',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['name', 'rd']
|
||||
|
||||
|
||||
class AggregateFilter(django_filters.FilterSet):
|
||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rir',
|
||||
queryset=RIR.objects.all(),
|
||||
label='RIR (ID)',
|
||||
)
|
||||
rir = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rir',
|
||||
queryset=RIR.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='RIR (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = ['family', 'rir_id', 'rir', 'date_added']
|
||||
|
||||
|
||||
class PrefixFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
parent = django_filters.MethodFilter(
|
||||
action='search_by_parent',
|
||||
label='Parent prefix',
|
||||
)
|
||||
vrf_id = django_filters.MethodFilter(
|
||||
action='vrf',
|
||||
label='VRF (ID)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
vlan_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='vlan',
|
||||
queryset=VLAN.objects.all(),
|
||||
label='VLAN (ID)',
|
||||
)
|
||||
vlan_vid = django_filters.NumberFilter(
|
||||
name='vlan__vid',
|
||||
label='VLAN number (1-4095)',
|
||||
)
|
||||
status_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='status',
|
||||
queryset=Status.objects.all(),
|
||||
label='Status (ID)',
|
||||
)
|
||||
status = django_filters.ModelMultipleChoiceFilter(
|
||||
name='status',
|
||||
queryset=Status.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Status (slug)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=Role.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=Role.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['family', 'site_id', 'site', 'vrf_id', 'vrf', 'vlan_id', 'vlan_vid', 'status_id', 'status', 'role_id',
|
||||
'role']
|
||||
|
||||
def search(self, queryset, value):
|
||||
value = value.strip()
|
||||
try:
|
||||
query = str(IPNetwork(value).cidr)
|
||||
return queryset.filter(prefix__net_contains_or_equals=query)
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
def search_by_parent(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
query = str(IPNetwork(value).cidr)
|
||||
return queryset.filter(prefix__net_contained_or_equal=query)
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
def vrf(self, queryset, value):
|
||||
if str(value) == '':
|
||||
return queryset
|
||||
try:
|
||||
vrf_id = int(value)
|
||||
except ValueError:
|
||||
return queryset.none()
|
||||
if vrf_id == 0:
|
||||
return queryset.filter(vrf__isnull=True)
|
||||
return queryset.filter(vrf__pk=value)
|
||||
|
||||
|
||||
class IPAddressFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='vrf',
|
||||
queryset=VRF.objects.all(),
|
||||
label='VRF (ID)',
|
||||
)
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='interface__device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='interface__device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
)
|
||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='interface',
|
||||
queryset=Interface.objects.all(),
|
||||
label='Interface (ID)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['q', 'family', 'vrf_id', 'vrf', 'device_id', 'device', 'interface_id']
|
||||
|
||||
def search(self, queryset, value):
|
||||
value = value.strip()
|
||||
try:
|
||||
query = str(IPNetwork(value))
|
||||
return queryset.filter(address__net_host=query)
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class VLANFilter(django_filters.FilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
name = django_filters.CharFilter(
|
||||
name='name',
|
||||
lookup_type='icontains',
|
||||
label='Name',
|
||||
)
|
||||
vid = django_filters.NumberFilter(
|
||||
name='vid',
|
||||
label='VLAN number (1-4095)',
|
||||
)
|
||||
status_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='status',
|
||||
queryset=Status.objects.all(),
|
||||
label='Status (ID)',
|
||||
)
|
||||
status = django_filters.ModelMultipleChoiceFilter(
|
||||
name='status',
|
||||
queryset=Status.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Status (slug)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=Role.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=Role.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site_id', 'site', 'vid', 'name', 'status_id', 'status', 'role_id', 'role']
|
|
@ -0,0 +1,98 @@
|
|||
- model: ipam.status
|
||||
pk: 1
|
||||
fields: {name: Active, slug: active, weight: 1000, bootstrap_class: 1}
|
||||
- model: ipam.status
|
||||
pk: 2
|
||||
fields: {name: Inactive, slug: inactive, weight: 500, bootstrap_class: 3}
|
||||
- model: ipam.role
|
||||
pk: 1
|
||||
fields: {name: Lab Network, slug: lab-network, weight: 1000}
|
||||
- model: ipam.rir
|
||||
pk: 1
|
||||
fields: {name: RFC1918, slug: rfc1918}
|
||||
- model: ipam.aggregate
|
||||
pk: 1
|
||||
fields: {family: 4, prefix: 10.0.0.0/8, rir: 1, date_added: 2016-01-01, description: ''}
|
||||
- model: ipam.prefix
|
||||
pk: 1
|
||||
fields: {family: 4, prefix: 10.1.1.0/24, site: 1, vrf: null, vlan: null, status: 1,
|
||||
role: 1, description: ''}
|
||||
- model: ipam.prefix
|
||||
pk: 2
|
||||
fields: {family: 4, prefix: 10.0.255.0/24, site: 1, vrf: null, vlan: null, status: 1,
|
||||
role: 1, description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 1
|
||||
fields: {family: 4, address: 10.0.255.1/32, vrf: null, interface: 3, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 2
|
||||
fields: {family: 4, address: 169.254.254.1/31, vrf: null, interface: 4, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 3
|
||||
fields: {family: 4, address: 10.0.255.2/32, vrf: null, interface: 185, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 4
|
||||
fields: {family: 4, address: 169.254.1.1/31, vrf: null, interface: 213, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 5
|
||||
fields: {family: 4, address: 10.0.254.1/24, vrf: null, interface: 12, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 8
|
||||
fields: {family: 4, address: 10.15.21.1/31, vrf: null, interface: 218, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 9
|
||||
fields: {family: 4, address: 10.15.21.2/31, vrf: null, interface: 9, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 10
|
||||
fields: {family: 4, address: 10.15.22.1/31, vrf: null, interface: 8, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 11
|
||||
fields: {family: 4, address: 10.15.20.1/31, vrf: null, interface: 7, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 12
|
||||
fields: {family: 4, address: 10.16.20.1/31, vrf: null, interface: 216, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 13
|
||||
fields: {family: 4, address: 10.15.22.2/31, vrf: null, interface: 206, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 14
|
||||
fields: {family: 4, address: 10.16.22.1/31, vrf: null, interface: 217, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 15
|
||||
fields: {family: 4, address: 10.16.22.2/31, vrf: null, interface: 205, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 16
|
||||
fields: {family: 4, address: 10.16.20.2/31, vrf: null, interface: 211, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 17
|
||||
fields: {family: 4, address: 10.15.22.2/31, vrf: null, interface: 212, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 19
|
||||
fields: {family: 4, address: 10.0.254.2/32, vrf: null, interface: 188, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 20
|
||||
fields: {family: 4, address: 169.254.1.1/31, vrf: null, interface: 200, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.ipaddress
|
||||
pk: 21
|
||||
fields: {family: 4, address: 169.254.1.2/31, vrf: null, interface: 194, nat_inside: null,
|
||||
description: ''}
|
||||
- model: ipam.vlan
|
||||
pk: 1
|
||||
fields: {site: 1, vid: 999, name: TEST, status: 1, role: 1}
|
|
@ -0,0 +1,30 @@
|
|||
from netaddr import IPNetwork, AddrFormatError
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
#
|
||||
# Form fields
|
||||
#
|
||||
|
||||
class IPFormField(forms.Field):
|
||||
default_error_messages = {
|
||||
'invalid': "Enter a valid IPv4 or IPv6 address (with CIDR mask).",
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
if not value:
|
||||
return None
|
||||
|
||||
if isinstance(value, IPNetwork):
|
||||
return value
|
||||
|
||||
# Ensure that a subnet mask has been specified. This prevents IPs from defaulting to a /32 or /128.
|
||||
if len(value.split('/')) != 2:
|
||||
raise ValidationError('CIDR mask (e.g. /24) is required.')
|
||||
|
||||
try:
|
||||
return IPNetwork(value)
|
||||
except AddrFormatError:
|
||||
raise ValidationError("Please specify a valid IPv4 or IPv6 address.")
|
|
@ -0,0 +1,408 @@
|
|||
from netaddr import IPNetwork
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface
|
||||
from utilities.forms import BootstrapMixin, ConfirmationForm, APISelect, Livesearch, CSVDataField, BulkImportForm
|
||||
from .models import VRF, RIR, Aggregate, Prefix, IPAddress, VLAN, Status, Role
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
|
||||
class VRFForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['name', 'rd', 'description']
|
||||
help_texts = {
|
||||
'rd': "Route distinguisher in any format",
|
||||
}
|
||||
|
||||
|
||||
class VRFFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['name', 'rd', 'description']
|
||||
|
||||
|
||||
class VRFImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=VRFFromCSVForm)
|
||||
|
||||
|
||||
class VRFBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
|
||||
class VRFBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Aggregates
|
||||
#
|
||||
|
||||
class AggregateForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = ['prefix', 'rir', 'date_added', 'description']
|
||||
help_texts = {
|
||||
'prefix': "IPv4 or IPv6 network",
|
||||
'rir': "Regional Internet Registry responsible for this prefix",
|
||||
'date_added': "Format: YYYY-MM-DD",
|
||||
}
|
||||
|
||||
|
||||
class AggregateFromCSVForm(forms.ModelForm):
|
||||
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'RIR not found.'})
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = ['prefix', 'rir', 'date_added', 'description']
|
||||
|
||||
|
||||
class AggregateImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=AggregateFromCSVForm)
|
||||
|
||||
|
||||
class AggregateBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
|
||||
date_added = forms.DateField(required=False)
|
||||
description = forms.CharField(max_length=50, required=False)
|
||||
|
||||
|
||||
class AggregateBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def aggregate_rir_choices():
|
||||
rir_choices = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
|
||||
|
||||
|
||||
class AggregateFilterForm(forms.Form, BootstrapMixin):
|
||||
rir = forms.MultipleChoiceField(required=False, choices=aggregate_rir_choices, label='RIR',
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
|
||||
|
||||
#
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
class PrefixForm(forms.ModelForm, BootstrapMixin):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
||||
widget=forms.Select(attrs={'filter-for': 'vlan'}))
|
||||
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
|
||||
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}'))
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'description']
|
||||
help_texts = {
|
||||
'prefix': "IPv4 or IPv6 network",
|
||||
'vrf': "VRF (if applicable)",
|
||||
'site': "The site to which this prefix is assigned (if applicable)",
|
||||
'vlan': "The VLAN to which this prefix is assigned (if applicable)",
|
||||
'status': "Operational status of this prefix",
|
||||
'role': "The primary function of this prefix",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PrefixForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
# Initialize field without choices to avoid pulling all VLANs from the database
|
||||
if self.is_bound and self.data.get('site'):
|
||||
self.fields['vlan'].queryset = VLAN.objects.filter(site__pk=self.data['site'])
|
||||
elif self.initial.get('site'):
|
||||
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
|
||||
else:
|
||||
self.fields['vlan'].choices = []
|
||||
|
||||
def clean_prefix(self):
|
||||
data = self.cleaned_data['prefix']
|
||||
try:
|
||||
prefix = IPNetwork(data)
|
||||
except:
|
||||
raise
|
||||
if prefix.version == 4 and prefix.prefixlen == 32:
|
||||
raise forms.ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 "
|
||||
"addresses instead.")
|
||||
elif prefix.version == 6 and prefix.prefixlen == 128:
|
||||
raise forms.ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 "
|
||||
"addresses instead.")
|
||||
return data
|
||||
|
||||
|
||||
class PrefixFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
error_messages={'invalid_choice': 'VRF not found.'})
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
status = forms.ModelChoiceField(queryset=Status.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid status.'})
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid role.'})
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['prefix', 'vrf', 'site', 'status', 'role', 'description']
|
||||
|
||||
|
||||
class PrefixImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=PrefixFromCSVForm)
|
||||
|
||||
|
||||
class PrefixBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
|
||||
help_text="Select the VRF to assign, or check below to remove VRF assignment")
|
||||
vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
|
||||
status = forms.ModelChoiceField(queryset=Status.objects.all(), required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=50, required=False)
|
||||
|
||||
|
||||
class PrefixBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def prefix_vrf_choices():
|
||||
vrf_choices = [('', 'All'), (0, 'Global')]
|
||||
vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
|
||||
return vrf_choices
|
||||
|
||||
|
||||
def prefix_status_choices():
|
||||
status_choices = Status.objects.annotate(prefix_count=Count('prefixes'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.prefix_count)) for s in status_choices]
|
||||
|
||||
|
||||
def prefix_site_choices():
|
||||
site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
|
||||
|
||||
|
||||
def prefix_role_choices():
|
||||
role_choices = Role.objects.annotate(prefix_count=Count('prefixes'))
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
|
||||
|
||||
|
||||
class PrefixFilterForm(forms.Form, BootstrapMixin):
|
||||
parent = forms.CharField(required=False, label='Search Within')
|
||||
vrf = forms.ChoiceField(required=False, choices=prefix_vrf_choices, label='VRF')
|
||||
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices)
|
||||
site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
||||
|
||||
|
||||
#
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
||||
nat_site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
||||
widget=forms.Select(attrs={'filter-for': 'nat_device'}))
|
||||
nat_device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
|
||||
widget=APISelect(api_url='/api/dcim/devices/?site_id={{nat_site}}',
|
||||
attrs={'filter-for': 'nat_inside'}))
|
||||
livesearch = forms.CharField(required=False, label='IP Address', widget=Livesearch(
|
||||
query_key='q', query_url='ipam-api:ipaddress_list', field_to_update='nat_inside', obj_label='address')
|
||||
)
|
||||
nat_inside = forms.ModelChoiceField(queryset=IPAddress.objects.all(), required=False, label='NAT (Inside)',
|
||||
widget=APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
|
||||
display_field='address'))
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'nat_device', 'nat_inside', 'description']
|
||||
help_texts = {
|
||||
'address': "IPv4 or IPv6 address and mask",
|
||||
'vrf': "VRF (if applicable)",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(IPAddressForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
if self.instance.nat_inside:
|
||||
|
||||
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_device'] = self.instance.nat_inside.interface.device.pk
|
||||
self.fields['nat_device'].queryset = Device.objects.filter(rack__site=nat_inside.interface.device.rack.site)
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(interface__device=nat_inside.interface.device)
|
||||
else:
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(pk=nat_inside.pk)
|
||||
|
||||
else:
|
||||
|
||||
# Initialize nat_device choices if nat_site is set
|
||||
if self.is_bound and self.data.get('nat_site'):
|
||||
self.fields['nat_device'].queryset = Device.objects.filter(rack__site__pk=self.data['nat_site'])
|
||||
elif self.initial.get('nat_site'):
|
||||
self.fields['nat_device'].queryset = Device.objects.filter(rack__site=self.initial['nat_site'])
|
||||
else:
|
||||
self.fields['nat_device'].choices = []
|
||||
|
||||
# Initialize nat_inside choices if nat_device is set
|
||||
if self.is_bound and self.data.get('nat_device'):
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(interface__device__pk=self.data['nat_device'])
|
||||
elif self.initial.get('nat_device'):
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(interface__device__pk=self.initial['nat_device'])
|
||||
else:
|
||||
self.fields['nat_inside'].choices = []
|
||||
|
||||
|
||||
class IPAddressFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found.'})
|
||||
interface_name = forms.CharField(required=False)
|
||||
is_primary = forms.BooleanField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'device', 'interface_name', 'is_primary', 'description']
|
||||
|
||||
def clean(self):
|
||||
|
||||
device = self.cleaned_data.get('device')
|
||||
interface_name = self.cleaned_data.get('interface_name')
|
||||
is_primary = self.cleaned_data.get('is_primary')
|
||||
|
||||
# Validate interface
|
||||
if device and interface_name:
|
||||
try:
|
||||
Interface.objects.get(device=device, name=interface_name)
|
||||
except Interface.DoesNotExist:
|
||||
self.add_error('interface_name', "Invalid interface ({}) for {}".format(interface_name, device))
|
||||
elif device and not interface_name:
|
||||
self.add_error('interface_name', "Device set ({}) but interface missing".format(device))
|
||||
elif interface_name and not device:
|
||||
self.add_error('device', "Interface set ({}) but device missing or invalid".format(interface_name))
|
||||
|
||||
# Validate is_primary
|
||||
if is_primary and not device:
|
||||
self.add_error('is_primary', "No device specified; cannot set as primary IP")
|
||||
|
||||
def save(self, commit=True):
|
||||
|
||||
# Set interface
|
||||
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
|
||||
self.instance.interface = Interface.objects.get(device=self.cleaned_data['device'],
|
||||
name=self.cleaned_data['interface_name'])
|
||||
# Set as primary for device
|
||||
if self.cleaned_data['is_primary']:
|
||||
self.instance.primary_for = self.cleaned_data['device']
|
||||
|
||||
return super(IPAddressFromCSVForm, self).save(commit=commit)
|
||||
|
||||
|
||||
class IPAddressImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=IPAddressFromCSVForm)
|
||||
|
||||
|
||||
class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
|
||||
help_text="Select the VRF to assign, or check below to remove VRF assignment")
|
||||
vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
|
||||
description = forms.CharField(max_length=50, required=False)
|
||||
|
||||
|
||||
class IPAddressBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def ipaddress_family_choices():
|
||||
return [('', 'All'), (4, 'IPv4'), (6, 'IPv6')]
|
||||
|
||||
|
||||
class IPAddressFilterForm(forms.Form, BootstrapMixin):
|
||||
family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
|
||||
|
||||
|
||||
#
|
||||
# VLANs
|
||||
#
|
||||
|
||||
class VLANForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site', 'vid', 'name', 'status', 'role']
|
||||
help_texts = {
|
||||
'site': "The site at which this VLAN exists",
|
||||
'vid': "Configured VLAN ID",
|
||||
'name': "Configured VLAN name",
|
||||
'status': "Operational status of this VLAN",
|
||||
'role': "The primary function of this VLAN",
|
||||
}
|
||||
|
||||
|
||||
class VLANFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found.'})
|
||||
status = forms.ModelChoiceField(queryset=Status.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid status.'})
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid role.'})
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site', 'vid', 'name', 'status', 'role']
|
||||
|
||||
|
||||
class VLANImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=VLANFromCSVForm)
|
||||
|
||||
|
||||
class VLANBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
status = forms.ModelChoiceField(queryset=Status.objects.all(), required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
|
||||
|
||||
class VLANBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def vlan_site_choices():
|
||||
site_choices = Site.objects.annotate(vlan_count=Count('vlans'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
|
||||
|
||||
|
||||
def vlan_status_choices():
|
||||
status_choices = Status.objects.annotate(vlan_count=Count('vlans'))
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.vlan_count)) for s in status_choices]
|
||||
|
||||
|
||||
def vlan_role_choices():
|
||||
role_choices = Role.objects.annotate(vlan_count=Count('vlans'))
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
|
||||
|
||||
|
||||
class VLANFilterForm(forms.Form, BootstrapMixin):
|
||||
site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
|
||||
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
|
@ -0,0 +1,89 @@
|
|||
from django.db.models import Lookup
|
||||
from django.db.models.lookups import BuiltinLookup
|
||||
|
||||
|
||||
class NetFieldDecoratorMixin(object):
|
||||
|
||||
def process_lhs(self, qn, connection, lhs=None):
|
||||
lhs = lhs or self.lhs
|
||||
lhs_string, lhs_params = qn.compile(lhs)
|
||||
lhs_string = 'TEXT(%s)' % lhs_string
|
||||
return lhs_string, lhs_params
|
||||
|
||||
|
||||
class EndsWith(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
lookup_name = 'endswith'
|
||||
|
||||
|
||||
class IEndsWith(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
lookup_name = 'iendswith'
|
||||
|
||||
|
||||
class StartsWith(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
lookup_name = 'startswith'
|
||||
|
||||
|
||||
class IStartsWith(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
lookup_name = 'istartswith'
|
||||
|
||||
|
||||
class Regex(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
lookup_name = 'regex'
|
||||
|
||||
|
||||
class IRegex(NetFieldDecoratorMixin, BuiltinLookup):
|
||||
lookup_name = 'iregex'
|
||||
|
||||
|
||||
class NetContainsOrEquals(Lookup):
|
||||
lookup_name = 'net_contains_or_equals'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
||||
params = lhs_params + rhs_params
|
||||
return '%s >>= %s' % (lhs, rhs), params
|
||||
|
||||
|
||||
class NetContains(Lookup):
|
||||
lookup_name = 'net_contains'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
||||
params = lhs_params + rhs_params
|
||||
return '%s >> %s' % (lhs, rhs), params
|
||||
|
||||
|
||||
class NetContained(Lookup):
|
||||
lookup_name = 'net_contained'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
||||
params = lhs_params + rhs_params
|
||||
return '%s << %s' % (lhs, rhs), params
|
||||
|
||||
|
||||
class NetContainedOrEqual(Lookup):
|
||||
lookup_name = 'net_contained_or_equal'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
||||
params = lhs_params + rhs_params
|
||||
return '%s <<= %s' % (lhs, rhs), params
|
||||
|
||||
|
||||
class NetHost(Lookup):
|
||||
lookup_name = 'net_host'
|
||||
|
||||
def as_sql(self, qn, connection):
|
||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
||||
# Query parameters are automatically converted to IPNetwork objects, which are then turned to strings. We need
|
||||
# to omit the mask portion of the object's string representation to match PostgreSQL's HOST() function.
|
||||
if rhs_params:
|
||||
rhs_params[0] = rhs_params[0].split('/')[0]
|
||||
params = lhs_params + rhs_params
|
||||
return 'HOST(%s) = %s' % (lhs, rhs), params
|
|
@ -0,0 +1,166 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.1 on 2016-02-27 02:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import ipam.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Aggregate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('family', models.PositiveSmallIntegerField(choices=[(4, b'IPv4'), (6, b'IPv6')])),
|
||||
('prefix', ipam.fields.IPNetworkField()),
|
||||
('date_added', models.DateField(blank=True, null=True)),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['family', 'prefix'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IPAddress',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('family', models.PositiveSmallIntegerField(choices=[(4, b'IPv4'), (6, b'IPv6')], editable=False)),
|
||||
('address', ipam.fields.IPAddressField()),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
('interface', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ip_addresses', to='dcim.Interface')),
|
||||
('nat_inside', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name=b'NAT IP (inside)')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['family', 'address'],
|
||||
'verbose_name': 'IP address',
|
||||
'verbose_name_plural': 'IP addresses',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Prefix',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('family', models.PositiveSmallIntegerField(choices=[(4, b'IPv4'), (6, b'IPv6')], editable=False)),
|
||||
('prefix', ipam.fields.IPNetworkField()),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['family', 'prefix'],
|
||||
'verbose_name_plural': 'prefixes',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RIR',
|
||||
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)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
'verbose_name': 'RIR',
|
||||
'verbose_name_plural': 'RIRs',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
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)),
|
||||
('weight', models.PositiveSmallIntegerField(default=1000)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['weight', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Status',
|
||||
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)),
|
||||
('weight', models.PositiveSmallIntegerField(default=1000)),
|
||||
('bootstrap_class', models.PositiveSmallIntegerField(choices=[(0, b'Default'), (1, b'Primary'), (2, b'Success'), (3, b'Info'), (4, b'Warning'), (5, b'Danger')], default=0)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['weight', 'name'],
|
||||
'verbose_name_plural': 'statuses',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VLAN',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('vid', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)], verbose_name=b'ID')),
|
||||
('name', models.CharField(max_length=30)),
|
||||
('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vlans', to='ipam.Role')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='dcim.Site')),
|
||||
('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='ipam.Status')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['site', 'vid'],
|
||||
'verbose_name': 'VLAN',
|
||||
'verbose_name_plural': 'VLANs',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VRF',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('rd', models.CharField(max_length=21, unique=True, verbose_name=b'Route distinguisher')),
|
||||
('description', models.CharField(blank=True, max_length=100)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
'verbose_name': 'VRF',
|
||||
'verbose_name_plural': 'VRFs',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='role',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prefixes', to='ipam.Role'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='site',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='dcim.Site'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='status',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.Status'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='vlan',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VLAN', verbose_name=b'VLAN'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='vrf',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VRF', verbose_name=b'VRF'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ipaddress',
|
||||
name='vrf',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='ipam.VRF', verbose_name=b'VRF'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aggregate',
|
||||
name='rir',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='aggregates', to='ipam.RIR', verbose_name=b'RIR'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,275 @@
|
|||
from netaddr import cidr_merge
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
|
||||
from dcim.models import Interface
|
||||
from .fields import IPNetworkField, IPAddressField
|
||||
|
||||
|
||||
AF_CHOICES = (
|
||||
(4, 'IPv4'),
|
||||
(6, 'IPv6'),
|
||||
)
|
||||
|
||||
BOOTSTRAP_CLASS_CHOICES = (
|
||||
(0, 'Default'),
|
||||
(1, 'Primary'),
|
||||
(2, 'Success'),
|
||||
(3, 'Info'),
|
||||
(4, 'Warning'),
|
||||
(5, 'Danger'),
|
||||
)
|
||||
|
||||
|
||||
class VRF(models.Model):
|
||||
"""
|
||||
A discrete layer three forwarding domain (e.g. a routing table)
|
||||
"""
|
||||
name = models.CharField(max_length=50)
|
||||
rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = 'VRF'
|
||||
verbose_name_plural = 'VRFs'
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:vrf', args=[self.pk])
|
||||
|
||||
|
||||
class Status(models.Model):
|
||||
"""
|
||||
The status of a prefix or VLAN (e.g. allocated, reserved, etc.)
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
weight = models.PositiveSmallIntegerField(default=1000)
|
||||
bootstrap_class = models.PositiveSmallIntegerField(choices=BOOTSTRAP_CLASS_CHOICES, default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ['weight', 'name']
|
||||
verbose_name_plural = 'statuses'
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
"""
|
||||
The role of an address resource (e.g. customer, infrastructure, mgmt, etc.)
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
weight = models.PositiveSmallIntegerField(default=1000)
|
||||
|
||||
class Meta:
|
||||
ordering = ['weight', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class RIR(models.Model):
|
||||
"""
|
||||
A regional Internet registry (e.g. ARIN) or governing standard (e.g. RFC 1918)
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = 'RIR'
|
||||
verbose_name_plural = 'RIRs'
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Aggregate(models.Model):
|
||||
"""
|
||||
A top-level IPv4 or IPv6 prefix
|
||||
"""
|
||||
family = models.PositiveSmallIntegerField(choices=AF_CHOICES)
|
||||
prefix = IPNetworkField()
|
||||
rir = models.ForeignKey('RIR', related_name='aggregates', on_delete=models.PROTECT, verbose_name='RIR')
|
||||
date_added = models.DateField(blank=True, null=True)
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['family', 'prefix']
|
||||
|
||||
def __unicode__(self):
|
||||
return str(self.prefix)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:aggregate', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.prefix:
|
||||
|
||||
# Clear host bits from prefix
|
||||
self.prefix = self.prefix.cidr
|
||||
|
||||
# Ensure that the aggregate being added is not covered by an existing aggregate
|
||||
covering_aggregates = Aggregate.objects.filter(prefix__net_contains_or_equals=str(self.prefix))
|
||||
if self.pk:
|
||||
covering_aggregates = covering_aggregates.exclude(pk=self.pk)
|
||||
if covering_aggregates:
|
||||
raise ValidationError("{} is already covered by an existing aggregate ({})"
|
||||
.format(self.prefix, covering_aggregates[0]))
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.prefix:
|
||||
# Infer address family from IPNetwork object
|
||||
self.family = self.prefix.version
|
||||
super(Aggregate, self).save(*args, **kwargs)
|
||||
|
||||
def get_utilization(self):
|
||||
"""
|
||||
Determine the utilization rate of the aggregate prefix and return it as a percentage.
|
||||
"""
|
||||
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
|
||||
# Remove overlapping prefixes from list of children
|
||||
networks = cidr_merge([c.prefix for c in child_prefixes])
|
||||
children_size = float(0)
|
||||
for p in networks:
|
||||
children_size += p.size
|
||||
return int(children_size / self.prefix.size * 100)
|
||||
|
||||
|
||||
class PrefixQuerySet(models.QuerySet):
|
||||
|
||||
def annotate_depth(self, limit=None):
|
||||
"""
|
||||
Iterate through a QuerySet of Prefixes and annotate the hierarchical level of each. While it would be preferable
|
||||
to do this using .extra() on the QuerySet to count the unique parents of each prefix, that approach introduces
|
||||
performance issues at scale.
|
||||
|
||||
Because we're adding a non-field attribute to the model, annotation must be made *after* any QuerySet
|
||||
modifications.
|
||||
"""
|
||||
queryset = self
|
||||
stack = []
|
||||
for p in queryset:
|
||||
try:
|
||||
prev_p = stack[-1]
|
||||
except IndexError:
|
||||
prev_p = None
|
||||
if prev_p is not None:
|
||||
while (p.prefix not in prev_p.prefix) or p.prefix == prev_p.prefix:
|
||||
stack.pop()
|
||||
try:
|
||||
prev_p = stack[-1]
|
||||
except IndexError:
|
||||
prev_p = None
|
||||
break
|
||||
if prev_p is not None:
|
||||
prev_p.has_children = True
|
||||
stack.append(p)
|
||||
p.depth = len(stack) - 1
|
||||
if limit is None:
|
||||
return queryset
|
||||
return filter(lambda p: p.depth <= limit, queryset)
|
||||
|
||||
|
||||
class Prefix(models.Model):
|
||||
"""
|
||||
An IPv4 or IPv6 prefix, including mask length
|
||||
"""
|
||||
family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
|
||||
prefix = IPNetworkField()
|
||||
site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True)
|
||||
vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True, verbose_name='VRF')
|
||||
vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True, verbose_name='VLAN')
|
||||
status = models.ForeignKey('Status', related_name='prefixes', on_delete=models.PROTECT)
|
||||
role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True)
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
|
||||
objects = PrefixQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['family', 'prefix']
|
||||
verbose_name_plural = 'prefixes'
|
||||
|
||||
def __unicode__(self):
|
||||
return str(self.prefix)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:prefix', args=[self.pk])
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.prefix:
|
||||
# Clear host bits from prefix
|
||||
self.prefix = self.prefix.cidr
|
||||
# Infer address family from IPNetwork object
|
||||
self.family = self.prefix.version
|
||||
super(Prefix, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class IPAddress(models.Model):
|
||||
"""
|
||||
An IPv4 or IPv6 address
|
||||
"""
|
||||
family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
|
||||
address = IPAddressField()
|
||||
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True, verbose_name='VRF')
|
||||
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True, null=True)
|
||||
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True, null=True, verbose_name='NAT IP (inside)')
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['family', 'address']
|
||||
verbose_name = 'IP address'
|
||||
verbose_name_plural = 'IP addresses'
|
||||
|
||||
def __unicode__(self):
|
||||
return str(self.address)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:ipaddress', args=[self.pk])
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.address:
|
||||
# Infer address family from IPAddress object
|
||||
self.family = self.address.version
|
||||
super(IPAddress, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
if self.interface:
|
||||
return self.interface.device
|
||||
return None
|
||||
|
||||
|
||||
class VLAN(models.Model):
|
||||
"""
|
||||
A VLAN within a site
|
||||
"""
|
||||
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT)
|
||||
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
|
||||
MinValueValidator(1),
|
||||
MaxValueValidator(4094)
|
||||
])
|
||||
name = models.CharField(max_length=30)
|
||||
status = models.ForeignKey('Status', related_name='vlans', on_delete=models.PROTECT)
|
||||
role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'vid']
|
||||
verbose_name = 'VLAN'
|
||||
verbose_name_plural = 'VLANs'
|
||||
|
||||
def __unicode__(self):
|
||||
return "{0} ({1})".format(self.vid, self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:vlan', args=[self.pk])
|
|
@ -0,0 +1,210 @@
|
|||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from .models import Aggregate, Prefix, IPAddress, VLAN, VRF
|
||||
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% with record.get_utilization as percentage %}
|
||||
<div class="progress text-center">
|
||||
{% if percentage < 15 %}<span style="font-size: 12px;">{{ percentage }}%</span>{% endif %}
|
||||
<div class="progress-bar progress-bar-{% if percentage >= 90 %}danger{% elif percentage >= 75 %}warning{% else %}success{% endif %}" role="progressbar" aria-valuenow="{{ percentage }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage }}%">
|
||||
{% if percentage >= 15 %}{{ percentage }}%{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
"""
|
||||
|
||||
PREFIX_LINK = """
|
||||
{% if record.has_children %}
|
||||
<span style="padding-left: {{ record.depth }}0px "><i class="fa fa-caret-right"></i></a>
|
||||
{% else %}
|
||||
<span style="padding-left: {{ record.depth }}9px">
|
||||
{% endif %}
|
||||
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% endif %}">{{ record.prefix }}</a>
|
||||
</span>
|
||||
"""
|
||||
|
||||
PREFIX_LINK_BRIEF = """
|
||||
<span style="padding-left: {{ record.depth }}0px">
|
||||
<a href="{% if record.pk %}{% url 'ipam:prefix' pk=record.pk %}{% else %}{% url 'ipam:prefix_add' %}?prefix={{ record }}{% endif %}">{{ record.prefix }}</a>
|
||||
</span>
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
{% if record.pk %}
|
||||
<span class="label label-{{ record.status.get_bootstrap_class_display|lower }}">{{ record.status.name }}</span>
|
||||
{% else %}
|
||||
<span class="label label-success">Available</span>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
|
||||
class VRFTable(tables.Table):
|
||||
name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name')
|
||||
rd = tables.Column(verbose_name='RD')
|
||||
description = tables.Column(sortable=False, verbose_name='Description')
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ('name', 'rd', 'description')
|
||||
empty_text = "No VRFs found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class VRFBulkEditTable(VRFTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(VRFTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'name', 'rd', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Aggregates
|
||||
#
|
||||
|
||||
class AggregateTable(tables.Table):
|
||||
prefix = tables.LinkColumn('ipam:aggregate', args=[Accessor('pk')], verbose_name='Aggregate')
|
||||
rir = tables.Column(verbose_name='RIR')
|
||||
child_count = tables.Column(verbose_name='Prefixes')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
||||
description = tables.Column(sortable=False, verbose_name='Description')
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = ('prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
|
||||
empty_text = "No aggregates found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class AggregateBulkEditTable(AggregateTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(AggregateTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
class PrefixTable(tables.Table):
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix')
|
||||
vrf = tables.Column(orderable=False, default='Global', verbose_name='VRF')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
role = tables.Column(verbose_name='Role')
|
||||
description = tables.Column(sortable=False, verbose_name='Description')
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ('prefix', 'status', 'vrf', 'site', 'role', 'description')
|
||||
empty_text = "No prefixes found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class PrefixBriefTable(tables.Table):
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF, verbose_name='Prefix')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
role = tables.Column(verbose_name='Role')
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ('prefix', 'status', 'site', 'role')
|
||||
empty_text = "No prefixes found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class PrefixBulkEditTable(PrefixTable):
|
||||
pk = tables.CheckBoxColumn(default='')
|
||||
|
||||
class Meta(PrefixTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'site', 'role', 'description')
|
||||
|
||||
|
||||
#
|
||||
# IPAddresses
|
||||
#
|
||||
|
||||
class IPAddressTable(tables.Table):
|
||||
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
|
||||
vrf = tables.Column(orderable=False, default='Global', verbose_name='VRF')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False, verbose_name='Device')
|
||||
interface = tables.Column(orderable=False, verbose_name='Interface')
|
||||
description = tables.Column(sortable=False, verbose_name='Description')
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ('address', 'vrf', 'device', 'interface', 'description')
|
||||
empty_text = "No IP addresses found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class IPAddressBriefTable(tables.Table):
|
||||
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False, verbose_name='Device')
|
||||
interface = tables.Column(orderable=False, verbose_name='Interface')
|
||||
nat_inside = tables.LinkColumn('ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)')
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ('address', 'device', 'interface', 'nat_inside')
|
||||
empty_text = "No IP addresses found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class IPAddressBulkEditTable(IPAddressTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(IPAddressTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'address', 'vrf', 'device', 'interface', 'description')
|
||||
|
||||
|
||||
#
|
||||
# VLANs
|
||||
#
|
||||
|
||||
class VLANTable(tables.Table):
|
||||
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
name = tables.Column(verbose_name='Name')
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
role = tables.Column(verbose_name='Role')
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ('vid', 'site', 'name', 'status', 'role')
|
||||
empty_text = "No VLANs found."
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class VLANBulkEditTable(VLANTable):
|
||||
pk = tables.CheckBoxColumn()
|
||||
|
||||
class Meta(VLANTable.Meta):
|
||||
model = None # django_tables2 bugfix
|
||||
fields = ('pk', 'vid', 'site', 'name', 'status', 'role')
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,51 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^vrfs/$', views.vrf_list, name='vrf_list'),
|
||||
url(r'^vrfs/add/$', views.vrf_add, name='vrf_add'),
|
||||
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
|
||||
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
|
||||
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/$', views.vrf, name='vrf'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/edit/$', views.vrf_edit, name='vrf_edit'),
|
||||
url(r'^vrfs/(?P<pk>\d+)/delete/$', views.vrf_delete, name='vrf_delete'),
|
||||
|
||||
url(r'^aggregates/$', views.aggregate_list, name='aggregate_list'),
|
||||
url(r'^aggregates/add/$', views.aggregate_add, name='aggregate_add'),
|
||||
url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
|
||||
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
|
||||
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/$', views.aggregate, name='aggregate'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/edit/$', views.aggregate_edit, name='aggregate_edit'),
|
||||
url(r'^aggregates/(?P<pk>\d+)/delete/$', views.aggregate_delete, name='aggregate_delete'),
|
||||
|
||||
url(r'^prefixes/$', views.prefix_list, name='prefix_list'),
|
||||
url(r'^prefixes/add/$', views.prefix_add, name='prefix_add'),
|
||||
url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
|
||||
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
|
||||
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/$', views.prefix, name='prefix'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.prefix_edit, name='prefix_edit'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.prefix_delete, name='prefix_delete'),
|
||||
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.prefix_ipaddresses, name='prefix_ipaddresses'),
|
||||
|
||||
url(r'^ip-addresses/$', views.ipaddress_list, name='ipaddress_list'),
|
||||
url(r'^ip-addresses/add/$', views.ipaddress_add, name='ipaddress_add'),
|
||||
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
|
||||
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
||||
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/$', views.ipaddress, name='ipaddress'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.ipaddress_edit, name='ipaddress_edit'),
|
||||
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.ipaddress_delete, name='ipaddress_delete'),
|
||||
|
||||
url(r'^vlans/$', views.vlan_list, name='vlan_list'),
|
||||
url(r'^vlans/add/$', views.vlan_add, name='vlan_add'),
|
||||
url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
|
||||
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
||||
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||
url(r'^vlans/(?P<pk>\d+)/$', views.vlan, name='vlan'),
|
||||
url(r'^vlans/(?P<pk>\d+)/edit/$', views.vlan_edit, name='vlan_edit'),
|
||||
url(r'^vlans/(?P<pk>\d+)/delete/$', views.vlan_delete, name='vlan_delete'),
|
||||
]
|
|
@ -0,0 +1,899 @@
|
|||
from netaddr import IPNetwork, IPSet
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from django_tables2 import RequestConfig
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import ProtectedError
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from dcim.models import Device
|
||||
from extras.models import ExportTemplate
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.views import BulkImportView, BulkEditView, BulkDeleteView
|
||||
|
||||
from .filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter, VRFFilter
|
||||
from .forms import AggregateForm, AggregateImportForm, AggregateBulkEditForm, AggregateBulkDeleteForm, \
|
||||
AggregateFilterForm, PrefixForm, PrefixImportForm, PrefixBulkEditForm, PrefixBulkDeleteForm, PrefixFilterForm, \
|
||||
IPAddressForm, IPAddressImportForm, IPAddressBulkEditForm, IPAddressBulkDeleteForm, IPAddressFilterForm, VLANForm, \
|
||||
VLANImportForm, VLANBulkEditForm, VLANBulkDeleteForm, VRFForm, VRFImportForm, VRFBulkEditForm, VRFBulkDeleteForm, \
|
||||
VLANFilterForm
|
||||
from .models import VRF, Aggregate, Prefix, VLAN
|
||||
from .tables import AggregateTable, AggregateBulkEditTable, PrefixTable, PrefixBriefTable, PrefixBulkEditTable, \
|
||||
IPAddress, IPAddressBriefTable, IPAddressTable, IPAddressBulkEditTable, VLANTable, VLANBulkEditTable, VRFTable, \
|
||||
VRFBulkEditTable
|
||||
|
||||
|
||||
def add_available_prefixes(parent, prefix_list):
|
||||
"""
|
||||
Create fake Prefix objects for all unallocated space within a prefix.
|
||||
"""
|
||||
|
||||
# Find all unallocated space
|
||||
available_prefixes = IPSet(parent) ^ IPSet([p.prefix for p in prefix_list])
|
||||
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
|
||||
|
||||
# Concatenate and sort complete list of children
|
||||
prefix_list = list(prefix_list) + available_prefixes
|
||||
prefix_list.sort(key=lambda p: p.prefix)
|
||||
|
||||
return prefix_list
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
|
||||
def vrf_list(request):
|
||||
|
||||
queryset = VRF.objects.all()
|
||||
queryset = VRFFilter(request.GET, queryset).qs
|
||||
# annotate_depth(queryset)
|
||||
|
||||
# Export
|
||||
if 'export' in request.GET:
|
||||
et = get_object_or_404(ExportTemplate, content_type__model='vrf', name=request.GET.get('export'))
|
||||
response = et.to_response(context_dict={'queryset': queryset}, filename='netbox_vrfs')
|
||||
return response
|
||||
|
||||
if request.user.has_perm('ipam.change_vrf') or request.user.has_perm('ipam.delete_vrf'):
|
||||
vrf_table = VRFBulkEditTable(queryset)
|
||||
else:
|
||||
vrf_table = VRFTable(queryset)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator}).configure(vrf_table)
|
||||
|
||||
export_templates = ExportTemplate.objects.filter(content_type__model='vrf')
|
||||
|
||||
return render(request, 'ipam/vrf_list.html', {
|
||||
'vrf_table': vrf_table,
|
||||
'export_templates': export_templates,
|
||||
})
|
||||
|
||||
|
||||
def vrf(request, pk):
|
||||
|
||||
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
||||
prefixes = Prefix.objects.filter(vrf=vrf)
|
||||
|
||||
return render(request, 'ipam/vrf.html', {
|
||||
'vrf': vrf,
|
||||
'prefixes': prefixes,
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.add_vrf')
|
||||
def vrf_add(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
form = VRFForm(request.POST)
|
||||
if form.is_valid():
|
||||
vrf = form.save()
|
||||
messages.success(request, "Added new VRF: {0}".format(vrf))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('ipam:vrf_add')
|
||||
else:
|
||||
return redirect('ipam:vrf', pk=vrf.pk)
|
||||
|
||||
else:
|
||||
form = VRFForm()
|
||||
|
||||
return render(request, 'ipam/vrf_edit.html', {
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:vrf_list'),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.change_vrf')
|
||||
def vrf_edit(request, pk):
|
||||
|
||||
vrf = get_object_or_404(VRF, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = VRFForm(request.POST, instance=vrf)
|
||||
if form.is_valid():
|
||||
vrf = form.save()
|
||||
messages.success(request, "Modified VRF {0}".format(vrf))
|
||||
return redirect('ipam:vrf', pk=vrf.pk)
|
||||
|
||||
else:
|
||||
form = VRFForm(instance=vrf)
|
||||
|
||||
return render(request, 'ipam/vrf_edit.html', {
|
||||
'vrf': vrf,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:vrf', kwargs={'pk': vrf.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.delete_vrf')
|
||||
def vrf_delete(request, pk):
|
||||
|
||||
vrf = get_object_or_404(VRF, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
vrf.delete()
|
||||
messages.success(request, "VRF {0} has been deleted".format(vrf))
|
||||
return redirect('ipam:vrf_list')
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(vrf, request, e)
|
||||
return redirect('ipam:vrf', pk=vrf.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'ipam/vrf_delete.html', {
|
||||
'vrf': vrf,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:vrf', kwargs={'pk': vrf.pk})
|
||||
})
|
||||
|
||||
|
||||
class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_vrf'
|
||||
form = VRFImportForm
|
||||
table = VRFTable
|
||||
template_name = 'ipam/vrf_import.html'
|
||||
obj_list_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_vrf'
|
||||
cls = VRF
|
||||
form = VRFBulkEditForm
|
||||
template_name = 'ipam/vrf_bulk_edit.html'
|
||||
redirect_url = 'ipam:vrf_list'
|
||||
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['description']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} VRFs".format(updated_count))
|
||||
|
||||
|
||||
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vrf'
|
||||
cls = VRF
|
||||
form = VRFBulkDeleteForm
|
||||
template_name = 'ipam/vrf_bulk_delete.html'
|
||||
redirect_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
#
|
||||
# Aggregates
|
||||
#
|
||||
|
||||
def aggregate_list(request):
|
||||
|
||||
queryset = Aggregate.objects.select_related('rir').extra(
|
||||
select = {
|
||||
'child_count': 'SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix',
|
||||
}
|
||||
)
|
||||
queryset = AggregateFilter(request.GET, queryset).qs
|
||||
|
||||
# Export
|
||||
if 'export' in request.GET:
|
||||
et = get_object_or_404(ExportTemplate, content_type__model='aggregate', name=request.GET.get('export'))
|
||||
response = et.to_response(context_dict={'queryset': queryset}, filename='netbox_aggregates')
|
||||
return response
|
||||
|
||||
if request.user.has_perm('ipam.change_aggregate') or request.user.has_perm('ipam.delete_aggregate'):
|
||||
aggregate_table = AggregateBulkEditTable(queryset)
|
||||
else:
|
||||
aggregate_table = AggregateTable(queryset)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator})\
|
||||
.configure(aggregate_table)
|
||||
|
||||
export_templates = ExportTemplate.objects.filter(content_type__model='aggregate')
|
||||
|
||||
return render(request, 'ipam/aggregate_list.html', {
|
||||
'aggregate_table': aggregate_table,
|
||||
'export_templates': export_templates,
|
||||
'filter_form': AggregateFilterForm(request.GET, label_suffix=''),
|
||||
})
|
||||
|
||||
|
||||
def aggregate(request, pk):
|
||||
|
||||
aggregate = get_object_or_404(Aggregate, pk=pk)
|
||||
|
||||
# Find all child prefixes contained by this aggregate
|
||||
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))\
|
||||
.select_related('site', 'status', 'role').annotate_depth(limit=0)
|
||||
child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
|
||||
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
prefix_table = PrefixBulkEditTable(child_prefixes)
|
||||
else:
|
||||
prefix_table = PrefixTable(child_prefixes)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator})\
|
||||
.configure(prefix_table)
|
||||
|
||||
return render(request, 'ipam/aggregate.html', {
|
||||
'aggregate': aggregate,
|
||||
'prefix_table': prefix_table,
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.add_aggregate')
|
||||
def aggregate_add(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
form = AggregateForm(request.POST)
|
||||
if form.is_valid():
|
||||
aggregate = form.save()
|
||||
messages.success(request, "Added new aggregate: {0}".format(aggregate.prefix))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('ipam:aggregate_add')
|
||||
else:
|
||||
return redirect('ipam:aggregate', pk=aggregate.pk)
|
||||
|
||||
else:
|
||||
form = AggregateForm()
|
||||
|
||||
return render(request, 'ipam/aggregate_edit.html', {
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:aggregate_list'),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.change_aggregate')
|
||||
def aggregate_edit(request, pk):
|
||||
|
||||
aggregate = get_object_or_404(Aggregate, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = AggregateForm(request.POST, instance=aggregate)
|
||||
if form.is_valid():
|
||||
aggregate = form.save()
|
||||
messages.success(request, "Modified aggregate {0}".format(aggregate.prefix))
|
||||
return redirect('ipam:aggregate', pk=aggregate.pk)
|
||||
|
||||
else:
|
||||
form = AggregateForm(instance=aggregate)
|
||||
|
||||
return render(request, 'ipam/aggregate_edit.html', {
|
||||
'aggregate': aggregate,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:aggregate', kwargs={'pk': aggregate.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.delete_aggregate')
|
||||
def aggregate_delete(request, pk):
|
||||
|
||||
aggregate = get_object_or_404(Aggregate, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
aggregate.delete()
|
||||
messages.success(request, "Aggregate {0} has been deleted".format(aggregate))
|
||||
return redirect('ipam:aggregate_list')
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(aggregate, request, e)
|
||||
return redirect('ipam:aggregate', pk=aggregate.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'ipam/aggregate_delete.html', {
|
||||
'aggregate': aggregate,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:aggregate', kwargs={'pk': aggregate.pk})
|
||||
})
|
||||
|
||||
|
||||
class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_aggregate'
|
||||
form = AggregateImportForm
|
||||
table = AggregateTable
|
||||
template_name = 'ipam/aggregate_import.html'
|
||||
obj_list_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_aggregate'
|
||||
cls = Aggregate
|
||||
form = AggregateBulkEditForm
|
||||
template_name = 'ipam/aggregate_bulk_edit.html'
|
||||
redirect_url = 'ipam:aggregate_list'
|
||||
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['rir', 'date_added', 'description']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} aggregates".format(updated_count))
|
||||
|
||||
|
||||
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_aggregate'
|
||||
cls = Aggregate
|
||||
form = AggregateBulkDeleteForm
|
||||
template_name = 'ipam/aggregate_bulk_delete.html'
|
||||
redirect_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
#
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
def prefix_list(request):
|
||||
|
||||
queryset = Prefix.objects.select_related('site', 'status', 'role')
|
||||
queryset = PrefixFilter(request.GET, queryset).qs
|
||||
|
||||
# Export
|
||||
if 'export' in request.GET:
|
||||
et = get_object_or_404(ExportTemplate, content_type__model='prefix', name=request.GET.get('export'))
|
||||
response = et.to_response(context_dict={'queryset': queryset}, filename='netbox_prefixes')
|
||||
return response
|
||||
|
||||
# Show only top-level prefixes by default
|
||||
limit = None if request.GET.get('expand') else 0
|
||||
prefixes = queryset.annotate_depth(limit=limit)
|
||||
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
prefix_table = PrefixBulkEditTable(prefixes)
|
||||
else:
|
||||
prefix_table = PrefixTable(prefixes)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator}).configure(prefix_table)
|
||||
|
||||
export_templates = ExportTemplate.objects.filter(content_type__model='prefix')
|
||||
|
||||
return render(request, 'ipam/prefix_list.html', {
|
||||
'prefix_table': prefix_table,
|
||||
'export_templates': export_templates,
|
||||
'filter_form': PrefixFilterForm(request.GET, label_suffix=''),
|
||||
})
|
||||
|
||||
|
||||
def prefix(request, pk):
|
||||
|
||||
prefix = get_object_or_404(Prefix.objects.select_related('site', 'vlan', 'status', 'role'), pk=pk)
|
||||
|
||||
try:
|
||||
aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
|
||||
except Aggregate.DoesNotExist:
|
||||
aggregate = None
|
||||
|
||||
# Count child IP addresses
|
||||
ipaddress_count = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix)).count()
|
||||
|
||||
# Parent prefixes table
|
||||
parent_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contains=str(prefix.prefix))\
|
||||
.select_related('site', 'status', 'role').annotate_depth()
|
||||
parent_prefix_table = PrefixBriefTable(parent_prefixes)
|
||||
|
||||
# Duplicate prefixes table
|
||||
duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\
|
||||
.select_related('site', 'status', 'role')
|
||||
duplicate_prefix_table = PrefixBriefTable(duplicate_prefixes)
|
||||
|
||||
# Child prefixes table
|
||||
child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\
|
||||
.select_related('site', 'status', 'role').annotate_depth(limit=0)
|
||||
if child_prefixes:
|
||||
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
child_prefix_table = PrefixBulkEditTable(child_prefixes)
|
||||
else:
|
||||
child_prefix_table = PrefixTable(child_prefixes)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator})\
|
||||
.configure(child_prefix_table)
|
||||
|
||||
return render(request, 'ipam/prefix.html', {
|
||||
'prefix': prefix,
|
||||
'aggregate': aggregate,
|
||||
'ipaddress_count': ipaddress_count,
|
||||
'parent_prefix_table': parent_prefix_table,
|
||||
'child_prefix_table': child_prefix_table,
|
||||
'duplicate_prefix_table': duplicate_prefix_table,
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.add_prefix')
|
||||
def prefix_add(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
form = PrefixForm(request.POST)
|
||||
if form.is_valid():
|
||||
prefix = form.save()
|
||||
messages.success(request, "Added new prefix: {0}".format(prefix.prefix))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('ipam:prefix_add')
|
||||
else:
|
||||
return redirect('ipam:prefix', pk=prefix.pk)
|
||||
|
||||
else:
|
||||
form = PrefixForm(initial={
|
||||
'site': request.GET.get('site'),
|
||||
'prefix': request.GET.get('prefix'),
|
||||
})
|
||||
|
||||
return render(request, 'ipam/prefix_edit.html', {
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:prefix_list'),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.change_prefix')
|
||||
def prefix_edit(request, pk):
|
||||
|
||||
prefix = get_object_or_404(Prefix, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = PrefixForm(request.POST, instance=prefix)
|
||||
if form.is_valid():
|
||||
prefix = form.save()
|
||||
messages.success(request, "Modified prefix {0}".format(prefix.prefix))
|
||||
return redirect('ipam:prefix', pk=prefix.pk)
|
||||
|
||||
else:
|
||||
form = PrefixForm(instance=prefix)
|
||||
|
||||
return render(request, 'ipam/prefix_edit.html', {
|
||||
'prefix': prefix,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:prefix', kwargs={'pk': prefix.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.delete_prefix')
|
||||
def prefix_delete(request, pk):
|
||||
|
||||
prefix = get_object_or_404(Prefix, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
prefix.delete()
|
||||
messages.success(request, "Prefix {0} has been deleted".format(prefix))
|
||||
return redirect('ipam:prefix_list')
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(prefix, request, e)
|
||||
return redirect('ipam:prefix', pk=prefix.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'ipam/prefix_delete.html', {
|
||||
'prefix': prefix,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:prefix', kwargs={'pk': prefix.pk})
|
||||
})
|
||||
|
||||
|
||||
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_prefix'
|
||||
form = PrefixImportForm
|
||||
table = PrefixTable
|
||||
template_name = 'ipam/prefix_import.html'
|
||||
obj_list_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_prefix'
|
||||
cls = Prefix
|
||||
form = PrefixBulkEditForm
|
||||
template_name = 'ipam/prefix_bulk_edit.html'
|
||||
redirect_url = 'ipam:prefix_list'
|
||||
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
if form.cleaned_data['vrf']:
|
||||
fields_to_update['vrf'] = form.cleaned_data['vrf']
|
||||
elif form.cleaned_data['vrf_global']:
|
||||
fields_to_update['vrf'] = None
|
||||
for field in ['site', 'status', 'role', 'description']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} prefixes".format(updated_count))
|
||||
|
||||
|
||||
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_prefix'
|
||||
cls = Prefix
|
||||
form = PrefixBulkDeleteForm
|
||||
template_name = 'ipam/prefix_bulk_delete.html'
|
||||
redirect_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
def prefix_ipaddresses(request, pk):
|
||||
|
||||
prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
|
||||
|
||||
# Find all IPAddresses belonging to this Prefix
|
||||
ipaddresses = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix))\
|
||||
.select_related('vrf', 'interface__device', 'primary_for')
|
||||
|
||||
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
||||
ip_table = IPAddressBulkEditTable(ipaddresses)
|
||||
else:
|
||||
ip_table = IPAddressTable(ipaddresses)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator})\
|
||||
.configure(ip_table)
|
||||
|
||||
return render(request, 'ipam/prefix_ipaddresses.html', {
|
||||
'prefix': prefix,
|
||||
'ip_table': ip_table,
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
def ipaddress_list(request):
|
||||
|
||||
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'primary_for')
|
||||
queryset = IPAddressFilter(request.GET, queryset).qs
|
||||
|
||||
# Export
|
||||
if 'export' in request.GET:
|
||||
et = get_object_or_404(ExportTemplate, content_type__model='ipaddress', name=request.GET.get('export'))
|
||||
response = et.to_response(context_dict={'queryset': queryset}, filename='netbox_ips')
|
||||
return response
|
||||
|
||||
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
||||
ip_table = IPAddressBulkEditTable(queryset)
|
||||
else:
|
||||
ip_table = IPAddressTable(queryset)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator}).configure(ip_table)
|
||||
|
||||
export_templates = ExportTemplate.objects.filter(content_type__model='ipaddress')
|
||||
|
||||
# If searching and no IPAddresses were found, include a list of parent prefixes matching the query
|
||||
prefix_table = None
|
||||
if request.GET.get('q') and not queryset:
|
||||
try:
|
||||
ip = str(IPNetwork(request.GET.get('q')))
|
||||
prefix_table = PrefixTable(Prefix.objects.filter(prefix__net_contains_or_equals=ip))
|
||||
RequestConfig(request).configure(prefix_table)
|
||||
except AddrFormatError:
|
||||
pass
|
||||
|
||||
return render(request, 'ipam/ipaddress_list.html', {
|
||||
'ip_table': ip_table,
|
||||
'prefix_table': prefix_table,
|
||||
'export_templates': export_templates,
|
||||
'filter_form': IPAddressFilterForm(request.GET, label_suffix=''),
|
||||
})
|
||||
|
||||
|
||||
def ipaddress(request, pk):
|
||||
|
||||
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
|
||||
|
||||
parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))
|
||||
related_ips = IPAddress.objects.select_related('interface__device').exclude(pk=ipaddress.pk).filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
|
||||
|
||||
related_ips_table = IPAddressBriefTable(related_ips)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator}).configure(related_ips_table)
|
||||
|
||||
return render(request, 'ipam/ipaddress.html', {
|
||||
'ipaddress': ipaddress,
|
||||
'parent_prefixes': parent_prefixes,
|
||||
'related_ips_table': related_ips_table,
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.add_ipaddress')
|
||||
def ipaddress_add(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
form = IPAddressForm(request.POST)
|
||||
if form.is_valid():
|
||||
ipaddress = form.save()
|
||||
messages.success(request, "Created new IP Address: {0}".format(ipaddress))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('ipam:ipaddress_add')
|
||||
else:
|
||||
return redirect('ipam:ipaddress', pk=ipaddress.pk)
|
||||
|
||||
else:
|
||||
form = IPAddressForm(initial={
|
||||
'ipaddress': request.GET.get('ipaddress', None),
|
||||
})
|
||||
|
||||
return render(request, 'ipam/ipaddress_edit.html', {
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:ipaddress_list'),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.change_ipaddress')
|
||||
def ipaddress_edit(request, pk):
|
||||
|
||||
ipaddress = get_object_or_404(IPAddress, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = IPAddressForm(request.POST, instance=ipaddress)
|
||||
if form.is_valid():
|
||||
ipaddress = form.save()
|
||||
messages.success(request, "Modified IP address {0}".format(ipaddress))
|
||||
return redirect('ipam:ipaddress', pk=ipaddress.pk)
|
||||
|
||||
else:
|
||||
form = IPAddressForm(instance=ipaddress)
|
||||
|
||||
return render(request, 'ipam/ipaddress_edit.html', {
|
||||
'ipaddress': ipaddress,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.delete_ipaddress')
|
||||
def ipaddress_delete(request, pk):
|
||||
|
||||
ipaddress = get_object_or_404(IPAddress, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
ipaddress.delete()
|
||||
messages.success(request, "IP address {0} has been deleted".format(ipaddress))
|
||||
if ipaddress.interface:
|
||||
return redirect('dcim:device', pk=ipaddress.interface.device.pk)
|
||||
else:
|
||||
return redirect('ipam:ipaddress_list')
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(ipaddress, request, e)
|
||||
return redirect('ipam:ipaddress', pk=ipaddress.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
# Upon cancellation, redirect to the assigned device if one exists
|
||||
if ipaddress.interface:
|
||||
cancel_url = reverse('dcim:device', kwargs={'pk': ipaddress.interface.device.pk})
|
||||
else:
|
||||
cancel_url = reverse('ipam:ipaddress_list')
|
||||
|
||||
return render(request, 'ipam/ipaddress_delete.html', {
|
||||
'ipaddress': ipaddress,
|
||||
'form': form,
|
||||
'cancel_url': cancel_url,
|
||||
})
|
||||
|
||||
|
||||
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_ipaddress'
|
||||
form = IPAddressImportForm
|
||||
table = IPAddressTable
|
||||
template_name = 'ipam/ipaddress_import.html'
|
||||
obj_list_url = 'ipam:ipaddress_list'
|
||||
|
||||
def save_obj(self, obj):
|
||||
obj.save()
|
||||
# Update primary IP for device if needed
|
||||
try:
|
||||
device = obj.primary_for
|
||||
device.primary_ip = obj
|
||||
device.save()
|
||||
except Device.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_ipaddress'
|
||||
cls = IPAddress
|
||||
form = IPAddressBulkEditForm
|
||||
template_name = 'ipam/ipaddress_bulk_edit.html'
|
||||
redirect_url = 'ipam:ipaddress_list'
|
||||
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
if form.cleaned_data['vrf']:
|
||||
fields_to_update['vrf'] = form.cleaned_data['vrf']
|
||||
elif form.cleaned_data['vrf_global']:
|
||||
fields_to_update['vrf'] = None
|
||||
for field in ['description']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} IP addresses".format(updated_count))
|
||||
|
||||
|
||||
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_ipaddress'
|
||||
cls = IPAddress
|
||||
form = IPAddressBulkDeleteForm
|
||||
template_name = 'ipam/ipaddress_bulk_delete.html'
|
||||
redirect_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
#
|
||||
# VLANs
|
||||
#
|
||||
|
||||
def vlan_list(request):
|
||||
|
||||
queryset = VLAN.objects.select_related('site', 'status', 'role')
|
||||
queryset = VLANFilter(request.GET, queryset).qs
|
||||
|
||||
# Export
|
||||
if 'export' in request.GET:
|
||||
et = get_object_or_404(ExportTemplate, content_type__model='vlan', name=request.GET.get('export'))
|
||||
response = et.to_response(context_dict={'queryset': queryset}, filename='netbox_vlans')
|
||||
return response
|
||||
|
||||
if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
|
||||
vlan_table = VLANBulkEditTable(queryset)
|
||||
else:
|
||||
vlan_table = VLANTable(queryset)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator}).configure(vlan_table)
|
||||
|
||||
export_templates = ExportTemplate.objects.filter(content_type__model='vlan')
|
||||
|
||||
return render(request, 'ipam/vlan_list.html', {
|
||||
'vlan_table': vlan_table,
|
||||
'export_templates': export_templates,
|
||||
'filter_form': VLANFilterForm(request.GET, label_suffix=''),
|
||||
})
|
||||
|
||||
|
||||
def vlan(request, pk):
|
||||
|
||||
vlan = get_object_or_404(VLAN.objects.select_related('site', 'status', 'role'), pk=pk)
|
||||
prefixes = Prefix.objects.filter(vlan=vlan)
|
||||
|
||||
return render(request, 'ipam/vlan.html', {
|
||||
'vlan': vlan,
|
||||
'prefixes': prefixes,
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.add_vlan')
|
||||
def vlan_add(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
form = VLANForm(request.POST)
|
||||
if form.is_valid():
|
||||
vlan = form.save()
|
||||
messages.success(request, "Added new VLAN: {0}".format(vlan))
|
||||
if '_addanother' in request.POST:
|
||||
base_url = reverse('ipam:vlan_add')
|
||||
params = urlencode({
|
||||
'site': vlan.site.pk,
|
||||
})
|
||||
return HttpResponseRedirect('{}?{}'.format(base_url, params))
|
||||
else:
|
||||
return redirect('ipam:vlan', pk=vlan.pk)
|
||||
|
||||
else:
|
||||
form = VLANForm()
|
||||
|
||||
return render(request, 'ipam/vlan_edit.html', {
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:vlan_list'),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.change_vlan')
|
||||
def vlan_edit(request, pk):
|
||||
|
||||
vlan = get_object_or_404(VLAN, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = VLANForm(request.POST, instance=vlan)
|
||||
if form.is_valid():
|
||||
vlan = form.save()
|
||||
messages.success(request, "Modified VLAN {0}".format(vlan))
|
||||
return redirect('ipam:vlan', pk=vlan.pk)
|
||||
|
||||
else:
|
||||
form = VLANForm(instance=vlan)
|
||||
|
||||
return render(request, 'ipam/vlan_edit.html', {
|
||||
'vlan': vlan,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:vlan', kwargs={'pk': vlan.pk}),
|
||||
})
|
||||
|
||||
|
||||
@permission_required('ipam.delete_vlan')
|
||||
def vlan_delete(request, pk):
|
||||
|
||||
vlan = get_object_or_404(VLAN, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConfirmationForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
vlan.delete()
|
||||
messages.success(request, "VLAN {0} has been deleted".format(vlan))
|
||||
return redirect('ipam:vlan_list')
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(vlan, request, e)
|
||||
return redirect('ipam:vlan', pk=vlan.pk)
|
||||
|
||||
else:
|
||||
form = ConfirmationForm()
|
||||
|
||||
return render(request, 'ipam/vlan_delete.html', {
|
||||
'vlan': vlan,
|
||||
'form': form,
|
||||
'cancel_url': reverse('ipam:vlan', kwargs={'pk': vlan.pk})
|
||||
})
|
||||
|
||||
|
||||
class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_vlan'
|
||||
form = VLANImportForm
|
||||
table = VLANTable
|
||||
template_name = 'ipam/vlan_import.html'
|
||||
obj_list_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_vlan'
|
||||
cls = VLAN
|
||||
form = VLANBulkEditForm
|
||||
template_name = 'ipam/vlan_bulk_edit.html'
|
||||
redirect_url = 'ipam:vlan_list'
|
||||
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['site', 'status', 'role']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} VLANs".format(updated_count))
|
||||
|
||||
|
||||
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vlan'
|
||||
cls = VLAN
|
||||
form = VLANBulkDeleteForm
|
||||
template_name = 'ipam/vlan_bulk_delete.html'
|
||||
redirect_url = 'ipam:vlan_list'
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
|
@ -0,0 +1,36 @@
|
|||
# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
|
||||
# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
|
||||
# symbols. NetBox will not run without this defined. For more information, see
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECRET_KEY
|
||||
SECRET_KEY = ''
|
||||
|
||||
# If enabled, NetBox will run with debugging turned on. This should only be used for development or troubleshooting.
|
||||
# NEVER ENABLE DEBUGGING ON A PRODUCTION SYSTEM.
|
||||
DEBUG = False
|
||||
|
||||
# Set this to your server's FQDN. This is required when DEBUG = False.
|
||||
# E.g. ALLOWED_HOSTS = ['netbox.yourdomain.com']
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# Setting this to true will display a "maintenance mode" banner at the top of every page.
|
||||
MAINTENANCE_MODE = False
|
||||
|
||||
# PostgreSQL database configuration.
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'netbox', # Database name
|
||||
'USER': 'netbox', # PostgreSQL username
|
||||
'PASSWORD': '', # PostgreSQL password
|
||||
'HOST': 'localhost', # Database server
|
||||
'PORT': '', # Database port (leave blank for default)
|
||||
}
|
||||
}
|
||||
|
||||
# If true, user authentication will be required for all site access. If false, unauthenticated users will be able to
|
||||
# access NetBox but not make any changes.
|
||||
LOGIN_REQUIRED = False
|
||||
|
||||
# Credentials that NetBox will use to access live devices. (Optional)
|
||||
NETBOX_USERNAME = ''
|
||||
NETBOX_PASSWORD = ''
|
|
@ -0,0 +1,140 @@
|
|||
"""
|
||||
Django settings for netbox project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 1.8.2.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.8/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.8/ref/settings/
|
||||
"""
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
import socket
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'corsheaders',
|
||||
'debug_toolbar',
|
||||
'django_extensions',
|
||||
'django_tables2',
|
||||
'rest_framework',
|
||||
'rest_framework_swagger',
|
||||
'circuits',
|
||||
'dcim',
|
||||
'ipam',
|
||||
'extras',
|
||||
'secrets',
|
||||
'users',
|
||||
'utilities',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'utilities.middleware.LoginRequiredMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'netbox.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR + '/templates/'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'utilities.context_processors.settings',
|
||||
'django.core.context_processors.request',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'netbox.wsgi.application'
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
TIME_ZONE = 'UTC'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.8/howto/static-files/
|
||||
STATIC_URL = '/static/'
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, "project-static"),
|
||||
)
|
||||
|
||||
# Messages
|
||||
from django.contrib.messages import constants as messages
|
||||
MESSAGE_TAGS = {
|
||||
messages.ERROR: 'danger',
|
||||
}
|
||||
|
||||
# Pagination
|
||||
PAGINATE_COUNT = 50
|
||||
|
||||
# Authentication
|
||||
LOGIN_URL = '/login/'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGOUT_URL = '/logout/'
|
||||
|
||||
# Default time formats
|
||||
DATE_FORMAT = 'N j, Y'
|
||||
SHORT_DATE_FORMAT = 'Y-m-d'
|
||||
TIME_FORMAT = 'g:i:s a'
|
||||
SHORT_TIME_FORMAT = 'H:i:s'
|
||||
DATETIME_FORMAT = 'N j, Y \a\t g:i a'
|
||||
SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
|
||||
|
||||
# Secrets
|
||||
SECRETS_MIN_PUBKEY_SIZE = 2048
|
||||
|
||||
# Django REST framework
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',)
|
||||
}
|
||||
|
||||
try:
|
||||
HOSTNAME = socket.gethostname()
|
||||
except:
|
||||
HOSTNAME = 'localhost'
|
||||
|
||||
# Import local configuration
|
||||
try:
|
||||
from configuration import *
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# django-cors-headers (API Cross-Origin Resource Sharing)
|
||||
if DEBUG:
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
CORS_ALLOW_METHODS = (
|
||||
'GET',
|
||||
'OPTIONS',
|
||||
)
|
|
@ -0,0 +1,32 @@
|
|||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
from django.views.defaults import page_not_found
|
||||
|
||||
from views import home, trigger_500
|
||||
from users.views import login, logout
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', home, name='home'),
|
||||
url(r'^circuits/', include('circuits.urls', namespace='circuits')),
|
||||
url(r'^dcim/', include('dcim.urls', namespace='dcim')),
|
||||
url(r'^ipam/', include('ipam.urls', namespace='ipam')),
|
||||
url(r'^secrets/', include('secrets.urls', namespace='secrets')),
|
||||
url(r'^profile/', include('users.urls', namespace='users')),
|
||||
|
||||
url(r'^login/$', login, name='login'),
|
||||
url(r'^logout/$', logout, name='logout'),
|
||||
|
||||
url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')),
|
||||
url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')),
|
||||
url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
|
||||
url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
|
||||
url(r'^api/docs/', include('rest_framework_swagger.urls')),
|
||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
|
||||
# Error testing
|
||||
url(r'^404/$', page_not_found),
|
||||
url(r'^500/$', trigger_500),
|
||||
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
]
|
|
@ -0,0 +1,45 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
from circuits.models import Provider, Circuit
|
||||
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
|
||||
from ipam.models import Aggregate, Prefix, IPAddress, VLAN
|
||||
from secrets.models import Secret
|
||||
|
||||
|
||||
def home(request):
|
||||
|
||||
stats = {
|
||||
|
||||
# DCIM
|
||||
'site_count': Site.objects.count(),
|
||||
'rack_count': Rack.objects.count(),
|
||||
'device_count': Device.objects.count(),
|
||||
'interface_connections_count': InterfaceConnection.objects.count(),
|
||||
'console_connections_count': ConsolePort.objects.filter(cs_port__isnull=False).count(),
|
||||
'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
|
||||
|
||||
# IPAM
|
||||
'aggregate_count': Aggregate.objects.count(),
|
||||
'prefix_count': Prefix.objects.count(),
|
||||
'ipaddress_count': IPAddress.objects.count(),
|
||||
'vlan_count': VLAN.objects.count(),
|
||||
|
||||
# Circuits
|
||||
'provider_count': Provider.objects.count(),
|
||||
'circuit_count': Circuit.objects.count(),
|
||||
|
||||
# Secrets
|
||||
'secret_count': Secret.objects.count(),
|
||||
|
||||
}
|
||||
|
||||
return render(request, 'home.html', {
|
||||
'stats': stats,
|
||||
})
|
||||
|
||||
|
||||
def trigger_500(request):
|
||||
"""Hot-wired method of triggering a server error to test reporting."""
|
||||
|
||||
raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional "
|
||||
"person you are.")
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for do_ipam project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
|
||||
|
||||
application = get_wsgi_application()
|
|
@ -0,0 +1,224 @@
|
|||
/* Layout */
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
padding-top: 70px;
|
||||
}
|
||||
.container {
|
||||
width: 1340px;
|
||||
}
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
margin: 0 auto -61px; /* the bottom margin is the negative value of the footer's height */
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
.footer, .push {
|
||||
height: 60px; /* .push must be the same height as .footer */
|
||||
}
|
||||
.footer {
|
||||
background-color: #f5f5f5;
|
||||
border-top: 1px solid #d0d0d0;
|
||||
}
|
||||
footer p {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
label {
|
||||
font-weight: normal;
|
||||
}
|
||||
label.required {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
th.pk, td.pk {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
|
||||
/* Paginator */
|
||||
nav ul.pagination {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Racks */
|
||||
div.rack_header {
|
||||
margin-left: 36px;
|
||||
text-align: center;
|
||||
width: 200px;
|
||||
}
|
||||
ul.rack_legend {
|
||||
float: left;
|
||||
list-style-type: none;
|
||||
margin-right: 6px;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
}
|
||||
ul.rack_legend li {
|
||||
color: #c0c0c0;
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
overflow: hidden;
|
||||
padding: 5px 0;
|
||||
text-align: right;
|
||||
}
|
||||
div.rack_frame {
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
ul.rack {
|
||||
border: 2px solid #404040;
|
||||
float: left;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
}
|
||||
ul.rack li {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
height: 20px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
ul.rack_empty li {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
height: 20px;
|
||||
}
|
||||
ul.rack li.empty:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
ul.rack_far_face {
|
||||
z-index: 100;
|
||||
}
|
||||
ul.rack_near_face {
|
||||
z-index: 200;
|
||||
}
|
||||
ul.rack li.h2u { height: 40px; }
|
||||
ul.rack li.h2u a, ul.rack li.h2u span { padding: 10px 0; }
|
||||
ul.rack li.h3u { height: 60px; }
|
||||
ul.rack li.h3u a, ul.rack li.h3u span { padding: 20px 0; }
|
||||
ul.rack li.h4u { height: 80px; }
|
||||
ul.rack li.h4u a, ul.rack li.h4u span { padding: 30px 0; }
|
||||
ul.rack li.h5u { height: 100px; }
|
||||
ul.rack li.h5u a, ul.rack li.h5u span { padding: 40px 0; }
|
||||
ul.rack li.h6u { height: 120px; }
|
||||
ul.rack li.h6u a, ul.rack li.h6u span { padding: 50px 0; }
|
||||
ul.rack li.h7u { height: 140px; }
|
||||
ul.rack li.h7u a, ul.rack li.h7u span { padding: 60px 0; }
|
||||
ul.rack li.h8u { height: 160px; }
|
||||
ul.rack li.h8u a, ul.rack li.h8u span { padding: 70px 0; }
|
||||
ul.rack li.h9u { height: 180px; }
|
||||
ul.rack li.h9u a, ul.rack li.h9u span { padding: 80px 0; }
|
||||
ul.rack li.h10u { height: 200px; }
|
||||
ul.rack li.h10u a, ul.rack li.h10u span { padding: 90px 0; }
|
||||
ul.rack li.h11u { height: 220px; }
|
||||
ul.rack li.h11u a, ul.rack li.h11u span { padding: 100px 0; }
|
||||
ul.rack li.h12u { height: 240px; }
|
||||
ul.rack li.h12u a, ul.rack li.h12u span { padding: 110px 0; }
|
||||
ul.rack li.h13u { height: 260px; }
|
||||
ul.rack li.h13u a, ul.rack li.h13u span { padding: 120px 0; }
|
||||
ul.rack li.h14u { height: 280px; }
|
||||
ul.rack li.h14u a, ul.rack li.h14u span { padding: 130px 0; }
|
||||
ul.rack li.h15u { height: 300px; }
|
||||
ul.rack li.h15u a, ul.rack li.h15u span { padding: 140px 0; }
|
||||
ul.rack li.h16u { height: 320px; }
|
||||
ul.rack li.h16u a, ul.rack li.h16u span { padding: 150px 0; }
|
||||
ul.rack li.occupied a {
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
ul.rack li.occupied a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
ul.rack li.occupied span {
|
||||
display: block;
|
||||
}
|
||||
ul.rack_near_face li {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
ul.rack_near_face li.occupied {
|
||||
color: #474747;
|
||||
}
|
||||
ul.rack_far_face li.occupied {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
#f7f7f7,
|
||||
#f7f7f7 7px,
|
||||
#f0f0f0 7px,
|
||||
#f0f0f0 14px
|
||||
);
|
||||
color: #303030;
|
||||
}
|
||||
ul.rack_far_face li.blocked {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
#f7f7f7,
|
||||
#f7f7f7 7px,
|
||||
#ffc7c7 7px,
|
||||
#ffc7c7 14px
|
||||
);
|
||||
color: #303030;
|
||||
}
|
||||
ul.rack_near_face li.empty a {
|
||||
color: #0000ff;
|
||||
display: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
ul.rack_near_face li.empty:hover {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
ul.rack_near_face li.empty:hover a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Rack elevation colors (from http://flatuicolors.com) */
|
||||
.teal { background-color: #1abc9c; border-bottom: 1px solid #16a085; }
|
||||
.teal:hover { background-color: #16a085; }
|
||||
.green { background-color: #2ecc71; border-bottom: 1px solid #27ae60; }
|
||||
.green:hover { background-color: #27ae60; }
|
||||
.blue { background-color: #3498db; border-bottom: 1px solid #2980b9; }
|
||||
.blue:hover { background-color: #2980b9; }
|
||||
.purple { background-color: #9b59b6; border-bottom: 1px solid #8e44ad; }
|
||||
.purple:hover { background-color: #8e44ad; }
|
||||
.yellow { background-color: #f1c40f; border-bottom: 1px solid #f39c12; }
|
||||
.yellow:hover { background-color: #f39c12; }
|
||||
.orange { background-color: #e67e22; border-bottom: 1px solid #d35400; }
|
||||
.orange:hover { background-color: #d35400; }
|
||||
.red { background-color: #e74c3c; border-bottom: 1px solid #c0392b; }
|
||||
.red:hover { background-color: #c0392b; }
|
||||
.light_gray { background-color: #ecf0f1; border-bottom: 1px solid #bdc3c7; }
|
||||
.light_gray:hover { background-color: #bdc3c7; }
|
||||
.medium_gray { background-color: #95a5a6; border-bottom: 1px solid #7f8c8d; }
|
||||
.medium_gray:hover { background-color: #7f8c8d; }
|
||||
.dark_gray { background-color: #34495e; border-bottom: 1px solid #2c3e50; }
|
||||
.dark_gray:hover { background-color: #2c3e50; }
|
||||
|
||||
/* Misc */
|
||||
.panel table>thead>tr>th {
|
||||
border-bottom: 0;
|
||||
}
|
||||
ul.nav-tabs, ul.nav-pills {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.panel .list-group {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
/* Fix progress bar margin inside table cells */
|
||||
td .progress {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
textarea {
|
||||
font-family: Consolas, Lucida Console, monospace;
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
$(document).ready(function() {
|
||||
|
||||
// "Select all" checkbox in a table header
|
||||
$('th input:checkbox').click(function (event) {
|
||||
$(this).parents('table').find('td input:checkbox').prop('checked', $(this).prop('checked'));
|
||||
});
|
||||
|
||||
// Helper select fields
|
||||
$('select.helper-parent').change(function () {
|
||||
|
||||
// Resolve child field by ID specified in parent
|
||||
var child_field = $('#id_' + $(this).attr('child'));
|
||||
|
||||
// Wipe out any existing options within the child field
|
||||
child_field.empty();
|
||||
child_field.append($("<option></option>").attr("value", "").text(""));
|
||||
|
||||
// If the parent has a value set, fetch a list of child options via the API and populate the child field with them
|
||||
if ($(this).val()) {
|
||||
|
||||
// Construct the API request URL
|
||||
var api_url = $(this).attr('child-source');
|
||||
var parent_accessor = $(this).attr('parent-accessor');
|
||||
if (parent_accessor) {
|
||||
api_url += '?' + parent_accessor + '=' + $(this).val();
|
||||
} else {
|
||||
api_url += '?' + $(this).attr('name') + '_id=' + $(this).val();
|
||||
}
|
||||
var api_url_extra = $(this).attr('child-filter');
|
||||
if (api_url_extra) {
|
||||
api_url += '&' + api_url_extra;
|
||||
}
|
||||
|
||||
var disabled_indicator = $(this).attr('disabled-indicator');
|
||||
var disabled_exempt = child_field.attr('exempt');
|
||||
var child_display = $(this).attr('child-display');
|
||||
if (!child_display) {
|
||||
child_display = 'name';
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: api_url,
|
||||
dataType: 'json',
|
||||
success: function (response, status) {
|
||||
console.log(response);
|
||||
$.each(response, function (index, choice) {
|
||||
var option = $("<option></option>").attr("value", choice.id).text(choice[child_display]);
|
||||
if (disabled_indicator && choice[disabled_indicator] && choice.id != disabled_exempt) {
|
||||
option.attr("disabled", "disabled")
|
||||
}
|
||||
child_field.append(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Trigger change event in case the child field is the parent of another field
|
||||
child_field.change();
|
||||
|
||||
});
|
||||
|
||||
// API select widget
|
||||
$('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);
|
||||
|
||||
// Wipe out any existing options within the child field
|
||||
child_field.empty();
|
||||
child_field.append($("<option></option>").attr("value", "").text(""));
|
||||
|
||||
if ($(this).val()) {
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// If all URL variables have been replaced, make the API call
|
||||
if (api_url.search('{{') < 0) {
|
||||
$.ajax({
|
||||
url: api_url,
|
||||
dataType: 'json',
|
||||
success: function (response, status) {
|
||||
$.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")
|
||||
}
|
||||
child_field.append(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Trigger change event in case the child field is the parent of another field
|
||||
child_field.change();
|
||||
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
$(document).ready(function() {
|
||||
var search_field = $('#id_livesearch');
|
||||
var search_key = search_field.attr('data-key');
|
||||
var label = search_field.attr('data-label');
|
||||
if (!label) {
|
||||
label = 'name';
|
||||
}
|
||||
|
||||
search_field.autocomplete({
|
||||
source: function(request, response) {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: search_field.attr('data-source'),
|
||||
data: search_key + '=' + request.term,
|
||||
success: function(data) {
|
||||
var choices = [];
|
||||
$.each(data, function (index, choice) {
|
||||
choices.push({
|
||||
value: choice.id,
|
||||
label: choice[label]
|
||||
});
|
||||
});
|
||||
response(choices);
|
||||
}
|
||||
});
|
||||
},
|
||||
select: function(event, ui) {
|
||||
event.preventDefault();
|
||||
search_field.val(ui.item.label);
|
||||
var real_field = $('#id_' + search_field.attr('data-field'));
|
||||
real_field.empty();
|
||||
real_field.append($("<option></option>").attr('value', ui.item.value).text(ui.item.label));
|
||||
real_field.change();
|
||||
// If the field has a parent helper, reset the parent to no selection
|
||||
$('select[filter-for="' + real_field.attr('name') + '"]').val('');
|
||||
},
|
||||
minLength: 4,
|
||||
delay: 500
|
||||
});
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
$(document).ready(function() {
|
||||
|
||||
// Unlocking a secret
|
||||
$('button.unlock-secret').click(function (event) {
|
||||
var secret_id = $(this).attr('secret-id');
|
||||
|
||||
// Retrieve from storage or prompt for private key
|
||||
var private_key = sessionStorage.getItem('private_key');
|
||||
if (!private_key) {
|
||||
$('#privkey_modal').modal('show');
|
||||
} else {
|
||||
unlock_secret(secret_id, private_key);
|
||||
$(this).hide();
|
||||
$(this).siblings('button.lock-secret').show();
|
||||
}
|
||||
});
|
||||
|
||||
// Locking a secret
|
||||
$('button.lock-secret').click(function (event) {
|
||||
var secret_id = $(this).attr('secret-id');
|
||||
$('#secret_' + secret_id).html('********');
|
||||
$(this).hide();
|
||||
$(this).siblings('button.unlock-secret').show();
|
||||
});
|
||||
|
||||
// Adding/editing a secret
|
||||
$('form.requires-private-key').submit(function(event) {
|
||||
var private_key = sessionStorage.getItem('private_key');
|
||||
if (private_key) {
|
||||
$('#id_private_key').val(private_key);
|
||||
} else {
|
||||
$('#privkey_modal').modal('show');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Prompt the user to enter a private RSA key for decryption
|
||||
$('#submit_privkey').click(function() {
|
||||
var private_key = $('#user_privkey').val();
|
||||
sessionStorage.setItem('private_key', private_key);
|
||||
});
|
||||
|
||||
// Generate a new public/private key pair via the API
|
||||
$('#generate_keypair').click(function() {
|
||||
$('#new_keypair_modal').modal('show');
|
||||
$.ajax({
|
||||
url: '/api/secrets/generate-keys/',
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success: function (response, status) {
|
||||
var public_key = response.public_key;
|
||||
var private_key = response.private_key;
|
||||
$('#new_pubkey').val(public_key);
|
||||
$('#new_privkey').val(private_key);
|
||||
},
|
||||
error: function (xhr, ajaxOptions, thrownError) {
|
||||
alert("There was an error generating a new key pair.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Enter a newly generated public key
|
||||
$('#use_new_pubkey').click(function() {
|
||||
var new_pubkey = $('#new_pubkey');
|
||||
if (new_pubkey.val()) {
|
||||
$('#id_public_key').val(new_pubkey.val());
|
||||
}
|
||||
});
|
||||
|
||||
// Retrieve a secret via the API
|
||||
function unlock_secret(secret_id, private_key) {
|
||||
var csrf_token = $('input[name=csrfmiddlewaretoken]').val();
|
||||
$.ajax({
|
||||
url: '/api/secrets/secrets/' + secret_id + '/decrypt/',
|
||||
type: 'POST',
|
||||
data: {
|
||||
private_key: private_key
|
||||
},
|
||||
dataType: 'json',
|
||||
beforeSend: function(xhr, settings) {
|
||||
xhr.setRequestHeader("X-CSRFToken", csrf_token);
|
||||
},
|
||||
success: function (response, status) {
|
||||
var secret_plaintext = response.plaintext;
|
||||
$('#secret_' + secret_id).html(secret_plaintext);
|
||||
return true;
|
||||
},
|
||||
error: function (xhr, ajaxOptions, thrownError) {
|
||||
if (xhr.status == 403) {
|
||||
alert("Decryption failed: " + xhr.statusText);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
from django.contrib import admin, messages
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from .forms import ActivateUserKeyForm
|
||||
from .models import UserKey, SecretRole, Secret
|
||||
|
||||
|
||||
@admin.register(UserKey)
|
||||
class UserKeyAdmin(admin.ModelAdmin):
|
||||
actions = ['activate_selected']
|
||||
list_display = ['user', 'is_filled', 'is_active', 'created']
|
||||
fields = ['user', 'public_key', 'is_active', 'last_modified']
|
||||
readonly_fields = ['is_active', 'last_modified']
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
# Don't allow a user to modify an existing public key directly.
|
||||
if obj and obj.public_key:
|
||||
return ['public_key'] + self.readonly_fields
|
||||
return self.readonly_fields
|
||||
|
||||
def get_actions(self, request):
|
||||
# Bulk deletion is disabled at the manager level, so remove the action from the admin site for this model.
|
||||
actions = super(UserKeyAdmin, self).get_actions(request)
|
||||
if 'delete_selected' in actions:
|
||||
del actions['delete_selected']
|
||||
if not request.user.has_perm('secrets.activate_userkey'):
|
||||
del actions['activate_selected']
|
||||
return actions
|
||||
|
||||
def activate_selected(modeladmin, request, queryset):
|
||||
"""
|
||||
Enable bulk activation of UserKeys
|
||||
"""
|
||||
try:
|
||||
my_userkey = UserKey.objects.get(user=request.user)
|
||||
except UserKey.DoesNotExist:
|
||||
messages.error(request, "You do not have an active User Key.")
|
||||
return redirect('/admin/secrets/userkey/')
|
||||
|
||||
if 'activate' in request.POST:
|
||||
form = ActivateUserKeyForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
|
||||
for uk in form.cleaned_data['_selected_action']:
|
||||
uk.activate(master_key)
|
||||
return redirect('/admin/secrets/userkey/')
|
||||
except ValueError:
|
||||
messages.error(request, "Invalid private key provided. Unable to retrieve master key.")
|
||||
else:
|
||||
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
|
||||
|
||||
return render(request, 'activate_keys.html', {
|
||||
'form': form,
|
||||
})
|
||||
activate_selected.short_description = "Activate selected user keys"
|
||||
|
||||
|
||||
@admin.register(SecretRole)
|
||||
class SecretRoleAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(Secret)
|
||||
class SecretAdmin(admin.ModelAdmin):
|
||||
list_display = ['parent', 'role', 'name', 'created', 'last_modified']
|
||||
fields = ['parent', 'role', 'name', 'hash', 'created', 'last_modified']
|
||||
readonly_fields = ['parent', 'hash', 'created', 'last_modified']
|
|
@ -0,0 +1,39 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from secrets.models import Secret, SecretRole
|
||||
|
||||
|
||||
#
|
||||
# SecretRoles
|
||||
#
|
||||
|
||||
class SecretRoleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class SecretRoleNestedSerializer(SecretRoleSerializer):
|
||||
|
||||
class Meta(SecretRoleSerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# Secrets
|
||||
#
|
||||
|
||||
# TODO: Serialize parent info
|
||||
class SecretSerializer(serializers.ModelSerializer):
|
||||
role = SecretRoleNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
fields = ['id', 'role', 'name', 'hash', 'created', 'last_modified']
|
||||
|
||||
|
||||
class SecretNestedSerializer(SecretSerializer):
|
||||
|
||||
class Meta(SecretSerializer.Meta):
|
||||
fields = ['id', 'name']
|
|
@ -0,0 +1,20 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from .views import *
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# Secrets
|
||||
url(r'^secrets/$', SecretListView.as_view(), name='secret_list'),
|
||||
url(r'^secrets/(?P<pk>\d+)/$', SecretDetailView.as_view(), name='secret_detail'),
|
||||
url(r'^secrets/(?P<pk>\d+)/decrypt/$', SecretDecryptView.as_view(), name='secret_decrypt'),
|
||||
|
||||
# Secret roles
|
||||
url(r'^secret-roles/$', SecretRoleListView.as_view(), name='secretrole_list'),
|
||||
url(r'^secret-roles/(?P<pk>\d+)/$', SecretRoleDetailView.as_view(), name='secretrole_detail'),
|
||||
|
||||
# Miscellaneous
|
||||
url(r'^generate-keys/$', RSAKeyGeneratorView.as_view(), name='generate_keys'),
|
||||
|
||||
]
|
|
@ -0,0 +1,104 @@
|
|||
from Crypto.PublicKey import RSA
|
||||
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from rest_framework import generics
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from secrets.models import Secret, SecretRole, UserKey
|
||||
from .serializers import SecretRoleSerializer, SecretSerializer
|
||||
|
||||
|
||||
class SecretRoleListView(generics.ListAPIView):
|
||||
"""
|
||||
List all secret roles
|
||||
"""
|
||||
queryset = SecretRole.objects.all()
|
||||
serializer_class = SecretRoleSerializer
|
||||
|
||||
|
||||
class SecretRoleDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single secret role
|
||||
"""
|
||||
queryset = SecretRole.objects.all()
|
||||
serializer_class = SecretRoleSerializer
|
||||
|
||||
|
||||
class SecretListView(generics.ListAPIView):
|
||||
"""
|
||||
List secrets (filterable)
|
||||
"""
|
||||
queryset = Secret.objects.select_related('role')
|
||||
serializer_class = SecretSerializer
|
||||
#filter_class = SecretFilter
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
|
||||
class SecretDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single Secret
|
||||
"""
|
||||
queryset = Secret.objects.select_related('role')
|
||||
serializer_class = SecretSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
|
||||
class SecretDecryptView(APIView):
|
||||
"""
|
||||
Retrieve the plaintext from a stored Secret. The request must include a valid private key.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request, pk):
|
||||
|
||||
secret = get_object_or_404(Secret, pk=pk)
|
||||
private_key = request.POST.get('private_key')
|
||||
if not private_key:
|
||||
raise ValidationError("Private key is missing from request.")
|
||||
|
||||
# Retrieve the Secret's plaintext with the user's private key
|
||||
try:
|
||||
uk = UserKey.objects.get(user=request.user)
|
||||
except UserKey.DoesNotExist:
|
||||
return HttpResponseForbidden(reason="No UserKey found.")
|
||||
if not uk.is_active():
|
||||
return HttpResponseForbidden(reason="UserKey is inactive.")
|
||||
|
||||
# Attempt to decrypt the Secret.
|
||||
master_key = uk.get_master_key(private_key)
|
||||
if master_key is None:
|
||||
return HttpResponseForbidden(reason="Invalid secret key.")
|
||||
secret.decrypt(master_key)
|
||||
|
||||
return Response({
|
||||
'plaintext': secret.plaintext,
|
||||
})
|
||||
|
||||
|
||||
class RSAKeyGeneratorView(APIView):
|
||||
"""
|
||||
Generate a new RSA key pair for a user. Authenticated because it's a ripe avenue for DoS.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
|
||||
# Determine what size key to generate
|
||||
key_size = request.GET.get('key_size', 2048)
|
||||
if key_size not in range(2048, 4097, 256):
|
||||
key_size = 2048
|
||||
|
||||
# Export RSA private and public keys in PEM format
|
||||
key = RSA.generate(key_size)
|
||||
private_key = key.exportKey('PEM')
|
||||
public_key = key.publickey().exportKey('PEM')
|
||||
|
||||
return Response({
|
||||
'private_key': private_key,
|
||||
'public_key': public_key,
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SecretsConfig(AppConfig):
|
||||
name = 'secrets'
|
|
@ -0,0 +1,24 @@
|
|||
from django.contrib import messages
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from .models import UserKey
|
||||
|
||||
|
||||
def userkey_required():
|
||||
"""
|
||||
Decorator for views which require that the user has an active UserKey (typically for encryption/decryption of
|
||||
Secrets).
|
||||
"""
|
||||
def _decorator(view):
|
||||
def wrapped_view(request, *args, **kwargs):
|
||||
try:
|
||||
uk = UserKey.objects.get(user=request.user)
|
||||
except UserKey.DoesNotExist:
|
||||
messages.warning(request, "This operation requires an active user key, but you don't have one.")
|
||||
return redirect('users:userkey')
|
||||
if not uk.is_active():
|
||||
messages.warning(request, "This operation is not available. Your user key has not been activated.")
|
||||
return redirect('users:userkey')
|
||||
return view(request, *args, **kwargs)
|
||||
return wrapped_view
|
||||
return _decorator
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue