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:
Reopening for more improvements in 0.8 as described in [this comment](https://github.com/freedomofpress/securedrop/pull/3327#discussion_r185136483)

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
@@ -23,6 +23,7 @@
 instances.
 """
 
+from __future__ import print_function
 import argparse
 import logging
 import os
@@ -38,6 +39,9 @@
 
 sdlog = logging.getLogger(__name__)
 RELEASE_KEY = '22245C81E3BAEB4138B36061310F561200F4AD77'
+EXIT_SUCCESS = 0
+EXIT_SUBPROCESS_ERROR = 1
+EXIT_INTERRUPT = 2
 
 
 class FingerprintException(Exception):
@@ -788,17 +792,23 @@ def main(argv):
     setup_logger(args.v)
     if args.v:
         return_code = args.func(args)
-        sys.exit(return_code)
+        if return_code != 0:
+            sys.exit(EXIT_SUBPROCESS_ERROR)
     else:
         try:
             return_code = args.func(args)
         except KeyboardInterrupt:
-            sys.exit(-1)
+            print('Process was interrupted.')
+            sys.exit(EXIT_INTERRUPT)
+        except subprocess.CalledProcessError as e:
+            print('ERROR (run with -v for more): {msg}'.format(msg=e),
+                  file=sys.stderr)
+            sys.exit(EXIT_SUBPROCESS_ERROR)
         except Exception as e:
             raise SystemExit(
                 'ERROR (run with -v for more): {msg}'.format(msg=e))
         else:
-            sys.exit(return_code)
+            sys.exit(EXIT_SUCCESS)
 
 
 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
@@ -295,6 +295,33 @@ def test_no_signature_on_update(self, tmpdir, caplog):
                     assert "Updated to SecureDrop" not in caplog.text
                     assert ret_code != 0
 
+    def test_exit_codes(self, tmpdir):
+        """Ensure that securedrop-admin returns the correct
+           exit codes for success or failure."""
+        with mock.patch(
+                'securedrop_admin.install_securedrop',
+                return_value=True):
+            with pytest.raises(SystemExit) as e:
+                securedrop_admin.main(
+                    ['--root', str(tmpdir), 'install'])
+                assert e.value.code == securedrop_admin.EXIT_SUCCESS
+
+        with mock.patch(
+                'securedrop_admin.install_securedrop',
+                side_effect=subprocess.CalledProcessError(1, 'TestError')):
+            with pytest.raises(SystemExit) as e:
+                securedrop_admin.main(
+                    ['--root', str(tmpdir), 'install'])
+                assert e.value.code == securedrop_admin.EXIT_SUBPROCESS_ERROR
+
+        with mock.patch(
+                'securedrop_admin.install_securedrop',
+                side_effect=KeyboardInterrupt):
+            with pytest.raises(SystemExit) as e:
+                securedrop_admin.main(
+                    ['--root', str(tmpdir), 'install'])
+                assert e.value.code == securedrop_admin.EXIT_INTERRUPT
+
 
 class TestSiteConfig(object):