Compare commits

..

1 Commits

Author SHA1 Message Date
Jeremy Stretch
1e04bd3c79 Release v2.1-beta 2017-07-10 13:15:52 -04:00
128 changed files with 1096 additions and 1364 deletions

View File

@@ -135,53 +135,11 @@ An API consumer can request an arbitrary number of objects by appending the "lim
---
## NAPALM_USERNAME
## NETBOX_USERNAME
## NAPALM_PASSWORD
## NETBOX_PASSWORD
NetBox will use these credentials when authenticating to remote devices via the [NAPALM library](https://napalm-automation.net/), if installed. Both parameters are optional.
Note: If SSH public key authentication has been set up for the system account under which NetBox runs, these parameters are not needed.
---
## NAPALM_ARGS
A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](http://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example:
```
NAPALM_ARGS = {
'api_key': '472071a93b60a1bd1fafb401d9f8ef41',
'port': 2222,
}
```
Note: Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument:
```
NAPALM_USERNAME = 'username'
NAPALM_PASSWORD = 'MySecretPassword'
NAPALM_ARGS = {
'secret': NAPALM_PASSWORD,
# Include any additional args here
}
```
---
## NAPALM_TIMEOUT
Default: 30 seconds
The amount of time (in seconds) to wait for NAPALM to connect to a device.
---
## NETBOX_USERNAME (Deprecated)
## NETBOX_PASSWORD (Deprecated)
These settings have been deprecated and will be removed in NetBox v2.2. Please use `NAPALM_USERNAME` and `NAPALM_PASSWORD` instead.
If provided, NetBox will use these credentials to authenticate against devices when collecting data.
---

View File

@@ -72,8 +72,7 @@ AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com"
# You can map user attributes to Django attributes as so.
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
"email": "mail"
"last_name": "sn"
}
```
@@ -109,3 +108,12 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
It is also possible map user attributes to Django attributes:
```python
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
}
```

View File

@@ -1,33 +0,0 @@
# 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
```

View File

