migrate napalm/lldp to typescript

This commit is contained in:
checktheroads 2021-04-20 01:18:24 -07:00
parent f83fb57d38
commit f3eb0df081
17 changed files with 199 additions and 67 deletions

Binary file not shown.

Binary file not shown.

BIN
netbox/project-static/dist/lldp.js vendored Normal file

Binary file not shown.

BIN
netbox/project-static/dist/lldp.js.map vendored Normal file

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -436,3 +436,19 @@ pre.change-diff {
background-color: rgba($green, 0.4);
}
}
div.card-overlay {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba($white, 0.75);
border-radius: $border-radius;
display: flex;
justify-content: center;
align-items: center;
& > div.spinner-border {
width: 6rem;
height: 6rem;
color: $secondary;
}
}

View File

@ -5,7 +5,7 @@
"license": "Apache2",
"scripts": {
"bundle:css": "parcel build --public-url /static -o netbox.css main.scss && parcel build --public-url /static -o rack_elevation.css rack_elevation.scss",
"bundle:js": "parcel build -o netbox.js src/index.ts && parcel build -o jobs.js src/jobs.ts",
"bundle:js": "parcel build --public-url /static -o netbox.js src/index.ts && parcel build --public-url /static -o jobs.js src/jobs.ts && parcel build --public-url /static -o lldp.js src/lldp.ts",
"bundle": "yarn bundle:css && yarn bundle:js"
},
"dependencies": {
@ -62,5 +62,8 @@
"@babel/proposal-object-rest-spread",
"@babel/plugin-proposal-nullish-coalescing-operator"
]
}
},
"browserslist": [
"defaults"
]
}

View File

@ -98,6 +98,21 @@ type APIUserConfig = {
[k: string]: unknown;
};
type LLDPInterface = {
parent_interface: string | null;
remote_chassis_id: string | null;
remote_port: string | null;
remote_port_description: string | null;
remote_system_capab: string[];
remote_system_description: string | null;
remote_system_enable_capab: string[];
remote_system_name: string | null;
};
type LLDPNeighborDetail = {
get_lldp_neighbors_detail: { [interface: string]: LLDPInterface[] };
};
interface ObjectWithGroup extends APIObjectBase {
group: Nullable<APIReference>;
}

View File

