Ryota Ozaki | f513cbf | 2012-09-14 21:44:22 +0900 | [diff] [blame] | 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: http://wiki.qemu.org/Features/QAPI/GuestAgent |
| 37 | # |
| 38 | |
| 39 | import base64 |
| 40 | import random |
| 41 | |
| 42 | import qmp |
| 43 | |
| 44 | |
| 45 | class 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 | |
| 52 | class 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 | |
| 166 | def _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 | |
| 174 | def _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 | |
| 194 | def _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 | |
| 202 | def _cmd_ifconfig(client, args): |
| 203 | print(client.ifconfig()) |
| 204 | |
| 205 | |
| 206 | def _cmd_info(client, args): |
| 207 | print(client.info()) |
| 208 | |
| 209 | |
| 210 | def _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 | |
| 221 | def _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 | |
| 234 | def _cmd_shutdown(client, args): |
| 235 | client.shutdown() |
| 236 | _cmd_powerdown = _cmd_shutdown |
| 237 | |
| 238 | |
| 239 | def _cmd_halt(client, args): |
| 240 | client.shutdown('halt') |
| 241 | |
| 242 | |
| 243 | def _cmd_reboot(client, args): |
| 244 | client.shutdown('reboot') |
| 245 | |
| 246 | |
| 247 | commands = [m.replace('_cmd_', '') for m in dir() if '_cmd_' in m] |
| 248 | |
| 249 | |
| 250 | def 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, 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 != 'ping': |
| 271 | client.sync() |
| 272 | |
| 273 | globals()['_cmd_' + cmd](client, args) |
| 274 | |
| 275 | |
| 276 | if __name__ == '__main__': |
| 277 | import sys |
| 278 | import os |
| 279 | import optparse |
| 280 | |
| 281 | address = os.environ['QGA_CLIENT_ADDRESS'] if 'QGA_CLIENT_ADDRESS' in os.environ else None |
| 282 | |
| 283 | usage = "%prog [--address=<unix_path>|<ipv4_address>] <command> [args...]\n" |
| 284 | usage += '<command>: ' + ', '.join(commands) |
| 285 | parser = optparse.OptionParser(usage=usage) |
| 286 | parser.add_option('--address', action='store', type='string', |
| 287 | default=address, help='Specify a ip:port pair or a unix socket path') |
| 288 | options, args = parser.parse_args() |
| 289 | |
| 290 | address = options.address |
| 291 | if address is None: |
| 292 | parser.error('address is not specified') |
| 293 | sys.exit(1) |
| 294 | |
| 295 | if len(args) == 0: |
| 296 | parser.error('Less argument') |
| 297 | sys.exit(1) |
| 298 | |
| 299 | main(address, args[0], args[1:]) |