01QubesHVm.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  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 qubes.qubes import QubesVm,register_qubes_vm_class,vmm,dry_run
  34. from qubes.qubes import system_path,defaults
  35. from qubes.qubes import QubesException
  36. system_path["config_template_hvm"] = '/usr/share/qubes/vm-template-hvm.xml'
  37. defaults["hvm_disk_size"] = 20*1024*1024*1024
  38. defaults["hvm_private_img_size"] = 2*1024*1024*1024
  39. defaults["hvm_memory"] = 512
  40. class QubesHVm(QubesVm):
  41. """
  42. A class that represents an HVM. A child of QubesVm.
  43. """
  44. # FIXME: logically should inherit after QubesAppVm, but none of its methods
  45. # are useful for HVM
  46. def get_attrs_config(self):
  47. attrs = super(QubesHVm, self).get_attrs_config()
  48. attrs.pop('kernel')
  49. attrs.pop('kernels_dir')
  50. attrs.pop('kernelopts')
  51. attrs.pop('uses_default_kernel')
  52. attrs.pop('uses_default_kernelopts')
  53. attrs['dir_path']['func'] = lambda value: value if value is not None \
  54. else os.path.join(system_path["qubes_appvms_dir"], self.name)
  55. attrs['config_file_template']['func'] = \
  56. lambda x: system_path["config_template_hvm"]
  57. attrs['drive'] = { 'attr': '_drive',
  58. 'save': lambda: str(self.drive) }
  59. # Remove this two lines when HVM will get qmemman support
  60. attrs['maxmem'].pop('save')
  61. attrs['maxmem']['func'] = lambda x: self.memory
  62. attrs['timezone'] = { 'default': 'localtime',
  63. 'save': lambda: str(self.timezone) }
  64. attrs['qrexec_installed'] = { 'default': False,
  65. 'attr': '_qrexec_installed',
  66. 'save': lambda: str(self._qrexec_installed) }
  67. attrs['guiagent_installed'] = { 'default' : False,
  68. 'attr': '_guiagent_installed',
  69. 'save': lambda: str(self._guiagent_installed) }
  70. attrs['seamless_gui_mode'] = { 'default': False,
  71. 'attr': '_seamless_gui_mode',
  72. 'save': lambda: str(self._seamless_gui_mode) }
  73. attrs['_start_guid_first']['func'] = lambda x: True
  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. # Disable qemu GUID if the user installed qubes gui agent
  86. if self.guiagent_installed:
  87. self._start_guid_first = False
  88. self.storage.volatile_img = None
  89. @property
  90. def type(self):
  91. return "HVM"
  92. def is_appvm(self):
  93. return True
  94. @classmethod
  95. def is_template_compatible(cls, template):
  96. if template and (not template.is_template() or template.type != "TemplateHVM"):
  97. return False
  98. return True
  99. def get_clone_attrs(self):
  100. attrs = super(QubesHVm, self).get_clone_attrs()
  101. attrs.remove('kernel')
  102. attrs.remove('uses_default_kernel')
  103. attrs.remove('kernelopts')
  104. attrs.remove('uses_default_kernelopts')
  105. attrs += [ 'timezone' ]
  106. attrs += [ 'qrexec_installed' ]
  107. attrs += [ 'guiagent_installed' ]
  108. return attrs
  109. @property
  110. def qrexec_installed(self):
  111. return self._qrexec_installed or \
  112. bool(self.template and self.template.qrexec_installed)
  113. @qrexec_installed.setter
  114. def qrexec_installed(self, value):
  115. if self.template and self.template.qrexec_installed and not value:
  116. print >>sys.stderr, "WARNING: When qrexec_installed set in template, it will be propagated to the VM"
  117. self._qrexec_installed = value
  118. @property
  119. def guiagent_installed(self):
  120. return self._guiagent_installed or \
  121. bool(self.template and self.template.guiagent_installed)
  122. @guiagent_installed.setter
  123. def guiagent_installed(self, value):
  124. if self.template and self.template.guiagent_installed and not value:
  125. print >>sys.stderr, "WARNING: When guiagent_installed set in template, it will be propagated to the VM"
  126. self._guiagent_installed = value
  127. @property
  128. def seamless_gui_mode(self):
  129. if not self.guiagent_installed:
  130. return False
  131. return self._seamless_gui_mode
  132. @seamless_gui_mode.setter
  133. def seamless_gui_mode(self, value):
  134. if self._seamless_gui_mode == value:
  135. return
  136. if not self.guiagent_installed and value:
  137. raise ValueError("Seamless GUI mode requires GUI agent installed")
  138. self._seamless_gui_mode = value
  139. if self.is_running():
  140. self.send_gui_mode()
  141. @property
  142. def drive(self):
  143. return self._drive
  144. @drive.setter
  145. def drive(self, value):
  146. if value is None:
  147. self._drive = None
  148. return
  149. # strip type for a moment
  150. drv_type = "cdrom"
  151. if value.startswith("hd:") or value.startswith("cdrom:"):
  152. (drv_type, unused, value) = value.partition(":")
  153. drv_type = drv_type.lower()
  154. # sanity check
  155. if drv_type not in ['hd', 'cdrom']:
  156. raise QubesException("Unsupported drive type: %s" % type)
  157. if value.count(":") == 0:
  158. value = "dom0:" + value
  159. if value.count(":/") == 0:
  160. # FIXME: when Windows backend will be supported, improve this
  161. raise QubesException("Drive path must be absolute")
  162. self._drive = drv_type + ":" + value
  163. def create_on_disk(self, verbose, source_template = None):
  164. if dry_run:
  165. return
  166. # create empty disk
  167. self.storage.private_img_size = defaults["hvm_private_img_size"]
  168. self.storage.root_img_size = defaults["hvm_disk_size"]
  169. self.storage.create_on_disk(verbose, source_template)
  170. if verbose:
  171. print >> sys.stderr, "--> Creating icon symlink: {0} -> {1}".format(self.icon_path, self.label.icon_path)
  172. try:
  173. if hasattr(os, "symlink"):
  174. os.symlink (self.label.icon_path, self.icon_path)
  175. else:
  176. shutil.copy(self.label.icon_path, self.icon_path)
  177. except Exception as e:
  178. print >> sys.stderr, "WARNING: Failed to set VM icon: %s" % str(e)
  179. # fire hooks
  180. for hook in self.hooks_create_on_disk:
  181. hook(self, verbose, source_template=source_template)
  182. def get_private_img_sz(self):
  183. if not os.path.exists(self.private_img):
  184. return 0
  185. return os.path.getsize(self.private_img)
  186. def resize_private_img(self, size):
  187. assert size >= self.get_private_img_sz(), "Cannot shrink private.img"
  188. if self.is_running():
  189. raise NotImplementedError("Online resize of HVM's private.img not implemented, shutdown the VM first")
  190. f_private = open (self.private_img, "a+b")
  191. f_private.truncate (size)
  192. f_private.close ()
  193. def resize_root_img(self, size):
  194. if self.template:
  195. raise QubesException("Cannot resize root.img of template-based VM"
  196. ". Resize the root.img of the template "
  197. "instead.")
  198. if self.is_running():
  199. raise QubesException("Cannot resize root.img of running HVM")
  200. if size < self.get_root_img_sz():
  201. raise QubesException(
  202. "For your own safety shringing of root.img is disabled. If "
  203. "you really know what you are doing, use 'truncate' manually.")
  204. f_root = open (self.root_img, "a+b")
  205. f_root.truncate (size)
  206. f_root.close ()
  207. def get_rootdev(self, source_template=None):
  208. if self.template:
  209. return "'script:snapshot:{template_root}:{volatile},xvda,w',".format(
  210. template_root=self.template.root_img,
  211. volatile=self.volatile_img)
  212. else:
  213. return "'script:file:{root_img},xvda,w',".format(root_img=self.root_img)
  214. def get_config_params(self):
  215. params = super(QubesHVm, self).get_config_params()
  216. self.storage.drive = self.drive
  217. params.update(self.storage.get_config_params())
  218. params['volatiledev'] = ''
  219. if self.timezone.lower() == 'localtime':
  220. params['time_basis'] = 'localtime'
  221. params['timeoffset'] = '0'
  222. elif self.timezone.isdigit():
  223. params['time_basis'] = 'UTC'
  224. params['timeoffset'] = self.timezone
  225. else:
  226. print >>sys.stderr, "WARNING: invalid 'timezone' value: %s" % self.timezone
  227. params['time_basis'] = 'UTC'
  228. params['timeoffset'] = '0'
  229. return params
  230. def verify_files(self):
  231. if dry_run:
  232. return
  233. self.storage.verify_files()
  234. # fire hooks
  235. for hook in self.hooks_verify_files:
  236. hook(self)
  237. return True
  238. def reset_volatile_storage(self, **kwargs):
  239. assert not self.is_running(), "Attempt to clean volatile image of running VM!"
  240. source_template = kwargs.get("source_template", self.template)
  241. if source_template is None:
  242. # Nothing to do on non-template based VM
  243. return
  244. if os.path.exists (self.volatile_img):
  245. if self.debug:
  246. if os.path.getmtime(self.template.root_img) > os.path.getmtime(self.volatile_img):
  247. if kwargs.get("verbose", False):
  248. print >>sys.stderr, "--> WARNING: template have changed, resetting root.img"
  249. else:
  250. if kwargs.get("verbose", False):
  251. print >>sys.stderr, "--> Debug mode: not resetting root.img"
  252. print >>sys.stderr, "--> Debug mode: if you want to force root.img reset, either update template VM, or remove volatile.img file"
  253. return
  254. os.remove (self.volatile_img)
  255. f_volatile = open (self.volatile_img, "w")
  256. f_root = open (self.template.root_img, "r")
  257. f_root.seek(0, os.SEEK_END)
  258. f_volatile.truncate (f_root.tell()) # make empty sparse file of the same size as root.img
  259. f_volatile.close ()
  260. f_root.close()
  261. @property
  262. def vif(self):
  263. if self.xid < 0:
  264. return None
  265. if self.netvm is None:
  266. return None
  267. return "vif{0}.+".format(self.stubdom_xid)
  268. @property
  269. def mac(self):
  270. if self._mac is not None:
  271. return self._mac
  272. elif self.template is not None:
  273. return self.template.mac
  274. else:
  275. return "00:16:3E:5E:6C:{qid:02X}".format(qid=self.qid)
  276. @mac.setter
  277. def mac(self, value):
  278. self._mac = value
  279. def run(self, command, **kwargs):
  280. if self.qrexec_installed:
  281. if 'gui' in kwargs and kwargs['gui']==False:
  282. command = "nogui:" + command
  283. return super(QubesHVm, self).run(command, **kwargs)
  284. else:
  285. raise QubesException("Needs qrexec agent installed in VM to use this function. See also qvm-prefs.")
  286. @property
  287. def stubdom_xid(self):
  288. if self.xid < 0:
  289. return -1
  290. if vmm.xs is None:
  291. return -1
  292. stubdom_xid_str = vmm.xs.read('', '/local/domain/%d/image/device-model-domid' % self.xid)
  293. if stubdom_xid_str is not None:
  294. return int(stubdom_xid_str)
  295. else:
  296. return -1
  297. def start(self, *args, **kwargs):
  298. if self.template and self.template.is_running():
  299. raise QubesException("Cannot start the HVM while its template is running")
  300. try:
  301. if 'mem_required' not in kwargs:
  302. # Reserve 32MB for stubdomain
  303. kwargs['mem_required'] = (self.memory + 32) * 1024 * 1024
  304. return super(QubesHVm, self).start(*args, **kwargs)
  305. except QubesException as e:
  306. if xc.physinfo()['virt_caps'].count('hvm') == 0:
  307. raise QubesException("Cannot start HVM without VT-x/AMD-v enabled")
  308. else:
  309. raise
  310. def start_stubdom_guid(self):
  311. cmdline = [system_path["qubes_guid_path"],
  312. "-d", str(self.stubdom_xid),
  313. "-t", str(self.xid),
  314. "-N", self.name,
  315. "-c", self.label.color,
  316. "-i", self.label.icon_path,
  317. "-l", str(self.label.index)]
  318. retcode = subprocess.call (cmdline)
  319. if (retcode != 0) :
  320. raise QubesException("Cannot start qubes-guid!")
  321. def start_guid(self, verbose = True, notify_function = None,
  322. before_qrexec=False, **kwargs):
  323. # If user force the guiagent, start_guid will mimic a standard QubesVM
  324. if not before_qrexec and self.guiagent_installed:
  325. super(QubesHVm, self).start_guid(verbose, notify_function, extra_guid_args=["-Q"], **kwargs)
  326. stubdom_guid_pidfile = '/var/run/qubes/guid-running.%d' % self.stubdom_xid
  327. if os.path.exists(stubdom_guid_pidfile) and not self.debug:
  328. try:
  329. stubdom_guid_pid = int(open(stubdom_guid_pidfile, 'r').read())
  330. os.kill(stubdom_guid_pid, signal.SIGTERM)
  331. except Exception as ex:
  332. print >> sys.stderr, "WARNING: Failed to kill stubdom gui daemon: %s" % str(ex)
  333. elif before_qrexec and (not self.guiagent_installed or self.debug):
  334. if verbose:
  335. print >> sys.stderr, "--> Starting Qubes GUId (full screen)..."
  336. self.start_stubdom_guid()
  337. def start_qrexec_daemon(self, **kwargs):
  338. if not self.qrexec_installed:
  339. if kwargs.get('verbose', False):
  340. print >> sys.stderr, "--> Starting the qrexec daemon..."
  341. xid = self.get_xid()
  342. qrexec_env = os.environ.copy()
  343. qrexec_env['QREXEC_STARTUP_NOWAIT'] = '1'
  344. retcode = subprocess.call ([system_path["qrexec_daemon_path"], str(xid), self.name, self.default_user], env=qrexec_env)
  345. if (retcode != 0) :
  346. self.force_shutdown(xid=xid)
  347. raise OSError ("ERROR: Cannot execute qrexec-daemon!")
  348. else:
  349. super(QubesHVm, self).start_qrexec_daemon(**kwargs)
  350. if self._start_guid_first:
  351. if kwargs.get('verbose'):
  352. print >> sys.stderr, "--> Waiting for user '%s' login..." % self.default_user
  353. self.wait_for_session(notify_function=kwargs.get('notify_function', None))
  354. self.send_gui_mode()
  355. def send_gui_mode(self):
  356. if self.seamless_gui_mode:
  357. service_input = "SEAMLESS"
  358. else:
  359. service_input = "FULLSCREEN"
  360. self.run_service("qubes.SetGuiMode", input=service_input)
  361. def create_xenstore_entries(self, xid = None):
  362. if dry_run:
  363. return
  364. super(QubesHVm, self).create_xenstore_entries(xid)
  365. if xid is None:
  366. xid = self.xid
  367. domain_path = xs.get_domain_path(xid)
  368. # Prepare xenstore directory for tools advertise
  369. xs.write('',
  370. "{0}/qubes-tools".format(domain_path),
  371. '')
  372. # Allow VM writes there
  373. xs.set_permissions('', '{0}/qubes-tools'.format(domain_path),
  374. [{ 'dom': xid }])
  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)