summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniil Baturin <daniil@vyos.io>2022-06-09 09:16:53 -0400
committerDaniil Baturin <daniil@vyos.io>2022-06-09 09:16:53 -0400
commit812a4fc3f3063741da0fa01cbbbf17dead66a664 (patch)
tree3f882254041d6b4c4c05b0d961156e93832fd9c0
parentc40b9064d97d14bee7c5f9f9674f898a1b25a6ae (diff)
downloadvyos-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.in6
-rw-r--r--op-mode-definitions/show-arp.xml.in4
-rw-r--r--op-mode-definitions/show-ip.xml.in6
-rw-r--r--python/vyos/opmode.py123
-rwxr-xr-xsrc/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)
+