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