Repository: freedomofpress/securedrop

Issue:
Cannot create virtualenv with `./securedrop-admin setup` on fresh Tails 5.20 disks
## Description

When trying to step through a SecureDrop installation or provisioning a new Admin or Journliast Workstation drive, the process fails at the first step while running `./securedrop-admin setup`, with a message of "Unable to create virtualenv. Check network settings and try again."

The full output is:
```
./securedrop-admin setup -v
INFO: Checking apt dependencies are installed
INFO: Setting up virtualenv
DEBUG: b'ERROR: virtualenv cannot be found.\n'
ERROR: Unable to create virtualenv. Check network settings and try again.
DEBUG: Cleaning up virtualenv
```

## Steps to Reproduce

Important note: This appears to only be happening on USB drives that were flashed with a fresh copy of Tails 5.20. I **could not** reproduce this on my primary Admin Workstation drive, with an in-place upgrade to 5.20.

1. Download Tails 5.20 .img file
2. Verify the file, then flash it to a USB drive
3. Boot the USB and create a new Persistent Storage Volume
4. Connect to Tor and clone the SD repo
5. Run `./securedrop-admin setup` from the local checkout

## Expected Behavior

A virtual environment is created and depedencies are installed.

## Actual Behavior

An error appears alerting that the virtualenv could not be created.

## Comments



Discussion:
On line 211, `bootstrap.py` tries to run:

`torify virtualenv --python=python3 admin/.venv`

When trying that manually, I get:

`virtualenv: command not found`
I've discovered at least part of the underlying issue.

When you run `./securedrop-admin setup` on a fresh system, it _says_ it's checking to make sure the apt dependencies are installed, but it doesn't. It runs instantaneously, and at no point are you prompted for the local admin PW.

I discovered that if I manually run `sudo apt update` to update the local apt cache first, _then_ run `./securedrop-admin setup`, it behaves completely differently. It asks for the local admin PW, and then offers to install all the missing apt packages - which it does successfully, and then sets up the virtualenv.

It appears that a temporary workaround would be to have folks run `sudo apt update` manually first, and then try `./securedrop-admin setup` again. Of course, this doesn't explain why it's not actually checking for dependencies.
That second behaviour is what you should expect to see on a fresh instance, so this seems like a good workaround. If necessary we can do a Tails-only hotfix for this but documenting the `apt` command might be enough!

Resolving patch:
diff --git a/admin/bootstrap.py b/admin/bootstrap.py
--- a/admin/bootstrap.py
+++ b/admin/bootstrap.py
@@ -30,14 +30,8 @@
 VENV_DIR = os.path.join(DIR, ".venv3")
 
 # Space-separated list of apt dependencies
-APT_DEPENDENCIES_STR = "python3-virtualenv \
-                   python3-yaml \
-                   python3-pip \
-                   virtualenv \
-                   libffi-dev \
-                   libssl-dev \
-                   libpython3-dev \
-                   sq-keyring-linter"
+APT_DEPENDENCIES_STR = "python3-virtualenv python3-yaml python3-pip virtualenv \
+libffi-dev libssl-dev libpython3-dev sq-keyring-linter"
 
 
 def setup_logger(verbose: bool = False) -> None:
@@ -111,24 +105,22 @@ def is_missing_dependency() -> bool:
     This applies to existing Tails systems where `securedrop-setup` may not have been
     run recently.
     """
-    # apt-cache -q0 policy $dependency1 $dependency2 $dependency3 | grep "Installed: (none)"
+
     apt_query = f"apt-cache -q0 policy {APT_DEPENDENCIES_STR}".split(" ")
-    grep_command = ["grep", "Installed: (none)"]
 
     try:
         sdlog.info("Checking apt dependencies are installed")
-        apt_process = subprocess.Popen(apt_query, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        apt_query_result = subprocess.run(apt_query, capture_output=True, text=True, check=True)
 
-        grep_process = subprocess.Popen(
-            grep_command, stdin=apt_process.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE
-        )
+        # If any packages are marked as not installed, we are missing dependencies
+        if "Installed: (none)" in apt_query_result.stdout:
+            return True
 
-        # Wait for the process to complete before checking the returncode
-        grep_process.communicate()
-        returncode = grep_process.returncode
+        # If any packages couldn't be found, it may point to an apt cache issue
+        if "Unable to locate package" in apt_query_result.stderr:
+            return True
 
-        # If the above command returns 0, then one or more packages are not installed.
-        return returncode == 0
+        return False
 
     except subprocess.CalledProcessError as e:
         sdlog.error("Error checking apt dependencies")


Test patch: