xref: /qemu/python/qemu/utils/qemu_ga_client.py (revision 684750ab)
10347c4c4SJohn Snow"""
20347c4c4SJohn SnowQEMU Guest Agent Client
30347c4c4SJohn Snow
40347c4c4SJohn SnowUsage:
50347c4c4SJohn Snow
60347c4c4SJohn SnowStart QEMU with:
70347c4c4SJohn Snow
80347c4c4SJohn Snow# qemu [...] -chardev socket,path=/tmp/qga.sock,server=on,wait=off,id=qga0 \
90347c4c4SJohn Snow  -device virtio-serial \
100347c4c4SJohn Snow  -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0
110347c4c4SJohn Snow
120347c4c4SJohn SnowRun the script:
130347c4c4SJohn Snow
140347c4c4SJohn Snow$ qemu-ga-client --address=/tmp/qga.sock <command> [args...]
150347c4c4SJohn Snow
160347c4c4SJohn Snowor
170347c4c4SJohn Snow
180347c4c4SJohn Snow$ export QGA_CLIENT_ADDRESS=/tmp/qga.sock
190347c4c4SJohn Snow$ qemu-ga-client <command> [args...]
200347c4c4SJohn Snow
210347c4c4SJohn SnowFor example:
220347c4c4SJohn Snow
230347c4c4SJohn Snow$ qemu-ga-client cat /etc/resolv.conf
240347c4c4SJohn Snow# Generated by NetworkManager
250347c4c4SJohn Snownameserver 10.0.2.3
260347c4c4SJohn Snow$ qemu-ga-client fsfreeze status
270347c4c4SJohn Snowthawed
280347c4c4SJohn Snow$ qemu-ga-client fsfreeze freeze
290347c4c4SJohn Snow2 filesystems frozen
300347c4c4SJohn Snow
310347c4c4SJohn SnowSee also: https://wiki.qemu.org/Features/QAPI/GuestAgent
320347c4c4SJohn Snow"""
330347c4c4SJohn Snow
340347c4c4SJohn Snow# Copyright (C) 2012 Ryota Ozaki <ozaki.ryota@gmail.com>
350347c4c4SJohn Snow#
360347c4c4SJohn Snow# This work is licensed under the terms of the GNU GPL, version 2.  See
370347c4c4SJohn Snow# the COPYING file in the top-level directory.
380347c4c4SJohn Snow
390347c4c4SJohn Snowimport argparse
400347c4c4SJohn Snowimport asyncio
410347c4c4SJohn Snowimport base64
420347c4c4SJohn Snowimport os
430347c4c4SJohn Snowimport random
440347c4c4SJohn Snowimport sys
450347c4c4SJohn Snowfrom typing import (
460347c4c4SJohn Snow    Any,
470347c4c4SJohn Snow    Callable,
480347c4c4SJohn Snow    Dict,
490347c4c4SJohn Snow    Optional,
500347c4c4SJohn Snow    Sequence,
510347c4c4SJohn Snow)
520347c4c4SJohn Snow
5337094b6dSJohn Snowfrom qemu.qmp import ConnectError, SocketAddrT
5437094b6dSJohn Snowfrom qemu.qmp.legacy import QEMUMonitorProtocol
550347c4c4SJohn Snow
560347c4c4SJohn Snow
570347c4c4SJohn Snow# This script has not seen many patches or careful attention in quite
580347c4c4SJohn Snow# some time. If you would like to improve it, please review the design
590347c4c4SJohn Snow# carefully and add docstrings at that point in time. Until then:
600347c4c4SJohn Snow
610347c4c4SJohn Snow# pylint: disable=missing-docstring
620347c4c4SJohn Snow
630347c4c4SJohn Snow
640347c4c4SJohn Snowclass QemuGuestAgent(QEMUMonitorProtocol):
650347c4c4SJohn Snow    def __getattr__(self, name: str) -> Callable[..., Any]:
660347c4c4SJohn Snow        def wrapper(**kwds: object) -> object:
67684750abSVladimir Sementsov-Ogievskiy            return self.cmd('guest-' + name.replace('_', '-'), **kwds)
680347c4c4SJohn Snow        return wrapper
690347c4c4SJohn Snow
700347c4c4SJohn Snow
710347c4c4SJohn Snowclass QemuGuestAgentClient:
720347c4c4SJohn Snow    def __init__(self, address: SocketAddrT):
730347c4c4SJohn Snow        self.qga = QemuGuestAgent(address)
740347c4c4SJohn Snow        self.qga.connect(negotiate=False)
750347c4c4SJohn Snow
760347c4c4SJohn Snow    def sync(self, timeout: Optional[float] = 3) -> None:
770347c4c4SJohn Snow        # Avoid being blocked forever
780347c4c4SJohn Snow        if not self.ping(timeout):
790347c4c4SJohn Snow            raise EnvironmentError('Agent seems not alive')
800347c4c4SJohn Snow        uid = random.randint(0, (1 << 32) - 1)
810347c4c4SJohn Snow        while True:
820347c4c4SJohn Snow            ret = self.qga.sync(id=uid)
830347c4c4SJohn Snow            if isinstance(ret, int) and int(ret) == uid:
840347c4c4SJohn Snow                break
850347c4c4SJohn Snow
860347c4c4SJohn Snow    def __file_read_all(self, handle: int) -> bytes:
870347c4c4SJohn Snow        eof = False
880347c4c4SJohn Snow        data = b''
890347c4c4SJohn Snow        while not eof:
900347c4c4SJohn Snow            ret = self.qga.file_read(handle=handle, count=1024)
910347c4c4SJohn Snow            _data = base64.b64decode(ret['buf-b64'])
920347c4c4SJohn Snow            data += _data
930347c4c4SJohn Snow            eof = ret['eof']
940347c4c4SJohn Snow        return data
950347c4c4SJohn Snow
960347c4c4SJohn Snow    def read(self, path: str) -> bytes:
970347c4c4SJohn Snow        handle = self.qga.file_open(path=path)
980347c4c4SJohn Snow        try:
990347c4c4SJohn Snow            data = self.__file_read_all(handle)
1000347c4c4SJohn Snow        finally:
1010347c4c4SJohn Snow            self.qga.file_close(handle=handle)
1020347c4c4SJohn Snow        return data
1030347c4c4SJohn Snow
1040347c4c4SJohn Snow    def info(self) -> str:
1050347c4c4SJohn Snow        info = self.qga.info()
1060347c4c4SJohn Snow
1070347c4c4SJohn Snow        msgs = []
1080347c4c4SJohn Snow        msgs.append('version: ' + info['version'])
1090347c4c4SJohn Snow        msgs.append('supported_commands:')
1100347c4c4SJohn Snow        enabled = [c['name'] for c in info['supported_commands']
1110347c4c4SJohn Snow                   if c['enabled']]
1120347c4c4SJohn Snow        msgs.append('\tenabled: ' + ', '.join(enabled))
1130347c4c4SJohn Snow        disabled = [c['name'] for c in info['supported_commands']
1140347c4c4SJohn Snow                    if not c['enabled']]
1150347c4c4SJohn Snow        msgs.append('\tdisabled: ' + ', '.join(disabled))
1160347c4c4SJohn Snow
1170347c4c4SJohn Snow        return '\n'.join(msgs)
1180347c4c4SJohn Snow
1190347c4c4SJohn Snow    @classmethod
1200347c4c4SJohn Snow    def __gen_ipv4_netmask(cls, prefixlen: int) -> str:
1210347c4c4SJohn Snow        mask = int('1' * prefixlen + '0' * (32 - prefixlen), 2)
1220347c4c4SJohn Snow        return '.'.join([str(mask >> 24),
1230347c4c4SJohn Snow                         str((mask >> 16) & 0xff),
1240347c4c4SJohn Snow                         str((mask >> 8) & 0xff),
1250347c4c4SJohn Snow                         str(mask & 0xff)])
1260347c4c4SJohn Snow
1270347c4c4SJohn Snow    def ifconfig(self) -> str:
1280347c4c4SJohn Snow        nifs = self.qga.network_get_interfaces()
1290347c4c4SJohn Snow
1300347c4c4SJohn Snow        msgs = []
1310347c4c4SJohn Snow        for nif in nifs:
1320347c4c4SJohn Snow            msgs.append(nif['name'] + ':')
1330347c4c4SJohn Snow            if 'ip-addresses' in nif:
1340347c4c4SJohn Snow                for ipaddr in nif['ip-addresses']:
1350347c4c4SJohn Snow                    if ipaddr['ip-address-type'] == 'ipv4':
1360347c4c4SJohn Snow                        addr = ipaddr['ip-address']
1370347c4c4SJohn Snow                        mask = self.__gen_ipv4_netmask(int(ipaddr['prefix']))
1380347c4c4SJohn Snow                        msgs.append(f"\tinet {addr}  netmask {mask}")
1390347c4c4SJohn Snow                    elif ipaddr['ip-address-type'] == 'ipv6':
1400347c4c4SJohn Snow                        addr = ipaddr['ip-address']
1410347c4c4SJohn Snow                        prefix = ipaddr['prefix']
1420347c4c4SJohn Snow                        msgs.append(f"\tinet6 {addr}  prefixlen {prefix}")
1430347c4c4SJohn Snow            if nif['hardware-address'] != '00:00:00:00:00:00':
1440347c4c4SJohn Snow                msgs.append("\tether " + nif['hardware-address'])
1450347c4c4SJohn Snow
1460347c4c4SJohn Snow        return '\n'.join(msgs)
1470347c4c4SJohn Snow
1480347c4c4SJohn Snow    def ping(self, timeout: Optional[float]) -> bool:
1490347c4c4SJohn Snow        self.qga.settimeout(timeout)
1500347c4c4SJohn Snow        try:
1510347c4c4SJohn Snow            self.qga.ping()
1520347c4c4SJohn Snow        except asyncio.TimeoutError:
1530347c4c4SJohn Snow            return False
1540347c4c4SJohn Snow        return True
1550347c4c4SJohn Snow
1560347c4c4SJohn Snow    def fsfreeze(self, cmd: str) -> object:
1570347c4c4SJohn Snow        if cmd not in ['status', 'freeze', 'thaw']:
158aef633e7SJohn Snow            raise ValueError('Invalid command: ' + cmd)
1590347c4c4SJohn Snow        # Can be int (freeze, thaw) or GuestFsfreezeStatus (status)
1600347c4c4SJohn Snow        return getattr(self.qga, 'fsfreeze' + '_' + cmd)()
1610347c4c4SJohn Snow
1620347c4c4SJohn Snow    def fstrim(self, minimum: int) -> Dict[str, object]:
1630347c4c4SJohn Snow        # returns GuestFilesystemTrimResponse
1640347c4c4SJohn Snow        ret = getattr(self.qga, 'fstrim')(minimum=minimum)
1650347c4c4SJohn Snow        assert isinstance(ret, dict)
1660347c4c4SJohn Snow        return ret
1670347c4c4SJohn Snow
1680347c4c4SJohn Snow    def suspend(self, mode: str) -> None:
1690347c4c4SJohn Snow        if mode not in ['disk', 'ram', 'hybrid']:
170aef633e7SJohn Snow            raise ValueError('Invalid mode: ' + mode)
1710347c4c4SJohn Snow
1720347c4c4SJohn Snow        try:
1730347c4c4SJohn Snow            getattr(self.qga, 'suspend' + '_' + mode)()
1740347c4c4SJohn Snow            # On error exception will raise
1750347c4c4SJohn Snow        except asyncio.TimeoutError:
1760347c4c4SJohn Snow            # On success command will timed out
1770347c4c4SJohn Snow            return
1780347c4c4SJohn Snow
1790347c4c4SJohn Snow    def shutdown(self, mode: str = 'powerdown') -> None:
1800347c4c4SJohn Snow        if mode not in ['powerdown', 'halt', 'reboot']:
181aef633e7SJohn Snow            raise ValueError('Invalid mode: ' + mode)
1820347c4c4SJohn Snow
1830347c4c4SJohn Snow        try:
1840347c4c4SJohn Snow            self.qga.shutdown(mode=mode)
1850347c4c4SJohn Snow        except asyncio.TimeoutError:
1860347c4c4SJohn Snow            pass
1870347c4c4SJohn Snow
1880347c4c4SJohn Snow
1890347c4c4SJohn Snowdef _cmd_cat(client: QemuGuestAgentClient, args: Sequence[str]) -> None:
1900347c4c4SJohn Snow    if len(args) != 1:
1910347c4c4SJohn Snow        print('Invalid argument')
1920347c4c4SJohn Snow        print('Usage: cat <file>')
1930347c4c4SJohn Snow        sys.exit(1)
1940347c4c4SJohn Snow    print(client.read(args[0]))
1950347c4c4SJohn Snow
1960347c4c4SJohn Snow
1970347c4c4SJohn Snowdef _cmd_fsfreeze(client: QemuGuestAgentClient, args: Sequence[str]) -> None:
1980347c4c4SJohn Snow    usage = 'Usage: fsfreeze status|freeze|thaw'
1990347c4c4SJohn Snow    if len(args) != 1:
2000347c4c4SJohn Snow        print('Invalid argument')
2010347c4c4SJohn Snow        print(usage)
2020347c4c4SJohn Snow        sys.exit(1)
2030347c4c4SJohn Snow    if args[0] not in ['status', 'freeze', 'thaw']:
2040347c4c4SJohn Snow        print('Invalid command: ' + args[0])
2050347c4c4SJohn Snow        print(usage)
2060347c4c4SJohn Snow        sys.exit(1)
2070347c4c4SJohn Snow    cmd = args[0]
2080347c4c4SJohn Snow    ret = client.fsfreeze(cmd)
2090347c4c4SJohn Snow    if cmd == 'status':
2100347c4c4SJohn Snow        print(ret)
2110347c4c4SJohn Snow        return
2120347c4c4SJohn Snow
2130347c4c4SJohn Snow    assert isinstance(ret, int)
2140347c4c4SJohn Snow    verb = 'frozen' if cmd == 'freeze' else 'thawed'
2150347c4c4SJohn Snow    print(f"{ret:d} filesystems {verb}")
2160347c4c4SJohn Snow
2170347c4c4SJohn Snow
2180347c4c4SJohn Snowdef _cmd_fstrim(client: QemuGuestAgentClient, args: Sequence[str]) -> None:
2190347c4c4SJohn Snow    if len(args) == 0:
2200347c4c4SJohn Snow        minimum = 0
2210347c4c4SJohn Snow    else:
2220347c4c4SJohn Snow        minimum = int(args[0])
2230347c4c4SJohn Snow    print(client.fstrim(minimum))
2240347c4c4SJohn Snow
2250347c4c4SJohn Snow
2260347c4c4SJohn Snowdef _cmd_ifconfig(client: QemuGuestAgentClient, args: Sequence[str]) -> None:
2270347c4c4SJohn Snow    assert not args
2280347c4c4SJohn Snow    print(client.ifconfig())
2290347c4c4SJohn Snow
2300347c4c4SJohn Snow
2310347c4c4SJohn Snowdef _cmd_info(client: QemuGuestAgentClient, args: Sequence[str]) -> None:
2320347c4c4SJohn Snow    assert not args
2330347c4c4SJohn Snow    print(client.info())
2340347c4c4SJohn Snow
2350347c4c4SJohn Snow
2360347c4c4SJohn Snowdef _cmd_ping(client: QemuGuestAgentClient, args: Sequence[str]) -> None:
2370347c4c4SJohn Snow    timeout = 3.0 if len(args) == 0 else float(args[0])
2380347c4c4SJohn Snow    alive = client.ping(timeout)
2390347c4c4SJohn Snow    if not alive:
2400347c4c4SJohn Snow        print("Not responded in %s sec" % args[0])
2410347c4c4SJohn Snow        sys.exit(1)
2420347c4c4SJohn Snow
2430347c4c4SJohn Snow
2440347c4c4SJohn Snowdef _cmd_suspend(client: QemuGuestAgentClient, args: Sequence[str]) -> None:
2450347c4c4SJohn Snow    usage = 'Usage: suspend disk|ram|hybrid'
2460347c4c4SJohn Snow    if len(args) != 1:
2470347c4c4SJohn Snow        print('Less argument')
2480347c4c4SJohn Snow        print(usage)
2490347c4c4SJohn Snow        sys.exit(1)
2500347c4c4SJohn Snow    if args[0] not in ['disk', 'ram', 'hybrid']:
2510347c4c4SJohn Snow        print('Invalid command: ' + args[0])
2520347c4c4SJohn Snow        print(usage)
2530347c4c4SJohn Snow        sys.exit(1)
2540347c4c4SJohn Snow    client.suspend(args[0])
2550347c4c4SJohn Snow
2560347c4c4SJohn Snow
2570347c4c4SJohn Snowdef _cmd_shutdown(client: QemuGuestAgentClient, args: Sequence[str]) -> None:
2580347c4c4SJohn Snow    assert not args
2590347c4c4SJohn Snow    client.shutdown()
2600347c4c4SJohn Snow
2610347c4c4SJohn Snow
2620347c4c4SJohn Snow_cmd_powerdown = _cmd_shutdown
2630347c4c4SJohn Snow
2640347c4c4SJohn Snow
2650347c4c4SJohn Snowdef _cmd_halt(client: QemuGuestAgentClient, args: Sequence[str]) -> None:
2660347c4c4SJohn Snow    assert not args
2670347c4c4SJohn Snow    client.shutdown('halt')
2680347c4c4SJohn Snow
2690347c4c4SJohn Snow
2700347c4c4SJohn Snowdef _cmd_reboot(client: QemuGuestAgentClient, args: Sequence[str]) -> None:
2710347c4c4SJohn Snow    assert not args
2720347c4c4SJohn Snow    client.shutdown('reboot')
2730347c4c4SJohn Snow
2740347c4c4SJohn Snow
2750347c4c4SJohn Snowcommands = [m.replace('_cmd_', '') for m in dir() if '_cmd_' in m]
2760347c4c4SJohn Snow
2770347c4c4SJohn Snow
2780347c4c4SJohn Snowdef send_command(address: str, cmd: str, args: Sequence[str]) -> None:
2790347c4c4SJohn Snow    if not os.path.exists(address):
2800347c4c4SJohn Snow        print(f"'{address}' not found. (Is QEMU running?)")
2810347c4c4SJohn Snow        sys.exit(1)
2820347c4c4SJohn Snow
2830347c4c4SJohn Snow    if cmd not in commands:
2840347c4c4SJohn Snow        print('Invalid command: ' + cmd)
2850347c4c4SJohn Snow        print('Available commands: ' + ', '.join(commands))
2860347c4c4SJohn Snow        sys.exit(1)
2870347c4c4SJohn Snow
2880347c4c4SJohn Snow    try:
2890347c4c4SJohn Snow        client = QemuGuestAgentClient(address)
2900347c4c4SJohn Snow    except ConnectError as err:
2910347c4c4SJohn Snow        print(err)
2920347c4c4SJohn Snow        if isinstance(err.exc, ConnectionError):
2930347c4c4SJohn Snow            print('(Is QEMU running?)')
2940347c4c4SJohn Snow        sys.exit(1)
2950347c4c4SJohn Snow
2960347c4c4SJohn Snow    if cmd == 'fsfreeze' and args[0] == 'freeze':
2970347c4c4SJohn Snow        client.sync(60)
2980347c4c4SJohn Snow    elif cmd != 'ping':
2990347c4c4SJohn Snow        client.sync()
3000347c4c4SJohn Snow
3010347c4c4SJohn Snow    globals()['_cmd_' + cmd](client, args)
3020347c4c4SJohn Snow
3030347c4c4SJohn Snow
3040347c4c4SJohn Snowdef main() -> None:
3050347c4c4SJohn Snow    address = os.environ.get('QGA_CLIENT_ADDRESS')
3060347c4c4SJohn Snow
3070347c4c4SJohn Snow    parser = argparse.ArgumentParser()
3080347c4c4SJohn Snow    parser.add_argument('--address', action='store',
3090347c4c4SJohn Snow                        default=address,
3100347c4c4SJohn Snow                        help='Specify a ip:port pair or a unix socket path')
3110347c4c4SJohn Snow    parser.add_argument('command', choices=commands)
3120347c4c4SJohn Snow    parser.add_argument('args', nargs='*')
3130347c4c4SJohn Snow
3140347c4c4SJohn Snow    args = parser.parse_args()
3150347c4c4SJohn Snow    if args.address is None:
3160347c4c4SJohn Snow        parser.error('address is not specified')
3170347c4c4SJohn Snow        sys.exit(1)
3180347c4c4SJohn Snow
3190347c4c4SJohn Snow    send_command(args.address, args.command, args.args)
3200347c4c4SJohn Snow
3210347c4c4SJohn Snow
3220347c4c4SJohn Snowif __name__ == '__main__':
3230347c4c4SJohn Snow    main()
324