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