diff --git a/.gitignore b/.gitignore index 4591c0f..c49c267 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.po~ +.tmp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f43d83..8cc83f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ All dates in this document are in `DD.MM.YYYY` format. *Nothing yet* +## [Version 2.0.0] (22.04.2020) +- Coverted to client side rendering. + ## [Version 1.3.0] (21.04.2020) ### Added - Added ACL rules for latest LuCI support @@ -84,6 +87,8 @@ All dates in this document are in `DD.MM.YYYY` format. Initial release [Unreleased]: https://github.com/tano-systems/luci-app-tn-lldpd/tree/master +[Version 2.0.0]: https://github.com/tano-systems/luci-app-tn-lldpd/releases/tag/v2.0.0 +[Version 1.3.0]: https://github.com/tano-systems/luci-app-tn-lldpd/releases/tag/v1.3.0 [Version 1.2.2]: https://github.com/tano-systems/luci-app-tn-lldpd/releases/tag/v1.2.2 [Version 1.2.1]: https://github.com/tano-systems/luci-app-tn-lldpd/releases/tag/v1.2.1 [Version 1.2.0]: https://github.com/tano-systems/luci-app-tn-lldpd/releases/tag/v1.2.0 diff --git a/README.md b/README.md index 0d131af..3c2850d 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,46 @@ # LuCI support for LLDP daemon ## Description -This package allows you to control LLDPd and view discovered neighbors by LLDPd -in LuCI web interface. +This application allows you to configure LLDPd and view discovered neighbors and statistics over LuCI web interface. ## Dependencies -Package depends on lldpd package. +This LuCI application requires the installed lldpd package with customized procd initialization scripts, which can be founded in the recipes of the OpenEmbedded layer of the [TanoWrt Linux Distribution](https://github.com/tano-systems/meta-tanowrt). -LLDPd LuCI application developed for LuCI 18.06 branch. For OpenWrt/LEDE 17.01 -use old luci-app-tn-lldpd [version 1.0.0]. +Master branch of this repository requires latest LuCI revision with client side rendering feature. Support for older LuCI releases (e.g. for version 18.06.x, 19.07.x) is left in the [1.x](https://github.com/tano-systems/luci-app-tn-mstpd/tree/1.x) branch of this repository. -Starting with [version 1.2.0], in order for all available settings to work correctly, -you must use the modified procd initialization script for lldpd, that can be founded -in [meta-tanowrt](https://github.com/tano-systems/meta-tanowrt.git) OpenEmbedded layer. - -## Supported languages +## Supported Languages - Russian - English +## Supported (tested) LuCI Themes +- [luci-theme-tano](https://github.com/tano-systems/luci-theme-tano) ([screenshots](#screenshots) are taken with this theme) +- luci-theme-bootstrap +- luci-theme-openwrt-2020 +- luci-theme-openwrt + ## Screenshots ### Status -#### Discovered neighbors -![Discovered neighbors](screenshots/luci-app-lldpd-status-neighbors.png?raw=true "Discovered neighbors") - -#### Local interfaces statistics -![Local interfaces statistics](screenshots/luci-app-lldpd-status-statistics.png?raw=true "Local interfaces statistics") +#### Discovered Neighbors +![Discovered Neighbors](screenshots/luci-app-tn-lldpd-status-neighbors.png?raw=true) -#### Local chassis information -![Local chassis](screenshots/luci-app-lldpd-status-chassis.png?raw=true "Local chassis") +#### Local Interfaces Statistics +![Local Interfaces Statistics](screenshots/luci-app-tn-lldpd-status-statistics.png?raw=true) ### Settings -#### Basic settings -![Basic settings](screenshots/luci-app-lldpd-settings-basic.png?raw=true "Basic settings") +#### Basic Settings +![Basic Settings](screenshots/luci-app-tn-lldpd-settings-basic.png?raw=true) -#### Network interfaces settings -![Network interfaces](screenshots/luci-app-lldpd-settings-interfaces.png?raw=true "Network interfaces") +#### Network Interfaces Settings +![Network Interfaces](screenshots/luci-app-tn-lldpd-settings-interfaces.png?raw=true) -#### Advanced settings -![Advanced settings](screenshots/luci-app-lldpd-settings-advanced.png?raw=true "Advanced settings") +#### Advanced Settings +![Advanced Settings](screenshots/luci-app-tn-lldpd-settings-advanced.png?raw=true) -#### Protocols support -![Protocols support](screenshots/luci-app-lldpd-settings-protocols.png?raw=true "Protocols support") +#### Protocols Support +![Protocols Support](screenshots/luci-app-tn-lldpd-settings-protocols.png?raw=true) [version 1.2.0]: https://github.com/tano-systems/luci-app-tn-lldpd/releases/tag/v1.2.0 [version 1.0.0]: https://github.com/tano-systems/luci-app-tn-lldpd/releases/tag/v1.0.0 diff --git a/htdocs/luci-static/resources/lldpd.js b/htdocs/luci-static/resources/lldpd.js new file mode 100644 index 0000000..16a9f3a --- /dev/null +++ b/htdocs/luci-static/resources/lldpd.js @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2018-2020, Tano Systems LLC. All Rights Reserved. + * Anton Kikin + */ + +'use strict'; +'require ui'; +'require form'; +'require network'; +'require session'; +'require uci'; + +const appVersion = '2.0.0'; +const appHomeUrl = 'https://github.com/tano-systems/luci-app-tn-lldpd'; + +const appFooter = E('div', { 'class': 'cbi-section' }, [ + E('p', { 'class': 'cbi-section-node tano-copyright' }, [ + E('a', { 'href': appHomeUrl }, + _('LLDPd LuCI application (version %s)').format(appVersion)), + E('br', {}), + _('© 2018–2020, Tano Systems LLC, Anton Kikin'), + ' <', + E('a', { 'href': 'mailto:a.kikin@tano-systems.com' }, + E('nobr', {}, 'a.kikin@tano-systems.com')), + '>' + ]) +]); + +/* + * Filter neighbors (-H) + * + * The filter column means that filtering is enabled + * The 1proto column tells that only one protocol will be kept. + * The 1neigh column tells that only one neighbor will be kept. + * + * incoming outgoing + * filter 1proto 1neigh filter 1proto 1neigh + * 0 + * 1 x x x x + * 2 x x + * 3 x x + * 4 x x + * 5 x + * 6 x + * 7 x x x x x + * 8 x x x + * 9 x x x x + * 10 x x + * 11 x x + * 12 x x x x + * 13 x x x + * 14 x x x x + * 15 x x x + * 16 x x x x x + * 17 x x x x + * 18 x x x + * 19 x x x + */ +var cbiFilterSelect = form.Value.extend({ + __name__: 'CBI.LLDPD.FilterSelect', + + __init__: function() { + this.super('__init__', arguments); + + this.selected = null; + + this.filterVal = [ + [ 0, 0, 0, 0, 0, 0 ], + [ 1, 1, 0, 1, 1, 0 ], + [ 1, 1, 0, 0, 0, 0 ], + [ 0, 0, 0, 1, 1, 0 ], + [ 1, 0, 0, 1, 0, 0 ], + [ 1, 0, 0, 0, 0, 0 ], + [ 0, 0, 0, 1, 0, 0 ], + [ 1, 1, 1, 1, 1, 0 ], + [ 1, 1, 1, 0, 0, 0 ], + [ 1, 0, 1, 1, 1, 0 ], + [ 0, 0, 0, 1, 0, 1 ], + [ 1, 0, 1, 0, 0, 0 ], + [ 1, 0, 1, 1, 0, 1 ], + [ 1, 0, 1, 1, 0, 0 ], + [ 1, 1, 0, 1, 0, 1 ], + [ 1, 1, 0, 1, 0, 0 ], + [ 1, 1, 1, 1, 0, 1 ], + [ 1, 1, 1, 1, 0, 0 ], + [ 1, 0, 0, 1, 0, 1 ], + [ 1, 0, 0, 1, 1, 0 ] + ]; + }, + + /** @private */ + handleRowClick: function(section_id, ev) { + var row = ev.currentTarget; + var tbody = row.parentNode; + var selected = row.getAttribute('data-filter'); + var input = tbody.querySelector('[id="' + this.cbid(section_id) + '-' + selected + '"]'); + + this.selected = selected; + + tbody.querySelectorAll('tr').forEach(function(e) { + e.classList.remove('lldpd-filter-selected'); + }); + + input.checked = true; + row.classList.add('lldpd-filter-selected'); + }, + + formvalue: function(section_id) { + return this.selected || this.cfgvalue(section_id); + }, + + renderFrame: function(section_id, in_table, option_index, nodes) { + var tmp = this.description; + + // Prepend description with table legend + this.description = + '' + this.description; + + var rendered = this.super('renderFrame', arguments); + + // Restore original description + this.description = tmp; + + return rendered; + }, + + renderWidget: function(section_id, option_index, cfgvalue) { + var selected = parseInt(cfgvalue) - 1; + + var tbody = []; + + var renderFilterVal = L.bind(function(row, col) { + return this.filterVal[row][col] ? '✔' : ''; + }, this); + + for (var i = 0; i < this.filterVal.length; i++) { + tbody.push(E('tr', { + 'class': ((selected == i) ? 'lldpd-filter-selected' : ''), + 'click': L.bind(this.handleRowClick, this, section_id), + 'data-filter': i + 1, + }, [ + E('td', {}, [ + E('input', { + 'class': 'cbi-input-radio', + 'data-update': 'click change', + 'type': 'radio', + 'id': this.cbid(section_id) + '-' + (i + 1), + 'name': this.cbid(section_id), + 'checked': (selected == i) ? '' : null, + 'value': i + 1 + }) + ]), + E('td', {}, i + 1), + E('td', {}, renderFilterVal(i, 0)), + E('td', {}, renderFilterVal(i, 1)), + E('td', {}, renderFilterVal(i, 2)), + E('td', {}, renderFilterVal(i, 3)), + E('td', {}, renderFilterVal(i, 4)), + E('td', {}, renderFilterVal(i, 5)) + ])); + }; + + var table = E('table', { 'class': 'lldpd-filter', 'id': this.cbid(section_id) }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', { 'rowspan': 2 }), + E('th', { 'rowspan': 2 }, _('Filter')), + E('th', { 'colspan': 3 }, _('Incoming')), + E('th', { 'colspan': 3 }, _('Outgoing')) + ]), + E('tr', {}, [ + E('th', {}, 'E'), + E('th', {}, 'P'), + E('th', {}, 'N'), + E('th', {}, 'E'), + E('th', {}, 'P'), + E('th', {}, 'N'), + ]) + ]), + E('tbody', {}, tbody) + ]); + + return table; + }, +}); + +function init() { + return new Promise(function(resolveFn, rejectFn) { + var data = session.getLocalData('luci-app-tn-lldpd'); + if ((data !== null) && data.hasOwnProperty('hideFooter')) { + return resolveFn(); + } + + data = {}; + + return uci.load('luci').then(function() { + data.hideFooter = (uci.get('luci', 'app_tn_lldpd', 'hide_footer') == '1') + ? true : false; + session.setLocalData('luci-app-tn-lldpd', data); + return resolveFn(); + }); + }); +} + +function isNeedToHideFooter() { + var data = session.getLocalData('luci-app-tn-lldpd'); + + if (data === null) + data = {}; + + if (data.hasOwnProperty('hideFooter')) + return data.hideFooter; + else + return false; +} + +function renderFooter() { + return isNeedToHideFooter() ? '' : appFooter; +} + +return L.Class.extend({ + cbiFilterSelect: cbiFilterSelect, + renderFooter: renderFooter, + init: init, +}); diff --git a/htdocs/luci-static/resources/lldpd/lldpd.css b/htdocs/luci-static/resources/lldpd/lldpd.css new file mode 100644 index 0000000..77ce16f --- /dev/null +++ b/htdocs/luci-static/resources/lldpd/lldpd.css @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2020, Tano Systems LLC. All Rights Reserved. + * Author: Anton Kikin + */ + +/* + * Filter select widget + */ +table.lldpd-filter td, +table.lldpd-filter th { + border: 1px solid #ccc !important; + padding: 2px 10px 2px 10px; + text-align: center; +} + +table.lldpd-filter td, +table.lldpd-filter td input[type="radio"] { + vertical-align: middle; +} + +table.lldpd-filter th { font-weight: bold; } + +table.lldpd-filter tbody tr { + cursor: pointer; +} + +table.lldpd-filter tr.lldpd-filter-selected td { + background-color: #eeeeee; +} + +/* + * Parameters + */ +.lldpd-params { + column-count: 2; + -moz-column-count: 2; + -webkit-column-count: 2; + column-gap: 24px; + -moz-column-gap: 24px; + -webkit-column-gap: 24px; +} + +@media only screen and (max-width: 850px) { + .lldpd-params { + column-count: 1; + -moz-column-count: 1; + -webkit-column-count: 1; + } +} + +.lldpd-params > div { + display: grid; + grid-template-columns: 1fr auto; + border-bottom: 1px solid #e6e6e6; + padding: 0 8px; + -webkit-column-break-inside: avoid; + -moz-column-break-inside: avoid; + column-break-inside: avoid; +} + +.td .lldpd-params > div:last-of-type { + border-bottom: none; +} + +.lldpd-params .lldpd-param { + margin-right: 10px; + font-weight: bold; +} + +.lldpd-params .lldpd-param::after { + content: ':'; +} + +.lldpd-params .lldpd-param-value { + white-space: normal; + font-weight: normal; + text-align: right; +} + +.td .lldpd-params { + column-count: 1; + -moz-column-count: 1; + -webkit-column-count: 1; +} + +.td .lldpd-params > div { + padding: 0; +} + +/* + * Status table + */ +.lldpd-folded, +.lldpd-unfolded { + width: 100%; +} + +.lldpd-table .tr .td { cursor: pointer; } + +.lldpd-protocol-badge { + display: inline-block; + width: auto !important; + width: fit-content !important; + width: -moz-fit-content !important; + width: -webkit-fit-content !important; + box-shadow: 0 1px 3px 0 grey; + padding: 0px 8px; + border-radius: 5px; + width: 100%; + background-color: #e6e6e6; + border: 0; + color: #595959; +} + +.lldpd-protocol-badge.lldpd-protocol-lldp { background-color: #b7efcf; border-color: #2abd69; color: #165e34; } +.lldpd-protocol-badge.lldpd-protocol-cdp { background-color: #b2daf3; border-color: #46a6e2; color: #1a74ac; } +.lldpd-protocol-badge.lldpd-protocol-fdp { background-color: #f9e3b3; border-color: #b7820f; color: #b7820f; } +.lldpd-protocol-badge.lldpd-protocol-edp { background-color: #f9e3f9; border-color: #e380e3; color: #b70f82; } +.lldpd-protocol-badge.lldpd-protocol-sonmp { background-color: #f4ffc4; border-color: #a7ce00; color: #7e9b00; } diff --git a/htdocs/luci-static/resources/lldpd/lldpd.js b/htdocs/luci-static/resources/lldpd/lldpd.js deleted file mode 100644 index 0eb0bae..0000000 --- a/htdocs/luci-static/resources/lldpd/lldpd.js +++ /dev/null @@ -1,397 +0,0 @@ -/* - * Copyright (c) 2018, Tano Systems. All Rights Reserved. - * Anton Kikin - */ - -function lldpd_id(str) -{ - return str.replace(/[^a-z0-9]/gi, '-'); -} - -function lldpd_fmt_number(v) -{ - if (parseInt(v)) - return v; - - return '–'; -} - -function lldpd_fmt_port_id_type(v) -{ - if (typeof v === 'undefined') - return "–"; - - if (v == 'mac') - return _("MAC address"); - else if (v == 'ifname') - return _("Interface name"); - else if (v == 'local') - return _("Local ID"); - else if (v == 'ip') - return _("IP address"); - - return v; -} - -function lldpd_fmt_age(v) -{ - if (typeof v === 'undefined') - return "–"; - - return '' + v + ''; -} - -function lldpd_fmt_protocol(v) -{ - if (typeof v === 'undefined') - return "–"; - - if (v == 'unknown') - return "–"; - else if (v == 'LLDP') - return '' + v + ''; - else if ((v == 'CDPv1') || (v == 'CDPv2')) - return '' + v + ''; - else if (v == 'FDP') - return '' + v + ''; - else if (v == 'EDP') - return '' + v + ''; - else if (v == 'SONMP') - return '' + v + ''; - else - return '' + v + ''; -} - -function lldpd_kvtable_add_row(description, value) -{ - if (typeof value == 'undefined') - return ''; - - return '' + description + - ':' + value + ''; -} - -function lldpd_fmt_chassis_kvtable(ch) -{ - var unfolded = '' - - unfolded += ''; - - if (typeof ch.name !== 'undefined') - unfolded += lldpd_kvtable_add_row(_("Name"), ch.name[0].value); - - if (typeof ch.descr !== 'undefined') - unfolded += lldpd_kvtable_add_row(_("Description"), ch.descr[0].value); - - if (typeof ch.id !== 'undefined') - { - unfolded += lldpd_kvtable_add_row(_("ID type"), lldpd_fmt_port_id_type(ch.id[0].type)); - unfolded += lldpd_kvtable_add_row(_("ID"), ch.id[0].value); - } - - // Management addresses - if (typeof ch["mgmt-ip"] !== 'undefined') - { - var ips = ""; - - if (ch["mgmt-ip"].length > 0) - { - // Array of addresses - for (var ip = 0; ip < ch["mgmt-ip"].length; ip++) - ips += ch["mgmt-ip"][ip].value + "
"; - } - else - { - // One address - ips += ch["mgmt-ip"][0].value; - } - - unfolded += lldpd_kvtable_add_row(_("Management IP(s)"), ips); - } - - if (typeof ch.capability !== 'undefined') - { - var caps = ""; - - if (ch.capability.length > 0) - { - // Array of capabilities - for (var cap = 0; cap < ch.capability.length; cap++) - { - caps += ch.capability[cap].type; - caps += " (" + (ch.capability[cap].enabled ? _("enabled") : _("disabled")) + ")"; - caps += "
"; - } - } - else - { - // One capability - caps += ch.capability[0].type; - caps += " (" + (ch.capability[0].enabled ? _("enabled") : _("disabled")) + ")"; - } - - unfolded += lldpd_kvtable_add_row(_("Capabilities"), caps); - } - - unfolded += "
"; - - return unfolded; -} - -function lldpd_fmt_chassis(ch) -{ - var folded = ''; - - if (typeof ch.name !== 'undefined' && - typeof ch.descr !== 'undefined' && - typeof ch.name[0].value !== 'undefined' && - typeof ch.descr[0].value !== 'undefined') - { - folded += '' + ch.name[0].value + ''; - folded += '
' + ch.descr[0].value; - } - else if (typeof ch.name !== 'undefined' && - typeof ch.name[0].value !== 'undefined') - folded += '' + ch.name[0].value + ''; - else if (typeof ch.descr !== 'undefined' && - typeof ch.descr[0].value !== 'undefined') - folded += ch.descr[0].value; - else if (typeof ch.id !== 'undefined' && - typeof ch.id[0].value !== 'undefined') - folded += ch.id[0].value; - else - folded += _('Unknown'); - - return folded; -} - -function lldpd_fmt_port_kvtable(port, only_id_and_ttl) -{ - var unfolded = ''; - - unfolded = ''; - - if (!only_id_and_ttl) - { - unfolded += lldpd_kvtable_add_row(_("Name"), port.name); - unfolded += lldpd_kvtable_add_row(_("Age"), lldpd_fmt_age(port.age)); - } - - if (typeof port.port !== 'undefined') - { - if (typeof port.port[0].id !== 'undefined') - { - unfolded += lldpd_kvtable_add_row(_("Port ID type"), - lldpd_fmt_port_id_type(port.port[0].id[0].type)); - - unfolded += lldpd_kvtable_add_row(_("Port ID"), port.port[0].id[0].value); - } - - if (typeof port.port[0].descr !== 'undefined') - unfolded += lldpd_kvtable_add_row(_("Port description"), port.port[0].descr[0].value); - - if (typeof port.ttl !== 'undefined') - unfolded += lldpd_kvtable_add_row(_("TTL"), port.ttl[0].ttl); - else if (port.port[0].ttl !== 'undefined') - unfolded += lldpd_kvtable_add_row(_("TTL"), port.port[0].ttl[0].value); - } - - unfolded += "
"; - - return unfolded; -} - -function lldpd_fmt_port(port) -{ - var folded = ''; - - if (typeof port.port !== 'undefined') - { - if (typeof port.port[0].descr !== 'undefined' && - typeof port.port[0].id[0].value !== 'undefined' && - port.port[0].descr[0].value !== port.port[0].id[0].value) - { - folded += '' + port.port[0].descr[0].value + '' + - '
' + port.port[0].id[0].value; - } - else - { - if (typeof port.port[0].descr !== 'undefined') - folded += port.port[0].descr[0].value; - else - folded += port.port[0].id[0].value; - } - } - else - { - folded = "%s".format(port.name); - } - - return folded; -} - -var lldpd_row_folded = [] - -function lldpd_folding_img_file(folded) { - return L.resource('lldpd/details_' + - (folded ? 'show' : 'hide') + '.svg'); -} - -function lldpd_folding_toggle(row, row_id) -{ - var e_img = document.getElementById("lldpd-fold-img-" + row_id); - var e_folded = document.querySelectorAll("[id='lldpd-cell-" + row_id + "-folded']"); - var e_unfolded = document.querySelectorAll("[id='lldpd-cell-" + row_id + "-unfolded']"); - - if (e_folded.length != e_unfolded.length) - return; - - var do_fold = e_folded[0].classList.contains('lldpd-cell-hidden'); - lldpd_row_folded[row_id] = do_fold; - - for (var i = 0; i < e_folded.length; i++) - { - if (do_fold) - { - e_folded[i].classList.remove("lldpd-cell-hidden"); - e_folded[i].classList.add("lldpd-cell-visible"); - e_unfolded[i].classList.remove("lldpd-cell-visible"); - e_unfolded[i].classList.add("lldpd-cell-hidden"); - } - else - { - e_folded[i].classList.remove("lldpd-cell-visible"); - e_folded[i].classList.add("lldpd-cell-hidden"); - e_unfolded[i].classList.remove("lldpd-cell-hidden"); - e_unfolded[i].classList.add("lldpd-cell-visible"); - } - } - - e_img.src = lldpd_folding_img_file(do_fold); -} - -function lldpd_folding_row(row_id, cells_folded, cells_unfolded, default_folded) -{ - var row = []; - var len = cells_folded.length; - var i; - - if (!len || (cells_folded.length != cells_unfolded.length)) - return null; - - if (typeof lldpd_row_folded[row_id] == 'undefined') - lldpd_row_folded[row_id] = default_folded; - - for (i = 0; i < len; i++) - { - var cell = ''; - - if (i == 0) - { - cell += '
'; - - // Fold/unfold icon - cell += '
'; - cell += ''; - - cell += '
'; - } - - if ((cells_unfolded[i] !== null) && (cells_folded[i] !== null)) - { - cell += '
'; - cell += cells_folded[i]; - cell += '
'; - - cell += '
'; - cell += cells_unfolded[i]; - cell += '
'; - } - else - { - cell += (cells_folded[i] == null) - ? cells_unfolded[i] - : cells_folded[i]; - } - - if (i == 0) - cell += '
'; - - row.push(cell); - } - - return row; -} - -function lldpd_update_table(table, data, placeholder) { - var target = isElem(table) ? table : document.querySelector(table); - - if (!isElem(target)) - return; - - target.querySelectorAll('.tr.table-titles, .cbi-section-table-titles').forEach(function(thead) { - var titles = []; - - thead.querySelectorAll('.th').forEach(function(th) { - titles.push(th); - }); - - if (Array.isArray(data)) { - var n = 0, rows = target.querySelectorAll('.tr'); - - data.forEach(function(row) { - var id = row[0]; - var trow = E('div', { 'class': 'tr', 'onclick': 'lldpd_folding_toggle(this, \'' + id + '\')' }); - - for (var i = 0; i < titles.length; i++) { - var text = (titles[i].innerText || '').trim(); - var td = trow.appendChild(E('div', { - 'class': titles[i].className, - 'data-title': (text !== '') ? text : null - }, row[i + 1] || '')); - - td.classList.remove('th'); - td.classList.add('td'); - } - - trow.classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1)); - - if (rows[n]) - target.replaceChild(trow, rows[n]); - else - target.appendChild(trow); - }); - - while (rows[++n]) - target.removeChild(rows[n]); - - if (placeholder && target.firstElementChild === target.lastElementChild) { - var trow = target.appendChild(E('div', { 'class': 'tr placeholder' })); - var td = trow.appendChild(E('div', { 'class': titles[0].className }, placeholder)); - - td.classList.remove('th'); - td.classList.add('td'); - } - } else { - thead.parentNode.style.display = 'none'; - - thead.parentNode.querySelectorAll('.tr, .cbi-section-table-row').forEach(function(trow) { - if (trow !== thead) { - var n = 0; - trow.querySelectorAll('.th, .td').forEach(function(td) { - if (n < titles.length) { - var text = (titles[n++].innerText || '').trim(); - if (text !== '') - td.setAttribute('data-title', text); - } - }); - } - }); - - thead.parentNode.style.display = ''; - } - }); -} diff --git a/htdocs/luci-static/resources/view/lldpd/config.js b/htdocs/luci-static/resources/view/lldpd/config.js new file mode 100644 index 0000000..49d54d0 --- /dev/null +++ b/htdocs/luci-static/resources/view/lldpd/config.js @@ -0,0 +1,475 @@ +/* + * Copyright (c) 2020 Tano Systems LLC. All Rights Reserved. + * Author: Anton Kikin + */ + +'use strict'; +'require rpc'; +'require form'; +'require lldpd'; +'require uci'; +'require tools.widgets as widgets'; + +var callInitList = rpc.declare({ + object: 'luci', + method: 'getInitList', + params: [ 'name' ], + expect: { '': {} }, + filter: function(res) { + for (var k in res) + return +res[k].enabled; + return null; + } +}); + +var callInitAction = rpc.declare({ + object: 'luci', + method: 'setInitAction', + params: [ 'name', 'action' ], + expect: { result: false } +}); + +return L.view.extend({ + __init__: function() { + this.super('__init__', arguments); + + // Inject CSS + var head = document.getElementsByTagName('head')[0]; + var css = E('link', { 'href': + L.resource('lldpd/lldpd.css') + + '?v=#PKG_VERSION', 'rel': 'stylesheet' }); + + head.appendChild(css); + }, + + load: function() { + return Promise.all([ + callInitList('lldpd'), + lldpd.init(), + uci.load('lldpd') + ]); + }, + + // ----------------------------------------------------------------------------------------- + // + // Basic Options + // + // ----------------------------------------------------------------------------------------- + + /** @private */ + populateBasicOptions: function(s, tab, data) { + var o; + var serviceEnabled = data[0]; + + // Service enable/disable + o = s.taboption(tab, form.Flag, 'enabled', _('Enable service')); + o.optional = false; + o.rmempty = false; + + o.cfgvalue = function() { + return serviceEnabled ? this.enabled : this.disabled; + }; + + o.write = function(section_id, value) { + uci.set('mstpd', section_id, 'enabled', value); + + if (value == '1') { + // Enable and start + return callInitAction('lldpd', 'enable').then(function() { + return callInitAction('lldpd', 'start'); + }); + } + else { + // Stop and disable + return callInitAction('lldpd', 'stop').then(function() { + return callInitAction('lldpd', 'disable'); + }); + } + }; + + // System description + o = s.taboption(tab, form.Value, 'lldp_description', + _('System description'), + _('Override system description with the provided description.')); + + o.placeholder = 'System description'; + + // System hostname + o = s.taboption(tab, form.Value, 'lldp_hostname', + _('System hostname'), + _('Override system hostname with the provided value.')); + + o.placeholder = 'System hostname'; + + // Platform + o = s.taboption(tab, form.Value, 'lldp_platform', + _('System platform description'), + _('Override the platform description with the provided value. ' + + 'The default description is the kernel name (Linux).')); + + o.placeholder = 'System platform description'; + + // Management addresses of this system + o = s.taboption(tab, form.Value, 'lldp_sys_mgmt_ip', + _('Management addresses of this system'), + _('Specify the management addresses of this system. ' + + 'If not specified, the first IPv4 and the first ' + + 'IPv6 are used. If an exact IP address is provided, it is used ' + + 'as a management address without any check. If you want to ' + + 'blacklist IPv6 addresses, you can use !*:*. ' + + 'See more details about available patterns ' + + 'here.')); + + o.placeholder = 'Management addresses'; + + // LLDP tx interval + o = s.taboption(tab, form.Value, 'lldp_tx_interval', + _('Transmit delay'), + _('The transmit delay is the delay between two ' + + 'transmissions of LLDP PDU. The default value ' + + 'is 30 seconds.')); + + o.datatype = 'uinteger'; + o.default = 30; + o.placeholder = 30; + o.rmempty = false; + + o.validate = function(section_id, value) { + if (value != parseInt(value)) + return _('Must be a number'); + else if (value <= 0) + return _('Transmit delay must be greater than 0'); + return true; + }; + + // LLDP tx hold + o = s.taboption(tab, form.Value, 'lldp_tx_hold', + _('Transmit hold value'), + _('This value is used to compute the TTL of transmitted ' + + 'packets which is the product of this value and of the ' + + 'transmit delay. The default value is 4 and therefore ' + + 'the default TTL is 120 seconds.')); + + o.datatype = 'uinteger'; + o.default = 4; + o.placeholder = 4; + o.rmempty = false; + + o.validate = function(section_id, value) { + if (value != parseInt(value)) + return _('Must be a number'); + else if (value <= 0) + return _('Transmit hold value must be greater than 0'); + return true; + }; + + // Received-only mode (-r) + o = s.taboption(tab, form.Flag, 'readonly_mode', + _('Enable receive-only mode'), + _('With this option, LLDPd will not send any frames. ' + + 'It will only listen to neighbors.')); + + o.rmempty = false; + o.optional = false; + o.default = '0'; + }, + + // ----------------------------------------------------------------------------------------- + // + // Network Interfaces + // + // ----------------------------------------------------------------------------------------- + + /** @private */ + populateIfacesOptions: function(s, tab, data) { + var o; + + // Interfaces to listen on + o = s.taboption(tab, widgets.DeviceSelect, 'interface', + _('Network interfaces'), + _('Specify which interface to listen and send LLDPDU to. ' + + 'If no interfaces is specified, LLDPd will use all available physical interfaces.')); + + o.nobridges = true; + o.rmempty = true; + o.multiple = true; + o.nocreate = true; + o.noaliases = true; + o.networks = null; + + // ChassisID interfaces + o = s.taboption(tab, widgets.DeviceSelect, 'cid_interface', + _('Network interfaces for chassis ID computing'), + _('Specify which interfaces to use for computing chassis ID. ' + + 'If no interfaces is specified, all interfaces are considered. ' + + 'LLDPd will take the first MAC address from all the considered ' + + 'interfaces to compute the chassis ID.')); + + o.nobridges = false; + o.rmempty = true; + o.multiple = true; + o.nocreate = true; + o.noaliases = true; + o.networks = null; + }, + + // ----------------------------------------------------------------------------------------- + // + // Advanced Options + // + // ----------------------------------------------------------------------------------------- + + /** @private */ + populateAdvancedOptions: function(s, tab, data) { + var o; + + // SNMP agentX socket + o = s.taboption(tab, form.Value, 'agentxsocket', + _('SNMP agentX socket path'), + _('If the path to the socket is set, then LLDPd will enable an ' + + 'SNMP subagent using AgentX protocol. This allows you to get ' + + 'information about local system and remote systems through SNMP.')); + + o.rmempty = false; + o.placeholder = '/var/run/agentx.sock'; + o.default = '/var/run/agentx.sock'; + + // LLDP class + o = s.taboption(tab, form.ListValue, 'lldp_class', + _('LLDP-MED device class')); + + o.value('1', _('Generic Endpoint (Class I)')); + o.value('2', _('Media Endpoint (Class II)')); + o.value('3', _('Communication Device Endpoints (Class III)')); + o.value('4', _('Network Connectivity Device (Class IV)')); + + o.default = '4'; + + // LLDP-MED inventory TLV transmission (-i) + o = s.taboption(tab, form.Flag, 'lldpmed_no_inventory', + _('Disable LLDP-MED inventory TLV transmission'), + _('LLDPd will still receive (and publish using SNMP if enabled) ' + + 'those LLDP-MED TLV but will not send them. Use this option ' + + 'if you don\'t want to transmit sensible information like serial numbers.')); + + o.default = '0'; + + // Filter neighbors (-H) + o = s.taboption(tab, lldpd.cbiFilterSelect, 'filter', + _('Specify the behaviour when detecting multiple neighbors'), + _('The default filter is 15. For more details see \"FILTERING NEIGHBORS\" section ' + + 'here.')); + + o.default = 15; + + // Force port ID subtype + o = s.taboption(tab, form.ListValue, 'lldp_portidsubtype', + _('Force port ID subtype'), + _('With this option, you can force the port identifier ' + + 'to be the interface name or the MAC address.')); + + o.value('macaddress', _('Interface MAC address')); + o.value('ifname', _('Interface name')); + + o.default = 'macaddress'; + + // The destination MAC address used to send LLDPDU + o = s.taboption(tab, form.ListValue, 'lldp_agenttype', + _('The destination MAC address used to send LLDPDU'), + _('The destination MAC address used to send LLDPDU allows an agent ' + + 'to control the propagation of LLDPDUs. By default, the ' + + '01:80:c2:00:00:0e MAC address is used and limit the propagation ' + + 'of the LLDPDU to the nearest bridge.')); + + o.value('nearest-bridge', '01:80:c2:00:00:0e (nearest-bridge)'); + o.value('nearest-nontpmr-bridge', '01:80:c2:00:00:03 (nearest-nontpmr-bridge)'); + o.value('nearest-customer-bridge', '01:80:c2:00:00:00 (nearest-customer-bridge)'); + + o.default = 'nearest-bridge'; + }, + + // ----------------------------------------------------------------------------------------- + // + // Protocols Support + // + // ----------------------------------------------------------------------------------------- + + /** @private */ + populateProtocolsOptions: function(s, tab, data) { + var o; + + o = s.taboption(tab, form.SectionValue, '_protocols', form.TypedSection, 'lldpd'); + var ss = o.subsection; + ss.anonymous = true; + ss.addremove = false; + + // + // LLDPD + // Link Layer Discovery Protocol + // + ss.tab('lldp', _('LLDP')); + o = ss.taboption('lldp', form.Flag, 'enable_lldp', + _('Enable LLDP')); + + o.default = '1'; + o.rmempty = true; + + o = ss.taboption('lldp', form.Flag, 'force_lldp', + _('Force to send LLDP packets'), + _('Force to send LLDP packets even when there is no LLDP peer ' + + 'detected but there is a peer speaking another protocol detected. ' + + 'By default, LLDP packets are sent when there is a peer speaking ' + + 'LLDP detected or when there is no peer at all.')); + + o.default = '0'; + o.rmempty = true; + o.depends('enable_lldp', '1'); + + // + // CDP + // Cisco Discovery Protocol + // + ss.tab('cdp', _('CDP')); + o = ss.taboption('cdp', form.Flag, 'enable_cdp', + _('Enable CDP'), + _('Enable the support of CDP protocol to deal with Cisco routers ' + + 'that do not speak LLDP')); + + o.default = '1'; + o.rmempty = false; + + o = ss.taboption('cdp', form.ListValue, 'cdp_version', + _('CDP version')); + + o.value('cdpv1v2', _('CDPv1 and CDPv2')); + o.value('cdpv2', _('Only CDPv2')); + o.depends('enable_cdp', '1'); + + o.default = 'cdpv1v2'; + + o = ss.taboption('cdp', form.Flag, 'force_cdp', + _('Send CDP packets even if no CDP peer detected')); + + o.default = '0'; + o.rmempty = true; + o.depends('enable_cdp', '1'); + + o = ss.taboption('cdp', form.Flag, 'force_cdpv2', + _('Force sending CDPv2 packets')); + + o.default = '0'; + o.rmempty = true; + o.depends({ + force_cdp: '1', + enable_cdp: '1', + cdp_version: 'cdpv1v2' + }); + + // + // FDP + // Foundry Discovery Protocol + // + ss.tab('fdp', _('FDP')); + o = ss.taboption('fdp', form.Flag, 'enable_fdp', + _('Enable FDP'), + _('Enable the support of FDP protocol to deal with Foundry routers ' + + 'that do not speak LLDP')); + + o.default = '1'; + o.rmempty = false; + + o = ss.taboption('fdp', form.Flag, 'force_fdp', + _('Send FDP packets even if no FDP peer detected')); + + o.default = '0'; + o.rmempty = true; + o.depends('enable_fdp', '1'); + + // + // EDP + // Extreme Discovery Protocol + // + ss.tab('edp', _('EDP')); + o = ss.taboption('edp', form.Flag, 'enable_edp', + _('Enable EDP'), + _('Enable the support of EDP protocol to deal with Extreme routers ' + + 'and switches that do not speak LLDP.')); + + o.default = '1'; + o.rmempty = false; + + o = ss.taboption('edp', form.Flag, 'force_edp', + _('Send EDP packets even if no EDP peer detected')); + + o.default = '0'; + o.rmempty = true; + o.depends('enable_edp', '1'); + + // + // SONMP + // SynOptics Network Management Protocol + // + // a.k.a. + // Nortel Topology Discovery Protocol (NTDP) + // Nortel Discovery Protocol (NDP) + // Bay Network Management Protocol (BNMP) + // Bay Discovery Protocol (BDP) + // + ss.tab('sonmp', _('SONMP (NTDP, NDP, BNMP, BDP)')); + o = ss.taboption('sonmp', form.Flag, 'enable_sonmp', + _('Enable SONMP'), + _('Enable the support of SONMP protocol to deal with Nortel ' + + 'routers and switches that do not speak LLDP.')); + + o.default = '1'; + o.rmempty = false; + + o = ss.taboption('sonmp', form.Flag, 'force_sonmp', + _('Send SONMP packets even if no SONMP peer detected')); + + o.default = '0'; + o.rmempty = true; + o.depends('enable_sonmp', '1'); + }, + + /** @private */ + populateOptions: function(s, data) { + var o; + + s.tab('basic', _('Basic Settings')); + this.populateBasicOptions(s, 'basic', data); + + s.tab('ifaces', _('Network Interfaces')); + this.populateIfacesOptions(s, 'ifaces', data); + + s.tab('advanced', _('Advanced Settings')); + this.populateAdvancedOptions(s, 'advanced', data); + + s.tab('protocols', _('Protocols Support')); + this.populateProtocolsOptions(s, 'protocols', data); + }, + + render: function(data) { + var m, s; + + m = new form.Map('lldpd', _('LLDPd Settings'), + _('LLDPd is a implementation of IEEE 802.1ab ' + + '(LLDP).') + + _('On this page you may configure LLDPd parameters.')); + + s = m.section(form.TypedSection, 'lldpd'); + s.addremove = false; + s.anonymous = true; + + this.populateOptions(s, data); + + return m.render(); + }, + + addFooter: function() { + return [ + this.super('addFooter', arguments), + lldpd.renderFooter() + ]; + }, +}); diff --git a/htdocs/luci-static/resources/view/lldpd/status.js b/htdocs/luci-static/resources/view/lldpd/status.js new file mode 100644 index 0000000..3e4fc07 --- /dev/null +++ b/htdocs/luci-static/resources/view/lldpd/status.js @@ -0,0 +1,674 @@ +/* + * Copyright (c) 2020 Tano Systems. All Rights Reserved. + * Author: Anton Kikin + */ + +'use strict'; +'require rpc'; +'require form'; +'require lldpd'; +'require dom'; +'require poll'; + +var callLLDPStatus = rpc.declare({ + object: 'lldpd', + method: 'getStatus', + expect: {} +}); + +var dataMap = { + local: { + localChassis: null, + }, + remote: { + neightbors: null, + statistics: null, + }, +}; + +return L.view.extend({ + __init__: function() { + this.super('__init__', arguments); + + this.rowsUnfolded = {}; + + this.tableNeighbors = E('div', { 'class': 'table lldpd-table' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th left top' }, _('Local interface')), + E('div', { 'class': 'th left top' }, _('Protocol')), + E('div', { 'class': 'th left top' }, _('Discovered chassis')), + E('div', { 'class': 'th left top' }, _('Discovered port')), + ]), + E('div', { 'class': 'tr center placeholder' }, [ + E('div', { 'class': 'td' }, E('em', { 'class': 'spinning' }, + _('Collecting data...'))), + ]) + ]); + + this.tableStatistics = E('div', { 'class': 'table lldpd-table' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th left top' }, _('Local interface')), + E('div', { 'class': 'th left top' }, _('Protocol')), + E('div', { 'class': 'th right top' }, _('Tx')), + E('div', { 'class': 'th right top' }, _('Rx')), + E('div', { 'class': 'th right top' }, _('Tx discarded')), + E('div', { 'class': 'th right top' }, _('Rx unrecognized')), + E('div', { 'class': 'th right top' }, _('Ageout count')), + E('div', { 'class': 'th right top' }, _('Insert count')), + E('div', { 'class': 'th right top' }, _('Delete count')), + ]), + E('div', { 'class': 'tr center placeholder' }, [ + E('div', { 'class': 'td' }, E('em', { 'class': 'spinning' }, + _('Collecting data...'))), + ]) + ]); + + // Inject CSS + var head = document.getElementsByTagName('head')[0]; + var css = E('link', { 'href': + L.resource('lldpd/lldpd.css') + + '?v=#PKG_VERSION', 'rel': 'stylesheet' }); + + head.appendChild(css); + }, + + load: function() { + return Promise.all([ + L.resolveDefault(callLLDPStatus(), {}), + lldpd.init(), + ]); + }, + + /** @private */ + renderParam: function(param, value) { + if (typeof value === 'undefined') + return ''; + + return E('div', {}, [ + E('span', { 'class': 'lldpd-param' }, param), + E('span', { 'class': 'lldpd-param-value' }, value) + ]); + }, + + /** @private */ + renderAge: function(v) { + if (typeof v === 'undefined') + return "–"; + + return E('nobr', {}, v); + }, + + /** @private */ + renderIdType: function(v) { + if (typeof v === 'undefined') + return "–"; + + if (v == 'mac') + return _('MAC address'); + else if (v == 'ifname') + return _('Interface name'); + else if (v == 'local') + return _('Local ID'); + else if (v == 'ip') + return _('IP address'); + + return v; + }, + + /** @private */ + renderProtocol: function(v) { + if (typeof v === 'undefined' || v == 'unknown') + return "–"; + + if (v == 'LLDP') + return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-lldp' }, v); + else if ((v == 'CDPv1') || (v == 'CDPv2')) + return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-cdp' }, v); + else if (v == 'FDP') + return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-fdp' }, v); + else if (v == 'EDP') + return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-edp' }, v); + else if (v == 'SONMP') + return E('span', { 'class': 'lldpd-protocol-badge lldpd-protocol-sonmp' }, v); + else + return E('span', { 'class': 'lldpd-protocol-badge' }, v); + }, + + /** @private */ + renderNumber: function(v) { + if (parseInt(v)) + return v; + + return '–'; + }, + + /** @private */ + renderPort: function(port) { + if (typeof port.port !== 'undefined') + { + if (typeof port.port[0].descr !== 'undefined' && + typeof port.port[0].id[0].value !== 'undefined' && + port.port[0].descr[0].value !== port.port[0].id[0].value) + { + return [ + E('strong', {}, port.port[0].descr[0].value), + E('br', {}), + port.port[0].id[0].value + ]; + } + else + { + if (typeof port.port[0].descr !== 'undefined') + return port.port[0].descr[0].value; + else + return port.port[0].id[0].value; + } + } + else + { + return '%s'.format(port.name); + } + }, + + /** @private */ + renderPortParamTableShort: function(port) { + var items = []; + + items.push(this.renderParam(_('Name'), port.name)); + items.push(this.renderParam(_('Age'), this.renderAge(port.age))); + + return E('div', { 'class': 'lldpd-params' }, items); + }, + + /** @private */ + renderPortParamTable: function(port, only_id_and_ttl) { + var items = []; + + if (!only_id_and_ttl) { + items.push(this.renderParam(_('Name'), port.name)); + items.push(this.renderParam(_('Age'), this.renderAge(port.age))); + } + + if (typeof port.port !== 'undefined') + { + if (typeof port.port[0].id !== 'undefined') + { + items.push(this.renderParam(_('Port ID'), + port.port[0].id[0].value)); + + items.push(this.renderParam(_('Port ID type'), + this.renderIdType(port.port[0].id[0].type))); + } + + if (typeof port.port[0].descr !== 'undefined') + items.push(this.renderParam(_('Port description'), + port.port[0].descr[0].value)); + + if (typeof port.ttl !== 'undefined') + items.push(this.renderParam(_('TTL'), port.ttl[0].ttl)); + else if (port.port[0].ttl !== 'undefined') + items.push(this.renderParam(_('TTL'), port.port[0].ttl[0].value)); + } + + return E('div', { 'class': 'lldpd-params' }, items); + }, + + /** @private */ + renderChassis: function(ch) { + if (typeof ch.name !== 'undefined' && + typeof ch.descr !== 'undefined' && + typeof ch.name[0].value !== 'undefined' && + typeof ch.descr[0].value !== 'undefined') + { + return [ + E('strong', {}, ch.name[0].value), + E('br', {}), + ch.descr[0].value + ]; + } + else if (typeof ch.name !== 'undefined' && + typeof ch.name[0].value !== 'undefined') + return E('strong', {}, ch.name[0].value); + else if (typeof ch.descr !== 'undefined' && + typeof ch.descr[0].value !== 'undefined') + return ch.descr[0].value; + else if (typeof ch.id !== 'undefined' && + typeof ch.id[0].value !== 'undefined') + return ch.id[0].value; + else + return _('Unknown'); + }, + + /** @private */ + renderChassisParamTable: function(ch) { + var items = []; + + if (typeof ch.name !== 'undefined') + items.push(this.renderParam(_('Name'), ch.name[0].value)); + + if (typeof ch.descr !== 'undefined') + items.push(this.renderParam(_('Description'), ch.descr[0].value)); + + if (typeof ch.id !== 'undefined') { + items.push(this.renderParam(_('ID'), ch.id[0].value)); + items.push(this.renderParam(_('ID type'), + this.renderIdType(ch.id[0].type))); + } + + // Management addresses + if (typeof ch['mgmt-ip'] !== 'undefined') { + var ips = ''; + + if (ch['mgmt-ip'].length > 0) { + // Array of addresses + for (var ip = 0; ip < ch["mgmt-ip"].length; ip++) + ips += ch['mgmt-ip'][ip].value + '
'; + } + else { + // One address + ips += ch['mgmt-ip'][0].value; + } + + items.push(this.renderParam(_('Management IP(s)'), ips)); + } + + if (typeof ch.capability !== 'undefined') { + var caps = ''; + + if (ch.capability.length > 0) + { + // Array of capabilities + for (var cap = 0; cap < ch.capability.length; cap++) { + caps += ch.capability[cap].type; + caps += ' (' + (ch.capability[cap].enabled + ? _('enabled') : _('disabled')) + ')'; + caps += '
'; + } + } + else + { + // One capability + caps += ch.capability[0].type; + caps += ' (' + (ch.capability[0].enabled + ? _('enabled') : _('disabled')) + ')'; + } + + items.push(this.renderParam(_('Capabilities'), caps)); + } + + return E('div', { 'class': 'lldpd-params' }, items); + }, + + /** @private */ + getFoldingImage: function(unfolded) { + return L.resource('lldpd/details_' + + (unfolded ? 'hide' : 'show') + '.svg'); + }, + + /** @private */ + generateRowId: function(str) { + return str.replace(/[^a-z0-9]/gi, '-'); + }, + + /** @private */ + handleToggleFoldingRow: function(row, row_id) { + var e_img = row.querySelector('img'); + var e_folded = row.querySelectorAll('.lldpd-folded'); + var e_unfolded = row.querySelectorAll('.lldpd-unfolded'); + + console.log(row); + console.log(row_id); + + if (e_folded.length != e_unfolded.length) + return; + + var do_unfold = (e_folded[0].style.display !== 'none'); + this.rowsUnfolded[row_id] = do_unfold; + + for (var i = 0; i < e_folded.length; i++) + { + if (do_unfold) + { + e_folded[i].style.display = 'none'; + e_unfolded[i].style.display = null; + } + else + { + e_folded[i].style.display = null; + e_unfolded[i].style.display = 'none'; + } + } + + e_img.src = this.getFoldingImage(do_unfold); + }, + + /** @private */ + makeFoldingTableRow: function(row, unfolded) { + // + // row[0] - row id + // row[1] - contents for first cell in row + // row[2] - contents for second cell in row + // ... + // row[N] - contents for N-th cell in row + // + if (row.length < 2) + return row; + + for (let i = 1; i < row.length; i++) { + if (i == 1) { + // Fold/unfold image appears only in first column + var dImg = E('div', { 'style': 'padding: 0 8px 0 0;' }, [ + E('img', { 'width': '16px', 'src': this.getFoldingImage(unfolded) }), + ]); + } + + if (Array.isArray(row[i])) { + // row[i][0] = folded contents + // row[i][1] = unfolded contents + + // Folded cell data + let dFolded = E('div', { + 'class': 'lldpd-folded', + 'style': unfolded ? 'display: none;' : 'display: block;' + }, row[i][0]); + + // Unfolded cell data + let dUnfolded = E('div', { + 'class': 'lldpd-unfolded', + 'style': unfolded ? 'display: block;' : 'display: none;' + }, row[i][1]); + + if (i == 1) { + row[i] = E('div', { + 'style': 'display: flex; flex-wrap: nowrap;' + }, [ dImg, dFolded, dUnfolded ]); + } + else { + row[i] = E('div', {}, [ dFolded, dUnfolded ]); + } + } + else { + // row[i] = same content for folded and unfolded states + + if (i == 1) { + row[i] = E('div', { + 'style': 'display: flex; flex-wrap: nowrap;' + }, [ dImg, E('div', row[i]) ]); + } + } + } + + return row; + }, + + /** @private */ + makeNeighborsTableRow: function(obj) { + if (typeof obj === 'undefined') + obj.name = 'Unknown'; + + var new_id = obj.name + '-' + obj.rid; + + if (typeof obj.port !== 'undefined') { + if (typeof obj.port[0].id !== 'undefined') + new_id += "-" + obj.port[0].id[0].value; + + if (typeof obj.port[0].descr !== 'undefined') + new_id += "-" + obj.port[0].descr[0].value; + } + + var row_id = this.generateRowId(new_id); + + return this.makeFoldingTableRow([ + row_id, + [ + '%s'.format(obj.name), + this.renderPortParamTableShort(obj) + ], + this.renderProtocol(obj.via), + [ + this.renderChassis(obj.chassis[0]), + this.renderChassisParamTable(obj.chassis[0]) + ], + [ + this.renderPort(obj), + this.renderPortParamTable(obj, true) + ] + ], this.rowsUnfolded[row_id] || false); + }, + + /** @private */ + makeStatisticsTableRow: function(sobj, iobj) { + var row_id = this.generateRowId(iobj.name); + + return this.makeFoldingTableRow([ + row_id, + [ + this.renderPort(iobj), // folded + this.renderPortParamTable(iobj, false) // unfolded + ], + this.renderProtocol(iobj.via), + this.renderNumber(sobj.tx[0].tx), + this.renderNumber(sobj.tx[0].rx), + this.renderNumber(sobj.rx_discarded_cnt[0].rx_discarded_cnt), + this.renderNumber(sobj.rx_unrecognized_cnt[0].rx_unrecognized_cnt), + this.renderNumber(sobj.ageout_cnt[0].ageout_cnt), + this.renderNumber(sobj.insert_cnt[0].insert_cnt), + this.renderNumber(sobj.delete_cnt[0].delete_cnt) + ], this.rowsUnfolded[row_id] || false); + }, + + /** @private */ + updateTable: function(table, data, placeholder) { + var target = isElem(table) ? table : document.querySelector(table); + + if (!isElem(target)) + return; + + target.querySelectorAll( + '.tr.table-titles, .cbi-section-table-titles').forEach(L.bind(function(thead) { + var titles = []; + + thead.querySelectorAll('.th').forEach(function(th) { + titles.push(th); + }); + + if (Array.isArray(data)) { + var n = 0, rows = target.querySelectorAll('.tr'); + + data.forEach(L.bind(function(row) { + var id = row[0]; + var trow = E('div', { 'class': 'tr', 'click': L.bind(function(ev) { + this.handleToggleFoldingRow(ev.currentTarget, id); + // lldpd_folding_toggle(ev.currentTarget, id); + }, this) }); + + for (var i = 0; i < titles.length; i++) { + var text = (titles[i].innerText || '').trim(); + var td = trow.appendChild(E('div', { + 'class': titles[i].className, + 'data-title': (text !== '') ? text : null + }, row[i + 1] || '')); + + td.classList.remove('th'); + td.classList.add('td'); + } + + trow.classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1)); + + if (rows[n]) + target.replaceChild(trow, rows[n]); + else + target.appendChild(trow); + }, this)); + + while (rows[++n]) + target.removeChild(rows[n]); + + if (placeholder && target.firstElementChild === target.lastElementChild) { + var trow = target.appendChild( + E('div', { 'class': 'tr placeholder' })); + + var td = trow.appendChild( + E('div', { 'class': 'center ' + titles[0].className }, placeholder)); + + td.classList.remove('th'); + td.classList.add('td'); + } + } else { + thead.parentNode.style.display = 'none'; + + thead.parentNode.querySelectorAll('.tr, .cbi-section-table-row').forEach(function(trow) { + if (trow !== thead) { + var n = 0; + trow.querySelectorAll('.th, .td').forEach(function(td) { + if (n < titles.length) { + var text = (titles[n++].innerText || '').trim(); + if (text !== '') + td.setAttribute('data-title', text); + } + }); + } + }); + + thead.parentNode.style.display = ''; + } + }, this)); + }, + + /** @private */ + startPolling: function() { + poll.add(L.bind(function() { + return callLLDPStatus().then(L.bind(function(data) { + this.renderData(data); + }, this)); + }, this)); + }, + + /** @private */ + renderDataLocalChassis: function(data) { + if (data && + typeof data !== 'undefined' && + typeof data['local-chassis'] !== 'undefined' && + typeof data['local-chassis'][0].chassis[0].name !== 'undefined') { + return this.renderChassisParamTable(data['local-chassis'][0].chassis[0]); + } + else { + return E('div', { 'class': 'alert-message warning' }, + _('No data to display')); + } + }, + + /** @private */ + renderDataNeighbors: function(neighbors) { + var rows = []; + + if (neighbors && + typeof neighbors !== 'undefined' && + typeof neighbors.lldp !== 'undefined') + { + var ifaces = neighbors.lldp[0].interface; + + // Fill table rows + if (typeof ifaces !== 'undefined') { + for (i = 0; i < ifaces.length; i++) + rows.push(this.makeNeighborsTableRow(ifaces[i])); + } + } + + return rows; + }, + + /** @private */ + renderDataStatistics: function(statistics, interfaces) { + var rows = []; + + if (statistics && + interfaces && + typeof statistics !== 'undefined' && + typeof interfaces !== 'undefined' && + typeof statistics.lldp !== 'undefined' && + typeof interfaces.lldp !== 'undefined') + { + var sifaces = statistics.lldp[0].interface; + var ifaces = interfaces.lldp[0].interface; + + if ((typeof sifaces !== 'undefined') && + (typeof ifaces !== 'undefined')) { + for (var i = 0; i < sifaces.length; i++) + rows.push(this.makeStatisticsTableRow(sifaces[i], ifaces[i])); + } + } + + return rows; + }, + + /** @private */ + renderData: function(data) { + var r; + + r = this.renderDataLocalChassis(data.chassis); + dom.content(document.getElementById('lldpd-local-chassis'), r); + + r = this.renderDataNeighbors(data.neighbors); + this.updateTable(this.tableNeighbors, r, + _('No data to display')); + + r = this.renderDataStatistics(data.statistics, data.interfaces); + this.updateTable(this.tableStatistics, r, + _('No data to display')); + }, + + render: function(data) { + var m, s, ss, o; + + m = new form.JSONMap(dataMap, + _('LLDP Status'), + _('This page allows you to see discovered LLDP neighbors, ' + + 'local interfaces statistics and local chassis information.')); + + s = m.section(form.NamedSection, 'local', 'local', + _('Local Chassis')); + + o = s.option(form.DummyValue, 'localChassis'); + o.render = function() { + return E('div', { 'id': 'lldpd-local-chassis' }, [ + E('em', { 'class': 'spinning' }, _('Collecting data...')) + ]); + }; + + s = m.section(form.NamedSection, 'remote', 'remote'); + + s.tab('neighbors', _('Discovered Neighbors')); + s.tab('statistics', _('Interface Statistics')); + + o = s.taboption('neighbors', form.DummyValue, 'neighbors'); + o.render = L.bind(function() { + return E('div', { 'class': 'table-wrapper' }, [ + this.tableNeighbors + ]); + }, this); + + o = s.taboption('statistics', form.DummyValue, 'statistics'); + o.render = L.bind(function() { + return E('div', { 'class': 'table-wrapper' }, [ + this.tableStatistics + ]); + }, this); + + return m.render().then(L.bind(function(rendered) { + this.startPolling(); + return rendered; + }, this)); + }, + + addFooter: function() { + return [ + this.super('addFooter', arguments), + lldpd.renderFooter() + ]; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luasrc/controller/lldpd.lua b/luasrc/controller/lldpd.lua deleted file mode 100644 index 701f9c8..0000000 --- a/luasrc/controller/lldpd.lua +++ /dev/null @@ -1,55 +0,0 @@ --- --- Copyright (c) 2018, Tano Systems. All Rights Reserved. --- Anton Kikin --- - -module("luci.controller.lldpd", package.seeall) - -require("luci.sys") - -function index() - if not nixio.fs.access("/etc/config/lldpd") then - return - end - - entry({"admin", "services", "lldpd"}, firstchild(), _("LLDP"), 80) - - entry({"admin", "services", "lldpd", "status"}, template("lldpd/status"), _("Status"), 10) - entry({"admin", "services", "lldpd", "config"}, cbi("lldpd/config"), _("Settings"), 20) - - entry({"admin", "services", "lldpd", "get_info"}, - call("action_get_info")) -end - --- LLDPCLI commands - -function lldpcli_show(section, format) - if not format then - format = "plain" - end - - return luci.util.exec("lldpcli show " .. section .. " -f " .. format) -end - --- Info - -function action_get_info() - - luci.http.prepare_content("application/json") - - luci.http.write('{"statistics":'); - luci.http.write(lldpcli_show("statistics", "json0")) - luci.http.write(','); - - luci.http.write('"neighbors":'); - luci.http.write(lldpcli_show("neighbors", "json0")) - luci.http.write(','); - - luci.http.write('"interfaces":'); - luci.http.write(lldpcli_show("interfaces", "json0")) - luci.http.write(','); - - luci.http.write('"chassis":'); - luci.http.write(lldpcli_show("chassis", "json0")) - luci.http.write('}'); -end diff --git a/luasrc/lldpd.lua b/luasrc/lldpd.lua deleted file mode 100644 index 67d6f7d..0000000 --- a/luasrc/lldpd.lua +++ /dev/null @@ -1,17 +0,0 @@ --- --- Copyright (c) 2018, Tano Systems. All Rights Reserved. --- Anton Kikin --- - -module "luci.lldpd" - -local app_version = "1.3.0" -local app_home = "https://github.com/tano-systems/luci-app-tn-lldpd" - -function version() - return app_version -end - -function home() - return app_home -end diff --git a/luasrc/model/cbi/lldpd/config.lua b/luasrc/model/cbi/lldpd/config.lua deleted file mode 100644 index 960122b..0000000 --- a/luasrc/model/cbi/lldpd/config.lua +++ /dev/null @@ -1,446 +0,0 @@ --- --- Copyright (c) 2018, Tano Systems. All Rights Reserved. --- Anton Kikin --- - -local sys = require "luci.sys" -local util = require "luci.util" - -local m, s, o - -m = Map("lldpd", - translate("LLDPd Settings"), - translate( - "LLDPd is a implementation of IEEE 802.1ab " .. - "(LLDP)." - ) .. - translate( - "On this page you may configure LLDPd parameters." - ) -) - -s = m:section(TypedSection, "lldpd") -s.anonymous = true -s.addremove = false - -s:tab("basic", translate("Basic Settings")) -s:tab("ifaces", translate("Network Interfaces")) -s:tab("advanced", translate("Advanced Settings")) -s:tab("protocols", translate("Protocols Support")) - ------------------------------------------------------------------------------------ --- --- Basic settings --- ------------------------------------------------------------------------------------ - --- Service enable/disable -o = s:taboption("basic", Flag, "enabled", - translate("Enable service") -) - -o.rmempty = false - -function o.cfgvalue(self, section) - return sys.init.enabled("lldpd") and self.enabled or self.disabled -end - -function o.write(self, section, value) - if value == "1" then - sys.init.enable("lldpd") - sys.call("/etc/init.d/lldpd start >/dev/null 2>&1") - else - sys.call("/etc/init.d/lldpd stop >/dev/null 2>&1") - sys.init.disable("lldpd") - end - - return Flag.write(self, section, value) -end - --- System description -o = s:taboption("basic", Value, "lldp_description", - translate("System description"), - translate( - "Override system description with the provided description." - ) -) - -o.placeholder = "System description" - --- System hostname -o = s:taboption("basic", Value, "lldp_hostname", - translate("System hostname"), - translate( - "Override system hostname with the provided value." - ) -) - -o.placeholder = "System hostname" - --- Platform -o = s:taboption("basic", Value, "lldp_platform", - translate("System platform description"), - translate( - "Override the platform description with the provided value. " .. - "The default description is the kernel name (Linux)." - ) -) - -o.placeholder = "System platform description" - --- Management addresses of this system -o = s:taboption("basic", Value, "lldp_sys_mgmt_ip", - translate("Management addresses of this system"), - translate( - "Specify the management addresses of this system. " .. - "If not specified, the first IPv4 and the first " .. - "IPv6 are used. If an exact IP address is provided, it is used " .. - "as a management address without any check. If you want to " .. - "blacklist IPv6 addresses, you can use !*:*. " .. - "See more details about available patterns " .. - "here." - ) -) - -o.placeholder = "Management addresses" - --- LLDP tx interval -o = s:taboption("basic", Value, "lldp_tx_interval", - translate("Transmit delay"), - translate( - "The transmit delay is the delay between two " .. - "transmissions of LLDP PDU. The default value " .. - "is 30 seconds." - ) -) - -o.datatype = "uinteger" -o.default = 30 -o.placeholder = 30 -o.rmempty = false - -function o.validate(self, value, section) - if tonumber(value) <= 0 then - return nil, translate("Transmit delay must be greater than 0") - end - return Value.validate(self, value, section) -end - --- LLDP tx hold -o = s:taboption("basic", Value, "lldp_tx_hold", - translate("Transmit hold value"), - translate( - "This value is used to compute the TTL of transmitted " .. - "packets which is the product of this value and of the " .. - "transmit delay. The default value is 4 and therefore " .. - "the default TTL is 120 seconds." - ) -) - -o.datatype = "uinteger" -o.default = 4 -o.placeholder = 4 -o.rmempty = false - -function o.validate(self, value, section) - if tonumber(value) <= 0 then - return nil, translate("Transmit hold value must be greater than 0") - end - return Value.validate(self, value, section) -end - --- Received-only mode (-r) -o = s:taboption("basic", Flag, "readonly_mode", - translate("Enable receive-only mode"), - translate( - "With this option, LLDPd will not send any frames. " .. - "It will only listen to neighbors." - ) -) - -o.default = "0" - ------------------------------------------------------------------------------------ --- --- Network Interfaces --- ------------------------------------------------------------------------------------ - --- Interfaces to listen on -o = s:taboption("ifaces", DynamicList, "interface", - translate("Network interfaces"), - translate( - "Specify which interface to listen and send LLDPDU to." .. - "If no interfaces is specified, LLDPd will use all available physical interfaces." - ) -) - -o.template = "cbi/network_ifacelist" -o.nobridges = true -o.rmempty = true -o.nocreate = true -o.network = "" -o.widget = "checkbox" - --- ChassisID interfaces -o = s:taboption("ifaces", DynamicList, "cid_interface", - translate("Network interfaces for chassis ID computing"), - translate( - "Specify which interfaces to use for computing chassis ID. " .. - "If no interfaces is specified, all interfaces are considered. " .. - "LLDPd will take the first MAC address from all the considered " .. - "interfaces to compute the chassis ID." - ) -) - -o.template = "cbi/network_ifacelist" -o.nobridges = false -o.rmempty = true -o.nocreate = true -o.network = "" -o.widget = "checkbox" - - ------------------------------------------------------------------------------------ --- --- Advanced Settings --- ------------------------------------------------------------------------------------ - --- SNMP agentX socket -o = s:taboption("advanced", Value, "agentxsocket", - translate("SNMP agentX socket path"), - translate( - "If the path to the socket is set, then LLDPd will enable an " .. - "SNMP subagent using AgentX protocol. This allows you to get " .. - "information about local system and remote systems through SNMP." - ) -) - -o.rmempty = false -o.placeholder = "/var/run/agentx.sock" -o.default = "/var/run/agentx.sock" - - --- LLDP class -o = s:taboption("advanced", ListValue, "lldp_class", - translate("LLDP-MED device class") -) - -o:value(1, translate("Generic Endpoint (Class I)")) -o:value(2, translate("Media Endpoint (Class II)")) -o:value(3, translate("Communication Device Endpoints (Class III)")) -o:value(4, translate("Network Connectivity Device (Class IV)")) - -o.default = 4 - --- LLDP-MED inventory TLV transmission (-i) -o = s:taboption("advanced", Flag, "lldpmed_no_inventory", - translate("Disable LLDP-MED inventory TLV transmission"), - translate( - "LLDPd will still receive (and publish using SNMP if enabled) " .. - "those LLDP-MED TLV but will not send them. Use this option " .. - "if you don't want to transmit sensible information like serial numbers." - ) -) - -o.default = "0" - --- Filter neighbors (-H) -o = s:taboption("advanced", Value, "filter", - translate("Specify the behaviour when detecting multiple neighbors"), - translate( - "The default filter is 15. For more details see \"FILTERING NEIGHBORS\" section " .. - "here." - ) -) - -o.template = "lldpd/filter_select" -o.default = 15 - --- Force port ID subtype -o = s:taboption("advanced", ListValue, "lldp_portidsubtype", - translate("Force port ID subtype"), - translate( - "With this option, you can force the port identifier " .. - "to be the interface name or the MAC address." - ) -) - -o:value("macaddress", translate("Interface MAC address")) -o:value("ifname", translate("Interface name")) - -o.default = "macaddress" - --- The destination MAC address used to send LLDPDU -o = s:taboption("advanced", ListValue, "lldp_agenttype", - translate("The destination MAC address used to send LLDPDU"), - translate( - "The destination MAC address used to send LLDPDU allows an agent " .. - "to control the propagation of LLDPDUs. By default, the " .. - "01:80:c2:00:00:0e MAC address is used and limit the propagation " .. - "of the LLDPDU to the nearest bridge." - ) -) - -o:value("nearest-bridge", "01:80:c2:00:00:0e (nearest-bridge)") -o:value("nearest-nontpmr-bridge", "01:80:c2:00:00:03 (nearest-nontpmr-bridge)") -o:value("nearest-customer-bridge", "01:80:c2:00:00:00 (nearest-customer-bridge)") - -o.default = "nearest-bridge" - ------------------------------------------------------------------------------------ --- --- Protocols Support --- ------------------------------------------------------------------------------------ - -local function make_proto_subtitle(s, title) - local t = s:taboption("protocols", DummyValue, "_cdp", " ") - t.default = '

' .. title .. "

" - t.rawhtml = true -end - --- LLDP -make_proto_subtitle(s, translate("LLDP protocol")) - -o = s:taboption("protocols", Flag, "enable_lldp", - translate("Enable LLDP") -) - -o.default = "1" -o.rmempty = true - --- -o = s:taboption("protocols", Flag, "force_lldp", - translate("Force to send LLDP packets"), - translate( - "Force to send LLDP packets even when there is no LLDP peer " .. - "detected but there is a peer speaking another protocol detected. " .. - "By default, LLDP packets are sent when there is a peer speaking " .. - "LLDP detected or when there is no peer at all." - ) -) - -o.default = "0" -o.rmempty = true -o:depends("enable_lldp", true) - --- CDP -make_proto_subtitle(s, translate("CDP protocol")) - -o = s:taboption("protocols", Flag, "enable_cdp", - translate("Enable CDP"), - translate( - "Enable the support of CDP protocol to deal with Cisco routers " .. - "that do not speak LLDP" - ) -) - -o.default = "1" -o.rmempty = false - --- -o = s:taboption("protocols", ListValue, "cdp_version", - translate("CDP version")) - -o:value("cdpv1v2", translate("CDPv1 and CDPv2")) -o:value("cdpv2", translate("Only CDPv2")) -o:depends("enable_cdp", true) - -o.default = "cdpv1v2" - --- -o = s:taboption("protocols", Flag, "force_cdp", - translate("Send CDP packets even if no CDP peer detected")) - -o.default = "0" -o.rmempty = true -o:depends("enable_cdp", true) - -o = s:taboption("protocols", Flag, "force_cdpv2", - translate("Force sending CDPv2 packets")) - -o.default = "0" -o.rmempty = true -o:depends({ - force_cdp = true, - enable_cdp = true, - cdp_version = "cdpv1v2" -}) - --- FDP -make_proto_subtitle(s, translate("FDP protocol")) - -o = s:taboption("protocols", Flag, "enable_fdp", - translate("Enable FDP"), - translate( - "Enable the support of FDP protocol to deal with Foundry routers " .. - "that do not speak LLDP" - ) -) - -o.default = "1" -o.rmempty = false - --- -o = s:taboption("protocols", Flag, "force_fdp", - translate("Send FDP packets even if no FDP peer detected")) - -o.default = "0" -o.rmempty = true -o:depends("enable_fdp", true) - --- EDP -make_proto_subtitle(s, translate("EDP protocol")) - -o = s:taboption("protocols", Flag, "enable_edp", - translate("Enable EDP"), - translate( - "Enable the support of EDP protocol to deal with Extreme routers " .. - "and switches that do not speak LLDP." - ) -) - -o.default = "1" -o.rmempty = false - --- -o = s:taboption("protocols", Flag, "force_edp", - translate("Send EDP packets even if no EDP peer detected")) - -o.default = "0" -o.rmempty = true -o:depends("enable_edp", true) - --- SONMP -make_proto_subtitle(s, translate("SONMP protocol")) - -o = s:taboption("protocols", Flag, "enable_sonmp", - translate("Enable SONMP"), - translate( - "Enable the support of SONMP protocol to deal with Nortel " .. - "routers and switches that do not speak LLDP." - ) -) - -o.default = "1" -o.rmempty = false - --- -o = s:taboption("protocols", Flag, "force_sonmp", - translate("Send SONMP packets even if no SONMP peer detected")) - -o.default = "0" -o.rmempty = true -o:depends("enable_sonmp", true) - ------------------------------------------------------------------------------------ - -local hide_footer = m.uci:get_bool("luci", "app_tn_lldpd", "hide_footer") or false -if hide_footer == false then - s = m:section(SimpleSection, nil) - s.template = "lldpd/footer" -end - -return m diff --git a/luasrc/view/lldpd/filter_select.htm b/luasrc/view/lldpd/filter_select.htm deleted file mode 100644 index 8e2b5dc..0000000 --- a/luasrc/view/lldpd/filter_select.htm +++ /dev/null @@ -1,151 +0,0 @@ -<%# - Copyright (c) 2018, Tano Systems. All Rights Reserved. - Anton Kikin --%> - -<% ---[[ - Filter neighbors (-H) - - The filter column means that filtering is enabled - The 1proto column tells that only one protocol will be kept. - The 1neigh column tells that only one neighbor will be kept. - - incoming outgoing - filter 1proto 1neigh filter 1proto 1neigh - 0 - 1 x x x x - 2 x x - 3 x x - 4 x x - 5 x - 6 x - 7 x x x x x - 8 x x x - 9 x x x x - 10 x x - 11 x x - 12 x x x x - 13 x x x - 14 x x x x - 15 x x x - 16 x x x x x - 17 x x x x - 18 x x x - 19 x x x ---]] -%> - -<%+cbi/valueheader%> - - - -<% local selected = tonumber((self:cfgvalue(section) or self.default)) %> - - - - - - - - - - - - - - - - - - - - - - <% - local filter = { - { 0, 0, 0, 0, 0, 0 }, - { 1, 1, 0, 1, 1, 0 }, - { 1, 1, 0, 0, 0, 0 }, - { 0, 0, 0, 1, 1, 0 }, - { 1, 0, 0, 1, 0, 0 }, - { 1, 0, 0, 0, 0, 0 }, - { 0, 0, 0, 1, 0, 0 }, - { 1, 1, 1, 1, 1, 0 }, - { 1, 1, 1, 0, 0, 0 }, - { 1, 0, 1, 1, 1, 0 }, - { 0, 0, 0, 1, 0, 1 }, - { 1, 0, 1, 0, 0, 0 }, - { 1, 0, 1, 1, 0, 1 }, - { 1, 0, 1, 1, 0, 0 }, - { 1, 1, 0, 1, 0, 1 }, - { 1, 1, 0, 1, 0, 0 }, - { 1, 1, 1, 1, 0, 1 }, - { 1, 1, 1, 1, 0, 0 }, - { 1, 0, 0, 1, 0, 1 }, - { 1, 0, 0, 1, 1, 0 } - } - - local _, idx, f - - local function filter_value(v) - if v and v == 1 then - return '✔' - end - - return '' - end - - for idx, f in pairs(filter) do - local i - local fid = idx - 1 - - %> - " onclick="lldpd_filter_onclick(this, '<%=fid%>')" <% - if fid == selected then %>class="lldpd-filter-selected"<% end - %>><% - - for i = 1,6 do - %><% - end - - %><% - end - %> - -
<%:Filter%><%:Incoming%><%:Outgoing%>
EPNEPN
/><%=fid%><%=filter_value(f[i])%>
- -<% -self.description = self.description or '' -self.description = - '
  • ' .. translate("E — enable filter") .. '
  • ' .. - '
  • ' .. translate("P — keep only one protocol") .. '
  • ' .. - '
  • ' .. translate("N — keep only one neighbor") .. '
  • ' .. - '
' .. self.description -%> -<%+cbi/valuefooter%> diff --git a/luasrc/view/lldpd/footer.htm b/luasrc/view/lldpd/footer.htm deleted file mode 100644 index db9c3a6..0000000 --- a/luasrc/view/lldpd/footer.htm +++ /dev/null @@ -1,10 +0,0 @@ -<%# - Copyright (c) 2018, Tano Systems. All Rights Reserved. - Anton Kikin --%> - -<% lldpd = require "luci.lldpd" %> -

diff --git a/luasrc/view/lldpd/status.htm b/luasrc/view/lldpd/status.htm deleted file mode 100644 index 27d5872..0000000 --- a/luasrc/view/lldpd/status.htm +++ /dev/null @@ -1,362 +0,0 @@ -<%# - Copyright (c) 2018, Tano Systems. All Rights Reserved. - Anton Kikin --%> - -<%+header%> - - - -

<%:LLDPd Status%>

-
<%:This page allows you to see discovered LLDP neighbors, local interfaces statistics and local chassis information.%>
- - - -
- -
-

<%:Discovered neighbors table%> (0)

-
-
-
<%:Local interface%>
-
<%:Protocol%>
-
<%:Discovered chassis%>
-
<%:Discovered port%>
-
-
-
- <%:Collecting data...%> -
-
-
-
- -
-

<%:Local interfaces statistics table%>

-
-
-
<%:Local interface%>
-
<%:Protocol%>
-
<%:Tx%>
-
<%:Rx%>
-
<%:Rx discarded%>
-
<%:Rx unrecognized%>
-
<%:Ageout count%>
-
<%:Insert count%>
-
<%:Delete count%>
-
-
-
- <%:Collecting data...%> -
-
-
-
- -
-

<%:Local chassis information%>

-
<%:Collecting data...%>
-
- -
- - - - - -<% - local uci = require("luci.model.uci").cursor() - local hide_footer = uci:get_bool("luci", "app_tn_lldpd", "hide_footer") or false - if hide_footer == false then -%> - <%+lldpd/footer%> -<% end %> - -<%+footer%> diff --git a/root/usr/libexec/rpcd/lldpd b/root/usr/libexec/rpcd/lldpd new file mode 100644 index 0000000..0584854 --- /dev/null +++ b/root/usr/libexec/rpcd/lldpd @@ -0,0 +1,94 @@ +#!/usr/bin/env lua +-- +-- Copyright (c) 2020, Tano Systems. All Rights Reserved. +-- Anton Kikin +-- + +local json = require "luci.jsonc" +local util = require("luci.util") + +-- LLDPCLI commands +local function lldpcli_show(section, format) + if not format then + format = "plain" + end + + return util.exec("lldpcli show " .. section .. " -f " .. format) +end + +local methods = { + getStatus = { + args = { bridge = "string" }, + call = function(args) + local status = { + statistics = json.parse(lldpcli_show("statistics", "json0")), + neighbors = json.parse(lldpcli_show("neighbors", "json0")), + interfaces = json.parse(lldpcli_show("interfaces", "json0")), + chassis = json.parse(lldpcli_show("chassis", "json0")) + } + + return status + end + } +} + +local function parseInput() + local parse = json.new() + local done, err + + while true do + local chunk = io.read(4096) + if not chunk then + break + elseif not done and not err then + done, err = parse:parse(chunk) + end + end + + if not done then + print(json.stringify({ error = err or "Incomplete input" })) + os.exit(1) + end + + return parse:get() +end + +local function validateArgs(func, uargs) + local method = methods[func] + if not method then + print(json.stringify({ error = "Method not found" })) + os.exit(1) + end + + if type(uargs) ~= "table" then + print(json.stringify({ error = "Invalid arguments" })) + os.exit(1) + end + + uargs.ubus_rpc_session = nil + + local k, v + local margs = method.args or {} + for k, v in pairs(uargs) do + if margs[k] == nil or + (v ~= nil and type(v) ~= type(margs[k])) + then + print(json.stringify({ error = "Invalid arguments" })) + os.exit(1) + end + end + + return method +end + +if arg[1] == "list" then + local _, method, rv = nil, nil, {} + for _, method in pairs(methods) do rv[_] = method.args or {} end + print((json.stringify(rv):gsub(":%[%]", ":{}"))) +elseif arg[1] == "call" then + local args = parseInput() + local method = validateArgs(arg[2], args) + local result, code = method.call(args) + print((json.stringify(result):gsub("^%[%]$", "{}"))) + os.exit(code or 0) +end diff --git a/root/usr/share/luci/menu.d/luci-app-tn-lldpd.json b/root/usr/share/luci/menu.d/luci-app-tn-lldpd.json new file mode 100644 index 0000000..bf592d3 --- /dev/null +++ b/root/usr/share/luci/menu.d/luci-app-tn-lldpd.json @@ -0,0 +1,38 @@ +{ + "admin/services/lldpd": { + "title": "LLDP", + "order": 80, + "action": { + "type": "firstchild" + }, + "depends": { + "uci": { + "lldpd": true + } + } + }, + + "admin/services/lldpd/status": { + "title": "Status", + "order": 10, + "action": { + "type": "view", + "path": "lldpd/status" + }, + "depends": { + "acl": [ "luci-app-tn-lldpd-status" ] + } + }, + + "admin/services/lldpd/config": { + "title": "Settings", + "order": 20, + "action": { + "type": "view", + "path": "lldpd/config" + }, + "depends": { + "acl": [ "luci-app-tn-lldpd-config" ] + } + } +} diff --git a/root/usr/share/rpcd/acl.d/luci-app-tn-lldpd.json b/root/usr/share/rpcd/acl.d/luci-app-tn-lldpd.json index 884e991..947db9d 100644 --- a/root/usr/share/rpcd/acl.d/luci-app-tn-lldpd.json +++ b/root/usr/share/rpcd/acl.d/luci-app-tn-lldpd.json @@ -1,11 +1,56 @@ { - "luci-app-tn-lldpd": { - "description": "Grant UCI access for luci-app-tn-lldpd", + "luci-app-tn-lldpd-status": { + "description": "Grant access for LLDP status information", "read": { - "uci": [ "lldpd", "luci" ] + "ubus": { + "lldpd": [ + "getStatus" + ] + }, + "uci": [ + "lldpd", + "luci" + ] + } + }, + + "luci-app-tn-lldpd-config": { + "description": "Grant access for LLDP configuration", + "read": { + "uci": [ + "lldpd", + "luci", + "network", + "wireless", + "firewall" + ], + "ubus": { + "luci": [ + "getInitList" + ], + "luci-rpc": [ + "getBoardJSON", + "getHostHints", + "getNetworkDevices", + "getWirelessDevices" + ], + "network": [ + "get_proto_handlers" + ], + "network.interface": [ + "dump" + ] + } }, "write": { - "uci": [ "lldpd" ] + "uci": [ + "lldpd" + ], + "ubus": { + "luci": [ + "setInitAction" + ] + } } } } diff --git a/screenshots/luci-app-lldpd-settings-advanced.png b/screenshots/luci-app-lldpd-settings-advanced.png deleted file mode 100644 index 3600461..0000000 Binary files a/screenshots/luci-app-lldpd-settings-advanced.png and /dev/null differ diff --git a/screenshots/luci-app-lldpd-settings-basic.png b/screenshots/luci-app-lldpd-settings-basic.png deleted file mode 100644 index 261ce6f..0000000 Binary files a/screenshots/luci-app-lldpd-settings-basic.png and /dev/null differ diff --git a/screenshots/luci-app-lldpd-settings-interfaces.png b/screenshots/luci-app-lldpd-settings-interfaces.png deleted file mode 100644 index 758cfdf..0000000 Binary files a/screenshots/luci-app-lldpd-settings-interfaces.png and /dev/null differ diff --git a/screenshots/luci-app-lldpd-settings-protocols.png b/screenshots/luci-app-lldpd-settings-protocols.png deleted file mode 100644 index 3e9fb1f..0000000 Binary files a/screenshots/luci-app-lldpd-settings-protocols.png and /dev/null differ diff --git a/screenshots/luci-app-lldpd-status-chassis.png b/screenshots/luci-app-lldpd-status-chassis.png deleted file mode 100644 index 9700c36..0000000 Binary files a/screenshots/luci-app-lldpd-status-chassis.png and /dev/null differ diff --git a/screenshots/luci-app-lldpd-status-neighbors.png b/screenshots/luci-app-lldpd-status-neighbors.png deleted file mode 100644 index d0c15a5..0000000 Binary files a/screenshots/luci-app-lldpd-status-neighbors.png and /dev/null differ diff --git a/screenshots/luci-app-lldpd-status-statistics.png b/screenshots/luci-app-lldpd-status-statistics.png deleted file mode 100644 index 1d0eeec..0000000 Binary files a/screenshots/luci-app-lldpd-status-statistics.png and /dev/null differ diff --git a/screenshots/luci-app-tn-lldpd-settings-advanced.png b/screenshots/luci-app-tn-lldpd-settings-advanced.png new file mode 100644 index 0000000..39bc64b Binary files /dev/null and b/screenshots/luci-app-tn-lldpd-settings-advanced.png differ diff --git a/screenshots/luci-app-tn-lldpd-settings-basic.png b/screenshots/luci-app-tn-lldpd-settings-basic.png new file mode 100644 index 0000000..4e401ed Binary files /dev/null and b/screenshots/luci-app-tn-lldpd-settings-basic.png differ diff --git a/screenshots/luci-app-tn-lldpd-settings-interfaces.png b/screenshots/luci-app-tn-lldpd-settings-interfaces.png new file mode 100644 index 0000000..df1860e Binary files /dev/null and b/screenshots/luci-app-tn-lldpd-settings-interfaces.png differ diff --git a/screenshots/luci-app-tn-lldpd-settings-protocols.png b/screenshots/luci-app-tn-lldpd-settings-protocols.png new file mode 100644 index 0000000..64ad808 Binary files /dev/null and b/screenshots/luci-app-tn-lldpd-settings-protocols.png differ diff --git a/screenshots/luci-app-tn-lldpd-status-neighbors.png b/screenshots/luci-app-tn-lldpd-status-neighbors.png new file mode 100644 index 0000000..e9a9c11 Binary files /dev/null and b/screenshots/luci-app-tn-lldpd-status-neighbors.png differ diff --git a/screenshots/luci-app-tn-lldpd-status-statistics.png b/screenshots/luci-app-tn-lldpd-status-statistics.png new file mode 100644 index 0000000..ab1cb5f Binary files /dev/null and b/screenshots/luci-app-tn-lldpd-status-statistics.png differ