mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-01 14:43:38 +01:00
Compare commits
71 Commits
v2.1-beta1
...
v2.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bda56ea23 | ||
|
|
c7e9d90321 | ||
|
|
7476194bd1 | ||
|
|
1770c85689 | ||
|
|
e364659c6e | ||
|
|
9e26198afe | ||
|
|
f9b6ddc230 | ||
|
|
0991f94d06 | ||
|
|
32513083b1 | ||
|
|
336cdcddc5 | ||
|
|
4047c1a4e4 | ||
|
|
091cf390d2 | ||
|
|
05aaafc1cf | ||
|
|
5885b833cd | ||
|
|
106627da04 | ||
|
|
d73ea54e08 | ||
|
|
a45bfaf3da | ||
|
|
e85cc0d856 | ||
|
|
0f608f3a15 | ||
|
|
4ad5c6f864 | ||
|
|
be47b6a6c0 | ||
|
|
1f982c94ce | ||
|
|
12472a2612 | ||
|
|
f6a8d32880 | ||
|
|
bb2f86463e | ||
|
|
e8dafc02f7 | ||
|
|
0655834938 | ||
|
|
64a34ced72 | ||
|
|
d0dc505220 | ||
|
|
b2d3f3ff22 | ||
|
|
39730b6834 | ||
|
|
dd1991f2c6 | ||
|
|
2f32e11f53 | ||
|
|
280f55a875 | ||
|
|
dc68be5abf | ||
|
|
1ef90902bd | ||
|
|
6f37e97c67 | ||
|
|
e54c74d972 | ||
|
|
af9fa85cc1 | ||
|
|
74828e1409 | ||
|
|
dc77400ab1 | ||
|
|
e05d379101 | ||
|
|
a355783377 | ||
|
|
88239e0b0d | ||
|
|
5c63a499d5 | ||
|
|
50496b1a59 | ||
|
|
f7b0d22f86 | ||
|
|
ad95b86fdd | ||
|
|
43e1e0dbc8 | ||
|
|
f731900e2f | ||
|
|
b1bcaa33e7 | ||
|
|
17873706b7 | ||
|
|
e0ad2b4555 | ||
|
|
f89d91783b | ||
|
|
3ffe36e5ed | ||
|
|
be393a9d10 | ||
|
|
27eefd8705 | ||
|
|
097e0f38ff | ||
|
|
ce26b566a4 | ||
|
|
0e14bc1e02 | ||
|
|
ce6796ed9b | ||
|
|
c90cecc2fb | ||
|
|
b6bbcb0609 | ||
|
|
23f6832d9c | ||
|
|
88dace75a1 | ||
|
|
8eb140fd65 | ||
|
|
1f09f3d096 | ||
|
|
66be85a41f | ||
|
|
814c11167e | ||
|
|
57ddd5086f | ||
|
|
c171547037 |
33
docs/installation/migrating-to-python3.md
Normal file
33
docs/installation/migrating-to-python3.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Migration
|
||||
|
||||
Remove Python 2 packages
|
||||
|
||||
```no-highlight
|
||||
# apt-get remove --purge -y python-dev python-pip
|
||||
```
|
||||
|
||||
Install Python 3 packages
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y python3 python3-dev python3-pip
|
||||
```
|
||||
|
||||
Install Python Packages
|
||||
|
||||
```no-highlight
|
||||
# cd /opt/netbox
|
||||
# pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
Gunicorn Update
|
||||
|
||||
```no-highlight
|
||||
# pip uninstall gunicorn
|
||||
# pip3 install gunicorn
|
||||
```
|
||||
|
||||
Re-install LDAP Module (optional if using LDAP for auth)
|
||||
|
||||
```no-highlight
|
||||
sudo pip3 install django-auth-ldap
|
||||
```
|
||||
@@ -97,6 +97,14 @@ Python 2:
|
||||
# pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### NAPALM Automation
|
||||
|
||||
As of v2.1.0, NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3:
|
||||
|
||||
```no-highlight
|
||||
# pip install napalm
|
||||
```
|
||||
|
||||
# Configuration
|
||||
|
||||
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
|
||||
|
||||
194
docs/shell/intro.md
Normal file
194
docs/shell/intro.md
Normal file
@@ -0,0 +1,194 @@
|
||||
NetBox includes a Python shell withing which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
||||
|
||||
```
|
||||
./manage.py nbshell
|
||||
```
|
||||
|
||||
This will launch a customized version of [the built-in Django shell](https://docs.djangoproject.com/en/dev/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
|
||||
|
||||
```
|
||||
$ ./manage.py nbshell
|
||||
### NetBox interactive shell (jstretch-laptop)
|
||||
### Python 2.7.6 | Django 1.11.3 | NetBox 2.1.0-dev
|
||||
### lsmodels() will show available models. Use help(<model>) for more info.
|
||||
```
|
||||
|
||||
The function `lsmodels()` will print a list of all available NetBox models:
|
||||
|
||||
```
|
||||
>>> lsmodels()
|
||||
DCIM:
|
||||
ConsolePort
|
||||
ConsolePortTemplate
|
||||
ConsoleServerPort
|
||||
ConsoleServerPortTemplate
|
||||
Device
|
||||
...
|
||||
```
|
||||
|
||||
## Querying Objects
|
||||
|
||||
Objects are retrieved by forming a [Django queryset](https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-objects). The base queryset for an object takes the form `<model>.objects.all()`, which will return a (truncated) list of all objects of that type.
|
||||
|
||||
```
|
||||
>>> Device.objects.all()
|
||||
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>, <Device: TestDevice4>, <Device: TestDevice5>, '...(remaining elements truncated)...']>
|
||||
```
|
||||
|
||||
Use a `for` loop to cycle through all objects in the list:
|
||||
|
||||
```
|
||||
>>> for device in Device.objects.all():
|
||||
... print(device.name, device.device_type)
|
||||
...
|
||||
(u'TestDevice1', <DeviceType: PacketThingy 9000>)
|
||||
(u'TestDevice2', <DeviceType: PacketThingy 9000>)
|
||||
(u'TestDevice3', <DeviceType: PacketThingy 9000>)
|
||||
(u'TestDevice4', <DeviceType: PacketThingy 9000>)
|
||||
(u'TestDevice5', <DeviceType: PacketThingy 9000>)
|
||||
...
|
||||
```
|
||||
|
||||
To count all objects matching the query, replace `all()` with `count()`:
|
||||
|
||||
```
|
||||
>>> Device.objects.count()
|
||||
1274
|
||||
```
|
||||
|
||||
To retrieve a particular object (typically by its primary key or other unique field), use `get()`:
|
||||
|
||||
```
|
||||
>>> Site.objects.get(pk=7)
|
||||
<Site: Test Lab>
|
||||
```
|
||||
|
||||
### Filtering Querysets
|
||||
|
||||
In most cases, you want to retrieve only a specific subset of objects. To filter a queryset, replace `all()` with `filter()` and pass one or more keyword arguments. For example:
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(status=STATUS_ACTIVE)
|
||||
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>, <Device: TestDevice8>, <Device: TestDevice9>, '...(remaining elements truncated)...']>
|
||||
```
|
||||
|
||||
Querysets support slicing to return a specific range of objects.
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(status=STATUS_ACTIVE)[:3]
|
||||
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>]>
|
||||
```
|
||||
|
||||
The `count()` method can be appended to the queryset to return a count of objects rather than the full list.
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(status=STATUS_ACTIVE).count()
|
||||
982
|
||||
```
|
||||
|
||||
Relationships with other models can be traversed by concatenting field names with a double-underscore. For example, the following will return all devices assigned to the tenant named "Pied Piper."
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(tenant__name='Pied Piper')
|
||||
```
|
||||
|
||||
This approach can span multiple levels of relations. For example, the following will return all IP addresses assigned to a device in North America:
|
||||
|
||||
```
|
||||
>>> IPAddress.objects.filter(interface__device__site__region__slug='north-america')
|
||||
```
|
||||
|
||||
!!! note
|
||||
While the above query is functional, it is very inefficient. There are ways to optimize such requests, however they are out of the scope of this document. For more information, see the [Django queryset method reference](https://docs.djangoproject.com/en/dev/ref/models/querysets/) documentation.
|
||||
|
||||
Reverse relationships can be traversed as well. For example, the following will find all devices with an interface named "em0":
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(interfaces__name='em0')
|
||||
```
|
||||
|
||||
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the later of which is case-insensitive).
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(name__icontains='testdevice')
|
||||
```
|
||||
|
||||
Similarly, numeric fields can be filtered by values less than, greater than, and/or equal to a given value.
|
||||
|
||||
```
|
||||
>>> VLAN.objects.filter(vid__gt=2000)
|
||||
```
|
||||
|
||||
Multiple filters can be combined to further refine a queryset.
|
||||
|
||||
```
|
||||
>>> VLAN.objects.filter(vid__gt=2000, name__icontains='engineering')
|
||||
```
|
||||
|
||||
To return the inverse of a filtered queryset, use `exclude()` instead of `filter()`.
|
||||
|
||||
```
|
||||
>>> Device.objects.count()
|
||||
4479
|
||||
>>> Device.objects.filter(status=STATUS_ACTIVE).count()
|
||||
4133
|
||||
>>> Device.objects.exclude(status=STATUS_ACTIVE).count()
|
||||
346
|
||||
```
|
||||
|
||||
!!! info
|
||||
The examples above are intended only to provide a cursory introduction to queryset filtering. For an exhaustive list of the available filters, please consult the [Django queryset API docs](https://docs.djangoproject.com/en/dev/ref/models/querysets/).
|
||||
|
||||
## Creating and Updating Objects
|
||||
|
||||
New objects can be created by instantiating the desired model, defining values for all required attributes, and calling `save()` on the instance.
|
||||
|
||||
```
|
||||
>>> lab1 = Site.objects.get(pk=7)
|
||||
>>> myvlan = VLAN(vid=123, name='MyNewVLAN', site=lab1)
|
||||
>>> myvlan.save()
|
||||
```
|
||||
|
||||
Alternatively, the above can be performed as a single operation:
|
||||
|
||||
```
|
||||
>>> VLAN(vid=123, name='MyNewVLAN', site=Site.objects.get(pk=7)).save()
|
||||
```
|
||||
|
||||
To modify an object, retrieve it, update the desired field(s), and call `save()` again.
|
||||
|
||||
```
|
||||
>>> vlan = VLAN.objects.get(pk=1280)
|
||||
>>> vlan.name
|
||||
u'MyNewVLAN'
|
||||
>>> vlan.name = 'BetterName'
|
||||
>>> vlan.save()
|
||||
>>> VLAN.objects.get(pk=1280).name
|
||||
u'BetterName'
|
||||
```
|
||||
|
||||
!!! warning
|
||||
The Django ORM provides methods to create/edit many objects at once, namely `bulk_create()` and `update()`. These are best avoided in most cases as they bypass a model's built-in validation and can easily lead to database corruption if not used carefully.
|
||||
|
||||
## Deleting Objects
|
||||
|
||||
To delete an object, simply call `delete()` on its instance. This will return a dictionary of all objects (including related objects) which have been deleted as a result of this operation.
|
||||
|
||||
```
|
||||
>>> vlan
|
||||
<VLAN: 123 (BetterName)>
|
||||
>>> vlan.delete()
|
||||
(1, {u'extras.CustomFieldValue': 0, u'ipam.VLAN': 1})
|
||||
```
|
||||
|
||||
To delete multiple objects at once, call `delete()` on a filtered queryset. It's a good idea to always sanity-check the count of selected objects _before_ deleting them.
|
||||
|
||||
```
|
||||
>>> Device.objects.filter(name__icontains='test').count()
|
||||
27
|
||||
>>> Device.objects.filter(name__icontains='test').delete()
|
||||
(35, {u'extras.CustomFieldValue': 0, u'dcim.DeviceBay': 0, u'secrets.Secret': 0, u'dcim.InterfaceConnection': 4, u'extras.ImageAttachment': 0, u'dcim.Device': 27, u'dcim.Interface': 4, u'dcim.ConsolePort': 0, u'dcim.PowerPort': 0})
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Deletions are immediate and irreversible. Always think very carefully before calling `delete()` on an instance or queryset.
|
||||
@@ -8,6 +8,7 @@ pages:
|
||||
- 'Web Server': 'installation/web-server.md'
|
||||
- 'LDAP (Optional)': 'installation/ldap.md'
|
||||
- 'Upgrading': 'installation/upgrading.md'
|
||||
- 'Migrating to Python3': 'installation/migrating-to-python3.md'
|
||||
- 'Configuration':
|
||||
- 'Mandatory Settings': 'configuration/mandatory-settings.md'
|
||||
- 'Optional Settings': 'configuration/optional-settings.md'
|
||||
@@ -23,6 +24,8 @@ pages:
|
||||
- 'Authentication': 'api/authentication.md'
|
||||
- 'Working with Secrets': 'api/working-with-secrets.md'
|
||||
- 'Examples': 'api/examples.md'
|
||||
- 'Shell':
|
||||
- 'Introduction': 'shell/intro.md'
|
||||
|
||||
markdown_extensions:
|
||||
- admonition:
|
||||
|
||||
@@ -244,7 +244,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
if instance and instance.interface is not None:
|
||||
initial = kwargs.get('initial', {})
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
initial['rack'] = instance.interface.device.rack
|
||||
initial['device'] = instance.interface.device
|
||||
kwargs['initial'] = initial
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
|
||||
@@ -21,19 +21,18 @@ CIRCUITTYPE_ACTIONS = """
|
||||
class ProviderTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
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', 'account',)
|
||||
|
||||
|
||||
class ProviderSearchTable(SearchTable):
|
||||
name = tables.LinkColumn()
|
||||
class ProviderDetailTable(ProviderTable):
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
class Meta(ProviderTable.Meta):
|
||||
model = Provider
|
||||
fields = ('name', 'asn', 'account')
|
||||
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||
|
||||
|
||||
#
|
||||
@@ -74,19 +73,3 @@ class CircuitTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Circuit
|
||||
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
||||
|
||||
|
||||
class CircuitSearchTable(SearchTable):
|
||||
cid = tables.LinkColumn(verbose_name='ID')
|
||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
a_side = tables.LinkColumn(
|
||||
'dcim:site', accessor=Accessor('termination_a.site'), args=[Accessor('termination_a.site.slug')]
|
||||
)
|
||||
z_side = tables.LinkColumn(
|
||||
'dcim:site', accessor=Accessor('termination_z.site'), args=[Accessor('termination_z.site.slug')]
|
||||
)
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Circuit
|
||||
fields = ('cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
||||
|
||||
@@ -26,7 +26,7 @@ class ProviderListView(ObjectListView):
|
||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||
filter = filters.ProviderFilter
|
||||
filter_form = forms.ProviderFilterForm
|
||||
table = tables.ProviderTable
|
||||
table = tables.ProviderDetailTable
|
||||
template_name = 'circuits/provider_list.html'
|
||||
|
||||
|
||||
@@ -78,8 +78,8 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'circuits.change_provider'
|
||||
cls = Provider
|
||||
filter = filters.ProviderFilter
|
||||
table = tables.ProviderTable
|
||||
form = forms.ProviderBulkEditForm
|
||||
template_name = 'circuits/provider_bulk_edit.html'
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_provider'
|
||||
cls = Provider
|
||||
filter = filters.ProviderFilter
|
||||
table = tables.ProviderTable
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
@@ -116,6 +117,8 @@ class CircuitTypeEditView(CircuitTypeCreateView):
|
||||
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuittype'
|
||||
cls = CircuitType
|
||||
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||
table = tables.CircuitTypeTable
|
||||
default_return_url = 'circuits:circuittype_list'
|
||||
|
||||
|
||||
@@ -182,16 +185,19 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
cls = Circuit
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||
filter = filters.CircuitFilter
|
||||
table = tables.CircuitTable
|
||||
form = forms.CircuitBulkEditForm
|
||||
template_name = 'circuits/circuit_bulk_edit.html'
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'circuits.delete_circuit'
|
||||
cls = Circuit
|
||||
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||
filter = filters.CircuitFilter
|
||||
table = tables.CircuitTable
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
|
||||
@@ -422,7 +422,7 @@ class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['id', 'name', 'slug', 'rpc_client']
|
||||
fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client']
|
||||
|
||||
|
||||
class NestedPlatformSerializer(serializers.ModelSerializer):
|
||||
@@ -473,14 +473,10 @@ class DeviceSerializer(CustomFieldModelSerializer):
|
||||
device_bay = obj.parent_bay
|
||||
except DeviceBay.DoesNotExist:
|
||||
return None
|
||||
return {
|
||||
'id': device_bay.device.pk,
|
||||
'name': device_bay.device.name,
|
||||
'device_bay': {
|
||||
'id': device_bay.pk,
|
||||
'name': device_bay.name,
|
||||
}
|
||||
}
|
||||
context = {'request': self.context['request']}
|
||||
data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
|
||||
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
|
||||
return data
|
||||
|
||||
|
||||
class WritableDeviceSerializer(CustomFieldModelSerializer):
|
||||
@@ -690,6 +686,14 @@ class DeviceBaySerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device', 'name', 'installed_device']
|
||||
|
||||
|
||||
class NestedDeviceBaySerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
from collections import OrderedDict
|
||||
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
@@ -7,6 +8,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from dcim.models import (
|
||||
@@ -224,27 +226,64 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
write_serializer_class = serializers.WritableDeviceSerializer
|
||||
filter_class = filters.DeviceFilter
|
||||
|
||||
@detail_route(url_path='lldp-neighbors')
|
||||
def lldp_neighbors(self, request, pk):
|
||||
@detail_route(url_path='napalm')
|
||||
def napalm(self, request, pk):
|
||||
"""
|
||||
Retrieve live LLDP neighbors of a device
|
||||
Execute a NAPALM method on a Device
|
||||
"""
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
if not device.primary_ip:
|
||||
raise ServiceUnavailable("No IP configured for this device.")
|
||||
raise ServiceUnavailable("This device does not have a primary IP address configured.")
|
||||
if device.platform is None:
|
||||
raise ServiceUnavailable("No platform is configured for this device.")
|
||||
if not device.platform.napalm_driver:
|
||||
raise ServiceUnavailable("No NAPALM driver is configured for this device's platform ().".format(
|
||||
device.platform
|
||||
))
|
||||
|
||||
RPC = device.get_rpc_client()
|
||||
if not RPC:
|
||||
raise ServiceUnavailable("No RPC client available for this platform ({}).".format(device.platform))
|
||||
|
||||
# Connect to device and retrieve inventory info
|
||||
# Check that NAPALM is installed and verify the configured driver
|
||||
try:
|
||||
with RPC(device, username=settings.NETBOX_USERNAME, password=settings.NETBOX_PASSWORD) as rpc_client:
|
||||
lldp_neighbors = rpc_client.get_lldp_neighbors()
|
||||
except:
|
||||
raise ServiceUnavailable("Error connecting to the remote device.")
|
||||
import napalm
|
||||
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
|
||||
except ImportError:
|
||||
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
||||
try:
|
||||
driver = napalm.get_network_driver(device.platform.napalm_driver)
|
||||
except ModuleImportError:
|
||||
raise ServiceUnavailable("NAPALM driver for platform {} not found: {}.".format(
|
||||
device.platform, device.platform.napalm_driver
|
||||
))
|
||||
|
||||
return Response(lldp_neighbors)
|
||||
# Verify user permission
|
||||
if not request.user.has_perm('dcim.napalm_read'):
|
||||
return HttpResponseForbidden()
|
||||
|
||||
# Validate requested NAPALM methods
|
||||
napalm_methods = request.GET.getlist('method')
|
||||
for method in napalm_methods:
|
||||
if not hasattr(driver, method):
|
||||
return HttpResponseBadRequest("Unknown NAPALM method: {}".format(method))
|
||||
elif not method.startswith('get_'):
|
||||
return HttpResponseBadRequest("Unsupported NAPALM method: {}".format(method))
|
||||
|
||||
# Connect to the device and execute the requested methods
|
||||
# TODO: Improve error handling
|
||||
response = OrderedDict([(m, None) for m in napalm_methods])
|
||||
ip_address = str(device.primary_ip.address.ip)
|
||||
d = driver(
|
||||
hostname=ip_address,
|
||||
username=settings.NETBOX_USERNAME,
|
||||
password=settings.NETBOX_PASSWORD
|
||||
)
|
||||
try:
|
||||
d.open()
|
||||
for method in napalm_methods:
|
||||
response[method] = getattr(d, method)()
|
||||
except Exception as e:
|
||||
raise ServiceUnavailable("Error connecting to the device: {}".format(e))
|
||||
|
||||
d.close()
|
||||
return Response(response)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -402,7 +402,9 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
u_height = forms.IntegerField(min_value=1, required=False)
|
||||
is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
|
||||
interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
|
||||
is_console_server = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
|
||||
is_console_server = forms.NullBooleanField(
|
||||
required=False, widget=BulkEditNullBooleanSelect, label='Is a console server'
|
||||
)
|
||||
is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU')
|
||||
is_network_device = forms.NullBooleanField(
|
||||
required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
|
||||
@@ -556,7 +558,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['name', 'slug', 'rpc_client']
|
||||
fields = ['name', 'slug', 'napalm_driver', 'rpc_client']
|
||||
|
||||
|
||||
#
|
||||
@@ -630,7 +632,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
instance = kwargs.get('instance')
|
||||
# Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
|
||||
if instance and hasattr(instance, 'device_type'):
|
||||
initial = kwargs.get('initial', {})
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
initial['manufacturer'] = instance.device_type.manufacturer
|
||||
kwargs['initial'] = initial
|
||||
|
||||
@@ -1479,7 +1481,7 @@ class InterfaceCreateForm(DeviceComponentForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Set interfaces enabled by default
|
||||
kwargs['initial'] = kwargs.get('initial', {})
|
||||
kwargs['initial'] = kwargs.get('initial', {}).copy()
|
||||
kwargs['initial'].update({'enabled': True})
|
||||
|
||||
super(InterfaceCreateForm, self).__init__(*args, **kwargs)
|
||||
@@ -1694,8 +1696,7 @@ class InterfaceConnectionCSVForm(forms.ModelForm):
|
||||
return interface
|
||||
|
||||
|
||||
class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form):
|
||||
confirm = forms.BooleanField(required=True)
|
||||
class InterfaceConnectionDeletionForm(ConfirmationForm):
|
||||
# Used for HTTP redirect upon successful deletion
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
|
||||
|
||||
|
||||
40
netbox/dcim/migrations/0041_napalm_integration.py
Normal file
40
netbox/dcim/migrations/0041_napalm_integration.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.3 on 2017-07-14 17:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def rpc_client_to_napalm_driver(apps, schema_editor):
|
||||
"""
|
||||
Migrate legacy RPC clients to their respective NAPALM drivers
|
||||
"""
|
||||
Platform = apps.get_model('dcim', 'Platform')
|
||||
|
||||
Platform.objects.filter(rpc_client='juniper-junos').update(napalm_driver='junos')
|
||||
Platform.objects.filter(rpc_client='cisco-ios').update(napalm_driver='ios')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0040_inventoryitem_add_asset_tag_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='device',
|
||||
options={'ordering': ['name'], 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platform',
|
||||
name='napalm_driver',
|
||||
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices.', max_length=50, verbose_name='NAPALM driver'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='platform',
|
||||
name='rpc_client',
|
||||
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='Legacy RPC client'),
|
||||
),
|
||||
migrations.RunPython(rpc_client_to_napalm_driver),
|
||||
]
|
||||
@@ -738,7 +738,10 @@ class Platform(models.Model):
|
||||
"""
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True, verbose_name='RPC client')
|
||||
napalm_driver = models.CharField(max_length=50, blank=True, verbose_name='NAPALM driver',
|
||||
help_text="The name of the NAPALM driver to use when interacting with devices.")
|
||||
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True,
|
||||
verbose_name='Legacy RPC client')
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
@@ -809,6 +812,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = ['rack', 'position', 'face']
|
||||
permissions = (
|
||||
('napalm_read', 'Read-only access to devices via NAPALM'),
|
||||
('napalm_write', 'Read/write access to devices via NAPALM'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name or super(Device, self).__str__()
|
||||
|
||||
@@ -3,11 +3,11 @@ from __future__ import unicode_literals
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, Region, Site,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
|
||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
|
||||
)
|
||||
|
||||
|
||||
@@ -142,30 +142,26 @@ class SiteTable(BaseTable):
|
||||
name = tables.LinkColumn()
|
||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Site
|
||||
fields = ('pk', 'name', 'facility', 'region', 'tenant', 'asn')
|
||||
|
||||
|
||||
class SiteDetailTable(SiteTable):
|
||||
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
|
||||
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
|
||||
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
||||
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
|
||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Site
|
||||
class Meta(SiteTable.Meta):
|
||||
fields = (
|
||||
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
||||
'vlan_count', 'circuit_count',
|
||||
)
|
||||
|
||||
|
||||
class SiteSearchTable(SearchTable):
|
||||
name = tables.LinkColumn()
|
||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Site
|
||||
fields = ('name', 'facility', 'region', 'tenant', 'asn')
|
||||
|
||||
|
||||
#
|
||||
# Rack groups
|
||||
#
|
||||
@@ -214,29 +210,22 @@ class RackTable(BaseTable):
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
role = tables.TemplateColumn(RACK_ROLE)
|
||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||
devices = tables.Column(accessor=Accessor('device_count'))
|
||||
get_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')
|
||||
|
||||
|
||||
class RackDetailTable(RackTable):
|
||||
devices = tables.Column(accessor=Accessor('device_count'))
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(RackTable.Meta):
|
||||
fields = (
|
||||
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization'
|
||||
)
|
||||
|
||||
|
||||
class RackSearchTable(SearchTable):
|
||||
name = tables.LinkColumn()
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
role = tables.TemplateColumn(RACK_ROLE)
|
||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Rack
|
||||
fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height')
|
||||
|
||||
|
||||
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')
|
||||
@@ -302,23 +291,7 @@ class DeviceTypeTable(BaseTable):
|
||||
model = DeviceType
|
||||
fields = (
|
||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role', 'instance_count'
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeSearchTable(SearchTable):
|
||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
||||
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
||||
is_console_server = tables.BooleanColumn(verbose_name='CS')
|
||||
is_pdu = tables.BooleanColumn(verbose_name='PDU')
|
||||
is_network_device = tables.BooleanColumn(verbose_name='Net')
|
||||
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = DeviceType
|
||||
fields = (
|
||||
'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role',
|
||||
'is_network_device', 'subdevice_role', 'instance_count',
|
||||
)
|
||||
|
||||
|
||||
@@ -333,7 +306,6 @@ class ConsolePortTemplateTable(BaseTable):
|
||||
model = ConsolePortTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateTable(BaseTable):
|
||||
@@ -343,7 +315,6 @@ class ConsoleServerPortTemplateTable(BaseTable):
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
class PowerPortTemplateTable(BaseTable):
|
||||
@@ -353,7 +324,6 @@ class PowerPortTemplateTable(BaseTable):
|
||||
model = PowerPortTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
class PowerOutletTemplateTable(BaseTable):
|
||||
@@ -363,7 +333,6 @@ class PowerOutletTemplateTable(BaseTable):
|
||||
model = PowerOutletTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
class InterfaceTemplateTable(BaseTable):
|
||||
@@ -374,7 +343,6 @@ class InterfaceTemplateTable(BaseTable):
|
||||
model = InterfaceTemplate
|
||||
fields = ('pk', 'name', 'mgmt_only', 'form_factor')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
class DeviceBayTemplateTable(BaseTable):
|
||||
@@ -384,7 +352,6 @@ class DeviceBayTemplateTable(BaseTable):
|
||||
model = DeviceBayTemplate
|
||||
fields = ('pk', 'name')
|
||||
empty_text = "None"
|
||||
show_header = False
|
||||
|
||||
|
||||
#
|
||||
@@ -439,32 +406,22 @@ class DeviceTable(BaseTable):
|
||||
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
text=lambda record: record.device_type.full_name
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Device
|
||||
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
|
||||
|
||||
|
||||
class DeviceDetailTable(DeviceTable):
|
||||
primary_ip = tables.TemplateColumn(
|
||||
orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
class Meta(DeviceTable.Meta):
|
||||
model = Device
|
||||
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||
|
||||
|
||||
class DeviceSearchTable(SearchTable):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
||||
device_type = tables.LinkColumn(
|
||||
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||
text=lambda record: record.device_type.full_name
|
||||
)
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Device
|
||||
fields = ('name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
|
||||
|
||||
|
||||
class DeviceImportTable(BaseTable):
|
||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
||||
@@ -481,6 +438,52 @@ class DeviceImportTable(BaseTable):
|
||||
empty_text = False
|
||||
|
||||
|
||||
#
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class ConsoleServerPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class PowerPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class PowerOutletTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class InterfaceTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('name', 'form_factor', 'lag', 'enabled', 'mgmt_only', 'description')
|
||||
|
||||
|
||||
class DeviceBayTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
#
|
||||
# Device connections
|
||||
#
|
||||
|
||||
@@ -122,7 +122,9 @@ urlpatterns = [
|
||||
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
|
||||
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
|
||||
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||
url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
|
||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
|
||||
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
||||
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'),
|
||||
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||
|
||||
@@ -205,6 +205,8 @@ class RegionEditView(RegionCreateView):
|
||||
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_region'
|
||||
cls = Region
|
||||
queryset = Region.objects.annotate(site_count=Count('sites'))
|
||||
table = tables.RegionTable
|
||||
default_return_url = 'dcim:region_list'
|
||||
|
||||
|
||||
@@ -216,7 +218,7 @@ class SiteListView(ObjectListView):
|
||||
queryset = Site.objects.select_related('region', 'tenant')
|
||||
filter = filters.SiteFilter
|
||||
filter_form = forms.SiteFilterForm
|
||||
table = tables.SiteTable
|
||||
table = tables.SiteDetailTable
|
||||
template_name = 'dcim/site_list.html'
|
||||
|
||||
|
||||
@@ -273,9 +275,10 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_site'
|
||||
cls = Site
|
||||
queryset = Site.objects.select_related('region', 'tenant')
|
||||
filter = filters.SiteFilter
|
||||
table = tables.SiteTable
|
||||
form = forms.SiteBulkEditForm
|
||||
template_name = 'dcim/site_bulk_edit.html'
|
||||
default_return_url = 'dcim:site_list'
|
||||
|
||||
|
||||
@@ -307,7 +310,9 @@ class RackGroupEditView(RackGroupCreateView):
|
||||
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackgroup'
|
||||
cls = RackGroup
|
||||
queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
|
||||
filter = filters.RackGroupFilter
|
||||
table = tables.RackGroupTable
|
||||
default_return_url = 'dcim:rackgroup_list'
|
||||
|
||||
|
||||
@@ -337,6 +342,8 @@ class RackRoleEditView(RackRoleCreateView):
|
||||
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackrole'
|
||||
cls = RackRole
|
||||
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
|
||||
table = tables.RackRoleTable
|
||||
default_return_url = 'dcim:rackrole_list'
|
||||
|
||||
|
||||
@@ -354,7 +361,7 @@ class RackListView(ObjectListView):
|
||||
)
|
||||
filter = filters.RackFilter
|
||||
filter_form = forms.RackFilterForm
|
||||
table = tables.RackTable
|
||||
table = tables.RackDetailTable
|
||||
template_name = 'dcim/rack_list.html'
|
||||
|
||||
|
||||
@@ -455,16 +462,19 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_rack'
|
||||
cls = Rack
|
||||
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
filter = filters.RackFilter
|
||||
table = tables.RackTable
|
||||
form = forms.RackBulkEditForm
|
||||
template_name = 'dcim/rack_bulk_edit.html'
|
||||
default_return_url = 'dcim:rack_list'
|
||||
|
||||
|
||||
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rack'
|
||||
cls = Rack
|
||||
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
filter = filters.RackFilter
|
||||
table = tables.RackTable
|
||||
default_return_url = 'dcim:rack_list'
|
||||
|
||||
|
||||
@@ -510,6 +520,7 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackreservation'
|
||||
cls = RackReservation
|
||||
table = tables.RackReservationTable
|
||||
default_return_url = 'dcim:rackreservation_list'
|
||||
|
||||
|
||||
@@ -539,6 +550,8 @@ class ManufacturerEditView(ManufacturerCreateView):
|
||||
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_manufacturer'
|
||||
cls = Manufacturer
|
||||
queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
|
||||
table = tables.ManufacturerTable
|
||||
default_return_url = 'dcim:manufacturer_list'
|
||||
|
||||
|
||||
@@ -562,24 +575,30 @@ class DeviceTypeView(View):
|
||||
|
||||
# Component tables
|
||||
consoleport_table = tables.ConsolePortTemplateTable(
|
||||
natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||
natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||
show_header=False
|
||||
)
|
||||
consoleserverport_table = tables.ConsoleServerPortTemplateTable(
|
||||
natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||
natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||
show_header=False
|
||||
)
|
||||
powerport_table = tables.PowerPortTemplateTable(
|
||||
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||
show_header=False
|
||||
)
|
||||
poweroutlet_table = tables.PowerOutletTemplateTable(
|
||||
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||
show_header=False
|
||||
)
|
||||
interface_table = tables.InterfaceTemplateTable(
|
||||
list(InterfaceTemplate.objects.order_naturally(
|
||||
devicetype.interface_ordering
|
||||
).filter(device_type=devicetype))
|
||||
).filter(device_type=devicetype)),
|
||||
show_header=False
|
||||
)
|
||||
devicebay_table = tables.DeviceBayTemplateTable(
|
||||
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
||||
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||
show_header=False
|
||||
)
|
||||
if request.user.has_perm('dcim.change_devicetype'):
|
||||
consoleport_table.base_columns['pk'].visible = True
|
||||
@@ -621,16 +640,19 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_devicetype'
|
||||
cls = DeviceType
|
||||
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
|
||||
filter = filters.DeviceTypeFilter
|
||||
table = tables.DeviceTypeTable
|
||||
form = forms.DeviceTypeBulkEditForm
|
||||
template_name = 'dcim/devicetype_bulk_edit.html'
|
||||
default_return_url = 'dcim:devicetype_list'
|
||||
|
||||
|
||||
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicetype'
|
||||
cls = DeviceType
|
||||
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
|
||||
filter = filters.DeviceTypeFilter
|
||||
table = tables.DeviceTypeTable
|
||||
default_return_url = 'dcim:devicetype_list'
|
||||
|
||||
|
||||
@@ -653,6 +675,7 @@ class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
|
||||
parent_field = 'device_type'
|
||||
cls = ConsolePortTemplate
|
||||
parent_cls = DeviceType
|
||||
table = tables.ConsolePortTemplateTable
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
@@ -668,6 +691,7 @@ class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDelet
|
||||
permission_required = 'dcim.delete_consoleserverporttemplate'
|
||||
cls = ConsoleServerPortTemplate
|
||||
parent_cls = DeviceType
|
||||
table = tables.ConsoleServerPortTemplateTable
|
||||
|
||||
|
||||
class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
@@ -683,6 +707,7 @@ class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerporttemplate'
|
||||
cls = PowerPortTemplate
|
||||
parent_cls = DeviceType
|
||||
table = tables.PowerPortTemplateTable
|
||||
|
||||
|
||||
class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
@@ -698,6 +723,7 @@ class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
|
||||
permission_required = 'dcim.delete_poweroutlettemplate'
|
||||
cls = PowerOutletTemplate
|
||||
parent_cls = DeviceType
|
||||
table = tables.PowerOutletTemplateTable
|
||||
|
||||
|
||||
class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
@@ -713,14 +739,15 @@ class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interfacetemplate'
|
||||
cls = InterfaceTemplate
|
||||
parent_cls = DeviceType
|
||||
table = tables.InterfaceTemplateTable
|
||||
form = forms.InterfaceTemplateBulkEditForm
|
||||
template_name = 'dcim/interfacetemplate_bulk_edit.html'
|
||||
|
||||
|
||||
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interfacetemplate'
|
||||
cls = InterfaceTemplate
|
||||
parent_cls = DeviceType
|
||||
table = tables.InterfaceTemplateTable
|
||||
|
||||
|
||||
class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
@@ -736,6 +763,7 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicebaytemplate'
|
||||
cls = DeviceBayTemplate
|
||||
parent_cls = DeviceType
|
||||
table = tables.DeviceBayTemplateTable
|
||||
|
||||
|
||||
#
|
||||
@@ -764,6 +792,8 @@ class DeviceRoleEditView(DeviceRoleCreateView):
|
||||
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicerole'
|
||||
cls = DeviceRole
|
||||
queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
|
||||
table = tables.DeviceRoleTable
|
||||
default_return_url = 'dcim:devicerole_list'
|
||||
|
||||
|
||||
@@ -793,6 +823,8 @@ class PlatformEditView(PlatformCreateView):
|
||||
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_platform'
|
||||
cls = Platform
|
||||
queryset = Platform.objects.annotate(device_count=Count('devices'))
|
||||
table = tables.PlatformTable
|
||||
default_return_url = 'dcim:platform_list'
|
||||
|
||||
|
||||
@@ -805,7 +837,7 @@ class DeviceListView(ObjectListView):
|
||||
'primary_ip4', 'primary_ip6')
|
||||
filter = filters.DeviceFilter
|
||||
filter_form = forms.DeviceFilterForm
|
||||
table = tables.DeviceTable
|
||||
table = tables.DeviceDetailTable
|
||||
template_name = 'dcim/device_list.html'
|
||||
|
||||
|
||||
@@ -889,7 +921,20 @@ class DeviceInventoryView(View):
|
||||
})
|
||||
|
||||
|
||||
class DeviceLLDPNeighborsView(View):
|
||||
class DeviceStatusView(PermissionRequiredMixin, View):
|
||||
permission_required = 'dcim.napalm_read'
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
return render(request, 'dcim/device_status.html', {
|
||||
'device': device,
|
||||
})
|
||||
|
||||
|
||||
class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
|
||||
permission_required = 'dcim.napalm_read'
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
@@ -908,6 +953,18 @@ class DeviceLLDPNeighborsView(View):
|
||||
})
|
||||
|
||||
|
||||
class DeviceConfigView(PermissionRequiredMixin, View):
|
||||
permission_required = 'dcim.napalm_read'
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
return render(request, 'dcim/device_config.html', {
|
||||
'device': device,
|
||||
})
|
||||
|
||||
|
||||
class DeviceCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.add_device'
|
||||
model = Device
|
||||
@@ -956,16 +1013,19 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_device'
|
||||
cls = Device
|
||||
queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
form = forms.DeviceBulkEditForm
|
||||
template_name = 'dcim/device_bulk_edit.html'
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
|
||||
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_device'
|
||||
cls = Device
|
||||
queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||
filter = filters.DeviceFilter
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
|
||||
@@ -1073,6 +1133,7 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleport'
|
||||
cls = ConsolePort
|
||||
parent_cls = Device
|
||||
table = tables.ConsolePortTable
|
||||
|
||||
|
||||
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
@@ -1198,6 +1259,7 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverport'
|
||||
cls = ConsoleServerPort
|
||||
parent_cls = Device
|
||||
table = tables.ConsoleServerPortTable
|
||||
|
||||
|
||||
#
|
||||
@@ -1304,6 +1366,7 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerport'
|
||||
cls = PowerPort
|
||||
parent_cls = Device
|
||||
table = tables.PowerPortTable
|
||||
|
||||
|
||||
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
@@ -1431,6 +1494,7 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlet'
|
||||
cls = PowerOutlet
|
||||
parent_cls = Device
|
||||
table = tables.PowerOutletTable
|
||||
|
||||
|
||||
#
|
||||
@@ -1473,14 +1537,15 @@ class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
cls = Interface
|
||||
parent_cls = Device
|
||||
table = tables.InterfaceTable
|
||||
form = forms.InterfaceBulkEditForm
|
||||
template_name = 'dcim/interface_bulk_edit.html'
|
||||
|
||||
|
||||
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interface'
|
||||
cls = Interface
|
||||
parent_cls = Device
|
||||
table = tables.InterfaceTable
|
||||
|
||||
|
||||
#
|
||||
@@ -1561,6 +1626,7 @@ class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicebay'
|
||||
cls = DeviceBay
|
||||
parent_cls = Device
|
||||
table = tables.DeviceBayTable
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -37,19 +37,29 @@ class Command(BaseCommand):
|
||||
def get_namespace(self):
|
||||
namespace = {}
|
||||
|
||||
# Gather Django models from each app
|
||||
# Gather Django models and constants from each app
|
||||
for app in APPS:
|
||||
self.django_models[app] = []
|
||||
|
||||
# Models
|
||||
app_models = sys.modules['{}.models'.format(app)]
|
||||
for name in dir(app_models):
|
||||
model = getattr(app_models, name)
|
||||
try:
|
||||
if issubclass(model, Model):
|
||||
if issubclass(model, Model) and model._meta.app_label == app:
|
||||
namespace[name] = model
|
||||
self.django_models[app].append(name)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Constants
|
||||
try:
|
||||
app_constants = sys.modules['{}.constants'.format(app)]
|
||||
for name in dir(app_constants):
|
||||
namespace[name] = getattr(app_constants, name)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Load convenience commands
|
||||
namespace.update({
|
||||
'lsmodels': self._lsmodels,
|
||||
|
||||
@@ -217,7 +217,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
if instance and instance.vlan is not None:
|
||||
initial['vlan_group'] = instance.vlan.group
|
||||
kwargs['initial'] = initial
|
||||
@@ -264,7 +264,7 @@ class PrefixCSVForm(forms.ModelForm):
|
||||
required=False
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=IPADDRESS_STATUS_CHOICES,
|
||||
choices=PREFIX_STATUS_CHOICES,
|
||||
help_text='Operational status'
|
||||
)
|
||||
role = forms.ModelChoiceField(
|
||||
@@ -492,7 +492,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
if instance and instance.interface is not None:
|
||||
initial['interface_site'] = instance.interface.device.site
|
||||
initial['interface_rack'] = instance.interface.device.rack
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
@@ -152,16 +152,6 @@ class VRFTable(BaseTable):
|
||||
fields = ('pk', 'name', 'rd', 'tenant', 'description')
|
||||
|
||||
|
||||
class VRFSearchTable(SearchTable):
|
||||
name = tables.LinkColumn()
|
||||
rd = tables.Column(verbose_name='RD')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = VRF
|
||||
fields = ('name', 'rd', 'tenant', 'description')
|
||||
|
||||
|
||||
#
|
||||
# RIRs
|
||||
#
|
||||
@@ -171,6 +161,14 @@ class RIRTable(BaseTable):
|
||||
name = tables.LinkColumn(verbose_name='Name')
|
||||
is_private = tables.BooleanColumn(verbose_name='Private')
|
||||
aggregate_count = tables.Column(verbose_name='Aggregates')
|
||||
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RIR
|
||||
fields = ('pk', 'name', 'is_private', 'aggregate_count', 'actions')
|
||||
|
||||
|
||||
class RIRDetailTable(RIRTable):
|
||||
stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
|
||||
footer=lambda table: sum(r.stats['total'] for r in table.data))
|
||||
stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
|
||||
@@ -182,12 +180,12 @@ class RIRTable(BaseTable):
|
||||
stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
|
||||
footer=lambda table: sum(r.stats['available'] for r in table.data))
|
||||
utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
|
||||
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RIR
|
||||
fields = ('pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
|
||||
'stats_deprecated', 'stats_available', 'utilization', 'actions')
|
||||
class Meta(RIRTable.Meta):
|
||||
fields = (
|
||||
'pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
|
||||
'stats_deprecated', 'stats_available', 'utilization', 'actions',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -197,24 +195,21 @@ class RIRTable(BaseTable):
|
||||
class AggregateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
prefix = tables.LinkColumn(verbose_name='Aggregate')
|
||||
child_count = tables.Column(verbose_name='Prefixes')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Aggregate
|
||||
fields = ('pk', 'prefix', 'rir', 'date_added', 'description')
|
||||
|
||||
|
||||
class AggregateDetailTable(AggregateTable):
|
||||
child_count = tables.Column(verbose_name='Prefixes')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(AggregateTable.Meta):
|
||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description')
|
||||
|
||||
|
||||
class AggregateSearchTable(SearchTable):
|
||||
prefix = tables.LinkColumn(verbose_name='Aggregate')
|
||||
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Aggregate
|
||||
fields = ('prefix', 'rir', 'date_added', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Roles
|
||||
#
|
||||
@@ -241,7 +236,6 @@ class PrefixTable(BaseTable):
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
|
||||
@@ -249,37 +243,17 @@ class PrefixTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Prefix
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not record.pk else '',
|
||||
}
|
||||
|
||||
|
||||
class PrefixBriefTable(BaseTable):
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF)
|
||||
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')])
|
||||
class PrefixDetailTable(PrefixTable):
|
||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Prefix
|
||||
fields = ('prefix', 'vrf', 'status', 'site', 'vlan', 'role')
|
||||
orderable = False
|
||||
|
||||
|
||||
class PrefixSearchTable(SearchTable):
|
||||
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
|
||||
role = tables.TemplateColumn(PREFIX_ROLE_LINK)
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Prefix
|
||||
fields = ('prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
class Meta(PrefixTable.Meta):
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
|
||||
|
||||
#
|
||||
@@ -292,43 +266,26 @@ class IPAddressTable(BaseTable):
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
||||
nat_inside = tables.LinkColumn(
|
||||
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
|
||||
)
|
||||
device = tables.TemplateColumn(IPADDRESS_DEVICE, orderable=False)
|
||||
interface = tables.Column(orderable=False)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'description')
|
||||
fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'device', 'interface', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||
}
|
||||
|
||||
|
||||
class IPAddressBriefTable(BaseTable):
|
||||
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
|
||||
interface = tables.Column(orderable=False)
|
||||
class IPAddressDetailTable(IPAddressTable):
|
||||
nat_inside = tables.LinkColumn(
|
||||
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('address', 'device', 'interface', 'nat_inside')
|
||||
|
||||
|
||||
class IPAddressSearchTable(SearchTable):
|
||||
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
|
||||
interface = tables.Column(orderable=False)
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = IPAddress
|
||||
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'device', 'interface', 'description')
|
||||
class Meta(IPAddressTable.Meta):
|
||||
fields = (
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'interface', 'description',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -358,24 +315,17 @@ class VLANTable(BaseTable):
|
||||
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
role = tables.TemplateColumn(VLAN_ROLE_LINK)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLAN
|
||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
|
||||
|
||||
|
||||
class VLANDetailTable(VLANTable):
|
||||
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
|
||||
|
||||
class Meta(VLANTable.Meta):
|
||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
|
||||
|
||||
|
||||
class VLANSearchTable(SearchTable):
|
||||
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
role = tables.TemplateColumn(VLAN_ROLE_LINK)
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = VLAN
|
||||
fields = ('vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
|
||||
|
||||
@@ -103,8 +103,8 @@ class VRFView(View):
|
||||
def get(self, request, pk):
|
||||
|
||||
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
||||
prefix_table = tables.PrefixBriefTable(
|
||||
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role'))
|
||||
prefix_table = tables.PrefixTable(
|
||||
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False
|
||||
)
|
||||
prefix_table.exclude = ('vrf',)
|
||||
|
||||
@@ -142,16 +142,19 @@ class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_vrf'
|
||||
cls = VRF
|
||||
queryset = VRF.objects.select_related('tenant')
|
||||
filter = filters.VRFFilter
|
||||
table = tables.VRFTable
|
||||
form = forms.VRFBulkEditForm
|
||||
template_name = 'ipam/vrf_bulk_edit.html'
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vrf'
|
||||
cls = VRF
|
||||
queryset = VRF.objects.select_related('tenant')
|
||||
filter = filters.VRFFilter
|
||||
table = tables.VRFTable
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
@@ -163,7 +166,7 @@ class RIRListView(ObjectListView):
|
||||
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
||||
filter = filters.RIRFilter
|
||||
filter_form = forms.RIRFilterForm
|
||||
table = tables.RIRTable
|
||||
table = tables.RIRDetailTable
|
||||
template_name = 'ipam/rir_list.html'
|
||||
|
||||
def alter_queryset(self, request):
|
||||
@@ -259,7 +262,9 @@ class RIREditView(RIRCreateView):
|
||||
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_rir'
|
||||
cls = RIR
|
||||
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
||||
filter = filters.RIRFilter
|
||||
table = tables.RIRTable
|
||||
default_return_url = 'ipam:rir_list'
|
||||
|
||||
|
||||
@@ -273,7 +278,7 @@ class AggregateListView(ObjectListView):
|
||||
})
|
||||
filter = filters.AggregateFilter
|
||||
filter_form = forms.AggregateFilterForm
|
||||
table = tables.AggregateTable
|
||||
table = tables.AggregateDetailTable
|
||||
template_name = 'ipam/aggregate_list.html'
|
||||
|
||||
def extra_context(self):
|
||||
@@ -360,16 +365,19 @@ class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_aggregate'
|
||||
cls = Aggregate
|
||||
queryset = Aggregate.objects.select_related('rir')
|
||||
filter = filters.AggregateFilter
|
||||
table = tables.AggregateTable
|
||||
form = forms.AggregateBulkEditForm
|
||||
template_name = 'ipam/aggregate_bulk_edit.html'
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_aggregate'
|
||||
cls = Aggregate
|
||||
queryset = Aggregate.objects.select_related('rir')
|
||||
filter = filters.AggregateFilter
|
||||
table = tables.AggregateTable
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
@@ -399,6 +407,7 @@ class RoleEditView(RoleCreateView):
|
||||
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_role'
|
||||
cls = Role
|
||||
table = tables.RoleTable
|
||||
default_return_url = 'ipam:role_list'
|
||||
|
||||
|
||||
@@ -410,7 +419,7 @@ class PrefixListView(ObjectListView):
|
||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
filter = filters.PrefixFilter
|
||||
filter_form = forms.PrefixFilterForm
|
||||
table = tables.PrefixTable
|
||||
table = tables.PrefixDetailTable
|
||||
template_name = 'ipam/prefix_list.html'
|
||||
|
||||
def alter_queryset(self, request):
|
||||
@@ -445,7 +454,7 @@ class PrefixView(View):
|
||||
).select_related(
|
||||
'site', 'role'
|
||||
).annotate_depth()
|
||||
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
|
||||
parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
|
||||
parent_prefix_table.exclude = ('vrf',)
|
||||
|
||||
# Duplicate prefixes table
|
||||
@@ -456,7 +465,7 @@ class PrefixView(View):
|
||||
).select_related(
|
||||
'site', 'role'
|
||||
)
|
||||
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
|
||||
duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
|
||||
duplicate_prefix_table.exclude = ('vrf',)
|
||||
|
||||
# Child prefixes table
|
||||
@@ -564,16 +573,19 @@ class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_prefix'
|
||||
cls = Prefix
|
||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
filter = filters.PrefixFilter
|
||||
table = tables.PrefixTable
|
||||
form = forms.PrefixBulkEditForm
|
||||
template_name = 'ipam/prefix_bulk_edit.html'
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_prefix'
|
||||
cls = Prefix
|
||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
filter = filters.PrefixFilter
|
||||
table = tables.PrefixTable
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
@@ -585,7 +597,7 @@ class IPAddressListView(ObjectListView):
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
|
||||
filter = filters.IPAddressFilter
|
||||
filter_form = forms.IPAddressFilterForm
|
||||
table = tables.IPAddressTable
|
||||
table = tables.IPAddressDetailTable
|
||||
template_name = 'ipam/ipaddress_list.html'
|
||||
|
||||
|
||||
@@ -601,7 +613,7 @@ class IPAddressView(View):
|
||||
).select_related(
|
||||
'site', 'role'
|
||||
)
|
||||
parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
|
||||
parent_prefixes_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
|
||||
parent_prefixes_table.exclude = ('vrf',)
|
||||
|
||||
# Duplicate IPs table
|
||||
@@ -612,7 +624,7 @@ class IPAddressView(View):
|
||||
).select_related(
|
||||
'interface__device', 'nat_inside'
|
||||
)
|
||||
duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
|
||||
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
|
||||
|
||||
# Related IP table
|
||||
related_ips = IPAddress.objects.select_related(
|
||||
@@ -622,7 +634,7 @@ class IPAddressView(View):
|
||||
).filter(
|
||||
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
|
||||
)
|
||||
related_ips_table = tables.IPAddressBriefTable(list(related_ips))
|
||||
related_ips_table = tables.IPAddressTable(list(related_ips), orderable=False)
|
||||
|
||||
return render(request, 'ipam/ipaddress.html', {
|
||||
'ipaddress': ipaddress,
|
||||
@@ -669,16 +681,19 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_ipaddress'
|
||||
cls = IPAddress
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
|
||||
filter = filters.IPAddressFilter
|
||||
table = tables.IPAddressTable
|
||||
form = forms.IPAddressBulkEditForm
|
||||
template_name = 'ipam/ipaddress_bulk_edit.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_ipaddress'
|
||||
cls = IPAddress
|
||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
|
||||
filter = filters.IPAddressFilter
|
||||
table = tables.IPAddressTable
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
|
||||
@@ -710,7 +725,9 @@ class VLANGroupEditView(VLANGroupCreateView):
|
||||
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vlangroup'
|
||||
cls = VLANGroup
|
||||
queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
|
||||
filter = filters.VLANGroupFilter
|
||||
table = tables.VLANGroupTable
|
||||
default_return_url = 'ipam:vlangroup_list'
|
||||
|
||||
|
||||
@@ -722,7 +739,7 @@ class VLANListView(ObjectListView):
|
||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
|
||||
filter = filters.VLANFilter
|
||||
filter_form = forms.VLANFilterForm
|
||||
table = tables.VLANTable
|
||||
table = tables.VLANDetailTable
|
||||
template_name = 'ipam/vlan_list.html'
|
||||
|
||||
|
||||
@@ -734,7 +751,7 @@ class VLANView(View):
|
||||
'site__region', 'tenant__group', 'role'
|
||||
), pk=pk)
|
||||
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
|
||||
prefix_table = tables.PrefixBriefTable(list(prefixes))
|
||||
prefix_table = tables.PrefixTable(list(prefixes), orderable=False)
|
||||
prefix_table.exclude = ('vlan',)
|
||||
|
||||
return render(request, 'ipam/vlan.html', {
|
||||
@@ -771,16 +788,19 @@ class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'ipam.change_vlan'
|
||||
cls = VLAN
|
||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
filter = filters.VLANFilter
|
||||
table = tables.VLANTable
|
||||
form = forms.VLANBulkEditForm
|
||||
template_name = 'ipam/vlan_bulk_edit.html'
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'ipam.delete_vlan'
|
||||
cls = VLAN
|
||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||
filter = filters.VLANFilter
|
||||
table = tables.VLANTable
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.1.0-dev'
|
||||
VERSION = '2.1.0'
|
||||
|
||||
# Import required configuration parameters
|
||||
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
|
||||
|
||||
@@ -11,20 +11,20 @@ from django.views.generic import View
|
||||
|
||||
from circuits.filters import CircuitFilter, ProviderFilter
|
||||
from circuits.models import Circuit, Provider
|
||||
from circuits.tables import CircuitSearchTable, ProviderSearchTable
|
||||
from circuits.tables import CircuitTable, ProviderTable
|
||||
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
|
||||
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
|
||||
from dcim.tables import DeviceSearchTable, DeviceTypeSearchTable, RackSearchTable, SiteSearchTable
|
||||
from dcim.tables import DeviceTable, DeviceTypeTable, RackTable, SiteTable
|
||||
from extras.models import TopologyMap, UserAction
|
||||
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||
from ipam.tables import AggregateSearchTable, IPAddressSearchTable, PrefixSearchTable, VLANSearchTable, VRFSearchTable
|
||||
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
||||
from secrets.filters import SecretFilter
|
||||
from secrets.models import Secret
|
||||
from secrets.tables import SecretSearchTable
|
||||
from secrets.tables import SecretTable
|
||||
from tenancy.filters import TenantFilter
|
||||
from tenancy.models import Tenant
|
||||
from tenancy.tables import TenantSearchTable
|
||||
from tenancy.tables import TenantTable
|
||||
from .forms import SearchForm
|
||||
|
||||
|
||||
@@ -34,83 +34,85 @@ SEARCH_TYPES = OrderedDict((
|
||||
('provider', {
|
||||
'queryset': Provider.objects.all(),
|
||||
'filter': ProviderFilter,
|
||||
'table': ProviderSearchTable,
|
||||
'table': ProviderTable,
|
||||
'url': 'circuits:provider_list',
|
||||
}),
|
||||
('circuit', {
|
||||
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
|
||||
'filter': CircuitFilter,
|
||||
'table': CircuitSearchTable,
|
||||
'table': CircuitTable,
|
||||
'url': 'circuits:circuit_list',
|
||||
}),
|
||||
# DCIM
|
||||
('site', {
|
||||
'queryset': Site.objects.select_related('region', 'tenant'),
|
||||
'filter': SiteFilter,
|
||||
'table': SiteSearchTable,
|
||||
'table': SiteTable,
|
||||
'url': 'dcim:site_list',
|
||||
}),
|
||||
('rack', {
|
||||
'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'),
|
||||
'filter': RackFilter,
|
||||
'table': RackSearchTable,
|
||||
'table': RackTable,
|
||||
'url': 'dcim:rack_list',
|
||||
}),
|
||||
('devicetype', {
|
||||
'queryset': DeviceType.objects.select_related('manufacturer'),
|
||||
'filter': DeviceTypeFilter,
|
||||
'table': DeviceTypeSearchTable,
|
||||
'table': DeviceTypeTable,
|
||||
'url': 'dcim:devicetype_list',
|
||||
}),
|
||||
('device', {
|
||||
'queryset': Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'),
|
||||
'queryset': Device.objects.select_related(
|
||||
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'
|
||||
),
|
||||
'filter': DeviceFilter,
|
||||
'table': DeviceSearchTable,
|
||||
'table': DeviceTable,
|
||||
'url': 'dcim:device_list',
|
||||
}),
|
||||
# IPAM
|
||||
('vrf', {
|
||||
'queryset': VRF.objects.select_related('tenant'),
|
||||
'filter': VRFFilter,
|
||||
'table': VRFSearchTable,
|
||||
'table': VRFTable,
|
||||
'url': 'ipam:vrf_list',
|
||||
}),
|
||||
('aggregate', {
|
||||
'queryset': Aggregate.objects.select_related('rir'),
|
||||
'filter': AggregateFilter,
|
||||
'table': AggregateSearchTable,
|
||||
'table': AggregateTable,
|
||||
'url': 'ipam:aggregate_list',
|
||||
}),
|
||||
('prefix', {
|
||||
'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
|
||||
'filter': PrefixFilter,
|
||||
'table': PrefixSearchTable,
|
||||
'table': PrefixTable,
|
||||
'url': 'ipam:prefix_list',
|
||||
}),
|
||||
('ipaddress', {
|
||||
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
|
||||
'filter': IPAddressFilter,
|
||||
'table': IPAddressSearchTable,
|
||||
'table': IPAddressTable,
|
||||
'url': 'ipam:ipaddress_list',
|
||||
}),
|
||||
('vlan', {
|
||||
'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'),
|
||||
'filter': VLANFilter,
|
||||
'table': VLANSearchTable,
|
||||
'table': VLANTable,
|
||||
'url': 'ipam:vlan_list',
|
||||
}),
|
||||
# Secrets
|
||||
('secret', {
|
||||
'queryset': Secret.objects.select_related('role', 'device'),
|
||||
'filter': SecretFilter,
|
||||
'table': SecretSearchTable,
|
||||
'table': SecretTable,
|
||||
'url': 'secrets:secret_list',
|
||||
}),
|
||||
# Tenancy
|
||||
('tenant', {
|
||||
'queryset': Tenant.objects.select_related('group'),
|
||||
'filter': TenantFilter,
|
||||
'table': TenantSearchTable,
|
||||
'table': TenantTable,
|
||||
'url': 'tenancy:tenant_list',
|
||||
}),
|
||||
))
|
||||
@@ -189,7 +191,7 @@ class SearchView(View):
|
||||
|
||||
# Construct the results table for this object type
|
||||
filtered_queryset = filter_cls({'q': form.cleaned_data['q']}, queryset=queryset).qs
|
||||
table = table(filtered_queryset)
|
||||
table = table(filtered_queryset, orderable=False)
|
||||
table.paginate(per_page=SEARCH_MAX_RESULTS)
|
||||
|
||||
if table.page:
|
||||
|
||||
@@ -333,6 +333,31 @@ table.component-list tr.ipaddress:hover td {
|
||||
background-color: #e6f7f7;
|
||||
}
|
||||
|
||||
/* AJAX loader */
|
||||
.loading {
|
||||
position: fixed;
|
||||
display: none;
|
||||
z-index: 999;
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
overflow: show;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
.loading:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* Misc */
|
||||
.banner-bottom {
|
||||
margin-bottom: 50px;
|
||||
|
||||
BIN
netbox/project-static/img/ajax-loader.gif
Normal file
BIN
netbox/project-static/img/ajax-loader.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
File diff suppressed because one or more lines are too long
@@ -42,13 +42,15 @@ class UserKeyAdmin(admin.ModelAdmin):
|
||||
if 'activate' in request.POST:
|
||||
form = ActivateUserKeyForm(request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
|
||||
master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
|
||||
if master_key is not None:
|
||||
for uk in form.cleaned_data['_selected_action']:
|
||||
uk.activate(master_key)
|
||||
return redirect('admin:secrets_userkey_changelist')
|
||||
except ValueError:
|
||||
messages.error(request, "Invalid private key provided. Unable to retrieve master key.")
|
||||
else:
|
||||
messages.error(
|
||||
request, "Invalid private key provided. Unable to retrieve master key.", extra_tags='error'
|
||||
)
|
||||
else:
|
||||
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
fields = ['name', 'slug']
|
||||
fields = ['name', 'slug', 'users', 'groups']
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import django_tables2 as tables
|
||||
|
||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
|
||||
from .models import SecretRole, Secret
|
||||
|
||||
@@ -43,11 +43,3 @@ class SecretTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Secret
|
||||
fields = ('pk', 'device', 'role', 'name', 'last_updated')
|
||||
|
||||
|
||||
class SecretSearchTable(SearchTable):
|
||||
device = tables.LinkColumn()
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Secret
|
||||
fields = ('device', 'role', 'name', 'last_updated')
|
||||
|
||||
@@ -4,7 +4,6 @@ import base64
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required, login_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db import transaction, IntegrityError
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
@@ -56,6 +55,8 @@ class SecretRoleEditView(SecretRoleCreateView):
|
||||
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'secrets.delete_secretrole'
|
||||
cls = SecretRole
|
||||
queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
||||
table = tables.SecretRoleTable
|
||||
default_return_url = 'secrets:secretrole_list'
|
||||
|
||||
|
||||
@@ -240,14 +241,17 @@ class SecretBulkImportView(BulkImportView):
|
||||
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'secrets.change_secret'
|
||||
cls = Secret
|
||||
queryset = Secret.objects.select_related('role', 'device')
|
||||
filter = filters.SecretFilter
|
||||
table = tables.SecretTable
|
||||
form = forms.SecretBulkEditForm
|
||||
template_name = 'secrets/secret_bulk_edit.html'
|
||||
default_return_url = 'secrets:secret_list'
|
||||
|
||||
|
||||
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'secrets.delete_secret'
|
||||
cls = Secret
|
||||
queryset = Secret.objects.select_related('role', 'device')
|
||||
filter = filters.SecretFilter
|
||||
table = tables.SecretTable
|
||||
default_return_url = 'secrets:secret_list'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>NetBox - {% block title %}Home{% endblock %}</title>
|
||||
<title>{% block title %}Home{% endblock %} - NetBox</title>
|
||||
<link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.css' %}">
|
||||
@@ -323,13 +323,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="text/javascript">
|
||||
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
||||
</script>
|
||||
<script src="{% static 'js/jquery-3.2.0.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery-3.2.1.min.js' %}"></script>
|
||||
<script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
|
||||
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'js/forms.js' %}?v{{ settings.VERSION }}"></script>
|
||||
<script type="text/javascript">
|
||||
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
||||
var loading = $(".loading");
|
||||
$(document).ajaxStart(function() {
|
||||
loading.show();
|
||||
}).ajaxStop(function() {
|
||||
loading.hide();
|
||||
});
|
||||
</script>
|
||||
{% block javascript %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Circuit Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Circuit</th>
|
||||
<th>Type</th>
|
||||
<th>Provider</th>
|
||||
<th>Port speed</th>
|
||||
<th>Commit rate</th>
|
||||
</tr>
|
||||
{% for circuit in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'circuits:circuit' pk=circuit.pk %}">{{ circuit }}</a></td>
|
||||
<td>{{ circuit.type }}</td>
|
||||
<td>{{ circuit.provider }}</td>
|
||||
<td>{{ circuit.port_speed_human }}</td>
|
||||
<td>{{ circuit.commit_rate_human }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,19 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Provider Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Provider</th>
|
||||
<th>Account</th>
|
||||
<th>ASN</th>
|
||||
</tr>
|
||||
{% for provider in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'circuits:provider' slug=provider.slug %}">{{ provider }}</a></td>
|
||||
<td>{{ provider.account }}</td>
|
||||
<td>{{ provider.asn }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,23 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Device Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
<th>Type</th>
|
||||
<th>Role</th>
|
||||
<th>Tenant</th>
|
||||
<th>Serial</th>
|
||||
</tr>
|
||||
{% for device in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
||||
<td>{{ device.device_type.full_name }}</td>
|
||||
<td>{{ device.device_role }}</td>
|
||||
<td>{{ device.tenant }}</td>
|
||||
<td>{{ device.serial }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
53
netbox/templates/dcim/device_config.html
Normal file
53
netbox/templates/dcim/device_config.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}{{ device }} - Config{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/ajax_loader.html' %}
|
||||
{% include 'dcim/inc/device_header.html' with active_tab='config' %}
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Device Configuration</strong></div>
|
||||
<div class="panel-body">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#running" aria-controls="running" role="tab" data-toggle="tab">Running</a></li>
|
||||
<li role="presentation"><a href="#startup" aria-controls="startup" role="tab" data-toggle="tab">Startup</a></li>
|
||||
<li role="presentation"><a href="#candidate" aria-controls="candidate" role="tab" data-toggle="tab">Candidate</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="running">
|
||||
<pre id="running_config"></pre>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="startup">
|
||||
<pre id="startup_config"></pre>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="candidate">
|
||||
<pre id="candidate_config"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$.ajax({
|
||||
url: "{% url 'dcim-api:device-napalm' pk=device.pk %}?method=get_config",
|
||||
dataType: 'json',
|
||||
success: function(json) {
|
||||
$('#running_config').html($.trim(json['get_config']['running']));
|
||||
$('#startup_config').html($.trim(json['get_config']['startup']));
|
||||
$('#candidate_config').html($.trim(json['get_config']['candidate']));
|
||||
},
|
||||
error: function(xhr) {
|
||||
alert(xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -3,64 +3,66 @@
|
||||
{% block title %}{{ device }} - LLDP Neighbors{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'dcim/inc/device_header.html' with active_tab='lldp-neighbors' %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>LLDP Neighbors</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Interface</th>
|
||||
<th>Configured Device</th>
|
||||
<th>Configured Interface</th>
|
||||
<th>LLDP Device</th>
|
||||
<th>LLDP Interface</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for iface in interfaces %}
|
||||
<tr id="{{ iface }}">
|
||||
<td>{{ iface }}</td>
|
||||
{% if iface.connection %}
|
||||
{% with iface.connected_interface as connected_iface %}
|
||||
<td class="configured_device" data="{{ connected_iface.device }}">
|
||||
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a>
|
||||
</td>
|
||||
<td class="configured_interface" data="{{ connected_iface }}">
|
||||
<span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span>
|
||||
</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td colspan="2">None</td>
|
||||
{% endif %}
|
||||
<td class="device"></td>
|
||||
<td class="interface"></td>
|
||||
{% include 'inc/ajax_loader.html' %}
|
||||
{% include 'dcim/inc/device_header.html' with active_tab='lldp-neighbors' %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>LLDP Neighbors</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Interface</th>
|
||||
<th>Configured Device</th>
|
||||
<th>Configured Interface</th>
|
||||
<th>LLDP Device</th>
|
||||
<th>LLDP Interface</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for iface in interfaces %}
|
||||
<tr id="{{ iface.name }}">
|
||||
<td>{{ iface }}</td>
|
||||
{% if iface.connection %}
|
||||
{% with iface.connected_interface as connected_iface %}
|
||||
<td class="configured_device" data="{{ connected_iface.device }}">
|
||||
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a>
|
||||
</td>
|
||||
<td class="configured_interface" data="{{ connected_iface }}">
|
||||
<span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span>
|
||||
</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td colspan="2">None</td>
|
||||
{% endif %}
|
||||
<td class="device"></td>
|
||||
<td class="interface"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$.ajax({
|
||||
url: "{% url 'dcim-api:device-lldp-neighbors' pk=device.pk %}",
|
||||
url: "{% url 'dcim-api:device-napalm' pk=device.pk %}?method=get_lldp_neighbors",
|
||||
dataType: 'json',
|
||||
success: function(json) {
|
||||
$.each(json, function(i, neighbor) {
|
||||
var row = $('#' + neighbor['local-interface'].replace(/(\/)/g, "\\$1"));
|
||||
$.each(json['get_lldp_neighbors'], function(iface, neighbors) {
|
||||
var neighbor = neighbors[0];
|
||||
var row = $('#' + iface.replace(/(\/)/g, "\\$1"));
|
||||
var configured_device = row.children('td.configured_device').attr('data');
|
||||
var configured_interface = row.children('td.configured_interface').attr('data');
|
||||
// Add LLDP neighbors to table
|
||||
row.children('td.device').html(neighbor['name']);
|
||||
row.children('td.interface').html(neighbor['remote-interface']);
|
||||
row.children('td.device').html(neighbor['hostname']);
|
||||
row.children('td.interface').html(neighbor['port']);
|
||||
// Apply colors to rows
|
||||
if (!configured_device && neighbor['name']) {
|
||||
if (!configured_device && neighbor['hostname']) {
|
||||
row.addClass('info');
|
||||
} else if (configured_device == neighbor['name'] && configured_interface == neighbor['remote-interface']) {
|
||||
} else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port']) {
|
||||
row.addClass('success');
|
||||
} else {
|
||||
row.addClass('danger');
|
||||
|
||||
125
netbox/templates/dcim/device_status.html
Normal file
125
netbox/templates/dcim/device_status.html
Normal file
@@ -0,0 +1,125 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}{{ device }} - Status{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/ajax_loader.html' %}
|
||||
{% include 'dcim/inc/device_header.html' with active_tab='status' %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Device Facts</strong></div>
|
||||
<table class="table panel-body">
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<td id="hostname"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>FQDN</th>
|
||||
<td id="fqdn"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Vendor</th>
|
||||
<td id="vendor"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Model</th>
|
||||
<td id="model"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Serial Number</th>
|
||||
<td id="serial_number"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>OS Version</th>
|
||||
<td id="os_version"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Uptime</th>
|
||||
<td id="uptime"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Environment</strong></div>
|
||||
<table class="table panel-body">
|
||||
<tr id="cpu">
|
||||
<th colspan="2"><i class="fa fa-tachometer"></i> CPU</th>
|
||||
</tr>
|
||||
<tr id="memory">
|
||||
<th colspan="2"><i class="fa fa-microchip"></i> Memory</th>
|
||||
</tr>
|
||||
<tr id="temperature">
|
||||
<th colspan="2"><i class="fa fa-thermometer"></i> Temperature</th>
|
||||
</tr>
|
||||
<tr id="fans">
|
||||
<th colspan="2"><i class="fa fa-superpowers"></i> Fans</th>
|
||||
</tr>
|
||||
<tr id="power">
|
||||
<th colspan="2"><i class="fa fa-bolt"></i> Power</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$.ajax({
|
||||
url: "{% url 'dcim-api:device-napalm' pk=device.pk %}?method=get_facts&method=get_environment",
|
||||
dataType: 'json',
|
||||
success: function(json) {
|
||||
$('#hostname').html(json['get_facts']['hostname']);
|
||||
$('#fqdn').html(json['get_facts']['fqdn']);
|
||||
$('#vendor').html(json['get_facts']['vendor']);
|
||||
$('#model').html(json['get_facts']['model']);
|
||||
$('#serial_number').html(json['get_facts']['serial_number']);
|
||||
$('#os_version').html(json['get_facts']['os_version']);
|
||||
$('#uptime').html(json['get_facts']['uptime']);
|
||||
$.each(json['get_environment']['cpu'], function(name, obj) {
|
||||
var row="<tr><td>" + name + "</td><td>" + obj['%usage'] + "%</td></tr>";
|
||||
$("#cpu").after(row)
|
||||
});
|
||||
$('#memory').after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "MB</td></tr>");
|
||||
$('#memory').after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "MB</td></tr>");
|
||||
$.each(json['get_environment']['temperature'], function(name, obj) {
|
||||
var style = "success";
|
||||
if (obj['is_alert']) {
|
||||
style = "warning";
|
||||
} else if (obj['is_critical']) {
|
||||
style = "danger";
|
||||
}
|
||||
var row="<tr class=\"" + style +"\"><td>" + name + "</td><td>" + obj['temperature'] + "°C</td></tr>";
|
||||
$("#temperature").after(row)
|
||||
});
|
||||
$.each(json['get_environment']['fans'], function(name, obj) {
|
||||
var row;
|
||||
if (obj['status']) {
|
||||
row="<tr class=\"success\"><td>" + name + "</td><td><i class=\"fa fa-check text-success\"></i></td></tr>";
|
||||
} else {
|
||||
row="<tr class=\"error\"><td>" + name + "</td><td><i class=\"fa fa-close text-error\"></i></td></tr>";
|
||||
}
|
||||
$("#fans").after(row)
|
||||
});
|
||||
$.each(json['get_environment']['power'], function(name, obj) {
|
||||
var row;
|
||||
if (obj['status']) {
|
||||
row="<tr class=\"success\"><td>" + name + "</td><td><i class=\"fa fa-check text-success\"></i></td></tr>";
|
||||
} else {
|
||||
row="<tr class=\"danger\"><td>" + name + "</td><td><i class=\"fa fa-close text-danger\"></i></td></tr>";
|
||||
}
|
||||
$("#power").after(row)
|
||||
});
|
||||
},
|
||||
error: function(xhr) {
|
||||
alert(xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,19 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Device Type Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Device type</th>
|
||||
<th>Manufacturer</th>
|
||||
<th>Height</th>
|
||||
</tr>
|
||||
{% for devicetype in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:devicetype' pk=devicetype.pk %}">{{ devicetype.model }}</a></td>
|
||||
<td>{{ devicetype.manufacturer }}</td>
|
||||
<td>{{ devicetype.u_height }}U</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -45,7 +45,15 @@
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if active_tab == 'info' %} class="active"{% endif %}><a href="{% url 'dcim:device' pk=device.pk %}">Info</a></li>
|
||||
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}><a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a></li>
|
||||
{% if device.status == 1 and device.platform.rpc_client and device.primary_ip %}
|
||||
<li role="presentation"{% if active_tab == 'lldp-neighbors' %} class="active"{% endif %}><a href="{% url 'dcim:device_lldp_neighbors' pk=device.pk %}">LLDP Neighbors</a></li>
|
||||
{% if perms.dcim.napalm_read %}
|
||||
{% if device.status == 1 and device.platform.napalm_driver and device.primary_ip %}
|
||||
<li role="presentation"{% if active_tab == 'status' %} class="active"{% endif %}><a href="{% url 'dcim:device_status' pk=device.pk %}">Status</a></li>
|
||||
<li role="presentation"{% if active_tab == 'lldp-neighbors' %} class="active"{% endif %}><a href="{% url 'dcim:device_lldp_neighbors' pk=device.pk %}">LLDP Neighbors</a></li>
|
||||
<li role="presentation"{% if active_tab == 'config' %} class="active"{% endif %}><a href="{% url 'dcim:device_config' pk=device.pk %}">Configuration</a></li>
|
||||
{% else %}
|
||||
<li role="presentation" class="disabled"><a href="#">Status</a></li>
|
||||
<li role="presentation" class="disabled"><a href="#">LLDP Neighbors</a></li>
|
||||
<li role="presentation" class="disabled"><a href="#">Configuration</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
<td colspan="2">
|
||||
<td colspan="3">
|
||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||
{% if ip.description %}
|
||||
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Interface Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Form Factor</th>
|
||||
</tr>
|
||||
{% for iface in selected_objects %}
|
||||
<tr>
|
||||
<td>{{ iface.name }}</td>
|
||||
<td>{{ iface.get_form_factor_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,25 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Interface Template Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Form Factor</th>
|
||||
<th>Management</th>
|
||||
</tr>
|
||||
{% for iface in selected_objects %}
|
||||
<tr>
|
||||
<td>{{ iface.name }}</td>
|
||||
<td>{{ iface.get_form_factor_display }}</td>
|
||||
<td>
|
||||
{% if iface.mgmt_only %}
|
||||
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
||||
{% else %}
|
||||
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,29 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Rack Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Site</th>
|
||||
<th>Group</th>
|
||||
<th>Tenant</th>
|
||||
<th>Role</th>
|
||||
<th>Type</th>
|
||||
<th>Width</th>
|
||||
<th>Height</th>
|
||||
</tr>
|
||||
{% for rack in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack }}</a></td>
|
||||
<td>{{ rack.site }}</td>
|
||||
<td>{{ rack.group }}</td>
|
||||
<td>{{ rack.tenant }}</td>
|
||||
<td>{{ rack.role }}</td>
|
||||
<td>{{ rack.get_type_display }}</td>
|
||||
<td>{{ rack.get_width_display }}</td>
|
||||
<td>{{ rack.u_height }}U</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,17 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Site Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Site</th>
|
||||
<th>Tenant</th>
|
||||
</tr>
|
||||
{% for site in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'dcim:site' slug=site.slug %}">{{ site }}</a></td>
|
||||
<td>{{ site.tenant }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
4
netbox/templates/inc/ajax_loader.html
Normal file
4
netbox/templates/inc/ajax_loader.html
Normal file
@@ -0,0 +1,4 @@
|
||||
{% load staticfiles %}
|
||||
<div class="loading text-center">
|
||||
<img src="{% static 'img/ajax-loader.gif' %}" />
|
||||
</div>
|
||||
@@ -1,21 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Aggregate Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Aggregate</th>
|
||||
<th>RIR</th>
|
||||
<th>Date Added</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for aggregate in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate }}</a></td>
|
||||
<td>{{ aggregate.rir }}</td>
|
||||
<td>{{ aggregate.date_added }}</td>
|
||||
<td>{{ aggregate.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,25 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}IP Address Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>VRF</th>
|
||||
<th>Tenant</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for ipaddress in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
|
||||
<td>{{ ipaddress.vrf|default:"Global" }}</td>
|
||||
<td>{{ ipaddress.tenant }}</td>
|
||||
<td>{{ ipaddress.get_status_display }}</td>
|
||||
<td>{% if ipaddress.interface %}<i class="glyphicon glyphicon-ok text-success" title="{{ ipaddress.interface.device }} {{ ipaddress.interface }}"></i>{% endif %}</td>
|
||||
<td>{{ ipaddress.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,25 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Prefix Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Prefix</th>
|
||||
<th>Site</th>
|
||||
<th>VRF</th>
|
||||
<th>Tenant</th>
|
||||
<th>Status</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
{% for prefix in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a></td>
|
||||
<td>{{ prefix.site }}</td>
|
||||
<td>{{ prefix.vrf|default:"Global" }}</td>
|
||||
<td>{{ prefix.tenant }}</td>
|
||||
<td>{{ prefix.get_status_display }}</td>
|
||||
<td>{{ prefix.role }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,25 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}VLAN Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>VLAN</th>
|
||||
<th>Site</th>
|
||||
<th>Group</th>
|
||||
<th>Tenant</th>
|
||||
<th>Status</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
{% for vlan in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ipam:vlan' pk=vlan.pk %}">{{ vlan }}</a></td>
|
||||
<td>{{ vlan.site }}</td>
|
||||
<td>{{ vlan.group }}</td>
|
||||
<td>{{ vlan.tenant }}</td>
|
||||
<td>{{ vlan.get_status_display }}</td>
|
||||
<td>{{ vlan.role }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,21 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}VRF Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>VRF</th>
|
||||
<th>RD</th>
|
||||
<th>Tenant</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for vrf in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ipam:vrf' pk=vrf.pk %}">{{ vrf.name }}</a></td>
|
||||
<td>{{ vrf.rd }}</td>
|
||||
<td>{{ vrf.tenant }}</td>
|
||||
<td>{{ vrf.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,19 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Secret Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
<th>Role</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
{% for secret in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret.device }}</a></td>
|
||||
<td>{{ secret.role }}</td>
|
||||
<td>{{ secret.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,17 +0,0 @@
|
||||
{% extends 'utilities/bulk_edit_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Tenant Bulk Edit{% endblock %}
|
||||
|
||||
{% block selected_objects_table %}
|
||||
<tr>
|
||||
<th>Tenant</th>
|
||||
<th>Group</th>
|
||||
</tr>
|
||||
{% for tenant in selected_objects %}
|
||||
<tr>
|
||||
<td><a href="{% url 'tenancy:tenant' slug=tenant.slug %}">{{ tenant }}</a></td>
|
||||
<td>{{ tenant.group }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,19 +0,0 @@
|
||||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete {{ obj_type_plural|default:"objects" }}?{% endblock %}
|
||||
|
||||
{% block message %}
|
||||
<p>
|
||||
Are you sure you want to delete these {{ selected_objects|length }} {{ obj_type_plural|default:"objects" }}{% if parent_obj %} from <a href="{{ parent_obj.get_absolute_url }}">{{ parent_obj }}</a>{% endif %}?
|
||||
</p>
|
||||
<ul>
|
||||
{% for obj in selected_objects %}
|
||||
{% if obj.get_absolute_url %}
|
||||
<li><a href="{{ obj.get_absolute_url }}">{{ obj }}</a></li>
|
||||
{% else %}
|
||||
<li>{{ obj }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
@@ -5,22 +5,14 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="panel panel-{{ panel_class|default:"danger" }}">
|
||||
<div class="panel-heading">{% block title %}{% endblock %}</div>
|
||||
<div class="panel-body">
|
||||
{% block message %}<p>Are you sure?</p>{% endblock %}
|
||||
<div class="form-group">
|
||||
<div class="checkbox{% if form.confirm.errors %} has-error{% endif %}">
|
||||
<label for="{{ form.confirm.id_for_label }}">
|
||||
{{ form.confirm }}
|
||||
{{ form.confirm.label }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="submit" name="_confirm" class="btn btn-{{ button_class|default:"danger" }}">Confirm</button>
|
||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
|
||||
38
netbox/templates/utilities/obj_bulk_delete.html
Normal file
38
netbox/templates/utilities/obj_bulk_delete.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block title %}Delete {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Confirm Bulk Deletion</strong></div>
|
||||
<div class="panel-body">
|
||||
<strong>Warning:</strong> The following operation will delete {{ table.rows|length }} {{ obj_type_plural }}. Please carefully review the {{ obj_type_plural }} to be deleted and confirm below.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-default">
|
||||
{% include 'inc/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="text-center">
|
||||
<button type="submit" name="_confirm" class="btn btn-danger">Delete these {{ table.rows|length }} {{ obj_type_plural }}</button>
|
||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,8 +1,9 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% block title %}{% endblock %}</h1>
|
||||
<h1>{% block title %}Editing {{ table.rows|length }} {{ obj_type_plural|bettertitle }}{% endblock %}</h1>
|
||||
<form action="." method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if request.POST.return_url %}
|
||||
@@ -12,15 +13,12 @@
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>{% block selected_objects_title %}{{ selected_objects|length }} Selected For Editing{% endblock %}</strong></div>
|
||||
<table class="panel-body table table-hover">
|
||||
{% block selected_objects_table %}{% endblock %}
|
||||
</table>
|
||||
{% include 'inc/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="col-md-4">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
@@ -44,7 +44,7 @@
|
||||
<td>
|
||||
{{ field.help_text|default:field.label }}
|
||||
{% if field.choices %}
|
||||
<br /><small class="text-muted">Choices: {{ field.choices|example_choices }}</small>
|
||||
<br /><small class="text-muted">Choices: {{ field|example_choices }}</small>
|
||||
{% elif field|widget_type == 'dateinput' %}
|
||||
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
|
||||
@@ -102,7 +102,7 @@ class TenancyForm(ChainedFieldsMixin, forms.Form):
|
||||
# Initialize helper selector
|
||||
instance = kwargs.get('instance')
|
||||
if instance and instance.tenant is not None:
|
||||
initial = kwargs.get('initial', {})
|
||||
initial = kwargs.get('initial', {}).copy()
|
||||
initial['tenant_group'] = instance.tenant.group
|
||||
kwargs['initial'] = initial
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import django_tables2 as tables
|
||||
|
||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
@@ -43,11 +43,3 @@ class TenantTable(BaseTable):
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Tenant
|
||||
fields = ('pk', 'name', 'group', 'description')
|
||||
|
||||
|
||||
class TenantSearchTable(SearchTable):
|
||||
name = tables.LinkColumn()
|
||||
|
||||
class Meta(SearchTable.Meta):
|
||||
model = Tenant
|
||||
fields = ('name', 'group', 'description')
|
||||
|
||||
@@ -42,6 +42,8 @@ class TenantGroupEditView(TenantGroupCreateView):
|
||||
class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'tenancy.delete_tenantgroup'
|
||||
cls = TenantGroup
|
||||
queryset = TenantGroup.objects.annotate(tenant_count=Count('tenants'))
|
||||
table = tables.TenantGroupTable
|
||||
default_return_url = 'tenancy:tenantgroup_list'
|
||||
|
||||
|
||||
@@ -113,14 +115,17 @@ class TenantBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class TenantBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'tenancy.change_tenant'
|
||||
cls = Tenant
|
||||
queryset = Tenant.objects.select_related('group')
|
||||
filter = filters.TenantFilter
|
||||
table = tables.TenantTable
|
||||
form = forms.TenantBulkEditForm
|
||||
template_name = 'tenancy/tenant_bulk_edit.html'
|
||||
default_return_url = 'tenancy:tenant_list'
|
||||
|
||||
|
||||
class TenantBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'tenancy.delete_tenant'
|
||||
cls = Tenant
|
||||
queryset = Tenant.objects.select_related('group')
|
||||
filter = filters.TenantFilter
|
||||
table = tables.TenantTable
|
||||
default_return_url = 'tenancy:tenant_list'
|
||||
|
||||
@@ -244,6 +244,7 @@ class TokenEditView(LoginRequiredMixin, View):
|
||||
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
|
||||
form = TokenForm(request.POST, instance=token)
|
||||
else:
|
||||
token = Token()
|
||||
form = TokenForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
@@ -259,6 +260,13 @@ class TokenEditView(LoginRequiredMixin, View):
|
||||
else:
|
||||
return redirect('user:token_list')
|
||||
|
||||
return render(request, 'utilities/obj_edit.html', {
|
||||
'obj': token,
|
||||
'obj_type': token._meta.verbose_name,
|
||||
'form': form,
|
||||
'return_url': reverse('user:token_list'),
|
||||
})
|
||||
|
||||
|
||||
class TokenDeleteView(LoginRequiredMixin, View):
|
||||
|
||||
|
||||
@@ -510,7 +510,7 @@ class ConfirmationForm(BootstrapMixin, ReturnURLForm):
|
||||
"""
|
||||
A generic confirmation form. The form is not valid unless the confirm field is checked.
|
||||
"""
|
||||
confirm = forms.BooleanField(required=True)
|
||||
confirm = forms.BooleanField(required=True, widget=forms.HiddenInput(), initial=True)
|
||||
|
||||
|
||||
class BulkEditForm(forms.Form):
|
||||
|
||||
@@ -16,21 +16,10 @@ class BaseTable(tables.Table):
|
||||
if self.empty_text is None:
|
||||
self.empty_text = 'No {} found.'.format(self._meta.model._meta.verbose_name_plural)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-hover',
|
||||
}
|
||||
|
||||
|
||||
class SearchTable(tables.Table):
|
||||
"""
|
||||
Default table for search results
|
||||
"""
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-hover table-headings',
|
||||
}
|
||||
orderable = False
|
||||
|
||||
|
||||
class ToggleColumn(tables.CheckBoxColumn):
|
||||
|
||||
@@ -63,19 +63,23 @@ def bettertitle(value):
|
||||
|
||||
|
||||
@register.filter()
|
||||
def example_choices(value, arg=3):
|
||||
def example_choices(field, arg=3):
|
||||
"""
|
||||
Returns a number (default: 3) of example choices for a ChoiceFiled (useful for CSV import forms).
|
||||
"""
|
||||
choices = []
|
||||
for id, label in value:
|
||||
if len(choices) == arg:
|
||||
choices.append('etc.')
|
||||
examples = []
|
||||
if hasattr(field, 'queryset'):
|
||||
choices = [(obj.pk, getattr(obj, field.to_field_name)) for obj in field.queryset[:arg + 1]]
|
||||
else:
|
||||
choices = field.choices
|
||||
for id, label in choices:
|
||||
if len(examples) == arg:
|
||||
examples.append('etc.')
|
||||
break
|
||||
if not id:
|
||||
continue
|
||||
choices.append(label)
|
||||
return ', '.join(choices) or 'None'
|
||||
examples.append(label)
|
||||
return ', '.join(examples) or 'None'
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -123,7 +123,7 @@ class ObjectListView(View):
|
||||
# Construct the table based on the user's permissions
|
||||
table = self.table(self.queryset)
|
||||
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
||||
table.base_columns['pk'].visible = True
|
||||
table.columns.show('pk')
|
||||
|
||||
# Apply the request context
|
||||
paginate = {
|
||||
@@ -461,7 +461,9 @@ class BulkEditView(View):
|
||||
|
||||
cls: The model of the objects being edited
|
||||
parent_cls: The model of the parent object (if any)
|
||||
queryset: Custom queryset to use when retrieving objects (e.g. to select related objects)
|
||||
filter: FilterSet to apply when deleting by QuerySet
|
||||
table: The table used to display devices being edited
|
||||
form: The form class used to edit objects in bulk
|
||||
template_name: The name of the template
|
||||
default_return_url: Name of the URL to which the user is redirected after editing the objects (can be overridden by
|
||||
@@ -469,9 +471,11 @@ class BulkEditView(View):
|
||||
"""
|
||||
cls = None
|
||||
parent_cls = None
|
||||
queryset = None
|
||||
filter = None
|
||||
table = None
|
||||
form = None
|
||||
template_name = None
|
||||
template_name = 'utilities/obj_bulk_edit.html'
|
||||
default_return_url = 'home'
|
||||
|
||||
def get(self):
|
||||
@@ -537,14 +541,17 @@ class BulkEditView(View):
|
||||
initial_data['pk'] = pk_list
|
||||
form = self.form(self.cls, initial=initial_data)
|
||||
|
||||
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
||||
if not selected_objects:
|
||||
# Retrieve objects being edited
|
||||
queryset = self.queryset or self.cls.objects.all()
|
||||
table = self.table(queryset.filter(pk__in=pk_list), orderable=False)
|
||||
if not table.rows:
|
||||
messages.warning(request, "No {} were selected.".format(self.cls._meta.verbose_name_plural))
|
||||
return redirect(return_url)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'selected_objects': selected_objects,
|
||||
'table': table,
|
||||
'obj_type_plural': self.cls._meta.verbose_name_plural,
|
||||
'return_url': return_url,
|
||||
})
|
||||
|
||||
@@ -602,7 +609,9 @@ class BulkDeleteView(View):
|
||||
|
||||
cls: The model of the objects being deleted
|
||||
parent_cls: The model of the parent object (if any)
|
||||
queryset: Custom queryset to use when retrieving objects (e.g. to select related objects)
|
||||
filter: FilterSet to apply when deleting by QuerySet
|
||||
table: The table used to display devices being deleted
|
||||
form: The form class used to delete objects in bulk
|
||||
template_name: The name of the template
|
||||
default_return_url: Name of the URL to which the user is redirected after deleting the objects (can be overriden by
|
||||
@@ -610,9 +619,11 @@ class BulkDeleteView(View):
|
||||
"""
|
||||
cls = None
|
||||
parent_cls = None
|
||||
queryset = None
|
||||
filter = None
|
||||
table = None
|
||||
form = None
|
||||
template_name = 'utilities/confirm_bulk_delete.html'
|
||||
template_name = 'utilities/obj_bulk_delete.html'
|
||||
default_return_url = 'home'
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
@@ -660,8 +671,10 @@ class BulkDeleteView(View):
|
||||
else:
|
||||
form = form_cls(initial={'pk': pk_list, 'return_url': return_url})
|
||||
|
||||
selected_objects = self.cls.objects.filter(pk__in=pk_list)
|
||||
if not selected_objects:
|
||||
# Retrieve objects being deleted
|
||||
queryset = self.queryset or self.cls.objects.all()
|
||||
table = self.table(queryset.filter(pk__in=pk_list), orderable=False)
|
||||
if not table.rows:
|
||||
messages.warning(request, "No {} were selected for deletion.".format(self.cls._meta.verbose_name_plural))
|
||||
return redirect(return_url)
|
||||
|
||||
@@ -669,7 +682,7 @@ class BulkDeleteView(View):
|
||||
'form': form,
|
||||
'parent_obj': parent_obj,
|
||||
'obj_type_plural': self.cls._meta.verbose_name_plural,
|
||||
'selected_objects': selected_objects,
|
||||
'table': table,
|
||||
'return_url': return_url,
|
||||
})
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ django-debug-toolbar>=1.7
|
||||
django-filter>=1.0.2
|
||||
django-mptt==0.8.7
|
||||
django-rest-swagger>=2.1.0
|
||||
django-tables2>=1.6.0
|
||||
django-tables2>=1.7.0
|
||||
djangorestframework>=3.6.2
|
||||
graphviz>=0.6
|
||||
Markdown>=2.6.7
|
||||
|
||||
Reference in New Issue
Block a user