Repository: freedomofpress/securedrop

Issue:
Python3 admin virtualenv is broken if improperly set up (doesn't contain pip3)
## Description

Initially discovered while reviewing https://github.com/freedomofpress/securedrop/pull/4927#issuecomment-543763957 , if the admin virtualenv is improperly setup, one must manually remove `admin/.venv3` to fix the virtualenv.

## Steps to Reproduce
This is very hard to reproduce, but you can do it by pressing ctrl+c when the virtualenv is being setup as part of an initial `./securedrop-admin setup` run. Subsequent `./securedrop-admin setup` invocations will no longer work

## Expected Behavior
`./securedrop-admin setup` should not fail

## Actual Behavior
`./securedrop-admin setup` fails with the following output:
```
amnesia@amnesia:~/Persistent/securedrop$ ./securedrop-admin setup
INFO: Virtualenv already exists, not creating
INFO: Checking Python dependencies for securedrop-admin
ERROR: Failed to install pip dependencies. Check network connection and try again.
```


Discussion:

Resolving patch:
diff --git a/admin/bootstrap.py b/admin/bootstrap.py
--- a/admin/bootstrap.py
+++ b/admin/bootstrap.py
@@ -154,7 +154,7 @@ def install_apt_dependencies(args):
         raise
 
 
-def envsetup(args):
+def envsetup(args, virtualenv_dir=VENV_DIR):
     """Installs Admin tooling required for managing SecureDrop. Specifically:
 
         * updates apt-cache
@@ -167,10 +167,10 @@ def envsetup(args):
     installation of packages again.
     """
     # clean up tails 3.x venv when migrating to tails 4.x
-    clean_up_tails3_venv(VENV_DIR)
+    clean_up_tails3_venv(virtualenv_dir)
 
     # virtualenv doesnt exist? Install dependencies and create
-    if not os.path.exists(VENV_DIR):
+    if not os.path.exists(virtualenv_dir):
 
         install_apt_dependencies(args)
 
@@ -181,12 +181,18 @@ def envsetup(args):
         sdlog.info("Setting up virtualenv")
         try:
             sdlog.debug(subprocess.check_output(
-                maybe_torify() + ['virtualenv', '--python=python3', VENV_DIR],
+                maybe_torify() + ['virtualenv',
+                                  '--python=python3',
+                                  virtualenv_dir
+                                  ],
                 stderr=subprocess.STDOUT))
         except subprocess.CalledProcessError as e:
             sdlog.debug(e.output)
             sdlog.error(("Unable to create virtualenv. Check network settings"
                          " and try again."))
+            sdlog.debug("Cleaning up virtualenv")
+            if os.path.exists(virtualenv_dir):
+                shutil.rmtree(virtualenv_dir)
             raise
     else:
         sdlog.info("Virtualenv already exists, not creating")


Test patch:
diff --git a/admin/tests/test_securedrop-admin-setup.py b/admin/tests/test_securedrop-admin-setup.py
--- a/admin/tests/test_securedrop-admin-setup.py
+++ b/admin/tests/test_securedrop-admin-setup.py
@@ -119,3 +119,19 @@ def test_venv_cleanup_subprocess_exception(self, tmpdir, caplog):
                                                                       ':o')):
                 bootstrap.clean_up_tails3_venv(venv_path)
                 assert os.path.exists(venv_path)
+
+    def test_envsetup_cleanup(self, tmpdir, caplog):
+        venv = os.path.join(str(tmpdir), "empty_dir")
+        args = ""
+        with pytest.raises(subprocess.CalledProcessError):
+            with mock.patch('subprocess.check_output',
+                            side_effect=self.side_effect_venv_bootstrap(venv)):
+                bootstrap.envsetup(args, venv)
+                assert not os.path.exists(venv)
+                assert 'Cleaning up virtualenv' in caplog.text
+
+    def side_effect_venv_bootstrap(self, venv_path):
+        # emulate the venv being created, and raise exception to simulate
+        # failure in virtualenv creation
+        os.makedirs(venv_path)
+        raise subprocess.CalledProcessError(1, ':o')