Python Automation Practice Lab
One control node where scripts run and two hosts they act on. The second target is not padding: with a single remote host an inventory loop runs once and every partial-failure case disappears, which is most of what makes automation hard. This guide also sets up the two things the rest of the path assumes - a virtual environment, because Ubuntu refuses to let pip near the system Python, and key-based SSH to both targets.
Start Here Guide 2 of 39 Beginner
- Python3.14.4
- Control nodeUbuntu 26.04 LTS
- Managed hostsRHEL 10.0
- requests2.34.2
- paramiko5.0.0
- pytest9.1.1
- PyYAML6.0.3
- boto3 / botocore1.43.78
- TimeAbout 13 min
- Reviewed24 August 2026
Written against the versions above. The control node runs **Ubuntu 26.04 with Python 3.14.4**; the managed hosts run **RHEL 10 with Python 3.12.9**. That mismatch is deliberate and is the normal case in real work - a script written on one is not guaranteed to run on the other, and this path says so wherever it matters.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| RUNNER01 | 192.168.0.27 | Ubuntu 26.04 LTS | Control node - every script in this path runs here | 2 Core | 4 GB | 50 GB |
| RHCSA-A01 | 192.168.0.31 | RHEL 10.0 | Managed host - reached over SSH from the control node | 2 Core | 4 GB | 50 GB |
| RHCSA-B01 | 192.168.0.33 | RHEL 10.0 | Second managed host - so an inventory has more than one row | 2 Core | 4 GB | 50 GB |
Before you start
- guide 1 - the five domains.
- A shell. Everything here happens on the control node except the SSH checks.
-
The control node
One machine where every script in this path runs. It needs Python and nothing else - no agent, no daemon, no privileged access to anything.
That is the control node pattern, and it is worth naming because it is how Ansible, Salt and most CI runners work too: one place holds the code and the credentials, and the machines being managed hold nothing.
bash Example session hostname; python3 --version; cat /etc/os-release | head -2ahm-runner01Python 3.14.4PRETTY_NAME="Ubuntu 26.04 LTS"NAME="Ubuntu"mkdir -p ~/auto && ls -d ~/auto/home/sysadmin/autoExpected result
ahm-runner01, Python 3.14.4, Ubuntu 26.04, and a scratch directory.Success conditionYou know which machine the scripts run on.
-
Why the system Python refuses to install anything
First real obstacle, and it is not a misconfiguration. Ubuntu 26.04 marks its system Python as externally managed - PEP 668 - and
pip installinto it is refused outright.bash Example session python3 -m pip install requests 2>&1 | head -8error: externally-managed-environment × This environment is externally managed╰─> To install Python packages system-wide, try apt install python3-xyz, where xyz is the package you are trying to install. If you wish to install a non-Debian-packaged Python package,ls -l /usr/lib/python3.14/EXTERNALLY-MANAGED-rw-r--r-- 1 root root 645 Jun 18 14:25 /usr/lib/python3.14/EXTERNALLY-MANAGEDExpected result
error: externally-managed-environment, and the marker file that causes it.Success conditionYou can recognise this error and know it is not your fault.
-
The virtual environment
A directory with its own
site-packagesand its ownpython. Nothing is activated and nothing needs to be - the path's scripts invoke~/autoenv/bin/pythondirectly, which is also the only way a cron job or a systemd unit can invoke it.bash Example session ls -d ~/autoenv && ~/autoenv/bin/python --version/home/sysadmin/autoenvPython 3.14.4~/autoenv/bin/python -c "import requests, paramiko, pytest, yaml; print('requests', requests.__version__); print('paramiko', paramiko.__version__); print('pytest', pytest.__version__); print('PyYAML', yaml.__version__)"requests 2.34.2paramiko 5.0.0pytest 9.1.1PyYAML 6.0.3~/autoenv/bin/python -c "import sys; print('sys.prefix ', sys.prefix); print('sys.base_prefix', sys.base_prefix)"sys.prefix /home/sysadmin/autoenvsys.base_prefix /usrExpected resultThe four dependencies with their versions, then
sys.prefixpointing at the venv whilesys.base_prefixstill points at/usr.Success conditionYou have somewhere to install things.
-
The two managed hosts
Two ordinary Linux boxes, reachable over SSH with a key and no password prompt.
BatchMode=yesis the flag that makes that testable: it disables every interactive prompt, so the command either works or fails immediately rather than hanging waiting for a password.bash Example session for ip in 192.168.0.31 192.168.0.33; do ssh -o BatchMode=yes sysadmin@$ip 'echo "$(hostname) $(cat /etc/redhat-release)"'; donerhcsa-a01 Red Hat Enterprise Linux release 10.0 (Coughlan)rhcsa-b01 Red Hat Enterprise Linux release 10.0 (Coughlan)Expected result
rhcsa-a01andrhcsa-b01, both RHEL 10.Success conditionThe control node can reach both targets without a prompt.
-
The targets do not run your Python
This is the one lab fact most worth carrying forward. The control node has 3.14.4. The targets have 3.12.9.
bash Example session ssh -o BatchMode=yes sysadmin@192.168.0.31 'python3 --version'Python 3.12.9ssh -o BatchMode=yes sysadmin@192.168.0.31 'python3 --version'Python 3.12.9Expected result
Python 3.12.9on the target,Python 3.14.4on the control node.Success conditionYou will not assume a script that runs here runs there.
-
What is already there
Eighteen standard library modules, imported on the plain system Python with nothing installed. This is most of the track.
bash Example session cat > ~/auto/stdlib.py <<'PY'mods = ["pathlib", "csv", "json", "configparser", "tomllib", "sqlite3", "re", "subprocess", "os", "shutil", "tempfile", "logging", "argparse", "urllib.request", "unittest.mock", "signal", "shlex", "http.server"] for name in mods: __import__(name) print(len(mods), "standard library modules imported, nothing installed:")for name in mods: print(" ", name)PYpython3 ~/auto/stdlib.py18 standard library modules imported, nothing installed: pathlib csv json configparser tomllib sqlite3 re subprocess os shutil tempfile logging argparse urllib.request unittest.mock signal shlex http.serverExpected resultEighteen module names, no errors.
Success conditionYou know how much needs no dependency at all.
Troubleshooting
error: externally-managed-environmentfrom pip.Why: PEP 668. The distribution's Python is managed by apt and pip is locked out.
Fix:Use a venv -
python3 -m venv ~/autoenv- orapt install python3-<name>for a packaged library. Avoid--break-system-packages.ensurepip is not availablewhen creating a venv.Why: Debian and Ubuntu split
venvinto its own package.Fix:
sudo apt install python3.14-venv, matching your minor version, then create the venv again.SSH to a target asks for a password.
Why: No key installed, or the key has the wrong permissions on either end.
Fix:
ssh-copy-id sysadmin@host. Test withssh -o BatchMode=yes host true, which fails rather than prompting. See guide 32.A script works interactively and fails from cron.
Why: Cron has no shell profile, so the venv was never activated and
PATHis minimal.Fix:Invoke the interpreter by absolute path -
~/autoenv/bin/python script.py- and never rely on activation. See guide 23.A script that runs on the control node fails on a managed host.
Why: Different Python - 3.14 here, 3.12 there - or a library that only the control node has.
Fix:Keep the Python on the control node and send shell commands to the targets. If code really must run remotely, target the oldest interpreter in the fleet.