01QubesHVm.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  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. @property
  89. def type(self):
  90. return "HVM"
  91. def is_appvm(self):
  92. return True
  93. @classmethod
  94. def is_template_compatible(cls, template):
  95. if template and (not template.is_template() or template.type != "TemplateHVM"):
  96. return False
  97. return True
  98. def get_clone_attrs(self):
  99. attrs = super(QubesHVm, self).get_clone_attrs()
  100. attrs.remove('kernel')
  101. attrs.remove('uses_default_kernel')
  102. attrs.remove('kernelopts')
  103. attrs.remove('uses_default_kernelopts')
  104. attrs += [ 'timezone' ]
  105. attrs += [ 'qrexec_installed' ]
  106. attrs += [ 'guiagent_installed' ]
  107. return attrs
  108. @property
  109. def qrexec_installed(self):
  110. return self._qrexec_installed or \
  111. bool(self.template and self.template.qrexec_installed)
  112. @qrexec_installed.setter
  113. def qrexec_installed(self, value):
  114. if self.template and self.template.qrexec_installed and not value:
  115. print >>sys.stderr, "WARNING: When qrexec_installed set in template, it will be propagated to the VM"
  116. self._qrexec_installed = value
  117. @property
  118. def guiagent_installed(self):
  119. return self._guiagent_installed or \
  120. bool(self.template and self.template.guiagent_installed)
  121. @guiagent_installed.setter
  122. def guiagent_installed(self, value):
  123. if self.template and self.template.guiagent_installed and not value:
  124. print >>sys.stderr, "WARNING: When guiagent_installed set in template, it will be propagated to the VM"
  125. self._guiagent_installed = value
  126. @property
  127. def seamless_gui_mode(self):
  128. if not self.guiagent_installed:
  129. return False
  130. return self._seamless_gui_mode
  131. @seamless_gui_mode.setter
  132. def seamless_gui_mode(self, value):
  133. if self._seamless_gui_mode == value:
  134. return
  135. if not self.guiagent_installed and value:
  136. raise ValueError("Seamless GUI mode requires GUI agent installed")
  137. self._seamless_gui_mode = value
  138. if self.is_running():
  139. self.send_gui_mode()
  140. @property
  141. def drive(self):
  142. return self._drive
  143. @drive.setter
  144. def drive(self, value):
  145. if value is None:
  146. self._drive = None
  147. return
  148. # strip type for a moment
  149. drv_type = "cdrom"
  150. if value.startswith("hd:") or value.startswith("cdrom:"):
  151. (drv_type, unused, value) = value.partition(":")
  152. drv_type = drv_type.lower()
  153. # sanity check
  154. if drv_type not in ['hd', 'cdrom']:
  155. raise QubesException("Unsupported drive type: %s" % type)
  156. if value.count(":") == 0:
  157. value = "dom0:" + value
  158. if value.count(":/") == 0:
  159. # FIXME: when Windows backend will be supported, improve this
  160. raise QubesException("Drive path must be absolute")
  161. self._drive = drv_type + ":" + value
  162. def create_on_disk(self, verbose, source_template = None):
  163. if dry_run:
  164. return
  165. # create empty disk
  166. self.storage.private_img_size = defaults["hvm_private_img_size"]
  167. self.storage.root_img_size = defaults["hvm_disk_size"]
  168. self.storage.create_on_disk(verbose, source_template)
  169. if verbose:
  170. print >> sys.stderr, "--> Creating icon symlink: {0} -> {1}".format(self.icon_path, self.label.icon_path)
  171. try:
  172. if hasattr(os, "symlink"):
  173. os.symlink (self.label.icon_path, self.icon_path)
  174. else:
  175. shutil.copy(self.label.icon_path, self.icon_path)
  176. except Exception as e:
  177. print >> sys.stderr, "WARNING: Failed to set VM icon: %s" % str(e)
  178. # fire hooks
  179. for hook in self.hooks_create_on_disk:
  180. hook(self, verbose, source_template=source_template)
  181. def get_private_img_sz(self):
  182. if not os.path.exists(self.private_img):
  183. return 0
  184. return os.path.getsize(self.private_img)
  185. def resize_private_img(self, size):
  186. assert size >= self.get_private_img_sz(), "Cannot shrink private.img"
  187. if self.is_running():
  188. raise NotImplementedError("Online resize of HVM's private.img not implemented, shutdown the VM first")
  189. f_private = open (self.private_img, "a+b")
  190. f_private.truncate (size)
  191. f_private.close ()
  192. def resize_root_img(self, size):
  193. if self.template:
  194. raise QubesException("Cannot resize root.img of template-based VM"
  195. ". Resize the root.img of the template "
  196. "instead.")
  197. if self.is_running():
  198. raise QubesException("Cannot resize root.img of running HVM")
  199. if size < self.get_root_img_sz():
  200. raise QubesException(
  201. "For your own safety shringing of root.img is disabled. If "
  202. "you really know what you are doing, use 'truncate' manually.")
  203. f_root = open (self.root_img, "a+b")
  204. f_root.truncate (size)
  205. f_root.close ()
  206. def get_rootdev(self, source_template=None):
  207. if self.template:
  208. return "'script:snapshot:{template_root}:{volatile},xvda,w',".format(
  209. template_root=self.template.root_img,
  210. volatile=self.volatile_img)
  211. else:
  212. return "'script:file:{root_img},xvda,w',".format(root_img=self.root_img)
  213. def get_config_params(self):
  214. params = super(QubesHVm, self).get_config_params()
  215. self.storage.drive = self.drive
  216. params.update(self.storage.get_config_params())
  217. params['volatiledev'] = ''
  218. if self.timezone.lower() == 'localtime':
  219. params['time_basis'] = 'localtime'
  220. params['timeoffset'] = '0'
  221. elif self.timezone.isdigit():
  222. params['time_basis'] = 'UTC'
  223. params['timeoffset'] = self.timezone
  224. else:
  225. print >>sys.stderr, "WARNING: invalid 'timezone' value: %s" % self.timezone
  226. params['time_basis'] = 'UTC'
  227. params['timeoffset'] = '0'
  228. return params
  229. def verify_files(self):
  230. if dry_run:
  231. return
  232. self.storage.verify_files()
  233. if not os.path.exists (self.private_img):
  234. print >>sys.stderr, "WARNING: Creating empty VM private image file: {0}".\
  235. format(self.private_img)
  236. self.storage.create_on_disk_private_img(verbose=False)
  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. stubdom_xid_str = vmm.xs.read('', '/local/domain/%d/image/device-model-domid' % self.xid)
  294. if stubdom_xid_str is not None:
  295. return int(stubdom_xid_str)
  296. else:
  297. return -1
  298. def start(self, *args, **kwargs):
  299. if self.template and self.template.is_running():
  300. raise QubesException("Cannot start the HVM while its template is running")
  301. try:
  302. if 'mem_required' not in kwargs:
  303. # Reserve 32MB for stubdomain
  304. kwargs['mem_required'] = (self.memory + 32) * 1024 * 1024
  305. return super(QubesHVm, self).start(*args, **kwargs)
  306. except QubesException as e:
  307. if xc.physinfo()['virt_caps'].count('hvm') == 0:
  308. raise QubesException("Cannot start HVM without VT-x/AMD-v enabled")
  309. else:
  310. raise
  311. def start_stubdom_guid(self):
  312. cmdline = [system_path["qubes_guid_path"],
  313. "-d", str(self.stubdom_xid),
  314. "-t", str(self.xid),
  315. "-N", self.name,
  316. "-c", self.label.color,
  317. "-i", self.label.icon_path,
  318. "-l", str(self.label.index)]
  319. retcode = subprocess.call (cmdline)
  320. if (retcode != 0) :
  321. raise QubesException("Cannot start qubes-guid!")
  322. def start_guid(self, verbose = True, notify_function = None,
  323. before_qrexec=False, **kwargs):
  324. # If user force the guiagent, start_guid will mimic a standard QubesVM
  325. if not before_qrexec and self.guiagent_installed:
  326. super(QubesHVm, self).start_guid(verbose, notify_function, extra_guid_args=["-Q"], **kwargs)
  327. stubdom_guid_pidfile = '/var/run/qubes/guid-running.%d' % self.stubdom_xid
  328. if os.path.exists(stubdom_guid_pidfile) and not self.debug:
  329. try:
  330. stubdom_guid_pid = int(open(stubdom_guid_pidfile, 'r').read())
  331. os.kill(stubdom_guid_pid, signal.SIGTERM)
  332. except Exception as ex:
  333. print >> sys.stderr, "WARNING: Failed to kill stubdom gui daemon: %s" % str(ex)
  334. elif before_qrexec and (not self.guiagent_installed or self.debug):
  335. if verbose:
  336. print >> sys.stderr, "--> Starting Qubes GUId (full screen)..."
  337. self.start_stubdom_guid()
  338. def start_qrexec_daemon(self, **kwargs):
  339. if not self.qrexec_installed:
  340. if kwargs.get('verbose', False):
  341. print >> sys.stderr, "--> Starting the qrexec daemon..."
  342. xid = self.get_xid()
  343. qrexec_env = os.environ.copy()
  344. qrexec_env['QREXEC_STARTUP_NOWAIT'] = '1'
  345. retcode = subprocess.call ([system_path["qrexec_daemon_path"], str(xid), self.name, self.default_user], env=qrexec_env)
  346. if (retcode != 0) :
  347. self.force_shutdown(xid=xid)
  348. raise OSError ("ERROR: Cannot execute qrexec-daemon!")
  349. else:
  350. super(QubesHVm, self).start_qrexec_daemon(**kwargs)
  351. if self._start_guid_first:
  352. if kwargs.get('verbose'):
  353. print >> sys.stderr, "--> Waiting for user '%s' login..." % self.default_user
  354. self.wait_for_session(notify_function=kwargs.get('notify_function', None))
  355. self.send_gui_mode()
  356. def send_gui_mode(self):
  357. if self.seamless_gui_mode:
  358. service_input = "SEAMLESS"
  359. else:
  360. service_input = "FULLSCREEN"
  361. self.run_service("qubes.SetGuiMode", input=service_input)
  362. def create_xenstore_entries(self, xid = None):
  363. if dry_run:
  364. return
  365. super(QubesHVm, self).create_xenstore_entries(xid)
  366. if xid is None:
  367. xid = self.xid
  368. domain_path = xs.get_domain_path(xid)
  369. # Prepare xenstore directory for tools advertise
  370. xs.write('',
  371. "{0}/qubes-tools".format(domain_path),
  372. '')
  373. # Allow VM writes there
  374. xs.set_permissions('', '{0}/qubes-tools'.format(domain_path),
  375. [{ 'dom': xid }])
  376. def _cleanup_zombie_domains(self):
  377. super(QubesHVm, self)._cleanup_zombie_domains()
  378. if not self.is_running():
  379. xc_stubdom = self.get_xc_dominfo(name=self.name+'-dm')
  380. if xc_stubdom is not None:
  381. if xc_stubdom['paused'] == 1:
  382. subprocess.call(['xl', 'destroy', str(xc_stubdom['domid'])])
  383. if xc_stubdom['dying'] == 1:
  384. # GUID still running?
  385. guid_pidfile = \
  386. '/var/run/qubes/guid-running.%d' % xc_stubdom['domid']
  387. if os.path.exists(guid_pidfile):
  388. guid_pid = open(guid_pidfile).read().strip()
  389. os.kill(int(guid_pid), 15)
  390. def suspend(self):
  391. if dry_run:
  392. return
  393. if not self.is_running() and not self.is_paused():
  394. raise QubesException ("VM not running!")
  395. self.pause()
  396. def is_guid_running(self):
  397. # If user force the guiagent, is_guid_running will mimic a standard QubesVM
  398. if self.guiagent_installed:
  399. return super(QubesHVm, self).is_guid_running()
  400. else:
  401. xid = self.stubdom_xid
  402. if xid < 0:
  403. return False
  404. if not os.path.exists('/var/run/qubes/guid-running.%d' % xid):
  405. return False
  406. return True
  407. def is_fully_usable(self):
  408. # Running gui-daemon implies also VM running
  409. if not self.is_guid_running():
  410. return False
  411. if self.qrexec_installed and not self.is_qrexec_running():
  412. return False
  413. return True
  414. register_qubes_vm_class(QubesHVm)