__init__.py 14 KB

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