Repository: freedomofpress/securedrop

Issue:
Support restoring from a backup file that has been manually transferred to the server?
## Description

Failures while transferring multi-gigabyte backups during the restore procedures are pretty common, as they take a long time over Tor. The "partial" flag is set in the Ansible synchronize task, but that doesn't seem to help in practice.

It would be cool to allow for cases where:

- admins have physical server access, and can use a Transfer Device to copy a backup file manually to the app server
- admins use rsync directly to copy a  backup (with resume/progress support, most likely).

Probably the simplest way to do this would be to:

1. Add an arg `--no-transfer` to the restore command
1. when arg set, expect the backup to be present on both Admin WS and server with the same name, in `.../install_files/ansible-base` and `/tmp` respectively (you need the local backup to verify Tor configs)
2. compare checksums of local and server files, and only proceed if they match
3. skip the sync step and just start unpacking.

## User Research Evidence

Multiple users have raised this issue.

## User Stories

As an admin, I don't want to spend hours or days of my life waiting for Tor file transfers that have a high-chance of failing.


Discussion:

Resolving patch:
diff --git a/admin/securedrop_admin/__init__.py b/admin/securedrop_admin/__init__.py
--- a/admin/securedrop_admin/__init__.py
+++ b/admin/securedrop_admin/__init__.py
@@ -883,25 +883,23 @@ def restore_securedrop(args: argparse.Namespace) -> int:
     # Would like readable output if there's a problem
     os.environ["ANSIBLE_STDOUT_CALLBACK"] = "debug"
 
-    ansible_cmd_full_restore = [
+    ansible_cmd = [
         'ansible-playbook',
         os.path.join(args.ansible_path, 'securedrop-restore.yml'),
         '-e',
-        "restore_file='{}'".format(restore_file_basename),
     ]
 
-    ansible_cmd_skip_tor = [
-        'ansible-playbook',
-        os.path.join(args.ansible_path, 'securedrop-restore.yml'),
-        '-e',
-        "restore_file='{}' restore_skip_tor='True'".format(restore_file_basename),
+    ansible_cmd_extras = [
+        "restore_file='{}'".format(restore_file_basename),
     ]
 
     if args.restore_skip_tor:
-        ansible_cmd = ansible_cmd_skip_tor
-    else:
-        ansible_cmd = ansible_cmd_full_restore
+        ansible_cmd_extras.append("restore_skip_tor='True'")
+
+    if args.restore_manual_transfer:
+        ansible_cmd_extras.append("restore_manual_transfer='True'")
 
+    ansible_cmd.append(' '.join(ansible_cmd_extras))
     return subprocess.check_call(ansible_cmd, cwd=args.ansible_path)
 
 
@@ -1161,6 +1159,11 @@ class ArgParseFormatterCombo(argparse.ArgumentDefaultsHelpFormatter,
                                dest='restore_skip_tor',
                                help="Preserve the server's current Tor config")
 
+    parse_restore.add_argument("--no-transfer", default=False,
+                               action='store_true',
+                               dest='restore_manual_transfer',
+                               help="Restore using a backup file already present on the server")
+
     parse_update = subparsers.add_parser('update', help=update.__doc__)
     parse_update.set_defaults(func=update)
 


Test patch: