qvm-backup 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. #!/usr/bin/python2.6
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2010 Joanna Rutkowska <joanna@invisiblethingslab.com>
  6. #
  7. # This program is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU General Public License
  9. # as published by the Free Software Foundation; either version 2
  10. # of the License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program; if not, write to the Free Software
  19. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  20. #
  21. #
  22. from qubes.qubes import QubesVmCollection
  23. from qubes.qubes import QubesException
  24. from qubes.qubes import qubes_store_filename
  25. from qubes.qubes import qubes_base_dir
  26. from optparse import OptionParser
  27. import os
  28. import time
  29. import subprocess
  30. import sys
  31. def size_to_human (size):
  32. if size < 1024:
  33. return str (size);
  34. elif size < 1024*1024:
  35. return str(round(size/1024.0,1)) + ' KiB'
  36. elif size < 1024*1024*1024:
  37. return str(round(size/(1024.0*1024),1)) + ' MiB'
  38. else:
  39. return str(round(size/(1024.0*1024*1024),1)) + ' GiB'
  40. def file_to_backup (file_path, sz = None):
  41. if sz is None:
  42. sz = os.path.getsize (qubes_store_filename)
  43. abs_file_path = os.path.abspath (file_path)
  44. abs_base_dir = os.path.abspath (qubes_base_dir) + '/'
  45. abs_file_dir = os.path.dirname (abs_file_path) + '/'
  46. (nothing, dir, subdir) = abs_file_dir.partition (abs_base_dir)
  47. assert nothing == ""
  48. assert dir == abs_base_dir
  49. return [ { "path" : file_path, "size": sz, "subdir": subdir} ]
  50. def main():
  51. usage = "usage: %prog [options] <backup-dir-path>"
  52. parser = OptionParser (usage)
  53. (options, args) = parser.parse_args ()
  54. if (len (args) != 1):
  55. print "You must specify the target backup directory (e.g. /mnt/backup)"
  56. print "qvm-backup will create a subdirectory there for each individual backup."
  57. exit (0)
  58. base_backup_dir = args[0]
  59. if not os.path.exists (base_backup_dir):
  60. print "The target directory doesn't exist!"
  61. exit(1)
  62. qvm_collection = QubesVmCollection()
  63. qvm_collection.lock_db_for_reading()
  64. qvm_collection.load()
  65. vms_list = [vm for vm in qvm_collection.values()]
  66. no_vms = len (vms_list)
  67. files_to_backup = file_to_backup (qubes_store_filename)
  68. appvms_to_backup = [vm for vm in vms_list if vm.is_appvm()]
  69. there_are_running_vms = False
  70. fields_to_display = [
  71. { "name": "VM", "width": 16},
  72. { "name": "type","width": 12 },
  73. { "name": "size", "width": 12}
  74. ]
  75. # Display the header
  76. s = ""
  77. for f in fields_to_display:
  78. fmt="{{0:-^{0}}}-+".format(f["width"] + 1)
  79. s += fmt.format('-')
  80. print s
  81. s = ""
  82. for f in fields_to_display:
  83. fmt="{{0:>{0}}} |".format(f["width"] + 1)
  84. s += fmt.format(f["name"])
  85. print s
  86. s = ""
  87. for f in fields_to_display:
  88. fmt="{{0:-^{0}}}-+".format(f["width"] + 1)
  89. s += fmt.format('-')
  90. print s
  91. if len (appvms_to_backup):
  92. for vm in appvms_to_backup:
  93. vm_sz = vm.get_disk_usage (vm.private_img)
  94. files_to_backup += file_to_backup(vm.private_img, vm_sz )
  95. files_to_backup += file_to_backup(vm.icon_path)
  96. files_to_backup += file_to_backup(vm.conf_file)
  97. #files_to_backup += file_to_backup(vm.dir_path + "/apps")
  98. if vm.is_updateable():
  99. sz = vm.get_disk_usage(vm.rootcow_img)
  100. files_to_backup += file_to_backup(vm.rootcow_img, sz)
  101. vm_sz += sz
  102. s = ""
  103. fmt="{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
  104. s += fmt.format(vm.name)
  105. fmt="{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1)
  106. s += fmt.format("AppVM" + (" + COW" if vm.is_updateable() else ""))
  107. fmt="{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1)
  108. s += fmt.format(size_to_human(vm_sz))
  109. if vm.is_running():
  110. s += " <-- The VM is running, please shut it down before proceeding with the backup!"
  111. there_are_running_vms = True
  112. print s
  113. template_vms_worth_backingup = [ vm for vm in vms_list if (vm.is_templete() and not vm.installed_by_rpm)]
  114. if len (template_vms_worth_backingup):
  115. for vm in template_vms_worth_backingup:
  116. vm_sz = vm.get_disk_utilization()
  117. files_to_backup += file_to_backup (vm.dir_path, vm_sz)
  118. s = ""
  119. fmt="{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
  120. s += fmt.format(vm.name)
  121. fmt="{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1)
  122. s += fmt.format("Template VM")
  123. fmt="{{0:>{0}}} |".format(fields_to_display[2]["width"] + 1)
  124. s += fmt.format(size_to_human(vm_sz))
  125. if vm.is_running():
  126. s += " <-- The VM is running, please shut it down before proceeding with the backup!"
  127. there_are_running_vms = True
  128. print s
  129. total_backup_sz = 0
  130. for file in files_to_backup:
  131. total_backup_sz += file["size"]
  132. s = ""
  133. for f in fields_to_display:
  134. fmt="{{0:-^{0}}}-+".format(f["width"] + 1)
  135. s += fmt.format('-')
  136. print s
  137. s = ""
  138. fmt="{{0:>{0}}} |".format(fields_to_display[0]["width"] + 1)
  139. s += fmt.format("Total size:")
  140. fmt="{{0:>{0}}} |".format(fields_to_display[1]["width"] + 1 + 2 + fields_to_display[2]["width"] + 1)
  141. s += fmt.format(size_to_human(total_backup_sz))
  142. print s
  143. s = ""
  144. for f in fields_to_display:
  145. fmt="{{0:-^{0}}}-+".format(f["width"] + 1)
  146. s += fmt.format('-')
  147. print s
  148. stat = os.statvfs(base_backup_dir)
  149. backup_fs_free_sz = stat.f_bsize * stat.f_bavail
  150. print
  151. if (total_backup_sz > backup_fs_free_sz):
  152. print "Not enough space avilable on the backup filesystem!"
  153. exit (1)
  154. if (there_are_running_vms):
  155. print "Please shutdown all VMs before proceeding."
  156. exit (1)
  157. backup_dir = base_backup_dir + "/qubes-{0}".format (time.strftime("%Y-%m-%d-%H%M%S"))
  158. if os.path.exists (backup_dir):
  159. print "ERROR: the path {0} already exists?!".format(backup_dir)
  160. print "Aborting..."
  161. exit (1)
  162. print "-> Backup dir: {0}".format (backup_dir)
  163. print "-> Avilable space: {0}".format(size_to_human(backup_fs_free_sz))
  164. prompt = raw_input ("Do you want to proceed? [y/N] ")
  165. if not (prompt == "y" or prompt == "Y"):
  166. exit (0)
  167. os.mkdir (backup_dir)
  168. if not os.path.exists (backup_dir):
  169. print "ERROR: Strange: couldn't create backup dir: {0}?!".format(backup_dir)
  170. print "Aborting..."
  171. exit (1)
  172. bytes_backedup = 0
  173. for file in files_to_backup:
  174. # We prefer to use Linux's cp, because it nicely handles sparse files
  175. progress = bytes_backedup * 100 / total_backup_sz
  176. print >> sys.stderr, "\r-> Backing up files: {0}%...".format (progress),
  177. dest_dir = backup_dir + '/' + file["subdir"]
  178. if file["subdir"] != "":
  179. retcode = subprocess.call (["mkdir", "-p", dest_dir])
  180. if retcode != 0:
  181. print "Cannot create directory: {0}?!".format(dest_dir)
  182. print "Aborting..."
  183. exit(1)
  184. retcode = subprocess.call (["cp", "-rp", file["path"], dest_dir])
  185. if retcode != 0:
  186. print "Error while copying file {0} to {1}".format(file["path"], dest_dir)
  187. exit (1)
  188. bytes_backedup += file["size"]
  189. progress = bytes_backedup * 100 / total_backup_sz
  190. print >> sys.stderr, "\r-> Backing up files: {0}%...".format (progress),
  191. print
  192. print "-> Backup completed."
  193. qvm_collection.unlock_db()
  194. main()