qvm_backup.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. #
  2. # The Qubes OS Project, http://www.qubes-os.org
  3. #
  4. # Copyright (C) 2016 Marek Marczykowski-Górecki
  5. # <marmarek@invisiblethingslab.com>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (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 along
  18. # with this program; if not, write to the Free Software Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. from __future__ import print_function
  21. import getpass
  22. import locale
  23. import os
  24. import sys
  25. import qubes.backup
  26. import qubes.tools
  27. import qubes.utils
  28. parser = qubes.tools.QubesArgumentParser(want_force_root=True)
  29. parser.add_argument("--exclude", "-x", action="append",
  30. dest="exclude_list", default=[],
  31. help="Exclude the specified VM from the backup (may be "
  32. "repeated)")
  33. parser.add_argument("--dest-vm", "-d", action="store",
  34. dest="appvm", default=None,
  35. help="Specify the destination VM to which the backup "
  36. "will be sent (implies -e)")
  37. parser.add_argument("--encrypt", "-e", action="store_true", dest="encrypted",
  38. default=False,
  39. help="Encrypt the backup")
  40. parser.add_argument("--no-encrypt", action="store_true",
  41. dest="no_encrypt", default=False,
  42. help="Skip encryption even if sending the backup to a "
  43. "VM")
  44. parser.add_argument("--passphrase-file", "-p", action="store",
  45. dest="pass_file", default=None,
  46. help="Read passphrase from a file, or use '-' to read "
  47. "from stdin")
  48. parser.add_argument("--enc-algo", "-E", action="store",
  49. dest="crypto_algorithm", default=None,
  50. help="Specify a non-default encryption algorithm. For a "
  51. "list of supported algorithms, execute 'openssl "
  52. "list-cipher-algorithms' (implies -e)")
  53. parser.add_argument("--hmac-algo", "-H", action="store",
  54. dest="hmac_algorithm", default=None,
  55. help="Specify a non-default HMAC algorithm. For a list "
  56. "of supported algorithms, execute 'openssl "
  57. "list-message-digest-algorithms'")
  58. parser.add_argument("--compress", "-z", action="store_true", dest="compressed",
  59. default=False,
  60. help="Compress the backup")
  61. parser.add_argument("--compress-filter", "-Z", action="store",
  62. dest="compression_filter", default=False,
  63. help="Specify a non-default compression filter program "
  64. "(default: gzip)")
  65. parser.add_argument("--tmpdir", action="store", dest="tmpdir", default=None,
  66. help="Specify a temporary directory (if you have at least "
  67. "1GB free RAM in dom0, use of /tmp is advised) ("
  68. "default: /var/tmp)")
  69. parser.add_argument("backup_location", action="store",
  70. help="Backup location (directory path, or command to pipe backup to)")
  71. parser.add_argument("vms", nargs="*", action=qubes.tools.VmNameAction,
  72. help="Backup only those VMs")
  73. def main(args=None):
  74. args = parser.parse_args(args)
  75. appvm = None
  76. if args.appvm:
  77. try:
  78. appvm = args.app.domains[args.appvm]
  79. except KeyError:
  80. parser.error('no such domain: {!r}'.format(args.appvm))
  81. args.app.log.info(("NOTE: VM {} will be excluded because it is "
  82. "the backup destination.").format(args.appvm))
  83. if appvm:
  84. args.exclude_list.append(appvm.name)
  85. if args.appvm or args.crypto_algorithm:
  86. args.encrypted = True
  87. if args.no_encrypt:
  88. args.encrypted = False
  89. try:
  90. backup = qubes.backup.Backup(args.app,
  91. args.domains if args.domains else None,
  92. exclude_list=args.exclude_list)
  93. except qubes.exc.QubesException as e:
  94. parser.error_runtime(str(e))
  95. # unreachable - error_runtime will raise SystemExit
  96. return 1
  97. backup.target_dir = args.backup_location
  98. if not appvm:
  99. if os.path.isdir(args.backup_location):
  100. stat = os.statvfs(args.backup_location)
  101. else:
  102. stat = os.statvfs(os.path.dirname(args.backup_location))
  103. backup_fs_free_sz = stat.f_bsize * stat.f_bavail
  104. print()
  105. if backup.total_backup_bytes > backup_fs_free_sz:
  106. parser.error_runtime("Not enough space available on the "
  107. "backup filesystem!")
  108. args.app.log.info("Available space: {0}".format(
  109. qubes.utils.size_to_human(backup_fs_free_sz)))
  110. else:
  111. stat = os.statvfs('/var/tmp')
  112. backup_fs_free_sz = stat.f_bsize * stat.f_bavail
  113. print()
  114. if backup_fs_free_sz < 1000000000:
  115. parser.error_runtime("Not enough space available "
  116. "on the local filesystem (1GB required for temporary files)!")
  117. if not appvm.is_running():
  118. appvm.start()
  119. if not args.encrypted:
  120. args.app.log.info("WARNING: The backup will NOT be encrypted!")
  121. if args.pass_file is not None:
  122. pass_f = open(args.pass_file) if args.pass_file != "-" else sys.stdin
  123. passphrase = pass_f.readline().rstrip()
  124. if pass_f is not sys.stdin:
  125. pass_f.close()
  126. else:
  127. if input("Do you want to proceed? [y/N] ").upper() != "Y":
  128. return 0
  129. prompt = ("Please enter the passphrase that will be used to {}verify "
  130. "the backup: ").format('encrypt and ' if args.encrypted else '')
  131. passphrase = getpass.getpass(prompt)
  132. if getpass.getpass("Enter again for verification: ") != passphrase:
  133. parser.error_runtime("Passphrase mismatch!")
  134. backup.encrypted = args.encrypted
  135. backup.compressed = args.compressed
  136. if args.compression_filter:
  137. backup.compression_filter = args.compression_filter
  138. encoding = sys.stdin.encoding or locale.getpreferredencoding()
  139. backup.passphrase = passphrase.decode(encoding)
  140. if args.hmac_algorithm:
  141. backup.hmac_algorithm = args.hmac_algorithm
  142. if args.crypto_algorithm:
  143. backup.crypto_algorithm = args.crypto_algorithm
  144. if args.tmpdir:
  145. backup.tmpdir = args.tmpdir
  146. if appvm:
  147. backup.target_vm = appvm
  148. try:
  149. backup.backup_do()
  150. except qubes.exc.QubesException as e:
  151. parser.error_runtime(str(e))
  152. print()
  153. args.app.log.info("Backup completed.")
  154. return 0
  155. if __name__ == '__main__':
  156. main()