__init__.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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. 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. return self._get_current_state()['power_state']
  147. except qubesadmin.exc.QubesDaemonNoResponseError:
  148. return 'NA'
  149. def get_mem(self):
  150. '''Get current memory usage from VM.'''
  151. return int(self._get_current_state()['mem'])
  152. def _get_current_state(self):
  153. '''Call admin.vm.CurrentState, and return the result as a dict.'''
  154. state = {}
  155. response = self.qubesd_call(self._method_dest, 'admin.vm.CurrentState')
  156. for part in response.decode('ascii').split():
  157. name, value = part.split('=', 1)
  158. state[name] = value
  159. return state
  160. def is_halted(self):
  161. ''' Check whether this domain's state is 'Halted'
  162. :returns: :py:obj:`True` if this domain is halted, \
  163. :py:obj:`False` otherwise.
  164. :rtype: bool
  165. '''
  166. return self.get_power_state() == 'Halted'
  167. def is_paused(self):
  168. '''Check whether this domain is paused.
  169. :returns: :py:obj:`True` if this domain is paused, \
  170. :py:obj:`False` otherwise.
  171. :rtype: bool
  172. '''
  173. return self.get_power_state() == 'Paused'
  174. def is_running(self):
  175. '''Check whether this domain is running.
  176. :returns: :py:obj:`True` if this domain is started, \
  177. :py:obj:`False` otherwise.
  178. :rtype: bool
  179. '''
  180. return self.get_power_state() not in ('Halted', 'NA')
  181. def is_networked(self):
  182. '''Check whether this VM can reach network (firewall notwithstanding).
  183. :returns: :py:obj:`True` if is machine can reach network, \
  184. :py:obj:`False` otherwise.
  185. :rtype: bool
  186. '''
  187. if self.provides_network:
  188. return True
  189. return self.netvm is not None
  190. @property
  191. def volumes(self):
  192. '''VM disk volumes'''
  193. if self._volumes is None:
  194. volumes_list = self.qubesd_call(
  195. self._method_dest, 'admin.vm.volume.List')
  196. self._volumes = {}
  197. for volname in volumes_list.decode('ascii').splitlines():
  198. if not volname:
  199. continue
  200. self._volumes[volname] = qubesadmin.storage.Volume(self.app,
  201. vm=self.name, vm_name=volname)
  202. return self._volumes
  203. def get_disk_utilization(self):
  204. '''Get total disk usage of the VM'''
  205. return sum(vol.usage for vol in self.volumes.values())
  206. def run_service(self, service, **kwargs):
  207. '''Run service on this VM
  208. :param str service: service name
  209. :rtype: subprocess.Popen
  210. '''
  211. return self.app.run_service(self._method_dest, service, **kwargs)
  212. def run_service_for_stdio(self, service, input=None, **kwargs):
  213. '''Run a service, pass an optional input and return (stdout, stderr).
  214. Raises an exception if return code != 0.
  215. *args* and *kwargs* are passed verbatim to :py:meth:`run_service`.
  216. .. warning::
  217. There are some combinations if stdio-related *kwargs*, which are
  218. not filtered for problems originating between the keyboard and the
  219. chair.
  220. ''' # pylint: disable=redefined-builtin
  221. p = self.run_service(service, **kwargs)
  222. # this one is actually a tuple, but there is no need to unpack it
  223. stdouterr = p.communicate(input=input)
  224. if p.returncode:
  225. exc = subprocess.CalledProcessError(p.returncode, service)
  226. # Python < 3.5 didn't have those
  227. exc.output, exc.stderr = stdouterr
  228. raise exc
  229. return stdouterr
  230. def prepare_input_for_vmshell(self, command, input=None):
  231. '''Prepare shell input for the given command and optional (real) input
  232. ''' # pylint: disable=redefined-builtin
  233. if input is None:
  234. input = b''
  235. close_shell_suffix = b'; exit\n'
  236. if self.features.check_with_template('os', 'Linux') == 'Windows':
  237. close_shell_suffix = b'& exit\n'
  238. return b''.join((command.rstrip('\n').encode('utf-8'),
  239. close_shell_suffix, input))
  240. def run(self, command, input=None, **kwargs):
  241. '''Run a shell command inside the domain using qubes.VMShell qrexec.
  242. ''' # pylint: disable=redefined-builtin
  243. try:
  244. return self.run_service_for_stdio('qubes.VMShell',
  245. input=self.prepare_input_for_vmshell(command, input), **kwargs)
  246. except subprocess.CalledProcessError as e:
  247. e.cmd = command
  248. raise e
  249. def run_with_args(self, *args, **kwargs):
  250. '''Run a single command inside the domain. Use the qubes.VMExec qrexec,
  251. if available.
  252. This method execute a single command, without interpreting any shell
  253. special characters.
  254. ''' # pylint: disable=redefined-builtin
  255. if self.features.check_with_template('vmexec', False):
  256. try:
  257. return self.run_service_for_stdio(
  258. 'qubes.VMExec+' + qubesadmin.utils.encode_for_vmexec(args),
  259. **kwargs)
  260. except subprocess.CalledProcessError as e:
  261. e.cmd = str(args)
  262. raise e
  263. return self.run(' '.join(shlex.quote(arg) for arg in args), **kwargs)
  264. @property
  265. def appvms(self):
  266. ''' Returns a generator containing all domains based on the current
  267. TemplateVM.
  268. Do not check vm type of self, core (including its extentions) have
  269. ultimate control what can be a template of what.
  270. '''
  271. for vm in self.app.domains:
  272. try:
  273. if vm.template == self:
  274. yield vm
  275. except AttributeError:
  276. pass
  277. @property
  278. def connected_vms(self):
  279. ''' Return a generator containing all domains connected to the current
  280. NetVM.
  281. '''
  282. for vm in self.app.domains:
  283. try:
  284. if vm.netvm == self:
  285. yield vm
  286. except AttributeError:
  287. pass
  288. @property
  289. def klass(self):
  290. ''' Qube class '''
  291. # use cached value if available
  292. if self._klass is None:
  293. # pylint: disable=no-member
  294. self._klass = super(QubesVM, self).klass
  295. return self._klass
  296. class DispVMWrapper(QubesVM):
  297. '''Wrapper class for new DispVM, supporting only service call
  298. Note that when running in dom0, one need to manually kill the DispVM after
  299. service call ends.
  300. '''
  301. def run_service(self, service, **kwargs):
  302. if self.app.qubesd_connection_type == 'socket':
  303. # create dispvm at service call
  304. if self._method_dest.startswith('$dispvm'):
  305. if self._method_dest.startswith('$dispvm:'):
  306. method_dest = self._method_dest[len('$dispvm:'):]
  307. else:
  308. method_dest = 'dom0'
  309. dispvm = self.app.qubesd_call(method_dest,
  310. 'admin.vm.CreateDisposable')
  311. dispvm = dispvm.decode('ascii')
  312. self._method_dest = dispvm
  313. # Service call may wait for session start, give it more time
  314. # than default 5s
  315. kwargs['connect_timeout'] = self.qrexec_timeout
  316. return super(DispVMWrapper, self).run_service(service, **kwargs)
  317. def cleanup(self):
  318. '''Cleanup after DispVM usage'''
  319. # in 'remote' case nothing is needed, as DispVM is cleaned up
  320. # automatically
  321. if self.app.qubesd_connection_type == 'socket' and \
  322. not self._method_dest.startswith('$dispvm'):
  323. try:
  324. self.kill()
  325. except qubesadmin.exc.QubesVMNotRunningError:
  326. pass
  327. class DispVM(QubesVM):
  328. '''Disposable VM'''
  329. @classmethod
  330. def from_appvm(cls, app, appvm):
  331. '''Returns a wrapper for calling service in a new DispVM based on given
  332. AppVM. If *appvm* is none, use default DispVM template'''
  333. if appvm:
  334. method_dest = '$dispvm:' + str(appvm)
  335. else:
  336. method_dest = '$dispvm'
  337. wrapper = DispVMWrapper(app, method_dest)
  338. return wrapper