@@ -97,14 +97,6 @@ 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`.

View File

@@ -1,194 +0,0 @@
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.

View File

@@ -8,7 +8,6 @@ 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'
@@ -24,8 +23,6 @@ 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:

View File

@@ -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', {}).copy()
initial = kwargs.get('initial', {})
initial['rack'] = instance.interface.device.rack
initial['device'] = instance.interface.device
kwargs['initial'] = initial

View File

@@ -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, ToggleColumn
from utilities.tables import BaseTable, SearchTable, ToggleColumn
from .models import Circuit, CircuitType, Provider
@@ -21,20 +21,21 @@ 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',)
class ProviderDetailTable(ProviderTable):
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
class Meta(ProviderTable.Meta):
model = Provider
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
class ProviderSearchTable(SearchTable):
name = tables.LinkColumn()
class Meta(SearchTable.Meta):
model = Provider
fields = ('name', 'asn', 'account')
#
# Circuit types
#
@@ -73,3 +74,19 @@ 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')

View File

@@ -26,7 +26,7 @@ class ProviderListView(ObjectListView):
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filter = filters.ProviderFilter
filter_form = forms.ProviderFilterForm
table = tables.ProviderDetailTable
table = tables.ProviderTable
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,7 +87,6 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider'
cls = Provider
filter = filters.ProviderFilter
table = tables.ProviderTable
default_return_url = 'circuits:provider_list'
@@ -117,8 +116,6 @@ 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'
@@ -185,19 +182,16 @@ 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'

View File

@@ -422,7 +422,7 @@ class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta:
model = Platform
fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client']
fields = ['id', 'name', 'slug', 'rpc_client']
class NestedPlatformSerializer(serializers.ModelSerializer):
@@ -473,10 +473,14 @@ class DeviceSerializer(CustomFieldModelSerializer):
device_bay = obj.parent_bay
except DeviceBay.DoesNotExist:
return None
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
return {
'id': device_bay.device.pk,
'name': device_bay.device.name,
'device_bay': {
'id': device_bay.pk,
'name': device_bay.name,
}
}
class WritableDeviceSerializer(CustomFieldModelSerializer):
@@ -686,14 +690,6 @@ 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:

View File

@@ -1,13 +1,12 @@
from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework.decorators import detail_route
from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAuthenticated
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 (
@@ -20,7 +19,7 @@ from dcim import filters
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.api import IsAuthenticatedOrLoginNotRequired, ServiceUnavailable, WritableSerializerMixin
from utilities.api import ServiceUnavailable, WritableSerializerMixin
from .exceptions import MissingFilterException
from . import serializers
@@ -225,66 +224,27 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
write_serializer_class = serializers.WritableDeviceSerializer
filter_class = filters.DeviceFilter
@detail_route(url_path='napalm')
def napalm(self, request, pk):
@detail_route(url_path='lldp-neighbors')
def lldp_neighbors(self, request, pk):
"""
Execute a NAPALM method on a Device
Retrieve live LLDP neighbors of a device
"""
device = get_object_or_404(Device, pk=pk)
if not device.primary_ip:
raise ServiceUnavailable("This device does not have a primary IP address configured.")
if device.platform is None:
raise ServiceUnavailable("No platform is configured for this device.")
if not device.platform.napalm_driver:
raise ServiceUnavailable("No NAPALM driver is configured for this device's platform ().".format(
device.platform
))
raise ServiceUnavailable("No IP configured for this device.")
# Check that NAPALM is installed and verify the configured driver
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
try:
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
))
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.")
# 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.NAPALM_USERNAME,
password=settings.NAPALM_PASSWORD,
timeout=settings.NAPALM_TIMEOUT,
optional_args=settings.NAPALM_ARGS
)
try:
d.open()
for method in napalm_methods:
response[method] = getattr(d, method)()
except Exception as e:
raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
d.close()
return Response(response)
return Response(lldp_neighbors)
#
@@ -386,7 +346,7 @@ class ConnectedDeviceViewSet(ViewSet):
* `peer-device`: The name of the peer device
* `peer-interface`: The name of the peer interface
"""
permission_classes = [IsAuthenticatedOrLoginNotRequired]
permission_classes = [IsAuthenticated]
def get_view_name(self):
return "Connected Device Locator"

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
import django_filters
from netaddr import EUI
from netaddr.core import AddrFormatError
from django.contrib.auth.models import User
@@ -9,7 +8,7 @@ from django.db.models import Q
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
@@ -114,7 +113,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search',
label='Search',
)
facility_id = NullableCharFieldFilter()
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@@ -158,7 +156,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Rack
fields = ['type', 'width', 'u_height', 'desc_units']
fields = ['facility_id', 'type', 'width', 'u_height', 'desc_units']
def search(self, queryset, name, value):
if not value.strip():
@@ -385,8 +383,6 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
to_field_name='slug',
label='Platform (slug)',
)
name = NullableCharFieldFilter()
asset_tag = NullableCharFieldFilter()
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@@ -443,7 +439,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Device
fields = ['serial']
fields = ['name', 'serial', 'asset_tag']
def search(self, queryset, name, value):
if not value.strip():
@@ -461,8 +457,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
if not value:
return queryset
try:
mac = EUI(value.strip())
return queryset.filter(interfaces__mac_address=mac).distinct()
return queryset.filter(interfaces__mac_address=value).distinct()
except AddrFormatError:
return queryset.none()
@@ -574,8 +569,7 @@ class InterfaceFilter(django_filters.FilterSet):
if not value:
return queryset
try:
mac = EUI(value.strip())
return queryset.filter(mac_address=mac)
return queryset.filter(mac_address=value)
except AddrFormatError:
return queryset.none()
@@ -602,11 +596,10 @@ class InventoryItemFilter(DeviceComponentFilterSet):
to_field_name='slug',
label='Manufacturer (slug)',
)
asset_tag = NullableCharFieldFilter()
class Meta:
model = InventoryItem
fields = ['name', 'part_id', 'serial', 'discovered']
fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
class ConsoleConnectionFilter(django_filters.FilterSet):

View File

@@ -402,9 +402,7 @@ 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 a console server'
)
is_console_server = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
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'
@@ -558,7 +556,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = Platform
fields = ['name', 'slug', 'napalm_driver', 'rpc_client']
fields = ['name', 'slug', 'rpc_client']
#
@@ -632,7 +630,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', {}).copy()
initial = kwargs.get('initial', {})
initial['manufacturer'] = instance.device_type.manufacturer
kwargs['initial'] = initial
@@ -1481,7 +1479,7 @@ class InterfaceCreateForm(DeviceComponentForm):
def __init__(self, *args, **kwargs):
# Set interfaces enabled by default
kwargs['initial'] = kwargs.get('initial', {}).copy()
kwargs['initial'] = kwargs.get('initial', {})
kwargs['initial'].update({'enabled': True})
super(InterfaceCreateForm, self).__init__(*args, **kwargs)
@@ -1696,7 +1694,8 @@ class InterfaceConnectionCSVForm(forms.ModelForm):
return interface
class InterfaceConnectionDeletionForm(ConfirmationForm):
class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form):
confirm = forms.BooleanField(required=True)
# Used for HTTP redirect upon successful deletion
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)

View File

@@ -1,40 +0,0 @@
# -*- 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),
]

View File

@@ -357,16 +357,6 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
return list(reversed(available_units))
def get_reserved_units(self):
"""
Return a dictionary mapping all reserved units within the rack to their reservation.
"""
reserved_units = {}
for r in self.reservations.all():
for u in r.units:
reserved_units[u] = r
return reserved_units
def get_0u_devices(self):
return self.devices.filter(position=0)
@@ -748,10 +738,7 @@ class Platform(models.Model):
"""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
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')
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True, verbose_name='RPC client')
class Meta:
ordering = ['name']
@@ -822,10 +809,6 @@ 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__()

View File

@@ -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, ToggleColumn
from utilities.tables import BaseTable, SearchTable, ToggleColumn
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, Region, Site,
)
@@ -142,26 +142,30 @@ 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(SiteTable.Meta):
class Meta(BaseTable.Meta):
model = Site
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
#
@@ -210,22 +214,29 @@ 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')
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):
class Meta(BaseTable.Meta):
model = Rack
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')
@@ -291,7 +302,23 @@ 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',
'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',
)
@@ -306,6 +333,7 @@ class ConsolePortTemplateTable(BaseTable):
model = ConsolePortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
class ConsoleServerPortTemplateTable(BaseTable):
@@ -315,6 +343,7 @@ class ConsoleServerPortTemplateTable(BaseTable):
model = ConsoleServerPortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
class PowerPortTemplateTable(BaseTable):
@@ -324,6 +353,7 @@ class PowerPortTemplateTable(BaseTable):
model = PowerPortTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
class PowerOutletTemplateTable(BaseTable):
@@ -333,6 +363,7 @@ class PowerOutletTemplateTable(BaseTable):
model = PowerOutletTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
class InterfaceTemplateTable(BaseTable):
@@ -343,6 +374,7 @@ class InterfaceTemplateTable(BaseTable):
model = InterfaceTemplate
fields = ('pk', 'name', 'mgmt_only', 'form_factor')
empty_text = "None"
show_header = False
class DeviceBayTemplateTable(BaseTable):
@@ -352,6 +384,7 @@ class DeviceBayTemplateTable(BaseTable):
model = DeviceBayTemplate
fields = ('pk', 'name')
empty_text = "None"
show_header = False
#
@@ -406,22 +439,32 @@ 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(DeviceTable.Meta):
class Meta(BaseTable.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')
@@ -438,52 +481,6 @@ 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
#

View File

@@ -122,9 +122,7 @@ 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}),

View File

@@ -205,8 +205,6 @@ 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'
@@ -218,7 +216,7 @@ class SiteListView(ObjectListView):
queryset = Site.objects.select_related('region', 'tenant')
filter = filters.SiteFilter
filter_form = forms.SiteFilterForm
table = tables.SiteDetailTable
table = tables.SiteTable
template_name = 'dcim/site_list.html'
@@ -275,10 +273,9 @@ 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'
@@ -310,9 +307,7 @@ 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'
@@ -342,8 +337,6 @@ 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'
@@ -361,7 +354,7 @@ class RackListView(ObjectListView):
)
filter = filters.RackFilter
filter_form = forms.RackFilterForm
table = tables.RackDetailTable
table = tables.RackTable
template_name = 'dcim/rack_list.html'
@@ -417,10 +410,15 @@ class RackView(View):
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
reservations = RackReservation.objects.filter(rack=rack)
reserved_units = {}
for r in reservations:
for u in r.units:
reserved_units[u] = r
return render(request, 'dcim/rack.html', {
'rack': rack,
'reservations': reservations,
'reserved_units': reserved_units,
'nonracked_devices': nonracked_devices,
'next_rack': next_rack,
'prev_rack': prev_rack,
@@ -457,19 +455,16 @@ 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'
@@ -515,7 +510,6 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackreservation'
cls = RackReservation
table = tables.RackReservationTable
default_return_url = 'dcim:rackreservation_list'
@@ -545,8 +539,6 @@ 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'
@@ -570,30 +562,24 @@ class DeviceTypeView(View):
# Component tables
consoleport_table = tables.ConsolePortTemplateTable(
natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
show_header=False
natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
consoleserverport_table = tables.ConsoleServerPortTemplateTable(
natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
show_header=False
natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
powerport_table = tables.PowerPortTemplateTable(
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
show_header=False
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
poweroutlet_table = tables.PowerOutletTemplateTable(
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
show_header=False
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
interface_table = tables.InterfaceTemplateTable(
list(InterfaceTemplate.objects.order_naturally(
devicetype.interface_ordering
).filter(device_type=devicetype)),
show_header=False
).filter(device_type=devicetype))
)
devicebay_table = tables.DeviceBayTemplateTable(
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
show_header=False
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
if request.user.has_perm('dcim.change_devicetype'):
consoleport_table.base_columns['pk'].visible = True
@@ -635,19 +621,16 @@ 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'
@@ -670,7 +653,6 @@ class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
parent_field = 'device_type'
cls = ConsolePortTemplate
parent_cls = DeviceType
table = tables.ConsolePortTemplateTable
class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
@@ -686,7 +668,6 @@ class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDelet
permission_required = 'dcim.delete_consoleserverporttemplate'
cls = ConsoleServerPortTemplate
parent_cls = DeviceType
table = tables.ConsoleServerPortTemplateTable
class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
@@ -702,7 +683,6 @@ class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerporttemplate'
cls = PowerPortTemplate
parent_cls = DeviceType
table = tables.PowerPortTemplateTable
class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
@@ -718,7 +698,6 @@ class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
permission_required = 'dcim.delete_poweroutlettemplate'
cls = PowerOutletTemplate
parent_cls = DeviceType
table = tables.PowerOutletTemplateTable
class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
@@ -734,15 +713,14 @@ 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):
@@ -758,7 +736,6 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebaytemplate'
cls = DeviceBayTemplate
parent_cls = DeviceType
table = tables.DeviceBayTemplateTable
#
@@ -787,8 +764,6 @@ 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'
@@ -818,8 +793,6 @@ 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'
@@ -832,7 +805,7 @@ class DeviceListView(ObjectListView):
'primary_ip4', 'primary_ip6')
filter = filters.DeviceFilter
filter_form = forms.DeviceFilterForm
table = tables.DeviceDetailTable
table = tables.DeviceTable
template_name = 'dcim/device_list.html'
@@ -916,20 +889,7 @@ class DeviceInventoryView(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'
class DeviceLLDPNeighborsView(View):
def get(self, request, pk):
@@ -948,18 +908,6 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, 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
@@ -1008,19 +956,16 @@ 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'
@@ -1128,7 +1073,6 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport'
cls = ConsolePort
parent_cls = Device
table = tables.ConsolePortTable
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
@@ -1254,7 +1198,6 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverport'
cls = ConsoleServerPort
parent_cls = Device
table = tables.ConsoleServerPortTable
#
@@ -1361,7 +1304,6 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport'
cls = PowerPort
parent_cls = Device
table = tables.PowerPortTable
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
@@ -1489,7 +1431,6 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlet'
cls = PowerOutlet
parent_cls = Device
table = tables.PowerOutletTable
#
@@ -1532,15 +1473,14 @@ 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
#
@@ -1621,7 +1561,6 @@ class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebay'
cls = DeviceBay
parent_cls = Device
table = tables.DeviceBayTable
#

View File

@@ -37,29 +37,19 @@ class Command(BaseCommand):
def get_namespace(self):
namespace = {}
# Gather Django models and constants from each app
# Gather Django models 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) and model._meta.app_label == app:
if issubclass(model, Model):
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,

View File

@@ -13,8 +13,8 @@ from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
class Command(BaseCommand):
help = "Update inventory information for specified devices"
username = settings.NAPALM_USERNAME
password = settings.NAPALM_PASSWORD
username = settings.NETBOX_USERNAME
password = settings.NETBOX_PASSWORD
def add_arguments(self, parser):
parser.add_argument('-u', '--username', dest='username', help="Specify the username to use")

View File

@@ -115,11 +115,7 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
limit = min(limit, settings.MAX_PAGE_SIZE)
# Calculate available IPs within the prefix
ip_list = []
for index, ip in enumerate(prefix.get_available_ips(), start=1):
ip_list.append(ip)
if index == limit:
break
ip_list = list(prefix.get_available_ips())[:limit]
serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={
'request': request,
'prefix': prefix.prefix,

View File

@@ -217,7 +217,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
# Initialize helper selectors
instance = kwargs.get('instance')
initial = kwargs.get('initial', {}).copy()
initial = kwargs.get('initial', {})
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=PREFIX_STATUS_CHOICES,
choices=IPADDRESS_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', {}).copy()
initial = kwargs.get('initial', {})
if instance and instance.interface is not None:
initial['interface_site'] = instance.interface.device.site
initial['interface_rack'] = instance.interface.device.rack

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-08-03 19:37
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ipam', '0017_ipaddress_roles'),
]
operations = [
migrations.AlterUniqueTogether(
name='service',
unique_together=set([]),
),
]

View File

@@ -600,6 +600,7 @@ class Service(CreatedUpdatedModel):
class Meta:
ordering = ['device', 'protocol', 'port']
unique_together = ['device', 'protocol', 'port']
def __str__(self):
return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())

View File

@@ -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, ToggleColumn
from utilities.tables import BaseTable, SearchTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
@@ -152,6 +152,16 @@ 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
#
@@ -161,14 +171,6 @@ 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',
@@ -180,12 +182,12 @@ class RIRDetailTable(RIRTable):
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(RIRTable.Meta):
fields = (
'pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
'stats_deprecated', 'stats_available', 'utilization', 'actions',
)
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')
#
@@ -195,21 +197,24 @@ class RIRDetailTable(RIRTable):
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
#
@@ -236,6 +241,7 @@ 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')
@@ -243,17 +249,37 @@ class PrefixTable(BaseTable):
class Meta(BaseTable.Meta):
model = Prefix
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
row_attrs = {
'class': lambda record: 'success' if not record.pk else '',
}
class PrefixDetailTable(PrefixTable):
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
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 Meta(PrefixTable.Meta):
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
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')
#
@@ -266,26 +292,43 @@ 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', 'device', 'interface', 'description')
fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'description')
row_attrs = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
}
class IPAddressDetailTable(IPAddressTable):
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)
nat_inside = tables.LinkColumn(
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
)
class Meta(IPAddressTable.Meta):
fields = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'interface', 'description',
)
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')
#
@@ -315,17 +358,24 @@ 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')

View File

@@ -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.PrefixTable(
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False
prefix_table = tables.PrefixBriefTable(
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role'))
)
prefix_table.exclude = ('vrf',)
@@ -142,19 +142,16 @@ 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'
@@ -166,7 +163,7 @@ class RIRListView(ObjectListView):
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
filter = filters.RIRFilter
filter_form = forms.RIRFilterForm
table = tables.RIRDetailTable
table = tables.RIRTable
template_name = 'ipam/rir_list.html'
def alter_queryset(self, request):
@@ -262,9 +259,7 @@ 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'
@@ -278,7 +273,7 @@ class AggregateListView(ObjectListView):
})
filter = filters.AggregateFilter
filter_form = forms.AggregateFilterForm
table = tables.AggregateDetailTable
table = tables.AggregateTable
template_name = 'ipam/aggregate_list.html'
def extra_context(self):
@@ -365,19 +360,16 @@ 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'
@@ -407,7 +399,6 @@ class RoleEditView(RoleCreateView):
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_role'
cls = Role
table = tables.RoleTable
default_return_url = 'ipam:role_list'
@@ -419,7 +410,7 @@ class PrefixListView(ObjectListView):
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filter = filters.PrefixFilter
filter_form = forms.PrefixFilterForm
table = tables.PrefixDetailTable
table = tables.PrefixTable
template_name = 'ipam/prefix_list.html'
def alter_queryset(self, request):
@@ -454,7 +445,7 @@ class PrefixView(View):
).select_related(
'site', 'role'
).annotate_depth()
parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
parent_prefix_table.exclude = ('vrf',)
# Duplicate prefixes table
@@ -465,7 +456,7 @@ class PrefixView(View):
).select_related(
'site', 'role'
)
duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
duplicate_prefix_table.exclude = ('vrf',)
# Child prefixes table
@@ -573,19 +564,16 @@ 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'
@@ -597,7 +585,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.IPAddressDetailTable
table = tables.IPAddressTable
template_name = 'ipam/ipaddress_list.html'
@@ -613,7 +601,7 @@ class IPAddressView(View):
).select_related(
'site', 'role'
)
parent_prefixes_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
parent_prefixes_table.exclude = ('vrf',)
# Duplicate IPs table
@@ -624,7 +612,7 @@ class IPAddressView(View):
).select_related(
'interface__device', 'nat_inside'
)
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
# Related IP table
related_ips = IPAddress.objects.select_related(
@@ -634,7 +622,7 @@ class IPAddressView(View):
).filter(
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
)
related_ips_table = tables.IPAddressTable(list(related_ips), orderable=False)
related_ips_table = tables.IPAddressBriefTable(list(related_ips))
return render(request, 'ipam/ipaddress.html', {
'ipaddress': ipaddress,
@@ -681,19 +669,16 @@ 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'
@@ -725,9 +710,7 @@ 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'
@@ -739,7 +722,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.VLANDetailTable
table = tables.VLANTable
template_name = 'ipam/vlan_list.html'
@@ -751,7 +734,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.PrefixTable(list(prefixes), orderable=False)
prefix_table = tables.PrefixBriefTable(list(prefixes))
prefix_table.exclude = ('vlan',)
return render(request, 'ipam/vlan.html', {
@@ -788,19 +771,16 @@ 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'

View File

@@ -60,8 +60,8 @@ BASE_PATH = os.environ.get('BASE_PATH', '')
MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False)
# Credentials that NetBox will use to access live devices.
NAPALM_USERNAME = os.environ.get('NAPALM_USERNAME', '')
NAPALM_PASSWORD = os.environ.get('NAPALM_PASSWORD', '')
NETBOX_USERNAME = os.environ.get('NETBOX_USERNAME', '')
NETBOX_PASSWORD = os.environ.get('NETBOX_PASSWORD', '')
# Determine how many objects to display per page within a list. (Default: 50)
PAGINATE_COUNT = os.environ.get('PAGINATE_COUNT', 50)

View File

@@ -93,16 +93,9 @@ MAINTENANCE_MODE = False
# all objects by specifying "?limit=0".
MAX_PAGE_SIZE = 1000
# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
NAPALM_USERNAME = ''
NAPALM_PASSWORD = ''
# NAPALM timeout (in seconds). (Default: 30)
NAPALM_TIMEOUT = 30
# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
# be provided as a dictionary.
NAPALM_ARGS = {}
# Credentials that NetBox will use to access live devices (future use).
NETBOX_USERNAME = ''
NETBOX_PASSWORD = ''
# Determine how many objects to display per page within a list. (Default: 50)
PAGINATE_COUNT = 50

View File

@@ -13,7 +13,7 @@ except ImportError:
)
VERSION = '2.1.2'
VERSION = '2.1-beta'
# Import required configuration parameters
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
@@ -46,12 +46,8 @@ MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30)
NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '') # Deprecated
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '') # Deprecated
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
@@ -60,19 +56,6 @@ TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# Check for deprecated configuration parameters
config_logger = logging.getLogger('configuration')
config_logger.addHandler(logging.StreamHandler())
config_logger.setLevel(logging.WARNING)
if NETBOX_USERNAME:
config_logger.warning('NETBOX_USERNAME is deprecated and will be removed in v2.2. Please use NAPALM_USERNAME instead.')
if not NAPALM_USERNAME:
NAPALM_USERNAME = NETBOX_USERNAME
if NETBOX_PASSWORD:
config_logger.warning('NETBOX_PASSWORD is deprecated and will be removed in v2.2. Please use NAPALM_PASSWORD instead.')
if not NAPALM_PASSWORD:
NAPALM_PASSWORD = NETBOX_PASSWORD
# Attempt to import LDAP configuration if it has been defined
LDAP_IGNORE_CERT_ERRORS = False
try:
@@ -95,9 +78,9 @@ if LDAP_CONFIGURED:
if LDAP_IGNORE_CERT_ERRORS:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
# Enable logging for django_auth_ldap
ldap_logger = logging.getLogger('django_auth_ldap')
ldap_logger.addHandler(logging.StreamHandler())
ldap_logger.setLevel(logging.DEBUG)
logger = logging.getLogger('django_auth_ldap')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)
except ImportError:
raise ImproperlyConfigured(
"LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove "

View File

@@ -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 CircuitTable, ProviderTable
from circuits.tables import CircuitSearchTable, ProviderSearchTable
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
from dcim.tables import DeviceTable, DeviceTypeTable, RackTable, SiteTable
from dcim.tables import DeviceSearchTable, DeviceTypeSearchTable, RackSearchTable, SiteSearchTable
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 AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
from ipam.tables import AggregateSearchTable, IPAddressSearchTable, PrefixSearchTable, VLANSearchTable, VRFSearchTable
from secrets.filters import SecretFilter
from secrets.models import Secret
from secrets.tables import SecretTable
from secrets.tables import SecretSearchTable
from tenancy.filters import TenantFilter
from tenancy.models import Tenant
from tenancy.tables import TenantTable
from tenancy.tables import TenantSearchTable
from .forms import SearchForm
@@ -34,85 +34,83 @@ SEARCH_TYPES = OrderedDict((
('provider', {
'queryset': Provider.objects.all(),
'filter': ProviderFilter,
'table': ProviderTable,
'table': ProviderSearchTable,
'url': 'circuits:provider_list',
}),
('circuit', {
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
'filter': CircuitFilter,
'table': CircuitTable,
'table': CircuitSearchTable,
'url': 'circuits:circuit_list',
}),
# DCIM
('site', {
'queryset': Site.objects.select_related('region', 'tenant'),
'filter': SiteFilter,
'table': SiteTable,
'table': SiteSearchTable,
'url': 'dcim:site_list',
}),
('rack', {
'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'),
'filter': RackFilter,
'table': RackTable,
'table': RackSearchTable,
'url': 'dcim:rack_list',
}),
('devicetype', {
'queryset': DeviceType.objects.select_related('manufacturer'),
'filter': DeviceTypeFilter,
'table': DeviceTypeTable,
'table': DeviceTypeSearchTable,
'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': DeviceTable,
'table': DeviceSearchTable,
'url': 'dcim:device_list',
}),
# IPAM
('vrf', {
'queryset': VRF.objects.select_related('tenant'),
'filter': VRFFilter,
'table': VRFTable,
'table': VRFSearchTable,
'url': 'ipam:vrf_list',
}),
('aggregate', {
'queryset': Aggregate.objects.select_related('rir'),
'filter': AggregateFilter,
'table': AggregateTable,
'table': AggregateSearchTable,
'url': 'ipam:aggregate_list',
}),
('prefix', {
'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
'filter': PrefixFilter,
'table': PrefixTable,
'table': PrefixSearchTable,
'url': 'ipam:prefix_list',
}),
('ipaddress', {
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
'filter': IPAddressFilter,
'table': IPAddressTable,
'table': IPAddressSearchTable,
'url': 'ipam:ipaddress_list',
}),
('vlan', {
'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'),
'filter': VLANFilter,
'table': VLANTable,
'table': VLANSearchTable,
'url': 'ipam:vlan_list',
}),
# Secrets
('secret', {
'queryset': Secret.objects.select_related('role', 'device'),
'filter': SecretFilter,
'table': SecretTable,
'table': SecretSearchTable,
'url': 'secrets:secret_list',
}),
# Tenancy
('tenant', {
'queryset': Tenant.objects.select_related('group'),
'filter': TenantFilter,
'table': TenantTable,
'table': TenantSearchTable,
'url': 'tenancy:tenant_list',
}),
))
@@ -191,7 +189,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, orderable=False)
table = table(filtered_queryset)
table.paginate(per_page=SEARCH_MAX_RESULTS)
if table.page:

View File

@@ -81,11 +81,6 @@ footer p {
}
}
/* Navigation menu */
li.subnav > a {
padding-left: 30px;
}
/* Forms */
label {
font-weight: normal;
@@ -338,31 +333,6 @@ 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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because one or more lines are too long

View File

@@ -42,15 +42,13 @@ class UserKeyAdmin(admin.ModelAdmin):
if 'activate' in request.POST:
form = ActivateUserKeyForm(request.POST)
if form.is_valid():
master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
if master_key is not None:
try:
master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
for uk in form.cleaned_data['_selected_action']:
uk.activate(master_key)
return redirect('admin:secrets_userkey_changelist')
else:
messages.error(
request, "Invalid private key provided. Unable to retrieve master key.", extra_tags='error'
)
except ValueError:
messages.error(request, "Invalid private key provided. Unable to retrieve master key.")
else:
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})

View File

@@ -40,7 +40,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = SecretRole
fields = ['name', 'slug', 'users', 'groups']
fields = ['name', 'slug']
#

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import django_tables2 as tables
from utilities.tables import BaseTable, ToggleColumn
from utilities.tables import BaseTable, SearchTable, ToggleColumn
from .models import SecretRole, Secret
@@ -43,3 +43,11 @@ 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')

View File

@@ -4,6 +4,7 @@ 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
@@ -55,8 +56,6 @@ 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'
@@ -241,17 +240,14 @@ 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'

View File

@@ -3,7 +3,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}Home{% endblock %} - NetBox</title>
<title>NetBox - {% block title %}Home{% endblock %}</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' %}">
@@ -31,197 +31,197 @@
<li class="dropdown{% if request.path|contains:'/dcim/sites/,/dcim/regions/,/tenancy/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:site_list' %}"><strong>Sites</strong></a></li>
<li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li>
{% if perms.dcim.add_site %}
<li class="subnav"><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus"></i> Add a Site</a></li>
<li class="subnav"><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download"></i> Import Sites</a></li>
<li><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Site</a></li>
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:region_list' %}"><strong>Regions</strong></a></li>
<li><a href="{% url 'dcim:region_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Regions</a></li>
{% if perms.dcim.add_region %}
<li class="subnav"><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus"></i> Add a Region</a></li>
<li><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Region</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'tenancy:tenant_list' %}"><strong>Tenants</strong></a></li>
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li>
{% if perms.tenancy.add_tenant %}
<li class="subnav"><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus"></i> Add a Tenant</a></li>
<li class="subnav"><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download"></i> Import Tenants</a></li>
<li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>
<li><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Tenants</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'tenancy:tenantgroup_list' %}"><strong>Tenant Groups</strong></a></li>
<li><a href="{% url 'tenancy:tenantgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenant Groups</a></li>
{% if perms.tenancy.add_tenantgroup %}
<li class="subnav"><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus"></i> Add a Tenant Group</a></li>
<li><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant Group</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/dcim/rack' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:rack_list' %}"><strong>Racks</strong></a></li>
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
<li><a href="{% url 'dcim:rack_elevation_list' %}"><i class="fa fa-bars" aria-hidden="true"></i> Rack Elevations</a></li>
{% if perms.dcim.add_rack %}
<li class="subnav"><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus"></i> Add a Rack</a></li>
<li class="subnav"><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download"></i> Import Racks</a></li>
<li><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack</a></li>
<li><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Racks</a></li>
{% endif %}
<li><a href="{% url 'dcim:rack_elevation_list' %}"><strong>Rack Elevations</strong></a></li>
<li class="divider"></li>
<li><a href="{% url 'dcim:rackgroup_list' %}"><strong>Rack Groups</strong></a></li>
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Groups</a></li>
{% if perms.dcim.add_rackgroup %}
<li class="subnav"><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus"></i> Add a Rack Group</a></li>
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:rackrole_list' %}"><strong>Rack Roles</strong></a></li>
<li><a href="{% url 'dcim:rackrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Roles</a></li>
{% if perms.dcim.add_rackrole %}
<li class="subnav"><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus"></i> Add a Rack Role</a></li>
<li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:rackreservation_list' %}"><strong>Rack Reservations</strong></a></li>
<li><a href="{% url 'dcim:rackreservation_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Reservations</a></li>
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:device_list' %}"><strong>Devices</strong></a></li>
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
{% if perms.dcim.add_device %}
<li class="subnav"><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus"></i> Add a Device</a></li>
<li class="subnav"><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download"></i> Import Devices</a></li>
<li><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device</a></li>
<li><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Devices</a></li>
{% endif %}
{% if perms.ipam.add_device or perms.ipam.add_devicetype %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:devicetype_list' %}"><strong>Device Types</strong></a></li>
<li><a href="{% url 'dcim:devicetype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Types</a></li>
{% if perms.dcim.add_devicetype %}
<li class="subnav"><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus"></i> Add a Device Type</a></li>
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Type</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'dcim:devicerole_list' %}"><strong>Device Roles</strong></a></li>
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Roles</a></li>
{% if perms.dcim.add_devicerole %}
<li class="subnav"><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus"></i> Add a Device Role</a></li>
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Role</a></li>
{% endif %}
{% if perms.dcim.add_devicerole or perms.dcim.add_manufacturer %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:manufacturer_list' %}"><strong>Manufacturers</strong></a></li>
<li><a href="{% url 'dcim:manufacturer_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Manufacturers</a></li>
{% if perms.dcim.add_manufacturer %}
<li class="subnav"><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus"></i> Add a Manufacturer</a></li>
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
{% endif %}
{% if perms.dcim.add_manufacturer or perms.dcim.add_platform %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:platform_list' %}"><strong>Platforms</strong></a></li>
<li><a href="{% url 'dcim:platform_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Platforms</a></li>
{% if perms.dcim.add_platform %}
<li class="subnav"><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus"></i> Add a Platform</a></li>
<li><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Platform</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/dcim/console-connections/,/dcim/power-connections/,/dcim/interface-connections/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'dcim:console_connections_list' %}"><strong>Console Connections</strong></a></li>
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li>
{% if perms.dcim.change_consoleport %}
<li class="subnav"><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download"></i> Import Console Connections</a></li>
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Console Connections</a></li>
{% endif %}
{% if perms.ipam.change_consoleport or perms.ipam.change_powerport %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:power_connections_list' %}"><strong>Power Connections</strong></a></li>
<li><a href="{% url 'dcim:power_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Power Connections</a></li>
{% if perms.dcim.change_powerport %}
<li class="subnav"><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download"></i> Import Power Connections</a></li>
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Power Connections</a></li>
{% endif %}
{% if perms.ipam.change_powerport or perms.ipam.add_interfaceconnection %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'dcim:interface_connections_list' %}"><strong>Interface Connections</strong></a></li>
<li><a href="{% url 'dcim:interface_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Interface Connections</a></li>
{% if perms.dcim.add_interfaceconnection %}
<li class="subnav"><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download"></i> Import Interface Connections</a></li>
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Interface Connections</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/ipam/' and not request.path|contains:'/ipam/vlan' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'ipam:ipaddress_list' %}"><strong>IP Addresses</strong></a></li>
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li>
{% if perms.ipam.add_ipaddress %}
<li class="subnav"><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus"></i> Add an IP</a></li>
<li class="subnav"><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download"></i> Import IPs</a></li>
<li><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an IP</a></li>
<li><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import IPs</a></li>
{% endif %}
{% if perms.ipam.add_ipaddress or perms.ipam.add_prefix %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:prefix_list' %}"><strong>Prefixes</strong></a></li>
<li><a href="{% url 'ipam:prefix_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefixes</a></li>
{% if perms.ipam.add_prefix %}
<li class="subnav"><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus"></i> Add a Prefix</a></li>
<li class="subnav"><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download"></i> Import Prefixes</a></li>
<li><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Prefix</a></li>
<li><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Prefixes</a></li>
{% endif %}
{% if perms.ipam.add_prefix or perms.ipam.add_aggregate %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:aggregate_list' %}"><strong>Aggregates</strong></a></li>
<li><a href="{% url 'ipam:aggregate_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Aggregates</a></li>
{% if perms.ipam.add_aggregate %}
<li class="subnav"><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus"></i> Add an Aggregate</a></li>
<li class="subnav"><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download"></i> Import Aggregates</a></li>
<li><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
<li><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Aggregates</a></li>
{% endif %}
{% if perms.ipam.add_aggregate or perms.ipam.add_vrf %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:vrf_list' %}"><strong>VRFs</strong></a></li>
<li><a href="{% url 'ipam:vrf_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VRFs</a></li>
{% if perms.ipam.add_vrf %}
<li class="subnav"><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus"></i> Add a VRF</a></li>
<li class="subnav"><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download"></i> Import VRFs</a></li>
<li><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VRF</a></li>
<li><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VRFs</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:rir_list' %}"><strong>RIRs</strong></a></li>
<li><a href="{% url 'ipam:rir_list' %}"><i class="fa fa-search" aria-hidden="true"></i> RIRs</a></li>
{% if perms.ipam.add_rir %}
<li class="subnav"><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus"></i> Add a RIR</a></li>
<li><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a RIR</a></li>
{% endif %}
{% if perms.ipam.add_rir or perms.ipam.add_role %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'ipam:role_list' %}"><strong>Prefix Roles</strong></a></li>
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix Roles</a></li>
{% if perms.ipam.add_role %}
<li class="subnav"><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus"></i> Add a Role</a></li>
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/ipam/vlan' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'ipam:vlan_list' %}"><strong>VLANs</strong></a></li>
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
{% if perms.ipam.add_vlan %}
<li class="subnav"><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus"></i> Add a VLAN</a></li>
<li class="subnav"><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download"></i> Import VLANs</a></li>
<li><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN</a></li>
<li><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VLANs</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:vlangroup_list' %}"><strong>VLAN Groups</strong></a></li>
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Groups</a></li>
{% if perms.ipam.add_vlangroup %}
<li class="subnav"><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus"></i> Add a VLAN Group</a></li>
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:role_list' %}"><strong>VLAN Roles</strong></a></li>
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Roles</a></li>
{% if perms.ipam.add_role %}
<li class="subnav"><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus"></i> Add a Role</a></li>
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
{% endif %}
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/circuits/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'circuits:provider_list' %}"><strong>Providers</strong></a></li>
<li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li>
{% if perms.circuits.add_provider %}
<li class="subnav"><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus"></i> Add a Provider</a></li>
<li class="subnav"><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download"></i> Import Providers</a></li>
<li><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Provider</a></li>
<li><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Providers</a></li>
{% endif %}
{% if perms.circuits.add_circuit or perms.circuits.add_provider %}
<li class="divider"></li>
{% endif %}
<li><a href="{% url 'circuits:circuit_list' %}"><strong>Circuits</strong></a></li>
<li><a href="{% url 'circuits:circuit_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuits</a></li>
{% if perms.circuits.add_circuit %}
<li class="subnav"><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus"></i> Add a Circuit</a></li>
<li class="subnav"><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download"></i> Import Circuits</a></li>
<li><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit</a></li>
<li><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Circuits</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'circuits:circuittype_list' %}"><strong>Circuit Types</strong></a></li>
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuit Types</a></li>
{% if perms.circuits.add_circuittype %}
<li class="subnav"><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus"></i> Add a Circuit Type</a></li>
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
{% endif %}
</ul>
</li>
@@ -229,14 +229,14 @@
<li class="dropdown{% if request.path|contains:'/secrets/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{% url 'secrets:secret_list' %}"><strong>Secrets</strong></a></li>
<li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li>
{% if perms.secrets.add_secret %}
<li class="subnav"><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download"></i> Import Secrets</a></li>
<li><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Secrets</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'secrets:secretrole_list' %}"><strong>Secret Roles</strong></a></li>
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secret Roles</a></li>
{% if perms.secrets.add_secretrole %}
<li class="subnav"><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus"></i> Add a Secret Role</a></li>
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
{% endif %}
</ul>
</li>
@@ -250,16 +250,16 @@
{{ request.user|truncatechars:"30" }} <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user"></i> Profile</a></li>
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li>
{% if request.user.is_staff %}
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs"></i> Admin</a></li>
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs" aria-hidden="true"></i> Admin</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out"></i> Log out</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> Log out</a></li>
</ul>
</li>
{% else %}
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in"></i> Log in</a></li>
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
{% endif %}
</ul>
<form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" id="navbar_search" role="search">
@@ -267,7 +267,7 @@
<input type="text" name="q" class="form-control" placeholder="Search">
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">
<i class="fa fa-search"></i>
<i class="fa fa-search" aria-hidden="true"></i>
</button>
</span>
</div>
@@ -290,7 +290,7 @@
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissable" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span>&times;</span>
<span aria-hidden="true">&times;</span>
</button>
{{ message }}
</div>
@@ -323,19 +323,13 @@
</div>
</div>
</footer>
<script src="{% static 'js/jquery-3.2.1.min.js' %}"></script>
<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 '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>

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -37,7 +39,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}</h1>
<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
{% include 'inc/created_updated.html' with obj=circuit %}
<div class="row">
<div class="col-md-6">

View File

@@ -0,0 +1,23 @@
{% 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 %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Circuits{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.circuits.add_circuit %}
@@ -15,7 +17,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='circuits' %}
</div>
<h1>{% block title %}Circuits{% endblock %}</h1>
<h1>Circuits</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}

View File

@@ -2,6 +2,10 @@
{% load staticfiles %}
{% load form_helpers %}
{% block title %}
Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}
{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -10,7 +14,7 @@
{% endfor %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h3>{% block title %}Circuit {{ obj.circuit }} - {{ form.term_side.value }} Side{% endblock %}</h3>
<h3>Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}</h3>
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Circuit Types{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.circuits.add_circuittype %}
@@ -10,7 +12,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}Circuit Types{% endblock %}</h1>
<h1>Circuit Types</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='circuits:circuittype_bulk_delete' %}

View File

@@ -2,6 +2,8 @@
{% load static from staticfiles %}
{% load helpers %}
{% block title %}{{ provider }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -43,7 +45,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}{{ provider }}{% endblock %}</h1>
<h1>{{ provider }}</h1>
{% include 'inc/created_updated.html' with obj=provider %}
<div class="row">
<div class="col-md-4">

View File

@@ -0,0 +1,19 @@
{% 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 %}

View File

@@ -1,5 +1,7 @@
{% extends '_base.html' %}
{% block title %}Providers{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.circuits.add_provider %}
@@ -14,7 +16,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='providers' %}
</div>
<h1>{% block title %}Providers{% endblock %}</h1>
<h1>Providers</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}

View File

@@ -1,5 +1,7 @@
{% extends '_base.html' %}
{% block title %}Console Connections{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.change_consoleport %}
@@ -10,7 +12,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='connections' %}
</div>
<h1>{% block title %}Console Connections{% endblock %}</h1>
<h1>Console Connections</h1>
<div class="row">
<div class="col-md-9">
{% include 'responsive_table.html' %}

View File

@@ -2,6 +2,8 @@
{% load static from staticfiles %}
{% load form_helpers %}
{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -19,7 +21,7 @@
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %}</div>
<div class="panel-heading">Connect {{ consoleport.device }} {{ consoleport }}</div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

View File

@@ -2,6 +2,8 @@
{% load static from staticfiles %}
{% load form_helpers %}
{% block title %}Connect {{ consoleserverport.device }} {{ consoleserverport }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -19,7 +21,7 @@
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">{% block title %}Connect {{ consoleserverport.device }} {{ consoleserverport }}{% endblock %}</div>
<div class="panel-heading">Connect {{ consoleserverport.device }} {{ consoleserverport }}</div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

View File

@@ -0,0 +1,23 @@
{% 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 %}

View File

@@ -1,53 +0,0 @@
{% 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 %}

View File

@@ -45,7 +45,7 @@
</p>
</div>
</div>
{% else %}
{% elif not obj.device_type.is_child_device %}
{% render_field form.face %}
{% render_field form.position %}
{% endif %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Devices{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_device %}
@@ -15,7 +17,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='devices' %}
</div>
<h1>{% block title %}Devices{% endblock %}</h1>
<h1>Devices</h1>
<div class="row">
<div class="col-md-9">
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}

View File

@@ -3,66 +3,64 @@
{% block title %}{{ device }} - LLDP Neighbors{% endblock %}
{% block content %}
{% 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>
</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>
{% 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>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
$.ajax({
url: "{% url 'dcim-api:device-napalm' pk=device.pk %}?method=get_lldp_neighbors",
url: "{% url 'dcim-api:device-lldp-neighbors' pk=device.pk %}",
dataType: 'json',
success: function(json) {
$.each(json['get_lldp_neighbors'], function(iface, neighbors) {
var neighbor = neighbors[0];
var row = $('#' + iface.replace(/(\/)/g, "\\$1"));
$.each(json, function(i, neighbor) {
var row = $('#' + neighbor['local-interface'].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['hostname']);
row.children('td.interface').html(neighbor['port']);
row.children('td.device').html(neighbor['name']);
row.children('td.interface').html(neighbor['remote-interface']);
// Apply colors to rows
if (!configured_device && neighbor['hostname']) {
if (!configured_device && neighbor['name']) {
row.addClass('info');
} else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port']) {
} else if (configured_device == neighbor['name'] && configured_interface == neighbor['remote-interface']) {
row.addClass('success');
} else {
row.addClass('danger');

View File

@@ -1,125 +0,0 @@
{% 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 %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% block title %}Populate {{ device_bay }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -15,7 +17,7 @@
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">{% block title %}Populate {{ device_bay }}{% endblock %}</div>
<div class="panel-heading">Populate {{ device_bay }}</div>
<div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label required">Parent Device</label>

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Device Roles{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_devicerole %}
@@ -10,7 +12,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}Device Roles{% endblock %}</h1>
<h1>Device Roles</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicerole_bulk_delete' %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
@@ -29,7 +31,7 @@
</div>
{% endif %}
<h1>{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}</h1>
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
<div class="row">
<div class="col-md-5">
<div class="panel panel-default">

View File

@@ -0,0 +1,19 @@
{% 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 %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Device Types{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_devicetype %}
@@ -10,7 +12,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}Device Types{% endblock %}</h1>
<h1>Device Types</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}

View File

@@ -45,15 +45,7 @@
<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 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 %}
{% 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>
{% endif %}
</ul>

View File

@@ -118,7 +118,7 @@
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<td></td>
{% endif %}
<td colspan="3">
<td colspan="2">
<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>

View File

@@ -0,0 +1,17 @@
{% 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 %}

View File

@@ -1,5 +1,7 @@
{% extends '_base.html' %}
{% block title %}Interface Connections{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_interfaceconnection %}
@@ -10,7 +12,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='connections' %}
</div>
<h1>{% block title %}Interface Connections{% endblock %}</h1>
<h1>Interface Connections</h1>
<div class="row">
<div class="col-md-9">
{% include 'responsive_table.html' %}

View File

@@ -0,0 +1,25 @@
{% 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 %}

View File

@@ -0,0 +1,64 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% block title %}Assign a New IP Address{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.non_field_errors }}
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>IP Address</strong>
</div>
<div class="panel-body">
{% render_field form.address %}
{% render_field form.vrf %}
{% render_field form.tenant %}
{% render_field form.status %}
{% render_field form.description %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Interface Assignment</strong>
</div>
<div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label">Device</label>
<div class="col-md-9">
<p class="form-control-static">{{ device }}</p>
</div>
</div>
{% render_field form.interface %}
{% render_field form.set_as_primary %}
</div>
</div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<button type="submit" name="_create" class="btn btn-primary">Create</button>
<button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>
</div>
</form>
{% endblock %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Manufacturers{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_manufacturer %}
@@ -10,7 +12,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}Manufacturers{% endblock %}</h1>
<h1>Manufacturers</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:manufacturer_bulk_delete' %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Platforms{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_platform %}
@@ -10,7 +12,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}Platforms{% endblock %}</h1>
<h1>Platforms</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:platform_bulk_delete' %}

View File

@@ -1,5 +1,7 @@
{% extends '_base.html' %}
{% block title %}Power Connections{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.change_powerport %}
@@ -10,7 +12,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='connections' %}
</div>
<h1>{% block title %}Power Connections{% endblock %}</h1>
<h1>Power Connections</h1>
<div class="row">
<div class="col-md-9">
{% include 'responsive_table.html' %}

View File

@@ -2,6 +2,8 @@
{% load static from staticfiles %}
{% load form_helpers %}
{% block title %}Connect {{ poweroutlet.device }} {{ poweroutlet }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -19,7 +21,7 @@
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">{% block title %}Connect {{ poweroutlet.device }} {{ poweroutlet }}{% endblock %}</div>
<div class="panel-heading">Connect {{ poweroutlet.device }} {{ poweroutlet }}</div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

View File

@@ -2,6 +2,8 @@
{% load static from staticfiles %}
{% load form_helpers %}
{% block title %}Connect {{ powerport.device }} {{ powerport }}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -19,7 +21,7 @@
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">{% block title %}Connect {{ powerport.device }} {{ powerport }}{% endblock %}</div>
<div class="panel-heading">Connect {{ powerport.device }} {{ powerport }}</div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}{{ rack.site }} - Rack {{ rack.name }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -49,7 +51,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}Rack {{ rack.name }}{% endblock %}</h1>
<h1>Rack {{ rack.name }}</h1>
{% include 'inc/created_updated.html' with obj=rack %}
<div class="row">
<div class="col-md-6">
@@ -259,13 +261,13 @@
<div class="rack_header">
<h4>Front</h4>
</div>
{% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 %}
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="rack_header">
<h4>Rear</h4>
</div>
{% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 %}
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
{% 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 %}

View File

@@ -18,9 +18,9 @@
<p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
</div>
{% if face_id %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
{% else %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 %}
{% endif %}
<div class="clearfix"></div>
<div class="rack_header">

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Racks{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_rack %}
@@ -15,7 +17,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='racks' %}
</div>
<h1>{% block title %}Racks{% endblock %}</h1>
<h1>Racks</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Rack Groups{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_rackgroup %}
@@ -10,7 +12,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}Rack Groups{% endblock %}</h1>
<h1>Rack Groups</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Rack Role{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_rackrole %}
@@ -10,7 +12,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}Rack Roles{% endblock %}</h1>
<h1>Rack Roles</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackrole_bulk_delete' %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Regions{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_region %}
@@ -10,7 +12,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}Regions{% endblock %}</h1>
<h1>{{ block.title }}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}

View File

@@ -2,6 +2,8 @@
{% load static from staticfiles %}
{% load helpers %}
{% block title %}{{ site }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -48,7 +50,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}{{ site }}{% endblock %}</h1>
<h1>{{ site.name }}</h1>
{% include 'inc/created_updated.html' with obj=site %}
<div class="row">
<div class="col-md-7">

View File

@@ -0,0 +1,17 @@
{% 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 %}

View File

@@ -1,5 +1,7 @@
{% extends '_base.html' %}
{% block title %}Sites{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_site %}
@@ -14,7 +16,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='sites' %}
</div>
<h1>{% block title %}Sites{% endblock %}</h1>
<h1>Sites</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %}

View File

@@ -1,14 +1,6 @@
{% extends '_base.html' %}
{% block content %}
{% if settings.NETBOX_USERNAME or settings.NETBOX_PASSWORD %}
<div class="alert alert-warning alert-dismissable" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<strong>Warning:</strong> The <code>NETBOX_USERNAME</code> and <code>NETBOX_PASSWORD</code> configuration parameters have been deprecated. Please replace them in configuration.py with <code>NAPALM_USERNAME</code> and <code>NAPALM_PASSWORD</code>.
</div>
{% endif %}
{% include 'search_form.html' %}
<div class="row">
<div class="col-sm-6 col-md-4">

View File

@@ -1,4 +0,0 @@
{% load staticfiles %}
<div class="loading text-center">
<img src="{% static 'img/ajax-loader.gif' %}" />
</div>

View File

@@ -1,5 +1,7 @@
{% extends '_base.html' %}
{% block title %}Aggregate: {{ aggregate }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -36,7 +38,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}{{ aggregate }}{% endblock %}</h1>
<h1>{{ aggregate }}</h1>
{% include 'inc/created_updated.html' with obj=aggregate %}
<div class="row">
<div class="col-md-6">

View File

@@ -0,0 +1,21 @@
{% 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 %}

View File

@@ -2,6 +2,8 @@
{% load humanize %}
{% load helpers %}
{% block title %}Aggregates{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_aggregate %}
@@ -16,7 +18,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='aggregates' %}
</div>
<h1>{% block title %}Aggregates{% endblock %}</h1>
<h1>Aggregates</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:aggregate_bulk_edit' bulk_delete_url='ipam:aggregate_bulk_delete' %}

View File

@@ -1,5 +1,7 @@
{% extends '_base.html' %}
{% block title %}{{ ipaddress }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -38,10 +40,10 @@
</a>
{% endif %}
</div>
<h1>{% block title %}{{ ipaddress }}{% endblock %}</h1>
<h1>{{ ipaddress }}</h1>
{% include 'inc/created_updated.html' with obj=ipaddress %}
<div class="row">
<div class="col-md-4">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>IP Address</strong>
@@ -135,7 +137,7 @@
{% include 'inc/custom_fields_panel.html' %}
{% endwith %}
</div>
<div class="col-md-8">
<div class="col-md-6">
{% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
{% if duplicate_ips_table.rows %}
{% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %}

View File

@@ -2,6 +2,8 @@
{% load static from staticfiles %}
{% load form_helpers %}
{% block title %}Assign an IP Address{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
{% csrf_token %}
@@ -17,7 +19,7 @@
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>{% block title %}Assign an IP Address{% endblock %}</strong>
<strong>Assign an IP Address</strong>
</div>
<div class="panel-body">
<div class="form-group">

View File

@@ -0,0 +1,25 @@
{% 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 %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}IP Addresses{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_ipaddress %}
@@ -15,7 +17,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='IPs' %}
</div>
<h1>{% block title %}IP Addresses{% endblock %}</h1>
<h1>IP Addresses</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}

View File

@@ -0,0 +1,25 @@
{% 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 %}

View File

@@ -1,6 +1,6 @@
{% extends '_base.html' %}
{% block title %}{{ prefix }} - IP Addresses{% endblock %}
{% block title %}{{ prefix }}{% endblock %}
{% block content %}
{% include 'ipam/inc/prefix_header.html' with active_tab='ip-addresses' %}

View File

@@ -2,6 +2,8 @@
{% load helpers %}
{% load form_helpers %}
{% block title %}Prefixes{% endblock %}
{% block content %}
<div class="pull-right">
<div class="btn-group" role="group">
@@ -20,7 +22,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='prefixes' %}
</div>
<h1>{% block title %}Prefixes{% endblock %}</h1>
<h1>Prefixes</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}

View File

@@ -2,6 +2,8 @@
{% load humanize %}
{% load helpers %}
{% block title %}RIRs{% endblock %}
{% block content %}
<div class="pull-right">
{% if request.GET.family == '6' %}
@@ -22,7 +24,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}RIRs{% endblock %}</h1>
<h1>RIRs</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}Prefix/VLAN Roles{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.dcim.add_devicerole %}
@@ -10,7 +12,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}Prefix/VLAN Roles{% endblock %}</h1>
<h1>Prefix/VLAN Roles</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:role_bulk_delete' %}

View File

@@ -1,5 +1,7 @@
{% extends '_base.html' %}
{% block title %}VLAN {{ vlan.display_name }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -41,7 +43,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}VLAN {{ vlan.display_name }}{% endblock %}</h1>
<h1>VLAN {{ vlan.display_name }}</h1>
{% include 'inc/created_updated.html' with obj=vlan %}
<div class="row">
<div class="col-md-6">

View File

@@ -0,0 +1,25 @@
{% 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 %}

View File

@@ -2,6 +2,8 @@
{% load helpers %}
{% load form_helpers %}
{% block title %}VLANs{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_vlan %}
@@ -16,7 +18,7 @@
{% endif %}
{% include 'inc/export_button.html' with obj_type='VLANs' %}
</div>
<h1>{% block title %}VLANs{% endblock %}</h1>
<h1>VLANs</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}

View File

@@ -1,6 +1,8 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}VLAN Groups{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_vlangroup %}
@@ -10,7 +12,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}VLAN Groups{% endblock %}</h1>
<h1>VLAN Groups</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:vlangroup_bulk_delete' %}

View File

@@ -1,5 +1,7 @@
{% extends '_base.html' %}
{% block title %}VRF {{ vrf }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8 col-md-9">
@@ -35,7 +37,7 @@
</a>
{% endif %}
</div>
<h1>{% block title %}VRF {{ vrf }}{% endblock %}</h1>
<h1>{{ vrf }}</h1>
{% include 'inc/created_updated.html' with obj=vrf %}
<div class="row">
<div class="col-md-6">

Some files were not shown because too many files have changed in this diff Show More