1#!/usr/bin/env python3 2 3""" 4QEMU Guest Agent Client 5 6Usage: 7 8Start QEMU with: 9 10# qemu [...] -chardev socket,path=/tmp/qga.sock,server,wait=off,id=qga0 \ 11 -device virtio-serial \ 12 -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 13 14Run the script: 15 16$ qemu-ga-client --address=/tmp/qga.sock <command> [args...] 17 18or 19 20$ export QGA_CLIENT_ADDRESS=/tmp/qga.sock 21$ qemu-ga-client <command> [args...] 22 23For example: 24 25$ qemu-ga-client cat /etc/resolv.conf 26# Generated by NetworkManager 27nameserver 10.0.2.3 28$ qemu-ga-client fsfreeze status 29thawed 30$ qemu-ga-client fsfreeze freeze 312 filesystems frozen 32 33See also: https://wiki.qemu.org/Features/QAPI/GuestAgent 34""" 35 36# Copyright (C) 2012 Ryota Ozaki <ozaki.ryota@gmail.com> 37# 38# This work is licensed under the terms of the GNU GPL, version 2. See 39# the COPYING file in the top-level directory. 40 41import argparse 42import base64 43import os 44import random 45import sys 46 47 48sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) 49from qemu import qmp 50 51 52class QemuGuestAgent(qmp.QEMUMonitorProtocol): 53 def __getattr__(self, name): 54 def wrapper(**kwds): 55 return self.command('guest-' + name.replace('_', '-'), **kwds) 56 return wrapper 57 58 59class QemuGuestAgentClient: 60 def __init__(self, address): 61 self.qga = QemuGuestAgent(address) 62 self.qga.connect(negotiate=False) 63 64 def sync(self, timeout=3): 65 # Avoid being blocked forever 66 if not self.ping(timeout): 67 raise EnvironmentError('Agent seems not alive') 68 uid = random.randint(0, (1 << 32) - 1) 69 while True: 70 ret = self.qga.sync(id=uid) 71 if isinstance(ret, int) and int(ret) == uid: 72 break 73 74 def __file_read_all(self, handle): 75 eof = False 76 data = '' 77 while not eof: 78 ret = self.qga.file_read(handle=handle, count=1024) 79 _data = base64.b64decode(ret['buf-b64']) 80 data += _data 81 eof = ret['eof'] 82 return data 83 84 def read(self, path): 85 handle = self.qga.file_open(path=path) 86 try: 87 data = self.__file_read_all(handle) 88 finally: 89 self.qga.file_close(handle=handle) 90 return data 91 92 def info(self): 93 info = self.qga.info() 94 95 msgs = [] 96 msgs.append('version: ' + info['version']) 97 msgs.append('supported_commands:') 98 enabled = [c['name'] for c in info['supported_commands'] 99 if c['enabled']] 100 msgs.append('\tenabled: ' + ', '.join(enabled)) 101 disabled = [c['name'] for c in info['supported_commands'] 102 if not c['enabled']] 103 msgs.append('\tdisabled: ' + ', '.join(disabled)) 104 105 return '\n'.join(msgs) 106 107 def __gen_ipv4_netmask(self, prefixlen): 108 mask = int('1' * prefixlen + '0' * (32 - prefixlen), 2) 109 return '.'.join([str(mask >> 24), 110 str((mask >> 16) & 0xff), 111 str((mask >> 8) & 0xff), 112 str(mask & 0xff)]) 113 114 def ifconfig(self): 115 nifs = self.qga.network_get_interfaces() 116 117 msgs = [] 118 for nif in nifs: 119 msgs.append(nif['name'] + ':') 120 if 'ip-addresses' in nif: 121 for ipaddr in nif['ip-addresses']: 122 if ipaddr['ip-address-type'] == 'ipv4': 123 addr = ipaddr['ip-address'] 124 mask = self.__gen_ipv4_netmask(int(ipaddr['prefix'])) 125 msgs.append(f"\tinet {addr} netmask {mask}") 126 elif ipaddr['ip-address-type'] == 'ipv6': 127 addr = ipaddr['ip-address'] 128 prefix = ipaddr['prefix'] 129 msgs.append(f"\tinet6 {addr} prefixlen {prefix}") 130 if nif['hardware-address'] != '00:00:00:00:00:00': 131 msgs.append("\tether " + nif['hardware-address']) 132 133 return '\n'.join(msgs) 134 135 def ping(self, timeout): 136 self.qga.settimeout(timeout) 137 try: 138 self.qga.ping() 139 except TimeoutError: 140 return False 141 return True 142 143 def fsfreeze(self, cmd): 144 if cmd not in ['status', 'freeze', 'thaw']: 145 raise Exception('Invalid command: ' + cmd) 146 147 return getattr(self.qga, 'fsfreeze' + '_' + cmd)() 148 149 def fstrim(self, minimum=0): 150 return getattr(self.qga, 'fstrim')(minimum=minimum) 151 152 def suspend(self, mode): 153 if mode not in ['disk', 'ram', 'hybrid']: 154 raise Exception('Invalid mode: ' + mode) 155 156 try: 157 getattr(self.qga, 'suspend' + '_' + mode)() 158 # On error exception will raise 159 except self.qga.timeout: 160 # On success command will timed out 161 return 162 163 def shutdown(self, mode='powerdown'): 164 if mode not in ['powerdown', 'halt', 'reboot']: 165 raise Exception('Invalid mode: ' + mode) 166 167 try: 168 self.qga.shutdown(mode=mode) 169 except self.qga.timeout: 170 return 171 172 173def _cmd_cat(client, args): 174 if len(args) != 1: 175 print('Invalid argument') 176 print('Usage: cat <file>') 177 sys.exit(1) 178 print(client.read(args[0])) 179 180 181def _cmd_fsfreeze(client, args): 182 usage = 'Usage: fsfreeze status|freeze|thaw' 183 if len(args) != 1: 184 print('Invalid argument') 185 print(usage) 186 sys.exit(1) 187 if args[0] not in ['status', 'freeze', 'thaw']: 188 print('Invalid command: ' + args[0]) 189 print(usage) 190 sys.exit(1) 191 cmd = args[0] 192 ret = client.fsfreeze(cmd) 193 if cmd == 'status': 194 print(ret) 195 elif cmd == 'freeze': 196 print("%d filesystems frozen" % ret) 197 else: 198 print("%d filesystems thawed" % ret) 199 200 201def _cmd_fstrim(client, args): 202 if len(args) == 0: 203 minimum = 0 204 else: 205 minimum = int(args[0]) 206 print(client.fstrim(minimum)) 207 208 209def _cmd_ifconfig(client, args): 210 print(client.ifconfig()) 211 212 213def _cmd_info(client, args): 214 print(client.info()) 215 216 217def _cmd_ping(client, args): 218 if len(args) == 0: 219 timeout = 3 220 else: 221 timeout = float(args[0]) 222 alive = client.ping(timeout) 223 if not alive: 224 print("Not responded in %s sec" % args[0]) 225 sys.exit(1) 226 227 228def _cmd_suspend(client, args): 229 usage = 'Usage: suspend disk|ram|hybrid' 230 if len(args) != 1: 231 print('Less argument') 232 print(usage) 233 sys.exit(1) 234 if args[0] not in ['disk', 'ram', 'hybrid']: 235 print('Invalid command: ' + args[0]) 236 print(usage) 237 sys.exit(1) 238 client.suspend(args[0]) 239 240 241def _cmd_shutdown(client, args): 242 client.shutdown() 243 244 245_cmd_powerdown = _cmd_shutdown 246 247 248def _cmd_halt(client, args): 249 client.shutdown('halt') 250 251 252def _cmd_reboot(client, args): 253 client.shutdown('reboot') 254 255 256commands = [m.replace('_cmd_', '') for m in dir() if '_cmd_' in m] 257 258 259def send_command(address, cmd, args): 260 if not os.path.exists(address): 261 print('%s not found' % address) 262 sys.exit(1) 263 264 if cmd not in commands: 265 print('Invalid command: ' + cmd) 266 print('Available commands: ' + ', '.join(commands)) 267 sys.exit(1) 268 269 try: 270 client = QemuGuestAgentClient(address) 271 except OSError as err: 272 import errno 273 274 print(err) 275 if err.errno == errno.ECONNREFUSED: 276 print('Hint: qemu is not running?') 277 sys.exit(1) 278 279 if cmd == 'fsfreeze' and args[0] == 'freeze': 280 client.sync(60) 281 elif cmd != 'ping': 282 client.sync() 283 284 globals()['_cmd_' + cmd](client, args) 285 286 287def main(): 288 address = os.environ.get('QGA_CLIENT_ADDRESS') 289 290 parser = argparse.ArgumentParser() 291 parser.add_argument('--address', action='store', 292 default=address, 293 help='Specify a ip:port pair or a unix socket path') 294 parser.add_argument('command', choices=commands) 295 parser.add_argument('args', nargs='*') 296 297 args = parser.parse_args() 298 if args.address is None: 299 parser.error('address is not specified') 300 sys.exit(1) 301 302 send_command(args.address, args.command, args.args) 303 304 305if __name__ == '__main__': 306 main() 307