191 lines
7.3 KiB
Markdown
191 lines
7.3 KiB
Markdown
# Ansible control project
|
|
|
|
This directory contains the reproducible control project for `ghostnet-ki`.
|
|
The read-only `playbooks/preflight.yml` verifies the documented host identity
|
|
and hardware. The shared `playbooks/host.yml` applies the host baseline,
|
|
Podman runtime, workload identities and narrowly scoped AMD device access
|
|
through the dedicated deployment account.
|
|
|
|
## Control-node setup
|
|
|
|
Use Python 3.12 through 3.14 and create a virtual environment at the repository
|
|
root:
|
|
|
|
```bash
|
|
python3 -m venv .venv
|
|
.venv/bin/python -m pip install --upgrade pip
|
|
.venv/bin/python -m pip install -r ansible/requirements-control.txt
|
|
```
|
|
|
|
Load the private key for the read-only `ghost-agent` account into an SSH agent.
|
|
Do not store private keys or passphrases in this repository:
|
|
|
|
```bash
|
|
ssh-add /path/to/ghost-agent-private-key
|
|
```
|
|
|
|
The workstation must already trust the verified SSH host key for `10.0.0.90`.
|
|
An unexpected host-key change must be checked through a trusted console before
|
|
continuing.
|
|
|
|
## Read-only preflight
|
|
|
|
Run the commands from the `ansible/` directory:
|
|
|
|
```bash
|
|
cd ansible
|
|
../.venv/bin/ansible-inventory --graph
|
|
../.venv/bin/ansible-playbook playbooks/preflight.yml --syntax-check
|
|
../.venv/bin/ansible-playbook playbooks/preflight.yml
|
|
```
|
|
|
|
The final recap must report `changed=0` and `failed=0`. The playbook verifies:
|
|
|
|
- hostname `ghostnet-ki`;
|
|
- Debian major version 13;
|
|
- default IPv4 address `10.0.0.90`;
|
|
- remote Python availability; and
|
|
- two AMD Navi 21 PCI devices with ID `1002:73bf`.
|
|
|
|
The inventory deliberately contains no private-key path. Each operator supplies
|
|
their authorized key through `ssh-agent` or a local Ansible override.
|
|
|
|
## One-time deployment-account bootstrap
|
|
|
|
Status 2026-09-19: The initial bootstrap has been applied to `ghostnet-ki`, and
|
|
the separate verification playbook confirmed SSH access and non-interactive
|
|
effective UID 0 with `changed=0` and `failed=0`. Run the bootstrap again only
|
|
for an intentional repair, rotation or idempotence check.
|
|
|
|
The bootstrap creates the dedicated `ansible-deploy` account. Its password is
|
|
locked, its committed public key is restricted to the LAN and WireGuard source
|
|
networks, and `/etc/sudoers.d/90-ansible-deploy` is validated with `visudo`
|
|
before installation. The account receives `NOPASSWD` root escalation because
|
|
the planned host, package, systemd and container management cannot be expressed
|
|
as a small stable command allowlist.
|
|
|
|
The matching private key stays outside this repository. On the workstation on
|
|
which the initial key was created, it is stored as:
|
|
|
|
```text
|
|
~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy
|
|
```
|
|
|
|
The automation key has no interactive passphrase so a future runner can use it
|
|
non-interactively. It is protected by filesystem mode `0600`, SSH source-network
|
|
restrictions and the dedicated account. Compromise of this private key would
|
|
nevertheless grant root-equivalent deployment access; rotate it when moving the
|
|
deployment identity to the runner or after any suspected disclosure.
|
|
|
|
Load the existing personal administrator key into an SSH agent, then run the
|
|
bootstrap interactively from the `ansible/` directory. Ansible will ask for the
|
|
personal administrator's existing sudo password:
|
|
|
|
```bash
|
|
ssh-add /path/to/personal-ghost-private-key
|
|
../.venv/bin/ansible-playbook \
|
|
--inventory inventories/bootstrap/hosts.yml \
|
|
playbooks/bootstrap-deploy-account.yml \
|
|
--ask-become-pass
|
|
```
|
|
|
|
Do not send the password through chat, store it in Git, or place it in a shell
|
|
history entry. The bootstrap is idempotent; a second run should report no
|
|
changes.
|
|
|
|
After a successful bootstrap, verify the new account independently:
|
|
|
|
```bash
|
|
../.venv/bin/ansible-playbook \
|
|
playbooks/verify-deploy-access.yml \
|
|
--extra-vars ansible_user=ansible-deploy \
|
|
--private-key ~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy
|
|
```
|
|
|
|
The verification must report `changed=0` and `failed=0`. It confirms both the
|
|
unprivileged SSH identity and non-interactive escalation to effective UID 0.
|
|
|
|
## Shared host baseline
|
|
|
|
The host baseline is common to all workloads. It verifies the target identity
|
|
and at least 100 GiB free on the root filesystem, then manages these root-owned
|
|
parent and shared-data roots:
|
|
|
|
```text
|
|
/etc/ghostnet-ai
|
|
/var/lib/ghostnet-ai
|
|
/var/lib/ghostnet-ai/shared
|
|
```
|
|
|
|
It does not install software or start services. Use the dedicated host
|
|
inventory and supply the private deployment key outside the repository:
|
|
|
|
```bash
|
|
../.venv/bin/ansible-playbook \
|
|
--inventory inventories/host/hosts.yml \
|
|
playbooks/host.yml \
|
|
--syntax-check
|
|
|
|
../.venv/bin/ansible-playbook \
|
|
--inventory inventories/host/hosts.yml \
|
|
playbooks/host.yml \
|
|
--check --diff \
|
|
--private-key ~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy
|
|
|
|
../.venv/bin/ansible-playbook \
|
|
--inventory inventories/host/hosts.yml \
|
|
playbooks/host.yml \
|
|
--private-key ~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy
|
|
```
|
|
|
|
Run the final command a second time. The second recap must report `changed=0`
|
|
and `failed=0`; otherwise the role has not demonstrated convergence.
|
|
|
|
## Podman runtime
|
|
|
|
The shared host play also installs the Podman runtime from Debian 13. The role
|
|
requires systemd and cgroup v2, prepares `/etc/containers/systemd`, verifies the
|
|
overlay storage driver and exercises the installed Quadlet generator without
|
|
creating a workload.
|
|
|
|
The role explicitly keeps the Podman API socket and automatic image-update
|
|
timer disabled. It pulls no image. Apply and check it through the same
|
|
`playbooks/host.yml` commands shown above. The first run may install packages;
|
|
an immediate second run must report `changed=0`.
|
|
|
|
## Workload accounts
|
|
|
|
The shared host play creates three locked service identities with deterministic
|
|
UID/GID and subordinate-ID ranges:
|
|
|
|
| Account | UID/GID | Purpose | Rootless Podman graph root |
|
|
| --- | --- | --- | --- |
|
|
| `gnai-production` | 980 | stable AI Production | `/var/lib/ghostnet-ai/production/.local/share/containers/storage` |
|
|
| `gnai-testing` | 981 | temporary AI Testing | `/var/lib/ghostnet-ai/testing/.local/share/containers/storage` |
|
|
| `gn-encoding` | 982 | scheduled CPU video encoding | `/var/lib/ghostnet-encoding/.local/share/containers/storage` |
|
|
|
|
Each account has a persistent systemd user manager and a separate,
|
|
administrator-managed directory below `/etc/containers/systemd/users/<UID>`.
|
|
Configuration is root-owned and group-readable; workload data belongs to its
|
|
service account. The two AI identities have `render` as their explicit
|
|
supplementary-group baseline; the encoding identity has no GPU group. The role
|
|
creates no workload containers.
|
|
|
|
## AMD GPU access
|
|
|
|
The `amd_gpu_access` role verifies exactly two PCI devices `1002:73bf` using
|
|
the `amdgpu` kernel driver. It maps their sysfs identities to the corresponding
|
|
DRM render nodes, checks `/dev/kfd`, derives the device groups and confirms
|
|
read/write access as both AI workload identities.
|
|
|
|
On the current host, the verified devices are `/dev/kfd`,
|
|
`/dev/dri/renderD128` and `/dev/dri/renderD129`; all use group `render`.
|
|
`gnai-production` and `gnai-testing` can access them, while `gn-encoding`
|
|
remains outside that group. The final convergence run reported
|
|
`ok=55 changed=0 unreachable=0 failed=0`.
|
|
|
|
This proves host-level device permissions only. No ROCm userspace image has
|
|
been pulled and no GPU workload has run yet. The next workload check must pass
|
|
these devices into a rootless container with `GroupAdd=keep-groups` and prove
|
|
that `llama.cpp` detects both cards as `gfx1030`.
|