__init__.py 12 KB

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