diff options
author | Daniil Baturin <daniil@vyos.io> | 2022-06-09 09:16:53 -0400 |
---|---|---|
committer | Daniil Baturin <daniil@vyos.io> | 2022-06-09 09:16:53 -0400 |
commit | 812a4fc3f3063741da0fa01cbbbf17dead66a664 (patch) | |
tree | 3f882254041d6b4c4c05b0d961156e93832fd9c0 | |
parent | c40b9064d97d14bee7c5f9f9674f898a1b25a6ae (diff) | |
download | vyos-1x-812a4fc3f3063741da0fa01cbbbf17dead66a664.tar.gz vyos-1x-812a4fc3f3063741da0fa01cbbbf17dead66a664.zip |
T2719: prototype of an op mode command runner
based on type hints and introspection
-rw-r--r-- | op-mode-definitions/ipv6-route.xml.in | 6 | ||||
-rw-r--r-- | op-mode-definitions/show-arp.xml.in | 4 | ||||
-rw-r--r-- | op-mode-definitions/show-ip.xml.in | 6 | ||||
-rw-r--r-- | python/vyos/opmode.py | 123 | ||||
-rwxr-xr-x | src/op_mode/neighbor.py (renamed from src/op_mode/show_neigh.py) | 42 |
5 files changed, 153 insertions, 28 deletions
diff --git a/op-mode-definitions/ipv6-route.xml.in b/op-mode-definitions/ipv6-route.xml.in index 4f8792f9f..50f34dd56 100644 --- a/op-mode-definitions/ipv6-route.xml.in +++ b/op-mode-definitions/ipv6-route.xml.in @@ -20,7 +20,7 @@ <properties> <help>Show IPv6 neighbor (NDP) table</help> </properties> - <command>${vyos_op_scripts_dir}/show_neigh.py --family inet6</command> + <command>${vyos_op_scripts_dir}/neighbor.py --family inet6</command> <children> <tagNode name="interface"> <properties> @@ -29,7 +29,7 @@ <script>${vyos_completion_dir}/list_interfaces.py -b</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_neigh.py --family inet6 --interface "$5"</command> + <command>${vyos_op_scripts_dir}/neighbor.py --family inet6 --interface "$5"</command> </tagNode> <tagNode name="state"> <properties> @@ -38,7 +38,7 @@ <list>reachable stale failed permanent</list> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_neigh.py --family inet6 --state "$5"</command> + <command>${vyos_op_scripts_dir}/neighbor.py --family inet6 --state "$5"</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/show-arp.xml.in b/op-mode-definitions/show-arp.xml.in index 58cc6e45e..11ac6a252 100644 --- a/op-mode-definitions/show-arp.xml.in +++ b/op-mode-definitions/show-arp.xml.in @@ -6,7 +6,7 @@ <properties> <help>Show Address Resolution Protocol (ARP) information</help> </properties> - <command>${vyos_op_scripts_dir}/show_neigh.py --family inet</command> + <command>${vyos_op_scripts_dir}/neighbor.py --family inet</command> <children> <tagNode name="interface"> <properties> @@ -15,7 +15,7 @@ <script>${vyos_completion_dir}/list_interfaces.py -b</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_neigh.py --family inet --interface "$4"</command> + <command>${vyos_op_scripts_dir}/neighbor.py --family inet --interface "$4"</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/show-ip.xml.in b/op-mode-definitions/show-ip.xml.in index d342ac192..1903aca05 100644 --- a/op-mode-definitions/show-ip.xml.in +++ b/op-mode-definitions/show-ip.xml.in @@ -11,7 +11,7 @@ <properties> <help>Show IPv4 neighbor (ARP) table</help> </properties> - <command>${vyos_op_scripts_dir}/show_neigh.py --family inet</command> + <command>${vyos_op_scripts_dir}/neighbor.py --family inet</command> <children> <tagNode name="interface"> <properties> @@ -20,7 +20,7 @@ <script>${vyos_completion_dir}/list_interfaces.py -b</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_neigh.py --family inet --interface "$5"</command> + <command>${vyos_op_scripts_dir}/neighbor.py --family inet --interface "$5"</command> </tagNode> <tagNode name="state"> <properties> @@ -29,7 +29,7 @@ <list>reachable stale failed permanent</list> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_neigh.py --family inet --state "$5"</command> + <command>${vyos_op_scripts_dir}/neighbor.py --family inet --state "$5"</command> </tagNode> </children> </node> diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py new file mode 100644 index 000000000..c0812c7fd --- /dev/null +++ b/python/vyos/opmode.py @@ -0,0 +1,123 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import typing + + +def _is_op_mode_function_name(name): + from re import match + + if match(r"^(show|clear|reset|restart)", name): + return True + else: + return False + +def _is_show(name): + from re import match + + if match(r"^show", name): + return True + else: + return False + +def _get_op_mode_functions(module): + from inspect import getmembers, isfunction + + # Get all functions in that module + funcs = getmembers(module, isfunction) + + # getmembers returns (name, func) tuples + funcs = list(filter(lambda ft: _is_op_mode_function_name(ft[0]), funcs)) + + funcs_dict = {} + for (name, thunk) in funcs: + funcs_dict[name] = thunk + + return funcs_dict + +def _is_optional_type(t): + # Optional[t] is internally an alias for Union[t, NoneType] + # and there's no easy way to get union members it seems + if (type(t) == typing._UnionGenericAlias): + if (len(t.__args__) == 2): + if t.__args__[1] == type(None): + return True + + return False + +def _get_arg_type(t): + """ Returns the type itself if it's a primitive type, + or the "real" type of typing.Optional + + Doesn't work with anything else at the moment! + """ + if _is_optional_type(t): + t.__args__[0] + else: + return t + +def run(module): + from argparse import ArgumentParser + + functions = _get_op_mode_functions(module) + + parser = ArgumentParser() + subparsers = parser.add_subparsers(dest="subcommand") + + for function_name in functions: + subparser = subparsers.add_parser(function_name, help=functions[function_name].__doc__) + + type_hints = typing.get_type_hints(functions[function_name]) + for opt in type_hints: + th = type_hints[opt] + + # Show commands require an option to choose between raw JSON and human-readable + # formatted output. + # For interactive use, they default to formatted output. + if (function_name == "show") and (opt == "raw"): + subparser.add_argument(f"--raw", action='store_true') + elif _is_optional_type(th): + subparser.add_argument(f"--{opt}", type=_get_arg_type(th), default=None) + else: + subparser.add_argument(f"--{opt}", type=_get_arg_type(th), required=True) + + # Get options as a dict rather than a namespace, + # so that we can modify it and pack for passing to functions + args = vars(parser.parse_args()) + + func = functions[args["subcommand"]] + + # Remove the subcommand from the arguments, + # it would cause an extra argument error when we pass the dict to a function + del args["subcommand"] + + if "raw" not in args: + args["raw"] = False + + if function_name == "show": + # Show commands are slightly special: + # they may return human-formatted output + # or a raw dict that we need to serialize in JSON for printing + res = func(**args) + if not args["raw"]: + return res + else: + from json import dumps + return dumps(res, indent=4) + else: + # Other functions should not return anything, + # although they may print their own warnings or status messages + func(**args) + diff --git a/src/op_mode/show_neigh.py b/src/op_mode/neighbor.py index d874bd544..b50e75d91 100755 --- a/src/op_mode/show_neigh.py +++ b/src/op_mode/neighbor.py @@ -28,29 +28,32 @@ # ] import sys +import typing +import opmode -def get_raw_data(family, device=None, state=None): + +def get_raw_data(family, interface=None, state=None): from json import loads from vyos.util import cmd - if device: - device = f"dev {device}" + if interface: + interface = f"dev {interface}" else: - device = "" + interface = "" if state: state = f"nud {state}" else: state = "" - neigh_cmd = f"ip --family {family} --json neighbor list {device} {state}" + neigh_cmd = f"ip --family {family} --json neighbor list {interface} {state}" data = loads(cmd(neigh_cmd)) return data -def get_formatted_output(family, device=None, state=None): +def format_neighbors(neighs, interface=None): from tabulate import tabulate def entry_to_list(e, intf=None): @@ -68,35 +71,34 @@ def get_formatted_output(family, device=None, state=None): # Device field is absent from outputs of `ip neigh list dev ...` if "dev" in e: dev = e["dev"] - elif device: - dev = device + elif interface: + dev = interface else: raise ValueError("interface is not defined") return [dst, dev, lladdr, state] - neighs = get_raw_data(family, device=device, state=state) neighs = map(entry_to_list, neighs) headers = ["Address", "Interface", "Link layer address", "State"] return tabulate(neighs, headers) -if __name__ == '__main__': - from argparse import ArgumentParser +def show(raw: bool, family: str, interface: typing.Optional[str], state: typing.Optional[str]): + """ Display neighbor table contents """ + data = get_raw_data(family, interface, state=state) - parser = ArgumentParser() - parser.add_argument("-f", "--family", type=str, default="inet", help="Address family") - parser.add_argument("-i", "--interface", type=str, help="Network interface") - parser.add_argument("-s", "--state", type=str, help="Neighbor table entry state") + if raw: + return data + else: + return format_neighbors(data, interface) - args = parser.parse_args() - if args.state: - if args.state not in ["reachable", "failed", "stale", "permanent"]: - raise ValueError(f"""Incorrect state "{args.state}"! Must be one of: reachable, stale, failed, permanent""") +if __name__ == '__main__': + from argparse import ArgumentParser try: - print(get_formatted_output(args.family, device=args.interface, state=args.state)) + print(opmode.run(sys.modules[__name__])) except ValueError as e: print(e) sys.exit(1) + |