commit 991be3dd900416b90f5192e80a6d79be688daf09 Author: Twentyninehairs_bot Date: Mon May 4 22:26:08 2026 -0700 init(opencode-ansible): scaffold dedicated Ansible repo for opencode propagation diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a42954 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# OpenCode Ansible + +Dedicated Ansible orchestration repo for propagating opencode (oh-my-openagent) across the fleet. + +## Structure + +``` +opencode-ansible/ +├── ansible.cfg # Ansible configuration +├── inventory/ +│ ├── opencode.yml # Main inventory +│ └── group_vars/ +│ └── opencode.yml # Group-wide defaults +├── host_vars/ # Per-host overrides +├── playbooks/ # Deployment playbooks +├── roles/ # Ansible roles (if needed) +├── templates/ # Jinja2 templates +├── scripts/ # CLI wrapper scripts +├── callback_plugins/ # Audit logging +├── audit/ # JSONL audit trail +└── docs/ # Documentation +``` + +## Quick Start + +```bash +# Check readiness of a host +ansible-playbook -i inventory/opencode.yml playbooks/readiness.yml --limit crew2 + +# Full deployment pipeline +bash scripts/full-deploy.sh --limit crew2 + +# Verify deployment +ansible-playbook -i inventory/opencode.yml playbooks/verify.yml --limit crew2 +``` + +## Playbooks + +| Playbook | Purpose | +|----------|---------| +| `readiness.yml` | Pre-install host assessment | +| `bootstrap.yml` | Install prerequisites and service accounts | +| `install.yml` | Install opencode binary/runtime | +| `config.yml` | Deploy configuration and sync repos | +| `service.yml` | Deploy and enable systemd services | +| `verify.yml` | Post-deployment health checks | +| `rollback.yml` | Revert opencode deployment | + +## Target Hosts + +crew2, crewsupport, llm01, erpnext, poweredget150, a10, noc, kenny-GE76-Raider-11UE diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..f64eae9 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,33 @@ +[defaults] +# Inventory +inventory = inventory/opencode.yml + +# Connection +host_key_checking = False +timeout = 30 + +# Output +stdout_callback = yaml +bin_ansible_callbacks = True + +# Callback plugins (audit logging) +callback_plugins = callback_plugins +callbacks_enabled = audit_log + +# Retry files — disable, use audit log instead +retry_files_enabled = False + +# Forks — fleet is small, keep low +forks = 5 + +# Logging (in addition to audit callback) +log_path = audit/ansible.log + +# Vault — reference existing netman vault password file +vault_password_file = /home/kenny/nc/ansible/.ansible/vault_pass.txt + +[privilege_escalation] +become = False + +[ssh_connection] +pipelining = True diff --git a/audit/.gitkeep b/audit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/callback_plugins/audit_log.py b/callback_plugins/audit_log.py new file mode 100644 index 0000000..86e82a6 --- /dev/null +++ b/callback_plugins/audit_log.py @@ -0,0 +1,107 @@ +# audit_log.py — Ansible callback plugin for fleet-ops audit trail +# Logs each playbook run to /audit/-.jsonl +# Format: one JSON object per line (JSONL), one file per playbook per day + +from __future__ import annotations +from ansible.plugins.callback import CallbackBase +import json +import os +from datetime import datetime, timezone + + +DOCUMENTATION = """ + name: audit_log + type: notification + short_description: Write structured audit log to /audit/ + description: + - Appends a JSON record for each playbook run to /audit/-.jsonl + - Fields: timestamp, completed, playbook, check_mode, outcome, summary, tasks + options: {} +""" + + +class CallbackModule(CallbackBase): + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = "notification" + CALLBACK_NAME = "audit_log" + CALLBACK_NEEDS_WHITELIST = True + + AUDIT_DIR = "/audit" + + def __init__(self): + super().__init__() + self._playbook_name = "unknown" + self._start_time = None + self._check_mode = False + self._task_results: list[dict] = [] + + def v2_playbook_on_start(self, playbook): + self._playbook_name = os.path.basename(playbook._file_name) + self._start_time = datetime.now(timezone.utc).isoformat() + self._task_results = [] + + def v2_playbook_on_play_start(self, play): + self._check_mode = play.check_mode + + def _record(self, result, status: str): + entry = { + "host": result._host.name, + "task": result._task.get_name(), + "status": status, + } + if status == "failed": + entry["msg"] = str(result._result.get("msg", "")) + self._task_results.append(entry) + + def v2_runner_on_ok(self, result): + self._record(result, "ok") + + def v2_runner_on_failed(self, result, ignore_errors=False): + self._record(result, "failed" if not ignore_errors else "ignored") + + def v2_runner_on_unreachable(self, result): + self._record(result, "unreachable") + + def v2_runner_on_skipped(self, result): + self._record(result, "skipped") + + def v2_playbook_on_stats(self, stats): + end_time = datetime.now(timezone.utc).isoformat() + + summary = {} + for host in stats.processed: + summary[host] = { + "ok": stats.ok.get(host, 0), + "changed": stats.changed.get(host, 0), + "failed": stats.failures.get(host, 0), + "unreachable": stats.dark.get(host, 0), + "skipped": stats.skipped.get(host, 0), + } + + any_failed = any( + v["failed"] > 0 or v["unreachable"] > 0 for v in summary.values() + ) + outcome = "failed" if any_failed else "success" + if self._check_mode: + outcome = f"check-{outcome}" + + log_entry = { + "timestamp": self._start_time, + "completed": end_time, + "playbook": self._playbook_name, + "check_mode": self._check_mode, + "outcome": outcome, + "summary": summary, + "tasks": self._task_results, + } + + os.makedirs(self.AUDIT_DIR, exist_ok=True) + date_str = (self._start_time or end_time)[:10] + playbook_stem = self._playbook_name.replace(".yml", "").replace(".yaml", "") + log_file = os.path.join(self.AUDIT_DIR, f"{date_str}-{playbook_stem}.jsonl") + + try: + with open(log_file, "a") as f: + f.write(json.dumps(log_entry) + "\n") + except OSError as e: + self._display.warning(f"audit_log: could not write to {log_file}: {e}") diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/host_vars/.gitkeep b/host_vars/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/inventory/group_vars/opencode.yml b/inventory/group_vars/opencode.yml new file mode 100644 index 0000000..76af60d --- /dev/null +++ b/inventory/group_vars/opencode.yml @@ -0,0 +1,3 @@ +--- +# OpenCode Propagation: Group-wide Defaults +# TODO: Implement in Task 2 diff --git a/inventory/opencode.yml b/inventory/opencode.yml new file mode 100644 index 0000000..ecd909a --- /dev/null +++ b/inventory/opencode.yml @@ -0,0 +1,3 @@ +--- +# OpenCode Propagation: Main Inventory +# TODO: Implement in Task 2 diff --git a/playbooks/bootstrap.yml b/playbooks/bootstrap.yml new file mode 100644 index 0000000..35f2954 --- /dev/null +++ b/playbooks/bootstrap.yml @@ -0,0 +1,4 @@ +--- +# OpenCode Propagation: Bootstrap Prerequisites +# Installs prerequisites (Python, Node.js, service accounts, etc.) +# TODO: Implement in Task 8 diff --git a/playbooks/config.yml b/playbooks/config.yml new file mode 100644 index 0000000..a74b657 --- /dev/null +++ b/playbooks/config.yml @@ -0,0 +1,4 @@ +--- +# OpenCode Propagation: Config and Repo Sync +# Deploys configuration and syncs Gitea repos on target hosts +# TODO: Implement in Task 10 diff --git a/playbooks/install.yml b/playbooks/install.yml new file mode 100644 index 0000000..543659c --- /dev/null +++ b/playbooks/install.yml @@ -0,0 +1,4 @@ +--- +# OpenCode Propagation: Install OpenCode +# Installs opencode binary/runtime on target hosts +# TODO: Implement in Task 9 diff --git a/playbooks/readiness.yml b/playbooks/readiness.yml new file mode 100644 index 0000000..69dcba1 --- /dev/null +++ b/playbooks/readiness.yml @@ -0,0 +1,4 @@ +--- +# OpenCode Propagation: Readiness Assessment +# Verifies target hosts are ready for opencode installation +# TODO: Implement in Task 7 diff --git a/playbooks/rollback.yml b/playbooks/rollback.yml new file mode 100644 index 0000000..1ca7678 --- /dev/null +++ b/playbooks/rollback.yml @@ -0,0 +1,5 @@ +--- +# OpenCode Propagation: Rollback +# Reverts opencode deployment to clean state +# Requires --extra-vars "confirm_rollback=yes" to run +# TODO: Implement in Task 15 diff --git a/playbooks/service.yml b/playbooks/service.yml new file mode 100644 index 0000000..994b175 --- /dev/null +++ b/playbooks/service.yml @@ -0,0 +1,4 @@ +--- +# OpenCode Propagation: Service Deployment +# Deploys and enables systemd user services for opencode +# TODO: Implement in Task 11 diff --git a/playbooks/verify.yml b/playbooks/verify.yml new file mode 100644 index 0000000..3dde0fa --- /dev/null +++ b/playbooks/verify.yml @@ -0,0 +1,4 @@ +--- +# OpenCode Propagation: Verification and Health Checks +# Post-deployment verification and health checks +# TODO: Implement in Task 12 diff --git a/roles/.gitkeep b/roles/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/.gitkeep b/templates/.gitkeep new file mode 100644 index 0000000..e69de29