Repository: freedomofpress/securedrop

Issue:
Upgrade Ansible to latest stable
## Description
The Admin Workstation currently runs Ansible v2.9.7. Soon enough, we'll need to bump to the already released 2.10.x series. That's a difficult jump, and violates expectations about semver. 

## Example output for complicated upgrade
See the following failure message about a naive upgrade from 2.9.7 -> 2.10.7

<details>
<summary>
pip error output
</summary>

```
$ ansible --version
ansible 2.9.7
  config file = /home/user/securedrop/ansible.cfg
  configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/user/.virtualenvs/sd/lib/python3.7/site-packages/ansible
  executable location = /home/user/.virtualenvs/sd/bin/ansible
  python version = 3.7.3 (default, Jul 25 2020, 13:03:44) [GCC 8.3.0]
[user@sd-dev:~/securedrop] [sd] develop+* 6h15m51s ± 
 $ pip install 'ansible>2.10,<2.11' 
Collecting ansible<2.11,>2.10
  Downloading https://files.pythonhosted.org/packages/ba/22/7b58a8ba8e43159dc5cb32d97dd50e2b70b016585dbb188e9f2b61dac1e2/ansible-2.10.7.tar.gz (29.9MB)
     |████████████████████████████████| 29.9MB 561kB/s 
    ERROR: Command errored out with exit status 1:
     command: /home/user/.virtualenvs/sd/bin/python3 -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/tmp/pip-install-312we13s/ansible/setup.py'"'"'; __file__='"'"'/tmp/pip-install-312we13s/ansible/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' egg_info --egg-base /tmp/pip-install-312we13s/ansible/pip-egg-info
         cwd: /tmp/pip-install-312we13s/ansible/
    Complete output (31 lines):
    
    
                ### ERROR ###
    
                Upgrading directly from ansible-2.9 or less to ansible-2.10 or greater with pip is
                known to cause problems.  Please uninstall the old version found at:
    
                /home/user/.virtualenvs/sd/lib/python3.7/site-packages/ansible/__init__.py
    
                and install the new version:
    
                    pip uninstall ansible
                    pip install ansible
    
                If you have a broken installation, perhaps because ansible-base was installed before
                ansible was upgraded, try this to resolve it:
    
                    pip install --force-reinstall ansible ansible-base
    
                If ansible is installed in a different location than you will be installing it now
                (for example, if the old version is installed by a system package manager to
                /usr/lib/python3.8/site-packages/ansible but you are installing the new version into
                ~/.local/lib/python3.8/site-packages/ansible with `pip install --user ansible`)
                or you want to install anyways and cleanup any breakage afterwards, then you may set
                the ANSIBLE_SKIP_CONFLICT_CHECK environment variable to ignore this check:
    
                    ANSIBLE_SKIP_CONFLICT_CHECK=1 pip install --user ansible
    
                ### END ERROR ###
    
    
    ----------------------------------------
ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.
WARNING: You are using pip version 19.3.1; however, version 21.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
```
</details>

The import bit:

<blockquote>
Upgrading directly from ansible-2.9 or less to ansible-2.10 or greater with pip is known to cause problems.  Please uninstall the old version found at: /home/user/.virtualenvs/sd/lib/python3.7/site-packages/ansible/__init__.py and install the new version:
    
pip uninstall ansible
pip install ansible
</blockquote>

That change will require special handling of the upgrade logic in the Admin Workstation, to avoid breakage. 

## Misc

Looks like the Ansible project is [switching to semver](https://docs.ansible.com/ansible/latest/roadmap/COLLECTIONS_3_0.html#release-schedule), presumably due to feedback regarding the breakaging changes in 2.10:

> Ansible is switching from its traditional versioning scheme to semantic versioning starting with this release. So this version is 3.0.0 instead of 2.11.0.


Discussion:

Resolving patch:
diff --git a/admin/bootstrap.py b/admin/bootstrap.py
--- a/admin/bootstrap.py
+++ b/admin/bootstrap.py
@@ -255,6 +255,40 @@ def install_pip_dependencies(
     """
     Install Python dependencies via pip into virtualenv.
     """
+
+    # Ansible version 2.9.* cannot be directly upgraded and must be removed
+    # before attempting to install a later version - so let's check for it
+    # and uninstall it if we find it!
+
+    ansible_vercheck_cmd = [
+        os.path.join(VENV_DIR, "bin", "python3"),
+        "-c",
+        "from importlib.metadata import version as v; print(v('ansible'))",
+    ]
+
+    ansible_uninstall_cmd = [
+        os.path.join(VENV_DIR, "bin", "pip3"),
+        "uninstall",
+        "-y",
+        "ansible",
+    ]
+
+    ansible_ver = subprocess.run(
+        maybe_torify() + ansible_vercheck_cmd, text=True, capture_output=True
+    )
+    if ansible_ver.stdout.startswith("2.9"):
+        sdlog.info("Ansible is out-of-date, removing it.")
+        delete_result = subprocess.run(
+            maybe_torify() + ansible_uninstall_cmd, capture_output=True, text=True
+        )
+        if delete_result.returncode != 0:
+            sdlog.error(
+                "Failed to remove old ansible version:\n"
+                f"  return num: {delete_result.returncode}\n"
+                f"  error text: {delete_result.stderr}\n"
+                "Attempting to continue."
+            )
+
     pip_install_cmd = [
         os.path.join(VENV_DIR, "bin", "pip3"),
         "install",
@@ -274,7 +308,7 @@ def install_pip_dependencies(
         )
     except subprocess.CalledProcessError as e:
         sdlog.debug(e.output)
-        sdlog.error("Failed to install {}. Check network" " connection and try again.".format(desc))
+        sdlog.error(f"Failed to install {desc}. Check network" " connection and try again.")
         raise
 
     sdlog.debug(pip_output)
diff --git a/install_files/ansible-base/callback_plugins/ansible_version_check.py b/install_files/ansible-base/callback_plugins/ansible_version_check.py
--- a/install_files/ansible-base/callback_plugins/ansible_version_check.py
+++ b/install_files/ansible-base/callback_plugins/ansible_version_check.py
@@ -17,8 +17,8 @@ class CallbackModule(CallbackBase):
     def __init__(self):
         # The acceptable version range needs to be synchronized with
         # requirements files.
-        viable_start = [2, 9, 7]
-        viable_end = [2, 10, 0]
+        viable_start = [2, 11, 0]
+        viable_end = [2, 15, 0]
         ansible_version = [int(v) for v in ansible.__version__.split(".")]
         if not (viable_start <= ansible_version < viable_end):
             print_red_bold(


Test patch:
diff --git a/molecule/testinfra/common/test_platform.py b/molecule/testinfra/common/test_platform.py
--- a/molecule/testinfra/common/test_platform.py
+++ b/molecule/testinfra/common/test_platform.py
@@ -10,7 +10,7 @@ def test_ansible_version(host):
     """
     localhost = host.get_host("local://")
     c = localhost.check_output("ansible --version")
-    assert c.startswith("ansible 2.")
+    assert c.startswith("ansible [core 2.")
 
 
 def test_platform(host):