01QubesHVm.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. #!/usr/bin/python2
  2. # -*- coding: utf-8 -*-
  3. #
  4. # The Qubes OS Project, http://www.qubes-os.org
  5. #
  6. # Copyright (C) 2010 Joanna Rutkowska <joanna@invisiblethingslab.com>
  7. # Copyright (C) 2013 Marek Marczykowski <marmarek@invisiblethingslab.com>
  8. #
  9. # This program is free software; you can redistribute it and/or
  10. # modify it under the terms of the GNU General Public License
  11. # as published by the Free Software Foundation; either version 2
  12. # of the License, or (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License
  20. # along with this program; if not, write to the Free Software
  21. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22. #
  23. #
  24. import os
  25. import os.path
  26. import signal
  27. import subprocess
  28. import stat
  29. import sys
  30. import re
  31. import shutil
  32. import stat
  33. from xml.etree import ElementTree
  34. from qubes.qubes import QubesVm,register_qubes_vm_class,vmm,dry_run
  35. from qubes.qubes import system_path,defaults
  36. from qubes.qubes import QubesException
  37. system_path["config_template_hvm"] = '/usr/share/qubes/vm-template-hvm.xml'
  38. defaults["hvm_disk_size"] = 20*1024*1024*1024
  39. defaults["hvm_private_img_size"] = 2*1024*1024*1024
  40. defaults["hvm_memory"] = 512
  41. class QubesHVm(QubesVm):
  42. """
  43. A class that represents an HVM. A child of QubesVm.
  44. """
  45. # FIXME: logically should inherit after QubesAppVm, but none of its methods
  46. # are useful for HVM
  47. def get_attrs_config(self):
  48. attrs = super(QubesHVm, self).get_attrs_config()
  49. attrs.pop('kernel')
  50. attrs.pop('kernels_dir')
  51. attrs.pop('kernelopts')
  52. attrs.pop('uses_default_kernel')
  53. attrs.pop('uses_default_kernelopts')
  54. attrs['dir_path']['func'] = lambda value: value if value is not None \
  55. else os.path.join(system_path["qubes_appvms_dir"], self.name)
  56. attrs['config_file_template']['func'] = \
  57. lambda x: system_path["config_template_hvm"]
  58. attrs['drive'] = { 'attr': '_drive',
  59. 'save': lambda: str(self.drive) }
  60. # Remove this two lines when HVM will get qmemman support
  61. attrs['maxmem'].pop('save')
  62. attrs['maxmem']['func'] = lambda x: self.memory
  63. attrs['timezone'] = { 'default': 'localtime',
  64. 'save': lambda: str(self.timezone) }
  65. attrs['qrexec_installed'] = { 'default': False,
  66. 'attr': '_qrexec_installed',
  67. 'save': lambda: str(self._qrexec_installed) }
  68. attrs['guiagent_installed'] = { 'default' : False,
  69. 'attr': '_guiagent_installed',
  70. 'save': lambda: str(self._guiagent_installed) }
  71. attrs['seamless_gui_mode'] = { 'default': False,
  72. 'attr': '_seamless_gui_mode',
  73. 'save': lambda: str(self._seamless_gui_mode) }
  74. attrs['services']['default'] = "{'meminfo-writer': False}"
  75. attrs['memory']['default'] = defaults["hvm_memory"]
  76. return attrs
  77. def __init__(self, **kwargs):
  78. super(QubesHVm, self).__init__(**kwargs)
  79. # Default for meminfo-writer have changed to (correct) False in the
  80. # same version as introduction of guiagent_installed, so for older VMs
  81. # with wrong setting, change is based on 'guiagent_installed' presence
  82. if "guiagent_installed" not in kwargs and \
  83. (not 'xml_element' in kwargs or kwargs['xml_element'].get('guiagent_installed') is None):
  84. self.services['meminfo-writer'] = False
  85. self.storage.volatile_img = None
  86. @property
  87. def type(self):
  88. return "HVM"
  89. def is_appvm(self):
  90. return True
  91. @classmethod
  92. def is_template_compatible(cls, template):
  93. if template and (not template.is_template() or template.type != "TemplateHVM"):
  94. return False
  95. return True
  96. def get_clone_attrs(self):
  97. attrs = super(QubesHVm, self).get_clone_attrs()
  98. attrs.remove('kernel')
  99. attrs.remove('uses_default_kernel')
  100. attrs.remove('kernelopts')
  101. attrs.remove('uses_default_kernelopts')
  102. attrs += [ 'timezone' ]
  103. attrs += [ 'qrexec_installed' ]
  104. attrs += [ 'guiagent_installed' ]
  105. return attrs
  106. @property
  107. def qrexec_installed(self):
  108. return self._qrexec_installed or \
  109. bool(self.template and self.template.qrexec_installed)
  110. @qrexec_installed.setter
  111. def qrexec_installed(self, value):
  112. if self.template and self.template.qrexec_installed and not value:
  113. print >>sys.stderr, "WARNING: When qrexec_installed set in template, it will be propagated to the VM"
  114. self._qrexec_installed = value
  115. @property
  116. def guiagent_installed(self):
  117. return self._guiagent_installed or \
  118. bool(self.template and self.template.guiagent_installed)
  119. @guiagent_installed.setter
  120. def guiagent_installed(self, value):
  121. if self.template and self.template.guiagent_installed and not value:
  122. print >>sys.stderr, "WARNING: When guiagent_installed set in template, it will be propagated to the VM"
  123. self._guiagent_installed = value
  124. @property
  125. def seamless_gui_mode(self):
  126. if not self.guiagent_installed:
  127. return False
  128. return self._seamless_gui_mode
  129. @seamless_gui_mode.setter
  130. def seamless_gui_mode(self, value):
  131. if self._seamless_gui_mode == value:
  132. return
  133. if not self.guiagent_installed and value:
  134. raise ValueError("Seamless GUI mode requires GUI agent installed")
  135. self._seamless_gui_mode = value
  136. if self.is_running():
  137. self.send_gui_mode()
  138. @property
  139. def drive(self):
  140. return self._drive
  141. @drive.setter
  142. def drive(self, value):
  143. if value is None:
  144. self._drive = None
  145. return
  146. # strip type for a moment
  147. drv_type = "cdrom"
  148. if value.startswith("hd:") or value.startswith("cdrom:"):
  149. (drv_type, unused, value) = value.partition(":")
  150. drv_type = drv_type.lower()
  151. # sanity check
  152. if drv_type not in ['hd', 'cdrom']:
  153. raise QubesException("Unsupported drive type: %s" % type)
  154. if value.count(":") == 0:
  155. value = "dom0:" + value
  156. if value.count(":/") == 0:
  157. # FIXME: when Windows backend will be supported, improve this
  158. raise QubesException("Drive path must be absolute")
  159. self._drive = drv_type + ":" + value
  160. def create_on_disk(self, verbose, source_template = None):
  161. self.log.debug('create_on_disk(source_template={!r})'.format(
  162. source_template))
  163. if dry_run:
  164. return
  165. if source_template is None:
  166. source_template = self.template
  167. # create empty disk
  168. self.storage.private_img_size = defaults["hvm_private_img_size"]
  169. self.storage.root_img_size = defaults["hvm_disk_size"]
  170. self.storage.create_on_disk(verbose, source_template)
  171. if verbose:
  172. print >> sys.stderr, "--> Creating icon symlink: {0} -> {1}".format(self.icon_path, self.label.icon_path)
  173. try:
  174. if hasattr(os, "symlink"):
  175. os.symlink (self.label.icon_path, self.icon_path)
  176. else:
  177. shutil.copy(self.label.icon_path, self.icon_path)
  178. except Exception as e:
  179. print >> sys.stderr, "WARNING: Failed to set VM icon: %s" % str(e)
  180. # Make sure that we have UUID allocated
  181. self._update_libvirt_domain()
  182. # fire hooks
  183. for hook in self.hooks_create_on_disk:
  184. hook(self, verbose, source_template=source_template)
  185. def get_private_img_sz(self):
  186. if not os.path.exists(self.private_img):
  187. return 0
  188. return os.path.getsize(self.private_img)
  189. def resize_private_img(self, size):
  190. assert size >= self.get_private_img_sz(), "Cannot shrink private.img"
  191. if self.is_running():
  192. raise NotImplementedError("Online resize of HVM's private.img not implemented, shutdown the VM first")
  193. f_private = open (self.private_img, "a+b")
  194. f_private.truncate (size)
  195. f_private.close ()
  196. def resize_root_img(self, size):
  197. if self.template:
  198. raise QubesException("Cannot resize root.img of template-based VM"
  199. ". Resize the root.img of the template "
  200. "instead.")
  201. if self.is_running():
  202. raise QubesException("Cannot resize root.img of running HVM")
  203. if size < self.get_root_img_sz():
  204. raise QubesException(
  205. "For your own safety shringing of root.img is disabled. If "
  206. "you really know what you are doing, use 'truncate' manually.")
  207. f_root = open (self.root_img, "a+b")
  208. f_root.truncate (size)
  209. f_root.close ()
  210. def get_rootdev(self, source_template=None):
  211. if self.template:
  212. return "'script:snapshot:{template_root}:{volatile},xvda,w',".format(
  213. template_root=self.template.root_img,
  214. volatile=self.volatile_img)
  215. else:
  216. return "'script:file:{root_img},xvda,w',".format(root_img=self.root_img)
  217. def get_config_params(self):
  218. params = super(QubesHVm, self).get_config_params()
  219. self.storage.drive = self.drive
  220. params.update(self.storage.get_config_params())
  221. params['volatiledev'] = ''
  222. if self.timezone.lower() == 'localtime':
  223. params['time_basis'] = 'localtime'
  224. params['timeoffset'] = '0'
  225. elif self.timezone.isdigit():
  226. params['time_basis'] = 'UTC'
  227. params['timeoffset'] = self.timezone
  228. else:
  229. print >>sys.stderr, "WARNING: invalid 'timezone' value: %s" % self.timezone
  230. params['time_basis'] = 'UTC'
  231. params['timeoffset'] = '0'
  232. return params
  233. def verify_files(self):
  234. if dry_run:
  235. return
  236. self.storage.verify_files()
  237. # fire hooks
  238. for hook in self.hooks_verify_files:
  239. hook(self)
  240. return True
  241. def reset_volatile_storage(self, **kwargs):
  242. assert not self.is_running(), "Attempt to clean volatile image of running VM!"
  243. source_template = kwargs.get("source_template", self.template)
  244. if source_template is None:
  245. # Nothing to do on non-template based VM
  246. return
  247. if os.path.exists (self.volatile_img):
  248. if self.debug:
  249. if os.path.getmtime(self.template.root_img) > os.path.getmtime(self.volatile_img):
  250. if kwargs.get("verbose", False):
  251. print >>sys.stderr, "--> WARNING: template have changed, resetting root.img"
  252. else:
  253. if kwargs.get("verbose", False):
  254. print >>sys.stderr, "--> Debug mode: not resetting root.img"
  255. print >>sys.stderr, "--> Debug mode: if you want to force root.img reset, either update template VM, or remove volatile.img file"
  256. return
  257. os.remove (self.volatile_img)
  258. f_volatile = open (self.volatile_img, "w")
  259. f_root = open (self.template.root_img, "r")
  260. f_root.seek(0, os.SEEK_END)
  261. f_volatile.truncate (f_root.tell()) # make empty sparse file of the same size as root.img
  262. f_volatile.close ()
  263. f_root.close()
  264. @property
  265. def vif(self):
  266. if self.xid < 0:
  267. return None
  268. if self.netvm is None:
  269. return None
  270. return "vif{0}.+".format(self.stubdom_xid)
  271. @property
  272. def mac(self):
  273. if self._mac is not None:
  274. return self._mac
  275. elif self.template is not None:
  276. return self.template.mac
  277. else:
  278. return "00:16:3E:5E:6C:{qid:02X}".format(qid=self.qid)
  279. @mac.setter
  280. def mac(self, value):
  281. self._mac = value
  282. def run(self, command, **kwargs):
  283. if self.qrexec_installed:
  284. if 'gui' in kwargs and kwargs['gui']==False:
  285. command = "nogui:" + command
  286. return super(QubesHVm, self).run(command, **kwargs)
  287. else:
  288. raise QubesException("Needs qrexec agent installed in VM to use this function. See also qvm-prefs.")
  289. @property
  290. def stubdom_xid(self):
  291. if self.xid < 0:
  292. return -1
  293. if vmm.xs is None:
  294. return -1
  295. stubdom_xid_str = vmm.xs.read('', '/local/domain/%d/image/device-model-domid' % self.xid)
  296. if stubdom_xid_str is not None:
  297. return int(stubdom_xid_str)
  298. else:
  299. return -1
  300. def start(self, *args, **kwargs):
  301. if self.template and self.template.is_running():
  302. raise QubesException("Cannot start the HVM while its template is running")
  303. try:
  304. if 'mem_required' not in kwargs:
  305. # Reserve 32MB for stubdomain
  306. kwargs['mem_required'] = (self.memory + 32) * 1024 * 1024
  307. return super(QubesHVm, self).start(*args, **kwargs)
  308. except QubesException as e:
  309. capabilities = vmm.libvirt_conn.getCapabilities()
  310. tree = ElementTree.fromstring(capabilities)
  311. os_types = tree.findall('./guest/os_type')
  312. if 'hvm' not in map(lambda x: x.text, os_types):
  313. raise QubesException("Cannot start HVM without VT-x/AMD-v enabled")
  314. else:
  315. raise
  316. def start_stubdom_guid(self, verbose=True):
  317. guid_cmd = [system_path["qubes_guid_path"],
  318. "-d", str(self.stubdom_xid),
  319. "-t", str(self.xid),
  320. "-N", self.name,
  321. "-c", self.label.color,
  322. "-i", self.label.icon_path,
  323. "-l", str(self.label.index)]
  324. if self.debug:
  325. guid_cmd += ['-v', '-v']
  326. elif not verbose:
  327. guid_cmd += ['-q']
  328. retcode = subprocess.call (guid_cmd)
  329. if (retcode != 0) :
  330. raise QubesException("Cannot start qubes-guid!")
  331. def start_guid(self, verbose = True, notify_function = None,
  332. before_qrexec=False, **kwargs):
  333. # If user force the guiagent, start_guid will mimic a standard QubesVM
  334. if not before_qrexec and self.guiagent_installed:
  335. kwargs['extra_guid_args'] = kwargs.get('extra_guid_args', []) + \
  336. ['-Q']
  337. super(QubesHVm, self).start_guid(verbose, notify_function, **kwargs)
  338. stubdom_guid_pidfile = '/var/run/qubes/guid-running.%d' % self.stubdom_xid
  339. if os.path.exists(stubdom_guid_pidfile) and not self.debug:
  340. try:
  341. stubdom_guid_pid = int(open(stubdom_guid_pidfile, 'r').read())
  342. os.kill(stubdom_guid_pid, signal.SIGTERM)
  343. except Exception as ex:
  344. print >> sys.stderr, "WARNING: Failed to kill stubdom gui daemon: %s" % str(ex)
  345. elif before_qrexec and (not self.guiagent_installed or self.debug):
  346. if verbose:
  347. print >> sys.stderr, "--> Starting Qubes GUId (full screen)..."
  348. self.start_stubdom_guid(verbose=verbose)
  349. def start_qrexec_daemon(self, **kwargs):
  350. if not self.qrexec_installed:
  351. if kwargs.get('verbose', False):
  352. print >> sys.stderr, "--> Starting the qrexec daemon..."
  353. xid = self.get_xid()
  354. qrexec_env = os.environ.copy()
  355. qrexec_env['QREXEC_STARTUP_NOWAIT'] = '1'
  356. retcode = subprocess.call ([system_path["qrexec_daemon_path"], str(xid), self.name, self.default_user], env=qrexec_env)
  357. if (retcode != 0) :
  358. self.force_shutdown(xid=xid)
  359. raise OSError ("ERROR: Cannot execute qrexec-daemon!")
  360. else:
  361. super(QubesHVm, self).start_qrexec_daemon(**kwargs)
  362. if self.guiagent_installed:
  363. if kwargs.get('verbose'):
  364. print >> sys.stderr, "--> Waiting for user '%s' login..." % self.default_user
  365. self.wait_for_session(notify_function=kwargs.get('notify_function', None))
  366. self.send_gui_mode()
  367. def send_gui_mode(self):
  368. if self.seamless_gui_mode:
  369. service_input = "SEAMLESS"
  370. else:
  371. service_input = "FULLSCREEN"
  372. self.run_service("qubes.SetGuiMode", input=service_input)
  373. def _cleanup_zombie_domains(self):
  374. super(QubesHVm, self)._cleanup_zombie_domains()
  375. if not self.is_running():
  376. xc_stubdom = self.get_xc_dominfo(name=self.name+'-dm')
  377. if xc_stubdom is not None:
  378. if xc_stubdom['paused'] == 1:
  379. subprocess.call(['xl', 'destroy', str(xc_stubdom['domid'])])
  380. if xc_stubdom['dying'] == 1:
  381. # GUID still running?
  382. guid_pidfile = \
  383. '/var/run/qubes/guid-running.%d' % xc_stubdom['domid']
  384. if os.path.exists(guid_pidfile):
  385. guid_pid = open(guid_pidfile).read().strip()
  386. os.kill(int(guid_pid), 15)
  387. def suspend(self):
  388. if dry_run:
  389. return
  390. if not self.is_running() and not self.is_paused():
  391. raise QubesException ("VM not running!")
  392. self.pause()
  393. def is_guid_running(self):
  394. # If user force the guiagent, is_guid_running will mimic a standard QubesVM
  395. if self.guiagent_installed:
  396. return super(QubesHVm, self).is_guid_running()
  397. else:
  398. xid = self.stubdom_xid
  399. if xid < 0:
  400. return False
  401. if not os.path.exists('/var/run/qubes/guid-running.%d' % xid):
  402. return False
  403. return True
  404. def is_fully_usable(self):
  405. # Running gui-daemon implies also VM running
  406. if not self.is_guid_running():
  407. return False
  408. if self.qrexec_installed and not self.is_qrexec_running():
  409. return False
  410. return True
  411. register_qubes_vm_class(QubesHVm)