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 errno 44import os 45import random 46import sys 47 48 49sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) 50from qemu import qmp 51 52 53# This script has not seen many patches or careful attention in quite 54# some time. If you would like to improve it, please review the design 55# carefully and add docstrings at that point in time. Until then: 56 57# pylint: disable=missing-docstring 58 59 60class QemuGuestAgent(qmp.QEMUMonitorProtocol): 61 def __getattr__(self, name): 62 def wrapper(**kwds): 63 return self.command('guest-' + name.replace('_', '-'), **kwds) 64 return wrapper 65 66 67class QemuGuestAgentClient: 68 def __init__(self, address): 69 self.qga = QemuGuestAgent(address) 70 self.qga.connect(negotiate=False) 71 72 def sync(self, timeout=3): 73 # Avoid being blocked forever 74 if not self.ping(timeout): 75 raise EnvironmentError('Agent seems not alive') 76 uid = random.randint(0, (1 << 32) - 1) 77 while True: 78 ret = self.qga.sync(id=uid) 79 if isinstance(ret, int) and int(ret) == uid: 80 break 81 82 def __file_read_all(self, handle): 83 eof = False 84 data = '' 85 while not eof: 86 ret = self.qga.file_read(handle=handle, count=1024) 87 _data = base64.b64decode(ret['buf-b64']) 88 data += _data 89 eof = ret['eof'] 90 return data 91 92 def read(self, path): 93 handle = self.qga.file_open(path=path) 94 try: 95 data = self.__file_read_all(handle) 96 finally: 97 self.qga.file_close(handle=handle) 98 return data 99 100 def info(self): 101 info = self.qga.info() 102 103 msgs = [] 104 msgs.append('version: ' + info['version']) 105 msgs.append('supported_commands:') 106 enabled = [c['name'] for c in info['supported_commands'] 107 if c['enabled']] 108 msgs.append('\tenabled: ' + ', '.join(enabled)) 109 disabled = [c['name'] for c in info['supported_commands'] 110 if not c['enabled']] 111 msgs.append('\tdisabled: ' + ', '.join(disabled)) 112 113 return '\n'.join(msgs) 114 115 @classmethod 116 def __gen_ipv4_netmask(cls, prefixlen): 117 mask = int('1' * prefixlen + '0' * (32 - prefixlen), 2) 118 return '.'.join([str(mask >> 24), 119 str((mask >> 16) & 0xff), 120 str((mask >> 8) & 0xff), 121 str(mask & 0xff)]) 122 123 def ifconfig(self): 124 nifs = self.qga.network_get_interfaces() 125 126 msgs = [] 127 for nif in nifs: 128 msgs.append(nif['name'] + ':') 129 if 'ip-addresses' in nif: 130 for ipaddr in nif['ip-addresses']: 131 if ipaddr['ip-address-type'] == 'ipv4': 132 addr = ipaddr['ip-address'] 133 mask = self.__gen_ipv4_netmask(int(ipaddr['prefix'])) 134 msgs.append(f"\tinet {addr} netmask {mask}") 135 elif ipaddr['ip-address-type'] == 'ipv6': 136 addr = ipaddr['ip-address'] 137 prefix = ipaddr['prefix'] 138 msgs.append(f"\tinet6 {addr} prefixlen {prefix}") 139 if nif['hardware-address'] != '00:00:00:00:00:00': 140 msgs.append("\tether " + nif['hardware-address']) 141 142 return '\n'.join(msgs) 143 144 def ping(self, timeout): 145 self.qga.settimeout(timeout) 146 try: 147 self.qga.ping() 148 except TimeoutError: 149 return False 150 return True 151 152 def fsfreeze(self, cmd): 153 if cmd not in ['status', 'freeze', 'thaw']: 154 raise Exception('Invalid command: ' + cmd) 155 156 return getattr(self.qga, 'fsfreeze' + '_' + cmd)() 157 158 def fstrim(self, minimum=0): 159 return getattr(self.qga, 'fstrim')(minimum=minimum) 160 161 def suspend(self, mode): 162 if mode not in ['disk', 'ram', 'hybrid']: 163 raise Exception('Invalid mode: ' + mode) 164 165 try: 166 getattr(self.qga, 'suspend' + '_' + mode)() 167 # On error exception will raise 168 except self.qga.timeout: 169 # On success command will timed out 170 return 171 172 def shutdown(self, mode='powerdown'): 173 if mode not in ['powerdown', 'halt', 'reboot']: 174 raise Exception('Invalid mode: ' + mode) 175 176 try: 177 self.qga.shutdown(mode=mode) 178 except self.qga.timeout: 179 return 180 181 182def _cmd_cat(client, args): 183 if len(args) != 1: 184 print('Invalid argument') 185 print('Usage: cat <file>') 186 sys.exit(1) 187 print(client.read(args[0])) 188 189 190def _cmd_fsfreeze(client, args): 191 usage = 'Usage: fsfreeze status|freeze|thaw' 192 if len(args) != 1: 193 print('Invalid argument') 194 print(usage) 195 sys.exit(1) 196 if args[0] not in ['status', 'freeze', 'thaw']: 197 print('Invalid command: ' + args[0]) 198 print(usage) 199 sys.exit(1) 200 cmd = args[0] 201 ret = client.fsfreeze(cmd) 202 if cmd == 'status': 203 print(ret) 204 elif cmd == 'freeze': 205 print("%d filesystems frozen" % ret) 206 else: 207 print("%d filesystems thawed" % ret) 208 209 210def _cmd_fstrim(client, args): 211 if len(args) == 0: 212 minimum = 0 213 else: 214 minimum = int(args[0]) 215 print(client.fstrim(minimum)) 216 217 218def _cmd_ifconfig(client, args): 219 assert not args 220 print(client.ifconfig()) 221 222 223def _cmd_info(client, args): 224 assert not args 225 print(client.info()) 226 227 228def _cmd_ping(client, args): 229 if len(args) == 0: 230 timeout = 3 231 else: 232 timeout = float(args[0]) 233 alive = client.ping(timeout) 234 if not alive: 235 print("Not responded in %s sec" % args[0]) 236 sys.exit(1) 237 238 239def _cmd_suspend(client, args): 240 usage = 'Usage: suspend disk|ram|hybrid' 241 if len(args) != 1: 242 print('Less argument') 243 print(usage) 244 sys.exit(1) 245 if args[0] not in ['disk', 'ram', 'hybrid']: 246 print('Invalid command: ' + args[0]) 247 print(usage) 248 sys.exit(1) 249 client.suspend(args[0]) 250 251 252def _cmd_shutdown(client, args): 253 assert not args 254 client.shutdown() 255 256 257_cmd_powerdown = _cmd_shutdown 258 259 260def _cmd_halt(client, args): 261 assert not args 262 client.shutdown('halt') 263 264 265def _cmd_reboot(client, args): 266 assert not args 267 client.shutdown('reboot') 268 269 270commands = [m.replace('_cmd_', '') for m in dir() if '_cmd_' in m] 271 272 273def send_command(address, cmd, args): 274 if not os.path.exists(address): 275 print('%s not found' % address) 276 sys.exit(1) 277 278 if cmd not in commands: 279 print('Invalid command: ' + cmd) 280 print('Available commands: ' + ', '.join(commands)) 281 sys.exit(1) 282 283 try: 284 client = QemuGuestAgentClient(address) 285 except OSError as err: 286 print(err) 287 if err.errno == errno.ECONNREFUSED: 288 print('Hint: qemu is not running?') 289 sys.exit(1) 290 291 if cmd == 'fsfreeze' and args[0] == 'freeze': 292 client.sync(60) 293 elif cmd != 'ping': 294 client.sync() 295 296 globals()['_cmd_' + cmd](client, args) 297 298 299def main(): 300 address = os.environ.get('QGA_CLIENT_ADDRESS') 301 302 parser = argparse.ArgumentParser() 303 parser.add_argument('--address', action='store', 304 default=address, 305 help='Specify a ip:port pair or a unix socket path') 306 parser.add_argument('command', choices=commands) 307 parser.add_argument('args', nargs='*') 308 309 args = parser.parse_args() 310 if args.address is None: 311 parser.error('address is not specified') 312 sys.exit(1) 313 314 send_command(args.address, args.command, args.args) 315 316 317if __name__ == '__main__': 318 main() 319