__init__.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. # -*- encoding: utf8 -*-
  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. raise NotImplementedError
  95. self.qubesd_call(self._method_dest, 'admin.vm.Shutdown')
  96. def kill(self):
  97. '''
  98. Kill domain (forcefuly shutdown).
  99. :return:
  100. '''
  101. self.qubesd_call(self._method_dest, 'admin.vm.Kill')
  102. def force_shutdown(self):
  103. '''Deprecated alias for :py:meth:`kill`'''
  104. warnings.warn(
  105. 'Call to deprecated function force_shutdown(), use kill() instead',
  106. DeprecationWarning, stacklevel=2)
  107. return self.kill()
  108. def pause(self):
  109. '''
  110. Pause domain.
  111. Pause its execution without any prior notification.
  112. :return:
  113. '''
  114. self.qubesd_call(self._method_dest, 'admin.vm.Pause')
  115. def unpause(self):
  116. '''
  117. Unpause domain.
  118. Opposite to :py:meth:`pause`.
  119. :return:
  120. '''
  121. self.qubesd_call(self._method_dest, 'admin.vm.Unpause')
  122. def get_power_state(self):
  123. '''Return power state description string.
  124. Return value may be one of those:
  125. =============== ========================================================
  126. return value meaning
  127. =============== ========================================================
  128. ``'Halted'`` Machine is not active.
  129. ``'Transient'`` Machine is running, but does not have :program:`guid`
  130. or :program:`qrexec` available.
  131. ``'Running'`` Machine is ready and running.
  132. ``'Paused'`` Machine is paused.
  133. ``'Suspended'`` Machine is S3-suspended.
  134. ``'Halting'`` Machine is in process of shutting down (OS shutdown).
  135. ``'Dying'`` Machine is in process of shutting down (cleanup).
  136. ``'Crashed'`` Machine crashed and is unusable.
  137. ``'NA'`` Machine is in unknown state.
  138. =============== ========================================================
  139. .. seealso::
  140. http://wiki.libvirt.org/page/VM_lifecycle
  141. Description of VM life cycle from the point of view of libvirt.
  142. https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState
  143. Libvirt's enum describing precise state of a domain.
  144. '''
  145. try:
  146. vm_list_info = [line
  147. for line in self.qubesd_call(
  148. self._method_dest, 'admin.vm.List', None, None
  149. ).decode('ascii').split('\n')
  150. if line.startswith(self._method_dest+' ')]
  151. except qubesadmin.exc.QubesDaemonNoResponseError:
  152. return 'NA'
  153. assert len(vm_list_info) == 1
  154. # name class=... state=... other=...
  155. # NOTE: when querying dom0, we get whole list
  156. vm_state = vm_list_info[0].strip().partition('state=')[2].split(' ')[0]
  157. return vm_state
  158. def is_halted(self):
  159. ''' Check whether this domain's state is 'Halted'
  160. :returns: :py:obj:`True` if this domain is halted, \
  161. :py:obj:`False` otherwise.
  162. :rtype: bool
  163. '''
  164. return self.get_power_state() == 'Halted'
  165. def is_paused(self):
  166. '''Check whether this domain is paused.
  167. :returns: :py:obj:`True` if this domain is paused, \
  168. :py:obj:`False` otherwise.
  169. :rtype: bool
  170. '''
  171. return self.get_power_state() == 'Paused'
  172. def is_running(self):
  173. '''Check whether this domain is running.
  174. :returns: :py:obj:`True` if this domain is started, \
  175. :py:obj:`False` otherwise.
  176. :rtype: bool
  177. '''
  178. return self.get_power_state() not in ('Halted', 'NA')
  179. def is_networked(self):
  180. '''Check whether this VM can reach network (firewall notwithstanding).
  181. :returns: :py:obj:`True` if is machine can reach network, \
  182. :py:obj:`False` otherwise.
  183. :rtype: bool
  184. '''
  185. if self.provides_network:
  186. return True
  187. return self.netvm is not None
  188. @property
  189. def volumes(self):
  190. '''VM disk volumes'''
  191. if self._volumes is None:
  192. volumes_list = self.qubesd_call(
  193. self._method_dest, 'admin.vm.volume.List')
  194. self._volumes = {}
  195. for volname in volumes_list.decode('ascii').splitlines():
  196. if not volname:
  197. continue
  198. self._volumes[volname] = qubesadmin.storage.Volume(self.app,
  199. vm=self.name, vm_name=volname)
  200. return self._volumes
  201. def get_disk_utilization(self):
  202. '''Get total disk usage of the VM'''
  203. return sum(vol.usage for vol in self.volumes.values())
  204. def run_service(self, service, **kwargs):
  205. '''Run service on this VM
  206. :param str service: service name
  207. :rtype: subprocess.Popen
  208. '''
  209. return self.app.run_service(self._method_dest, service, **kwargs)
  210. def run_service_for_stdio(self, service, input=None, **kwargs):
  211. '''Run a service, pass an optional input and return (stdout, stderr).
  212. Raises an exception if return code != 0.
  213. *args* and *kwargs* are passed verbatim to :py:meth:`run_service`.
  214. .. warning::
  215. There are some combinations if stdio-related *kwargs*, which are
  216. not filtered for problems originating between the keyboard and the
  217. chair.
  218. ''' # pylint: disable=redefined-builtin
  219. p = self.run_service(service, **kwargs)
  220. # this one is actually a tuple, but there is no need to unpack it
  221. stdouterr = p.communicate(input=input)
  222. if p.returncode:
  223. exc = subprocess.CalledProcessError(p.returncode, service)
  224. # Python < 3.5 didn't have those
  225. exc.output, exc.stderr = stdouterr
  226. raise exc
  227. return stdouterr
  228. def prepare_input_for_vmshell(self, command, input=None):
  229. '''Prepare shell input for the given command and optional (real) input
  230. ''' # pylint: disable=redefined-builtin
  231. if input is None:
  232. input = b''
  233. close_shell_suffix = b'; exit\n'
  234. if self.features.check_with_template('os', 'Linux') == 'Windows':
  235. close_shell_suffix = b'& exit\n'
  236. return b''.join((command.rstrip('\n').encode('utf-8'),
  237. close_shell_suffix, input))
  238. def run(self, command, input=None, **kwargs):
  239. '''Run a shell command inside the domain using qubes.VMShell qrexec.
  240. ''' # pylint: disable=redefined-builtin
  241. try:
  242. return self.run_service_for_stdio('qubes.VMShell',
  243. input=self.prepare_input_for_vmshell(command, input), **kwargs)
  244. except subprocess.CalledProcessError as e:
  245. e.cmd = command
  246. raise e
  247. def run_with_args(self, *args, **kwargs):
  248. '''Run a single command inside the domain using qubes.VMShell qrexec.
  249. This method execute a single command, without interpreting any shell
  250. special characters.
  251. ''' # pylint: disable=redefined-builtin
  252. return self.run(' '.join(shlex.quote(arg) for arg in args), **kwargs)
  253. @property
  254. def appvms(self):
  255. ''' Returns a generator containing all domains based on the current
  256. TemplateVM.
  257. Do not check vm type of self, core (including its extentions) have
  258. ultimate control what can be a template of what.
  259. '''
  260. for vm in self.app.domains:
  261. try:
  262. if vm.template == self:
  263. yield vm
  264. except AttributeError:
  265. pass
  266. @property
  267. def connected_vms(self):
  268. ''' Return a generator containing all domains connected to the current
  269. NetVM.
  270. '''
  271. for vm in self.app.domains:
  272. try:
  273. if vm.netvm == self:
  274. yield vm
  275. except AttributeError:
  276. pass
  277. @property
  278. def klass(self):
  279. ''' Qube class '''
  280. # use cached value if available
  281. if self._klass is None:
  282. # pylint: disable=no-member
  283. self._klass = super(QubesVM, self).klass
  284. return self._klass
  285. class DispVMWrapper(QubesVM):
  286. '''Wrapper class for new DispVM, supporting only service call
  287. Note that when running in dom0, one need to manually kill the DispVM after
  288. service call ends.
  289. '''
  290. def run_service(self, service, **kwargs):
  291. if self.app.qubesd_connection_type == 'socket':
  292. # create dispvm at service call
  293. if self._method_dest.startswith('$dispvm'):
  294. if self._method_dest.startswith('$dispvm:'):
  295. method_dest = self._method_dest[len('$dispvm:'):]
  296. else:
  297. method_dest = 'dom0'
  298. dispvm = self.app.qubesd_call(method_dest,
  299. 'admin.vm.CreateDisposable')
  300. dispvm = dispvm.decode('ascii')
  301. self._method_dest = dispvm
  302. # Service call may wait for session start, give it more time
  303. # than default 5s
  304. kwargs['connect_timeout'] = self.qrexec_timeout
  305. return super(DispVMWrapper, self).run_service(service, **kwargs)
  306. def cleanup(self):
  307. '''Cleanup after DispVM usage'''
  308. # in 'remote' case nothing is needed, as DispVM is cleaned up
  309. # automatically
  310. if self.app.qubesd_connection_type == 'socket' and \
  311. not self._method_dest.startswith('$dispvm'):
  312. try:
  313. self.kill()
  314. except qubesadmin.exc.QubesVMNotRunningError:
  315. pass
  316. class DispVM(QubesVM):
  317. '''Disposable VM'''
  318. @classmethod
  319. def from_appvm(cls, app, appvm):
  320. '''Returns a wrapper for calling service in a new DispVM based on given
  321. AppVM. If *appvm* is none, use default DispVM template'''
  322. if appvm:
  323. method_dest = '$dispvm:' + str(appvm)
  324. else:
  325. method_dest = '$dispvm'
  326. wrapper = DispVMWrapper(app, method_dest)
  327. return wrapper