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