10520 remove all Napalm code references

This commit is contained in:
Arthur 2023-01-31 09:26:59 -08:00
parent c8779a839e
commit 49fd6c5327
22 changed files with 12 additions and 637 deletions

View File

@ -618,7 +618,7 @@ class PlatformSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Platform model = Platform
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'description',
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
] ]
@ -723,10 +723,6 @@ class ModuleSerializer(NetBoxModelSerializer):
] ]
class DeviceNAPALMSerializer(serializers.Serializer):
method = serializers.JSONField()
# #
# Device components # Device components
# #

View File

@ -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(

View File

@ -787,7 +787,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):

View File

@ -479,11 +479,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
)
# TODO: Bulk edit support for napalm_args
description = forms.CharField( description = forms.CharField(
max_length=200, max_length=200,
required=False required=False
@ -491,9 +486,9 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
model = Platform model = Platform
fieldsets = ( fieldsets = (
(None, ('manufacturer', 'napalm_driver', 'description')), (None, ('manufacturer', 'description')),
) )
nullable_fields = ('manufacturer', 'napalm_driver', 'description') nullable_fields = ('manufacturer', 'description')
class DeviceBulkEditForm(NetBoxModelBulkEditForm): class DeviceBulkEditForm(NetBoxModelBulkEditForm):

View File

@ -323,7 +323,7 @@ class PlatformImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = Platform model = Platform
fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags') fields = ('name', 'slug', 'manufacturer', 'description', 'tags')
class BaseDeviceImportForm(NetBoxModelImportForm): class BaseDeviceImportForm(NetBoxModelImportForm):

View File

@ -459,7 +459,7 @@ class PlatformForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
('Platform', ( ('Platform', (
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags', 'name', 'slug', 'manufacturer', 'description', 'tags',
)), )),
) )
@ -467,11 +467,8 @@ class PlatformForm(NetBoxModelForm):
class Meta: class Meta:
model = Platform model = Platform
fields = [ fields = [
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags', 'name', 'slug', 'manufacturer', 'description', 'tags',
] ]
widgets = {
'napalm_args': SmallTextarea(),
}
class DeviceForm(TenancyForm, NetBoxModelForm): class DeviceForm(TenancyForm, NetBoxModelForm):

View File

@ -172,7 +172,6 @@ class PlatformIndex(SearchIndex):
fields = ( fields = (
('name', 100), ('name', 100),
('slug', 110), ('slug', 110),
('napalm_driver', 300),
('description', 500), ('description', 500),
) )

View File

@ -127,11 +127,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', 'napalm_driver', 'napalm_args', 'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug',
'description', 'tags', 'actions', 'created', 'last_updated', 'description', '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',
) )

View File

@ -1455,9 +1455,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)
@ -1473,10 +1473,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]}

View File

@ -1578,8 +1578,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],
} }
@ -1599,7 +1597,6 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {
'napalm_driver': 'ios',
'description': 'New description', 'description': 'New description',
} }

View File

@ -2043,46 +2043,16 @@ 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') @register_model_view(Device, 'status')
class DeviceStatusView(generic.ObjectView): class DeviceStatusView(generic.ObjectView):
additional_permissions = ['dcim.napalm_read_device']
queryset = Device.objects.all() queryset = Device.objects.all()
template_name = 'dcim/device/status.html' 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') @register_model_view(Device, 'lldp_neighbors', path='lldp-neighbors')
class DeviceLLDPNeighborsView(generic.ObjectView): class DeviceLLDPNeighborsView(generic.ObjectView):
additional_permissions = ['dcim.napalm_read_device']
queryset = Device.objects.all() queryset = Device.objects.all()
template_name = 'dcim/device/lldp_neighbors.html' 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): def get_extra_context(self, request, instance):
interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related( interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
@ -2098,14 +2068,8 @@ class DeviceLLDPNeighborsView(generic.ObjectView):
@register_model_view(Device, 'config') @register_model_view(Device, 'config')
class DeviceConfigView(generic.ObjectView): class DeviceConfigView(generic.ObjectView):
additional_permissions = ['dcim.napalm_read_device']
queryset = Device.objects.all() queryset = Device.objects.all()
template_name = 'dcim/device/config.html' template_name = 'dcim/device/config.html'
tab = NAPALMViewTab(
label=_('Config'),
permission='dcim.napalm_read_device',
weight=3200
)
# #

View File

@ -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',),
}), }),

View File

@ -149,39 +149,6 @@ PARAMS = (
}, },
), ),
# NAPALM
ConfigParam(
name='NAPALM_USERNAME',
label=_('NAPALM username'),
default='',
description=_("Username to use when connecting to devices via NAPALM")
),
ConfigParam(
name='NAPALM_PASSWORD',
label=_('NAPALM password'),
default='',
description=_("Password to use when connecting to devices via NAPALM")
),
ConfigParam(
name='NAPALM_TIMEOUT',
label=_('NAPALM timeout'),
default=30,
description=_("NAPALM connection timeout (in seconds)"),
field=forms.IntegerField
),
ConfigParam(
name='NAPALM_ARGS',
label=_('NAPALM arguments'),
default={},
description=_("Additional arguments to pass when invoking a NAPALM driver (as JSON data)"),
field=forms.JSONField,
field_kwargs={
'widget': forms.Textarea(
attrs={'class': 'vLargeTextField'}
),
},
),
# User preferences # User preferences
ConfigParam( ConfigParam(
name='DEFAULT_USER_PREFERENCES', name='DEFAULT_USER_PREFERENCES',

View File

@ -42,7 +42,6 @@ async function bundleNetBox() {
netbox: 'src/index.ts', netbox: 'src/index.ts',
lldp: 'src/device/lldp.ts', lldp: 'src/device/lldp.ts',
config: 'src/device/config.ts', config: 'src/device/config.ts',
status: 'src/device/status.ts',
}; };
try { try {
const result = await esbuild.build({ const result = await esbuild.build({

Binary file not shown.

Binary file not shown.

View File

@ -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);
}

View File

@ -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.
* *

View File

@ -39,7 +39,3 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block data %}
<span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_config"></span>
{% endblock %}

View File

@ -60,7 +60,3 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block data %}
<span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_lldp_neighbors_detail"></span>
{% endblock %}

View File

@ -79,15 +79,9 @@
<tr id="status-power"> <tr id="status-power">
<th colspan="2"><i class="mdi mdi-power"></i> Power</th> <th colspan="2"><i class="mdi mdi-power"></i> Power</th>
</tr> </tr>
<tr class="napalm-table-placeholder d-none invisible">
</tr>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block data %}
<span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_facts&method=get_environment"></span>
{% endblock %}

View File

@ -39,20 +39,10 @@
<th scope="row">Manufacturer</th> <th scope="row">Manufacturer</th>
<td>{{ object.manufacturer|linkify|placeholder }}</td> <td>{{ object.manufacturer|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">