浏览代码

fix: bootstrap Python 3 on remote hosts before Ansible modules run

Remote host relay-server lacks Python 3, causing ansible.legacy.setup to
fail. Add a raw pre-task play to install python3 via apt and set
ansible_python_interpreter to auto for resilient interpreter discovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
kotoyuuko 3 周之前
父节点
当前提交
ae16f6541e

+ 2 - 0
group_vars/all.yml.example

@@ -9,3 +9,5 @@ base_packages:
   - unattended-upgrades
   - unattended-upgrades
 
 
 ssh_port: "{{ ansible_port | default(22) }}"
 ssh_port: "{{ ansible_port | default(22) }}"
+
+ansible_python_interpreter: auto

+ 2 - 0
openspec/changes/archive/2026-04-22-fix-python-interpreter/.openspec.yaml

@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-04-22

+ 44 - 0
openspec/changes/archive/2026-04-22-fix-python-interpreter/design.md

@@ -0,0 +1,44 @@
+## Context
+
+Ansible requires Python on remote hosts to execute modules. The `relay-server` is a minimal server image without Python 3 installed. When Ansible runs the `setup` (fact gathering) module, it fails with `/usr/bin/python3: not found` (rc: 127). This blocks all plays against that host.
+
+The project currently has no `ansible_python_interpreter` configuration and no pre-play to bootstrap Python.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Ensure Python 3 is present on all managed hosts before any Ansible modules run
+- Configure Ansible to auto-discover the Python interpreter path
+- Make the solution idempotent and safe for hosts that already have Python
+
+**Non-Goals:**
+- Installing a specific Python version (system package manager default is fine)
+- Managing Python virtual environments or pip packages
+- Changing the existing role structure beyond what's necessary
+
+## Decisions
+
+### 1. Use a `raw` pre-task play to bootstrap Python
+
+**Choice**: Add a new play at the top of `site.yml` that uses the `raw` module (which doesn't require Python) to install `python3` via apt.
+
+**Rationale**: The `raw` module executes commands directly over SSH without requiring Python on the remote. This is the standard Ansible pattern for bootstrapping Python on minimal images. Using a dedicated play with `gather_facts: false` avoids the setup module entirely.
+
+**Alternatives considered**:
+- *Install Python manually before running Ansible*: Defeats the purpose of automation and requires out-of-band steps.
+- *Use a custom SSH script*: Adds complexity outside Ansible's management.
+
+### 2. Set `ansible_python_interpreter: auto` in `group_vars/all.yml`
+
+**Choice**: Configure interpreter auto-discovery globally.
+
+**Rationale**: `auto` tells Ansible to probe common Python paths on the remote host rather than assuming `/usr/bin/python3`. This handles cases where Python is at `/usr/bin/python3.x` or other locations. Applied globally since all hosts benefit from resilient interpreter discovery.
+
+**Alternatives considered**:
+- *Set per-host in inventory*: More granular but unnecessary since all hosts are Debian/Ubuntu.
+- *Hardcode `/usr/bin/python3`*: Fragile; breaks on systems where the path differs.
+
+## Risks / Trade-offs
+
+- **[First-run latency]** The raw apt install adds a few seconds on first run. Subsequent runs are idempotent (apt skips already-installed packages). -> Acceptable trade-off for reliability.
+- **[Assumes apt-based OS]** The `raw` task uses `apt-get`. If a non-Debian host is added later, this will fail. -> Mitigated by the project being explicitly Debian/Ubuntu-only (all roles use apt).

+ 24 - 0
openspec/changes/archive/2026-04-22-fix-python-interpreter/proposal.md

@@ -0,0 +1,24 @@
+## Why
+
+Ansible fails to run against `relay-server` because it cannot find a Python interpreter at the default path `/usr/bin/python3`. The remote host does not have Python installed at that location, causing `ansible.legacy.setup` (fact gathering) to fail with `rc: 127`.
+
+## What Changes
+
+- Install Python 3 on the relay server via a raw task (which doesn't require Python) before any other plays run.
+- Configure `ansible_python_interpreter` to use auto-discovery so Ansible can locate Python regardless of install path.
+
+## Capabilities
+
+### New Capabilities
+
+- `python-bootstrap`: Ensure Python 3 is installed on remote hosts before Ansible modules are used, using raw/shell commands that don't require Python.
+
+### Modified Capabilities
+
+- `server-base`: Add `ansible_python_interpreter` configuration to handle hosts where Python may not be at the default path.
+
+## Impact
+
+- `inventory/hosts.yml` or `group_vars/all.yml`: Add `ansible_python_interpreter: auto` setting.
+- `site.yml`: Add a pre-play that uses `raw` module to install Python 3 on hosts that lack it.
+- All hosts in the inventory are affected, but the bootstrap is idempotent and safe for hosts that already have Python installed.

+ 14 - 0
openspec/changes/archive/2026-04-22-fix-python-interpreter/specs/python-bootstrap/spec.md

@@ -0,0 +1,14 @@
+## ADDED Requirements
+
+### Requirement: Python 3 is bootstrapped on all managed hosts before module execution
+A pre-task play SHALL run before any other plays in `site.yml` that installs `python3` on all hosts using the `raw` module. This play SHALL set `gather_facts: false` to avoid triggering the setup module. The install command SHALL be idempotent (safe to re-run on hosts where Python is already installed).
+
+#### Scenario: Host without Python 3
+- **WHEN** Ansible runs against a host that does not have Python 3 installed
+- **THEN** the raw task installs `python3` via `apt-get install -y python3`
+- **THEN** subsequent plays can use standard Ansible modules
+
+#### Scenario: Host already has Python 3
+- **WHEN** Ansible runs against a host that already has Python 3 installed
+- **THEN** the raw task completes successfully without changing the system
+- **THEN** subsequent plays run normally

+ 24 - 0
openspec/changes/archive/2026-04-22-fix-python-interpreter/specs/server-base/spec.md

@@ -0,0 +1,24 @@
+## MODIFIED Requirements
+
+### Requirement: Ansible inventory defines relay and landing server groups
+The inventory SHALL define two host groups: `relay` and `landing`, each containing the respective server's connection details (IP, SSH user, SSH key). The `ansible_user` SHALL be a configurable placeholder supporting both root and non-root users. The repository SHALL ship `inventory/hosts.yml.example` as a template; the actual `inventory/hosts.yml` SHALL be gitignored and created by the user. The `group_vars/all.yml` SHALL set `ansible_python_interpreter: auto` to enable interpreter auto-discovery on all hosts.
+
+#### Scenario: Inventory is valid
+- **WHEN** the user copies `hosts.yml.example` to `hosts.yml` and fills in their values
+- **THEN** two groups `relay` and `landing` are available, each with at least one host
+
+#### Scenario: Non-root user with sudo
+- **WHEN** `ansible_user` is set to a non-root user (e.g., `ubuntu`)
+- **THEN** Ansible connects as that user and uses `become` for privilege escalation
+
+#### Scenario: Root user
+- **WHEN** `ansible_user` is set to `root`
+- **THEN** Ansible connects as root directly and `become` is a no-op
+
+#### Scenario: Missing inventory
+- **WHEN** the user has not copied `hosts.yml.example` to `hosts.yml`
+- **THEN** Ansible fails with an error indicating the inventory file is missing
+
+#### Scenario: Python interpreter auto-discovery
+- **WHEN** Ansible connects to any managed host
+- **THEN** it uses auto-discovery to locate the Python interpreter instead of assuming `/usr/bin/python3`

+ 14 - 0
openspec/changes/archive/2026-04-22-fix-python-interpreter/tasks.md

@@ -0,0 +1,14 @@
+## 1. Configure Python Interpreter Auto-Discovery
+
+- [x] 1.1 Add `ansible_python_interpreter: auto` to `group_vars/all.yml`
+- [x] 1.2 Add `ansible_python_interpreter: auto` to `group_vars/all.yml.example`
+
+## 2. Add Python Bootstrap Play
+
+- [x] 2.1 Add a new play at the top of `site.yml` that runs before all other plays, targeting `all` hosts with `gather_facts: false` and `become: true`, using the `raw` module to run `apt-get update && apt-get install -y python3`
+- [x] 2.2 Verify the bootstrap play is ordered before the existing "Base server setup" play
+
+## 3. Verification
+
+- [x] 3.1 Run `ansible-playbook site.yml --syntax-check` to validate playbook syntax
+- [ ] 3.2 Run the playbook against relay-server and confirm fact gathering succeeds

+ 14 - 0
openspec/specs/python-bootstrap/spec.md

@@ -0,0 +1,14 @@
+## ADDED Requirements
+
+### Requirement: Python 3 is bootstrapped on all managed hosts before module execution
+A pre-task play SHALL run before any other plays in `site.yml` that installs `python3` on all hosts using the `raw` module. This play SHALL set `gather_facts: false` to avoid triggering the setup module. The install command SHALL be idempotent (safe to re-run on hosts where Python is already installed).
+
+#### Scenario: Host without Python 3
+- **WHEN** Ansible runs against a host that does not have Python 3 installed
+- **THEN** the raw task installs `python3` via `apt-get install -y python3`
+- **THEN** subsequent plays can use standard Ansible modules
+
+#### Scenario: Host already has Python 3
+- **WHEN** Ansible runs against a host that already has Python 3 installed
+- **THEN** the raw task completes successfully without changing the system
+- **THEN** subsequent plays run normally

+ 5 - 1
openspec/specs/server-base/spec.md

@@ -1,7 +1,7 @@
 ## ADDED Requirements
 ## ADDED Requirements
 
 
 ### Requirement: Ansible inventory defines relay and landing server groups
 ### Requirement: Ansible inventory defines relay and landing server groups
-The inventory SHALL define two host groups: `relay` and `landing`, each containing the respective server's connection details (IP, SSH user, SSH key). The `ansible_user` SHALL be a configurable placeholder supporting both root and non-root users. The repository SHALL ship `inventory/hosts.yml.example` as a template; the actual `inventory/hosts.yml` SHALL be gitignored and created by the user.
+The inventory SHALL define two host groups: `relay` and `landing`, each containing the respective server's connection details (IP, SSH user, SSH key). The `ansible_user` SHALL be a configurable placeholder supporting both root and non-root users. The repository SHALL ship `inventory/hosts.yml.example` as a template; the actual `inventory/hosts.yml` SHALL be gitignored and created by the user. The `group_vars/all.yml` SHALL set `ansible_python_interpreter: auto` to enable interpreter auto-discovery on all hosts.
 
 
 #### Scenario: Inventory is valid
 #### Scenario: Inventory is valid
 - **WHEN** the user copies `hosts.yml.example` to `hosts.yml` and fills in their values
 - **WHEN** the user copies `hosts.yml.example` to `hosts.yml` and fills in their values
@@ -19,6 +19,10 @@ The inventory SHALL define two host groups: `relay` and `landing`, each containi
 - **WHEN** the user has not copied `hosts.yml.example` to `hosts.yml`
 - **WHEN** the user has not copied `hosts.yml.example` to `hosts.yml`
 - **THEN** Ansible fails with an error indicating the inventory file is missing
 - **THEN** Ansible fails with an error indicating the inventory file is missing
 
 
+#### Scenario: Python interpreter auto-discovery
+- **WHEN** Ansible connects to any managed host
+- **THEN** it uses auto-discovery to locate the Python interpreter instead of assuming `/usr/bin/python3`
+
 ### Requirement: Base packages are installed on all servers
 ### Requirement: Base packages are installed on all servers
 The base role SHALL install essential packages: `curl`, `wget`, `vim`, `htop`, `unzip`, `ufw`, `fail2ban`, `unattended-upgrades`.
 The base role SHALL install essential packages: `curl`, `wget`, `vim`, `htop`, `unzip`, `ufw`, `fail2ban`, `unattended-upgrades`.
 
 

+ 9 - 0
site.yml

@@ -1,4 +1,13 @@
 ---
 ---
+- name: Bootstrap Python on all hosts
+  hosts: all
+  gather_facts: false
+  become: true
+  tasks:
+    - name: Install Python 3
+      ansible.builtin.raw: apt-get update && apt-get install -y python3
+      changed_when: false
+
 - name: Base server setup (all hosts)
 - name: Base server setup (all hosts)
   hosts: all
   hosts: all
   roles:
   roles: