mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-03 11:07:46 -06:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ba5e8def9 | ||
|
|
4e64e1ea95 | ||
|
|
300aff71bb | ||
|
|
0c3970233e |
@@ -1,52 +1,38 @@
|
||||
## Getting Help
|
||||
# Contributing to NetBox
|
||||
|
||||
If you encounter any issues installing or using NetBox, try one of the following resources to get assistance. Please
|
||||
**do not** open an issue on GitHub except to report bugs or request features.
|
||||
Thank you for your interest in contributing to NetBox! This document contains some quick pointers on reporting bugs and
|
||||
requesting new features.
|
||||
|
||||
### Freenode IRC
|
||||
## Reporting Issues
|
||||
|
||||
Join the #netbox channel on [Freenode IRC](https://freenode.net/). You can connect to Freenode at irc.freenode.net using
|
||||
an IRC client, or you can use their [webchat client](https://webchat.freenode.net/).
|
||||
* First, ensure that you've installed the latest stable version of NetBox. If you're running an older version, it's
|
||||
possible that the bug has already been fixed.
|
||||
|
||||
### Reddit
|
||||
* Check the [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has already been
|
||||
reported. If you think you may be experiencing a reported issue, please add a quick comment to it with a "+1" and a
|
||||
quick description of how it's affecting your installation.
|
||||
|
||||
We have established [/r/netbox](https://www.reddit.com/r/netbox) on Reddit for NetBox issues and general discussion.
|
||||
Reddit registration is free and does not require providing an email address (although it is encouraged).
|
||||
* If you're unsure whether the behavior you're seeing is expected, you can join #netbox on irc.freenode.net and ask
|
||||
before going through the trouble of submitting an issue report.
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
|
||||
NetBox. If you're running an older version, it's possible that the bug has already been fixed.
|
||||
|
||||
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has
|
||||
already been reported. If you think you may be experiencing a reported issue that hasn't already been resolved, please
|
||||
click "add a reaction" in the top right corner of the issue and add a thumbs up (+1). You might also want to add a
|
||||
comment describing how it's affecting your installation. This will allow us to prioritize bugs based on how many users
|
||||
are affected.
|
||||
|
||||
* If you haven't found an existing issue that describes your suspected bug, please inquire about it on IRC or Reddit.
|
||||
**Do not** file an issue until you have received confirmation that it is in fact a bug. Invalid issues are very
|
||||
distracting and slow the pace at which NetBox is developed.
|
||||
|
||||
* When submitting an issue, please be as descriptive as possible. Be sure to include:
|
||||
* When submitting an issue, please be as descriptive as possible. Be sure to describe:
|
||||
|
||||
* The environment in which NetBox is running
|
||||
* The exact steps that can be taken to reproduce the issue (if applicable)
|
||||
* Any error messages returned
|
||||
* Screenshots (if applicable)
|
||||
|
||||
* Keep in mind that we prioritize bugs based on their severity and how much work is required to resolve them. It may
|
||||
take some time for someone to address your issue.
|
||||
take some time for someone to address your issue. If it's been longer than a week with no updates, please ping us on
|
||||
IRC.
|
||||
|
||||
## Feature Requests
|
||||
|
||||
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
|
||||
requesting is already listed. (Be sure to search closed issues as well, since some feature requests are rejected.) If
|
||||
the feature you'd like to see has already been requested, click "add a reaction" in the top right corner of the issue
|
||||
and add a thumbs up (+1). This ensures that the issue has a better chance of making it onto the roadmap. Also feel free
|
||||
to add a comment with any additional justification for the feature.
|
||||
* First, check the [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're requesting
|
||||
has already been requested (and possibly rejected). If it has, click "add a reaction" in the top right corner of the
|
||||
issue and add a thumbs up (+1). This ensures that the issue has a better chance of making it onto the roadmap. Also feel
|
||||
free to add a comment with any additional justification for the feature.
|
||||
|
||||
* While suggestions for new features are welcome, it's important to limit the scope of NetBox's feature set to avoid
|
||||
* While discussion of new features is welcome, it's important to limit the scope of NetBox's feature set to avoid
|
||||
feature creep. For example, the following features would be firmly out of scope for NetBox:
|
||||
|
||||
* Ticket management
|
||||
@@ -54,18 +40,14 @@ feature creep. For example, the following features would be firmly out of scope
|
||||
* Acting as a DNS server
|
||||
* Acting as an authentication server
|
||||
|
||||
* Before filing a new feature request, propose it on IRC or Reddit first. Feedback you receive there will help validate
|
||||
and shape the proposed feature before filing a formal issue.
|
||||
* If you're not sure whether the feature you want is a good fit for NetBox, please ask in #netbox on irc.freenode.net.
|
||||
Even if it's not quite right for NetBox, we may be able to point you to a tool better suited for the job.
|
||||
|
||||
* Good feature requests are very narrowly defined. Be sure to enumerate specific functionality and data schema. The more
|
||||
effort you put into writing a feature request, the better its chances are of being implemented. Overly broad feature
|
||||
requests will be closed.
|
||||
* When submitting a feature request, be sure to include the following:
|
||||
|
||||
* When submitting a feature request on GitHub, be sure to include the following:
|
||||
|
||||
* A detailed description of the proposed functionality
|
||||
* A brief description of the functionality
|
||||
* A use case for the feature; who would use it and what value it would add to NetBox
|
||||
* A rough description of any changes necessary to the database schema
|
||||
* A rough description of any changes necessary to the database schema (if applicable)
|
||||
* Any third-party libraries or other resources which would be involved
|
||||
|
||||
## Submitting Pull Requests
|
||||
@@ -74,8 +56,9 @@ requests will be closed.
|
||||
before beginning work. This will help prevent wasting time on something that might we might not be able to implement.
|
||||
When suggesting a new feature, also make sure it won't conflict with any work that's already in progress.
|
||||
|
||||
* When submitting a pull request, please be sure to work off of the `develop` branch, rather than `master`. In NetBox,
|
||||
the `develop` branch is used for ongoing development, while `master` is used for tagging new stable releases.
|
||||
* When submitting a pull request, please be sure to work off of branch `develop`, rather than branch `master`.
|
||||
In NetBox, the `develop` branch is used for ongoing development, while `master` is used for tagging new
|
||||
stable releases.
|
||||
|
||||
* All code submissions should meet the following criteria (CI will enforce these checks):
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||

|
||||
# NetBox
|
||||
|
||||
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.
|
||||
|
||||
@@ -25,6 +25,6 @@ Questions? Comments? Please join us on IRC in **#netbox** on **irc.freenode.net*
|
||||
|
||||
# Installation
|
||||
|
||||
Please see [the documentation](http://netbox.readthedocs.io/en/latest/) for instructions on installing NetBox.
|
||||
Please see docs/getting-started.md for instructions on installing NetBox.
|
||||
|
||||
To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases) and run `upgrade.sh`.
|
||||
|
||||
@@ -2,7 +2,7 @@ NetBox's local configuration is held in `netbox/netbox/configuration.py`. An exa
|
||||
|
||||
## ALLOWED_HOSTS
|
||||
|
||||
This is a list of valid fully-qualified domain names (FQDNs) that is used to reach the NetBox service. Usually this is the same as the hostname for the NetBox server, but can also be different (e.g. when using a reverse proxy serving the NetBox website under a different FQDN than the hostname of the NetBox server). NetBox will not permit access to the server via any other hostnames (or IPs). The value of this option is also used to set `CSRF_TRUSTED_ORIGINS`, which restricts `HTTP POST` to the same set of hosts (more about this [here](https://docs.djangoproject.com/en/1.9/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS)). Keep in mind that NetBox, by default, has `USE_X_FORWARDED_HOST = True` (in `netbox/netbox/settings.py`) which means that if you're using a reverse proxy, it's the FQDN used to reach that reverse proxy which needs to be in this list (more about this [here](https://docs.djangoproject.com/en/1.9/ref/settings/#allowed-hosts)).
|
||||
This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -10,21 +10,15 @@ Sites can be assigned an optional facility ID to identify the actual facility ho
|
||||
|
||||
# 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.
|
||||
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.
|
||||
|
||||
The available rack types include 2- and 4-post frames, 4-post cabinet, and wall-mounted frame and cabinet. Rail-to-rail width may be 19 or 23 inches.
|
||||
|
||||
### Rack Groups
|
||||
|
||||
Racks can be arranged into groups. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site is a campus, each group might be a building. If each site is a building, each rack group might be a floor or room.
|
||||
|
||||
Each group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not supported.
|
||||
|
||||
### Rack Roles
|
||||
|
||||
Each rak can optionally be assigned to a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices.
|
||||
Each group is assigned to a parent site for easy navigation. Hierarchical recursion of rack groups is not currently supported.
|
||||
|
||||
---
|
||||
|
||||
@@ -80,7 +74,7 @@ The assignment of platforms to devices is an entirely optional feature, and may
|
||||
|
||||
### 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. Each module can optionally be assigned to a manufacturer.
|
||||
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.
|
||||
|
||||
### Components
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
NetBox supports the concept of individual tenants within its parent organization. Typically, these are used to represent individual customers or internal departments.
|
||||
|
||||
# Tenants
|
||||
|
||||
A tenant represents a discrete organization. Certain resources within NetBox can be assigned to a tenant. This makes it very convenient to track which resources are assigned to which customers, for instance.
|
||||
|
||||
The following objects can be assigned to tenants:
|
||||
|
||||
* Sites
|
||||
* Racks
|
||||
* Devices
|
||||
* VRFs
|
||||
* Prefixes
|
||||
* IP addresses
|
||||
* VLANs
|
||||
* Circuits
|
||||
|
||||
If a prefix or IP address is not assigned to a tenant, it will appear to inherit the tenant to which its parent VRF is assigned, if any.
|
||||
|
||||
### Tenant Groups
|
||||
|
||||
Tenants can be grouped by type. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional.
|
||||
@@ -50,4 +50,4 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and
|
||||
|
||||
# Getting Started
|
||||
|
||||
See the [installation guide](installation/postgresql.md) for help getting NetBox up and running quickly.
|
||||
See the [getting started](getting-started.md) guide for help with getting NetBox up and running quickly.
|
||||
|
||||
@@ -163,18 +163,6 @@ Are you sure you want to do this?
|
||||
Type 'yes' to continue, or 'no' to cancel: yes
|
||||
```
|
||||
|
||||
# Load Initial Data (Optional)
|
||||
|
||||
NetBox ships with some initial data to help you get started: RIR definitions, common devices roles, etc. You can delete any seed data that you don't want to keep.
|
||||
|
||||
!!! note
|
||||
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
|
||||
|
||||
```
|
||||
# ./manage.py loaddata initial_data
|
||||
Installed 43 object(s) from 4 fixture(s)
|
||||
```
|
||||
|
||||
# Test the Application
|
||||
|
||||
At this point, NetBox should be able to run. We can verify this by starting a development instance:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -17,7 +17,6 @@ pages:
|
||||
- 'DCIM': 'data-model/dcim.md'
|
||||
- 'IPAM': 'data-model/ipam.md'
|
||||
- 'Secrets': 'data-model/secrets.md'
|
||||
- 'Tenancy': 'data-model/tenancy.md'
|
||||
- 'Extras': 'data-model/extras.md'
|
||||
- 'API Integration': 'api-integration.md'
|
||||
|
||||
|
||||
@@ -21,11 +21,10 @@ class CircuitTypeAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Circuit)
|
||||
class CircuitAdmin(admin.ModelAdmin):
|
||||
list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed_human',
|
||||
'upstream_speed_human', 'commit_rate_human', 'xconnect_id']
|
||||
list_filter = ['provider', 'type', 'tenant']
|
||||
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', 'tenant', 'site')
|
||||
return qs.select_related('provider', 'type', 'site')
|
||||
|
||||
@@ -2,7 +2,6 @@ from rest_framework import serializers
|
||||
|
||||
from circuits.models import Provider, CircuitType, Circuit
|
||||
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||
from tenancy.api.serializers import TenantNestedSerializer
|
||||
|
||||
|
||||
#
|
||||
@@ -46,14 +45,13 @@ class CircuitTypeNestedSerializer(CircuitTypeSerializer):
|
||||
class CircuitSerializer(serializers.ModelSerializer):
|
||||
provider = ProviderNestedSerializer()
|
||||
type = CircuitTypeNestedSerializer()
|
||||
tenant = TenantNestedSerializer()
|
||||
site = SiteNestedSerializer()
|
||||
interface = InterfaceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
|
||||
'upstream_speed', 'commit_rate', 'xconnect_id', 'comments']
|
||||
fields = ['id', 'cid', 'provider', 'type', 'site', 'interface', 'install_date', 'port_speed', 'commit_rate',
|
||||
'xconnect_id', 'comments']
|
||||
|
||||
|
||||
class CircuitNestedSerializer(CircuitSerializer):
|
||||
|
||||
@@ -42,7 +42,7 @@ class CircuitListView(generics.ListAPIView):
|
||||
"""
|
||||
List circuits (filterable)
|
||||
"""
|
||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
|
||||
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filter_class = CircuitFilter
|
||||
|
||||
@@ -51,5 +51,5 @@ class CircuitDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single circuit
|
||||
"""
|
||||
queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
|
||||
queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
|
||||
@@ -3,7 +3,6 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.models import Site
|
||||
from tenancy.models import Tenant
|
||||
from .models import Provider, Circuit, CircuitType
|
||||
|
||||
|
||||
@@ -29,10 +28,10 @@ class ProviderFilter(django_filters.FilterSet):
|
||||
fields = ['q', 'name', 'account', 'asn']
|
||||
|
||||
def search(self, queryset, value):
|
||||
value = value.strip()
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(account__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
Q(account__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
@@ -63,17 +62,6 @@ class CircuitFilter(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Circuit type (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
@@ -91,9 +79,5 @@ class CircuitFilter(django_filters.FilterSet):
|
||||
fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date']
|
||||
|
||||
def search(self, queryset, value):
|
||||
return queryset.filter(
|
||||
Q(cid__icontains=value) |
|
||||
Q(xconnect_id__icontains=value) |
|
||||
Q(pp_info__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
value = value.strip()
|
||||
return queryset.filter(cid__icontains=value)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
[
|
||||
{
|
||||
"model": "circuits.circuittype",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Internet",
|
||||
"slug": "internet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "circuits.circuittype",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Private WAN",
|
||||
"slug": "private-wan"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "circuits.circuittype",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Out-of-Band",
|
||||
"slug": "out-of-band"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -2,10 +2,9 @@ from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
|
||||
from tenancy.forms import bulkedit_tenant_choices
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField,
|
||||
APISelect, BootstrapMixin, BulkImportForm, CommentField, ConfirmationForm, CSVDataField, Livesearch, SmallTextarea,
|
||||
SlugField,
|
||||
)
|
||||
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
@@ -56,6 +55,10 @@ class ProviderBulkEditForm(forms.Form, BootstrapMixin):
|
||||
comments = CommentField()
|
||||
|
||||
|
||||
class ProviderBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def provider_site_choices():
|
||||
site_choices = Site.objects.all()
|
||||
return [(s.slug, s.name) for s in site_choices]
|
||||
@@ -78,6 +81,10 @@ class CircuitTypeForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class CircuitTypeBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=CircuitType.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
@@ -101,8 +108,8 @@ class CircuitForm(forms.ModelForm, BootstrapMixin):
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
|
||||
'port_speed', 'upstream_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
|
||||
'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",
|
||||
@@ -162,15 +169,13 @@ class CircuitFromCSVForm(forms.ModelForm):
|
||||
error_messages={'invalid_choice': 'Provider not found.'})
|
||||
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
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', 'tenant', 'site', 'install_date', 'port_speed', 'upstream_speed',
|
||||
'commit_rate', 'xconnect_id', 'pp_info']
|
||||
fields = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id',
|
||||
'pp_info']
|
||||
|
||||
|
||||
class CircuitImportForm(BulkImportForm, BootstrapMixin):
|
||||
@@ -181,37 +186,33 @@ 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)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
|
||||
commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
|
||||
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, u'{} ({})'.format(t.name, t.circuit_count)) for t in type_choices]
|
||||
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, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
|
||||
|
||||
|
||||
def circuit_tenant_choices():
|
||||
tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits'))
|
||||
return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices]
|
||||
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, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
|
||||
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}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-07-26 21:59
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0001_initial'),
|
||||
('circuits', '0003_provider_32bit_asn_support'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant'),
|
||||
),
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-08 20:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0004_circuit_add_tenant'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='upstream_speed',
|
||||
field=models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)'),
|
||||
),
|
||||
]
|
||||
@@ -3,7 +3,6 @@ from django.db import models
|
||||
|
||||
from dcim.fields import ASNField
|
||||
from dcim.models import Site, Interface
|
||||
from tenancy.models import Tenant
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
|
||||
|
||||
@@ -67,13 +66,10 @@ class Circuit(CreatedUpdatedModel):
|
||||
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)
|
||||
tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, 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.PositiveIntegerField(verbose_name='Port speed (Kbps)')
|
||||
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
||||
help_text='Upstream speed, if different from port speed')
|
||||
commit_rate = models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')
|
||||
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)')
|
||||
@@ -84,7 +80,7 @@ class Circuit(CreatedUpdatedModel):
|
||||
unique_together = ['provider', 'cid']
|
||||
|
||||
def __unicode__(self):
|
||||
return u'{} {}'.format(self.provider, self.cid)
|
||||
return "{0} {1}".format(self.provider, self.cid)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
@@ -94,11 +90,9 @@ class Circuit(CreatedUpdatedModel):
|
||||
self.cid,
|
||||
self.provider.name,
|
||||
self.type.name,
|
||||
self.tenant.name if self.tenant else '',
|
||||
self.site.name,
|
||||
self.install_date.isoformat() if self.install_date else '',
|
||||
str(self.port_speed),
|
||||
str(self.upstream_speed),
|
||||
str(self.commit_rate) if self.commit_rate else '',
|
||||
self.xconnect_id,
|
||||
self.pp_info,
|
||||
@@ -119,18 +113,12 @@ class Circuit(CreatedUpdatedModel):
|
||||
else:
|
||||
return '{} Kbps'.format(speed)
|
||||
|
||||
@property
|
||||
def port_speed_human(self):
|
||||
return self._humanize_speed(self.port_speed)
|
||||
port_speed_human.admin_order_field = 'port_speed'
|
||||
|
||||
def upstream_speed_human(self):
|
||||
if not self.upstream_speed:
|
||||
return ''
|
||||
return self._humanize_speed(self.upstream_speed)
|
||||
upstream_speed_human.admin_order_field = 'upstream_speed'
|
||||
|
||||
@property
|
||||
def commit_rate_human(self):
|
||||
if not self.commit_rate:
|
||||
return ''
|
||||
return self._humanize_speed(self.commit_rate)
|
||||
commit_rate_human.admin_order_field = 'commit_rate'
|
||||
|
||||
@@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
|
||||
CIRCUITTYPE_ACTIONS = """
|
||||
CIRCUITTYPE_EDIT_LINK = """
|
||||
{% if perms.circuit.change_circuittype %}
|
||||
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}">Edit</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -21,12 +21,11 @@ class ProviderTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
|
||||
asn = tables.Column(verbose_name='ASN')
|
||||
account = tables.Column(verbose_name='Account')
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Provider
|
||||
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
fields = ('pk', 'name', 'asn', 'circuit_count')
|
||||
|
||||
|
||||
#
|
||||
@@ -38,12 +37,11 @@ class CircuitTypeTable(BaseTable):
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
circuit_count = tables.Column(verbose_name='Circuits')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
edit = tables.TemplateColumn(template_code=CIRCUITTYPE_EDIT_LINK, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = CircuitType
|
||||
fields = ('pk', 'name', 'circuit_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'circuit_count', 'slug', 'edit')
|
||||
|
||||
|
||||
#
|
||||
@@ -55,13 +53,10 @@ class CircuitTable(BaseTable):
|
||||
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')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'),
|
||||
verbose_name='Port Speed')
|
||||
commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'),
|
||||
verbose_name='Commit Rate')
|
||||
port_speed_human = tables.Column(verbose_name='Port Speed')
|
||||
commit_rate_human = tables.Column(verbose_name='Commit Rate')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Circuit
|
||||
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed', 'commit_rate')
|
||||
fields = ('pk', 'cid', 'type', 'provider', 'site', 'port_speed_human', 'commit_rate_human')
|
||||
|
||||
@@ -2,7 +2,6 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
@@ -28,12 +27,10 @@ def provider(request, slug):
|
||||
|
||||
provider = get_object_or_404(Provider, slug=slug)
|
||||
circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device')
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||
|
||||
return render(request, 'circuits/provider.html', {
|
||||
'provider': provider,
|
||||
'circuits': circuits,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
|
||||
|
||||
@@ -79,6 +76,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_provider'
|
||||
cls = Provider
|
||||
form = forms.ProviderBulkDeleteForm
|
||||
default_redirect_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
@@ -104,6 +102,7 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuittype'
|
||||
cls = CircuitType
|
||||
form = forms.CircuitTypeBulkDeleteForm
|
||||
default_redirect_url = 'circuits:circuittype_list'
|
||||
|
||||
|
||||
@@ -112,7 +111,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class CircuitListView(ObjectListView):
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant', 'site')
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'site')
|
||||
filter = filters.CircuitFilter
|
||||
filter_form = forms.CircuitFilterForm
|
||||
table = tables.CircuitTable
|
||||
@@ -162,10 +161,6 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
if form.cleaned_data['tenant'] == 0:
|
||||
fields_to_update['tenant'] = None
|
||||
elif form.cleaned_data['tenant']:
|
||||
fields_to_update['tenant'] = form.cleaned_data['tenant']
|
||||
for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
@@ -176,4 +171,5 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuit'
|
||||
cls = Circuit
|
||||
form = forms.CircuitBulkDeleteForm
|
||||
default_redirect_url = 'circuits:circuit_list'
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.db.models import Count
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Module, Platform,
|
||||
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site,
|
||||
PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,17 +24,9 @@ class RackGroupAdmin(admin.ModelAdmin):
|
||||
}
|
||||
|
||||
|
||||
@admin.register(RackRole)
|
||||
class RackRoleAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'color']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(Rack)
|
||||
class RackAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height']
|
||||
list_display = ['name', 'facility_id', 'site', 'u_height']
|
||||
|
||||
|
||||
#
|
||||
@@ -86,8 +78,8 @@ class DeviceTypeAdmin(admin.ModelAdmin):
|
||||
InterfaceTemplateAdmin,
|
||||
DeviceBayTemplateAdmin,
|
||||
]
|
||||
list_display = ['model', 'manufacturer', 'slug', 'part_number', 'u_height', 'console_ports', 'console_server_ports',
|
||||
'power_ports', 'power_outlets', 'interfaces', 'device_bays']
|
||||
list_display = ['model', 'manufacturer', 'slug', 'u_height', 'console_ports', 'console_server_ports', 'power_ports',
|
||||
'power_outlets', 'interfaces', 'device_bays']
|
||||
list_filter = ['manufacturer']
|
||||
|
||||
def get_queryset(self, request):
|
||||
|
||||
@@ -3,10 +3,9 @@ from rest_framework import serializers
|
||||
from ipam.models import IPAddress
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType,
|
||||
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
|
||||
DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
|
||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
|
||||
)
|
||||
from tenancy.api.serializers import TenantNestedSerializer
|
||||
|
||||
|
||||
#
|
||||
@@ -14,11 +13,10 @@ from tenancy.api.serializers import TenantNestedSerializer
|
||||
#
|
||||
|
||||
class SiteSerializer(serializers.ModelSerializer):
|
||||
tenant = TenantNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
|
||||
fields = ['id', 'name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
|
||||
'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
|
||||
|
||||
|
||||
@@ -46,23 +44,6 @@ class RackGroupNestedSerializer(RackGroupSerializer):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
|
||||
class RackRoleNestedSerializer(RackRoleSerializer):
|
||||
|
||||
class Meta(RackRoleSerializer.Meta):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
@@ -71,13 +52,10 @@ class RackRoleNestedSerializer(RackRoleSerializer):
|
||||
class RackSerializer(serializers.ModelSerializer):
|
||||
site = SiteNestedSerializer()
|
||||
group = RackGroupNestedSerializer()
|
||||
tenant = TenantNestedSerializer()
|
||||
role = RackRoleNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
|
||||
'u_height', 'comments']
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments']
|
||||
|
||||
|
||||
class RackNestedSerializer(RackSerializer):
|
||||
@@ -91,8 +69,8 @@ class RackDetailSerializer(RackSerializer):
|
||||
rear_units = serializers.SerializerMethodField()
|
||||
|
||||
class Meta(RackSerializer.Meta):
|
||||
fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
|
||||
'u_height', 'comments', 'front_units', 'rear_units']
|
||||
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)
|
||||
@@ -133,8 +111,8 @@ class DeviceTypeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||
'is_console_server', 'is_pdu', 'is_network_device']
|
||||
fields = ['id', 'manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device']
|
||||
|
||||
|
||||
class DeviceTypeNestedSerializer(DeviceTypeSerializer):
|
||||
@@ -186,9 +164,9 @@ class DeviceTypeDetailSerializer(DeviceTypeSerializer):
|
||||
interface_templates = InterfaceTemplateNestedSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta(DeviceTypeSerializer.Meta):
|
||||
fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||
'is_console_server', 'is_pdu', 'is_network_device', 'console_port_templates', 'cs_port_templates',
|
||||
'power_port_templates', 'power_outlet_templates', 'interface_templates']
|
||||
fields = ['id', 'manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'console_port_templates', 'cs_port_templates', 'power_port_templates',
|
||||
'power_outlet_templates', 'interface_templates']
|
||||
|
||||
|
||||
#
|
||||
@@ -240,7 +218,6 @@ class DeviceIPAddressNestedSerializer(serializers.ModelSerializer):
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
device_type = DeviceTypeNestedSerializer()
|
||||
device_role = DeviceRoleNestedSerializer()
|
||||
tenant = TenantNestedSerializer()
|
||||
platform = PlatformNestedSerializer()
|
||||
rack = RackNestedSerializer()
|
||||
primary_ip = DeviceIPAddressNestedSerializer()
|
||||
@@ -250,8 +227,8 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'rack',
|
||||
'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
|
||||
fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
|
||||
'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
|
||||
|
||||
def get_parent_device(self, obj):
|
||||
try:
|
||||
@@ -403,25 +380,6 @@ class DeviceBayDetailSerializer(DeviceBaySerializer):
|
||||
fields = ['id', 'device', 'name', 'installed_device']
|
||||
|
||||
|
||||
#
|
||||
# Modules
|
||||
#
|
||||
|
||||
class ModuleSerializer(serializers.ModelSerializer):
|
||||
device = DeviceNestedSerializer()
|
||||
manufacturer = ManufacturerNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered']
|
||||
|
||||
|
||||
class ModuleNestedSerializer(ModuleSerializer):
|
||||
|
||||
class Meta(ModuleSerializer.Meta):
|
||||
fields = ['id', 'device', 'parent', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Interface connections
|
||||
#
|
||||
|
||||
@@ -18,10 +18,6 @@ urlpatterns = [
|
||||
url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'),
|
||||
url(r'^rack-groups/(?P<pk>\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'),
|
||||
|
||||
# Rack roles
|
||||
url(r'^rack-roles/$', RackRoleListView.as_view(), name='rackrole_list'),
|
||||
url(r'^rack-roles/(?P<pk>\d+)/$', RackRoleDetailView.as_view(), name='rackrole_detail'),
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', RackListView.as_view(), name='rack_list'),
|
||||
url(r'^racks/(?P<pk>\d+)/$', RackDetailView.as_view(), name='rack_detail'),
|
||||
@@ -54,7 +50,6 @@ urlpatterns = [
|
||||
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'),
|
||||
url(r'^devices/(?P<pk>\d+)/device-bays/$', DeviceBayListView.as_view(), name='device_devicebays'),
|
||||
url(r'^devices/(?P<pk>\d+)/modules/$', ModuleListView.as_view(), name='device_modules'),
|
||||
|
||||
# Console ports
|
||||
url(r'^console-ports/(?P<pk>\d+)/$', ConsolePortView.as_view(), name='consoleport'),
|
||||
@@ -66,8 +61,7 @@ urlpatterns = [
|
||||
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/$', InterfaceConnectionListView.as_view(), name='interfaceconnection_list'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection_detail'),
|
||||
url(r'^interface-connections/(?P<pk>\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection'),
|
||||
|
||||
# Miscellaneous
|
||||
url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'),
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.shortcuts import get_object_or_404
|
||||
|
||||
from dcim.models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface,
|
||||
InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
||||
InterfaceConnection, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
|
||||
)
|
||||
from dcim import filters
|
||||
from .exceptions import MissingFilterException
|
||||
@@ -27,7 +27,7 @@ class SiteListView(generics.ListAPIView):
|
||||
"""
|
||||
List all sites
|
||||
"""
|
||||
queryset = Site.objects.select_related('tenant')
|
||||
queryset = Site.objects.all()
|
||||
serializer_class = serializers.SiteSerializer
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class SiteDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single site
|
||||
"""
|
||||
queryset = Site.objects.select_related('tenant')
|
||||
queryset = Site.objects.all()
|
||||
serializer_class = serializers.SiteSerializer
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class RackGroupListView(generics.ListAPIView):
|
||||
"""
|
||||
List all rack groups
|
||||
"""
|
||||
queryset = RackGroup.objects.select_related('site')
|
||||
queryset = RackGroup.objects.all()
|
||||
serializer_class = serializers.RackGroupSerializer
|
||||
filter_class = filters.RackGroupFilter
|
||||
|
||||
@@ -56,30 +56,10 @@ class RackGroupDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single rack group
|
||||
"""
|
||||
queryset = RackGroup.objects.select_related('site')
|
||||
queryset = RackGroup.objects.all()
|
||||
serializer_class = serializers.RackGroupSerializer
|
||||
|
||||
|
||||
#
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleListView(generics.ListAPIView):
|
||||
"""
|
||||
List all rack roles
|
||||
"""
|
||||
queryset = RackRole.objects.all()
|
||||
serializer_class = serializers.RackRoleSerializer
|
||||
|
||||
|
||||
class RackRoleDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single rack role
|
||||
"""
|
||||
queryset = RackRole.objects.all()
|
||||
serializer_class = serializers.RackRoleSerializer
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
@@ -88,7 +68,7 @@ class RackListView(generics.ListAPIView):
|
||||
"""
|
||||
List racks (filterable)
|
||||
"""
|
||||
queryset = Rack.objects.select_related('site', 'group', 'tenant')
|
||||
queryset = Rack.objects.select_related('site')
|
||||
serializer_class = serializers.RackSerializer
|
||||
filter_class = filters.RackFilter
|
||||
|
||||
@@ -97,7 +77,7 @@ class RackDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single rack
|
||||
"""
|
||||
queryset = Rack.objects.select_related('site', 'group', 'tenant')
|
||||
queryset = Rack.objects.select_related('site')
|
||||
serializer_class = serializers.RackDetailSerializer
|
||||
|
||||
|
||||
@@ -213,9 +193,8 @@ class DeviceListView(generics.ListAPIView):
|
||||
"""
|
||||
List devices (filterable)
|
||||
"""
|
||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
|
||||
'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside',
|
||||
'primary_ip6__nat_outside')
|
||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'platform', 'rack__site')\
|
||||
.prefetch_related('primary_ip4__nat_outside', 'primary_ip6__nat_outside')
|
||||
serializer_class = serializers.DeviceSerializer
|
||||
filter_class = filters.DeviceFilter
|
||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
|
||||
@@ -225,8 +204,7 @@ class DeviceDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single device
|
||||
"""
|
||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
|
||||
'rack__site', 'parent_bay')
|
||||
queryset = Device.objects.all()
|
||||
serializer_class = serializers.DeviceSerializer
|
||||
|
||||
|
||||
@@ -348,14 +326,6 @@ class InterfaceConnectionView(generics.RetrieveUpdateDestroyAPIView):
|
||||
queryset = InterfaceConnection.objects.all()
|
||||
|
||||
|
||||
class InterfaceConnectionListView(generics.ListAPIView):
|
||||
"""
|
||||
Retrieve a list of all interface connections
|
||||
"""
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
queryset = InterfaceConnection.objects.all()
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
#
|
||||
@@ -369,23 +339,18 @@ class DeviceBayListView(generics.ListAPIView):
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return DeviceBay.objects.filter(device=device).select_related('installed_device')
|
||||
queryset = DeviceBay.objects.filter(device=device).select_related('installed_device')
|
||||
|
||||
# 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()
|
||||
|
||||
#
|
||||
# Modules
|
||||
#
|
||||
|
||||
class ModuleListView(generics.ListAPIView):
|
||||
"""
|
||||
List device modules (by device)
|
||||
"""
|
||||
serializer_class = serializers.ModuleSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
device = get_object_or_404(Device, pk=self.kwargs['pk'])
|
||||
return Module.objects.filter(device=device).select_related('device', 'manufacturer')
|
||||
return queryset
|
||||
|
||||
|
||||
#
|
||||
@@ -446,36 +411,53 @@ class RelatedConnectionsView(APIView):
|
||||
return Response()
|
||||
|
||||
else:
|
||||
raise MissingFilterException(detail='Must specify search parameters "peer-device" and "peer-interface".')
|
||||
raise MissingFilterException(detail='Must specify search parameters (peer-device and peer-interface).')
|
||||
|
||||
# Initialize response skeleton
|
||||
response = {
|
||||
'device': serializers.DeviceSerializer(device).data,
|
||||
'console-ports': [],
|
||||
'power-ports': [],
|
||||
'interfaces': [],
|
||||
}
|
||||
response = dict()
|
||||
response['device'] = serializers.DeviceSerializer(device).data
|
||||
response['console-ports'] = []
|
||||
response['power-ports'] = []
|
||||
response['interfaces'] = []
|
||||
|
||||
# Console connections
|
||||
# Build console connections
|
||||
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device')
|
||||
for cp in console_ports:
|
||||
data = serializers.ConsolePortSerializer(instance=cp).data
|
||||
del(data['device'])
|
||||
response['console-ports'].append(data)
|
||||
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)
|
||||
|
||||
# Power connections
|
||||
# Build power connections
|
||||
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device')
|
||||
for pp in power_ports:
|
||||
data = serializers.PowerPortSerializer(instance=pp).data
|
||||
del(data['device'])
|
||||
response['power-ports'].append(data)
|
||||
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)
|
||||
|
||||
# Interface connections
|
||||
interfaces = Interface.objects.filter(device=device).select_related('connected_as_a', 'connected_as_b',
|
||||
'circuit')
|
||||
# Built interface connections
|
||||
interfaces = Interface.objects.filter(device=device)
|
||||
for iface in interfaces:
|
||||
data = serializers.InterfaceDetailSerializer(instance=iface).data
|
||||
del(data['device'])
|
||||
response['interfaces'].append(data)
|
||||
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)
|
||||
|
||||
@@ -4,9 +4,8 @@ from django.db.models import Q
|
||||
|
||||
from .models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
|
||||
Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
|
||||
Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
|
||||
)
|
||||
from tenancy.models import Tenant
|
||||
|
||||
|
||||
class SiteFilter(django_filters.FilterSet):
|
||||
@@ -14,27 +13,17 @@ class SiteFilter(django_filters.FilterSet):
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['q', 'name', 'facility', 'asn']
|
||||
|
||||
def search(self, queryset, value):
|
||||
value = value.strip()
|
||||
qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
|
||||
Q(shipping_address__icontains=value) | Q(comments__icontains=value)
|
||||
Q(shipping_address__icontains=value)
|
||||
try:
|
||||
qs_filter |= Q(asn=int(value.strip()))
|
||||
qs_filter |= Q(asn=int(value))
|
||||
except ValueError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
@@ -85,38 +74,16 @@ class RackFilter(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=RackRole.objects.all(),
|
||||
label='Role (ID)',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=RackRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (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) |
|
||||
Q(comments__icontains=value)
|
||||
Q(facility_id__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
@@ -135,7 +102,7 @@ class DeviceTypeFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['manufacturer_id', 'manufacturer', 'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu',
|
||||
fields = ['manufacturer_id', 'manufacturer', 'model', 'u_height', 'is_console_server', 'is_pdu',
|
||||
'is_network_device']
|
||||
|
||||
|
||||
@@ -176,17 +143,6 @@ class DeviceFilter(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device_type',
|
||||
queryset=DeviceType.objects.all(),
|
||||
@@ -244,11 +200,11 @@ class DeviceFilter(django_filters.FilterSet):
|
||||
'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) |
|
||||
Q(comments__icontains=value)
|
||||
Q(modules__serial__icontains=value)
|
||||
).distinct()
|
||||
|
||||
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
[
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Console Server",
|
||||
"slug": "console-server",
|
||||
"color": "teal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Core Switch",
|
||||
"slug": "core-switch",
|
||||
"color": "blue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Distribution Switch",
|
||||
"slug": "distribution-switch",
|
||||
"color": "blue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Access Switch",
|
||||
"slug": "access-switch",
|
||||
"color": "blue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Management Switch",
|
||||
"slug": "management-switch",
|
||||
"color": "orange"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Firewall",
|
||||
"slug": "firewall",
|
||||
"color": "red"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Router",
|
||||
"slug": "router",
|
||||
"color": "purple"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"name": "Server",
|
||||
"slug": "server",
|
||||
"color": "medium_gray"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.devicerole",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"name": "PDU",
|
||||
"slug": "pdu",
|
||||
"color": "dark_gray"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "APC",
|
||||
"slug": "apc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Cisco",
|
||||
"slug": "cisco"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Dell",
|
||||
"slug": "dell"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "HP",
|
||||
"slug": "hp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Juniper",
|
||||
"slug": "juniper"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Arista",
|
||||
"slug": "arista"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Opengear",
|
||||
"slug": "opengear"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.manufacturer",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"name": "Super Micro",
|
||||
"slug": "super-micro"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Cisco IOS",
|
||||
"slug": "cisco-ios",
|
||||
"rpc_client": "cisco-ios"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Cisco NX-OS",
|
||||
"slug": "cisco-nx-os",
|
||||
"rpc_client": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Juniper Junos",
|
||||
"slug": "juniper-junos",
|
||||
"rpc_client": "juniper-junos"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Arista EOS",
|
||||
"slug": "arista-eos",
|
||||
"rpc_client": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Linux",
|
||||
"slug": "linux",
|
||||
"rpc_client": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "dcim.platform",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Opengear",
|
||||
"slug": "opengear",
|
||||
"rpc_client": "opengear"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -4,10 +4,8 @@ from django import forms
|
||||
from django.db.models import Count, Q
|
||||
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.forms import bulkedit_tenant_choices
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, add_blank_choice, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
|
||||
APISelect, BootstrapMixin, BulkImportForm, CommentField, ConfirmationForm, CSVDataField, ExpandableNameField,
|
||||
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||
)
|
||||
|
||||
@@ -15,8 +13,7 @@ from .models import (
|
||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||
Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
|
||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackRole,
|
||||
Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
|
||||
)
|
||||
|
||||
|
||||
@@ -41,39 +38,6 @@ def get_device_by_name_or_pk(name):
|
||||
return device
|
||||
|
||||
|
||||
def bulkedit_platform_choices():
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(p.pk, p.name) for p in Platform.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
def bulkedit_rackgroup_choices():
|
||||
"""
|
||||
Include an option to remove the currently assigned group from a rack.
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(r.pk, r) for r in RackGroup.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
def bulkedit_rackrole_choices():
|
||||
"""
|
||||
Include an option to remove the currently assigned role from a rack.
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'None'),
|
||||
]
|
||||
choices += [(r.pk, r.name) for r in RackRole.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
@@ -84,7 +48,7 @@ class SiteForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
|
||||
fields = ['name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
|
||||
widgets = {
|
||||
'physical_address': SmallTextarea(attrs={'rows': 3}),
|
||||
'shipping_address': SmallTextarea(attrs={'rows': 3}),
|
||||
@@ -99,33 +63,16 @@ class SiteForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
|
||||
class SiteFromCSVForm(forms.ModelForm):
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ['name', 'slug', 'tenant', 'facility', 'asn']
|
||||
fields = ['name', 'slug', 'facility', 'asn']
|
||||
|
||||
|
||||
class SiteImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=SiteFromCSVForm)
|
||||
|
||||
|
||||
class SiteBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
|
||||
|
||||
def site_tenant_choices():
|
||||
tenant_choices = Tenant.objects.annotate(site_count=Count('sites'))
|
||||
return [(t.slug, u'{} ({})'.format(t.name, t.site_count)) for t in tenant_choices]
|
||||
|
||||
|
||||
class SiteFilterForm(forms.Form, BootstrapMixin):
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=site_tenant_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
@@ -138,9 +85,13 @@ class RackGroupForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['site', 'name', 'slug']
|
||||
|
||||
|
||||
class RackGroupBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=RackGroup.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def rackgroup_site_choices():
|
||||
site_choices = Site.objects.annotate(rack_count=Count('rack_groups'))
|
||||
return [(s.slug, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
||||
|
||||
|
||||
class RackGroupFilterForm(forms.Form, BootstrapMixin):
|
||||
@@ -148,18 +99,6 @@ class RackGroupFilterForm(forms.Form, BootstrapMixin):
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
|
||||
|
||||
#
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleForm(forms.ModelForm, BootstrapMixin):
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['name', 'slug', 'color']
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
@@ -172,7 +111,7 @@ class RackForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'comments']
|
||||
fields = ['site', 'group', 'name', 'facility_id', 'u_height', 'comments']
|
||||
help_texts = {
|
||||
'site': "The site at which the rack exists",
|
||||
'name': "Organizational rack name",
|
||||
@@ -200,15 +139,10 @@ class RackFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
group_name = forms.CharField(required=False)
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
role = forms.ModelChoiceField(RackRole.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Role not found.'})
|
||||
type = forms.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height']
|
||||
fields = ['site', 'group_name', 'name', 'facility_id', 'u_height']
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -222,19 +156,6 @@ class RackFromCSVForm(forms.ModelForm):
|
||||
except RackGroup.DoesNotExist:
|
||||
self.add_error('group_name', "Invalid rack group ({})".format(group))
|
||||
|
||||
def clean_type(self):
|
||||
rack_type = self.cleaned_data['type']
|
||||
if not rack_type:
|
||||
return None
|
||||
try:
|
||||
choices = {v.lower(): k for k, v in RACK_TYPE_CHOICES}
|
||||
return choices[rack_type.lower()]
|
||||
except KeyError:
|
||||
raise forms.ValidationError('Invalid rack type ({}). Valid choices are: {}.'.format(
|
||||
rack_type,
|
||||
', '.join({v: k for k, v in RACK_TYPE_CHOICES}),
|
||||
))
|
||||
|
||||
|
||||
class RackImportForm(BulkImportForm, BootstrapMixin):
|
||||
csv = CSVDataField(csv_form=RackFromCSVForm)
|
||||
@@ -242,34 +163,24 @@ class RackImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
class RackBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
|
||||
group = forms.TypedChoiceField(choices=bulkedit_rackgroup_choices, coerce=int, required=False, label='Group')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
role = forms.TypedChoiceField(choices=bulkedit_rackrole_choices, coerce=int, required=False, label='Role')
|
||||
type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
|
||||
width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
|
||||
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, u'{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
|
||||
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.pk, u'{} ({})'.format(g, g.rack_count)) for g in group_choices]
|
||||
|
||||
|
||||
def rack_tenant_choices():
|
||||
tenant_choices = Tenant.objects.annotate(rack_count=Count('racks'))
|
||||
return [(t.slug, u'{} ({})'.format(t.name, t.rack_count)) for t in tenant_choices]
|
||||
|
||||
|
||||
def rack_role_choices():
|
||||
role_choices = RackRole.objects.annotate(rack_count=Count('racks'))
|
||||
return [(r.slug, u'{} ({})'.format(r.name, r.rack_count)) for r in role_choices]
|
||||
return [(g.pk, '{} ({})'.format(g, g.rack_count)) for g in group_choices]
|
||||
|
||||
|
||||
class RackFilterForm(forms.Form, BootstrapMixin):
|
||||
@@ -277,10 +188,6 @@ class RackFilterForm(forms.Form, BootstrapMixin):
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
role = forms.MultipleChoiceField(required=False, choices=rack_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
|
||||
|
||||
#
|
||||
@@ -295,6 +202,10 @@ class ManufacturerForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class ManufacturerBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Manufacturer.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Device types
|
||||
#
|
||||
@@ -304,8 +215,8 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
|
||||
'is_pdu', 'is_network_device', 'subdevice_role']
|
||||
fields = ['manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role']
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
||||
@@ -314,9 +225,13 @@ class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
|
||||
u_height = forms.IntegerField(min_value=1, required=False)
|
||||
|
||||
|
||||
class DeviceTypeBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def devicetype_manufacturer_choices():
|
||||
manufacturer_choices = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
|
||||
return [(m.slug, u'{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
|
||||
return [(m.slug, '{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
|
||||
|
||||
|
||||
class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
|
||||
@@ -388,6 +303,10 @@ class DeviceRoleForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name', 'slug', 'color']
|
||||
|
||||
|
||||
class DeviceRoleBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=DeviceRole.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Platforms
|
||||
#
|
||||
@@ -400,6 +319,10 @@ class PlatformForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class PlatformBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Platform.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Devices
|
||||
#
|
||||
@@ -425,7 +348,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['name', 'device_role', 'tenant', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
|
||||
fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
|
||||
'platform', 'primary_ip4', 'primary_ip6', 'comments']
|
||||
help_texts = {
|
||||
'device_role': "The function this device serves",
|
||||
@@ -450,10 +373,10 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
||||
for family in [4, 6]:
|
||||
ip_choices = []
|
||||
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
|
||||
ip_choices += [(ip.id, u'{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||
ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
|
||||
nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
|
||||
.select_related('nat_inside__interface')
|
||||
ip_choices += [(ip.id, u'{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
||||
ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
|
||||
self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
|
||||
|
||||
else:
|
||||
@@ -465,7 +388,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
||||
self.fields['primary_ip6'].widget.attrs['readonly'] = True
|
||||
|
||||
# Limit rack choices
|
||||
if self.is_bound and self.data.get('site'):
|
||||
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'])
|
||||
@@ -473,8 +396,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
||||
self.fields['rack'].choices = []
|
||||
|
||||
# Rack position
|
||||
pk = self.instance.pk if self.instance.pk else None
|
||||
try:
|
||||
pk = self.instance.pk if self.instance.pk else None
|
||||
if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
|
||||
position_choices = Rack.objects.get(pk=self.data['rack'])\
|
||||
.get_rack_units(face=self.data.get('face'), exclude=pk)
|
||||
@@ -502,19 +425,10 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
|
||||
else:
|
||||
self.fields['device_type'].choices = []
|
||||
|
||||
# Disable rack assignment if this is a child device installed in a parent device
|
||||
if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
|
||||
self.fields['site'].disabled = True
|
||||
self.fields['rack'].disabled = True
|
||||
self.initial['site'] = self.instance.parent_bay.device.rack.site_id
|
||||
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
||||
|
||||
|
||||
class BaseDeviceFromCSVForm(forms.ModelForm):
|
||||
device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid device role.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid manufacturer.'})
|
||||
model_name = forms.CharField()
|
||||
@@ -546,8 +460,8 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
face = forms.CharField(required=False)
|
||||
|
||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'site',
|
||||
'rack_name', 'position', 'face']
|
||||
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
|
||||
'position', 'face']
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -582,7 +496,7 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
device_bay_name = forms.CharField(required=False)
|
||||
|
||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'parent',
|
||||
fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'parent',
|
||||
'device_bay_name']
|
||||
|
||||
def clean(self):
|
||||
@@ -617,41 +531,39 @@ 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')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
platform = forms.TypedChoiceField(choices=bulkedit_platform_choices, coerce=int, required=False,
|
||||
label='Platform')
|
||||
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=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
|
||||
serial = forms.CharField(max_length=50, required=False, label='Serial Number')
|
||||
|
||||
|
||||
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, u'{} ({})'.format(s.name, s.device_count)) for s in site_choices]
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
|
||||
|
||||
|
||||
def device_rack_group_choices():
|
||||
group_choices = RackGroup.objects.select_related('site').annotate(device_count=Count('racks__devices'))
|
||||
return [(g.pk, u'{} ({})'.format(g, g.device_count)) for g in group_choices]
|
||||
return [(g.pk, '{} ({})'.format(g, g.device_count)) for g in group_choices]
|
||||
|
||||
|
||||
def device_role_choices():
|
||||
role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
|
||||
return [(r.slug, u'{} ({})'.format(r.name, r.device_count)) for r in role_choices]
|
||||
|
||||
|
||||
def device_tenant_choices():
|
||||
tenant_choices = Tenant.objects.annotate(device_count=Count('devices'))
|
||||
return [(t.slug, u'{} ({})'.format(t.name, t.device_count)) for t in tenant_choices]
|
||||
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.pk, u'{} ({})'.format(t, t.device_count)) for t in type_choices]
|
||||
return [(t.pk, '{} ({})'.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, u'{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
|
||||
return [(p.slug, '{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
|
||||
|
||||
|
||||
class DeviceFilterForm(forms.Form, BootstrapMixin):
|
||||
@@ -661,8 +573,6 @@ class DeviceFilterForm(forms.Form, BootstrapMixin):
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=device_tenant_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
|
||||
@@ -1319,4 +1229,4 @@ class ModuleForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = ['name', 'manufacturer', 'part_id', 'serial']
|
||||
fields = ['name', 'part_id', 'serial']
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-07-26 15:05
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0010_devicebay_installed_device_set_null'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='part_number',
|
||||
field=models.CharField(blank=True, help_text=b'Discrete part number (optional)', max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -1,32 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-07-26 21:59
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0001_initial'),
|
||||
('dcim', '0011_devicetype_part_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'),
|
||||
),
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-06 20:24
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0012_site_rack_device_add_tenant'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interface',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interfacetemplate',
|
||||
name='form_factor',
|
||||
field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200),
|
||||
),
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-08 21:11
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0013_add_interface_form_factors'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[(100, b'2-post frame'), (200, b'4-post frame'), (300, b'4-post cabinet'), (1000, b'Wall-mounted frame'), (1100, b'Wall-mounted cabinet')], null=True, verbose_name=b'Type'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='width',
|
||||
field=models.PositiveSmallIntegerField(choices=[(19, b'19 inches'), (23, b'23 inches')], default=19, help_text=b'Rail-to-rail width', verbose_name=b'Width'),
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-09 21:18
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0014_rack_add_type_width'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rack',
|
||||
name='u_height',
|
||||
field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name=b'Height (U)'),
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-10 13:45
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0015_rack_add_u_height_validator'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='module',
|
||||
name='manufacturer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modules', to='dcim.Manufacturer'),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-08-10 14:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0016_module_add_manufacturer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RackRole',
|
||||
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.AddField(
|
||||
model_name='rack',
|
||||
name='role',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.RackRole'),
|
||||
),
|
||||
]
|
||||
@@ -1,41 +1,18 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import MultipleObjectsReturned, ValidationError
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q, ObjectDoesNotExist
|
||||
|
||||
from extras.rpc import RPC_CLIENTS
|
||||
from tenancy.models import Tenant
|
||||
from utilities.fields import NullableCharField
|
||||
from utilities.managers import NaturalOrderByManager
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
|
||||
from .fields import ASNField, MACAddressField
|
||||
|
||||
|
||||
RACK_TYPE_2POST = 100
|
||||
RACK_TYPE_4POST = 200
|
||||
RACK_TYPE_CABINET = 300
|
||||
RACK_TYPE_WALLFRAME = 1000
|
||||
RACK_TYPE_WALLCABINET = 1100
|
||||
RACK_TYPE_CHOICES = (
|
||||
(RACK_TYPE_2POST, '2-post frame'),
|
||||
(RACK_TYPE_4POST, '4-post frame'),
|
||||
(RACK_TYPE_CABINET, '4-post cabinet'),
|
||||
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
|
||||
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
|
||||
)
|
||||
|
||||
RACK_WIDTH_19IN = 19
|
||||
RACK_WIDTH_23IN = 23
|
||||
RACK_WIDTH_CHOICES = (
|
||||
(RACK_WIDTH_19IN, '19 inches'),
|
||||
(RACK_WIDTH_23IN, '23 inches'),
|
||||
)
|
||||
|
||||
RACK_FACE_FRONT = 0
|
||||
RACK_FACE_REAR = 1
|
||||
RACK_FACE_CHOICES = [
|
||||
@@ -61,7 +38,7 @@ COLOR_RED = 'red'
|
||||
COLOR_GRAY1 = 'light_gray'
|
||||
COLOR_GRAY2 = 'medium_gray'
|
||||
COLOR_GRAY3 = 'dark_gray'
|
||||
ROLE_COLOR_CHOICES = [
|
||||
DEVICE_ROLE_COLOR_CHOICES = [
|
||||
[COLOR_TEAL, 'Teal'],
|
||||
[COLOR_GREEN, 'Green'],
|
||||
[COLOR_BLUE, 'Blue'],
|
||||
@@ -77,63 +54,20 @@ ROLE_COLOR_CHOICES = [
|
||||
IFACE_FF_VIRTUAL = 0
|
||||
IFACE_FF_100M_COPPER = 800
|
||||
IFACE_FF_1GE_COPPER = 1000
|
||||
IFACE_FF_GBIC = 1050
|
||||
IFACE_FF_SFP = 1100
|
||||
IFACE_FF_10GE_COPPER = 1150
|
||||
IFACE_FF_SFP_PLUS = 1200
|
||||
IFACE_FF_XFP = 1300
|
||||
IFACE_FF_QSFP_PLUS = 1400
|
||||
IFACE_FF_CFP = 1500
|
||||
IFACE_FF_QSFP28 = 1600
|
||||
IFACE_FF_T1 = 4000
|
||||
IFACE_FF_E1 = 4010
|
||||
IFACE_FF_T3 = 4040
|
||||
IFACE_FF_E3 = 4050
|
||||
IFACE_FF_STACKWISE = 5000
|
||||
IFACE_FF_STACKWISE_PLUS = 5050
|
||||
IFACE_FF_CHOICES = [
|
||||
[
|
||||
'Virtual interfaces',
|
||||
[
|
||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Ethernet',
|
||||
[
|
||||
[IFACE_FF_100M_COPPER, '100BASE-TX (10/100M)'],
|
||||
[IFACE_FF_1GE_COPPER, '1000BASE-T (1GE)'],
|
||||
[IFACE_FF_10GE_COPPER, '10GBASE-T (10GE)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Modular',
|
||||
[
|
||||
[IFACE_FF_GBIC, 'GBIC (1GE)'],
|
||||
[IFACE_FF_SFP, 'SFP (1GE)'],
|
||||
[IFACE_FF_XFP, 'XFP (10GE)'],
|
||||
[IFACE_FF_SFP_PLUS, 'SFP+ (10GE)'],
|
||||
[IFACE_FF_QSFP_PLUS, 'QSFP+ (40GE)'],
|
||||
[IFACE_FF_CFP, 'CFP (100GE)'],
|
||||
[IFACE_FF_QSFP28, 'QSFP28 (100GE)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Serial',
|
||||
[
|
||||
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
|
||||
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
|
||||
[IFACE_FF_T3, 'T3 (45 Mbps)'],
|
||||
[IFACE_FF_E3, 'E3 (34 Mbps)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'Stacking',
|
||||
[
|
||||
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
|
||||
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
|
||||
]
|
||||
],
|
||||
[IFACE_FF_VIRTUAL, 'Virtual'],
|
||||
[IFACE_FF_100M_COPPER, '10/100M (100BASE-TX)'],
|
||||
[IFACE_FF_1GE_COPPER, '1GE (1000BASE-T)'],
|
||||
[IFACE_FF_SFP, '1GE (SFP)'],
|
||||
[IFACE_FF_10GE_COPPER, '10GE (10GBASE-T)'],
|
||||
[IFACE_FF_SFP_PLUS, '10GE (SFP+)'],
|
||||
[IFACE_FF_XFP, '10GE (XFP)'],
|
||||
[IFACE_FF_QSFP_PLUS, '40GE (QSFP+)'],
|
||||
]
|
||||
|
||||
STATUS_ACTIVE = True
|
||||
@@ -203,16 +137,6 @@ def order_interfaces(queryset, sql_col, primary_ordering=tuple()):
|
||||
}).order_by(*ordering)
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteManager(NaturalOrderByManager):
|
||||
|
||||
def get_queryset(self):
|
||||
return self.natural_order_by('name')
|
||||
|
||||
|
||||
class Site(CreatedUpdatedModel):
|
||||
"""
|
||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||
@@ -220,15 +144,12 @@ class Site(CreatedUpdatedModel):
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='sites', on_delete=models.PROTECT)
|
||||
facility = models.CharField(max_length=50, blank=True)
|
||||
asn = ASNField(blank=True, null=True, verbose_name='ASN')
|
||||
physical_address = models.CharField(max_length=200, blank=True)
|
||||
shipping_address = models.CharField(max_length=200, blank=True)
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
objects = SiteManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -242,7 +163,6 @@ class Site(CreatedUpdatedModel):
|
||||
return ','.join([
|
||||
self.name,
|
||||
self.slug,
|
||||
self.tenant.name if self.tenant else '',
|
||||
self.facility,
|
||||
str(self.asn),
|
||||
])
|
||||
@@ -268,10 +188,6 @@ class Site(CreatedUpdatedModel):
|
||||
return self.circuits.count()
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackGroup(models.Model):
|
||||
"""
|
||||
Racks can be grouped as subsets within a Site. The scope of a group will depend on how Sites are defined. For
|
||||
@@ -290,36 +206,12 @@ class RackGroup(models.Model):
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
return u'{} - {}'.format(self.site.name, self.name)
|
||||
return '{} - {}'.format(self.site.name, self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||
|
||||
|
||||
class RackRole(models.Model):
|
||||
"""
|
||||
Racks can be organized by functional role, similar to Devices.
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?role={}".format(reverse('dcim:rack_list'), self.slug)
|
||||
|
||||
|
||||
class RackManager(NaturalOrderByManager):
|
||||
|
||||
def get_queryset(self):
|
||||
return self.natural_order_by('site__name', 'name')
|
||||
|
||||
|
||||
class Rack(CreatedUpdatedModel):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
@@ -329,17 +221,9 @@ class Rack(CreatedUpdatedModel):
|
||||
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)
|
||||
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
|
||||
role = models.ForeignKey('RackRole', related_name='racks', blank=True, null=True, on_delete=models.PROTECT)
|
||||
type = models.PositiveSmallIntegerField(choices=RACK_TYPE_CHOICES, blank=True, null=True, verbose_name='Type')
|
||||
width = models.PositiveSmallIntegerField(choices=RACK_WIDTH_CHOICES, default=RACK_WIDTH_19IN, verbose_name='Width',
|
||||
help_text='Rail-to-rail width')
|
||||
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)',
|
||||
validators=[MinValueValidator(1), MaxValueValidator(100)])
|
||||
u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
objects = RackManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
@@ -370,10 +254,6 @@ class Rack(CreatedUpdatedModel):
|
||||
self.group.name if self.group else '',
|
||||
self.name,
|
||||
self.facility_id or '',
|
||||
self.tenant.name if self.tenant else '',
|
||||
self.role.name if self.role else '',
|
||||
self.get_type_display() if self.type else '',
|
||||
self.width,
|
||||
str(self.u_height),
|
||||
])
|
||||
|
||||
@@ -462,15 +342,6 @@ class Rack(CreatedUpdatedModel):
|
||||
def get_0u_devices(self):
|
||||
return self.devices.filter(position=0)
|
||||
|
||||
def get_utilization(self):
|
||||
"""
|
||||
Determine the utilization rate of the rack and return it as a percentage.
|
||||
"""
|
||||
if self.u_consumed is None:
|
||||
self.u_consumed = 0
|
||||
u_available = self.u_height - self.u_consumed
|
||||
return int(float(self.u_height - u_available) / self.u_height * 100)
|
||||
|
||||
|
||||
#
|
||||
# Device Types
|
||||
@@ -511,7 +382,6 @@ class DeviceType(models.Model):
|
||||
manufacturer = models.ForeignKey('Manufacturer', related_name='device_types', on_delete=models.PROTECT)
|
||||
model = models.CharField(max_length=50)
|
||||
slug = models.SlugField()
|
||||
part_number = models.CharField(max_length=50, blank=True, help_text="Discrete part number (optional)")
|
||||
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")
|
||||
@@ -534,7 +404,7 @@ class DeviceType(models.Model):
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
return u'{} {}'.format(self.manufacturer, self.model)
|
||||
return "{} {}".format(self.manufacturer, self.model)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:devicetype', args=[self.pk])
|
||||
@@ -681,7 +551,7 @@ class DeviceRole(models.Model):
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
color = models.CharField(max_length=30, choices=ROLE_COLOR_CHOICES)
|
||||
color = models.CharField(max_length=30, choices=DEVICE_ROLE_COLOR_CHOICES)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@@ -713,12 +583,6 @@ class Platform(models.Model):
|
||||
return "{}?platform={}".format(reverse('dcim:device_list'), self.slug)
|
||||
|
||||
|
||||
class DeviceManager(NaturalOrderByManager):
|
||||
|
||||
def get_queryset(self):
|
||||
return self.natural_order_by('name')
|
||||
|
||||
|
||||
class Device(CreatedUpdatedModel):
|
||||
"""
|
||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||
@@ -733,7 +597,6 @@ class Device(CreatedUpdatedModel):
|
||||
"""
|
||||
device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
|
||||
device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
|
||||
tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='devices', on_delete=models.PROTECT)
|
||||
platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
|
||||
name = NullableCharField(max_length=50, blank=True, null=True, unique=True)
|
||||
serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
|
||||
@@ -749,8 +612,6 @@ class Device(CreatedUpdatedModel):
|
||||
blank=True, null=True, verbose_name='Primary IPv6')
|
||||
comments = models.TextField(blank=True)
|
||||
|
||||
objects = DeviceManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = ['rack', 'position', 'face']
|
||||
@@ -827,7 +688,6 @@ class Device(CreatedUpdatedModel):
|
||||
return ','.join([
|
||||
self.name or '',
|
||||
self.device_role.name,
|
||||
self.tenant.name if self.tenant else '',
|
||||
self.device_type.manufacturer.name,
|
||||
self.device_type.model,
|
||||
self.platform.name if self.platform else '',
|
||||
@@ -1062,8 +922,8 @@ class Interface(models.Model):
|
||||
return connection.interface_a
|
||||
except InterfaceConnection.DoesNotExist:
|
||||
return None
|
||||
except InterfaceConnection.MultipleObjectsReturned:
|
||||
raise MultipleObjectsReturned("Multiple connections found for {} interface {}!".format(self.device, self))
|
||||
except InterfaceConnection.MultipleObjectsReturned as e:
|
||||
raise e("Multiple connections found for {0} interface {1}!".format(self.device, self))
|
||||
|
||||
|
||||
class InterfaceConnection(models.Model):
|
||||
@@ -1105,7 +965,7 @@ class DeviceBay(models.Model):
|
||||
unique_together = ['device', 'name']
|
||||
|
||||
def __unicode__(self):
|
||||
return u'{} - {}'.format(self.device.name, self.name)
|
||||
return '{} - {}'.format(self.device.name, self.name)
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -1127,8 +987,6 @@ class Module(models.Model):
|
||||
device = models.ForeignKey('Device', related_name='modules', on_delete=models.CASCADE)
|
||||
parent = models.ForeignKey('self', related_name='submodules', blank=True, null=True, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=50, verbose_name='Name')
|
||||
manufacturer = models.ForeignKey('Manufacturer', related_name='modules', blank=True, null=True,
|
||||
on_delete=models.PROTECT)
|
||||
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)
|
||||
discovered = models.BooleanField(default=False, verbose_name='Discovered')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from utilities.tables import BaseTable, ColorColumn, ToggleColumn
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||
@@ -16,33 +16,27 @@ DEVICE_LINK = """
|
||||
</a>
|
||||
"""
|
||||
|
||||
RACKGROUP_ACTIONS = """
|
||||
RACKGROUP_EDIT_LINK = """
|
||||
{% if perms.dcim.change_rackgroup %}
|
||||
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}">Edit</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACKROLE_ACTIONS = """
|
||||
{% if perms.dcim.change_rackrole %}
|
||||
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_ACTIONS = """
|
||||
DEVICEROLE_EDIT_LINK = """
|
||||
{% if perms.dcim.change_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}">Edit</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
MANUFACTURER_ACTIONS = """
|
||||
MANUFACTURER_EDIT_LINK = """
|
||||
{% if perms.dcim.change_manufacturer %}
|
||||
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}">Edit</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
PLATFORM_ACTIONS = """
|
||||
PLATFORM_EDIT_LINK = """
|
||||
{% if perms.dcim.change_platform %}
|
||||
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'dcim:platform_edit' slug=record.slug %}">Edit</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -54,21 +48,14 @@ STATUS_ICON = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph record.get_utilization %}
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
#
|
||||
|
||||
class SiteTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
|
||||
facility = tables.Column(verbose_name='Facility')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
asn = tables.Column(verbose_name='ASN')
|
||||
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
|
||||
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
|
||||
@@ -78,8 +65,8 @@ class SiteTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Site
|
||||
fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
||||
'vlan_count', 'circuit_count')
|
||||
fields = ('name', 'facility', 'asn', 'rack_count', 'device_count', 'prefix_count', 'vlan_count',
|
||||
'circuit_count')
|
||||
|
||||
|
||||
#
|
||||
@@ -92,30 +79,11 @@ class RackGroupTable(BaseTable):
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
rack_count = tables.Column(verbose_name='Racks')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=RACKGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
edit = tables.TemplateColumn(template_code=RACKGROUP_EDIT_LINK, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackGroup
|
||||
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
rack_count = tables.Column(verbose_name='Racks')
|
||||
color = ColorColumn(verbose_name='Color')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackGroup
|
||||
fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'edit')
|
||||
|
||||
|
||||
#
|
||||
@@ -128,30 +96,12 @@ class RackTable(BaseTable):
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
facility_id = tables.Column(verbose_name='Facility ID')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
role = tables.Column(verbose_name='Role')
|
||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
|
||||
u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used')
|
||||
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'u_consumed',
|
||||
'utilization')
|
||||
|
||||
|
||||
class RackImportTable(BaseTable):
|
||||
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(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
facility_id = tables.Column(verbose_name='Facility ID')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
u_height = tables.Column(verbose_name='Height (U)')
|
||||
devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
|
||||
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'u_height', 'devices')
|
||||
|
||||
|
||||
#
|
||||
@@ -163,12 +113,11 @@ class ManufacturerTable(BaseTable):
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
devicetype_count = tables.Column(verbose_name='Device Types')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
edit = tables.TemplateColumn(template_code=MANUFACTURER_EDIT_LINK, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Manufacturer
|
||||
fields = ('pk', 'name', 'devicetype_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'devicetype_count', 'slug', 'edit')
|
||||
|
||||
|
||||
#
|
||||
@@ -177,77 +126,93 @@ class ManufacturerTable(BaseTable):
|
||||
|
||||
class DeviceTypeTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
manufacturer = tables.Column(verbose_name='Manufacturer')
|
||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
||||
part_number = tables.Column(verbose_name='Part Number')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceType
|
||||
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height')
|
||||
fields = ('pk', 'model', 'manufacturer', 'u_height')
|
||||
|
||||
|
||||
#
|
||||
# Device type components
|
||||
#
|
||||
|
||||
class ConsolePortTemplateTable(BaseTable):
|
||||
class ConsolePortTemplateTable(tables.Table):
|
||||
pk = ToggleColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateTable(BaseTable):
|
||||
class ConsoleServerPortTemplateTable(tables.Table):
|
||||
pk = ToggleColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class PowerPortTemplateTable(BaseTable):
|
||||
class PowerPortTemplateTable(tables.Table):
|
||||
pk = ToggleColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletTemplateTable(BaseTable):
|
||||
class PowerOutletTemplateTable(tables.Table):
|
||||
pk = ToggleColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class InterfaceTemplateTable(BaseTable):
|
||||
class InterfaceTemplateTable(tables.Table):
|
||||
pk = ToggleColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ('pk', 'name', 'form_factor')
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
attrs = {
|
||||
'class': 'table table-hover panel-body',
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayTemplateTable(BaseTable):
|
||||
class DeviceBayTemplateTable(tables.Table):
|
||||
pk = ToggleColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
attrs = {
|
||||
'class': 'table table-hover panel-body',
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
@@ -258,14 +223,13 @@ class DeviceRoleTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
device_count = tables.Column(verbose_name='Devices')
|
||||
color = ColorColumn(verbose_name='Color')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
color = tables.Column(verbose_name='Color')
|
||||
edit = tables.TemplateColumn(template_code=DEVICEROLE_EDIT_LINK, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceRole
|
||||
fields = ('pk', 'name', 'device_count', 'color', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'device_count', 'slug', 'color')
|
||||
|
||||
|
||||
#
|
||||
@@ -277,11 +241,11 @@ class PlatformTable(BaseTable):
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
device_count = tables.Column(verbose_name='Devices')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
edit = tables.TemplateColumn(template_code=PLATFORM_EDIT_LINK, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Platform
|
||||
fields = ('pk', 'name', 'device_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'device_count', 'slug', 'edit')
|
||||
|
||||
|
||||
#
|
||||
@@ -292,7 +256,6 @@ class DeviceTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
site = tables.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')
|
||||
@@ -302,12 +265,11 @@ class DeviceTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||
fields = ('pk', 'name', 'status', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||
|
||||
|
||||
class DeviceImportTable(BaseTable):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
site = tables.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')
|
||||
@@ -316,7 +278,7 @@ class DeviceImportTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = ('name', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
fields = ('name', 'site', 'rack', 'position', 'device_role', 'device_type')
|
||||
empty_text = False
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ class SiteTest(APITestCase):
|
||||
'id',
|
||||
'name',
|
||||
'slug',
|
||||
'tenant',
|
||||
'facility',
|
||||
'asn',
|
||||
'physical_address',
|
||||
@@ -41,10 +40,6 @@ class SiteTest(APITestCase):
|
||||
'display_name',
|
||||
'site',
|
||||
'group',
|
||||
'tenant',
|
||||
'role',
|
||||
'type',
|
||||
'width',
|
||||
'u_height',
|
||||
'comments'
|
||||
]
|
||||
@@ -120,10 +115,6 @@ class RackTest(APITestCase):
|
||||
'display_name',
|
||||
'site',
|
||||
'group',
|
||||
'tenant',
|
||||
'role',
|
||||
'type',
|
||||
'width',
|
||||
'u_height',
|
||||
'comments'
|
||||
]
|
||||
@@ -135,10 +126,6 @@ class RackTest(APITestCase):
|
||||
'display_name',
|
||||
'site',
|
||||
'group',
|
||||
'tenant',
|
||||
'role',
|
||||
'type',
|
||||
'width',
|
||||
'u_height',
|
||||
'comments',
|
||||
'front_units',
|
||||
@@ -217,7 +204,6 @@ class DeviceTypeTest(APITestCase):
|
||||
'manufacturer',
|
||||
'model',
|
||||
'slug',
|
||||
'part_number',
|
||||
'u_height',
|
||||
'is_full_depth',
|
||||
'is_console_server',
|
||||
@@ -324,7 +310,6 @@ class DeviceTest(APITestCase):
|
||||
'display_name',
|
||||
'device_type',
|
||||
'device_role',
|
||||
'tenant',
|
||||
'platform',
|
||||
'serial',
|
||||
'rack',
|
||||
@@ -402,7 +387,6 @@ class DeviceTest(APITestCase):
|
||||
'rack_name',
|
||||
'serial',
|
||||
'status',
|
||||
'tenant',
|
||||
]
|
||||
|
||||
response = self.client.get(endpoint)
|
||||
|
||||
@@ -13,67 +13,59 @@ class DeviceTestCase(TestCase):
|
||||
|
||||
def test_racked_device(self):
|
||||
test = DeviceForm(data={
|
||||
'name': 'test',
|
||||
'device_role': get_id(DeviceRole, 'leaf-switch'),
|
||||
'tenant': None,
|
||||
'manufacturer': get_id(Manufacturer, 'juniper'),
|
||||
'device_type': get_id(DeviceType, 'qfx5100-48s'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'rack': '1',
|
||||
'face': RACK_FACE_FRONT,
|
||||
'position': 41,
|
||||
'platform': get_id(Platform, 'juniper-junos'),
|
||||
'status': STATUS_ACTIVE,
|
||||
'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={
|
||||
'name': 'test',
|
||||
'device_role': get_id(DeviceRole, 'leaf-switch'),
|
||||
'tenant': None,
|
||||
'manufacturer': get_id(Manufacturer, 'juniper'),
|
||||
'device_type': get_id(DeviceType, 'qfx5100-48s'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'rack': '1',
|
||||
'face': RACK_FACE_FRONT,
|
||||
'position': 1,
|
||||
'platform': get_id(Platform, 'juniper-junos'),
|
||||
'status': STATUS_ACTIVE,
|
||||
'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={
|
||||
'name': 'test',
|
||||
'device_role': get_id(DeviceRole, 'pdu'),
|
||||
'tenant': None,
|
||||
'manufacturer': get_id(Manufacturer, 'servertech'),
|
||||
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'rack': '1',
|
||||
'face': None,
|
||||
'position': None,
|
||||
'platform': None,
|
||||
'status': STATUS_ACTIVE,
|
||||
'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={
|
||||
'name': 'test',
|
||||
'device_role': get_id(DeviceRole, 'pdu'),
|
||||
'tenant': None,
|
||||
'manufacturer': get_id(Manufacturer, 'servertech'),
|
||||
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
|
||||
'name': 'test',
|
||||
'site': get_id(Site, 'test1'),
|
||||
'rack': '1',
|
||||
'face': RACK_FACE_REAR,
|
||||
'position': None,
|
||||
'platform': None,
|
||||
'status': STATUS_ACTIVE,
|
||||
'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())
|
||||
|
||||
@@ -15,7 +15,6 @@ urlpatterns = [
|
||||
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
|
||||
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
|
||||
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
|
||||
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||
@@ -26,12 +25,6 @@ urlpatterns = [
|
||||
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
||||
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||
|
||||
# Rack roles
|
||||
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||
url(r'^rack-roles/add/$', views.RackRoleEditView.as_view(), name='rackrole_add'),
|
||||
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
|
||||
# Racks
|
||||
url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
|
||||
url(r'^racks/add/$', views.RackEditView.as_view(), name='rack_add'),
|
||||
@@ -57,29 +50,31 @@ urlpatterns = [
|
||||
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
||||
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||
|
||||
# Console port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(), name='devicetype_add_consoleport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
|
||||
|
||||
# Console server port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(), name='devicetype_add_consoleserverport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
|
||||
|
||||
# Power port templates
|
||||
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(), name='devicetype_add_powerport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
|
||||
|
||||
# Power outlet templates
|
||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(), name='devicetype_add_poweroutlet'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
|
||||
|
||||
# Interface templates
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
||||
|
||||
# Device bay templates
|
||||
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(), name='devicetype_add_devicebay'),
|
||||
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
|
||||
# Component templates
|
||||
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(),
|
||||
name='devicetype_add_consoleport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.component_template_delete,
|
||||
{'model': ConsolePortTemplate}, name='devicetype_delete_consoleport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(),
|
||||
name='devicetype_add_consoleserverport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.component_template_delete,
|
||||
{'model': ConsoleServerPortTemplate}, name='devicetype_delete_consoleserverport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(),
|
||||
name='devicetype_add_powerport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.component_template_delete,
|
||||
{'model': PowerPortTemplate}, name='devicetype_delete_powerport'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(),
|
||||
name='devicetype_add_poweroutlet'),
|
||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.component_template_delete,
|
||||
{'model': PowerOutletTemplate}, name='devicetype_delete_poweroutlet'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(),
|
||||
name='devicetype_add_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.component_template_delete,
|
||||
{'model': InterfaceTemplate}, name='devicetype_delete_interface'),
|
||||
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(),
|
||||
name='devicetype_add_devicebay'),
|
||||
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.component_template_delete,
|
||||
{'model': DeviceBayTemplate}, name='devicetype_delete_devicebay'),
|
||||
|
||||
# Device roles
|
||||
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||
@@ -110,7 +105,6 @@ urlpatterns = [
|
||||
|
||||
# Console ports
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
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'),
|
||||
@@ -118,7 +112,6 @@ urlpatterns = [
|
||||
|
||||
# Console server ports
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.consoleserverport_add, name='consoleserverport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
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'),
|
||||
@@ -126,7 +119,6 @@ urlpatterns = [
|
||||
|
||||
# Power ports
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.powerport_add, name='powerport_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
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'),
|
||||
@@ -134,7 +126,6 @@ urlpatterns = [
|
||||
|
||||
# Power outlets
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.poweroutlet_add, name='poweroutlet_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
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'),
|
||||
@@ -142,7 +133,6 @@ urlpatterns = [
|
||||
|
||||
# Device bays
|
||||
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.devicebay_add, name='devicebay_add'),
|
||||
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.devicebay_edit, name='devicebay_edit'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.devicebay_delete, name='devicebay_delete'),
|
||||
url(r'^device-bays/(?P<pk>\d+)/populate/$', views.devicebay_populate, name='devicebay_populate'),
|
||||
@@ -157,9 +147,8 @@ urlpatterns = [
|
||||
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_add_multi'),
|
||||
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+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
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'),
|
||||
|
||||
@@ -7,8 +7,8 @@ from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Count, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models import Count, ProtectedError
|
||||
from django.forms import ModelMultipleChoiceField, MultipleHiddenInput
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.http import urlencode
|
||||
@@ -16,7 +16,8 @@ from django.views.generic import View
|
||||
|
||||
from ipam.models import Prefix, IPAddress, VLAN
|
||||
from circuits.models import Circuit
|
||||
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from extras.models import TopologyMap
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
@@ -27,7 +28,7 @@ from .models import (
|
||||
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackRole, Site,
|
||||
Site,
|
||||
)
|
||||
|
||||
|
||||
@@ -62,11 +63,9 @@ def expand_pattern(string):
|
||||
#
|
||||
|
||||
class SiteListView(ObjectListView):
|
||||
queryset = Site.objects.select_related('tenant')
|
||||
queryset = Site.objects.all()
|
||||
filter = filters.SiteFilter
|
||||
filter_form = forms.SiteFilterForm
|
||||
table = tables.SiteTable
|
||||
edit_permissions = ['dcim.change_rack', 'dcim.delete_rack']
|
||||
template_name = 'dcim/site_list.html'
|
||||
|
||||
|
||||
@@ -82,14 +81,12 @@ def site(request, slug):
|
||||
}
|
||||
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
|
||||
topology_maps = TopologyMap.objects.filter(site=site)
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
|
||||
|
||||
return render(request, 'dcim/site.html', {
|
||||
'site': site,
|
||||
'stats': stats,
|
||||
'rack_groups': rack_groups,
|
||||
'topology_maps': topology_maps,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
|
||||
|
||||
@@ -115,30 +112,12 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
obj_list_url = 'dcim:site_list'
|
||||
|
||||
|
||||
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_site'
|
||||
cls = Site
|
||||
form = forms.SiteBulkEditForm
|
||||
template_name = 'dcim/site_bulk_edit.html'
|
||||
default_redirect_url = 'dcim:site_list'
|
||||
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
if form.cleaned_data['tenant'] == 0:
|
||||
fields_to_update['tenant'] = None
|
||||
elif form.cleaned_data['tenant']:
|
||||
fields_to_update['tenant'] = form.cleaned_data['tenant']
|
||||
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
|
||||
class RackGroupListView(ObjectListView):
|
||||
queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
|
||||
queryset = RackGroup.objects.annotate(rack_count=Count('racks'))
|
||||
filter = filters.RackGroupFilter
|
||||
filter_form = forms.RackGroupFilterForm
|
||||
table = tables.RackGroupTable
|
||||
@@ -150,49 +129,22 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_rackgroup'
|
||||
model = RackGroup
|
||||
form_class = forms.RackGroupForm
|
||||
success_url = 'dcim:rackgroup_list'
|
||||
cancel_url = 'dcim:rackgroup_list'
|
||||
|
||||
|
||||
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackgroup'
|
||||
cls = RackGroup
|
||||
form = forms.RackGroupBulkDeleteForm
|
||||
default_redirect_url = 'dcim:rackgroup_list'
|
||||
|
||||
|
||||
#
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleListView(ObjectListView):
|
||||
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
|
||||
table = tables.RackRoleTable
|
||||
edit_permissions = ['dcim.change_rackrole', 'dcim.delete_rackrole']
|
||||
template_name = 'dcim/rackrole_list.html'
|
||||
|
||||
|
||||
class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_rackrole'
|
||||
model = RackRole
|
||||
form_class = forms.RackRoleForm
|
||||
success_url = 'dcim:rackrole_list'
|
||||
cancel_url = 'dcim:rackrole_list'
|
||||
|
||||
|
||||
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackrole'
|
||||
cls = RackRole
|
||||
default_redirect_url = 'dcim:rackrole_list'
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackListView(ObjectListView):
|
||||
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type')\
|
||||
.annotate(device_count=Count('devices', distinct=True),
|
||||
u_consumed=Coalesce(Sum('devices__device_type__u_height'), 0))
|
||||
queryset = Rack.objects.select_related('site').annotate(device_count=Count('devices', distinct=True))
|
||||
filter = filters.RackFilter
|
||||
filter_form = forms.RackFilterForm
|
||||
table = tables.RackTable
|
||||
@@ -204,7 +156,7 @@ def rack(request, pk):
|
||||
|
||||
rack = get_object_or_404(Rack, pk=pk)
|
||||
|
||||
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
|
||||
nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True)\
|
||||
.select_related('device_type__manufacturer')
|
||||
next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
|
||||
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
|
||||
@@ -236,7 +188,7 @@ class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_rack'
|
||||
form = forms.RackImportForm
|
||||
table = tables.RackImportTable
|
||||
table = tables.RackTable
|
||||
template_name = 'dcim/rack_import.html'
|
||||
obj_list_url = 'dcim:rack_list'
|
||||
|
||||
@@ -251,12 +203,7 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['group', 'tenant', 'role']:
|
||||
if form.cleaned_data[field] == 0:
|
||||
fields_to_update[field] = None
|
||||
elif form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
for field in ['site', 'type', 'width', 'u_height', 'comments']:
|
||||
for field in ['site', 'group', 'u_height', 'comments']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
@@ -266,6 +213,7 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rack'
|
||||
cls = Rack
|
||||
form = forms.RackBulkDeleteForm
|
||||
default_redirect_url = 'dcim:rack_list'
|
||||
|
||||
|
||||
@@ -291,6 +239,7 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_manufacturer'
|
||||
cls = Manufacturer
|
||||
form = forms.ManufacturerBulkDeleteForm
|
||||
default_redirect_url = 'dcim:manufacturer_list'
|
||||
|
||||
|
||||
@@ -385,6 +334,7 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicetype'
|
||||
cls = DeviceType
|
||||
form = forms.DeviceTypeBulkDeleteForm
|
||||
default_redirect_url = 'dcim:devicetype_list'
|
||||
|
||||
|
||||
@@ -446,65 +396,68 @@ class ConsolePortTemplateAddView(ComponentTemplateCreateView):
|
||||
form = forms.ConsolePortTemplateForm
|
||||
|
||||
|
||||
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleporttemplate'
|
||||
cls = ConsolePortTemplate
|
||||
parent_cls = DeviceType
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateAddView(ComponentTemplateCreateView):
|
||||
model = ConsoleServerPortTemplate
|
||||
form = forms.ConsoleServerPortTemplateForm
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverporttemplate'
|
||||
cls = ConsoleServerPortTemplate
|
||||
parent_cls = DeviceType
|
||||
|
||||
|
||||
class PowerPortTemplateAddView(ComponentTemplateCreateView):
|
||||
model = PowerPortTemplate
|
||||
form = forms.PowerPortTemplateForm
|
||||
|
||||
|
||||
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerporttemplate'
|
||||
cls = PowerPortTemplate
|
||||
parent_cls = DeviceType
|
||||
|
||||
|
||||
class PowerOutletTemplateAddView(ComponentTemplateCreateView):
|
||||
model = PowerOutletTemplate
|
||||
form = forms.PowerOutletTemplateForm
|
||||
|
||||
|
||||
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlettemplate'
|
||||
cls = PowerOutletTemplate
|
||||
parent_cls = DeviceType
|
||||
|
||||
|
||||
class InterfaceTemplateAddView(ComponentTemplateCreateView):
|
||||
model = InterfaceTemplate
|
||||
form = forms.InterfaceTemplateForm
|
||||
|
||||
|
||||
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interfacetemplate'
|
||||
cls = InterfaceTemplate
|
||||
parent_cls = DeviceType
|
||||
|
||||
|
||||
class DeviceBayTemplateAddView(ComponentTemplateCreateView):
|
||||
model = DeviceBayTemplate
|
||||
form = forms.DeviceBayTemplateForm
|
||||
|
||||
|
||||
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicebaytemplate'
|
||||
cls = DeviceBayTemplate
|
||||
parent_cls = DeviceType
|
||||
def component_template_delete(request, pk, model):
|
||||
|
||||
devicetype = get_object_or_404(DeviceType, pk=pk)
|
||||
|
||||
class ComponentTemplateBulkDeleteForm(ConfirmationForm):
|
||||
pk = ModelMultipleChoiceField(queryset=model.objects.all(), widget=MultipleHiddenInput)
|
||||
|
||||
if '_confirm' in request.POST:
|
||||
form = ComponentTemplateBulkDeleteForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
# Delete component templates
|
||||
objects_to_delete = model.objects.filter(pk__in=[v.id for v in form.cleaned_data['pk']])
|
||||
try:
|
||||
deleted_count = objects_to_delete.count()
|
||||
objects_to_delete.delete()
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(list(objects_to_delete), request, e)
|
||||
return redirect('dcim:devicetype', {'pk': devicetype.pk})
|
||||
|
||||
messages.success(request, "Deleted {} {}".format(deleted_count, model._meta.verbose_name_plural))
|
||||
return redirect('dcim:devicetype', pk=devicetype.pk)
|
||||
|
||||
else:
|
||||
form = ComponentTemplateBulkDeleteForm(initial={'pk': request.POST.getlist('pk')})
|
||||
|
||||
selected_objects = model.objects.filter(pk__in=request.POST.getlist('pk'))
|
||||
if not selected_objects:
|
||||
messages.warning(request, "No {} were selected for deletion.".format(model._meta.verbose_name_plural))
|
||||
return redirect('dcim:devicetype', pk=devicetype.pk)
|
||||
|
||||
return render(request, 'dcim/component_template_delete.html', {
|
||||
'devicetype': devicetype,
|
||||
'form': form,
|
||||
'selected_objects': selected_objects,
|
||||
'cancel_url': reverse('dcim:devicetype', kwargs={'pk': devicetype.pk}),
|
||||
})
|
||||
|
||||
|
||||
#
|
||||
@@ -529,6 +482,7 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicerole'
|
||||
cls = DeviceRole
|
||||
form = forms.DeviceRoleBulkDeleteForm
|
||||
default_redirect_url = 'dcim:devicerole_list'
|
||||
|
||||
|
||||
@@ -554,6 +508,7 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_platform'
|
||||
cls = Platform
|
||||
form = forms.PlatformBulkDeleteForm
|
||||
default_redirect_url = 'dcim:platform_list'
|
||||
|
||||
|
||||
@@ -562,8 +517,8 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class DeviceListView(ObjectListView):
|
||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'rack__site',
|
||||
'primary_ip4', 'primary_ip6')
|
||||
queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'rack__site', 'primary_ip4',
|
||||
'primary_ip6')
|
||||
filter = filters.DeviceFilter
|
||||
filter_form = forms.DeviceFilterForm
|
||||
table = tables.DeviceTable
|
||||
@@ -616,9 +571,6 @@ def device(request, pk):
|
||||
related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
|
||||
.select_related('rack', 'device_type__manufacturer')[:10]
|
||||
|
||||
# Show graph button on interfaces only if at least one graph has been created.
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
|
||||
|
||||
return render(request, 'dcim/device.html', {
|
||||
'device': device,
|
||||
'console_ports': console_ports,
|
||||
@@ -631,7 +583,6 @@ def device(request, pk):
|
||||
'ip_addresses': ip_addresses,
|
||||
'secrets': secrets,
|
||||
'related_devices': related_devices,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
|
||||
|
||||
@@ -685,15 +636,14 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['tenant', 'platform']:
|
||||
if form.cleaned_data[field] == 0:
|
||||
fields_to_update[field] = None
|
||||
elif form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
if form.cleaned_data['platform']:
|
||||
fields_to_update['platform'] = form.cleaned_data['platform']
|
||||
elif form.cleaned_data['platform_delete']:
|
||||
fields_to_update['platform'] = None
|
||||
if form.cleaned_data['status']:
|
||||
status = form.cleaned_data['status']
|
||||
fields_to_update['status'] = True if status == 'True' else False
|
||||
for field in ['tenant', 'device_type', 'device_role', 'serial']:
|
||||
for field in ['device_type', 'device_role', 'serial']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
@@ -703,14 +653,14 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_device'
|
||||
cls = Device
|
||||
form = forms.DeviceBulkDeleteForm
|
||||
default_redirect_url = 'dcim:device_list'
|
||||
|
||||
|
||||
def device_inventory(request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
modules = Module.objects.filter(device=device, parent=None).select_related('manufacturer')\
|
||||
.prefetch_related('submodules')
|
||||
modules = Module.objects.filter(device=device, parent=None).prefetch_related('submodules')
|
||||
|
||||
return render(request, 'dcim/device_inventory.html', {
|
||||
'device': device,
|
||||
@@ -875,12 +825,6 @@ def consoleport_delete(request, pk):
|
||||
})
|
||||
|
||||
|
||||
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleport'
|
||||
cls = ConsolePort
|
||||
parent_cls = Device
|
||||
|
||||
|
||||
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.change_consoleport'
|
||||
form = forms.ConsoleConnectionImportForm
|
||||
@@ -1036,12 +980,6 @@ def consoleserverport_delete(request, pk):
|
||||
})
|
||||
|
||||
|
||||
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverport'
|
||||
cls = ConsoleServerPort
|
||||
parent_cls = Device
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
#
|
||||
@@ -1187,12 +1125,6 @@ def powerport_delete(request, pk):
|
||||
})
|
||||
|
||||
|
||||
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerport'
|
||||
cls = PowerPort
|
||||
parent_cls = Device
|
||||
|
||||
|
||||
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.change_powerport'
|
||||
form = forms.PowerConnectionImportForm
|
||||
@@ -1346,12 +1278,6 @@ def poweroutlet_delete(request, pk):
|
||||
})
|
||||
|
||||
|
||||
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlet'
|
||||
cls = PowerOutlet
|
||||
parent_cls = Device
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
@@ -1446,7 +1372,7 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.add_interface'
|
||||
cls = Device
|
||||
form = forms.InterfaceBulkCreateForm
|
||||
template_name = 'dcim/interface_add_multi.html'
|
||||
template_name = 'dcim/interface_bulk_add.html'
|
||||
default_redirect_url = 'dcim:device_list'
|
||||
|
||||
def update_objects(self, pk_list, form):
|
||||
@@ -1475,12 +1401,6 @@ class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
||||
len(selected_devices)))
|
||||
|
||||
|
||||
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interface'
|
||||
cls = Interface
|
||||
parent_cls = Device
|
||||
|
||||
|
||||
#
|
||||
# Device bays
|
||||
#
|
||||
@@ -1618,12 +1538,6 @@ def devicebay_depopulate(request, pk):
|
||||
})
|
||||
|
||||
|
||||
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicebay'
|
||||
cls = DeviceBay
|
||||
parent_cls = Device
|
||||
|
||||
|
||||
#
|
||||
# Interface connections
|
||||
#
|
||||
|
||||
@@ -19,9 +19,3 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(UserAction)
|
||||
class UserActionAdmin(admin.ModelAdmin):
|
||||
actions = None
|
||||
list_display = ['user', 'action', 'content_type', 'object_id', 'message']
|
||||
|
||||
@@ -77,7 +77,7 @@ class ExportTemplate(models.Model):
|
||||
]
|
||||
|
||||
def __unicode__(self):
|
||||
return u'{}: {}'.format(self.content_type, self.name)
|
||||
return "{}: {}".format(self.content_type, self.name)
|
||||
|
||||
def to_response(self, context_dict, filename):
|
||||
"""
|
||||
@@ -176,8 +176,8 @@ class UserAction(models.Model):
|
||||
|
||||
def __unicode__(self):
|
||||
if self.message:
|
||||
return u'{} {}'.format(self.user, self.message)
|
||||
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
||||
return ' '.join([self.user, self.message])
|
||||
return ' '.join([self.user, self.get_action_display(), self.content_type])
|
||||
|
||||
def icon(self):
|
||||
if self.action in [ACTION_CREATE, ACTION_IMPORT]:
|
||||
|
||||
@@ -7,12 +7,7 @@ from .models import (
|
||||
|
||||
@admin.register(VRF)
|
||||
class VRFAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'rd', 'tenant', 'enforce_unique']
|
||||
list_filter = ['tenant']
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(VRFAdmin, self).get_queryset(request)
|
||||
return qs.select_related('tenant')
|
||||
list_display = ['name', 'rd']
|
||||
|
||||
|
||||
@admin.register(Role)
|
||||
@@ -40,7 +35,7 @@ class AggregateAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Prefix)
|
||||
class PrefixAdmin(admin.ModelAdmin):
|
||||
list_display = ['prefix', 'vrf', 'tenant', 'site', 'status', 'role', 'vlan']
|
||||
list_display = ['prefix', 'vrf', 'site', 'status', 'role', 'vlan']
|
||||
list_filter = ['family', 'site', 'status', 'role']
|
||||
search_fields = ['prefix']
|
||||
|
||||
@@ -51,7 +46,7 @@ class PrefixAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(IPAddress)
|
||||
class IPAddressAdmin(admin.ModelAdmin):
|
||||
list_display = ['address', 'vrf', 'tenant', 'nat_inside']
|
||||
list_display = ['address', 'vrf', 'nat_inside']
|
||||
list_filter = ['family']
|
||||
fields = ['address', 'vrf', 'device', 'interface', 'nat_inside']
|
||||
readonly_fields = ['interface', 'device', 'nat_inside']
|
||||
@@ -72,10 +67,10 @@ class VLANGroupAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(VLAN)
|
||||
class VLANAdmin(admin.ModelAdmin):
|
||||
list_display = ['site', 'vid', 'name', 'tenant', 'status', 'role']
|
||||
list_filter = ['site', 'tenant', 'status', 'role']
|
||||
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', 'tenant', 'role')
|
||||
return qs.select_related('site', 'role')
|
||||
|
||||
@@ -2,7 +2,6 @@ from rest_framework import serializers
|
||||
|
||||
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
|
||||
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
|
||||
from tenancy.api.serializers import TenantNestedSerializer
|
||||
|
||||
|
||||
#
|
||||
@@ -10,11 +9,10 @@ from tenancy.api.serializers import TenantNestedSerializer
|
||||
#
|
||||
|
||||
class VRFSerializer(serializers.ModelSerializer):
|
||||
tenant = TenantNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
fields = ['id', 'name', 'rd', 'enforce_unique', 'description']
|
||||
|
||||
|
||||
class VRFNestedSerializer(VRFSerializer):
|
||||
@@ -23,15 +21,6 @@ class VRFNestedSerializer(VRFSerializer):
|
||||
fields = ['id', 'name', 'rd']
|
||||
|
||||
|
||||
class VRFTenantSerializer(VRFSerializer):
|
||||
"""
|
||||
Include tenant serializer. Useful for determining tenant inheritance for Prefixes and IPAddresses.
|
||||
"""
|
||||
|
||||
class Meta(VRFSerializer.Meta):
|
||||
fields = ['id', 'name', 'rd', 'tenant']
|
||||
|
||||
|
||||
#
|
||||
# Roles
|
||||
#
|
||||
@@ -109,12 +98,11 @@ class VLANGroupNestedSerializer(VLANGroupSerializer):
|
||||
class VLANSerializer(serializers.ModelSerializer):
|
||||
site = SiteNestedSerializer()
|
||||
group = VLANGroupNestedSerializer()
|
||||
tenant = TenantNestedSerializer()
|
||||
role = RoleNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name']
|
||||
fields = ['id', 'site', 'group', 'vid', 'name', 'status', 'role', 'display_name']
|
||||
|
||||
|
||||
class VLANNestedSerializer(VLANSerializer):
|
||||
@@ -129,14 +117,13 @@ class VLANNestedSerializer(VLANSerializer):
|
||||
|
||||
class PrefixSerializer(serializers.ModelSerializer):
|
||||
site = SiteNestedSerializer()
|
||||
vrf = VRFTenantSerializer()
|
||||
tenant = TenantNestedSerializer()
|
||||
vrf = VRFNestedSerializer()
|
||||
vlan = VLANNestedSerializer()
|
||||
role = RoleNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description']
|
||||
fields = ['id', 'family', 'prefix', 'site', 'vrf', 'vlan', 'status', 'role', 'description']
|
||||
|
||||
|
||||
class PrefixNestedSerializer(PrefixSerializer):
|
||||
@@ -150,13 +137,12 @@ class PrefixNestedSerializer(PrefixSerializer):
|
||||
#
|
||||
|
||||
class IPAddressSerializer(serializers.ModelSerializer):
|
||||
vrf = VRFTenantSerializer()
|
||||
tenant = TenantNestedSerializer()
|
||||
vrf = VRFNestedSerializer()
|
||||
interface = InterfaceNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside']
|
||||
fields = ['id', 'family', 'address', 'vrf', 'interface', 'description', 'nat_inside', 'nat_outside']
|
||||
|
||||
|
||||
class IPAddressNestedSerializer(IPAddressSerializer):
|
||||
|
||||
@@ -14,7 +14,7 @@ class VRFListView(generics.ListAPIView):
|
||||
"""
|
||||
List all VRFs
|
||||
"""
|
||||
queryset = VRF.objects.select_related('tenant')
|
||||
queryset = VRF.objects.all()
|
||||
serializer_class = serializers.VRFSerializer
|
||||
filter_class = filters.VRFFilter
|
||||
|
||||
@@ -23,7 +23,7 @@ class VRFDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single VRF
|
||||
"""
|
||||
queryset = VRF.objects.select_related('tenant')
|
||||
queryset = VRF.objects.all()
|
||||
serializer_class = serializers.VRFSerializer
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class PrefixListView(generics.ListAPIView):
|
||||
"""
|
||||
List prefixes (filterable)
|
||||
"""
|
||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
|
||||
serializer_class = serializers.PrefixSerializer
|
||||
filter_class = filters.PrefixFilter
|
||||
|
||||
@@ -105,7 +105,7 @@ class PrefixDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single prefix
|
||||
"""
|
||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
|
||||
serializer_class = serializers.PrefixSerializer
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ class IPAddressListView(generics.ListAPIView):
|
||||
"""
|
||||
List IP addresses (filterable)
|
||||
"""
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
|
||||
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
|
||||
.prefetch_related('nat_outside')
|
||||
serializer_class = serializers.IPAddressSerializer
|
||||
filter_class = filters.IPAddressFilter
|
||||
@@ -127,7 +127,7 @@ class IPAddressDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single IP address
|
||||
"""
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
|
||||
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
|
||||
.prefetch_related('nat_outside')
|
||||
serializer_class = serializers.IPAddressSerializer
|
||||
|
||||
@@ -140,7 +140,7 @@ class VLANGroupListView(generics.ListAPIView):
|
||||
"""
|
||||
List all VLAN groups
|
||||
"""
|
||||
queryset = VLANGroup.objects.select_related('site')
|
||||
queryset = VLANGroup.objects.all()
|
||||
serializer_class = serializers.VLANGroupSerializer
|
||||
filter_class = filters.VLANGroupFilter
|
||||
|
||||
@@ -149,7 +149,7 @@ class VLANGroupDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single VLAN group
|
||||
"""
|
||||
queryset = VLANGroup.objects.select_related('site')
|
||||
queryset = VLANGroup.objects.all()
|
||||
serializer_class = serializers.VLANGroupSerializer
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ class VLANListView(generics.ListAPIView):
|
||||
"""
|
||||
List VLANs (filterable)
|
||||
"""
|
||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
queryset = VLAN.objects.select_related('site', 'role')
|
||||
serializer_class = serializers.VLANSerializer
|
||||
filter_class = filters.VLANFilter
|
||||
|
||||
@@ -170,5 +170,5 @@ class VLANDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Retrieve a single VLAN
|
||||
"""
|
||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
queryset = VLAN.objects.select_related('site', 'role')
|
||||
serializer_class = serializers.VLANSerializer
|
||||
|
||||
@@ -2,42 +2,17 @@ import django_filters
|
||||
from netaddr import IPNetwork
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.models import Site, Device, Interface
|
||||
from tenancy.models import Tenant
|
||||
|
||||
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
|
||||
|
||||
|
||||
class VRFFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
name = django_filters.CharFilter(
|
||||
name='name',
|
||||
lookup_type='icontains',
|
||||
label='Name',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
|
||||
def search(self, queryset, value):
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(rd__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
@@ -45,10 +20,6 @@ class VRFFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class AggregateFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rir',
|
||||
queryset=RIR.objects.all(),
|
||||
@@ -65,15 +36,6 @@ class AggregateFilter(django_filters.FilterSet):
|
||||
model = Aggregate
|
||||
fields = ['family', 'rir_id', 'rir', 'date_added']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(description__icontains=value)
|
||||
try:
|
||||
prefix = str(IPNetwork(value.strip()).cidr)
|
||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||
except AddrFormatError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class PrefixFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
@@ -93,14 +55,6 @@ class PrefixFilter(django_filters.FilterSet):
|
||||
action='_vrf',
|
||||
label='VRF',
|
||||
)
|
||||
tenant_id = django_filters.MethodFilter(
|
||||
action='_tenant_id',
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.MethodFilter(
|
||||
action='_tenant',
|
||||
label='Tenant',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
@@ -138,13 +92,12 @@ class PrefixFilter(django_filters.FilterSet):
|
||||
fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(description__icontains=value)
|
||||
value = value.strip()
|
||||
try:
|
||||
prefix = str(IPNetwork(value.strip()).cidr)
|
||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||
query = str(IPNetwork(value).cidr)
|
||||
return queryset.filter(prefix__net_contains_or_equals=query)
|
||||
except AddrFormatError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
return queryset.none()
|
||||
|
||||
def search_by_parent(self, queryset, value):
|
||||
value = value.strip()
|
||||
@@ -167,34 +120,12 @@ class PrefixFilter(django_filters.FilterSet):
|
||||
return queryset.filter(vrf__isnull=True)
|
||||
return queryset.filter(vrf__pk=value)
|
||||
|
||||
def _tenant(self, queryset, value):
|
||||
if str(value) == '':
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(tenant__slug=value) |
|
||||
Q(tenant__isnull=True, vrf__tenant__slug=value)
|
||||
)
|
||||
|
||||
def _tenant_id(self, queryset, value):
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(tenant__pk=value) |
|
||||
Q(tenant__isnull=True, vrf__tenant__pk=value)
|
||||
)
|
||||
|
||||
|
||||
class IPAddressFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
parent = django_filters.MethodFilter(
|
||||
action='search_by_parent',
|
||||
label='Parent prefix',
|
||||
)
|
||||
vrf = django_filters.MethodFilter(
|
||||
action='_vrf',
|
||||
label='VRF',
|
||||
@@ -204,14 +135,6 @@ class IPAddressFilter(django_filters.FilterSet):
|
||||
action='_vrf',
|
||||
label='VRF',
|
||||
)
|
||||
tenant_id = django_filters.MethodFilter(
|
||||
action='_tenant_id',
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.MethodFilter(
|
||||
action='_tenant',
|
||||
label='Tenant',
|
||||
)
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='interface__device',
|
||||
queryset=Device.objects.all(),
|
||||
@@ -234,21 +157,10 @@ class IPAddressFilter(django_filters.FilterSet):
|
||||
fields = ['q', 'family', 'vrf_id', 'vrf', 'device_id', 'device', 'interface_id']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(description__icontains=value)
|
||||
try:
|
||||
ipaddress = str(IPNetwork(value.strip()))
|
||||
qs_filter |= Q(address__net_host=ipaddress)
|
||||
except AddrFormatError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def search_by_parent(self, queryset, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
query = str(IPNetwork(value).cidr)
|
||||
return queryset.filter(address__net_contained_or_equal=query)
|
||||
query = str(IPNetwork(value))
|
||||
return queryset.filter(address__net_host=query)
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
@@ -263,24 +175,6 @@ class IPAddressFilter(django_filters.FilterSet):
|
||||
return queryset.filter(vrf__isnull=True)
|
||||
return queryset.filter(vrf__pk=value)
|
||||
|
||||
def _tenant(self, queryset, value):
|
||||
if str(value) == '':
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(tenant__slug=value) |
|
||||
Q(tenant__isnull=True, vrf__tenant__slug=value)
|
||||
)
|
||||
|
||||
def _tenant_id(self, queryset, value):
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
return queryset.none()
|
||||
return queryset.filter(
|
||||
Q(tenant__pk=value) |
|
||||
Q(tenant__isnull=True, vrf__tenant__pk=value)
|
||||
)
|
||||
|
||||
|
||||
class VLANGroupFilter(django_filters.FilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@@ -301,10 +195,6 @@ class VLANGroupFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class VLANFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
queryset=Site.objects.all(),
|
||||
@@ -336,17 +226,6 @@ class VLANFilter(django_filters.FilterSet):
|
||||
name='vid',
|
||||
label='VLAN number (1-4095)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant (ID)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
name='tenant',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=Role.objects.all(),
|
||||
@@ -362,11 +241,3 @@ class VLANFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site_id', 'site', 'vid', 'name', 'status', 'role_id', 'role']
|
||||
|
||||
def search(self, queryset, value):
|
||||
qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
|
||||
try:
|
||||
qs_filter |= Q(vid=int(value))
|
||||
except ValueError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
[
|
||||
{
|
||||
"model": "ipam.aggregate",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"created": "2016-08-01",
|
||||
"last_updated": "2016-08-01T15:22:20.938Z",
|
||||
"family": 4,
|
||||
"prefix": "10.0.0.0/8",
|
||||
"rir": 6,
|
||||
"date_added": null,
|
||||
"description": "Private IPv4 space"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.aggregate",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"created": "2016-08-01",
|
||||
"last_updated": "2016-08-01T15:22:32.679Z",
|
||||
"family": 4,
|
||||
"prefix": "172.16.0.0/12",
|
||||
"rir": 6,
|
||||
"date_added": null,
|
||||
"description": "Private IPv4 space"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.aggregate",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"created": "2016-08-01",
|
||||
"last_updated": "2016-08-01T15:22:42.289Z",
|
||||
"family": 4,
|
||||
"prefix": "192.168.0.0/16",
|
||||
"rir": 6,
|
||||
"date_added": null,
|
||||
"description": "Private IPv4 space"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.rir",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "ARIN",
|
||||
"slug": "arin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.rir",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "RIPE",
|
||||
"slug": "ripe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.rir",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "APNIC",
|
||||
"slug": "apnic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.rir",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "LACNIC",
|
||||
"slug": "lacnic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.rir",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "AFRINIC",
|
||||
"slug": "afrinic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.rir",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "RFC 1918",
|
||||
"slug": "rfc-1918"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.role",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Production",
|
||||
"slug": "production",
|
||||
"weight": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.role",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Development",
|
||||
"slug": "development",
|
||||
"weight": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.role",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Management",
|
||||
"slug": "management",
|
||||
"weight": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "ipam.role",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Backup",
|
||||
"slug": "backup",
|
||||
"weight": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -4,9 +4,9 @@ from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Device, Interface
|
||||
from tenancy.forms import bulkedit_tenant_choices
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
|
||||
from utilities.forms import (
|
||||
BootstrapMixin, ConfirmationForm, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
||||
@@ -17,18 +17,6 @@ FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
|
||||
FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
|
||||
|
||||
|
||||
def bulkedit_vrf_choices():
|
||||
"""
|
||||
Include an option to assign the object to the global table.
|
||||
"""
|
||||
choices = [
|
||||
(None, '---------'),
|
||||
(0, 'Global'),
|
||||
]
|
||||
choices += [(v.pk, v.name) for v in VRF.objects.all()]
|
||||
return choices
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
@@ -37,7 +25,7 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
fields = ['name', 'rd', 'enforce_unique', 'description']
|
||||
labels = {
|
||||
'rd': "RD",
|
||||
}
|
||||
@@ -47,12 +35,10 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
|
||||
class VRFFromCSVForm(forms.ModelForm):
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
fields = ['name', 'rd', 'enforce_unique', 'description']
|
||||
|
||||
|
||||
class VRFImportForm(BulkImportForm, BootstrapMixin):
|
||||
@@ -61,18 +47,11 @@ class VRFImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
class VRFBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
|
||||
def vrf_tenant_choices():
|
||||
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vrfs'))
|
||||
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
|
||||
|
||||
|
||||
class VRFFilterForm(forms.Form, BootstrapMixin):
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=vrf_tenant_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
class VRFBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
@@ -87,6 +66,10 @@ class RIRForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class RIRBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=RIR.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Aggregates
|
||||
#
|
||||
@@ -120,12 +103,16 @@ 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=100, 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, u'{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
|
||||
|
||||
|
||||
class AggregateFilterForm(forms.Form, BootstrapMixin):
|
||||
@@ -145,6 +132,10 @@ class RoleForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class RoleBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Role.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Prefixes
|
||||
#
|
||||
@@ -158,7 +149,7 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description']
|
||||
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'description']
|
||||
help_texts = {
|
||||
'prefix': "IPv4 or IPv6 network",
|
||||
'vrf': "VRF (if applicable)",
|
||||
@@ -182,21 +173,23 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
|
||||
self.fields['vlan'].choices = []
|
||||
|
||||
def clean_prefix(self):
|
||||
prefix = self.cleaned_data['prefix']
|
||||
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 prefix
|
||||
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.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
vlan_group_name = forms.CharField(required=False)
|
||||
@@ -207,8 +200,7 @@ class PrefixFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role',
|
||||
'description']
|
||||
fields = ['prefix', 'vrf', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'description']
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -254,54 +246,49 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin):
|
||||
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.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
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.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=100, 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 = VRF.objects.annotate(prefix_count=Count('prefixes'))
|
||||
return [(v.pk, u'{} ({})'.format(v.name, v.prefix_count)) for v in vrf_choices]
|
||||
|
||||
|
||||
def tenant_choices():
|
||||
tenant_choices = Tenant.objects.all()
|
||||
return [(t.slug, t.name) for t in tenant_choices]
|
||||
vrf_choices = [('', 'All'), (0, 'Global')]
|
||||
vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
|
||||
return vrf_choices
|
||||
|
||||
|
||||
def prefix_site_choices():
|
||||
site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
|
||||
return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
|
||||
|
||||
|
||||
def prefix_status_choices():
|
||||
status_counts = {}
|
||||
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||
status_counts[status['status']] = status['count']
|
||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
|
||||
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
|
||||
|
||||
|
||||
def prefix_role_choices():
|
||||
role_choices = Role.objects.annotate(prefix_count=Count('prefixes'))
|
||||
return [(r.slug, u'{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
|
||||
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', widget=forms.TextInput(attrs={
|
||||
'placeholder': 'Network',
|
||||
}))
|
||||
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
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': 6}))
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
|
||||
|
||||
|
||||
@@ -324,7 +311,7 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'tenant', 'nat_device', 'nat_inside', 'description']
|
||||
fields = ['address', 'vrf', 'nat_device', 'nat_inside', 'description']
|
||||
help_texts = {
|
||||
'address': "IPv4 or IPv6 address and mask",
|
||||
'vrf': "VRF (if applicable)",
|
||||
@@ -373,8 +360,6 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
|
||||
class IPAddressFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
error_messages={'invalid_choice': 'VRF not found.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant 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)
|
||||
@@ -382,7 +367,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'tenant', 'device', 'interface_name', 'is_primary', 'description']
|
||||
fields = ['address', 'vrf', 'device', 'interface_name', 'is_primary', 'description']
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -427,9 +412,14 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin):
|
||||
|
||||
class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
description = forms.CharField(max_length=100, 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')
|
||||
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():
|
||||
@@ -437,19 +427,14 @@ def ipaddress_family_choices():
|
||||
|
||||
|
||||
def ipaddress_vrf_choices():
|
||||
vrf_choices = VRF.objects.annotate(ipaddress_count=Count('ip_addresses'))
|
||||
return [(v.pk, u'{} ({})'.format(v.name, v.ipaddress_count)) for v in vrf_choices]
|
||||
vrf_choices = [('', 'All'), (0, 'Global')]
|
||||
vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
|
||||
return vrf_choices
|
||||
|
||||
|
||||
class IPAddressFilterForm(forms.Form, BootstrapMixin):
|
||||
parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={
|
||||
'placeholder': 'Prefix',
|
||||
}))
|
||||
family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
|
||||
vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF',
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||
vrf = forms.ChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF')
|
||||
|
||||
|
||||
#
|
||||
@@ -464,9 +449,13 @@ class VLANGroupForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['site', 'name', 'slug']
|
||||
|
||||
|
||||
class VLANGroupBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VLANGroup.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def vlangroup_site_choices():
|
||||
site_choices = Site.objects.annotate(vlangroup_count=Count('vlan_groups'))
|
||||
return [(s.slug, u'{} ({})'.format(s.name, s.vlangroup_count)) for s in site_choices]
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.vlangroup_count)) for s in site_choices]
|
||||
|
||||
|
||||
class VLANGroupFilterForm(forms.Form, BootstrapMixin):
|
||||
@@ -485,7 +474,7 @@ class VLANForm(forms.ModelForm, BootstrapMixin):
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||
fields = ['site', 'group', 'vid', 'name', 'status', 'role']
|
||||
help_texts = {
|
||||
'site': "The site at which this VLAN exists",
|
||||
'group': "VLAN group (optional)",
|
||||
@@ -516,15 +505,13 @@ class VLANFromCSVForm(forms.ModelForm):
|
||||
error_messages={'invalid_choice': 'Device not found.'})
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'VLAN group not found.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
|
||||
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', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
|
||||
fields = ['site', 'group', 'vid', 'name', 'status_name', 'role']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
m = super(VLANFromCSVForm, self).save(commit=False)
|
||||
@@ -542,38 +529,34 @@ class VLANImportForm(BulkImportForm, BootstrapMixin):
|
||||
class VLANBulkEditForm(forms.Form, BootstrapMixin):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
|
||||
tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
|
||||
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
|
||||
description = forms.CharField(max_length=100, 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, u'{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
|
||||
return [(s.slug, '{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
|
||||
|
||||
|
||||
def vlan_group_choices():
|
||||
group_choices = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
|
||||
return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
|
||||
|
||||
|
||||
def vlan_tenant_choices():
|
||||
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vlans'))
|
||||
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
|
||||
return [(g.pk, '{} ({})'.format(g, g.vlan_count)) for g in group_choices]
|
||||
|
||||
|
||||
def vlan_status_choices():
|
||||
status_counts = {}
|
||||
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
|
||||
status_counts[status['status']] = status['count']
|
||||
return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
|
||||
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
|
||||
|
||||
|
||||
def vlan_role_choices():
|
||||
role_choices = Role.objects.annotate(vlan_count=Count('vlans'))
|
||||
return [(r.slug, u'{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
|
||||
|
||||
|
||||
class VLANFilterForm(forms.Form, BootstrapMixin):
|
||||
@@ -581,8 +564,6 @@ class VLANFilterForm(forms.Form, BootstrapMixin):
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_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}))
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-07-25 18:42
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0004_ipam_vlangroup_uniqueness'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vlan',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vlan',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-07-27 14:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0001_initial'),
|
||||
('ipam', '0005_auto_20160725_1842'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vlan',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vrf',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vrfs', to='tenancy.Tenant'),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-07-28 15:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0001_initial'),
|
||||
('ipam', '0006_vrf_vlan_add_tenant'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ipaddress',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='tenancy.Tenant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='tenancy.Tenant'),
|
||||
),
|
||||
]
|
||||
@@ -7,7 +7,6 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
|
||||
from dcim.models import Interface
|
||||
from tenancy.models import Tenant
|
||||
from utilities.models import CreatedUpdatedModel
|
||||
|
||||
from .fields import IPNetworkField, IPAddressField
|
||||
@@ -47,7 +46,6 @@ class VRF(CreatedUpdatedModel):
|
||||
"""
|
||||
name = models.CharField(max_length=50)
|
||||
rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
|
||||
tenant = models.ForeignKey(Tenant, related_name='vrfs', blank=True, null=True, on_delete=models.PROTECT)
|
||||
enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space',
|
||||
help_text="Prevent duplicate prefixes/IP addresses within this VRF")
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
@@ -67,8 +65,6 @@ class VRF(CreatedUpdatedModel):
|
||||
return ','.join([
|
||||
self.name,
|
||||
self.rd,
|
||||
self.tenant.name if self.tenant else '',
|
||||
'True' if self.enforce_unique else '',
|
||||
self.description,
|
||||
])
|
||||
|
||||
@@ -233,7 +229,6 @@ class Prefix(CreatedUpdatedModel):
|
||||
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')
|
||||
tenant = models.ForeignKey(Tenant, related_name='prefixes', blank=True, null=True, on_delete=models.PROTECT)
|
||||
vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
|
||||
verbose_name='VLAN')
|
||||
status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
|
||||
@@ -254,13 +249,12 @@ class Prefix(CreatedUpdatedModel):
|
||||
|
||||
def clean(self):
|
||||
# Disallow host masks
|
||||
if self.prefix:
|
||||
if self.prefix.version == 4 and self.prefix.prefixlen == 32:
|
||||
raise ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 addresses "
|
||||
"instead.")
|
||||
elif self.prefix.version == 6 and self.prefix.prefixlen == 128:
|
||||
raise ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 addresses "
|
||||
"instead.")
|
||||
if self.prefix.version == 4 and self.prefix.prefixlen == 32:
|
||||
raise ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 addresses "
|
||||
"instead.")
|
||||
elif self.prefix.version == 6 and self.prefix.prefixlen == 128:
|
||||
raise ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 addresses "
|
||||
"instead.")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.prefix:
|
||||
@@ -297,7 +291,7 @@ class Prefix(CreatedUpdatedModel):
|
||||
|
||||
class IPAddress(CreatedUpdatedModel):
|
||||
"""
|
||||
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
|
||||
An IPAddress represents an individual IPV4 or IPv6 address and its mask. The mask length should match what is
|
||||
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
|
||||
Prefixes, IPAddresses can optionally be assigned to a VRF. An IPAddress can optionally be assigned to an Interface.
|
||||
Interfaces can have zero or more IPAddresses assigned to them.
|
||||
@@ -310,7 +304,6 @@ class IPAddress(CreatedUpdatedModel):
|
||||
address = IPAddressField()
|
||||
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
|
||||
verbose_name='VRF')
|
||||
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
|
||||
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,
|
||||
@@ -392,7 +385,7 @@ class VLANGroup(models.Model):
|
||||
verbose_name_plural = 'VLAN groups'
|
||||
|
||||
def __unicode__(self):
|
||||
return u'{} - {}'.format(self.site.name, self.name)
|
||||
return '{} - {}'.format(self.site.name, self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||
@@ -413,11 +406,9 @@ class VLAN(CreatedUpdatedModel):
|
||||
MinValueValidator(1),
|
||||
MaxValueValidator(4094)
|
||||
])
|
||||
name = models.CharField(max_length=64)
|
||||
tenant = models.ForeignKey(Tenant, related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
|
||||
name = models.CharField(max_length=30)
|
||||
status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
|
||||
role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'group', 'vid']
|
||||
@@ -443,18 +434,15 @@ class VLAN(CreatedUpdatedModel):
|
||||
def to_csv(self):
|
||||
return ','.join([
|
||||
self.site.name,
|
||||
self.group.name if self.group else '',
|
||||
str(self.vid),
|
||||
self.name,
|
||||
self.tenant.name if self.tenant else '',
|
||||
self.get_status_display(),
|
||||
self.role.name if self.role else '',
|
||||
self.description,
|
||||
])
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return u'{} ({})'.format(self.vid, self.name)
|
||||
return u"{} ({})".format(self.vid, self.name)
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
@@ -6,21 +6,24 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
RIR_ACTIONS = """
|
||||
{% if perms.ipam.change_rir %}
|
||||
<a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
RIR_EDIT_LINK = """
|
||||
{% if perms.ipam.change_rir %}<a href="{% url 'ipam:rir_edit' slug=record.slug %}">Edit</a>{% endif %}
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph record.get_utilization %}
|
||||
{% 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 %}
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
{% if perms.ipam.change_role %}
|
||||
<a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
ROLE_EDIT_LINK = """
|
||||
{% if perms.ipam.change_role %}<a href="{% url 'ipam:role_edit' slug=record.slug %}">Edit</a>{% endif %}
|
||||
"""
|
||||
|
||||
PREFIX_LINK = """
|
||||
@@ -39,26 +42,6 @@ PREFIX_LINK_BRIEF = """
|
||||
</span>
|
||||
"""
|
||||
|
||||
IPADDRESS_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||
{% elif perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a>
|
||||
{% else %}
|
||||
{{ record.0 }}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VRF_LINK = """
|
||||
{% if record.vrf %}
|
||||
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
||||
{% elif prefix.vrf %}
|
||||
{{ prefix.vrf }}
|
||||
{% else %}
|
||||
Global
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
STATUS_LABEL = """
|
||||
{% if record.pk %}
|
||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||
@@ -67,19 +50,9 @@ STATUS_LABEL = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VLANGROUP_ACTIONS = """
|
||||
VLANGROUP_EDIT_LINK = """
|
||||
{% if perms.ipam.change_vlangroup %}
|
||||
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
TENANT_LINK = """
|
||||
{% if record.tenant %}
|
||||
<a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}">{{ record.tenant }}</a>
|
||||
{% elif record.vrf.tenant %}
|
||||
<a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}">{{ record.vrf.tenant }}</a>*
|
||||
{% else %}
|
||||
—
|
||||
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}">Edit</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -92,12 +65,11 @@ class VRFTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name')
|
||||
rd = tables.Column(verbose_name='RD')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
description = tables.Column(orderable=False, verbose_name='Description')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VRF
|
||||
fields = ('pk', 'name', 'rd', 'tenant', 'description')
|
||||
fields = ('pk', 'name', 'rd', 'description')
|
||||
|
||||
|
||||
#
|
||||
@@ -109,11 +81,11 @@ class RIRTable(BaseTable):
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
aggregate_count = tables.Column(verbose_name='Aggregates')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
edit = tables.TemplateColumn(template_code=RIR_EDIT_LINK, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RIR
|
||||
fields = ('pk', 'name', 'aggregate_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'aggregate_count', 'slug', 'edit')
|
||||
|
||||
|
||||
#
|
||||
@@ -144,11 +116,11 @@ class RoleTable(BaseTable):
|
||||
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')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
edit = tables.TemplateColumn(template_code=ROLE_EDIT_LINK, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Role
|
||||
fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'edit')
|
||||
|
||||
|
||||
#
|
||||
@@ -159,31 +131,25 @@ class PrefixTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix')
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
||||
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(orderable=False, verbose_name='Description')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Prefix
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not record.pk else '',
|
||||
}
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'site', 'role', 'description')
|
||||
|
||||
|
||||
class PrefixBriefTable(BaseTable):
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF, verbose_name='Prefix')
|
||||
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
|
||||
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(BaseTable.Meta):
|
||||
model = Prefix
|
||||
fields = ('prefix', 'vrf', 'status', 'site', 'role')
|
||||
orderable = False
|
||||
fields = ('prefix', 'status', 'site', 'role')
|
||||
|
||||
|
||||
#
|
||||
@@ -192,9 +158,8 @@ class PrefixBriefTable(BaseTable):
|
||||
|
||||
class IPAddressTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
||||
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')
|
||||
@@ -202,10 +167,7 @@ class IPAddressTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||
}
|
||||
fields = ('pk', 'address', 'vrf', 'device', 'interface', 'description')
|
||||
|
||||
|
||||
class IPAddressBriefTable(BaseTable):
|
||||
@@ -231,12 +193,11 @@ class VLANGroupTable(BaseTable):
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
vlan_count = tables.Column(verbose_name='VLANs')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
edit = tables.TemplateColumn(template_code=VLANGROUP_EDIT_LINK, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLANGroup
|
||||
fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'edit')
|
||||
|
||||
|
||||
#
|
||||
@@ -249,10 +210,9 @@ class VLANTable(BaseTable):
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
name = tables.Column(verbose_name='Name')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
role = tables.Column(verbose_name='Role')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLAN
|
||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role')
|
||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'status', 'role')
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import netaddr
|
||||
from netaddr import IPSet
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models import Count, Q
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
from dcim.models import Device
|
||||
@@ -21,7 +21,7 @@ def add_available_prefixes(parent, prefix_list):
|
||||
"""
|
||||
|
||||
# Find all unallocated space
|
||||
available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
|
||||
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
|
||||
@@ -31,65 +31,13 @@ def add_available_prefixes(parent, prefix_list):
|
||||
return prefix_list
|
||||
|
||||
|
||||
def add_available_ipaddresses(prefix, ipaddress_list):
|
||||
"""
|
||||
Annotate ranges of available IP addresses within a given prefix.
|
||||
"""
|
||||
|
||||
output = []
|
||||
prev_ip = None
|
||||
|
||||
# Ignore the "network address" for IPv4 prefixes larger than /31
|
||||
if prefix.version == 4 and prefix.prefixlen < 31:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
|
||||
else:
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.first)
|
||||
|
||||
# Ignore the broadcast address for IPv4 prefixes larger than /31
|
||||
if prefix.version == 4 and prefix.prefixlen < 31:
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
|
||||
else:
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.last)
|
||||
|
||||
if not ipaddress_list:
|
||||
return [(
|
||||
int(last_ip_in_prefix - first_ip_in_prefix + 1),
|
||||
'{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
)]
|
||||
|
||||
# Account for any available IPs before the first real IP
|
||||
if ipaddress_list[0].address.ip > first_ip_in_prefix:
|
||||
skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
|
||||
first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
|
||||
# Iterate through existing IPs and annotate free ranges
|
||||
for ip in ipaddress_list:
|
||||
if prev_ip:
|
||||
diff = int(ip.address.ip - prev_ip.address.ip)
|
||||
if diff > 1:
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((diff - 1, first_skipped))
|
||||
output.append(ip)
|
||||
prev_ip = ip
|
||||
|
||||
# Include any remaining available IPs
|
||||
if prev_ip.address.ip < last_ip_in_prefix:
|
||||
skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
|
||||
first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
|
||||
output.append((skipped_count, first_skipped))
|
||||
|
||||
return output
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
|
||||
class VRFListView(ObjectListView):
|
||||
queryset = VRF.objects.select_related('tenant')
|
||||
queryset = VRF.objects.all()
|
||||
filter = filters.VRFFilter
|
||||
filter_form = forms.VRFFilterForm
|
||||
table = tables.VRFTable
|
||||
edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
|
||||
template_name = 'ipam/vrf_list.html'
|
||||
@@ -99,11 +47,10 @@ def vrf(request, pk):
|
||||
|
||||
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
||||
prefixes = Prefix.objects.filter(vrf=vrf)
|
||||
prefix_table = tables.PrefixBriefTable(prefixes)
|
||||
|
||||
return render(request, 'ipam/vrf.html', {
|
||||
'vrf': vrf,
|
||||
'prefix_table': prefix_table,
|
||||
'prefixes': prefixes,
|
||||
})
|
||||
|
||||
|
||||
@@ -138,10 +85,6 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
if form.cleaned_data['tenant'] == 0:
|
||||
fields_to_update['tenant'] = None
|
||||
elif form.cleaned_data['tenant']:
|
||||
fields_to_update['tenant'] = form.cleaned_data['tenant']
|
||||
for field in ['description']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
@@ -152,6 +95,7 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vrf'
|
||||
cls = VRF
|
||||
form = forms.VRFBulkDeleteForm
|
||||
default_redirect_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
@@ -177,6 +121,7 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
|
||||
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_rir'
|
||||
cls = RIR
|
||||
form = forms.RIRBulkDeleteForm
|
||||
default_redirect_url = 'ipam:rir_list'
|
||||
|
||||
|
||||
@@ -202,7 +147,7 @@ class AggregateListView(ObjectListView):
|
||||
if a.prefix.version == 4:
|
||||
ipv4_total += a.prefix.size
|
||||
elif a.prefix.version == 6:
|
||||
ipv6_total += a.prefix.size / 2 ** 64
|
||||
ipv6_total += a.prefix.size / 2**64
|
||||
|
||||
return {
|
||||
'ipv4_total': ipv4_total,
|
||||
@@ -272,6 +217,7 @@ class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_aggregate'
|
||||
cls = Aggregate
|
||||
form = forms.AggregateBulkDeleteForm
|
||||
default_redirect_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
@@ -297,6 +243,7 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_role'
|
||||
cls = Role
|
||||
form = forms.RoleBulkDeleteForm
|
||||
default_redirect_url = 'ipam:role_list'
|
||||
|
||||
|
||||
@@ -305,7 +252,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class PrefixListView(ObjectListView):
|
||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'role')
|
||||
queryset = Prefix.objects.select_related('site', 'role')
|
||||
filter = filters.PrefixFilter
|
||||
filter_form = forms.PrefixFilterForm
|
||||
table = tables.PrefixTable
|
||||
@@ -328,12 +275,10 @@ def prefix(request, pk):
|
||||
aggregate = None
|
||||
|
||||
# Count child IP addresses
|
||||
ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
|
||||
.count()
|
||||
ipaddress_count = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix)).count()
|
||||
|
||||
# Parent prefixes table
|
||||
parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\
|
||||
.filter(prefix__net_contains=str(prefix.prefix))\
|
||||
parent_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contains=str(prefix.prefix))\
|
||||
.select_related('site', 'role').annotate_depth()
|
||||
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
|
||||
|
||||
@@ -343,13 +288,7 @@ def prefix(request, pk):
|
||||
duplicate_prefix_table = tables.PrefixBriefTable(duplicate_prefixes)
|
||||
|
||||
# Child prefixes table
|
||||
if prefix.vrf:
|
||||
# If the prefix is in a VRF, show child prefixes only within that VRF.
|
||||
child_prefixes = Prefix.objects.filter(vrf=prefix.vrf)
|
||||
else:
|
||||
# If the prefix is in the global table, show child prefixes from all VRFs.
|
||||
child_prefixes = Prefix.objects.all()
|
||||
child_prefixes = child_prefixes.filter(prefix__net_contained=str(prefix.prefix))\
|
||||
child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\
|
||||
.select_related('site', 'role').annotate_depth(limit=0)
|
||||
if child_prefixes:
|
||||
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
|
||||
@@ -373,7 +312,7 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_prefix'
|
||||
model = Prefix
|
||||
form_class = forms.PrefixForm
|
||||
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
|
||||
fields_initial = ['site', 'vrf', 'prefix']
|
||||
cancel_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
@@ -401,11 +340,10 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['vrf', 'tenant']:
|
||||
if form.cleaned_data[field] == 0:
|
||||
fields_to_update[field] = None
|
||||
elif form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
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]
|
||||
@@ -416,6 +354,7 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_prefix'
|
||||
cls = Prefix
|
||||
form = forms.PrefixBulkDeleteForm
|
||||
default_redirect_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
@@ -424,9 +363,8 @@ 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(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
|
||||
ipaddresses = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix))\
|
||||
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
||||
ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses)
|
||||
|
||||
ip_table = tables.IPAddressTable(ipaddresses)
|
||||
ip_table.model = IPAddress
|
||||
@@ -445,7 +383,7 @@ def prefix_ipaddresses(request, pk):
|
||||
#
|
||||
|
||||
class IPAddressListView(ObjectListView):
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
|
||||
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
|
||||
filter = filters.IPAddressFilter
|
||||
filter_form = forms.IPAddressFilterForm
|
||||
table = tables.IPAddressTable
|
||||
@@ -527,11 +465,10 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
for field in ['vrf', 'tenant']:
|
||||
if form.cleaned_data[field] == 0:
|
||||
fields_to_update[field] = None
|
||||
elif form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
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]
|
||||
@@ -542,6 +479,7 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_ipaddress'
|
||||
cls = IPAddress
|
||||
form = forms.IPAddressBulkDeleteForm
|
||||
default_redirect_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
@@ -550,7 +488,7 @@ class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class VLANGroupListView(ObjectListView):
|
||||
queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
|
||||
queryset = VLANGroup.objects.annotate(vlan_count=Count('vlans'))
|
||||
filter = filters.VLANGroupFilter
|
||||
filter_form = forms.VLANGroupFilterForm
|
||||
table = tables.VLANGroupTable
|
||||
@@ -562,13 +500,13 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'ipam.change_vlangroup'
|
||||
model = VLANGroup
|
||||
form_class = forms.VLANGroupForm
|
||||
success_url = 'ipam:vlangroup_list'
|
||||
cancel_url = 'ipam:vlangroup_list'
|
||||
|
||||
|
||||
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vlangroup'
|
||||
cls = VLANGroup
|
||||
form = forms.VLANGroupBulkDeleteForm
|
||||
default_redirect_url = 'ipam:vlangroup_list'
|
||||
|
||||
|
||||
@@ -577,7 +515,7 @@ class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
#
|
||||
|
||||
class VLANListView(ObjectListView):
|
||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
queryset = VLAN.objects.select_related('site', 'role')
|
||||
filter = filters.VLANFilter
|
||||
filter_form = forms.VLANFilterForm
|
||||
table = tables.VLANTable
|
||||
@@ -589,11 +527,10 @@ def vlan(request, pk):
|
||||
|
||||
vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
|
||||
prefixes = Prefix.objects.filter(vlan=vlan)
|
||||
prefix_table = tables.PrefixBriefTable(prefixes)
|
||||
|
||||
return render(request, 'ipam/vlan.html', {
|
||||
'vlan': vlan,
|
||||
'prefix_table': prefix_table,
|
||||
'prefixes': prefixes,
|
||||
})
|
||||
|
||||
|
||||
@@ -628,11 +565,7 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
def update_objects(self, pk_list, form):
|
||||
|
||||
fields_to_update = {}
|
||||
if form.cleaned_data['tenant'] == 0:
|
||||
fields_to_update['tenant'] = None
|
||||
elif form.cleaned_data['tenant']:
|
||||
fields_to_update['tenant'] = form.cleaned_data['tenant']
|
||||
for field in ['site', 'group', 'status', 'role', 'description']:
|
||||
for field in ['site', 'status', 'role']:
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
@@ -642,4 +575,5 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vlan'
|
||||
cls = VLAN
|
||||
form = forms.VLANBulkDeleteForm
|
||||
default_redirect_url = 'ipam:vlan_list'
|
||||
|
||||
@@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.5.1-dev'
|
||||
VERSION = '1.3.0'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
@@ -108,7 +108,6 @@ INSTALLED_APPS = (
|
||||
'ipam',
|
||||
'extras',
|
||||
'secrets',
|
||||
'tenancy',
|
||||
'users',
|
||||
'utilities',
|
||||
)
|
||||
@@ -172,6 +171,7 @@ MESSAGE_TAGS = {
|
||||
# Authentication URLs
|
||||
LOGIN_URL = '/login/'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGOUT_URL = '/logout/'
|
||||
|
||||
# Secrets
|
||||
SECRETS_MIN_PUBKEY_SIZE = 2048
|
||||
|
||||
@@ -22,7 +22,6 @@ urlpatterns = [
|
||||
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'^tenancy/', include('tenancy.urls', namespace='tenancy')),
|
||||
url(r'^profile/', include('users.urls', namespace='users')),
|
||||
|
||||
# API
|
||||
@@ -30,7 +29,6 @@ urlpatterns = [
|
||||
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/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
|
||||
url(r'^api/docs/', include('rest_framework_swagger.urls')),
|
||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
|
||||
|
||||
@@ -5,20 +5,16 @@ from django.shortcuts import render
|
||||
from circuits.models import Provider, Circuit
|
||||
from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
|
||||
from extras.models import UserAction
|
||||
from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF
|
||||
from ipam.models import Aggregate, Prefix, IPAddress, VLAN
|
||||
from secrets.models import Secret
|
||||
from tenancy.models import Tenant
|
||||
|
||||
|
||||
def home(request):
|
||||
|
||||
stats = {
|
||||
|
||||
# Organization
|
||||
'site_count': Site.objects.count(),
|
||||
'tenant_count': Tenant.objects.count(),
|
||||
|
||||
# DCIM
|
||||
'site_count': Site.objects.count(),
|
||||
'rack_count': Rack.objects.count(),
|
||||
'device_count': Device.objects.count(),
|
||||
'interface_connections_count': InterfaceConnection.objects.count(),
|
||||
@@ -26,7 +22,6 @@ def home(request):
|
||||
'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
|
||||
|
||||
# IPAM
|
||||
'vrf_count': VRF.objects.count(),
|
||||
'aggregate_count': Aggregate.objects.count(),
|
||||
'prefix_count': Prefix.objects.count(),
|
||||
'ipaddress_count': IPAddress.objects.count(),
|
||||
@@ -43,7 +38,7 @@ def home(request):
|
||||
|
||||
return render(request, 'home.html', {
|
||||
'stats': stats,
|
||||
'recent_activity': UserAction.objects.select_related('user')[:50]
|
||||
'recent_activity': UserAction.objects.select_related('user')[:15]
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -21,9 +21,6 @@ body {
|
||||
margin: 0 auto -61px; /* the bottom margin is the negative value of the footer's height */
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
.navbar-brand {
|
||||
padding: 12px 15px 8px;
|
||||
}
|
||||
.footer, .push {
|
||||
height: 60px; /* .push must be the same height as .footer */
|
||||
}
|
||||
@@ -294,39 +291,27 @@ ul.rack_near_face li.empty:hover a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Colors (from http://flatuicolors.com) */
|
||||
.teal { background-color: #1abc9c; }
|
||||
.green { background-color: #2ecc71; }
|
||||
.blue { background-color: #3498db; }
|
||||
.purple { background-color: #9b59b6; }
|
||||
.yellow { background-color: #f1c40f; }
|
||||
.orange { background-color: #e67e22; }
|
||||
.red { background-color: #e74c3c; }
|
||||
.light_gray { background-color: #dce2e3; }
|
||||
.medium_gray { background-color: #95a5a6; }
|
||||
.dark_gray { background-color: #34495e; }
|
||||
|
||||
/* Rack elevation coloring */
|
||||
ul.rack .teal { border-bottom: 1px solid #16a085; }
|
||||
ul.rack .teal:hover { background-color: #16a085; }
|
||||
ul.rack .green { border-bottom: 1px solid #27ae60; }
|
||||
ul.rack .green:hover { background-color: #27ae60; }
|
||||
ul.rack .blue { border-bottom: 1px solid #2980b9; }
|
||||
ul.rack .blue:hover { background-color: #2980b9; }
|
||||
ul.rack .purple { border-bottom: 1px solid #8e44ad; }
|
||||
ul.rack .purple:hover { background-color: #8e44ad; }
|
||||
ul.rack .yellow { border-bottom: 1px solid #f39c12; }
|
||||
ul.rack .yellow:hover { background-color: #f39c12; }
|
||||
ul.rack .orange { border-bottom: 1px solid #d35400; }
|
||||
ul.rack .orange:hover { background-color: #d35400; }
|
||||
ul.rack .red { border-bottom: 1px solid #c0392b; }
|
||||
ul.rack .red:hover { background-color: #c0392b; }
|
||||
ul.rack .light_gray { border-bottom: 1px solid #bdc3c7; }
|
||||
ul.rack .light_gray:hover { background-color: #bdc3c7; }
|
||||
ul.rack .medium_gray { border-bottom: 1px solid #7f8c8d; }
|
||||
ul.rack .medium_gray:hover { background-color: #7f8c8d; }
|
||||
ul.rack .dark_gray { border-bottom: 1px solid #2c3e50; }
|
||||
ul.rack .dark_gray:hover { background-color: #2c3e50; }
|
||||
/* 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: #dce2e3; 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 */
|
||||
.banner-bottom {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -13,11 +13,11 @@ $(document).ready(function() {
|
||||
|
||||
// Slugify
|
||||
function slugify(s, num_chars) {
|
||||
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
|
||||
s = s.replace(/^[\s\.]+|[\s\.]+$/g, ''); // Trim leading/trailing spaces
|
||||
s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens
|
||||
s = s.toLowerCase(); // Convert to lowercase
|
||||
return s.substring(0, num_chars); // Trim to first num_chars chars
|
||||
s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars
|
||||
s = s.replace(/^\s+|\s+$/g, ''); // Trim leading/trailing spaces
|
||||
s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens
|
||||
s = s.toLowerCase(); // Convert to lowercase
|
||||
return s.substring(0, num_chars); // Trim to first num_chars chars
|
||||
}
|
||||
var slug_field = $('#id_slug');
|
||||
slug_field.change(function() {
|
||||
|
||||
@@ -10,16 +10,15 @@ $(document).ready(function() {
|
||||
$('#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');
|
||||
var secret_div = $('#secret_' + secret_id);
|
||||
|
||||
// Delete the plaintext
|
||||
secret_div.html('********');
|
||||
$('#secret_' + secret_id).html('********');
|
||||
$(this).hide();
|
||||
$(this).siblings('button.unlock-secret').show();
|
||||
});
|
||||
@@ -82,16 +81,13 @@ $(document).ready(function() {
|
||||
xhr.setRequestHeader("X-CSRFToken", csrf_token);
|
||||
},
|
||||
success: function (response, status) {
|
||||
$('#secret_' + secret_id).html(response.plaintext);
|
||||
$('button.unlock-secret[secret-id=' + secret_id + ']').hide();
|
||||
$('button.lock-secret[secret-id=' + secret_id + ']').show();
|
||||
var secret_plaintext = response.plaintext;
|
||||
$('#secret_' + secret_id).html(secret_plaintext);
|
||||
return true;
|
||||
},
|
||||
error: function (xhr, ajaxOptions, thrownError) {
|
||||
if (xhr.status == 403) {
|
||||
alert("Permission denied");
|
||||
} else {
|
||||
var json = jQuery.parseJSON(xhr.responseText);
|
||||
alert("Decryption failed: " + json['error']);
|
||||
alert("Decryption failed: " + xhr.statusText);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,7 +28,6 @@ class SecretRoleListView(generics.ListAPIView):
|
||||
"""
|
||||
queryset = SecretRole.objects.all()
|
||||
serializer_class = serializers.SecretRoleSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
|
||||
class SecretRoleDetailView(generics.RetrieveAPIView):
|
||||
@@ -37,7 +36,6 @@ class SecretRoleDetailView(generics.RetrieveAPIView):
|
||||
"""
|
||||
queryset = SecretRole.objects.all()
|
||||
serializer_class = serializers.SecretRoleSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
|
||||
class SecretListView(generics.GenericAPIView):
|
||||
@@ -49,7 +47,6 @@ class SecretListView(generics.GenericAPIView):
|
||||
serializer_class = serializers.SecretSerializer
|
||||
filter_class = SecretFilter
|
||||
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, private_key=None):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
@@ -94,7 +91,6 @@ class SecretDetailView(generics.GenericAPIView):
|
||||
.prefetch_related('role__users', 'role__groups')
|
||||
serializer_class = serializers.SecretSerializer
|
||||
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk, private_key=None):
|
||||
secret = get_object_or_404(Secret, pk=pk)
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import django_filters
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from .models import Secret, SecretRole
|
||||
from dcim.models import Device
|
||||
|
||||
|
||||
class SecretFilter(django_filters.FilterSet):
|
||||
q = django_filters.MethodFilter(
|
||||
action='search',
|
||||
label='Search',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='role',
|
||||
queryset=SecretRole.objects.all(),
|
||||
@@ -22,19 +15,7 @@ class SecretFilter(django_filters.FilterSet):
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (Name)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
fields = ['name', 'role_id', 'role', 'device']
|
||||
|
||||
def search(self, queryset, value):
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(device__name__icontains=value)
|
||||
)
|
||||
fields = ['name', 'role_id', 'role']
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
[
|
||||
{
|
||||
"model": "secrets.secretrole",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Login Credentials",
|
||||
"slug": "login-credentials",
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "secrets.secretrole",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "RADIUS Key",
|
||||
"slug": "radius-key",
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "secrets.secretrole",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "SNMPv2 Community",
|
||||
"slug": "snmpv2-community",
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "secrets.secretrole",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "SNMPv3 Credentials",
|
||||
"slug": "snmpv3-credentials",
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -5,7 +5,7 @@ from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Device
|
||||
from utilities.forms import BootstrapMixin, BulkImportForm, CSVDataField, SlugField
|
||||
from utilities.forms import BootstrapMixin, BulkImportForm, ConfirmationForm, CSVDataField, SlugField
|
||||
|
||||
from .models import Secret, SecretRole, UserKey
|
||||
|
||||
@@ -42,6 +42,10 @@ class SecretRoleForm(forms.ModelForm, BootstrapMixin):
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class SecretRoleBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=SecretRole.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Secrets
|
||||
#
|
||||
@@ -93,9 +97,13 @@ class SecretBulkEditForm(forms.Form, BootstrapMixin):
|
||||
name = forms.CharField(max_length=100, required=False)
|
||||
|
||||
|
||||
class SecretBulkDeleteForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
def secret_role_choices():
|
||||
role_choices = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
||||
return [(r.slug, u'{} ({})'.format(r.name, r.secret_count)) for r in role_choices]
|
||||
return [(r.slug, '{} ({})'.format(r.name, r.secret_count)) for r in role_choices]
|
||||
|
||||
|
||||
class SecretFilterForm(forms.Form, BootstrapMixin):
|
||||
|
||||
@@ -219,8 +219,8 @@ class Secret(CreatedUpdatedModel):
|
||||
|
||||
def __unicode__(self):
|
||||
if self.role and self.device:
|
||||
return u'{} for {}'.format(self.role, self.device)
|
||||
return u'Secret'
|
||||
return "{} for {}".format(self.role, self.device)
|
||||
return "Secret"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('secrets:secret', args=[self.pk])
|
||||
|
||||
@@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import SecretRole, Secret
|
||||
|
||||
|
||||
SECRETROLE_ACTIONS = """
|
||||
SECRETROLE_EDIT_LINK = """
|
||||
{% if perms.secrets.change_secretrole %}
|
||||
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
<a href="{% url 'secrets:secretrole_edit' slug=record.slug %}">Edit</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@@ -22,12 +22,11 @@ class SecretRoleTable(BaseTable):
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
secret_count = tables.Column(verbose_name='Secrets')
|
||||
slug = tables.Column(verbose_name='Slug')
|
||||
actions = tables.TemplateColumn(template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
|
||||
verbose_name='')
|
||||
edit = tables.TemplateColumn(template_code=SECRETROLE_EDIT_LINK, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = SecretRole
|
||||
fields = ('pk', 'name', 'secret_count', 'slug', 'actions')
|
||||
fields = ('pk', 'name', 'secret_count', 'slug', 'edit')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -37,6 +37,7 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'secrets.delete_secretrole'
|
||||
cls = SecretRole
|
||||
form = forms.SecretRoleBulkDeleteForm
|
||||
default_redirect_url = 'secrets:secretrole_list'
|
||||
|
||||
|
||||
@@ -92,7 +93,7 @@ def secret_add(request, pk):
|
||||
|
||||
messages.success(request, "Added new secret: {0}".format(secret))
|
||||
if '_addanother' in request.POST:
|
||||
return redirect('dcim:device_addsecret', pk=device.pk)
|
||||
return redirect('secrets:secret_add')
|
||||
else:
|
||||
return redirect('secrets:secret', pk=secret.pk)
|
||||
|
||||
@@ -218,4 +219,5 @@ class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'secrets.delete_secret'
|
||||
cls = Secret
|
||||
form = forms.SecretBulkDeleteForm
|
||||
default_redirect_url = 'secrets:secret_list'
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="panel panel-danger" style="margin-top: 200px">
|
||||
<div class="panel-heading">
|
||||
<strong>
|
||||
<i class="fa fa-warning"></i>
|
||||
<i class="glyphicon glyphicon-warning-sign"></i>
|
||||
Server Error
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<link rel="stylesheet" href="{% static 'font-awesome-4.6.3/css/font-awesome.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'jquery-ui-1.11.4/jquery-ui.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}">
|
||||
<link rel="icon" type="image/png" href="{% static 'img/netbox.ico' %}" />
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-default navbar-fixed-top">
|
||||
@@ -20,194 +19,178 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="{% static 'img/netbox_logo.png' %}" />
|
||||
</a>
|
||||
<a class="navbar-brand" href="/">NetBox</a>
|
||||
</div>
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' or 'tenancy' in request.path %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li>
|
||||
{% if perms.dcim.add_site %}
|
||||
<li><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Site</a></li>
|
||||
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li>
|
||||
{% if perms.tenancy.add_tenant %}
|
||||
<li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>
|
||||
<li><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Tenants</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'tenancy:tenantgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenant Groups</a></li>
|
||||
{% if perms.tenancy.add_tenantgroup %}
|
||||
<li><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant Group</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' %} active{% endif %}">
|
||||
{% if perms.dcim.add_site %}
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Sites <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:site_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Sites</a></li>
|
||||
<li><a href="{% url 'dcim:site_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Site</a></li>
|
||||
<li><a href="{% url 'dcim:site_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Sites</a></li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:site_list' %}">Sites</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/dcim/rack' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
|
||||
<li><a href="{% url 'dcim:rack_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Racks</a></li>
|
||||
{% if perms.dcim.add_rack %}
|
||||
<li><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack</a></li>
|
||||
<li><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Racks</a></li>
|
||||
<li><a href="{% url 'dcim:rack_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Rack</a></li>
|
||||
<li><a href="{% url 'dcim:rack_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Racks</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Groups</a></li>
|
||||
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Rack Groups</a></li>
|
||||
{% if perms.dcim.add_rackgroup %}
|
||||
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'dcim:rackrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Roles</a></li>
|
||||
{% if perms.dcim.add_rackrole %}
|
||||
<li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li>
|
||||
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/dcim/platforms/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
|
||||
<li><a href="{% url 'dcim:device_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Devices</a></li>
|
||||
{% if perms.dcim.add_device %}
|
||||
<li><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device</a></li>
|
||||
<li><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Devices</a></li>
|
||||
<li><a href="{% url 'dcim:device_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device</a></li>
|
||||
<li><a href="{% url 'dcim:device_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Devices</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_device or perms.ipam.add_devicetype %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:devicetype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Types</a></li>
|
||||
<li><a href="{% url 'dcim:devicetype_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Device Types</a></li>
|
||||
{% if perms.dcim.add_devicetype %}
|
||||
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Type</a></li>
|
||||
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device Type</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Roles</a></li>
|
||||
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Device Roles</a></li>
|
||||
{% if perms.dcim.add_devicerole %}
|
||||
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Role</a></li>
|
||||
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device Role</a></li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_devicerole or perms.dcim.add_manufacturer %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:manufacturer_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Manufacturers</a></li>
|
||||
<li><a href="{% url 'dcim:manufacturer_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Manufacturers</a></li>
|
||||
{% if perms.dcim.add_manufacturer %}
|
||||
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
|
||||
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_manufacturer or perms.dcim.add_platform %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:platform_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Platforms</a></li>
|
||||
<li><a href="{% url 'dcim:platform_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Platforms</a></li>
|
||||
{% if perms.dcim.add_platform %}
|
||||
<li><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Platform</a></li>
|
||||
<li><a href="{% url 'dcim:platform_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Platform</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/dcim/console-connections/' or request.path|startswith:'/dcim/power-connections/' or request.path|startswith:'/dcim/interface-connections/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li>
|
||||
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Console Connections</a></li>
|
||||
{% if perms.dcim.change_consoleport %}
|
||||
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Console Connections</a></li>
|
||||
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Console Connections</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.change_consoleport or perms.ipam.change_powerport %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:power_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Power Connections</a></li>
|
||||
<li><a href="{% url 'dcim:power_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Power Connections</a></li>
|
||||
{% if perms.dcim.change_powerport %}
|
||||
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Power Connections</a></li>
|
||||
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Power Connections</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.change_powerport or perms.ipam.add_interfaceconnection %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:interface_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Interface Connections</a></li>
|
||||
<li><a href="{% url 'dcim:interface_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Interface Connections</a></li>
|
||||
{% if perms.dcim.add_interfaceconnection %}
|
||||
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Interface Connections</a></li>
|
||||
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Interface Connections</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/ipam/vlan' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li>
|
||||
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> IP Addresses</a></li>
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<li><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an IP</a></li>
|
||||
<li><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import IPs</a></li>
|
||||
<li><a href="{% url 'ipam:ipaddress_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add an IP</a></li>
|
||||
<li><a href="{% url 'ipam:ipaddress_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import IPs</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_ipaddress or perms.ipam.add_prefix %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'ipam:prefix_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefixes</a></li>
|
||||
<li><a href="{% url 'ipam:prefix_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Prefixes</a></li>
|
||||
{% if perms.ipam.add_prefix %}
|
||||
<li><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Prefix</a></li>
|
||||
<li><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Prefixes</a></li>
|
||||
<li><a href="{% url 'ipam:prefix_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Prefix</a></li>
|
||||
<li><a href="{% url 'ipam:prefix_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Prefixes</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_prefix or perms.ipam.add_aggregate %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'ipam:aggregate_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Aggregates</a></li>
|
||||
<li><a href="{% url 'ipam:aggregate_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Aggregates</a></li>
|
||||
{% if perms.ipam.add_aggregate %}
|
||||
<li><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
|
||||
<li><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Aggregates</a></li>
|
||||
<li><a href="{% url 'ipam:aggregate_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
|
||||
<li><a href="{% url 'ipam:aggregate_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Aggregates</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_aggregate or perms.ipam.add_vrf %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'ipam:vrf_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VRFs</a></li>
|
||||
<li><a href="{% url 'ipam:vrf_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VRFs</a></li>
|
||||
{% if perms.ipam.add_vrf %}
|
||||
<li><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VRF</a></li>
|
||||
<li><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VRFs</a></li>
|
||||
<li><a href="{% url 'ipam:vrf_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VRF</a></li>
|
||||
<li><a href="{% url 'ipam:vrf_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VRFs</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'ipam:rir_list' %}"><i class="fa fa-search" aria-hidden="true"></i> RIRs</a></li>
|
||||
<li><a href="{% url 'ipam:rir_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> RIRs</a></li>
|
||||
{% if perms.ipam.add_rir %}
|
||||
<li><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a RIR</a></li>
|
||||
<li><a href="{% url 'ipam:rir_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a RIR</a></li>
|
||||
{% endif %}
|
||||
{% if perms.ipam.add_rir or perms.ipam.add_role %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li>
|
||||
<li><a href="{% url 'ipam:role_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li>
|
||||
{% if perms.ipam.add_role %}
|
||||
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
|
||||
<li><a href="{% url 'ipam:role_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Role</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/ipam/vlan' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
|
||||
<li><a href="{% url 'ipam:vlan_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLANs</a></li>
|
||||
{% if perms.ipam.add_vlan %}
|
||||
<li><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN</a></li>
|
||||
<li><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VLANs</a></li>
|
||||
<li><a href="{% url 'ipam:vlan_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VLAN</a></li>
|
||||
<li><a href="{% url 'ipam:vlan_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VLANs</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Groups</a></li>
|
||||
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLAN Groups</a></li>
|
||||
{% if perms.ipam.add_vlangroup %}
|
||||
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
|
||||
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li>
|
||||
<li><a href="{% url 'circuits:provider_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Providers</a></li>
|
||||
{% if perms.circuits.add_provider %}
|
||||
<li><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Provider</a></li>
|
||||
<li><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Providers</a></li>
|
||||
<li><a href="{% url 'circuits:provider_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Provider</a></li>
|
||||
<li><a href="{% url 'circuits:provider_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Providers</a></li>
|
||||
{% endif %}
|
||||
{% if perms.circuits.add_circuit or perms.circuits.add_provider %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'circuits:circuit_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuits</a></li>
|
||||
<li><a href="{% url 'circuits:circuit_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Circuits</a></li>
|
||||
{% if perms.circuits.add_circuit %}
|
||||
<li><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit</a></li>
|
||||
<li><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Circuits</a></li>
|
||||
<li><a href="{% url 'circuits:circuit_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Circuit</a></li>
|
||||
<li><a href="{% url 'circuits:circuit_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Circuits</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuit Types</a></li>
|
||||
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Circuit Types</a></li>
|
||||
{% if perms.circuits.add_circuittype %}
|
||||
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
|
||||
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
@@ -215,14 +198,14 @@
|
||||
<li class="dropdown{% if request.path|startswith:'/secrets/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li>
|
||||
<li><a href="{% url 'secrets:secret_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Secrets</a></li>
|
||||
{% if perms.secrets.add_secret %}
|
||||
<li><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Secrets</a></li>
|
||||
<li><a href="{% url 'secrets:secret_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Secrets</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secret Roles</a></li>
|
||||
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Secret Roles</a></li>
|
||||
{% if perms.secrets.add_secretrole %}
|
||||
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
|
||||
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
@@ -232,12 +215,12 @@
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{% if request.user.is_authenticated %}
|
||||
{% if request.user.is_staff %}
|
||||
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs" aria-hidden="true"></i> Admin</a></li>
|
||||
<li><a href="{% url 'admin:index' %}"><i class="glyphicon glyphicon-cog" aria-hidden="true"></i> Admin</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'users:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li>
|
||||
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> Log out</a></li>
|
||||
<li><a href="{% url 'users:profile' %}"><i class="glyphicon glyphicon-user" aria-hidden="true"></i> Profile</a></li>
|
||||
<li><a href="{% url 'logout' %}"><i class="glyphicon glyphicon-log-out" aria-hidden="true"></i> Log out</a></li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
|
||||
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="glyphicon glyphicon-log-in" aria-hidden="true"></i> Log in</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}
|
||||
{% block title %}{{ circuit.provider }} Circuit {{ circuit.cid }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
|
||||
<li><a href="{% url 'circuits:circuit_list' %}?provider={{ circuit.provider.slug }}">{{ circuit.provider }}</a></li>
|
||||
<li>{{ circuit.cid }}</li>
|
||||
<li><a href="{% url 'dcim:site' slug=circuit.site.slug %}">{{ circuit.site }}</a></li>
|
||||
<li><a href="{% url 'circuits:circuit_list' %}?site={{ circuit.site.slug }}">Circuits</a></li>
|
||||
<li>{{ circuit }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" />
|
||||
<input type="text" name="q" class="form-control" placeholder="Circuit ID" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -28,18 +28,18 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.circuits.change_circuit %}
|
||||
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||
Edit this circuit
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.circuits.delete_circuit %}
|
||||
<a href="{% url 'circuits:circuit_delete' pk=circuit.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
||||
Delete this circuit
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
|
||||
<h1>{{ circuit.provider }} Circuit {{ circuit.cid }}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
@@ -57,68 +57,6 @@
|
||||
<td>Circuit ID</td>
|
||||
<td>{{ circuit.cid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td><a href="{{ circuit.type.get_absolute_url }}">{{ circuit.type }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
{% if circuit.tenant %}
|
||||
<a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Install Date</td>
|
||||
<td>
|
||||
{% if circuit.install_date %}
|
||||
{{ circuit.install_date }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Speed</td>
|
||||
<td>
|
||||
{% if circuit.upstream_speed %}
|
||||
<i class="fa fa-arrow-down" title="Downstream"></i> {{ circuit.port_speed_human }}
|
||||
<i class="fa fa-arrow-up" title="Upstream"></i> {{ circuit.upstream_speed_human }}
|
||||
{% else %}
|
||||
{{ circuit.port_speed_human }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Commit Rate</td>
|
||||
<td>
|
||||
{% if circuit.commit_speed %}
|
||||
{{ circuit.commit_speed_human }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
<td>{{ circuit.created }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last Updated</td>
|
||||
<td>{{ circuit.last_updated }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Termination</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>
|
||||
@@ -135,34 +73,44 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Install Date</td>
|
||||
<td>{{ circuit.install_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Port Speed</td>
|
||||
<td>{{ circuit.port_speed_human }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Commit Rate</td>
|
||||
<td>{% if circuit.commit_rate %}{{ circuit.commit_rate_human }}{% else %}<span class="text-muted">N/A</span>{% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cross-Connect</td>
|
||||
<td>
|
||||
{% if circuit.xconnect_id %}
|
||||
{{ circuit.xconnect_id }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ circuit.xconnect_id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Patch Panel/Port</td>
|
||||
<td>
|
||||
{% if circuit.pp_info %}
|
||||
{{ circuit.pp_info }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ circuit.pp_info }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
<td>{{ circuit.created }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last Updated</td>
|
||||
<td>{{ circuit.last_updated }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if circuit.comments %}
|
||||
{% if circuit.comments %}
|
||||
{{ circuit.comments|gfm }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
|
||||
@@ -9,20 +9,13 @@
|
||||
{% render_field form.provider %}
|
||||
{% render_field form.cid %}
|
||||
{% render_field form.type %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.install_date %}
|
||||
{% render_field form.port_speed %}
|
||||
{% render_field form.commit_rate %}
|
||||
{% render_field form.xconnect_id %}
|
||||
{% render_field form.pp_info %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Bandwidth</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.port_speed %}
|
||||
{% render_field form.upstream_speed %}
|
||||
{% render_field form.commit_rate %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Termination</strong></div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -43,11 +43,6 @@
|
||||
<td>Circuit type</td>
|
||||
<td>Transit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Strickland Propane</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Site name</td>
|
||||
@@ -60,13 +55,8 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Port Speed</td>
|
||||
<td>Physical speed in Kbps</td>
|
||||
<td>100000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Upstream Speed</td>
|
||||
<td>Upstream speed in Kbps (optional)</td>
|
||||
<td>20000</td>
|
||||
<td>Physical speed in Kbps/td>
|
||||
<td>10000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Commit rate</td>
|
||||
@@ -86,7 +76,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,ASH-4,2016-02-23,100000,,2000,937649,PP8371 ports 13/14</pre>
|
||||
<pre>IC-603122,TeliaSonera,Transit,ASH-4,2016-02-23,10000,2000,937649,PP8371 ports 13/14</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.circuits.add_circuit %}
|
||||
<a href="{% url 'circuits:circuit_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add a circuit
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -19,7 +19,23 @@
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="{% url 'circuits:circuit_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/filter_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.circuits.add_circuittype %}
|
||||
<a href="{% url 'circuits:circuittype_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add a circuit type
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -6,41 +6,27 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
|
||||
<li>{{ provider }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<form action="{% url 'circuits:provider_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if show_graphs %}
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider_graphs' pk=provider.pk %}" title="Show graphs">
|
||||
<i class="fa fa-signal" aria-hidden="true"></i>
|
||||
Graphs
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider_graphs' pk=provider.pk %}" title="Show graphs">
|
||||
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
||||
Graphs
|
||||
</button>
|
||||
{% if perms.circuits.change_provider %}
|
||||
<a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||
Edit this provider
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.circuits.delete_provider %}
|
||||
<a href="{% url 'circuits:provider_delete' slug=provider.slug %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
||||
Delete this provider
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -55,53 +41,25 @@
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<td>ASN</td>
|
||||
<td>
|
||||
{% if provider.asn %}
|
||||
{{ provider.asn }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ provider.asn }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Account</td>
|
||||
<td>
|
||||
{% if provider.account %}
|
||||
{{ provider.account }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ provider.account }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Customer Portal</td>
|
||||
<td>
|
||||
{% if provider.portal_url %}
|
||||
<a href="{{ provider.portal_url }}">{{ provider.portal_url }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
<a href="{{ provider.portal_url }}">{{ provider.portal_url }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NOC Contact</td>
|
||||
<td>
|
||||
{% if provider.noc_contact %}
|
||||
{{ provider.noc_contact|linebreaksbr }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ provider.noc_contact|linebreaksbr }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Admin Contact</td>
|
||||
<td>
|
||||
{% if provider.admin_contact %}
|
||||
{{ provider.admin_contact|linebreaksbr }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ provider.admin_contact|linebreaksbr }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
@@ -118,7 +76,7 @@
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if provider.comments %}
|
||||
{% if provider.comments %}
|
||||
{{ provider.comments|gfm }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.circuits.add_provider %}
|
||||
<a href="{% url 'circuits:provider_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add a provider
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -18,7 +18,23 @@
|
||||
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="{% url 'circuits:provider_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/filter_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.change_consoleport %}
|
||||
<a href="{% url 'dcim:console_connections_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
|
||||
Import connections
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -14,16 +14,6 @@
|
||||
<strong>Device</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
{% if device.tenant %}
|
||||
<a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>
|
||||
@@ -65,7 +55,7 @@
|
||||
{% if device.serial %}
|
||||
<span>{{ device.serial }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
<span class="text-muted">Not defined</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -96,7 +86,7 @@
|
||||
{% if device.platform %}
|
||||
<span>{{ device.platform }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
<span class="text-warning">Not assigned</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -121,7 +111,7 @@
|
||||
<span>(NAT: {{ device.primary_ip4.nat_outside.address.ip }})</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
<span class="text-muted">Not defined</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -136,7 +126,7 @@
|
||||
<span>(NAT: {{ device.primary_ip6.nat_outside.address.ip }})</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
<span class="text-muted">Not defined</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -267,7 +257,7 @@
|
||||
<strong>Comments</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if device.comments %}
|
||||
{% if device.comments %}
|
||||
{{ device.comments|gfm }}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
@@ -299,180 +289,100 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if device_bays or device.device_type.is_parent_device %}
|
||||
{% if perms.dcim.delete_devicebay %}
|
||||
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
||||
{% csrf_token %}
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Device Bays</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for devicebay in device_bays %}
|
||||
{% include 'dcim/inc/_devicebay.html' with selectable=True %}
|
||||
{% include 'dcim/inc/_devicebay.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">No device bays defined</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_devicebay or perms.dcim.delete_devicebay %}
|
||||
<div class="panel-footer">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% if device_bays and perms.dcim.delete_devicebay %}
|
||||
<button type="submit" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add device bay
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add device bays
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.delete_devicebay %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if interfaces or device.device_type.is_network_device %}
|
||||
{% if perms.dcim.delete_interface %}
|
||||
<form method="post" action="{% url 'dcim:interface_bulk_delete' pk=device.pk %}">
|
||||
{% csrf_token %}
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Interfaces</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for iface in interfaces %}
|
||||
{% include 'dcim/inc/_interface.html' with selectable=True %}
|
||||
{% include 'dcim/inc/_interface.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">No interfaces defined</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_interface or perms.dcim.delete_interface %}
|
||||
<div class="panel-footer">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% if interfaces and perms.dcim.delete_interface %}
|
||||
<button type="submit" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
{% if perms.dcim.add_interface %}
|
||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add interface
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.add_interface %}
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add interface
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.delete_interface %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if cs_ports or device.device_type.is_console_server %}
|
||||
{% if perms.dcim.delete_consoleserverport %}
|
||||
<form method="post" action="{% url 'dcim:consoleserverport_bulk_delete' pk=device.pk %}">
|
||||
{% csrf_token %}
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Console Server Ports</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for csp in cs_ports %}
|
||||
{% include 'dcim/inc/_consoleserverport.html' with selectable=True %}
|
||||
{% include 'dcim/inc/_consoleserverport.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">No console server ports defined</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
|
||||
<div class="panel-footer">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% if cs_ports and perms.dcim.delete_consoleserverport %}
|
||||
<button type="submit" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
{% if perms.dcim.add_consoleserverport %}
|
||||
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add console server ports
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.add_consoleserverport %}
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add console server ports
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.delete_consoleserverport %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if power_outlets or device.device_type.is_pdu %}
|
||||
{% if perms.dcim.delete_poweroutlet %}
|
||||
<form method="post" action="{% url 'dcim:poweroutlet_bulk_delete' pk=device.pk %}">
|
||||
{% csrf_token %}
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Outlets</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
{% for po in power_outlets %}
|
||||
{% include 'dcim/inc/_poweroutlet.html' with selectable=True %}
|
||||
{% include 'dcim/inc/_poweroutlet.html' %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">No power outlets defined</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||
<div class="panel-footer">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% if power_outlets and perms.dcim.delete_poweroutlet %}
|
||||
<button type="submit" class="btn btn-xs btn-danger">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
{% if perms.dcim.add_poweroutlet %}
|
||||
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add power outlets
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.dcim.add_poweroutlet %}
|
||||
<div class="panel-footer text-right">
|
||||
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add power outlets
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.delete_poweroutlet %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
||||
<td>{{ device.device_type }}</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
<td>{{ device.tenant }}</td>
|
||||
<td>{{ device.serial }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<div class="panel-body">
|
||||
{% render_field form.name %}
|
||||
{% render_field form.device_role %}
|
||||
{% render_field form.tenant %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
@@ -23,32 +22,8 @@
|
||||
<div class="panel-body">
|
||||
{% render_field form.site %}
|
||||
{% render_field form.rack %}
|
||||
{% if obj.device_type.is_child_device and obj.parent_bay %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Parent device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
<a href="{% url 'dcim:device' pk=obj.parent_bay.device.pk %}">{{ obj.parent_bay.device }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">Parent bay</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
{{ obj.parent_bay.name }}
|
||||
{% if perms.dcim.change_devicebay %}
|
||||
<a href="{% url 'dcim:devicebay_depopulate' pk=obj.parent_bay.pk %}" class="btn btn-danger btn-xs">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true" title="Remove device"></i> Remove
|
||||
</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif not obj.device_type.is_child_device %}
|
||||
{% render_field form.face %}
|
||||
{% render_field form.position %}
|
||||
{% endif %}
|
||||
{% render_field form.face %}
|
||||
{% render_field form.position %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
|
||||
@@ -36,11 +36,6 @@
|
||||
<td>Functional role of device</td>
|
||||
<td>ToR Switch</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Pied Piper</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device manufacturer</td>
|
||||
<td>Hardware manufacturer</td>
|
||||
@@ -84,7 +79,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear</pre>
|
||||
<pre>rack101_sw1,ToR Switch,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -36,11 +36,6 @@
|
||||
<td>Functional role of device</td>
|
||||
<td>Blade Server</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Pied Piper</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device manufacturer</td>
|
||||
<td>Hardware manufacturer</td>
|
||||
@@ -74,7 +69,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4</pre>
|
||||
<pre>Blade12,Blade Server,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
<tr>
|
||||
<th>Module</th>
|
||||
<th></th>
|
||||
<th>Manufacturer</th>
|
||||
<th>Part Number</th>
|
||||
<th>Serial Number</th>
|
||||
<th></th>
|
||||
@@ -43,7 +42,6 @@
|
||||
<tr>
|
||||
<td>{{ m.name }}</td>
|
||||
<td>{% if not m.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
|
||||
<td>{{ m.manufacturer|default:'' }}</td>
|
||||
<td>{{ m.part_id }}</td>
|
||||
<td>{{ m.serial }}</td>
|
||||
<td class="text-right">
|
||||
@@ -59,7 +57,6 @@
|
||||
<tr>
|
||||
<td style="padding-left: 20px">{{ m2.name }}</td>
|
||||
<td>{% if not m2.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
|
||||
<td>{{ m2.manufacturer|default:'' }}</td>
|
||||
<td>{{ m2.part_id }}</td>
|
||||
<td>{{ m2.serial }}</td>
|
||||
<td class="text-right">
|
||||
@@ -75,7 +72,6 @@
|
||||
<tr>
|
||||
<td style="padding-left: 40px">{{ m3.name }}</td>
|
||||
<td>{% if not m3.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
|
||||
<td>{{ m3.manufacturer|default:'' }}</td>
|
||||
<td>{{ m3.part_id }}</td>
|
||||
<td>{{ m3.serial }}</td>
|
||||
<td class="text-right">
|
||||
@@ -91,7 +87,6 @@
|
||||
<tr>
|
||||
<td style="padding-left: 60px">{{ m4.name }}</td>
|
||||
<td>{% if not m4.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
|
||||
<td>{{ m4.manufacturer|default:'' }}</td>
|
||||
<td>{{ m4.part_id }}</td>
|
||||
<td>{{ m4.serial }}</td>
|
||||
<td class="text-right">
|
||||
@@ -112,7 +107,7 @@
|
||||
</div>
|
||||
{% if perms.dcim.add_module %}
|
||||
<a href="{% url 'dcim:module_add' pk=device.pk %}" class="btn btn-success">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add a Module
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_device %}
|
||||
<a href="{% url 'dcim:device_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add a device
|
||||
</a>
|
||||
<a href="{% url 'dcim:device_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
|
||||
Import devices
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -23,7 +23,24 @@
|
||||
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Name or serial" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/filter_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add a device role
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.change_devicetype %}
|
||||
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
|
||||
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
|
||||
Edit this device type
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_devicetype %}
|
||||
<a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
||||
Delete this device type
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -48,16 +48,6 @@
|
||||
<td>Model Name</td>
|
||||
<td>{{ devicetype.model }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Part Number</td>
|
||||
<td>
|
||||
{% if devicetype.part_number %}
|
||||
{{ devicetype.part_number }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Height (U)</td>
|
||||
<td>{{ devicetype.u_height }}</td>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_devicetype %}
|
||||
<a href="{% url 'dcim:devicetype_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add a device type
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<tr{% if cp.cs_port and not cp.connection_status %} class="info"{% endif %}>
|
||||
{% if selectable and perms.dcim.delete_consoleport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
|
||||
</td>
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<tr{% if csp.connected_console and not csp.connected_console.connection_status %} class="info"{% endif %}>
|
||||
{% if selectable and perms.dcim.delete_consoleserverport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ csp.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp.name }}
|
||||
</td>
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
<div class="col-md-3">
|
||||
<form action="{% url 'dcim:device_list' %}" method="get">
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search devices" />
|
||||
<input type="text" name="q" class="form-control" placeholder="Device name or serial" />
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<tr>
|
||||
{% if selectable and perms.dcim.delete_devicebay %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
|
||||
</td>
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<tr{% if iface.connection and not iface.connection.connection_status %} class="info"{% endif %}>
|
||||
{% if selectable and perms.dcim.delete_interface %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
|
||||
{% if iface.description %}
|
||||
@@ -34,12 +29,10 @@
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
{% if show_graphs %}
|
||||
{% if iface.circuit or iface.connection %}
|
||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
|
||||
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if iface.circuit or iface.connection %}
|
||||
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
|
||||
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_interface %}
|
||||
{% if iface.is_physical %}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<tr{% if po.connected_port and not po.connected_port.connection_status %} class="info"{% endif %}>
|
||||
{% if selectable and perms.dcim.delete_poweroutlet %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ po.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ po.name }}
|
||||
</td>
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<tr{% if pp.power_outlet and not pp.connection_status %} class="info"{% endif %}>
|
||||
{% if selectable and perms.dcim.delete_powerport %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ pp.pk }}" />
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
|
||||
</td>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<input type="hidden" name="pk_all" value="{% for row in table.rows %}{{ row.record.pk|default:'' }}{% if not forloop.last %},{% endif %}{% endfor %}" />
|
||||
{% render_table table table_template|default:'table.html' %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_add_multi' %}" class="btn btn-primary btn-sm">
|
||||
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_add' %}" class="btn btn-primary btn-sm">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add Interfaces
|
||||
</button>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_interfaceconnection %}
|
||||
<a href="{% url 'dcim:interface_connections_import' %}" class="btn btn-info">
|
||||
<span class="fa fa-download" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
|
||||
Import connections
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="pull-right">
|
||||
{% if perms.dcim.add_manufacturer %}
|
||||
<a href="{% url 'dcim:manufacturer_add' %}" class="btn btn-primary">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
Add a manufacturer
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user