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