From 279720fef7a540d52fa2b138aeef14ba3a4d5dee Mon Sep 17 00:00:00 2001 From: Paul Donald Date: Tue, 1 Apr 2025 18:12:39 +0200 Subject: [PATCH] luci-app-lldpd: custom TLV handling Depends on https://github.com/openwrt/openwrt/pull/14872 Custom TLV have been possible in lldpd; now the machinery is in openwrt to use them. Extend GUI to enable their handling. TLV apply either globally or to specific interfaces. The parsing of TLV strings mirrors that of lldpd. For details see lldpd man page. Signed-off-by: Paul Donald --- .../resources/view/lldpd/config.js | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/applications/luci-app-lldpd/htdocs/luci-static/resources/view/lldpd/config.js b/applications/luci-app-lldpd/htdocs/luci-static/resources/view/lldpd/config.js index 32f18035bc..859ed567b2 100644 --- a/applications/luci-app-lldpd/htdocs/luci-static/resources/view/lldpd/config.js +++ b/applications/luci-app-lldpd/htdocs/luci-static/resources/view/lldpd/config.js @@ -828,6 +828,167 @@ return L.view.extend({ o.depends('enable_sonmp', '1'); }, + // ----------------------------------------------------------------------------------------- + // + // Extra TLV + // + // ----------------------------------------------------------------------------------------- + + /** @private */ + populateExtraTLVOptions: function(s, tab, data) { + let o, s, ss, oo; + + ///// Custom TLV + // # syntax: [add | replace] oui OUI subtype SUBTYPE [oui-info CONTENT] + ///// + + o = s.taboption(tab, form.SectionValue, '_tlv_', form.TableSection, 'custom-tlv', _('Custom TLV'), + _('TLV that lack network interfaces here are sent according to interface pattern (Network IO under Network Interfaces).') + '
' + + _('Default insert behaviour is add.')); + ss = o.subsection; + ss.anonymous = true; + ss.sortable = true; + ss.addremove = true; + ss.rowcolors = true; + ss.addbtntitle = _('Add Custom TLV', 'lldpd Custom TLV'); + oo = ss.option(lldpd.CBIMultiIOSelect, 'ports', + _('Network Interface(s)')); + data[3].forEach(nd => { + oo.value(nd.getName()); + oo.value('!'+nd.getName()); + }); + oo.validate = validateioentries; + + const validateHexBytes = (str, length) => str.split(',').length === length && str.split(',').every(byte => /^[0-9a-f]{2}$/i.test(byte)); + + function parse_custom_tlv_string(input) { + //[add | replace] oui OUI subtype SUBTYPE [oui-info CONTENT] + const pattern = /^(add|replace)?\s*oui\s+([0-9a-f]{2}(?:,[0-9a-f]{2})*)\s+subtype\s+([0-9a-f]{2})(?:\s+oui-info\s+([0-9a-f]{2}(?:,[0-9a-f]{2})*))?$/i; + const match = input.match(pattern); + + if (!match) return null; + + const [, action = '', ouidata, subtype, content = ''] = match; + + if (!validateHexBytes(ouidata, 3) || (content && !validateHexBytes(content, content.split(',').length))) { + return null; + } + + return { action, ouidata, subtype, content }; + } + + function load_custom_tlv(section_id, part) { + const tlv = uci.get('lldpd', section_id, 'tlv')?.trim().toLowerCase(); + if (!tlv) return ''; + + const parsed = parse_custom_tlv_string(tlv); + if (!parsed) return ''; + + switch (part) { + case 'cust_act': return parsed.action; + case 'cust_oui': return parsed.ouidata; + case 'cust_typ': return parsed.subtype; + case 'cust_inf': return parsed.content; + default: return tlv; + } + } + + function write_custom_tlv(section_id) { + const formValue = (part) => this.section.formvalue(section_id, part) || ''; + + const action = formValue('_cust_act'); + const ouidata = formValue('_cust_oui'); + const subtype = formValue('_cust_typ'); + const content = formValue('_cust_inf'); + + const tlvParts = [ + action, + ouidata && `oui ${ouidata}`, + subtype && `subtype ${subtype}`, + content && `oui-info ${content}` + ].filter(Boolean).join(' '); + + uci.set(this.config, section_id, 'tlv', tlvParts); + } + + function intToHexStr(int, uc) { + // return 00-FF for 0-255 int + const str = int.toString(16).padStart(2, '0'); + return uc ? str.toUpperCase() : str; + } + + function validate_custom_tlv_string_format(section_id, input) { + if (!input) return true; + return parse_custom_tlv_string(input) ? true : _('Invalid TLV string format'); + } + + // action + oo = ss.option(form.ListValue, '_cust_act', _('Behaviour')); + oo.value('', _('default')); + oo.value('add', _('add')); + oo.value('replace', _('replace')); + oo.rmempty = true; + oo.write = write_custom_tlv; + oo.load = function(section_id) { + return load_custom_tlv(section_id, 'cust_act'); + }; + + // OUI + oo = ss.option(form.TextValue, '_cust_oui', _('OUI CSV')); + oo.validate = function(section_id, value) { + if (!validateHexBytes(value, 3)) + return _('CSV of 3 hex values, e.g. aa,ee,ff'); + return true; + }; + oo.rmempty = false; + oo.width = 150; + oo.optional = false; + oo.placeholder = '0f,22,4b'; + oo.write = write_custom_tlv; + oo.load = function(section_id) { + return load_custom_tlv(section_id, 'cust_oui'); + }; + + // sub-type + oo = ss.option(form.ListValue, '_cust_typ', _('Subtype')); + const typchoices = Array.from({length: 256}, (_, index) => [index, index + " ("+intToHexStr(index, true)+")"] ); + typchoices.forEach(v => { + oo.value(v[0], v[1]); + }) + oo.rmempty = false; + oo.optional = false; + oo.write = write_custom_tlv; + oo.load = function(section_id) { + return load_custom_tlv(section_id, 'cust_typ'); + }; + + // content + oo = ss.option(form.TextValue, '_cust_inf', _('Content')); + oo.validate = function(section_id, value) { + if (!value) return true; + if (!validateHexBytes(value, value.split(',').length)) + return _('CSV of 1 or more hex values, e.g. aa or bb,cc or 11,55,1a'); + return true; + }; + oo.rmempty = true; + oo.optional = true; + oo.placeholder = 'ff,00,33'; + oo.write = write_custom_tlv; + oo.load = function(section_id) { + return load_custom_tlv(section_id, 'cust_inf'); + }; + + // raw + oo = ss.option(form.TextValue, '_raw', _('Raw')); + oo.write = function(section_id, value) { + uci.set(this.config, section_id, 'tlv', value); + }; + oo.load = function(section_id) { + return load_custom_tlv(section_id); + }; + oo.validate = validate_custom_tlv_string_format; + }, + /** @private */ populateOptions: function(s, data) { var o; @@ -838,6 +999,9 @@ return L.view.extend({ s.tab('ifaces', _('Network Interfaces')); this.populateIfacesOptions(s, 'ifaces', data); + s.tab('tlvtab', _('TLV')); + this.populateExtraTLVOptions(s, 'tlvtab', data); + s.tab('advanced', _('Advanced Settings')); this.populateAdvancedOptions(s, 'advanced', data); -- 2.30.2