xen.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. #!/usr/bin/python2 -O
  2. # vim: fileencoding=utf-8
  3. #
  4. # The Qubes OS Project, https://www.qubes-os.org/
  5. #
  6. # Copyright (C) 2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
  7. # Copyright (C) 2013-2015 Marek Marczykowski-Górecki
  8. # <marmarek@invisiblethingslab.com>
  9. # Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  10. #
  11. # This program is free software; you can redistribute it and/or modify
  12. # it under the terms of the GNU General Public License as published by
  13. # the Free Software Foundation; either version 2 of the License, or
  14. # (at your option) any later version.
  15. #
  16. # This program is distributed in the hope that it will be useful,
  17. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. # GNU General Public License for more details.
  20. #
  21. # You should have received a copy of the GNU General Public License along
  22. # with this program; if not, write to the Free Software Foundation, Inc.,
  23. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  24. #
  25. from __future__ import absolute_import
  26. import os
  27. import os.path
  28. import re
  29. import subprocess
  30. import lxml.etree
  31. import qubes
  32. import qubes.config
  33. import qubes.storage
  34. import qubes.vm.templatevm
  35. class XenStorage(qubes.storage.Storage):
  36. '''Class for VM storage of Xen VMs.
  37. '''
  38. root_dev = 'xvda'
  39. private_dev = 'xvdb'
  40. volatile_dev = 'xvdc'
  41. modules_dev = 'xvdd'
  42. def __init__(self, vm, vmdir, **kwargs):
  43. """ Instantiate the storage.
  44. Args:
  45. vm: a QubesVM
  46. vmdir: the root directory of the pool
  47. """
  48. assert vm is not None
  49. assert vmdir is not None
  50. super(XenStorage, self).__init__(vm, **kwargs)
  51. self.vmdir = vmdir
  52. @property
  53. def private_img(self):
  54. '''Path to the private image'''
  55. return self.abspath(qubes.config.vm_files['private_img'])
  56. @property
  57. def root_img(self):
  58. '''Path to the root image'''
  59. return self.vm.template.storage.root_img \
  60. if hasattr(self.vm, 'template') and self.vm.template \
  61. else self.abspath(qubes.config.vm_files['root_img'])
  62. @property
  63. def rootcow_img(self):
  64. '''Path to the root COW image'''
  65. if isinstance(self.vm, qubes.vm.templatevm.TemplateVM):
  66. return self.abspath(qubes.config.vm_files['rootcow_img'])
  67. return None
  68. @property
  69. def volatile_img(self):
  70. '''Path to the volatile image'''
  71. return self.abspath(qubes.config.vm_files['volatile_img'])
  72. def format_disk_dev(self, path, vdev, script=None, rw=True, devtype='disk',
  73. domain=None):
  74. if path is None:
  75. return ''
  76. element = lxml.etree.Element('disk')
  77. element.set('type', 'block')
  78. element.set('device', devtype)
  79. element.append(lxml.etree.Element('driver', name='phy'))
  80. element.append(lxml.etree.Element('source', dev=path))
  81. element.append(lxml.etree.Element('target', dev=vdev))
  82. if not rw:
  83. element.append(lxml.etree.Element('readonly'))
  84. if domain is not None:
  85. # XXX vm.name?
  86. element.append(lxml.etree.Element('domain', name=domain))
  87. if script:
  88. element.append(lxml.etree.Element('script', path=script))
  89. # TODO return element
  90. return lxml.etree.tostring(element)
  91. def root_dev_config(self):
  92. if isinstance(self.vm, qubes.vm.templatevm.TemplateVM):
  93. return self.format_disk_dev(
  94. '{root}:{rootcow}'.format(
  95. root=self.root_img,
  96. rootcow=self.rootcow_img),
  97. self.root_dev,
  98. script='block-origin')
  99. elif self.vm.hvm and hasattr(self.vm, 'template'):
  100. # HVM template-based VM - only one device-mapper layer, in dom0
  101. # (root+volatile)
  102. # HVM detection based on 'kernel' property is massive hack,
  103. # but taken from assumption that VM needs Qubes-specific kernel
  104. # (actually initramfs) to assemble the second layer of device-mapper
  105. return self.format_disk_dev(
  106. '{root}:{volatile}'.format(
  107. root=self.vm.template.storage.root_img,
  108. volatile=self.volatile_img),
  109. self.root_dev,
  110. script='block-snapshot')
  111. elif hasattr(self.vm, 'template'):
  112. # any other template-based VM - two device-mapper layers: one
  113. # in dom0 (here) from root+root-cow, and another one from
  114. # this+volatile.img
  115. return self.format_disk_dev(
  116. '{root}:{template_rootcow}'.format(
  117. root=self.root_img,
  118. template_rootcow=self.vm.template.storage.rootcow_img),
  119. self.root_dev,
  120. script='block-snapshot',
  121. rw=False)
  122. else:
  123. # standalone qube
  124. return self.format_disk_dev(self.root_img, self.root_dev)
  125. def private_dev_config(self):
  126. return self.format_disk_dev(self.private_img, self.private_dev)
  127. def volatile_dev_config(self):
  128. return self.format_disk_dev(self.volatile_img, self.volatile_dev)
  129. def create_on_disk_private_img(self, source_template=None):
  130. if source_template is None:
  131. f_private = open(self.private_img, 'a+b')
  132. f_private.truncate(self.private_img_size)
  133. f_private.close()
  134. else:
  135. self.vm.log.info("Copying the template's private image: {}".format(
  136. source_template.storage.private_img))
  137. self._copy_file(source_template.storage.private_img,
  138. self.private_img)
  139. def create_on_disk_root_img(self, source_template=None):
  140. if source_template is None:
  141. fd = open(self.root_img, 'a+b')
  142. fd.truncate(self.root_img_size)
  143. fd.close()
  144. elif self.vm.updateable:
  145. # if not updateable, just use template's disk
  146. self.vm.log.info("--> Copying the template's root image: {}".format(
  147. source_template.storage.root_img))
  148. self._copy_file(source_template.storage.root_img, self.root_img)
  149. def resize_private_img(self, size):
  150. fd = open(self.private_img, 'a+b')
  151. fd.truncate(size)
  152. fd.close()
  153. # find loop device if any
  154. p = subprocess.Popen(
  155. ['sudo', 'losetup', '--associated', self.private_img],
  156. stdout=subprocess.PIPE)
  157. result = p.communicate()
  158. m = re.match(r'^(/dev/loop\d+):\s', result[0])
  159. if m is not None:
  160. loop_dev = m.group(1)
  161. # resize loop device
  162. subprocess.check_call(
  163. ['sudo', 'losetup', '--set-capacity', loop_dev])
  164. def commit_template_changes(self):
  165. assert isinstance(self.vm, qubes.vm.templatevm.TemplateVM)
  166. # TODO: move rootcow_img to this class; the same for vm.is_outdated()
  167. if os.path.exists(self.vm.rootcow_img):
  168. os.rename(self.vm.rootcow_img, self.vm.rootcow_img + '.old')
  169. old_umask = os.umask(002)
  170. f_cow = open(self.vm.rootcow_img, 'w')
  171. f_root = open(self.root_img, 'r')
  172. f_root.seek(0, os.SEEK_END)
  173. # make empty sparse file of the same size as root.img
  174. f_cow.truncate(f_root.tell())
  175. f_cow.close()
  176. f_root.close()
  177. os.umask(old_umask)
  178. def reset_volatile_storage(self):
  179. try:
  180. # no template set, in any way (Standalone VM, Template VM)
  181. if self.vm.template is None:
  182. raise AttributeError
  183. # template-based HVM with only one device-mapper layer -
  184. # volatile.img used as upper layer on root.img, no root-cow.img
  185. # intermediate layer
  186. if self.vm.hvm:
  187. if os.path.exists(self.volatile_img):
  188. if self.vm.debug:
  189. if os.path.getmtime(self.vm.template.storage.root_img) \
  190. > os.path.getmtime(self.volatile_img):
  191. self.vm.log.warning(
  192. 'Template have changed, resetting root.img')
  193. else:
  194. self.vm.log.warning(
  195. 'Debug mode: not resetting root.img; if you'
  196. ' want to force root.img reset, either'
  197. ' update template VM, or remove volatile.img'
  198. ' file.')
  199. return
  200. os.remove(self.volatile_img)
  201. # FIXME stat on f_root; with open() ...
  202. f_volatile = open(self.volatile_img, "w")
  203. f_root = open(self.vm.template.storage.root_img, "r")
  204. # make empty sparse file of the same size as root.img
  205. f_root.seek(0, os.SEEK_END)
  206. f_volatile.truncate(f_root.tell())
  207. f_volatile.close()
  208. f_root.close()
  209. return # XXX why is that? super() does not run
  210. except AttributeError: # self.vm.template
  211. pass
  212. super(XenStorage, self).reset_volatile_storage()
  213. def prepare_for_vm_startup(self):
  214. super(XenStorage, self).prepare_for_vm_startup()
  215. if self.drive is not None:
  216. # pylint: disable=unused-variable
  217. (drive_type, drive_domain, drive_path) = self.drive.split(":")
  218. if drive_domain.lower() != "dom0":
  219. # XXX "VM '{}' holding '{}' does not exists".format(
  220. drive_vm = self.vm.app.domains[drive_domain]
  221. if not drive_vm.is_running():
  222. raise qubes.exc.QubesVMNotRunningError(drive_vm,
  223. 'VM {!r} holding {!r} isn\'t running'.format(
  224. drive_domain, drive_path))
  225. if self.rootcow_img and not os.path.exists(self.rootcow_img):
  226. self.commit_template_changes()
  227. class XenPool(qubes.storage.Pool):
  228. def get_storage(self):
  229. """ Returns an instantiated ``XenStorage``. """
  230. return XenStorage(self.vm, vmdir=self.vmdir)