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