diff options
| author | Christian Breunig <christian@breunig.cc> | 2023-01-12 21:13:02 +0100 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-01-12 21:13:02 +0100 | 
| commit | 3045b327585660d66093008833d75a6064c559ea (patch) | |
| tree | 9fb9e50177b303abce246d83c60167f950254b9d | |
| parent | 521fef5f0aded11e2341fe893ecf4b1a206dcfc2 (diff) | |
| parent | bc42dc94cd9d9979e84b4bd6e086893358417c92 (diff) | |
| download | vyos-1x-3045b327585660d66093008833d75a6064c559ea.tar.gz vyos-1x-3045b327585660d66093008833d75a6064c559ea.zip | |
Merge pull request #1756 from c-po/t4911-lldp-op-mode
T4911: op-mode: rewrite LLDP in standardised op-mode format
| -rw-r--r-- | data/op-mode-standardized.json | 1 | ||||
| -rw-r--r-- | op-mode-definitions/lldp.xml.in | 10 | ||||
| -rwxr-xr-x | src/op_mode/lldp.py | 138 | ||||
| -rwxr-xr-x | src/op_mode/lldp_op.py | 127 | 
4 files changed, 141 insertions, 135 deletions
| diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index 2ea4d945a..7c5524675 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -8,6 +8,7 @@  "dhcp.py",  "dns.py",  "interfaces.py", +"lldp.py",  "log.py",  "memory.py",  "nat.py", diff --git a/op-mode-definitions/lldp.xml.in b/op-mode-definitions/lldp.xml.in index 297ccf1f4..07cafa77f 100644 --- a/op-mode-definitions/lldp.xml.in +++ b/op-mode-definitions/lldp.xml.in @@ -11,14 +11,8 @@              <properties>                <help>Show LLDP neighbors</help>              </properties> -            <command>${vyos_op_scripts_dir}/lldp_op.py --all</command> +            <command>${vyos_op_scripts_dir}/lldp.py show_neighbors</command>              <children> -              <node name="detail"> -                <properties> -                  <help>Show LLDP neighbor details</help> -                </properties> -                <command>${vyos_op_scripts_dir}/lldp_op.py --detail</command> -              </node>                <tagNode name="interface">                  <properties>                    <help>Show LLDP for specified interface</help> @@ -26,7 +20,7 @@                      <script>${vyos_completion_dir}/list_interfaces.py</script>                    </completionHelp>                  </properties> -                <command>${vyos_op_scripts_dir}/lldp_op.py --interface $5</command> +                <command>${vyos_op_scripts_dir}/lldp.py show_neighbors --interface $5</command>                </tagNode>              </children>            </node> diff --git a/src/op_mode/lldp.py b/src/op_mode/lldp.py new file mode 100755 index 000000000..dc2b1e0b5 --- /dev/null +++ b/src/op_mode/lldp.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import jmespath +import json +import sys +import typing + +from tabulate import tabulate + +from vyos.configquery import ConfigTreeQuery +from vyos.util import cmd +from vyos.util import dict_search + +import vyos.opmode +unconf_message = 'LLDP is not configured' +capability_codes = """Capability Codes: R - Router, B - Bridge, W - Wlan r - Repeater, S - Station +                  D - Docsis, T - Telephone, O - Other + +""" + +def _verify(func): +    """Decorator checks if LLDP config exists""" +    from functools import wraps + +    @wraps(func) +    def _wrapper(*args, **kwargs): +        config = ConfigTreeQuery() +        if not config.exists(['service', 'lldp']): +            raise vyos.opmode.UnconfiguredSubsystem(unconf_message) +        return func(*args, **kwargs) +    return _wrapper + +def _get_raw_data(interface=None, detail=False): +    """ +    If interface name is not set - get all interfaces +    """ +    tmp = 'lldpcli -f json show neighbors' +    if detail: +        tmp += f' details' +    if interface: +        tmp += f' ports {interface}' +    output = cmd(tmp) +    data = json.loads(output) +    if not data: +        return [] +    return data + +def _get_formatted_output(raw_data): +    data_entries = [] +    for neighbor in dict_search('lldp.interface', raw_data): +        for local_if, values in neighbor.items(): +            tmp = [] + +            # Device field +            if 'chassis' in values: +                tmp.append(next(iter(values['chassis']))) +            else: +                tmp.append('') + +            # Local Port field +            tmp.append(local_if) + +            # Protocol field +            tmp.append(values['via']) + +            # Capabilities +            cap = '' +            capabilities = jmespath.search('chassis.[*][0][0].capability', values) +            if capabilities: +                for capability in capabilities: +                    if capability['enabled']: +                        if capability['type'] == 'Router': +                            cap += 'R' +                        if capability['type'] == 'Bridge': +                            cap += 'B' +                        if capability['type'] == 'Wlan': +                            cap += 'W' +                        if capability['type'] == 'Station': +                            cap += 'S' +                        if capability['type'] == 'Repeater': +                            cap += 'r' +                        if capability['type'] == 'Telephone': +                            cap += 'T' +                        if capability['type'] == 'Docsis': +                            cap += 'D' +                        if capability['type'] == 'Other': +                            cap += 'O' +            tmp.append(cap) + +            # Remote software platform +            platform = jmespath.search('chassis.[*][0][0].descr', values) +            tmp.append(platform[:37]) + +            # Remote interface +            interface = jmespath.search('port.descr', values) +            if not interface: +                interface = jmespath.search('port.id.value', values) +            if not interface: +                interface = 'Unknown' +            tmp.append(interface) + +            # Add individual neighbor to output list +            data_entries.append(tmp) + +    headers = ["Device", "Local Port", "Protocol", "Capability", "Platform", "Remote Port"] +    output = tabulate(data_entries, headers, numalign="left") +    return capability_codes + output + +@_verify +def show_neighbors(raw: bool, interface: typing.Optional[str], detail: typing.Optional[bool]): +    lldp_data = _get_raw_data(interface=interface, detail=detail) +    if raw: +        return lldp_data +    else: +        return _get_formatted_output(lldp_data) + +if __name__ == "__main__": +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except (ValueError, vyos.opmode.Error) as e: +        print(e) +        sys.exit(1) diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py deleted file mode 100755 index 17f6bf552..000000000 --- a/src/op_mode/lldp_op.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019-2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program.  If not, see <http://www.gnu.org/licenses/>. - -import argparse -import jinja2 -import json - -from sys import exit -from tabulate import tabulate - -from vyos.util import cmd -from vyos.config import Config - -parser = argparse.ArgumentParser() -parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces") -parser.add_argument("-d", "--detail", action="store_true", help="Show detailes LLDP neighbor information on all interfaces") -parser.add_argument("-i", "--interface", action="store", help="Show LLDP neighbors on specific interface") - -# Please be careful if you edit the template. -lldp_out = """Capability Codes: R - Router, B - Bridge, W - Wlan r - Repeater, S - Station -                  D - Docsis, T - Telephone, O - Other - -Device ID                 Local     Proto  Cap   Platform             Port ID ----------                 -----     -----  ---   --------             ------- -{% for neighbor in neighbors %} -{%   for local_if, info in neighbor.items() %} -{{ "%-25s" | format(info.chassis) }} {{ "%-9s" | format(local_if) }} {{ "%-6s" | format(info.proto) }} {{ "%-5s" | format(info.capabilities) }} {{ "%-20s" | format(info.platform[:18]) }} {{ info.remote_if }} -{%   endfor %} -{% endfor %} -""" - -def get_neighbors(): -    return cmd('/usr/sbin/lldpcli -f json show neighbors') - -def parse_data(data, interface): -    output = [] -    if not isinstance(data, list): -        data = [data] - -    for neighbor in data: -        for local_if, values in neighbor.items(): -            if interface is not None and local_if != interface: -                continue -            cap = '' -            for chassis, c_value in values.get('chassis', {}).items(): -                # bail out early if no capabilities found -                if 'capability' not in c_value: -                    continue -                capabilities = c_value['capability'] -                if isinstance(capabilities, dict): -                    capabilities = [capabilities] - -                for capability in capabilities: -                    if capability['enabled']: -                        if capability['type'] == 'Router': -                            cap += 'R' -                        if capability['type'] == 'Bridge': -                            cap += 'B' -                        if capability['type'] == 'Wlan': -                            cap += 'W' -                        if capability['type'] == 'Station': -                            cap += 'S' -                        if capability['type'] == 'Repeater': -                            cap += 'r' -                        if capability['type'] == 'Telephone': -                            cap += 'T' -                        if capability['type'] == 'Docsis': -                            cap += 'D' -                        if capability['type'] == 'Other': -                            cap += 'O' - -            remote_if = 'Unknown' -            if 'descr' in values.get('port', {}): -                remote_if = values.get('port', {}).get('descr') -            elif 'id' in values.get('port', {}): -                remote_if = values.get('port', {}).get('id').get('value', 'Unknown') - -            output.append({local_if: {'chassis': chassis, -                                       'remote_if': remote_if, -                                       'proto': values.get('via','Unknown'), -                                       'platform': c_value.get('descr', 'Unknown'), -                                       'capabilities': cap}}) - -    output = {'neighbors': output} -    return output - -if __name__ == '__main__': -    args = parser.parse_args() -    tmp = { 'neighbors' : [] } - -    c = Config() -    if not c.exists_effective(['service', 'lldp']): -        print('Service LLDP is not configured') -        exit(0) - -    if args.detail: -        print(cmd('/usr/sbin/lldpctl -f plain')) -        exit(0) -    elif args.all or args.interface: -        tmp = json.loads(get_neighbors()) -        neighbors = dict() - -        if 'interface' in tmp.get('lldp'): -            neighbors = tmp['lldp']['interface'] - -    else: -        parser.print_help() -        exit(1) - -    tmpl = jinja2.Template(lldp_out, trim_blocks=True) -    config_text = tmpl.render(parse_data(neighbors, interface=args.interface)) -    print(config_text) - -    exit(0) | 
