xref: /qemu/scripts/qmp/qemu-ga-client (revision f85d3252ef889b102eb42756450f45c973d3cb43)
1#!/usr/bin/env python3
2
3"""
4QEMU Guest Agent Client
5
6Usage:
7
8Start QEMU with:
9
10# qemu [...] -chardev socket,path=/tmp/qga.sock,server,wait=off,id=qga0 \
11  -device virtio-serial \
12  -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0
13
14Run the script:
15
16$ qemu-ga-client --address=/tmp/qga.sock <command> [args...]
17
18or
19
20$ export QGA_CLIENT_ADDRESS=/tmp/qga.sock
21$ qemu-ga-client <command> [args...]
22
23For example:
24
25$ qemu-ga-client cat /etc/resolv.conf
26# Generated by NetworkManager
27nameserver 10.0.2.3
28$ qemu-ga-client fsfreeze status
29thawed
30$ qemu-ga-client fsfreeze freeze
312 filesystems frozen
32
33See also: https://wiki.qemu.org/Features/QAPI/GuestAgent
34"""
35
36# Copyright (C) 2012 Ryota Ozaki <ozaki.ryota@gmail.com>
37#
38# This work is licensed under the terms of the GNU GPL, version 2.  See
39# the COPYING file in the top-level directory.
40
41import argparse
42import base64
43import errno
44import os
45import random
46import sys
47
48
49sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python'))
50from qemu import qmp
51
52
53# This script has not seen many patches or careful attention in quite
54# some time. If you would like to improve it, please review the design
55# carefully and add docstrings at that point in time. Until then:
56
57# pylint: disable=missing-docstring
58
59
60class QemuGuestAgent(qmp.QEMUMonitorProtocol):
61    def __getattr__(self, name):
62        def wrapper(**kwds):
63            return self.command('guest-' + name.replace('_', '-'), **kwds)
64        return wrapper
65
66
67class QemuGuestAgentClient:
68    def __init__(self, address):
69        self.qga = QemuGuestAgent(address)
70        self.qga.connect(negotiate=False)
71
72    def sync(self, timeout=3):
73        # Avoid being blocked forever
74        if not self.ping(timeout):
75            raise EnvironmentError('Agent seems not alive')
76        uid = random.randint(0, (1 << 32) - 1)
77        while True:
78            ret = self.qga.sync(id=uid)
79            if isinstance(ret, int) and int(ret) == uid:
80                break
81
82    def __file_read_all(self, handle):
83        eof = False
84        data = ''
85        while not eof:
86            ret = self.qga.file_read(handle=handle, count=1024)
87            _data = base64.b64decode(ret['buf-b64'])
88            data += _data
89            eof = ret['eof']
90        return data
91
92    def read(self, path):
93        handle = self.qga.file_open(path=path)
94        try:
95            data = self.__file_read_all(handle)
96        finally:
97            self.qga.file_close(handle=handle)
98        return data
99
100    def info(self):
101        info = self.qga.info()
102
103        msgs = []
104        msgs.append('version: ' + info['version'])
105        msgs.append('supported_commands:')
106        enabled = [c['name'] for c in info['supported_commands']
107                   if c['enabled']]
108        msgs.append('\tenabled: ' + ', '.join(enabled))
109        disabled = [c['name'] for c in info['supported_commands']
110                    if not c['enabled']]
111        msgs.append('\tdisabled: ' + ', '.join(disabled))
112
113        return '\n'.join(msgs)
114
115    @classmethod
116    def __gen_ipv4_netmask(cls, prefixlen):
117        mask = int('1' * prefixlen + '0' * (32 - prefixlen), 2)
118        return '.'.join([str(mask >> 24),
119                         str((mask >> 16) & 0xff),
120                         str((mask >> 8) & 0xff),
121                         str(mask & 0xff)])
122
123    def ifconfig(self):
124        nifs = self.qga.network_get_interfaces()
125
126        msgs = []
127        for nif in nifs:
128            msgs.append(nif['name'] + ':')
129            if 'ip-addresses' in nif:
130                for ipaddr in nif['ip-addresses']:
131                    if ipaddr['ip-address-type'] == 'ipv4':
132                        addr = ipaddr['ip-address']
133                        mask = self.__gen_ipv4_netmask(int(ipaddr['prefix']))
134                        msgs.append(f"\tinet {addr}  netmask {mask}")
135                    elif ipaddr['ip-address-type'] == 'ipv6':
136                        addr = ipaddr['ip-address']
137                        prefix = ipaddr['prefix']
138                        msgs.append(f"\tinet6 {addr}  prefixlen {prefix}")
139            if nif['hardware-address'] != '00:00:00:00:00:00':
140                msgs.append("\tether " + nif['hardware-address'])
141
142        return '\n'.join(msgs)
143
144    def ping(self, timeout):
145        self.qga.settimeout(timeout)
146        try:
147            self.qga.ping()
148        except TimeoutError:
149            return False
150        return True
151
152    def fsfreeze(self, cmd):
153        if cmd not in ['status', 'freeze', 'thaw']:
154            raise Exception('Invalid command: ' + cmd)
155
156        return getattr(self.qga, 'fsfreeze' + '_' + cmd)()
157
158    def fstrim(self, minimum=0):
159        return getattr(self.qga, 'fstrim')(minimum=minimum)
160
161    def suspend(self, mode):
162        if mode not in ['disk', 'ram', 'hybrid']:
163            raise Exception('Invalid mode: ' + mode)
164
165        try:
166            getattr(self.qga, 'suspend' + '_' + mode)()
167            # On error exception will raise
168        except self.qga.timeout:
169            # On success command will timed out
170            return
171
172    def shutdown(self, mode='powerdown'):
173        if mode not in ['powerdown', 'halt', 'reboot']:
174            raise Exception('Invalid mode: ' + mode)
175
176        try:
177            self.qga.shutdown(mode=mode)
178        except self.qga.timeout:
179            return
180
181
182def _cmd_cat(client, args):
183    if len(args) != 1:
184        print('Invalid argument')
185        print('Usage: cat <file>')
186        sys.exit(1)
187    print(client.read(args[0]))
188
189
190def _cmd_fsfreeze(client, args):
191    usage = 'Usage: fsfreeze status|freeze|thaw'
192    if len(args) != 1:
193        print('Invalid argument')
194        print(usage)
195        sys.exit(1)
196    if args[0] not in ['status', 'freeze', 'thaw']:
197        print('Invalid command: ' + args[0])
198        print(usage)
199        sys.exit(1)
200    cmd = args[0]
201    ret = client.fsfreeze(cmd)
202    if cmd == 'status':
203        print(ret)
204    elif cmd == 'freeze':
205        print("%d filesystems frozen" % ret)
206    else:
207        print("%d filesystems thawed" % ret)
208
209
210def _cmd_fstrim(client, args):
211    if len(args) == 0:
212        minimum = 0
213    else:
214        minimum = int(args[0])
215    print(client.fstrim(minimum))
216
217
218def _cmd_ifconfig(client, args):
219    assert not args
220    print(client.ifconfig())
221
222
223def _cmd_info(client, args):
224    assert not args
225    print(client.info())
226
227
228def _cmd_ping(client, args):
229    if len(args) == 0:
230        timeout = 3
231    else:
232        timeout = float(args[0])
233    alive = client.ping(timeout)
234    if not alive:
235        print("Not responded in %s sec" % args[0])
236        sys.exit(1)
237
238
239def _cmd_suspend(client, args):
240    usage = 'Usage: suspend disk|ram|hybrid'
241    if len(args) != 1:
242        print('Less argument')
243        print(usage)
244        sys.exit(1)
245    if args[0] not in ['disk', 'ram', 'hybrid']:
246        print('Invalid command: ' + args[0])
247        print(usage)
248        sys.exit(1)
249    client.suspend(args[0])
250
251
252def _cmd_shutdown(client, args):
253    assert not args
254    client.shutdown()
255
256
257_cmd_powerdown = _cmd_shutdown
258
259
260def _cmd_halt(client, args):
261    assert not args
262    client.shutdown('halt')
263
264
265def _cmd_reboot(client, args):
266    assert not args
267    client.shutdown('reboot')
268
269
270commands = [m.replace('_cmd_', '') for m in dir() if '_cmd_' in m]
271
272
273def send_command(address, cmd, args):
274    if not os.path.exists(address):
275        print('%s not found' % address)
276        sys.exit(1)
277
278    if cmd not in commands:
279        print('Invalid command: ' + cmd)
280        print('Available commands: ' + ', '.join(commands))
281        sys.exit(1)
282
283    try:
284        client = QemuGuestAgentClient(address)
285    except OSError as err:
286        print(err)
287        if err.errno == errno.ECONNREFUSED:
288            print('Hint: qemu is not running?')
289        sys.exit(1)
290
291    if cmd == 'fsfreeze' and args[0] == 'freeze':
292        client.sync(60)
293    elif cmd != 'ping':
294        client.sync()
295
296    globals()['_cmd_' + cmd](client, args)
297
298
299def main():
300    address = os.environ.get('QGA_CLIENT_ADDRESS')
301
302    parser = argparse.ArgumentParser()
303    parser.add_argument('--address', action='store',
304                        default=address,
305                        help='Specify a ip:port pair or a unix socket path')
306    parser.add_argument('command', choices=commands)
307    parser.add_argument('args', nargs='*')
308
309    args = parser.parse_args()
310    if args.address is None:
311        parser.error('address is not specified')
312        sys.exit(1)
313
314    send_command(args.address, args.command, args.args)
315
316
317if __name__ == '__main__':
318    main()
319