]>
Commit | Line | Data |
---|---|---|
f513cbf7 RO |
1 | #!/usr/bin/python |
2 | ||
3 | # QEMU Guest Agent Client | |
4 | # | |
5 | # Copyright (C) 2012 Ryota Ozaki <[email protected]> | |
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:]) |