__init__.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. # -*- encoding: utf-8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published by
  10. # the Free Software Foundation; either version 2.1 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. '''Qubes VM objects.'''
  21. import logging
  22. import shlex
  23. import subprocess
  24. import warnings
  25. import qubesadmin.base
  26. import qubesadmin.exc
  27. import qubesadmin.storage
  28. import qubesadmin.features
  29. import qubesadmin.devices
  30. import qubesadmin.firewall
  31. import qubesadmin.tags
  32. if not hasattr(shlex, 'quote'):
  33. # python2 compat
  34. import pipes
  35. shlex.quote = pipes.quote
  36. class QubesVM(qubesadmin.base.PropertyHolder):
  37. '''Qubes domain.'''
  38. log = None
  39. tags = None
  40. features = None
  41. devices = None
  42. firewall = None
  43. def __init__(self, app, name, klass=None):
  44. super(QubesVM, self).__init__(app, 'admin.vm.property.', name)
  45. self._volumes = None
  46. self._klass = klass
  47. self.log = logging.getLogger(name)
  48. self.tags = qubesadmin.tags.Tags(self)
  49. self.features = qubesadmin.features.Features(self)
  50. self.devices = qubesadmin.devices.DeviceManager(self)
  51. self.firewall = qubesadmin.firewall.Firewall(self)
  52. @property
  53. def name(self):
  54. '''Domain name'''
  55. return self._method_dest
  56. @name.setter
  57. def name(self, new_value):
  58. self.qubesd_call(
  59. self._method_dest,
  60. self._method_prefix + 'Set',
  61. 'name',
  62. str(new_value).encode('utf-8'))
  63. self._method_dest = new_value
  64. self._volumes = None
  65. self.app.domains.clear_cache()
  66. def __str__(self):
  67. return self._method_dest
  68. def __lt__(self, other):
  69. if isinstance(other, QubesVM):
  70. return self.name < other.name
  71. return NotImplemented
  72. def __eq__(self, other):
  73. if isinstance(other, QubesVM):
  74. return self.name == other.name
  75. if isinstance(other, str):
  76. return self.name == other
  77. return NotImplemented
  78. def __hash__(self):
  79. return hash(self.name)
  80. def start(self):
  81. '''
  82. Start domain.
  83. :return:
  84. '''
  85. self.qubesd_call(self._method_dest, 'admin.vm.Start')
  86. def shutdown(self, force=False):
  87. '''
  88. Shutdown domain.
  89. :return:
  90. '''
  91. # TODO: force parameter
  92. # TODO: wait parameter (using event?)
  93. if force:
  94. self.qubesd_call(self._method_dest, 'admin.vm.Shutdown', 'force')
  95. else:
  96. self.qubesd_call(self._method_dest, 'admin.vm.Shutdown')
  97. def kill(self):
  98. '''
  99. Kill domain (forcefuly shutdown).
  100. :return:
  101. '''
  102. self.qubesd_call(self._method_dest, 'admin.vm.Kill')
  103. def force_shutdown(self):
  104. '''Deprecated alias for :py:meth:`kill`'''
  105. warnings.warn(
  106. 'Call to deprecated function force_shutdown(), use kill() instead',
  107. DeprecationWarning, stacklevel=2)
  108. return self.kill()
  109. def pause(self):
  110. '''
  111. Pause domain.
  112. Pause its execution without any prior notification.
  113. :return:
  114. '''
  115. self.qubesd_call(self._method_dest, 'admin.vm.Pause')
  116. def unpause(self):
  117. '''
  118. Unpause domain.
  119. Opposite to :py:meth:`pause`.
  120. :return:
  121. '''
  122. self.qubesd_call(self._method_dest, 'admin.vm.Unpause')
  123. def get_power_state(self):
  124. '''Return power state description string.
  125. Return value may be one of those:
  126. =============== ========================================================
  127. return value meaning
  128. =============== ========================================================
  129. ``'Halted'`` Machine is not active.
  130. ``'Transient'`` Machine is running, but does not have :program:`guid`
  131. or :program:`qrexec` available.
  132. ``'Running'`` Machine is ready and running.
  133. ``'Paused'`` Machine is paused.
  134. ``'Suspended'`` Machine is S3-suspended.
  135. ``'Halting'`` Machine is in process of shutting down (OS shutdown).
  136. ``'Dying'`` Machine is in process of shutting down (cleanup).
  137. ``'Crashed'`` Machine crashed and is unusable.
  138. ``'NA'`` Machine is in unknown state.
  139. =============== ========================================================
  140. .. seealso::
  141. http://wiki.libvirt.org/page/VM_lifecycle
  142. Description of VM life cycle from the point of view of libvirt.
  143. https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState
  144. Libvirt's enum describing precise state of a domain.
  145. '''
  146. try:
  147. return self._get_current_state()['power_state']
  148. except qubesadmin.exc.QubesDaemonNoResponseError:
  149. return 'NA'
  150. def get_mem(self):
  151. '''Get current memory usage from VM.'''
  152. return int(self._get_current_state()['mem'])
  153. def _get_current_state(self):
  154. '''Call admin.vm.CurrentState, and return the result as a dict.'''
  155. state = {}
  156. response = self.qubesd_call(self._method_dest, 'admin.vm.CurrentState')
  157. for part in response.decode('ascii').split():
  158. name, value = part.split('=', 1)
  159. state[name] = value
  160. return state
  161. def is_halted(self):
  162. ''' Check whether this domain's state is 'Halted'
  163. :returns: :py:obj:`True` if this domain is halted, \
  164. :py:obj:`False` otherwise.
  165. :rtype: bool
  166. '''
  167. return self.get_power_state() == 'Halted'
  168. def is_paused(self):
  169. '''Check whether this domain is paused.
  170. :returns: :py:obj:`True` if this domain is paused, \
  171. :py:obj:`False` otherwise.
  172. :rtype: bool
  173. '''
  174. return self.get_power_state() == 'Paused'
  175. def is_running(self):
  176. '''Check whether this domain is running.
  177. :returns: :py:obj:`True` if this domain is started, \
  178. :py:obj:`False` otherwise.
  179. :rtype: bool
  180. '''
  181. return self.get_power_state() not in ('Halted', 'NA')
  182. def is_networked(self):
  183. '''Check whether this VM can reach network (firewall notwithstanding).
  184. :returns: :py:obj:`True` if is machine can reach network, \
  185. :py:obj:`False` otherwise.
  186. :rtype: bool
  187. '''
  188. if self.provides_network:
  189. return True
  190. return self.netvm is not None
  191. @property
  192. def volumes(self):
  193. '''VM disk volumes'''
  194. if self._volumes is None:
  195. volumes_list = self.qubesd_call(
  196. self._method_dest, 'admin.vm.volume.List')
  197. self._volumes = {}
  198. for volname in volumes_list.decode('ascii').splitlines():
  199. if not volname:
  200. continue
  201. self._volumes[volname] = qubesadmin.storage.Volume(self.app,
  202. vm=self.name, vm_name=volname)
  203. return self._volumes
  204. def get_disk_utilization(self):
  205. '''Get total disk usage of the VM'''
  206. return sum(vol.usage for vol in self.volumes.values())
  207. def run_service(self, service, **kwargs):
  208. '''Run service on this VM
  209. :param str service: service name
  210. :rtype: subprocess.Popen
  211. '''
  212. return self.app.run_service(self._method_dest, service, **kwargs)
  213. def run_service_for_stdio(self, service, input=None, **kwargs):
  214. '''Run a service, pass an optional input and return (stdout, stderr).
  215. Raises an exception if return code != 0.
  216. *args* and *kwargs* are passed verbatim to :py:meth:`run_service`.
  217. .. warning::
  218. There are some combinations if stdio-related *kwargs*, which are
  219. not filtered for problems originating between the keyboard and the
  220. chair.
  221. ''' # pylint: disable=redefined-builtin
  222. p = self.run_service(service, **kwargs)
  223. # this one is actually a tuple, but there is no need to unpack it
  224. stdouterr = p.communicate(input=input)
  225. if p.returncode:
  226. exc = subprocess.CalledProcessError(p.returncode, service)
  227. # Python < 3.5 didn't have those
  228. exc.output, exc.stderr = stdouterr
  229. raise exc
  230. return stdouterr
  231. def prepare_input_for_vmshell(self, command, input=None):
  232. '''Prepare shell input for the given command and optional (real) input
  233. ''' # pylint: disable=redefined-builtin
  234. if input is None:
  235. input = b''
  236. close_shell_suffix = b'; exit\n'
  237. if self.features.check_with_template('os', 'Linux') == 'Windows':
  238. close_shell_suffix = b'& exit\n'
  239. return b''.join((command.rstrip('\n').encode('utf-8'),
  240. close_shell_suffix, input))
  241. def run(self, command, input=None, **kwargs):
  242. '''Run a shell command inside the domain using qubes.VMShell qrexec.
  243. ''' # pylint: disable=redefined-builtin
  244. try:
  245. return self.run_service_for_stdio('qubes.VMShell',
  246. input=self.prepare_input_for_vmshell(command, input), **kwargs)
  247. except subprocess.CalledProcessError as e:
  248. e.cmd = command
  249. raise e
  250. def run_with_args(self, *args, **kwargs):
  251. '''Run a single command inside the domain using qubes.VMShell qrexec.
  252. This method execute a single command, without interpreting any shell
  253. special characters.
  254. ''' # pylint: disable=redefined-builtin
  255. return self.run(' '.join(shlex.quote(arg) for arg in args), **kwargs)
  256. @property
  257. def appvms(self):
  258. ''' Returns a generator containing all domains based on the current
  259. TemplateVM.
  260. Do not check vm type of self, core (including its extentions) have
  261. ultimate control what can be a template of what.
  262. '''
  263. for vm in self.app.domains:
  264. try:
  265. if vm.template == self:
  266. yield vm
  267. except AttributeError:
  268. pass
  269. @property
  270. def connected_vms(self):
  271. ''' Return a generator containing all domains connected to the current
  272. NetVM.
  273. '''
  274. for vm in self.app.domains:
  275. try:
  276. if vm.netvm == self:
  277. yield vm
  278. except AttributeError:
  279. pass
  280. @property
  281. def klass(self):
  282. ''' Qube class '''
  283. # use cached value if available
  284. if self._klass is None:
  285. # pylint: disable=no-member
  286. self._klass = super(QubesVM, self).klass
  287. return self._klass
  288. class DispVMWrapper(QubesVM):
  289. '''Wrapper class for new DispVM, supporting only service call
  290. Note that when running in dom0, one need to manually kill the DispVM after
  291. service call ends.
  292. '''
  293. def run_service(self, service, **kwargs):
  294. if self.app.qubesd_connection_type == 'socket':
  295. # create dispvm at service call
  296. if self._method_dest.startswith('$dispvm'):
  297. if self._method_dest.startswith('$dispvm:'):
  298. method_dest = self._method_dest[len('$dispvm:'):]
  299. else:
  300. method_dest = 'dom0'
  301. dispvm = self.app.qubesd_call(method_dest,
  302. 'admin.vm.CreateDisposable')
  303. dispvm = dispvm.decode('ascii')
  304. self._method_dest = dispvm
  305. # Service call may wait for session start, give it more time
  306. # than default 5s
  307. kwargs['connect_timeout'] = self.qrexec_timeout
  308. return super(DispVMWrapper, self).run_service(service, **kwargs)
  309. def cleanup(self):
  310. '''Cleanup after DispVM usage'''
  311. # in 'remote' case nothing is needed, as DispVM is cleaned up
  312. # automatically
  313. if self.app.qubesd_connection_type == 'socket' and \
  314. not self._method_dest.startswith('$dispvm'):
  315. try:
  316. self.kill()
  317. except qubesadmin.exc.QubesVMNotRunningError:
  318. pass
  319. class DispVM(QubesVM):
  320. '''Disposable VM'''
  321. @classmethod
  322. def from_appvm(cls, app, appvm):
  323. '''Returns a wrapper for calling service in a new DispVM based on given
  324. AppVM. If *appvm* is none, use default DispVM template'''
  325. if appvm:
  326. method_dest = '$dispvm:' + str(appvm)
  327. else:
  328. method_dest = '$dispvm'
  329. wrapper = DispVMWrapper(app, method_dest)
  330. return wrapper