mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
10520 remove Napalm code references (#11768)
* 10520 remove all Napalm code references * 10520 remove lldp * 10520 remove config, status - rebuild js * 10520 re-add config parameters * 10520 re-add serializer * 10520 update docs
This commit is contained in:
parent
927371b908
commit
36771e821c
@ -1,5 +1,7 @@
|
|||||||
# NAPALM Parameters
|
# NAPALM Parameters
|
||||||
|
|
||||||
|
!!! **Note:** As of NetBox v3.5, NAPALM integration has been moved to a plugin and these configuration parameters are now deprecated.
|
||||||
|
|
||||||
## NAPALM_USERNAME
|
## NAPALM_USERNAME
|
||||||
|
|
||||||
## NAPALM_PASSWORD
|
## NAPALM_PASSWORD
|
||||||
|
@ -36,6 +36,8 @@ To learn more about this feature, check out the [webhooks documentation](../inte
|
|||||||
|
|
||||||
To learn more about this feature, check out the [NAPALM documentation](../integrations/napalm.md).
|
To learn more about this feature, check out the [NAPALM documentation](../integrations/napalm.md).
|
||||||
|
|
||||||
|
As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions.
|
||||||
|
|
||||||
## Prometheus Metrics
|
## Prometheus Metrics
|
||||||
|
|
||||||
NetBox includes a special `/metrics` view which exposes metrics for a [Prometheus](https://prometheus.io/) scraper, powered by the open source [django-prometheus](https://github.com/korfuri/django-prometheus) library. To learn more about this feature, check out the [Prometheus metrics documentation](../integrations/prometheus-metrics.md).
|
NetBox includes a special `/metrics` view which exposes metrics for a [Prometheus](https://prometheus.io/) scraper, powered by the open source [django-prometheus](https://github.com/korfuri/django-prometheus) library. To learn more about this feature, check out the [Prometheus metrics documentation](../integrations/prometheus-metrics.md).
|
||||||
|
@ -199,14 +199,6 @@ When you have finished modifying the configuration, remember to save the file.
|
|||||||
|
|
||||||
All Python packages required by NetBox are listed in `requirements.txt` and will be installed automatically. NetBox also supports some optional packages. If desired, these packages must be listed in `local_requirements.txt` within the NetBox root directory.
|
All Python packages required by NetBox are listed in `requirements.txt` and will be installed automatically. NetBox also supports some optional packages. If desired, these packages must be listed in `local_requirements.txt` within the NetBox root directory.
|
||||||
|
|
||||||
### NAPALM
|
|
||||||
|
|
||||||
Integration with the [NAPALM automation](../integrations/napalm.md) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device.
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remote File Storage
|
### Remote File Storage
|
||||||
|
|
||||||
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storage_backend) in `configuration.py`.
|
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storage_backend) in `configuration.py`.
|
||||||
|
@ -1,74 +1,3 @@
|
|||||||
# NAPALM
|
# NAPALM
|
||||||
|
|
||||||
NetBox supports integration with the [NAPALM automation](https://github.com/napalm-automation/napalm) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.
|
As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions. **Note:** All previously entered NAPALM configuration data will be saved and automatically imported by the new plugin.
|
||||||
|
|
||||||
The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met:
|
|
||||||
|
|
||||||
* Device status is "Active"
|
|
||||||
* A primary IP has been assigned to the device
|
|
||||||
* A platform with a NAPALM driver has been assigned
|
|
||||||
* The authenticated user has the `dcim.napalm_read_device` permission
|
|
||||||
|
|
||||||
!!! note
|
|
||||||
To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information.
|
|
||||||
|
|
||||||
Below is an example REST API request and response:
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
GET /api/dcim/devices/1/napalm/?method=get_environment
|
|
||||||
|
|
||||||
{
|
|
||||||
"get_environment": {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! note
|
|
||||||
To make NAPALM requests via the NetBox REST API, a NetBox user must have assigned a permission granting the `napalm_read` action for the device object type.
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
By default, the [`NAPALM_USERNAME`](../configuration/napalm.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/napalm.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
|
|
||||||
-H "Authorization: Token $TOKEN" \
|
|
||||||
-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. Because there is no granular mechanism in place for limiting potentially disruptive requests, NetBox supports only read-only [get](https://napalm.readthedocs.io/en/latest/support/index.html#getters-support-matrix) methods.
|
|
||||||
|
|
||||||
## Multiple Methods
|
|
||||||
|
|
||||||
It is possible to request the output of multiple NAPALM methods in a single API request by passing multiple `method` parameters. For example:
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
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 example, 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 $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Accept: application/json; indent=4" \
|
|
||||||
-H "X-NAPALM-port: 2222"
|
|
||||||
```
|
|
||||||
|
@ -51,7 +51,6 @@ class DataSourceForm(NetBoxModelForm):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Determine the selected backend type
|
# Determine the selected backend type
|
||||||
backend_type = get_field_value(self, 'type')
|
backend_type = get_field_value(self, 'type')
|
||||||
backend = registry['data_backends'].get(backend_type)
|
backend = registry['data_backends'].get(backend_type)
|
||||||
|
@ -419,124 +419,6 @@ class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
|||||||
|
|
||||||
return serializers.DeviceWithConfigContextSerializer
|
return serializers.DeviceWithConfigContextSerializer
|
||||||
|
|
||||||
@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')
|
|
||||||
def napalm(self, request, pk):
|
|
||||||
"""
|
|
||||||
Execute a NAPALM method on a Device
|
|
||||||
"""
|
|
||||||
device = get_object_or_404(self.queryset, 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(f"No NAPALM driver is configured for this device's platform: {device.platform}.")
|
|
||||||
|
|
||||||
# Check for primary IP address from NetBox object
|
|
||||||
if device.primary_ip:
|
|
||||||
host = str(device.primary_ip.address.ip)
|
|
||||||
else:
|
|
||||||
# Raise exception for no IP address and no Name if device.name does not exist
|
|
||||||
if not device.name:
|
|
||||||
raise ServiceUnavailable(
|
|
||||||
"This device does not have a primary IP address or device name to lookup configured."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
# Attempt to complete a DNS name resolution if no primary_ip is set
|
|
||||||
host = socket.gethostbyname(device.name)
|
|
||||||
except socket.gaierror:
|
|
||||||
# Name lookup failure
|
|
||||||
raise ServiceUnavailable(
|
|
||||||
f"Name lookup failure, unable to resolve IP address for {device.name}. Please set Primary IP or "
|
|
||||||
f"setup name resolution.")
|
|
||||||
|
|
||||||
# Check that NAPALM is installed
|
|
||||||
try:
|
|
||||||
import napalm
|
|
||||||
from napalm.base.exceptions import ModuleImportError
|
|
||||||
except ModuleNotFoundError as e:
|
|
||||||
if getattr(e, 'name') == 'napalm':
|
|
||||||
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# Validate the configured driver
|
|
||||||
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
|
|
||||||
))
|
|
||||||
|
|
||||||
# Verify user permission
|
|
||||||
if not request.user.has_perm('dcim.napalm_read_device'):
|
|
||||||
return HttpResponseForbidden()
|
|
||||||
|
|
||||||
napalm_methods = request.GET.getlist('method')
|
|
||||||
response = {m: None for m in napalm_methods}
|
|
||||||
|
|
||||||
config = get_config()
|
|
||||||
username = config.NAPALM_USERNAME
|
|
||||||
password = config.NAPALM_PASSWORD
|
|
||||||
timeout = config.NAPALM_TIMEOUT
|
|
||||||
optional_args = config.NAPALM_ARGS.copy()
|
|
||||||
if device.platform.napalm_args is not None:
|
|
||||||
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]
|
|
||||||
|
|
||||||
# Connect to the device
|
|
||||||
d = driver(
|
|
||||||
hostname=host,
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
timeout=timeout,
|
|
||||||
optional_args=optional_args
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
d.open()
|
|
||||||
except Exception as e:
|
|
||||||
raise ServiceUnavailable("Error connecting to the device at {}: {}".format(host, e))
|
|
||||||
|
|
||||||
# Validate and execute each specified NAPALM method
|
|
||||||
for method in napalm_methods:
|
|
||||||
if not hasattr(driver, method):
|
|
||||||
response[method] = {'error': 'Unknown NAPALM method'}
|
|
||||||
continue
|
|
||||||
if not method.startswith('get_'):
|
|
||||||
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
response[method] = getattr(d, method)()
|
|
||||||
except NotImplementedError:
|
|
||||||
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
|
|
||||||
except Exception as e:
|
|
||||||
response[method] = {'error': 'Method {} failed: {}'.format(method, e)}
|
|
||||||
d.close()
|
|
||||||
|
|
||||||
return Response(response)
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
|
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
|
||||||
queryset = VirtualDeviceContext.objects.prefetch_related(
|
queryset = VirtualDeviceContext.objects.prefetch_related(
|
||||||
|
@ -806,7 +806,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
|
fields = ['id', 'name', 'slug', 'description']
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
||||||
|
@ -476,10 +476,6 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
napalm_driver = forms.CharField(
|
|
||||||
max_length=50,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
config_template = DynamicModelChoiceField(
|
config_template = DynamicModelChoiceField(
|
||||||
queryset=ConfigTemplate.objects.all(),
|
queryset=ConfigTemplate.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
@ -491,9 +487,9 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
model = Platform
|
model = Platform
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('manufacturer', 'config_template', 'napalm_driver', 'description')),
|
(None, ('manufacturer', 'config_template', 'description')),
|
||||||
)
|
)
|
||||||
nullable_fields = ('manufacturer', 'config_template', 'napalm_driver', 'description')
|
nullable_fields = ('manufacturer', 'config_template', 'description')
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
|
@ -342,7 +342,7 @@ class PlatformImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -451,19 +451,15 @@ class PlatformForm(NetBoxModelForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Platform', (
|
('Platform', (
|
||||||
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
|
||||||
|
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
|
'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
|
||||||
'napalm_args': forms.Textarea(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceForm(TenancyForm, NetBoxModelForm):
|
class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||||
|
@ -172,7 +172,6 @@ class PlatformIndex(SearchIndex):
|
|||||||
fields = (
|
fields = (
|
||||||
('name', 100),
|
('name', 100),
|
||||||
('slug', 110),
|
('slug', 110),
|
||||||
('napalm_driver', 300),
|
|
||||||
('description', 500),
|
('description', 500),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -133,11 +133,11 @@ class PlatformTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = models.Platform
|
model = models.Platform
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'napalm_driver',
|
'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'description',
|
||||||
'napalm_args', 'description', 'tags', 'actions', 'created', 'last_updated',
|
'tags', 'actions', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description',
|
'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'description',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1469,9 +1469,9 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Manufacturer.objects.bulk_create(manufacturers)
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
platforms = (
|
platforms = (
|
||||||
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], napalm_driver='driver-1', description='A'),
|
Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], description='A'),
|
||||||
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], napalm_driver='driver-2', description='B'),
|
Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], description='B'),
|
||||||
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], napalm_driver='driver-3', description='C'),
|
Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='C'),
|
||||||
)
|
)
|
||||||
Platform.objects.bulk_create(platforms)
|
Platform.objects.bulk_create(platforms)
|
||||||
|
|
||||||
@ -1487,10 +1487,6 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'description': ['A', 'B']}
|
params = {'description': ['A', 'B']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_napalm_driver(self):
|
|
||||||
params = {'napalm_driver': ['driver-1', 'driver-2']}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
|
|
||||||
def test_manufacturer(self):
|
def test_manufacturer(self):
|
||||||
manufacturers = Manufacturer.objects.all()[:2]
|
manufacturers = Manufacturer.objects.all()[:2]
|
||||||
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]}
|
||||||
|
@ -1591,8 +1591,6 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
'name': 'Platform X',
|
'name': 'Platform X',
|
||||||
'slug': 'platform-x',
|
'slug': 'platform-x',
|
||||||
'manufacturer': manufacturer.pk,
|
'manufacturer': manufacturer.pk,
|
||||||
'napalm_driver': 'junos',
|
|
||||||
'napalm_args': None,
|
|
||||||
'description': 'A new platform',
|
'description': 'A new platform',
|
||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
@ -1612,7 +1610,6 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'napalm_driver': 'ios',
|
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2080,71 +2080,6 @@ class DeviceBulkRenameView(generic.BulkRenameView):
|
|||||||
table = tables.DeviceTable
|
table = tables.DeviceTable
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Device NAPALM views
|
|
||||||
#
|
|
||||||
|
|
||||||
class NAPALMViewTab(ViewTab):
|
|
||||||
|
|
||||||
def render(self, instance):
|
|
||||||
# Display NAPALM tabs only for devices which meet certain requirements
|
|
||||||
if not (
|
|
||||||
instance.status == 'active' and
|
|
||||||
instance.primary_ip and
|
|
||||||
instance.platform and
|
|
||||||
instance.platform.napalm_driver
|
|
||||||
):
|
|
||||||
return None
|
|
||||||
return super().render(instance)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Device, 'status')
|
|
||||||
class DeviceStatusView(generic.ObjectView):
|
|
||||||
additional_permissions = ['dcim.napalm_read_device']
|
|
||||||
queryset = Device.objects.all()
|
|
||||||
template_name = 'dcim/device/status.html'
|
|
||||||
tab = NAPALMViewTab(
|
|
||||||
label=_('Status'),
|
|
||||||
permission='dcim.napalm_read_device',
|
|
||||||
weight=3000
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Device, 'lldp_neighbors', path='lldp-neighbors')
|
|
||||||
class DeviceLLDPNeighborsView(generic.ObjectView):
|
|
||||||
additional_permissions = ['dcim.napalm_read_device']
|
|
||||||
queryset = Device.objects.all()
|
|
||||||
template_name = 'dcim/device/lldp_neighbors.html'
|
|
||||||
tab = NAPALMViewTab(
|
|
||||||
label=_('LLDP Neighbors'),
|
|
||||||
permission='dcim.napalm_read_device',
|
|
||||||
weight=3100
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
|
||||||
interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
|
|
||||||
'_path'
|
|
||||||
).exclude(
|
|
||||||
type__in=NONCONNECTABLE_IFACE_TYPES
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'interfaces': interfaces,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Device, 'config')
|
|
||||||
class DeviceConfigView(generic.ObjectView):
|
|
||||||
additional_permissions = ['dcim.napalm_read_device']
|
|
||||||
queryset = Device.objects.all()
|
|
||||||
template_name = 'dcim/device/config.html'
|
|
||||||
tab = NAPALMViewTab(
|
|
||||||
label=_('Config'),
|
|
||||||
permission='dcim.napalm_read_device',
|
|
||||||
weight=3200
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Modules
|
# Modules
|
||||||
#
|
#
|
||||||
|
@ -35,10 +35,6 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
|
|||||||
'fields': ('CUSTOM_VALIDATORS',),
|
'fields': ('CUSTOM_VALIDATORS',),
|
||||||
'classes': ('monospace',),
|
'classes': ('monospace',),
|
||||||
}),
|
}),
|
||||||
('NAPALM', {
|
|
||||||
'fields': ('NAPALM_USERNAME', 'NAPALM_PASSWORD', 'NAPALM_TIMEOUT', 'NAPALM_ARGS'),
|
|
||||||
'classes': ('monospace',),
|
|
||||||
}),
|
|
||||||
('User Preferences', {
|
('User Preferences', {
|
||||||
'fields': ('DEFAULT_USER_PREFERENCES',),
|
'fields': ('DEFAULT_USER_PREFERENCES',),
|
||||||
}),
|
}),
|
||||||
|
@ -40,9 +40,6 @@ async function bundleGraphIQL() {
|
|||||||
async function bundleNetBox() {
|
async function bundleNetBox() {
|
||||||
const entryPoints = {
|
const entryPoints = {
|
||||||
netbox: 'src/index.ts',
|
netbox: 'src/index.ts',
|
||||||
lldp: 'src/device/lldp.ts',
|
|
||||||
config: 'src/device/config.ts',
|
|
||||||
status: 'src/device/status.ts',
|
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const result = await esbuild.build({
|
const result = await esbuild.build({
|
||||||
|
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -1,50 +0,0 @@
|
|||||||
import { createToast } from '../bs';
|
|
||||||
import { apiGetBase, getNetboxData, hasError, toggleLoader } from '../util';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize device config elements.
|
|
||||||
*/
|
|
||||||
function initConfig(): void {
|
|
||||||
toggleLoader('show');
|
|
||||||
const url = getNetboxData('data-object-url');
|
|
||||||
|
|
||||||
if (url !== null) {
|
|
||||||
apiGetBase<DeviceConfig>(url)
|
|
||||||
.then(data => {
|
|
||||||
if (hasError(data)) {
|
|
||||||
createToast('danger', 'Error Fetching Device Config', data.error).show();
|
|
||||||
console.error(data.error);
|
|
||||||
return;
|
|
||||||
} else if (hasError<Required<DeviceConfig['get_config']>>(data.get_config)) {
|
|
||||||
createToast('danger', 'Error Fetching Device Config', data.get_config.error).show();
|
|
||||||
console.error(data.get_config.error);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const configTypes = ['running', 'startup', 'candidate'] as DeviceConfigType[];
|
|
||||||
|
|
||||||
for (const configType of configTypes) {
|
|
||||||
const element = document.getElementById(`${configType}_config`);
|
|
||||||
if (element !== null) {
|
|
||||||
const config = data.get_config[configType];
|
|
||||||
if (typeof config === 'string') {
|
|
||||||
// If the returned config is a string, set the element innerHTML as-is.
|
|
||||||
element.innerHTML = config;
|
|
||||||
} else {
|
|
||||||
// If the returned config is an object (dict), convert it to JSON.
|
|
||||||
element.innerHTML = JSON.stringify(data.get_config[configType], null, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
toggleLoader('hide');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState !== 'loading') {
|
|
||||||
initConfig();
|
|
||||||
} else {
|
|
||||||
document.addEventListener('DOMContentLoaded', initConfig);
|
|
||||||
}
|
|
@ -1,143 +0,0 @@
|
|||||||
import { createToast } from '../bs';
|
|
||||||
import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util';
|
|
||||||
|
|
||||||
// Match an interface name that begins with a capital letter and is followed by at least one other
|
|
||||||
// alphabetic character, and ends with a forward-slash-separated numeric sequence such as 0/1/2.
|
|
||||||
const CISCO_IOS_PATTERN = new RegExp(/^([A-Z][A-Za-z]+)[^0-9]*([0-9/]+)$/);
|
|
||||||
|
|
||||||
// Mapping of overrides to default Cisco IOS interface alias behavior (default behavior is to use
|
|
||||||
// the first two characters).
|
|
||||||
const CISCO_IOS_OVERRIDES = new Map<string, string>([
|
|
||||||
// Cisco IOS abbreviates 25G (TwentyFiveGigE) interfaces as 'Twe'.
|
|
||||||
['TwentyFiveGigE', 'Twe'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an attribute from a row's cell.
|
|
||||||
*
|
|
||||||
* @param row Interface row
|
|
||||||
* @param query CSS media query
|
|
||||||
* @param attr Cell attribute
|
|
||||||
*/
|
|
||||||
function getData(row: HTMLTableRowElement, query: string, attr: string): string | null {
|
|
||||||
return row.querySelector(query)?.getAttribute(attr) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get preconfigured alias for given interface. Primarily for matching long-form Cisco IOS
|
|
||||||
* interface names with short-form Cisco IOS interface names. For example, `GigabitEthernet0/1/2`
|
|
||||||
* would become `Gi0/1/2`.
|
|
||||||
*
|
|
||||||
* This should probably be replaced with something in the primary application (Django), such as
|
|
||||||
* a database field attached to given interface types. However, this is a temporary measure to
|
|
||||||
* replace the functionality of this one-liner:
|
|
||||||
*
|
|
||||||
* @see https://github.com/netbox-community/netbox/blob/9cc4992fad2fe04ef0211d998c517414e8871d8c/netbox/templates/dcim/device/lldp_neighbors.html#L69
|
|
||||||
*
|
|
||||||
* @param name Long-form/original interface name.
|
|
||||||
*/
|
|
||||||
function getInterfaceAlias(name: string | null): string | null {
|
|
||||||
if (name === null) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
if (name.match(CISCO_IOS_PATTERN)) {
|
|
||||||
// Extract the base name and numeric portions of the interface. For example, an input interface
|
|
||||||
// of `GigabitEthernet0/0/1` would result in an array of `['GigabitEthernet', '0/0/1']`.
|
|
||||||
const [base, numeric] = (name.match(CISCO_IOS_PATTERN) ?? []).slice(1, 3);
|
|
||||||
|
|
||||||
if (isTruthy(base) && isTruthy(numeric)) {
|
|
||||||
// Check the override map and use its value if the base name is present in the map.
|
|
||||||
// Otherwise, use the first two characters of the base name. For example,
|
|
||||||
// `GigabitEthernet0/0/1` would become `Gi0/0/1`, but `TwentyFiveGigE0/0/1` would become
|
|
||||||
// `Twe0/0/1`.
|
|
||||||
const aliasBase = CISCO_IOS_OVERRIDES.get(base) || base.slice(0, 2);
|
|
||||||
return `${aliasBase}${numeric}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update row styles based on LLDP neighbor data.
|
|
||||||
*/
|
|
||||||
function updateRowStyle(data: LLDPNeighborDetail) {
|
|
||||||
for (const [fullIface, neighbors] of Object.entries(data.get_lldp_neighbors_detail)) {
|
|
||||||
const [iface] = fullIface.split('.');
|
|
||||||
|
|
||||||
const row = document.getElementById(iface) as Nullable<HTMLTableRowElement>;
|
|
||||||
|
|
||||||
if (row !== null) {
|
|
||||||
for (const neighbor of neighbors) {
|
|
||||||
const deviceCell = row.querySelector<HTMLTableCellElement>('td.device');
|
|
||||||
const interfaceCell = row.querySelector<HTMLTableCellElement>('td.interface');
|
|
||||||
const configuredDevice = getData(row, 'td.configured_device', 'data');
|
|
||||||
const configuredChassis = getData(row, 'td.configured_chassis', 'data-chassis');
|
|
||||||
const configuredIface = getData(row, 'td.configured_interface', 'data');
|
|
||||||
|
|
||||||
const interfaceAlias = getInterfaceAlias(configuredIface);
|
|
||||||
|
|
||||||
const remoteName = neighbor.remote_system_name ?? '';
|
|
||||||
const remotePort = neighbor.remote_port ?? '';
|
|
||||||
const [neighborDevice] = remoteName.split('.');
|
|
||||||
const [neighborIface] = remotePort.split('.');
|
|
||||||
|
|
||||||
if (deviceCell !== null) {
|
|
||||||
deviceCell.innerText = neighborDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interfaceCell !== null) {
|
|
||||||
interfaceCell.innerText = neighborIface;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface has an LLDP neighbor, but the neighbor is not configured in NetBox.
|
|
||||||
const nonConfiguredDevice = !isTruthy(configuredDevice) && isTruthy(neighborDevice);
|
|
||||||
|
|
||||||
// NetBox device or chassis matches LLDP neighbor.
|
|
||||||
const validNode =
|
|
||||||
configuredDevice === neighborDevice || configuredChassis === neighborDevice;
|
|
||||||
|
|
||||||
// NetBox configured interface matches LLDP neighbor interface.
|
|
||||||
const validInterface =
|
|
||||||
configuredIface === neighborIface || interfaceAlias === neighborIface;
|
|
||||||
|
|
||||||
if (nonConfiguredDevice) {
|
|
||||||
row.classList.add('info');
|
|
||||||
} else if (validNode && validInterface) {
|
|
||||||
row.classList.add('success');
|
|
||||||
} else {
|
|
||||||
row.classList.add('danger');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize LLDP Neighbor fetching.
|
|
||||||
*/
|
|
||||||
function initLldpNeighbors() {
|
|
||||||
toggleLoader('show');
|
|
||||||
const url = getNetboxData('object-url');
|
|
||||||
if (url !== null) {
|
|
||||||
apiGetBase<LLDPNeighborDetail>(url)
|
|
||||||
.then(data => {
|
|
||||||
if (hasError(data)) {
|
|
||||||
createToast('danger', 'Error Retrieving LLDP Neighbor Information', data.error).show();
|
|
||||||
toggleLoader('hide');
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
updateRowStyle(data);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
toggleLoader('hide');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState !== 'loading') {
|
|
||||||
initLldpNeighbors();
|
|
||||||
} else {
|
|
||||||
document.addEventListener('DOMContentLoaded', initLldpNeighbors);
|
|
||||||
}
|
|
@ -1,379 +0,0 @@
|
|||||||
import dayjs from 'dayjs';
|
|
||||||
import utc from 'dayjs/plugin/utc';
|
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
|
||||||
import duration from 'dayjs/plugin/duration';
|
|
||||||
import advancedFormat from 'dayjs/plugin/advancedFormat';
|
|
||||||
|
|
||||||
import { createToast } from '../bs';
|
|
||||||
import { apiGetBase, getNetboxData, hasError, toggleLoader, createElement, cToF } from '../util';
|
|
||||||
|
|
||||||
type Uptime = {
|
|
||||||
utc: string;
|
|
||||||
zoned: string | null;
|
|
||||||
duration: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
dayjs.extend(utc);
|
|
||||||
dayjs.extend(timezone);
|
|
||||||
dayjs.extend(advancedFormat);
|
|
||||||
dayjs.extend(duration);
|
|
||||||
|
|
||||||
const factKeys = [
|
|
||||||
'hostname',
|
|
||||||
'fqdn',
|
|
||||||
'vendor',
|
|
||||||
'model',
|
|
||||||
'serial_number',
|
|
||||||
'os_version',
|
|
||||||
] as (keyof DeviceFacts)[];
|
|
||||||
|
|
||||||
type DurationKeys = 'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds';
|
|
||||||
const formatKeys = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'] as DurationKeys[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* From a number of seconds that have elapsed since reboot, extract human-readable dates in the
|
|
||||||
* following formats:
|
|
||||||
* - Relative time since reboot (e.g. 1 month, 28 days, 1 hour, 30 seconds).
|
|
||||||
* - Time stamp in browser-relative timezone.
|
|
||||||
* - Time stamp in UTC.
|
|
||||||
* @param seconds Seconds since reboot.
|
|
||||||
*/
|
|
||||||
function getUptime(seconds: number): Uptime {
|
|
||||||
const relDate = new Date();
|
|
||||||
|
|
||||||
// Get the user's UTC offset, to determine if the user is in UTC or not.
|
|
||||||
const offset = relDate.getTimezoneOffset();
|
|
||||||
const relNow = dayjs(relDate);
|
|
||||||
|
|
||||||
// Get a dayjs object for the device reboot time (now - number of seconds).
|
|
||||||
const relThen = relNow.subtract(seconds, 'seconds');
|
|
||||||
|
|
||||||
// Get a human-readable version of the time in UTC.
|
|
||||||
const utc = relThen.tz('Etc/UTC').format('YYYY-MM-DD HH:MM:ss z');
|
|
||||||
|
|
||||||
// We only want to show the UTC time if the user is not already in UTC time.
|
|
||||||
let zoned = null;
|
|
||||||
if (offset !== 0) {
|
|
||||||
// If the user is not in UTC time, return a human-readable version in the user's timezone.
|
|
||||||
zoned = relThen.format('YYYY-MM-DD HH:MM:ss z');
|
|
||||||
}
|
|
||||||
// Get a dayjs duration object to create a human-readable relative time string.
|
|
||||||
const between = dayjs.duration(seconds, 'seconds');
|
|
||||||
|
|
||||||
// Array of all non-zero-value duration properties. For example, if duration.year() is 0, we
|
|
||||||
// don't care about it and shouldn't show it to the user.
|
|
||||||
let parts = [] as string[];
|
|
||||||
for (const key of formatKeys) {
|
|
||||||
// Get the property value. For example, duration.year(), duration.month(), etc.
|
|
||||||
const value = between[key]();
|
|
||||||
if (value === 1) {
|
|
||||||
// If the duration for this key is 1, drop the trailing 's'. For example, '1 seconds' would
|
|
||||||
// become '1 second'.
|
|
||||||
const label = key.replace(/s$/, '');
|
|
||||||
parts = [...parts, `${value} ${label}`];
|
|
||||||
} else if (value > 1) {
|
|
||||||
// If the duration for this key is more than one, add it to the array as-is.
|
|
||||||
parts = [...parts, `${value} ${key}`];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Set the duration to something safe, so we don't show 'undefined' or an empty string to the user.
|
|
||||||
let duration = 'None';
|
|
||||||
if (parts.length > 0) {
|
|
||||||
// If the array actually has elements, reassign the duration to a human-readable version.
|
|
||||||
duration = parts.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { utc, zoned, duration };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After the `get_facts` result is received, parse its content and update HTML elements
|
|
||||||
* accordingly.
|
|
||||||
*
|
|
||||||
* @param facts NAPALM Device Facts
|
|
||||||
*/
|
|
||||||
function processFacts(facts: DeviceFacts): void {
|
|
||||||
for (const key of factKeys) {
|
|
||||||
if (key in facts) {
|
|
||||||
// Find the target element which should have its innerHTML/innerText set to a NAPALM value.
|
|
||||||
const element = document.getElementById(key);
|
|
||||||
if (element !== null) {
|
|
||||||
element.innerHTML = String(facts[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { uptime } = facts;
|
|
||||||
const { utc, zoned, duration } = getUptime(uptime);
|
|
||||||
|
|
||||||
// Find the duration (relative time) element and set its value.
|
|
||||||
const uptimeDurationElement = document.getElementById('uptime-duration');
|
|
||||||
if (uptimeDurationElement !== null) {
|
|
||||||
uptimeDurationElement.innerHTML = duration;
|
|
||||||
}
|
|
||||||
// Find the time stamp element and set its value.
|
|
||||||
const uptimeElement = document.getElementById('uptime');
|
|
||||||
if (uptimeElement !== null) {
|
|
||||||
if (zoned === null) {
|
|
||||||
// If the user is in UTC time, only add the UTC time stamp.
|
|
||||||
uptimeElement.innerHTML = utc;
|
|
||||||
} else {
|
|
||||||
// Otherwise, add both time stamps.
|
|
||||||
uptimeElement.innerHTML = [zoned, `<span class="fst-italic d-block">${utc}</span>`].join('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a title row before the next table row. The title row describes each environment key/value
|
|
||||||
* pair from the NAPALM response.
|
|
||||||
*
|
|
||||||
* @param next Next adjacent element. For example, if this is the CPU data, `next` would be the
|
|
||||||
* memory row.
|
|
||||||
* @param title1 Column 1 Title
|
|
||||||
* @param title2 Column 2 Title
|
|
||||||
*/
|
|
||||||
function insertTitleRow<E extends HTMLElement>(next: E, title1: string, title2: string): void {
|
|
||||||
// Create cell element that contains the key title.
|
|
||||||
const col1Title = createElement('th', { innerText: title1 }, ['border-end', 'text-end']);
|
|
||||||
// Create cell element that contains the value title.
|
|
||||||
const col2Title = createElement('th', { innerText: title2 }, ['border-start', 'text-start']);
|
|
||||||
// Create title row element with the two header cells as children.
|
|
||||||
const titleRow = createElement('tr', {}, [], [col1Title, col2Title]);
|
|
||||||
// Insert the entire row just before the beginning of the next row (i.e., at the end of this row).
|
|
||||||
next.insertAdjacentElement('beforebegin', titleRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a "No Data" row, for when the NAPALM response doesn't contain this type of data.
|
|
||||||
*
|
|
||||||
* @param next Next adjacent element.For example, if this is the CPU data, `next` would be the
|
|
||||||
* memory row.
|
|
||||||
*/
|
|
||||||
function insertNoneRow<E extends Nullable<HTMLElement>>(next: E): void {
|
|
||||||
const none = createElement('td', { colSpan: '2', innerText: 'No Data' }, [
|
|
||||||
'text-muted',
|
|
||||||
'text-center',
|
|
||||||
]);
|
|
||||||
const titleRow = createElement('tr', {}, [], [none]);
|
|
||||||
if (next !== null) {
|
|
||||||
next.insertAdjacentElement('beforebegin', titleRow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNext<E extends HTMLElement>(id: string): Nullable<E> {
|
|
||||||
const element = document.getElementById(id);
|
|
||||||
if (element !== null) {
|
|
||||||
return element.nextElementSibling as Nullable<E>;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create & insert table rows for each CPU in the NAPALM response.
|
|
||||||
*
|
|
||||||
* @param cpu NAPALM CPU data.
|
|
||||||
*/
|
|
||||||
function processCpu(cpu: DeviceEnvironment['cpu']): void {
|
|
||||||
// Find the next adjacent element, so we can insert elements before it.
|
|
||||||
const next = getNext<HTMLTableRowElement>('status-cpu');
|
|
||||||
if (typeof cpu !== 'undefined') {
|
|
||||||
if (next !== null) {
|
|
||||||
insertTitleRow(next, 'Name', 'Usage');
|
|
||||||
for (const [core, data] of Object.entries(cpu)) {
|
|
||||||
const usage = data['%usage'];
|
|
||||||
const kCell = createElement('td', { innerText: core }, ['border-end', 'text-end']);
|
|
||||||
const vCell = createElement('td', { innerText: `${usage} %` }, [
|
|
||||||
'border-start',
|
|
||||||
'text-start',
|
|
||||||
]);
|
|
||||||
const row = createElement('tr', {}, [], [kCell, vCell]);
|
|
||||||
next.insertAdjacentElement('beforebegin', row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
insertNoneRow(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create & insert table rows for the memory in the NAPALM response.
|
|
||||||
*
|
|
||||||
* @param mem NAPALM memory data.
|
|
||||||
*/
|
|
||||||
function processMemory(mem: DeviceEnvironment['memory']): void {
|
|
||||||
// Find the next adjacent element, so we can insert elements before it.
|
|
||||||
const next = getNext<HTMLTableRowElement>('status-memory');
|
|
||||||
if (typeof mem !== 'undefined') {
|
|
||||||
if (next !== null) {
|
|
||||||
insertTitleRow(next, 'Available', 'Used');
|
|
||||||
const { available_ram: avail, used_ram: used } = mem;
|
|
||||||
const aCell = createElement('td', { innerText: avail }, ['border-end', 'text-end']);
|
|
||||||
const uCell = createElement('td', { innerText: used }, ['border-start', 'text-start']);
|
|
||||||
const row = createElement('tr', {}, [], [aCell, uCell]);
|
|
||||||
next.insertAdjacentElement('beforebegin', row);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
insertNoneRow(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create & insert table rows for each temperature sensor in the NAPALM response.
|
|
||||||
*
|
|
||||||
* @param temp NAPALM temperature data.
|
|
||||||
*/
|
|
||||||
function processTemp(temp: DeviceEnvironment['temperature']): void {
|
|
||||||
// Find the next adjacent element, so we can insert elements before it.
|
|
||||||
const next = getNext<HTMLTableRowElement>('status-temperature');
|
|
||||||
if (typeof temp !== 'undefined') {
|
|
||||||
if (next !== null) {
|
|
||||||
insertTitleRow(next, 'Sensor', 'Value');
|
|
||||||
for (const [sensor, data] of Object.entries(temp)) {
|
|
||||||
const tempC = data.temperature;
|
|
||||||
const tempF = cToF(tempC);
|
|
||||||
const innerHTML = `${tempC} °C <span class="ms-1 text-muted small">${tempF} °F</span>`;
|
|
||||||
const status = data.is_alert ? 'warning' : data.is_critical ? 'danger' : 'success';
|
|
||||||
const kCell = createElement('td', { innerText: sensor }, ['border-end', 'text-end']);
|
|
||||||
const vCell = createElement('td', { innerHTML }, ['border-start', 'text-start']);
|
|
||||||
const row = createElement('tr', {}, [`table-${status}`], [kCell, vCell]);
|
|
||||||
next.insertAdjacentElement('beforebegin', row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
insertNoneRow(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create & insert table rows for each fan in the NAPALM response.
|
|
||||||
*
|
|
||||||
* @param fans NAPALM fan data.
|
|
||||||
*/
|
|
||||||
function processFans(fans: DeviceEnvironment['fans']): void {
|
|
||||||
// Find the next adjacent element, so we can insert elements before it.
|
|
||||||
const next = getNext<HTMLTableRowElement>('status-fans');
|
|
||||||
if (typeof fans !== 'undefined') {
|
|
||||||
if (next !== null) {
|
|
||||||
insertTitleRow(next, 'Fan', 'Status');
|
|
||||||
for (const [fan, data] of Object.entries(fans)) {
|
|
||||||
const { status } = data;
|
|
||||||
const goodIcon = createElement('i', {}, ['mdi', 'mdi-check-bold', 'text-success']);
|
|
||||||
const badIcon = createElement('i', {}, ['mdi', 'mdi-close', 'text-warning']);
|
|
||||||
const kCell = createElement('td', { innerText: fan }, ['border-end', 'text-end']);
|
|
||||||
const vCell = createElement(
|
|
||||||
'td',
|
|
||||||
{},
|
|
||||||
['border-start', 'text-start'],
|
|
||||||
[status ? goodIcon : badIcon],
|
|
||||||
);
|
|
||||||
const row = createElement(
|
|
||||||
'tr',
|
|
||||||
{},
|
|
||||||
[`table-${status ? 'success' : 'warning'}`],
|
|
||||||
[kCell, vCell],
|
|
||||||
);
|
|
||||||
next.insertAdjacentElement('beforebegin', row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
insertNoneRow(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create & insert table rows for each PSU in the NAPALM response.
|
|
||||||
*
|
|
||||||
* @param power NAPALM power data.
|
|
||||||
*/
|
|
||||||
function processPower(power: DeviceEnvironment['power']): void {
|
|
||||||
// Find the next adjacent element, so we can insert elements before it.
|
|
||||||
const next = getNext<HTMLTableRowElement>('status-power');
|
|
||||||
if (typeof power !== 'undefined') {
|
|
||||||
if (next !== null) {
|
|
||||||
insertTitleRow(next, 'PSU', 'Status');
|
|
||||||
for (const [psu, data] of Object.entries(power)) {
|
|
||||||
const { status } = data;
|
|
||||||
const goodIcon = createElement('i', {}, ['mdi', 'mdi-check-bold', 'text-success']);
|
|
||||||
const badIcon = createElement('i', {}, ['mdi', 'mdi-close', 'text-warning']);
|
|
||||||
const kCell = createElement('td', { innerText: psu }, ['border-end', 'text-end']);
|
|
||||||
const vCell = createElement(
|
|
||||||
'td',
|
|
||||||
{},
|
|
||||||
['border-start', 'text-start'],
|
|
||||||
[status ? goodIcon : badIcon],
|
|
||||||
);
|
|
||||||
const row = createElement(
|
|
||||||
'tr',
|
|
||||||
{},
|
|
||||||
[`table-${status ? 'success' : 'warning'}`],
|
|
||||||
[kCell, vCell],
|
|
||||||
);
|
|
||||||
next.insertAdjacentElement('beforebegin', row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
insertNoneRow(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After the `get_environment` result is received, parse its content and update HTML elements
|
|
||||||
* accordingly.
|
|
||||||
*
|
|
||||||
* @param env NAPALM Device Environment
|
|
||||||
*/
|
|
||||||
function processEnvironment(env: DeviceEnvironment): void {
|
|
||||||
const { cpu, memory, temperature, fans, power } = env;
|
|
||||||
processCpu(cpu);
|
|
||||||
processMemory(memory);
|
|
||||||
processTemp(temperature);
|
|
||||||
processFans(fans);
|
|
||||||
processPower(power);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize NAPALM device status handlers.
|
|
||||||
*/
|
|
||||||
function initStatus(): void {
|
|
||||||
// Show loading state for both Facts & Environment cards.
|
|
||||||
toggleLoader('show');
|
|
||||||
|
|
||||||
const url = getNetboxData('data-object-url');
|
|
||||||
|
|
||||||
if (url !== null) {
|
|
||||||
apiGetBase<DeviceStatus>(url)
|
|
||||||
.then(data => {
|
|
||||||
if (hasError(data)) {
|
|
||||||
// If the API returns an error, show it to the user.
|
|
||||||
createToast('danger', 'Error Fetching Device Status', data.error).show();
|
|
||||||
} else {
|
|
||||||
if (!hasError(data.get_facts)) {
|
|
||||||
processFacts(data.get_facts);
|
|
||||||
} else {
|
|
||||||
// If the device facts data contains an error, show it to the user.
|
|
||||||
createToast('danger', 'Error Fetching Device Facts', data.get_facts.error).show();
|
|
||||||
}
|
|
||||||
if (!hasError(data.get_environment)) {
|
|
||||||
processEnvironment(data.get_environment);
|
|
||||||
} else {
|
|
||||||
// If the device environment data contains an error, show it to the user.
|
|
||||||
createToast(
|
|
||||||
'danger',
|
|
||||||
'Error Fetching Device Environment Data',
|
|
||||||
data.get_environment.error,
|
|
||||||
).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
.finally(() => toggleLoader('hide'));
|
|
||||||
} else {
|
|
||||||
toggleLoader('hide');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState !== 'loading') {
|
|
||||||
initStatus();
|
|
||||||
} else {
|
|
||||||
document.addEventListener('DOMContentLoaded', initStatus);
|
|
||||||
}
|
|
@ -397,16 +397,6 @@ export function createElement<
|
|||||||
return element as HTMLElementTagNameMap[T];
|
return element as HTMLElementTagNameMap[T];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert Celsius to Fahrenheit, for NAPALM temperature sensors.
|
|
||||||
*
|
|
||||||
* @param celsius Degrees in Celsius.
|
|
||||||
* @returns Degrees in Fahrenheit.
|
|
||||||
*/
|
|
||||||
export function cToF(celsius: number): number {
|
|
||||||
return Math.round((celsius * (9 / 5) + 32 + Number.EPSILON) * 10) / 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deduplicate an array of objects based on the value of a property.
|
* Deduplicate an array of objects based on the value of a property.
|
||||||
*
|
*
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
{% extends 'dcim/device/base.html' %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}{{ object }} - Config{% endblock %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<script type="text/javascript" src="{% static 'config.js' %}" onerror="window.location='{% url 'media_failure' %}?filename=config.js'"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-overlay">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h5 class="card-header">Device Configuration</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
<ul class="nav nav-tabs" role="tablist">
|
|
||||||
<li role="presentation"><a class="nav-link active" href="#running" aria-controls="running" role="tab" data-bs-toggle="tab">Running</a></li>
|
|
||||||
<li role="presentation"><a class="nav-link" href="#startup" aria-controls="startup" role="tab" data-bs-toggle="tab">Startup</a></li>
|
|
||||||
<li role="presentation"><a class="nav-link" href="#candidate" aria-controls="candidate" role="tab" data-bs-toggle="tab">Candidate</a></li>
|
|
||||||
</ul>
|
|
||||||
<div class="tab-content p-3">
|
|
||||||
<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 data %}
|
|
||||||
<span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_config"></span>
|
|
||||||
{% endblock %}
|
|
@ -1,66 +0,0 @@
|
|||||||
{% extends 'dcim/device/base.html' %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}{{ object }} - LLDP Neighbors{% endblock %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<script type="text/javascript" src="{% static 'lldp.js' %}" onerror="window.location='{% url 'media_failure' %}?filename=lldp.js'"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-overlay">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="d-inline">LLDP Neighbors</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<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>
|
|
||||||
{% with peer=iface.connected_endpoints.0 %}
|
|
||||||
{% if peer.device %}
|
|
||||||
<td class="configured_device" data="{{ peer.device.name }}" data-chassis="{{ peer.device.virtual_chassis.name }}">
|
|
||||||
<a href="{% url 'dcim:device' pk=peer.device.pk %}">{{ peer.device }}</a>
|
|
||||||
</td>
|
|
||||||
<td class="configured_interface" data="{{ peer.name }}">
|
|
||||||
<span title="{{ peer.get_type_display }}">{{ peer }}</span>
|
|
||||||
</td>
|
|
||||||
{% elif peer.circuit %}
|
|
||||||
{% with circuit=peer.circuit %}
|
|
||||||
<td colspan="2">
|
|
||||||
<i class="mdi mdi-lightning-bolt" title="Circuit"></i>
|
|
||||||
<a href="{{ circuit.get_absolute_url }}">{{ circuit.provider }} {{ circuit }}</a>
|
|
||||||
</td>
|
|
||||||
{% endwith %}
|
|
||||||
{% else %}
|
|
||||||
<td class="text-muted" colspan="2">None</td>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
<td class="device"></td>
|
|
||||||
<td class="interface"></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block data %}
|
|
||||||
<span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_lldp_neighbors_detail"></span>
|
|
||||||
{% endblock %}
|
|
@ -1,93 +0,0 @@
|
|||||||
{% extends 'dcim/device/base.html' %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}{{ object }} - Status{% endblock %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<script type="text/javascript" src="{% static 'status.js' %}" onerror="window.location='{% url 'media_failure' %}?filename=status.js'"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col col-md-6">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-overlay">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h5 class="card-header">Device Facts</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
<table class="table">
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Hostname</th>
|
|
||||||
<td id="hostname"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">FQDN</th>
|
|
||||||
<td id="fqdn"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Vendor</th>
|
|
||||||
<td id="vendor"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Model</th>
|
|
||||||
<td id="model"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Serial Number</th>
|
|
||||||
<td id="serial_number" class="text-monospace"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">OS Version</th>
|
|
||||||
<td id="os_version"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="align-middle">
|
|
||||||
<th scope="row">Uptime</th>
|
|
||||||
<td>
|
|
||||||
<div id="uptime-duration"></div>
|
|
||||||
<div id="uptime" class="small text-muted"></div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-6">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-overlay">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h5 class="card-header">Environment</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
<table class="table">
|
|
||||||
<tr id="status-cpu">
|
|
||||||
<th colspan="2"><i class="mdi mdi-gauge"></i> CPU</th>
|
|
||||||
</tr>
|
|
||||||
<tr id="status-memory">
|
|
||||||
<th colspan="2"><i class="mdi mdi-chip"></i> Memory</th>
|
|
||||||
</tr>
|
|
||||||
<tr id="status-temperature">
|
|
||||||
<th colspan="2"><i class="mdi mdi-thermometer"></i> Temperature</th>
|
|
||||||
</tr>
|
|
||||||
<tr id="status-fans">
|
|
||||||
<th colspan="2"><i class="mdi mdi-fan"></i> Fans</th>
|
|
||||||
</tr>
|
|
||||||
<tr id="status-power">
|
|
||||||
<th colspan="2"><i class="mdi mdi-power"></i> Power</th>
|
|
||||||
</tr>
|
|
||||||
<tr class="napalm-table-placeholder d-none invisible">
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block data %}
|
|
||||||
<span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_facts&method=get_environment"></span>
|
|
||||||
{% endblock %}
|
|
@ -43,20 +43,10 @@
|
|||||||
<th scope="row">Config Template</th>
|
<th scope="row">Config Template</th>
|
||||||
<td>{{ object.config_template|linkify|placeholder }}</td>
|
<td>{{ object.config_template|linkify|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">NAPALM Driver</th>
|
|
||||||
<td>{{ object.napalm_driver|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
<div class="card">
|
|
||||||
<h5 class="card-header">NAPALM Arguments</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
<pre>{{ object.napalm_args|json }}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
|
Loading…
Reference in New Issue
Block a user