mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge branch 'develop' into 3623-interface-word-expansion
This commit is contained in:
commit
a5413a5484
23
.github/lock.yml
vendored
Normal file
23
.github/lock.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Configuration for Lock (https://github.com/apps/lock)
|
||||||
|
|
||||||
|
# Number of days of inactivity before a closed issue or pull request is locked
|
||||||
|
daysUntilLock: 90
|
||||||
|
|
||||||
|
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||||
|
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||||
|
skipCreatedBefore: 2020-01-01
|
||||||
|
|
||||||
|
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||||
|
exemptLabels: []
|
||||||
|
|
||||||
|
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||||
|
lockLabel: false
|
||||||
|
|
||||||
|
# Comment to post before locking. Set to `false` to disable
|
||||||
|
lockComment: false
|
||||||
|
|
||||||
|
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||||
|
setLockReason: true
|
||||||
|
|
||||||
|
# Limit to only `issues` or `pulls`
|
||||||
|
# only: issues
|
7
.github/stale.yml
vendored
7
.github/stale.yml
vendored
@ -1,20 +1,27 @@
|
|||||||
|
# Configuration for Stale (https://github.com/apps/stale)
|
||||||
|
|
||||||
# Number of days of inactivity before an issue becomes stale
|
# Number of days of inactivity before an issue becomes stale
|
||||||
daysUntilStale: 14
|
daysUntilStale: 14
|
||||||
|
|
||||||
# Number of days of inactivity before a stale issue is closed
|
# Number of days of inactivity before a stale issue is closed
|
||||||
daysUntilClose: 7
|
daysUntilClose: 7
|
||||||
|
|
||||||
# Issues with these labels will never be considered stale
|
# Issues with these labels will never be considered stale
|
||||||
exemptLabels:
|
exemptLabels:
|
||||||
- "status: accepted"
|
- "status: accepted"
|
||||||
- "status: gathering feedback"
|
- "status: gathering feedback"
|
||||||
- "status: blocked"
|
- "status: blocked"
|
||||||
|
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: wontfix
|
staleLabel: wontfix
|
||||||
|
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
markComment: >
|
markComment: >
|
||||||
This issue has been automatically marked as stale because it has not had
|
This issue has been automatically marked as stale because it has not had
|
||||||
recent activity. It will be closed if no further activity occurs. NetBox
|
recent activity. It will be closed if no further activity occurs. NetBox
|
||||||
is governed by a small group of core maintainers which means not all opened
|
is governed by a small group of core maintainers which means not all opened
|
||||||
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
|
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
|
||||||
|
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
closeComment: >
|
closeComment: >
|
||||||
This issue has been automatically closed due to lack of activity. In an
|
This issue has been automatically closed due to lack of activity. In an
|
||||||
|
65
docs/additional-features/napalm.md
Normal file
65
docs/additional-features/napalm.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# NAPALM
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
To enable the integration, the NAPALM library must be installed. See [installation steps](../../installation/2-netbox/#napalm-automation-optional) for more information.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/dcim/devices/1/napalm/?method=get_environment
|
||||||
|
|
||||||
|
{
|
||||||
|
"get_environment": {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
By default, the [`NAPALM_USERNAME`](../../configuration/optional-settings/#napalm_username) and [`NAPALM_PASSWORD`](../../configuration/optional-settings/#napalm_password) are used for NAPALM authentication. They can be overridden for an individual API call through the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
|
||||||
|
-H "Authorization: Token f4b378553dacfcfd44c5a0b9ae49b57e29c552b5" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json; indent=4" \
|
||||||
|
-H "X-NAPALM-Username: foo" \
|
||||||
|
-H "X-NAPALM-Password: bar"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Method Support
|
||||||
|
|
||||||
|
The list of supported NAPALM methods depends on the [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html#general-support-matrix) configured for the platform of a device. NetBox only supports [get](https://napalm.readthedocs.io/en/latest/support/index.html#getters-support-matrix) methods.
|
||||||
|
|
||||||
|
## Multiple Methods
|
||||||
|
|
||||||
|
More than one method in an API call can be invoked by adding multiple `method` parameters. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/dcim/devices/1/napalm/?method=get_ntp_servers&method=get_ntp_peers
|
||||||
|
|
||||||
|
{
|
||||||
|
"get_ntp_servers": {
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"get_ntp_peers": {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional Arguments
|
||||||
|
|
||||||
|
The behavior of NAPALM drivers can be adjusted according to the [optional arguments](https://napalm.readthedocs.io/en/latest/support/index.html#optional-arguments). NetBox exposes those arguments using headers prefixed with `X-NAPALM-`.
|
||||||
|
|
||||||
|
|
||||||
|
For instance, the SSH port is changed to 2222 in this API call:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
|
||||||
|
-H "Authorization: Token f4b378553dacfcfd44c5a0b9ae49b57e29c552b5" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json; indent=4" \
|
||||||
|
-H "X-NAPALM-port: 2222"
|
||||||
|
```
|
@ -2,19 +2,28 @@
|
|||||||
|
|
||||||
## Enhancements
|
## Enhancements
|
||||||
|
|
||||||
|
* [#1982](https://github.com/netbox-community/netbox/issues/1982) - Improved NAPALM method documentation in Swagger
|
||||||
* [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link
|
* [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link
|
||||||
|
* [#2113](https://github.com/netbox-community/netbox/issues/2113) - Allow NAPALM driver settings to be changed with request headers
|
||||||
* [#2589](https://github.com/netbox-community/netbox/issues/2589) - Toggle for showing available prefixes/ip addresses
|
* [#2589](https://github.com/netbox-community/netbox/issues/2589) - Toggle for showing available prefixes/ip addresses
|
||||||
|
* [#3090](https://github.com/netbox-community/netbox/issues/3090) - Add filter field for device interfaces
|
||||||
* [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations
|
* [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations
|
||||||
|
* [#3393](https://github.com/netbox-community/netbox/issues/3393) - Paginate the circuits at the provider details view
|
||||||
|
* [#3440](https://github.com/netbox-community/netbox/issues/3440) - Add total length to cable trace
|
||||||
* [#3623](https://github.com/netbox-community/netbox/issues/3623) - Add word expansion during interface creation
|
* [#3623](https://github.com/netbox-community/netbox/issues/3623) - Add word expansion during interface creation
|
||||||
* [#3851](https://github.com/netbox-community/netbox/issues/3851) - Allow passing initial data to custom script forms
|
* [#3851](https://github.com/netbox-community/netbox/issues/3851) - Allow passing initial data to custom script forms
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
* [#3589](https://github.com/netbox-community/netbox/issues/3589) - Fix validation on tagged VLANs of an interface
|
* [#3589](https://github.com/netbox-community/netbox/issues/3589) - Fix validation on tagged VLANs of an interface
|
||||||
|
* [#3849](https://github.com/netbox-community/netbox/issues/3849) - Fix ordering of models when dumping data to JSON
|
||||||
* [#3853](https://github.com/netbox-community/netbox/issues/3853) - Fix device role link on config context view
|
* [#3853](https://github.com/netbox-community/netbox/issues/3853) - Fix device role link on config context view
|
||||||
* [#3856](https://github.com/netbox-community/netbox/issues/3856) - Allow filtering VM interfaces by multiple MAC addresses
|
* [#3856](https://github.com/netbox-community/netbox/issues/3856) - Allow filtering VM interfaces by multiple MAC addresses
|
||||||
* [#3857](https://github.com/netbox-community/netbox/issues/3857) - Fix group custom links rendering
|
* [#3857](https://github.com/netbox-community/netbox/issues/3857) - Fix group custom links rendering
|
||||||
* [#3862](https://github.com/netbox-community/netbox/issues/3862) - Allow filtering device components by multiple device names
|
* [#3862](https://github.com/netbox-community/netbox/issues/3862) - Allow filtering device components by multiple device names
|
||||||
|
* [#3864](https://github.com/netbox-community/netbox/issues/3864) - Disallow /0 masks
|
||||||
|
* [#3872](https://github.com/netbox-community/netbox/issues/3872) - Paginate related IPs of an address
|
||||||
|
* [#3876](https://github.com/netbox-community/netbox/issues/3876) - Fixed min/max to ASN input field at the site creation page
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ pages:
|
|||||||
- Custom Scripts: 'additional-features/custom-scripts.md'
|
- Custom Scripts: 'additional-features/custom-scripts.md'
|
||||||
- Export Templates: 'additional-features/export-templates.md'
|
- Export Templates: 'additional-features/export-templates.md'
|
||||||
- Graphs: 'additional-features/graphs.md'
|
- Graphs: 'additional-features/graphs.md'
|
||||||
|
- NAPALM: 'additional-features/napalm.md'
|
||||||
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
|
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
|
||||||
- Reports: 'additional-features/reports.md'
|
- Reports: 'additional-features/reports.md'
|
||||||
- Tags: 'additional-features/tags.md'
|
- Tags: 'additional-features/tags.md'
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import permission_required
|
from django.contrib.auth.decorators import permission_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
@ -5,9 +6,11 @@ from django.db import transaction
|
|||||||
from django.db.models import Count, OuterRef, Subquery
|
from django.db.models import Count, OuterRef, Subquery
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
|
from utilities.paginator import EnhancedPaginator
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
@ -38,9 +41,18 @@ class ProviderView(PermissionRequiredMixin, View):
|
|||||||
circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site')
|
circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site')
|
||||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
|
||||||
|
|
||||||
|
circuits_table = tables.CircuitTable(circuits, orderable=False)
|
||||||
|
circuits_table.columns.hide('provider')
|
||||||
|
|
||||||
|
paginate = {
|
||||||
|
'paginator_class': EnhancedPaginator,
|
||||||
|
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||||
|
}
|
||||||
|
RequestConfig(request, paginate).configure(circuits_table)
|
||||||
|
|
||||||
return render(request, 'circuits/provider.html', {
|
return render(request, 'circuits/provider.html', {
|
||||||
'provider': provider,
|
'provider': provider,
|
||||||
'circuits': circuits,
|
'circuits_table': circuits_table,
|
||||||
'show_graphs': show_graphs,
|
'show_graphs': show_graphs,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -370,6 +370,10 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
|||||||
return obj.get_config_context()
|
return obj.get_config_context()
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceNAPALMSerializer(serializers.Serializer):
|
||||||
|
method = serializers.DictField()
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
cable = NestedCableSerializer(read_only=True)
|
cable = NestedCableSerializer(read_only=True)
|
||||||
|
@ -358,6 +358,17 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
manual_parameters=[
|
||||||
|
Parameter(
|
||||||
|
name='method',
|
||||||
|
in_='query',
|
||||||
|
required=True,
|
||||||
|
type=openapi.TYPE_STRING
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={'200': serializers.DeviceNAPALMSerializer}
|
||||||
|
)
|
||||||
@action(detail=True, url_path='napalm')
|
@action(detail=True, url_path='napalm')
|
||||||
def napalm(self, request, pk):
|
def napalm(self, request, pk):
|
||||||
"""
|
"""
|
||||||
@ -396,13 +407,29 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
napalm_methods = request.GET.getlist('method')
|
napalm_methods = request.GET.getlist('method')
|
||||||
response = OrderedDict([(m, None) for m in napalm_methods])
|
response = OrderedDict([(m, None) for m in napalm_methods])
|
||||||
ip_address = str(device.primary_ip.address.ip)
|
ip_address = str(device.primary_ip.address.ip)
|
||||||
|
username = settings.NAPALM_USERNAME
|
||||||
|
password = settings.NAPALM_PASSWORD
|
||||||
optional_args = settings.NAPALM_ARGS.copy()
|
optional_args = settings.NAPALM_ARGS.copy()
|
||||||
if device.platform.napalm_args is not None:
|
if device.platform.napalm_args is not None:
|
||||||
optional_args.update(device.platform.napalm_args)
|
optional_args.update(device.platform.napalm_args)
|
||||||
|
|
||||||
|
# Update NAPALM parameters according to the request headers
|
||||||
|
for header in request.headers:
|
||||||
|
if header[:9].lower() != 'x-napalm-':
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = header[9:]
|
||||||
|
if key.lower() == 'username':
|
||||||
|
username = request.headers[header]
|
||||||
|
elif key.lower() == 'password':
|
||||||
|
password = request.headers[header]
|
||||||
|
elif key:
|
||||||
|
optional_args[key.lower()] = request.headers[header]
|
||||||
|
|
||||||
d = driver(
|
d = driver(
|
||||||
hostname=ip_address,
|
hostname=ip_address,
|
||||||
username=settings.NAPALM_USERNAME,
|
username=username,
|
||||||
password=settings.NAPALM_PASSWORD,
|
password=password,
|
||||||
timeout=settings.NAPALM_TIMEOUT,
|
timeout=settings.NAPALM_TIMEOUT,
|
||||||
optional_args=optional_args
|
optional_args=optional_args
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
|
|
||||||
|
# BGP ASN bounds
|
||||||
|
BGP_ASN_MIN = 1
|
||||||
|
BGP_ASN_MAX = 2**32 - 1
|
||||||
|
|
||||||
# Rack types
|
# Rack types
|
||||||
RACK_TYPE_2POST = 100
|
RACK_TYPE_2POST = 100
|
||||||
RACK_TYPE_4POST = 200
|
RACK_TYPE_4POST = 200
|
||||||
|
@ -3,14 +3,21 @@ from django.core.validators import MinValueValidator, MaxValueValidator
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from netaddr import AddrFormatError, EUI, mac_unix_expanded
|
from netaddr import AddrFormatError, EUI, mac_unix_expanded
|
||||||
|
|
||||||
|
from .constants import *
|
||||||
|
|
||||||
|
|
||||||
class ASNField(models.BigIntegerField):
|
class ASNField(models.BigIntegerField):
|
||||||
description = "32-bit ASN field"
|
description = "32-bit ASN field"
|
||||||
default_validators = [
|
default_validators = [
|
||||||
MinValueValidator(1),
|
MinValueValidator(BGP_ASN_MIN),
|
||||||
MaxValueValidator(4294967295),
|
MaxValueValidator(BGP_ASN_MAX),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def formfield(self, **kwargs):
|
||||||
|
defaults = {'min_value': BGP_ASN_MIN, 'max_value': BGP_ASN_MAX}
|
||||||
|
defaults.update(**kwargs)
|
||||||
|
return super().formfield(**defaults)
|
||||||
|
|
||||||
|
|
||||||
class mac_unix_expanded_uppercase(mac_unix_expanded):
|
class mac_unix_expanded_uppercase(mac_unix_expanded):
|
||||||
word_fmt = '%.2X'
|
word_fmt = '%.2X'
|
||||||
|
@ -292,8 +292,8 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
asn = forms.IntegerField(
|
asn = forms.IntegerField(
|
||||||
min_value=1,
|
min_value=BGP_ASN_MIN,
|
||||||
max_value=4294967295,
|
max_value=BGP_ASN_MAX,
|
||||||
required=False,
|
required=False,
|
||||||
label='ASN'
|
label='ASN'
|
||||||
)
|
)
|
||||||
|
@ -2755,6 +2755,187 @@ class VirtualChassis(ChangeLoggedModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power
|
||||||
|
#
|
||||||
|
|
||||||
|
class PowerPanel(ChangeLoggedModel):
|
||||||
|
"""
|
||||||
|
A distribution point for electrical power; e.g. a data center RPP.
|
||||||
|
"""
|
||||||
|
site = models.ForeignKey(
|
||||||
|
to='Site',
|
||||||
|
on_delete=models.PROTECT
|
||||||
|
)
|
||||||
|
rack_group = models.ForeignKey(
|
||||||
|
to='RackGroup',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_headers = ['site', 'rack_group_name', 'name']
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['site', 'name']
|
||||||
|
unique_together = ['site', 'name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('dcim:powerpanel', args=[self.pk])
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
|
return (
|
||||||
|
self.site.name,
|
||||||
|
self.rack_group.name if self.rack_group else None,
|
||||||
|
self.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# RackGroup must belong to assigned Site
|
||||||
|
if self.rack_group and self.rack_group.site != self.site:
|
||||||
|
raise ValidationError("Rack group {} ({}) is in a different site than {}".format(
|
||||||
|
self.rack_group, self.rack_group.site, self.site
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
||||||
|
"""
|
||||||
|
An electrical circuit delivered from a PowerPanel.
|
||||||
|
"""
|
||||||
|
power_panel = models.ForeignKey(
|
||||||
|
to='PowerPanel',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='powerfeeds'
|
||||||
|
)
|
||||||
|
rack = models.ForeignKey(
|
||||||
|
to='Rack',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
connected_endpoint = models.OneToOneField(
|
||||||
|
to='dcim.PowerPort',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='+',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
connection_status = models.NullBooleanField(
|
||||||
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
status = models.PositiveSmallIntegerField(
|
||||||
|
choices=POWERFEED_STATUS_CHOICES,
|
||||||
|
default=POWERFEED_STATUS_ACTIVE
|
||||||
|
)
|
||||||
|
type = models.PositiveSmallIntegerField(
|
||||||
|
choices=POWERFEED_TYPE_CHOICES,
|
||||||
|
default=POWERFEED_TYPE_PRIMARY
|
||||||
|
)
|
||||||
|
supply = models.PositiveSmallIntegerField(
|
||||||
|
choices=POWERFEED_SUPPLY_CHOICES,
|
||||||
|
default=POWERFEED_SUPPLY_AC
|
||||||
|
)
|
||||||
|
phase = models.PositiveSmallIntegerField(
|
||||||
|
choices=POWERFEED_PHASE_CHOICES,
|
||||||
|
default=POWERFEED_PHASE_SINGLE
|
||||||
|
)
|
||||||
|
voltage = models.PositiveSmallIntegerField(
|
||||||
|
validators=[MinValueValidator(1)],
|
||||||
|
default=120
|
||||||
|
)
|
||||||
|
amperage = models.PositiveSmallIntegerField(
|
||||||
|
validators=[MinValueValidator(1)],
|
||||||
|
default=20
|
||||||
|
)
|
||||||
|
max_utilization = models.PositiveSmallIntegerField(
|
||||||
|
validators=[MinValueValidator(1), MaxValueValidator(100)],
|
||||||
|
default=80,
|
||||||
|
help_text="Maximum permissible draw (percentage)"
|
||||||
|
)
|
||||||
|
available_power = models.PositiveSmallIntegerField(
|
||||||
|
default=0,
|
||||||
|
editable=False
|
||||||
|
)
|
||||||
|
comments = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
|
csv_headers = [
|
||||||
|
'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||||
|
'amperage', 'max_utilization', 'comments',
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['power_panel', 'name']
|
||||||
|
unique_together = ['power_panel', 'name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('dcim:powerfeed', args=[self.pk])
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
|
return (
|
||||||
|
self.power_panel.site.name,
|
||||||
|
self.power_panel.name,
|
||||||
|
self.rack.group.name if self.rack and self.rack.group else None,
|
||||||
|
self.rack.name if self.rack else None,
|
||||||
|
self.name,
|
||||||
|
self.get_status_display(),
|
||||||
|
self.get_type_display(),
|
||||||
|
self.get_supply_display(),
|
||||||
|
self.get_phase_display(),
|
||||||
|
self.voltage,
|
||||||
|
self.amperage,
|
||||||
|
self.max_utilization,
|
||||||
|
self.comments,
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Rack must belong to same Site as PowerPanel
|
||||||
|
if self.rack and self.rack.site != self.power_panel.site:
|
||||||
|
raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format(
|
||||||
|
self.rack, self.rack.site, self.power_panel, self.power_panel.site
|
||||||
|
))
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Cache the available_power property on the instance
|
||||||
|
kva = self.voltage * self.amperage * (self.max_utilization / 100)
|
||||||
|
if self.phase == POWERFEED_PHASE_3PHASE:
|
||||||
|
self.available_power = round(kva * 1.732)
|
||||||
|
else:
|
||||||
|
self.available_power = round(kva)
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_type_class(self):
|
||||||
|
return STATUS_CLASSES[self.type]
|
||||||
|
|
||||||
|
def get_status_class(self):
|
||||||
|
return STATUS_CLASSES[self.status]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
@ -2950,6 +3131,8 @@ class Cable(ChangeLoggedModel):
|
|||||||
# Store the given length (if any) in meters for use in database ordering
|
# Store the given length (if any) in meters for use in database ordering
|
||||||
if self.length and self.length_unit:
|
if self.length and self.length_unit:
|
||||||
self._abs_length = to_meters(self.length, self.length_unit)
|
self._abs_length = to_meters(self.length, self.length_unit)
|
||||||
|
else:
|
||||||
|
self._abs_length = None
|
||||||
|
|
||||||
# Store the parent Device for the A and B terminations (if applicable) to enable filtering
|
# Store the parent Device for the A and B terminations (if applicable) to enable filtering
|
||||||
if hasattr(self.termination_a, 'device'):
|
if hasattr(self.termination_a, 'device'):
|
||||||
@ -3006,184 +3189,3 @@ class Cable(ChangeLoggedModel):
|
|||||||
b_endpoint = b_path[-1][2]
|
b_endpoint = b_path[-1][2]
|
||||||
|
|
||||||
return a_endpoint, b_endpoint, path_status
|
return a_endpoint, b_endpoint, path_status
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Power
|
|
||||||
#
|
|
||||||
|
|
||||||
class PowerPanel(ChangeLoggedModel):
|
|
||||||
"""
|
|
||||||
A distribution point for electrical power; e.g. a data center RPP.
|
|
||||||
"""
|
|
||||||
site = models.ForeignKey(
|
|
||||||
to='Site',
|
|
||||||
on_delete=models.PROTECT
|
|
||||||
)
|
|
||||||
rack_group = models.ForeignKey(
|
|
||||||
to='RackGroup',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=50
|
|
||||||
)
|
|
||||||
|
|
||||||
csv_headers = ['site', 'rack_group_name', 'name']
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['site', 'name']
|
|
||||||
unique_together = ['site', 'name']
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('dcim:powerpanel', args=[self.pk])
|
|
||||||
|
|
||||||
def to_csv(self):
|
|
||||||
return (
|
|
||||||
self.site.name,
|
|
||||||
self.rack_group.name if self.rack_group else None,
|
|
||||||
self.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
|
|
||||||
# RackGroup must belong to assigned Site
|
|
||||||
if self.rack_group and self.rack_group.site != self.site:
|
|
||||||
raise ValidationError("Rack group {} ({}) is in a different site than {}".format(
|
|
||||||
self.rack_group, self.rack_group.site, self.site
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
|
||||||
"""
|
|
||||||
An electrical circuit delivered from a PowerPanel.
|
|
||||||
"""
|
|
||||||
power_panel = models.ForeignKey(
|
|
||||||
to='PowerPanel',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='powerfeeds'
|
|
||||||
)
|
|
||||||
rack = models.ForeignKey(
|
|
||||||
to='Rack',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
connected_endpoint = models.OneToOneField(
|
|
||||||
to='dcim.PowerPort',
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
related_name='+',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
connection_status = models.NullBooleanField(
|
|
||||||
choices=CONNECTION_STATUS_CHOICES,
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
name = models.CharField(
|
|
||||||
max_length=50
|
|
||||||
)
|
|
||||||
status = models.PositiveSmallIntegerField(
|
|
||||||
choices=POWERFEED_STATUS_CHOICES,
|
|
||||||
default=POWERFEED_STATUS_ACTIVE
|
|
||||||
)
|
|
||||||
type = models.PositiveSmallIntegerField(
|
|
||||||
choices=POWERFEED_TYPE_CHOICES,
|
|
||||||
default=POWERFEED_TYPE_PRIMARY
|
|
||||||
)
|
|
||||||
supply = models.PositiveSmallIntegerField(
|
|
||||||
choices=POWERFEED_SUPPLY_CHOICES,
|
|
||||||
default=POWERFEED_SUPPLY_AC
|
|
||||||
)
|
|
||||||
phase = models.PositiveSmallIntegerField(
|
|
||||||
choices=POWERFEED_PHASE_CHOICES,
|
|
||||||
default=POWERFEED_PHASE_SINGLE
|
|
||||||
)
|
|
||||||
voltage = models.PositiveSmallIntegerField(
|
|
||||||
validators=[MinValueValidator(1)],
|
|
||||||
default=120
|
|
||||||
)
|
|
||||||
amperage = models.PositiveSmallIntegerField(
|
|
||||||
validators=[MinValueValidator(1)],
|
|
||||||
default=20
|
|
||||||
)
|
|
||||||
max_utilization = models.PositiveSmallIntegerField(
|
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(100)],
|
|
||||||
default=80,
|
|
||||||
help_text="Maximum permissible draw (percentage)"
|
|
||||||
)
|
|
||||||
available_power = models.PositiveSmallIntegerField(
|
|
||||||
default=0,
|
|
||||||
editable=False
|
|
||||||
)
|
|
||||||
comments = models.TextField(
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
custom_field_values = GenericRelation(
|
|
||||||
to='extras.CustomFieldValue',
|
|
||||||
content_type_field='obj_type',
|
|
||||||
object_id_field='obj_id'
|
|
||||||
)
|
|
||||||
|
|
||||||
tags = TaggableManager(through=TaggedItem)
|
|
||||||
|
|
||||||
csv_headers = [
|
|
||||||
'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
|
||||||
'amperage', 'max_utilization', 'comments',
|
|
||||||
]
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['power_panel', 'name']
|
|
||||||
unique_together = ['power_panel', 'name']
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('dcim:powerfeed', args=[self.pk])
|
|
||||||
|
|
||||||
def to_csv(self):
|
|
||||||
return (
|
|
||||||
self.power_panel.site.name,
|
|
||||||
self.power_panel.name,
|
|
||||||
self.rack.group.name if self.rack and self.rack.group else None,
|
|
||||||
self.rack.name if self.rack else None,
|
|
||||||
self.name,
|
|
||||||
self.get_status_display(),
|
|
||||||
self.get_type_display(),
|
|
||||||
self.get_supply_display(),
|
|
||||||
self.get_phase_display(),
|
|
||||||
self.voltage,
|
|
||||||
self.amperage,
|
|
||||||
self.max_utilization,
|
|
||||||
self.comments,
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
|
|
||||||
# Rack must belong to same Site as PowerPanel
|
|
||||||
if self.rack and self.rack.site != self.power_panel.site:
|
|
||||||
raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format(
|
|
||||||
self.rack, self.rack.site, self.power_panel, self.power_panel.site
|
|
||||||
))
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
|
|
||||||
# Cache the available_power property on the instance
|
|
||||||
kva = self.voltage * self.amperage * (self.max_utilization / 100)
|
|
||||||
if self.phase == POWERFEED_PHASE_3PHASE:
|
|
||||||
self.available_power = round(kva * 1.732)
|
|
||||||
else:
|
|
||||||
self.available_power = round(kva)
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_type_class(self):
|
|
||||||
return STATUS_CLASSES[self.type]
|
|
||||||
|
|
||||||
def get_status_class(self):
|
|
||||||
return STATUS_CLASSES[self.status]
|
|
||||||
|
@ -1754,10 +1754,13 @@ class CableTraceView(PermissionRequiredMixin, View):
|
|||||||
def get(self, request, model, pk):
|
def get(self, request, model, pk):
|
||||||
|
|
||||||
obj = get_object_or_404(model, pk=pk)
|
obj = get_object_or_404(model, pk=pk)
|
||||||
|
trace = obj.trace(follow_circuits=True)
|
||||||
|
total_length = sum([entry[1]._abs_length for entry in trace if entry[1] and entry[1]._abs_length])
|
||||||
|
|
||||||
return render(request, 'dcim/cable_trace.html', {
|
return render(request, 'dcim/cable_trace.html', {
|
||||||
'obj': obj,
|
'obj': obj,
|
||||||
'trace': obj.trace(follow_circuits=True),
|
'trace': trace,
|
||||||
|
'total_length': total_length,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -177,6 +177,12 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
|||||||
# Clear host bits from prefix
|
# Clear host bits from prefix
|
||||||
self.prefix = self.prefix.cidr
|
self.prefix = self.prefix.cidr
|
||||||
|
|
||||||
|
# /0 masks are not acceptable
|
||||||
|
if self.prefix.prefixlen == 0:
|
||||||
|
raise ValidationError({
|
||||||
|
'prefix': "Cannot create aggregate with /0 mask."
|
||||||
|
})
|
||||||
|
|
||||||
# Ensure that the aggregate being added is not covered by an existing aggregate
|
# Ensure that the aggregate being added is not covered by an existing aggregate
|
||||||
covering_aggregates = Aggregate.objects.filter(prefix__net_contains_or_equals=str(self.prefix))
|
covering_aggregates = Aggregate.objects.filter(prefix__net_contains_or_equals=str(self.prefix))
|
||||||
if self.pk:
|
if self.pk:
|
||||||
@ -347,6 +353,12 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
|||||||
|
|
||||||
if self.prefix:
|
if self.prefix:
|
||||||
|
|
||||||
|
# /0 masks are not acceptable
|
||||||
|
if self.prefix.prefixlen == 0:
|
||||||
|
raise ValidationError({
|
||||||
|
'prefix': "Cannot create prefix with /0 mask."
|
||||||
|
})
|
||||||
|
|
||||||
# Disallow host masks
|
# Disallow host masks
|
||||||
if self.prefix.version == 4 and self.prefix.prefixlen == 32:
|
if self.prefix.version == 4 and self.prefix.prefixlen == 32:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
@ -622,6 +634,12 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
|
|
||||||
if self.address:
|
if self.address:
|
||||||
|
|
||||||
|
# /0 masks are not acceptable
|
||||||
|
if self.address.prefixlen == 0:
|
||||||
|
raise ValidationError({
|
||||||
|
'address': "Cannot create IP address with /0 mask."
|
||||||
|
})
|
||||||
|
|
||||||
# Enforce unique IP space (if applicable)
|
# Enforce unique IP space (if applicable)
|
||||||
if self.role not in IPADDRESS_ROLES_NONUNIQUE and ((
|
if self.role not in IPADDRESS_ROLES_NONUNIQUE and ((
|
||||||
self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE
|
self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE
|
||||||
|
@ -686,7 +686,14 @@ class IPAddressView(PermissionRequiredMixin, View):
|
|||||||
).filter(
|
).filter(
|
||||||
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
|
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.IPAddressTable(related_ips, orderable=False)
|
||||||
|
|
||||||
|
paginate = {
|
||||||
|
'paginator_class': EnhancedPaginator,
|
||||||
|
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||||
|
}
|
||||||
|
RequestConfig(request, paginate).configure(related_ips_table)
|
||||||
|
|
||||||
return render(request, 'ipam/ipaddress.html', {
|
return render(request, 'ipam/ipaddress.html', {
|
||||||
'ipaddress': ipaddress,
|
'ipaddress': ipaddress,
|
||||||
|
@ -7,7 +7,7 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
// "Toggle" checkbox for object lists (PK column)
|
// "Toggle" checkbox for object lists (PK column)
|
||||||
$('input:checkbox.toggle').click(function() {
|
$('input:checkbox.toggle').click(function() {
|
||||||
$(this).closest('table').find('input:checkbox[name=pk]').prop('checked', $(this).prop('checked'));
|
$(this).closest('table').find('input:checkbox[name=pk]:visible').prop('checked', $(this).prop('checked'));
|
||||||
|
|
||||||
// Show the "select all" box if present
|
// Show the "select all" box if present
|
||||||
if ($(this).is(':checked')) {
|
if ($(this).is(':checked')) {
|
||||||
@ -400,8 +400,8 @@ $(document).ready(function() {
|
|||||||
window.addEventListener('hashchange', headerOffsetScroll);
|
window.addEventListener('hashchange', headerOffsetScroll);
|
||||||
|
|
||||||
// Offset between the preview window and the window edges
|
// Offset between the preview window and the window edges
|
||||||
const IMAGE_PREVIEW_OFFSET_X = 20
|
const IMAGE_PREVIEW_OFFSET_X = 20;
|
||||||
const IMAGE_PREVIEW_OFFSET_Y = 10
|
const IMAGE_PREVIEW_OFFSET_Y = 10;
|
||||||
|
|
||||||
// Preview an image attachment when the link is hovered over
|
// Preview an image attachment when the link is hovered over
|
||||||
$('a.image-preview').on('mouseover', function(e) {
|
$('a.image-preview').on('mouseover', function(e) {
|
||||||
@ -435,6 +435,6 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
// Fade the image out; it will be deleted when another one is previewed
|
// Fade the image out; it will be deleted when another one is previewed
|
||||||
$('a.image-preview').on('mouseout', function() {
|
$('a.image-preview').on('mouseout', function() {
|
||||||
$('#image-preview-window').fadeOut('fast')
|
$('#image-preview-window').fadeOut('fast');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
30
netbox/project-static/js/interface_toggles.js
Normal file
30
netbox/project-static/js/interface_toggles.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Toggle the display of IP addresses under interfaces
|
||||||
|
$('button.toggle-ips').click(function() {
|
||||||
|
var selected = $(this).attr('selected');
|
||||||
|
if (selected) {
|
||||||
|
$('#interfaces_table tr.ipaddresses').hide();
|
||||||
|
} else {
|
||||||
|
$('#interfaces_table tr.ipaddresses').show();
|
||||||
|
}
|
||||||
|
$(this).attr('selected', !selected);
|
||||||
|
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inteface filtering
|
||||||
|
$('input.interface-filter').on('input', function() {
|
||||||
|
var filter = new RegExp(this.value);
|
||||||
|
|
||||||
|
for (interface of $(this).closest('form').find('tbody > tr')) {
|
||||||
|
// Slice off 'interface_' at the start of the ID
|
||||||
|
if (filter && filter.test(interface.id.slice(10))) {
|
||||||
|
// Match the toggle in case the filter now matches the interface
|
||||||
|
$(interface).find('input:checkbox[name=pk]').prop('checked', $('input.toggle').prop('checked'));
|
||||||
|
$(interface).show();
|
||||||
|
} else {
|
||||||
|
// Uncheck to prevent actions from including it when it doesn't match
|
||||||
|
$(interface).find('input:checkbox[name=pk]').prop('checked', false);
|
||||||
|
$(interface).hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -125,58 +125,7 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Circuits</strong>
|
<strong>Circuits</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
{% include 'inc/table.html' with table=circuits_table %}
|
||||||
<tr>
|
|
||||||
<th>Circuit ID</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Tenant</th>
|
|
||||||
<th>A Side</th>
|
|
||||||
<th>Z Side</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
{% for c in circuits %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'circuits:circuit' pk=c.pk %}">{{ c.cid }}</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'circuits:circuit_list' %}?type={{ c.type.slug }}">{{ c.type }}</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if c.tenant %}
|
|
||||||
<a href="{% url 'tenancy:tenant' slug=c.tenant.slug %}">{{ c.tenant }}</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if c.termination_a %}
|
|
||||||
<a href="{% url 'dcim:site' slug=c.termination_a.site.slug %}">{{ c.termination_a.site }}</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if c.termination_z %}
|
|
||||||
<a href="{% url 'dcim:site' slug=c.termination_z.site.slug %}">{{ c.termination_z.site }}</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if c.description %}
|
|
||||||
{{ c.description }}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-muted">None</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% if perms.circuits.add_circuit %}
|
{% if perms.circuits.add_circuit %}
|
||||||
<div class="panel-footer text-right noprint">
|
<div class="panel-footer text-right noprint">
|
||||||
<a href="{% url 'circuits:circuit_add' %}?provider={{ provider.pk }}" class="btn btn-xs btn-primary">
|
<a href="{% url 'circuits:circuit_add' %}?provider={{ provider.pk }}" class="btn btn-xs btn-primary">
|
||||||
@ -185,6 +134,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/modal.html' with modal_name='graphs' %}
|
{% include 'inc/modal.html' with modal_name='graphs' %}
|
||||||
|
@ -10,7 +10,10 @@
|
|||||||
<div class="col-md-4 col-md-offset-1 text-center">
|
<div class="col-md-4 col-md-offset-1 text-center">
|
||||||
<h4>Near End</h4>
|
<h4>Near End</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 col-md-offset-3 text-center">
|
<div class="col-md-3 text-center">
|
||||||
|
{% if total_length %}<h5>Total length: {{ total_length|floatformat:"-2" }} Meters<h5>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-center">
|
||||||
<h4>Far End</h4>
|
<h4>Far End</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -556,6 +556,9 @@
|
|||||||
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
|
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-2 pull-right noprint">
|
||||||
|
<input class="form-control interface-filter" type="text" placeholder="Filter" title="RegEx-enabled" style="height: 23px" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
|
<table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
|
||||||
<thead>
|
<thead>
|
||||||
@ -900,19 +903,8 @@ function toggleConnection(elem) {
|
|||||||
$(".cable-toggle").click(function() {
|
$(".cable-toggle").click(function() {
|
||||||
return toggleConnection($(this));
|
return toggleConnection($(this));
|
||||||
});
|
});
|
||||||
// Toggle the display of IP addresses under interfaces
|
|
||||||
$('button.toggle-ips').click(function() {
|
|
||||||
var selected = $(this).attr('selected');
|
|
||||||
if (selected) {
|
|
||||||
$('#interfaces_table tr.ipaddresses').hide();
|
|
||||||
} else {
|
|
||||||
$('#interfaces_table tr.ipaddresses').show();
|
|
||||||
}
|
|
||||||
$(this).attr('selected', !selected);
|
|
||||||
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
<script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
|
<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
|
<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -160,7 +160,7 @@
|
|||||||
{% if duplicate_ips_table.rows %}
|
{% if duplicate_ips_table.rows %}
|
||||||
{% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %}
|
{% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include 'panel_table.html' with table=related_ips_table heading='Related IP Addresses' panel_class='default noprint' %}
|
{% include 'utilities/obj_table.html' with table=related_ips_table table_template='panel_table.html' heading='Related IP Addresses' panel_class='default noprint' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% load custom_links %}
|
{% load custom_links %}
|
||||||
|
{% load static %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
@ -253,6 +254,9 @@
|
|||||||
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
|
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-2 pull-right noprint">
|
||||||
|
<input class="form-control interface-filter" type="text" placeholder="Filter" title="RegEx-enabled" style="height: 23px" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
|
<table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
|
||||||
<thead>
|
<thead>
|
||||||
@ -312,18 +316,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
<script type="text/javascript">
|
<script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
// Toggle the display of IP addresses under interfaces
|
|
||||||
$('button.toggle-ips').click(function() {
|
|
||||||
var selected = $(this).attr('selected');
|
|
||||||
if (selected) {
|
|
||||||
$('#interfaces_table tr.ipaddresses').hide();
|
|
||||||
} else {
|
|
||||||
$('#interfaces_table tr.ipaddresses').show();
|
|
||||||
}
|
|
||||||
$(this).attr('selected', !selected);
|
|
||||||
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Loading…
Reference in New Issue
Block a user