01QubesHVm.py 18 KB

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