Repository: freedomofpress/securedrop

Issue:
Improve return codes in securedrop-admin
## Description

I noticed while making a change in #3300 that the `return 1` in `securedrop-admin update` did not produce an eventual `sys.exit(1)` as I expected - it always returned `sys.exit(0)`. Looking at the logic in `securedrop-admin/__init__.py`:

```
        try:
            args.func(args)
        except KeyboardInterrupt:
            sys.exit(0)
        except Exception as e:
            raise SystemExit(
                'ERROR (run with -v for more): {msg}'.format(msg=e))
        else:
            sys.exit(0)
```

it looks like we `sys.exit(0)` unless there is an exception, i.e. we're not passing the return codes through to `sys.exit()`. Also, we `sys.exit(0)` for a `KeyboardInterrupt`. 

A suggested improvement is to exit non-zero for a `KeyboardInterrupt` and to pass the return codes through to `sys.exit()` (@kushaldas let me know if you have additional thoughts on this). I think this is a small but worthwhile change (I expect most of the diff will be in the unit tests), and I advocate that we resolve it during the extended (2 week) QA period. 


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
@@ -497,9 +497,9 @@ def install_securedrop(args):
                "servers.")
     sdlog.info("The sudo password is only necessary during initial "
                "installation.")
-    subprocess.check_call([os.path.join(args.ansible_path,
-                                        'securedrop-prod.yml'),
-                          '--ask-become-pass'], cwd=args.ansible_path)
+    return subprocess.check_call([os.path.join(args.ansible_path,
+                                 'securedrop-prod.yml'), '--ask-become-pass'],
+                                 cwd=args.ansible_path)
 
 
 def backup_securedrop(args):
@@ -512,7 +512,7 @@ def backup_securedrop(args):
         'ansible-playbook',
         os.path.join(args.ansible_path, 'securedrop-backup.yml'),
     ]
-    subprocess.check_call(ansible_cmd, cwd=args.ansible_path)
+    return subprocess.check_call(ansible_cmd, cwd=args.ansible_path)
 
 
 def restore_securedrop(args):
@@ -531,7 +531,7 @@ def restore_securedrop(args):
         '-e',
         "restore_file='{}'".format(restore_file_basename),
     ]
-    subprocess.check_call(ansible_cmd, cwd=args.ansible_path)
+    return subprocess.check_call(ansible_cmd, cwd=args.ansible_path)
 
 
 def run_tails_config(args):
@@ -546,8 +546,8 @@ def run_tails_config(args):
         # inventory script, which fails if no site vars are configured.
         '-i', '/dev/null',
     ]
-    subprocess.check_call(ansible_cmd,
-                          cwd=args.ansible_path)
+    return subprocess.check_call(ansible_cmd,
+                                 cwd=args.ansible_path)
 
 
 def check_for_updates(args):
@@ -622,10 +622,11 @@ def update(args):
 
     if 'Good signature' not in sig_result:
         sdlog.info("Signature verification failed.")
-        sys.exit(1)
+        return -1
     sdlog.info("Signature verification successful.")
 
     sdlog.info("Updated to SecureDrop {}.".format(latest_tag))
+    return 0
 
 
 def get_logs(args):
@@ -638,6 +639,7 @@ def get_logs(args):
     subprocess.check_call(ansible_cmd, cwd=args.ansible_path)
     sdlog.info("Encrypt logs and send to securedrop@freedom.press or upload "
                "to the SecureDrop support portal.")
+    return 0
 
 
 def set_default_paths(args):
@@ -713,17 +715,18 @@ def main(argv):
     args = parse_argv(argv)
     setup_logger(args.v)
     if args.v:
-        args.func(args)
+        return_code = args.func(args)
+        sys.exit(return_code)
     else:
         try:
-            args.func(args)
+            return_code = args.func(args)
         except KeyboardInterrupt:
-            sys.exit(0)
+            sys.exit(-1)
         except Exception as e:
             raise SystemExit(
                 'ERROR (run with -v for more): {msg}'.format(msg=e))
         else:
-            sys.exit(0)
+            sys.exit(return_code)
 
 
 if __name__ == "__main__":


Test patch:
diff --git a/admin/tests/test_securedrop-admin.py b/admin/tests/test_securedrop-admin.py
--- a/admin/tests/test_securedrop-admin.py
+++ b/admin/tests/test_securedrop-admin.py
@@ -102,9 +102,10 @@ def test_update_exits_if_not_needed(self, tmpdir, caplog):
 
         with mock.patch('securedrop_admin.check_for_updates',
                         return_value=(False, "0.6.1")):
-            securedrop_admin.update(args)
+            ret_code = securedrop_admin.update(args)
             assert "Applying SecureDrop updates..." in caplog.text
             assert "Updated to SecureDrop" not in caplog.text
+            assert ret_code == 0
 
     def test_update_gpg_recv_primary_key_failure(self, tmpdir, caplog):
         """We should try a secondary keyserver if for some reason the primary
@@ -131,10 +132,11 @@ def test_update_gpg_recv_primary_key_failure(self, tmpdir, caplog):
             patcher.start()
 
         try:
-            securedrop_admin.update(args)
+            ret_code = securedrop_admin.update(args)
             assert "Applying SecureDrop updates..." in caplog.text
             assert "Signature verification successful." in caplog.text
             assert "Updated to SecureDrop" in caplog.text
+            assert ret_code == 0
         finally:
             for patcher in patchers:
                 patcher.stop()
@@ -161,10 +163,11 @@ def test_update_signature_verifies(self, tmpdir, caplog):
             with mock.patch('subprocess.check_call'):
                 with mock.patch('subprocess.check_output',
                                 return_value=git_output):
-                    securedrop_admin.update(args)
+                    ret_code = securedrop_admin.update(args)
                     assert "Applying SecureDrop updates..." in caplog.text
                     assert "Signature verification successful." in caplog.text
                     assert "Updated to SecureDrop" in caplog.text
+                    assert ret_code == 0
 
     def test_update_signature_does_not_verify(self, tmpdir, caplog):
         git_repo_path = str(tmpdir)
@@ -177,11 +180,11 @@ def test_update_signature_does_not_verify(self, tmpdir, caplog):
             with mock.patch('subprocess.check_call'):
                 with mock.patch('subprocess.check_output',
                                 return_value=git_output):
-                    with pytest.raises(SystemExit):
-                        securedrop_admin.update(args)
-                        assert "Applying SecureDrop updates..." in caplog.text
-                        assert "Signature verification failed." in caplog.text
-                        assert "Updated to SecureDrop" not in caplog.text
+                    ret_code = securedrop_admin.update(args)
+                    assert "Applying SecureDrop updates..." in caplog.text
+                    assert "Signature verification failed." in caplog.text
+                    assert "Updated to SecureDrop" not in caplog.text
+                    assert ret_code != 0
 
 
 class TestSiteConfig(object):