]>
Commit | Line | Data |
---|---|---|
cedebdac LC |
1 | #!/usr/bin/python |
2 | # | |
9bed0d0d | 3 | # Low-level QEMU shell on top of QMP. |
cedebdac | 4 | # |
9bed0d0d | 5 | # Copyright (C) 2009, 2010 Red Hat Inc. |
cedebdac LC |
6 | # |
7 | # Authors: | |
8 | # Luiz Capitulino <lcapitulino@redhat.com> | |
9 | # | |
10 | # This work is licensed under the terms of the GNU GPL, version 2. See | |
11 | # the COPYING file in the top-level directory. | |
12 | # | |
13 | # Usage: | |
14 | # | |
15 | # Start QEMU with: | |
16 | # | |
9bed0d0d | 17 | # # qemu [...] -qmp unix:./qmp-sock,server |
cedebdac LC |
18 | # |
19 | # Run the shell: | |
20 | # | |
9bed0d0d | 21 | # $ qmp-shell ./qmp-sock |
cedebdac LC |
22 | # |
23 | # Commands have the following format: | |
24 | # | |
9bed0d0d | 25 | # < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] |
cedebdac LC |
26 | # |
27 | # For example: | |
28 | # | |
9bed0d0d LC |
29 | # (QEMU) device_add driver=e1000 id=net1 |
30 | # {u'return': {}} | |
31 | # (QEMU) | |
e2f9a657 JS |
32 | # |
33 | # key=value pairs also support Python or JSON object literal subset notations, | |
34 | # without spaces. Dictionaries/objects {} are supported as are arrays []. | |
35 | # | |
36 | # example-command arg-name1={'key':'value','obj'={'prop':"value"}} | |
37 | # | |
38 | # Both JSON and Python formatting should work, including both styles of | |
39 | # string literal quotes. Both paradigms of literal values should work, | |
40 | # including null/true/false for JSON and None/True/False for Python. | |
41 | # | |
42 | # | |
43 | # Transactions have the following multi-line format: | |
44 | # | |
45 | # transaction( | |
46 | # action-name1 [ arg-name1=arg1 ] ... [arg-nameN=argN ] | |
47 | # ... | |
48 | # action-nameN [ arg-name1=arg1 ] ... [arg-nameN=argN ] | |
49 | # ) | |
50 | # | |
51 | # One line transactions are also supported: | |
52 | # | |
53 | # transaction( action-name1 ... ) | |
54 | # | |
55 | # For example: | |
56 | # | |
57 | # (QEMU) transaction( | |
58 | # TRANS> block-dirty-bitmap-add node=drive0 name=bitmap1 | |
59 | # TRANS> block-dirty-bitmap-clear node=drive0 name=bitmap0 | |
60 | # TRANS> ) | |
61 | # {"return": {}} | |
62 | # (QEMU) | |
63 | # | |
64 | # Use the -v and -p options to activate the verbose and pretty-print options, | |
65 | # which will echo back the properly formatted JSON-compliant QMP that is being | |
66 | # sent to QEMU, which is useful for debugging and documentation generation. | |
cedebdac | 67 | |
f03868bd | 68 | from __future__ import print_function |
ff9ec34d | 69 | import json |
6092c3ec | 70 | import ast |
cedebdac | 71 | import readline |
9bed0d0d | 72 | import sys |
aa3b167f JS |
73 | import os |
74 | import errno | |
75 | import atexit | |
b35203b2 | 76 | import re |
cedebdac | 77 | |
8f8fd9ed CR |
78 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) |
79 | from qemu import qmp | |
80 | ||
9bed0d0d LC |
81 | class QMPCompleter(list): |
82 | def complete(self, text, state): | |
83 | for cmd in self: | |
84 | if cmd.startswith(text): | |
85 | if not state: | |
86 | return cmd | |
87 | else: | |
88 | state -= 1 | |
cedebdac | 89 | |
9bed0d0d LC |
90 | class QMPShellError(Exception): |
91 | pass | |
92 | ||
93 | class QMPShellBadPort(QMPShellError): | |
94 | pass | |
95 | ||
6092c3ec JS |
96 | class FuzzyJSON(ast.NodeTransformer): |
97 | '''This extension of ast.NodeTransformer filters literal "true/false/null" | |
98 | values in an AST and replaces them by proper "True/False/None" values that | |
99 | Python can properly evaluate.''' | |
100 | def visit_Name(self, node): | |
101 | if node.id == 'true': | |
102 | node.id = 'True' | |
103 | if node.id == 'false': | |
104 | node.id = 'False' | |
105 | if node.id == 'null': | |
106 | node.id = 'None' | |
107 | return node | |
108 | ||
9bed0d0d LC |
109 | # TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and |
110 | # _execute_cmd()). Let's design a better one. | |
111 | class QMPShell(qmp.QEMUMonitorProtocol): | |
e55250c6 | 112 | def __init__(self, address, pretty=False): |
3dd29b41 | 113 | super(QMPShell, self).__init__(self.__get_address(address)) |
9bed0d0d LC |
114 | self._greeting = None |
115 | self._completer = None | |
e55250c6 | 116 | self._pretty = pretty |
30bd6815 JS |
117 | self._transmode = False |
118 | self._actions = list() | |
aa3b167f JS |
119 | self._histfile = os.path.join(os.path.expanduser('~'), |
120 | '.qmp-shell_history') | |
9bed0d0d LC |
121 | |
122 | def __get_address(self, arg): | |
123 | """ | |
124 | Figure out if the argument is in the port:host form, if it's not it's | |
125 | probably a file path. | |
126 | """ | |
127 | addr = arg.split(':') | |
128 | if len(addr) == 2: | |
129 | try: | |
130 | port = int(addr[1]) | |
131 | except ValueError: | |
132 | raise QMPShellBadPort | |
133 | return ( addr[0], port ) | |
134 | # socket path | |
135 | return arg | |
136 | ||
137 | def _fill_completion(self): | |
daa5a72e | 138 | cmds = self.cmd('query-commands') |
d7a4228e | 139 | if 'error' in cmds: |
daa5a72e MAL |
140 | return |
141 | for cmd in cmds['return']: | |
9bed0d0d LC |
142 | self._completer.append(cmd['name']) |
143 | ||
144 | def __completer_setup(self): | |
145 | self._completer = QMPCompleter() | |
146 | self._fill_completion() | |
aa3b167f | 147 | readline.set_history_length(1024) |
9bed0d0d LC |
148 | readline.set_completer(self._completer.complete) |
149 | readline.parse_and_bind("tab: complete") | |
150 | # XXX: default delimiters conflict with some command names (eg. query-), | |
151 | # clearing everything as it doesn't seem to matter | |
152 | readline.set_completer_delims('') | |
aa3b167f JS |
153 | try: |
154 | readline.read_history_file(self._histfile) | |
155 | except Exception as e: | |
156 | if isinstance(e, IOError) and e.errno == errno.ENOENT: | |
157 | # File not found. No problem. | |
158 | pass | |
159 | else: | |
f03868bd | 160 | print("Failed to read history '%s'; %s" % (self._histfile, e)) |
aa3b167f JS |
161 | atexit.register(self.__save_history) |
162 | ||
163 | def __save_history(self): | |
164 | try: | |
165 | readline.write_history_file(self._histfile) | |
166 | except Exception as e: | |
f03868bd | 167 | print("Failed to save history file '%s'; %s" % (self._histfile, e)) |
9bed0d0d | 168 | |
6092c3ec JS |
169 | def __parse_value(self, val): |
170 | try: | |
171 | return int(val) | |
172 | except ValueError: | |
173 | pass | |
174 | ||
175 | if val.lower() == 'true': | |
176 | return True | |
177 | if val.lower() == 'false': | |
178 | return False | |
179 | if val.startswith(('{', '[')): | |
180 | # Try first as pure JSON: | |
9bed0d0d | 181 | try: |
6092c3ec | 182 | return json.loads(val) |
9bed0d0d | 183 | except ValueError: |
6092c3ec JS |
184 | pass |
185 | # Try once again as FuzzyJSON: | |
186 | try: | |
187 | st = ast.parse(val, mode='eval') | |
188 | return ast.literal_eval(FuzzyJSON().visit(st)) | |
189 | except SyntaxError: | |
190 | pass | |
191 | except ValueError: | |
192 | pass | |
193 | return val | |
194 | ||
195 | def __cli_expr(self, tokens, parent): | |
196 | for arg in tokens: | |
f880cd6b DB |
197 | (key, sep, val) = arg.partition('=') |
198 | if sep != '=': | |
6092c3ec JS |
199 | raise QMPShellError("Expected a key=value pair, got '%s'" % arg) |
200 | ||
201 | value = self.__parse_value(val) | |
202 | optpath = key.split('.') | |
cd159d09 FZ |
203 | curpath = [] |
204 | for p in optpath[:-1]: | |
205 | curpath.append(p) | |
206 | d = parent.get(p, {}) | |
207 | if type(d) is not dict: | |
208 | raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath)) | |
209 | parent[p] = d | |
210 | parent = d | |
211 | if optpath[-1] in parent: | |
212 | if type(parent[optpath[-1]]) is dict: | |
213 | raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath)) | |
214 | else: | |
6092c3ec | 215 | raise QMPShellError('Cannot set "%s" multiple times' % key) |
cd159d09 | 216 | parent[optpath[-1]] = value |
a7430a0b JS |
217 | |
218 | def __build_cmd(self, cmdline): | |
219 | """ | |
220 | Build a QMP input object from a user provided command-line in the | |
221 | following format: | |
222 | ||
223 | < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] | |
224 | """ | |
b35203b2 | 225 | cmdargs = re.findall(r'''(?:[^\s"']|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+''', cmdline) |
30bd6815 JS |
226 | |
227 | # Transactional CLI entry/exit: | |
228 | if cmdargs[0] == 'transaction(': | |
229 | self._transmode = True | |
230 | cmdargs.pop(0) | |
231 | elif cmdargs[0] == ')' and self._transmode: | |
232 | self._transmode = False | |
233 | if len(cmdargs) > 1: | |
234 | raise QMPShellError("Unexpected input after close of Transaction sub-shell") | |
235 | qmpcmd = { 'execute': 'transaction', | |
236 | 'arguments': { 'actions': self._actions } } | |
237 | self._actions = list() | |
238 | return qmpcmd | |
239 | ||
240 | # Nothing to process? | |
241 | if not cmdargs: | |
242 | return None | |
243 | ||
244 | # Parse and then cache this Transactional Action | |
245 | if self._transmode: | |
246 | finalize = False | |
247 | action = { 'type': cmdargs[0], 'data': {} } | |
248 | if cmdargs[-1] == ')': | |
249 | cmdargs.pop(-1) | |
250 | finalize = True | |
251 | self.__cli_expr(cmdargs[1:], action['data']) | |
252 | self._actions.append(action) | |
253 | return self.__build_cmd(')') if finalize else None | |
254 | ||
255 | # Standard command: parse and return it to be executed. | |
a7430a0b JS |
256 | qmpcmd = { 'execute': cmdargs[0], 'arguments': {} } |
257 | self.__cli_expr(cmdargs[1:], qmpcmd['arguments']) | |
9bed0d0d LC |
258 | return qmpcmd |
259 | ||
1ceca07e | 260 | def _print(self, qmp): |
e55250c6 DB |
261 | indent = None |
262 | if self._pretty: | |
263 | indent = 4 | |
264 | jsobj = json.dumps(qmp, indent=indent) | |
f03868bd | 265 | print(str(jsobj)) |
1ceca07e | 266 | |
9bed0d0d LC |
267 | def _execute_cmd(self, cmdline): |
268 | try: | |
269 | qmpcmd = self.__build_cmd(cmdline) | |
cf6c6345 | 270 | except Exception as e: |
f03868bd EH |
271 | print('Error while parsing command line: %s' % e) |
272 | print('command format: <command-name> ', end=' ') | |
273 | print('[arg-name1=arg1] ... [arg-nameN=argN]') | |
9bed0d0d | 274 | return True |
30bd6815 JS |
275 | # For transaction mode, we may have just cached the action: |
276 | if qmpcmd is None: | |
277 | return True | |
1ceca07e JS |
278 | if self._verbose: |
279 | self._print(qmpcmd) | |
9bed0d0d LC |
280 | resp = self.cmd_obj(qmpcmd) |
281 | if resp is None: | |
f03868bd | 282 | print('Disconnected') |
9bed0d0d | 283 | return False |
1ceca07e | 284 | self._print(resp) |
9bed0d0d LC |
285 | return True |
286 | ||
c5e397df | 287 | def connect(self, negotiate): |
3dd29b41 | 288 | self._greeting = super(QMPShell, self).connect(negotiate) |
9bed0d0d | 289 | self.__completer_setup() |
cedebdac | 290 | |
9bed0d0d | 291 | def show_banner(self, msg='Welcome to the QMP low-level shell!'): |
f03868bd | 292 | print(msg) |
b13d2ff3 | 293 | if not self._greeting: |
f03868bd | 294 | print('Connected') |
b13d2ff3 | 295 | return |
9bed0d0d | 296 | version = self._greeting['QMP']['version']['qemu'] |
f03868bd | 297 | print('Connected to QEMU %d.%d.%d\n' % (version['major'],version['minor'],version['micro'])) |
cedebdac | 298 | |
30bd6815 JS |
299 | def get_prompt(self): |
300 | if self._transmode: | |
301 | return "TRANS> " | |
302 | return "(QEMU) " | |
303 | ||
9bed0d0d LC |
304 | def read_exec_command(self, prompt): |
305 | """ | |
306 | Read and execute a command. | |
cedebdac | 307 | |
9bed0d0d LC |
308 | @return True if execution was ok, return False if disconnected. |
309 | """ | |
cedebdac | 310 | try: |
9bed0d0d | 311 | cmdline = raw_input(prompt) |
cedebdac | 312 | except EOFError: |
f03868bd | 313 | print() |
9bed0d0d LC |
314 | return False |
315 | if cmdline == '': | |
316 | for ev in self.get_events(): | |
f03868bd | 317 | print(ev) |
9bed0d0d LC |
318 | self.clear_events() |
319 | return True | |
cedebdac | 320 | else: |
9bed0d0d LC |
321 | return self._execute_cmd(cmdline) |
322 | ||
1ceca07e JS |
323 | def set_verbosity(self, verbose): |
324 | self._verbose = verbose | |
325 | ||
11217a75 LC |
326 | class HMPShell(QMPShell): |
327 | def __init__(self, address): | |
328 | QMPShell.__init__(self, address) | |
329 | self.__cpu_index = 0 | |
330 | ||
331 | def __cmd_completion(self): | |
332 | for cmd in self.__cmd_passthrough('help')['return'].split('\r\n'): | |
333 | if cmd and cmd[0] != '[' and cmd[0] != '\t': | |
334 | name = cmd.split()[0] # drop help text | |
335 | if name == 'info': | |
336 | continue | |
337 | if name.find('|') != -1: | |
338 | # Command in the form 'foobar|f' or 'f|foobar', take the | |
339 | # full name | |
340 | opt = name.split('|') | |
341 | if len(opt[0]) == 1: | |
342 | name = opt[1] | |
343 | else: | |
344 | name = opt[0] | |
345 | self._completer.append(name) | |
346 | self._completer.append('help ' + name) # help completion | |
347 | ||
348 | def __info_completion(self): | |
349 | for cmd in self.__cmd_passthrough('info')['return'].split('\r\n'): | |
350 | if cmd: | |
351 | self._completer.append('info ' + cmd.split()[1]) | |
352 | ||
353 | def __other_completion(self): | |
354 | # special cases | |
355 | self._completer.append('help info') | |
356 | ||
357 | def _fill_completion(self): | |
358 | self.__cmd_completion() | |
359 | self.__info_completion() | |
360 | self.__other_completion() | |
361 | ||
362 | def __cmd_passthrough(self, cmdline, cpu_index = 0): | |
363 | return self.cmd_obj({ 'execute': 'human-monitor-command', 'arguments': | |
364 | { 'command-line': cmdline, | |
365 | 'cpu-index': cpu_index } }) | |
366 | ||
367 | def _execute_cmd(self, cmdline): | |
368 | if cmdline.split()[0] == "cpu": | |
369 | # trap the cpu command, it requires special setting | |
370 | try: | |
371 | idx = int(cmdline.split()[1]) | |
372 | if not 'return' in self.__cmd_passthrough('info version', idx): | |
f03868bd | 373 | print('bad CPU index') |
11217a75 LC |
374 | return True |
375 | self.__cpu_index = idx | |
376 | except ValueError: | |
f03868bd | 377 | print('cpu command takes an integer argument') |
11217a75 LC |
378 | return True |
379 | resp = self.__cmd_passthrough(cmdline, self.__cpu_index) | |
380 | if resp is None: | |
f03868bd | 381 | print('Disconnected') |
11217a75 LC |
382 | return False |
383 | assert 'return' in resp or 'error' in resp | |
384 | if 'return' in resp: | |
385 | # Success | |
386 | if len(resp['return']) > 0: | |
f03868bd | 387 | print(resp['return'], end=' ') |
11217a75 LC |
388 | else: |
389 | # Error | |
f03868bd | 390 | print('%s: %s' % (resp['error']['class'], resp['error']['desc'])) |
11217a75 LC |
391 | return True |
392 | ||
393 | def show_banner(self): | |
394 | QMPShell.show_banner(self, msg='Welcome to the HMP shell!') | |
395 | ||
9bed0d0d LC |
396 | def die(msg): |
397 | sys.stderr.write('ERROR: %s\n' % msg) | |
398 | sys.exit(1) | |
399 | ||
400 | def fail_cmdline(option=None): | |
401 | if option: | |
402 | sys.stderr.write('ERROR: bad command-line option \'%s\'\n' % option) | |
dcd3b25d MAL |
403 | sys.stderr.write('qmp-shell [ -v ] [ -p ] [ -H ] [ -N ] < UNIX socket path> | < TCP address:port >\n') |
404 | sys.stderr.write(' -v Verbose (echo command sent and received)\n') | |
405 | sys.stderr.write(' -p Pretty-print JSON\n') | |
406 | sys.stderr.write(' -H Use HMP interface\n') | |
407 | sys.stderr.write(' -N Skip negotiate (for qemu-ga)\n') | |
9bed0d0d LC |
408 | sys.exit(1) |
409 | ||
410 | def main(): | |
11217a75 | 411 | addr = '' |
fa779b65 DB |
412 | qemu = None |
413 | hmp = False | |
e55250c6 | 414 | pretty = False |
1ceca07e | 415 | verbose = False |
c5e397df | 416 | negotiate = True |
fa779b65 | 417 | |
9bed0d0d | 418 | try: |
fa779b65 DB |
419 | for arg in sys.argv[1:]: |
420 | if arg == "-H": | |
421 | if qemu is not None: | |
422 | fail_cmdline(arg) | |
423 | hmp = True | |
424 | elif arg == "-p": | |
e55250c6 | 425 | pretty = True |
c5e397df MAL |
426 | elif arg == "-N": |
427 | negotiate = False | |
1ceca07e JS |
428 | elif arg == "-v": |
429 | verbose = True | |
fa779b65 DB |
430 | else: |
431 | if qemu is not None: | |
432 | fail_cmdline(arg) | |
433 | if hmp: | |
434 | qemu = HMPShell(arg) | |
435 | else: | |
e55250c6 | 436 | qemu = QMPShell(arg, pretty) |
fa779b65 DB |
437 | addr = arg |
438 | ||
439 | if qemu is None: | |
440 | fail_cmdline() | |
9bed0d0d LC |
441 | except QMPShellBadPort: |
442 | die('bad port number in command-line') | |
443 | ||
444 | try: | |
c5e397df | 445 | qemu.connect(negotiate) |
9bed0d0d LC |
446 | except qmp.QMPConnectError: |
447 | die('Didn\'t get QMP greeting message') | |
448 | except qmp.QMPCapabilitiesError: | |
449 | die('Could not negotiate capabilities') | |
450 | except qemu.error: | |
11217a75 | 451 | die('Could not connect to %s' % addr) |
9bed0d0d LC |
452 | |
453 | qemu.show_banner() | |
1ceca07e | 454 | qemu.set_verbosity(verbose) |
30bd6815 | 455 | while qemu.read_exec_command(qemu.get_prompt()): |
9bed0d0d LC |
456 | pass |
457 | qemu.close() | |
cedebdac LC |
458 | |
459 | if __name__ == '__main__': | |
460 | main() |