summaryrefslogtreecommitdiff
path: root/python/vyos/opmode.py
blob: 906bd0dcbffaaeefcf7cfefa5ec24de61cd9577a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# 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 re
import sys
import typing


def _is_op_mode_function_name(name):
    if re.match(r"^(show|clear|reset|restart)", name):
        return True
    else:
        return False

def _is_show(name):
    if re.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):
        return 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]

            if _get_arg_type(th) == bool:
                subparser.add_argument(f"--{opt}", action='store_true')
            else:
                if _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())

    if not args["subcommand"]:
       print("Subcommand required!")
       parser.print_usage()
       sys.exit(1)

    function_name = args["subcommand"]
    func = functions[function_name]

    # Remove the subcommand from the arguments,
    # it would cause an extra argument error when we pass the dict to a function
    del args["subcommand"]

    # Show commands must always get the "raw" argument,
    # but other commands (clear/reset/restart) should not,
    # because they produce no output and it makes no sense for them.
    if ("raw" not in args) and _is_show(function_name):
        args["raw"] = False

    if re.match(r"^show", function_name):
        # 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)