qemu_ga_client.py 9.3 KB

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