]>
Commit | Line | Data |
---|---|---|
c750c028 JS |
1 | """ |
2 | QOM Command abstractions. | |
3 | """ | |
4 | ## | |
5 | # Copyright John Snow 2020, for Red Hat, Inc. | |
6 | # Copyright IBM, Corp. 2011 | |
7 | # | |
8 | # Authors: | |
9 | # John Snow <jsnow@redhat.com> | |
10 | # Anthony Liguori <aliguori@amazon.com> | |
11 | # | |
12 | # This work is licensed under the terms of the GNU GPL, version 2 or later. | |
13 | # See the COPYING file in the top-level directory. | |
14 | # | |
15 | # Based on ./scripts/qmp/qom-[set|get|tree|list] | |
16 | ## | |
17 | ||
18 | import argparse | |
19 | import os | |
20 | import sys | |
21 | from typing import ( | |
22 | Any, | |
23 | Dict, | |
24 | List, | |
25 | Optional, | |
26 | Type, | |
27 | TypeVar, | |
28 | ) | |
29 | ||
30 | from . import QEMUMonitorProtocol, QMPError | |
31 | ||
32 | ||
33 | # The following is needed only for a type alias. | |
34 | Subparsers = argparse._SubParsersAction # pylint: disable=protected-access | |
35 | ||
36 | ||
37 | class ObjectPropertyInfo: | |
38 | """ | |
39 | Represents the return type from e.g. qom-list. | |
40 | """ | |
41 | def __init__(self, name: str, type_: str, | |
42 | description: Optional[str] = None, | |
43 | default_value: Optional[object] = None): | |
44 | self.name = name | |
45 | self.type = type_ | |
46 | self.description = description | |
47 | self.default_value = default_value | |
48 | ||
49 | @classmethod | |
50 | def make(cls, value: Dict[str, Any]) -> 'ObjectPropertyInfo': | |
51 | """ | |
52 | Build an ObjectPropertyInfo from a Dict with an unknown shape. | |
53 | """ | |
54 | assert value.keys() >= {'name', 'type'} | |
55 | assert value.keys() <= {'name', 'type', 'description', 'default-value'} | |
56 | return cls(value['name'], value['type'], | |
57 | value.get('description'), | |
58 | value.get('default-value')) | |
59 | ||
60 | @property | |
61 | def child(self) -> bool: | |
62 | """Is this property a child property?""" | |
63 | return self.type.startswith('child<') | |
64 | ||
65 | @property | |
66 | def link(self) -> bool: | |
67 | """Is this property a link property?""" | |
68 | return self.type.startswith('link<') | |
69 | ||
70 | ||
71 | CommandT = TypeVar('CommandT', bound='QOMCommand') | |
72 | ||
73 | ||
74 | class QOMCommand: | |
75 | """ | |
76 | Represents a QOM sub-command. | |
77 | ||
78 | :param args: Parsed arguments, as returned from parser.parse_args. | |
79 | """ | |
80 | name: str | |
81 | help: str | |
82 | ||
83 | def __init__(self, args: argparse.Namespace): | |
84 | if args.socket is None: | |
85 | raise QMPError("No QMP socket path or address given") | |
86 | self.qmp = QEMUMonitorProtocol( | |
87 | QEMUMonitorProtocol.parse_address(args.socket) | |
88 | ) | |
89 | self.qmp.connect() | |
90 | ||
91 | @classmethod | |
92 | def register(cls, subparsers: Subparsers) -> None: | |
93 | """ | |
94 | Register this command with the argument parser. | |
95 | ||
96 | :param subparsers: argparse subparsers object, from "add_subparsers". | |
97 | """ | |
98 | subparser = subparsers.add_parser(cls.name, help=cls.help, | |
99 | description=cls.help) | |
100 | cls.configure_parser(subparser) | |
101 | ||
102 | @classmethod | |
103 | def configure_parser(cls, parser: argparse.ArgumentParser) -> None: | |
104 | """ | |
105 | Configure a parser with this command's arguments. | |
106 | ||
107 | :param parser: argparse parser or subparser object. | |
108 | """ | |
109 | default_path = os.environ.get('QMP_SOCKET') | |
110 | parser.add_argument( | |
111 | '--socket', '-s', | |
112 | dest='socket', | |
113 | action='store', | |
114 | help='QMP socket path or address (addr:port).' | |
115 | ' May also be set via QMP_SOCKET environment variable.', | |
116 | default=default_path | |
117 | ) | |
118 | parser.set_defaults(cmd_class=cls) | |
119 | ||
120 | @classmethod | |
121 | def add_path_prop_arg(cls, parser: argparse.ArgumentParser) -> None: | |
122 | """ | |
123 | Add the <path>.<proptery> positional argument to this command. | |
124 | ||
125 | :param parser: The parser to add the argument to. | |
126 | """ | |
127 | parser.add_argument( | |
128 | 'path_prop', | |
129 | metavar='<path>.<property>', | |
130 | action='store', | |
131 | help="QOM path and property, separated by a period '.'" | |
132 | ) | |
133 | ||
134 | def run(self) -> int: | |
135 | """ | |
136 | Run this command. | |
137 | ||
138 | :return: 0 on success, 1 otherwise. | |
139 | """ | |
140 | raise NotImplementedError | |
141 | ||
142 | def qom_list(self, path: str) -> List[ObjectPropertyInfo]: | |
143 | """ | |
144 | :return: a strongly typed list from the 'qom-list' command. | |
145 | """ | |
146 | rsp = self.qmp.command('qom-list', path=path) | |
147 | # qom-list returns List[ObjectPropertyInfo] | |
148 | assert isinstance(rsp, list) | |
149 | return [ObjectPropertyInfo.make(x) for x in rsp] | |
150 | ||
151 | @classmethod | |
152 | def command_runner( | |
153 | cls: Type[CommandT], | |
154 | args: argparse.Namespace | |
155 | ) -> int: | |
156 | """ | |
157 | Run a fully-parsed subcommand, with error-handling for the CLI. | |
158 | ||
5c02c865 | 159 | :return: The return code from `run()`. |
c750c028 JS |
160 | """ |
161 | try: | |
162 | cmd = cls(args) | |
163 | return cmd.run() | |
164 | except QMPError as err: | |
165 | print(f"{type(err).__name__}: {err!s}", file=sys.stderr) | |
166 | return -1 | |
167 | ||
168 | @classmethod | |
169 | def entry_point(cls) -> int: | |
170 | """ | |
171 | Build this command's parser, parse arguments, and run the command. | |
172 | ||
173 | :return: `run`'s return code. | |
174 | """ | |
175 | parser = argparse.ArgumentParser(description=cls.help) | |
176 | cls.configure_parser(parser) | |
177 | args = parser.parse_args() | |
178 | return cls.command_runner(args) |