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