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