Repository: freedomofpress/securedrop

Issue:
Remove usage of lsb_release
## Description

lsb_release is mostly obsolete these days, instead we should read from `/etc/os-release`.

It's still used in https://github.com/freedomofpress/securedrop/blob/develop/securedrop/server_os.py, as well as:
```
user@dev ~/g/f/securedrop> ack lsb_release --ignore-dir=.venv
admin/bootstrap.py
67:        id = subprocess.check_output("lsb_release --id --short", shell=True).decode("utf-8").strip()
88:            dist = subprocess.check_output("lsb_release --codename --short", shell=True).strip()

securedrop/server_os.py
10:        ["/usr/bin/lsb_release", "--release", "--short"],

install_files/ansible-base/roles/tails-config/files/securedrop_init.py
127:cmd = ["lsb_release", "--id", "--short"]

install_files/ansible-base/roles/build-securedrop-app-code-deb-pkg/files/usr.sbin.apache2
101:  /usr/bin/lsb_release rix,

install_files/ansible-base/inventory-dynamic
131:    Returns output of `lsb_release --id --short`, to check
134:    cmd = ["lsb_release", "--id", "--short"]

install_files/securedrop-app-code/debian/rules
5:SECUREDROP_BUILD_PLATFORM=$(shell lsb_release -sc)
```

* https://0pointer.de/blog/projects/os-release (announcement, addresses "why not lsb_release")
* https://www.freedesktop.org/software/systemd/man/os-release.html


Discussion:
Should we use grep/awk to read `VERSION_ID` from `/etc/os-release`?

```
$ /usr/bin/lsb_release --release --short
20.04
$ cat /etc/os-release | grep VERSION_ID | cut -d \" -f 2
20.04
$ awk -F\" '$1=="VERSION_ID=" { print $2 ;}' /etc/os-release
20.04
```
So in Python code I think we can just do the string parsing in Python itself.

For shell scripting I tend to like `source /etc/os-release` and then use them as environment variables. But if that's not possible, or not a good idea because of potential name conflicts, then yeah, some grep/cut/awk oneliner is fine.

Resolving patch:
diff --git a/admin/bootstrap.py b/admin/bootstrap.py
--- a/admin/bootstrap.py
+++ b/admin/bootstrap.py
@@ -63,17 +63,8 @@ def run_command(command: List[str]) -> Iterator[bytes]:
 
 
 def is_tails() -> bool:
-    try:
-        id = subprocess.check_output("lsb_release --id --short", shell=True).decode("utf-8").strip()
-    except subprocess.CalledProcessError:
-        return False
-
-    # dirty hack to unreliably detect Tails 4.0~beta2
-    if id == "Debian":
-        if os.uname()[1] == "amnesia":
-            id = "Tails"
-
-    return id == "Tails"
+    with open("/etc/os-release") as f:
+        return "TAILS_PRODUCT_NAME" in f.read()
 
 
 def clean_up_old_tails_venv(virtualenv_dir: str = VENV_DIR) -> None:
@@ -84,18 +75,19 @@ def clean_up_old_tails_venv(virtualenv_dir: str = VENV_DIR) -> None:
     venv, so it'll get recreated.
     """
     if is_tails():
-        try:
-            dist = subprocess.check_output("lsb_release --codename --short", shell=True).strip()
-        except subprocess.CalledProcessError:
-            return None
-
-        # Tails 5 is based on bullseye / Python 3.9
-        if dist == b"bullseye":
-            python_lib_path = os.path.join(virtualenv_dir, "lib/python3.7")
-            if os.path.exists(python_lib_path):
-                sdlog.info("Tails 4 virtualenv detected. Removing it.")
-                shutil.rmtree(virtualenv_dir)
-                sdlog.info("Tails 4 virtualenv deleted.")
+        with open("/etc/os-release") as f:
+            os_release = f.readlines()
+            for line in os_release:
+                if line.startswith("TAILS_VERSION_ID="):
+                    version = line.split("=")[1].strip().strip('"')
+                    if version.startswith("5."):
+                        # Tails 5 is based on Python 3.9
+                        python_lib_path = os.path.join(virtualenv_dir, "lib/python3.7")
+                        if os.path.exists(python_lib_path):
+                            sdlog.info("Tails 4 virtualenv detected. Removing it.")
+                            shutil.rmtree(virtualenv_dir)
+                            sdlog.info("Tails 4 virtualenv deleted.")
+                    break
 
 
 def checkenv(args: argparse.Namespace) -> None:
diff --git a/install_files/ansible-base/roles/tails-config/files/securedrop_init.py b/install_files/ansible-base/roles/tails-config/files/securedrop_init.py
--- a/install_files/ansible-base/roles/tails-config/files/securedrop_init.py
+++ b/install_files/ansible-base/roles/tails-config/files/securedrop_init.py
@@ -124,10 +124,9 @@
     subprocess.call(["gio", "set", path_desktop + shortcut, "metadata::trusted", "true"], env=env)
 
 # in Tails 4, reload gnome-shell desktop icons extension to update with changes above
-cmd = ["lsb_release", "--id", "--short"]
-p = subprocess.check_output(cmd)
-distro_id = p.rstrip()
-if distro_id == "Debian" and os.uname()[1] == "amnesia":
+with open("/etc/os-release") as f:
+    is_tails = "TAILS_PRODUCT_NAME" in f.read()
+if is_tails:
     subprocess.call(["gnome-shell-extension-tool", "-r", "desktop-icons@csoriano"], env=env)
 
 # reacquire uid0 and notify the user
diff --git a/securedrop/server_os.py b/securedrop/server_os.py
--- a/securedrop/server_os.py
+++ b/securedrop/server_os.py
@@ -1,14 +1,14 @@
 import functools
-import subprocess
 
 FOCAL_VERSION = "20.04"
 
 
 @functools.lru_cache()
 def get_os_release() -> str:
-    return subprocess.run(
-        ["/usr/bin/lsb_release", "--release", "--short"],
-        check=True,
-        stdout=subprocess.PIPE,
-        universal_newlines=True,
-    ).stdout.strip()
+    with open("/etc/os-release") as f:
+        os_release = f.readlines()
+        for line in os_release:
+            if line.startswith("VERSION_ID="):
+                version_id = line.split("=")[1].strip().strip('"')
+                break
+    return version_id


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
@@ -82,7 +82,7 @@ def test_python3_buster_venv_deleted_in_bullseye(self, tmpdir, caplog):
         python_lib_path = os.path.join(str(tmpdir), "lib/python3.7")
         os.makedirs(python_lib_path)
         with mock.patch("bootstrap.is_tails", return_value=True):
-            with mock.patch("subprocess.check_output", return_value=b"bullseye"):
+            with mock.patch("builtins.open", mock.mock_open(read_data='TAILS_VERSION_ID="5.0"')):
                 bootstrap.clean_up_old_tails_venv(venv_path)
                 assert "Tails 4 virtualenv detected." in caplog.text
                 assert "Tails 4 virtualenv deleted." in caplog.text