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