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