@ -89,10 +89,16 @@ async function checkJobStatus(id: string) {
}
}
if (document !== null) {
function initJobs() {
const { id, complete } = getJobInfo();
if (id !== null && !complete) {
// If there is a job ID and it is not completed, check for the job's status.
Promise.resolve(checkJobStatus(id));
}
}
if (document.readyState !== 'loading') {
initJobs();
} else {
document.addEventListener('DOMContentLoaded', initJobs);
}

View File

@ -0,0 +1,109 @@
import { createToast } from './toast';
import { getNetboxData, apiGetBase, hasError, isTruthy } from './util';
/**
* Toggle visibility of card loader.
*/
function toggleLoader(action: 'show' | 'hide') {
const spinnerContainer = document.querySelector('div.card-overlay');
if (spinnerContainer !== null) {
if (action === 'show') {
spinnerContainer.classList.remove('d-none');
} else {
spinnerContainer.classList.add('d-none');
}
}
}
/**
* 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;
}
/**
* 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 cellDevice = row.querySelector<HTMLTableCellElement>('td.device');
const cellInterface = row.querySelector<HTMLTableCellElement>('td.interface');
const cDevice = getData(row, 'td.configured_device', 'data');
const cChassis = getData(row, 'td.configured_chassis', 'data-chassis');
const cInterface = getData(row, 'td.configured_interface', 'data');
let cInterfaceShort = null;
if (isTruthy(cInterface)) {
cInterfaceShort = cInterface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, '$1$2');
}
const nHost = neighbor.remote_system_name ?? '';
const nPort = neighbor.remote_port ?? '';
const [nDevice] = nHost.split('.');
const [nInterface] = nPort.split('.');
if (cellDevice !== null) {
cellDevice.innerText = nDevice;
}
if (cellInterface !== null) {
cellInterface.innerText = nInterface;
}
if (!isTruthy(cDevice) && isTruthy(nDevice)) {
row.classList.add('info');
} else if (
(cDevice === nDevice || cChassis === nDevice) &&
cInterfaceShort === nInterface
) {
row.classList.add('success');
} else if (cDevice === nDevice || cChassis === nDevice) {
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);
}

View File

@ -159,3 +159,16 @@ export function getSelectedOptions<E extends HTMLElement>(base: E): SelectedOpti
}
return selected;
}
export function getNetboxData(key: string): string | null {
if (!key.startsWith('data-')) {
key = `data-${key}`;
}
for (const element of getElements('body > div#netbox-data > *')) {
const value = element.getAttribute(key);
if (isTruthy(value)) {
return value;
}
}
return null;
}

View File

@ -15,19 +15,19 @@
name="viewport"
content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width"
/>
</head>
<body>
{% block layout %}{% endblock %}
<script
type="text/javascript"
src="{% static 'netbox.js' %}"
onerror="window.location='{% url 'media_failure' %}?filename=netbox.js'">
</script>
<script type="text/javascript">
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
var netbox_csrf_token = "{{ csrf_token }}";
</script>
{% block head %}{% endblock %}
</head>
<body>
{% block layout %}{% endblock %}
{% block javascript %}{% endblock %}
{% include './messages.html' %}
<div id="netbox-data" style="display: none!important; visibility: hidden!important">
{% block data %}{% endblock %}
</div>
</body>
</html>

View File

@ -1,11 +1,23 @@
{% 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 %}
{% include 'inc/ajax_loader.html' %}
<div class="card">
<h5 class="card-header">LLDP Neighbors</h5>
<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>
{% comment %} <div id="spinner-container" class="float-end"></div> {% endcomment %}
</div>
<div class="card-body">
<table class="table table-hover">
<thead>
@ -20,7 +32,7 @@
<tbody>
{% for iface in interfaces %}
<tr id="{{ iface.name }}">
<td>{{ iface }}</td>
<td class="font-monospace">{{ iface }}</td>
{% if iface.connected_endpoint.device %}
<td class="configured_device" data="{{ iface.connected_endpoint.device }}" data-chassis="{{ iface.connected_endpoint.device.virtual_chassis.name }}">
<a href="{% url 'dcim:device' pk=iface.connected_endpoint.device.pk %}">{{ iface.connected_endpoint.device }}</a>
@ -36,7 +48,7 @@
</td>
{% endwith %}
{% else %}
<td colspan="2">None</td>
<td class="text-muted" colspan="2">None</td>
{% endif %}
<td class="device"></td>
<td class="interface"></td>
@ -48,53 +60,6 @@
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
$.ajax({
url: "{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_lldp_neighbors_detail",
dataType: 'json',
success: function(json) {
$.each(json['get_lldp_neighbors_detail'], function(iface, neighbors) {
var neighbor = neighbors[0];
var row = $('#' + iface.split(".")[0].replace(/([\/:])/g, "\\$1"));
// Glean configured hostnames/interfaces from the DOM
var configured_device = row.children('td.configured_device').attr('data');
var configured_chassis = row.children('td.configured_device').attr('data-chassis');
var configured_interface = row.children('td.configured_interface').attr('data');
var configured_interface_short = null;
if (configured_interface) {
// Match long-form IOS names against short ones (e.g. Gi0/1 == GigabitEthernet0/1).
configured_interface_short = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2");
}
// Clean up hostnames/interfaces learned via LLDP
var neighbor_host = neighbor['remote_system_name'] || ""; // sanitize hostname if it's null to avoid breaking the split func
var neighbor_port = neighbor['remote_port'] || ""; // sanitize port if it's null to avoid breaking the split func
var lldp_device = neighbor_host.split(".")[0]; // Strip off any trailing domain name
var lldp_interface = neighbor_port.split(".")[0]; // Strip off any trailing subinterface ID
// Add LLDP neighbors to table
row.children('td.device').html(lldp_device);
row.children('td.interface').html(lldp_interface);
// Apply colors to rows
if (!configured_device && lldp_device) {
row.addClass('info');
} else if ((configured_device == lldp_device || configured_chassis == lldp_device) && configured_interface == lldp_interface) {
row.addClass('success');
} else if ((configured_device == lldp_device || configured_chassis == lldp_device) && configured_interface_short == lldp_interface) {
row.addClass('success');
} else {
row.addClass('danger');
}
});
},
error: function(xhr) {
alert(xhr.responseText);
}
});
});
</script>
{% block data %}
<span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_lldp_neighbors_detail"></span>
{% endblock %}

View File

@ -1,5 +1,8 @@
{% extends 'base.html' %} {% load search_options %} {% block layout %}
{% extends 'base.html' %}
{% load search_options %}
{% block head %}{% endblock %}
{% block layout %}
<div class="container-fluid">
<div class="row">
<nav
@ -56,4 +59,6 @@
</main>
</div>
</div>
{% endblock %} {%block javascript %} {%endblock%}
{% endblock %}
{% block javascript %}{% endblock %}
{% block data %}{% endblock %}