From e1bf172378e9364995970b24e2f0826653dc13cf Mon Sep 17 00:00:00 2001 From: vsutra-consultant Date: Tue, 11 Aug 2026 12:23:22 +0530 Subject: [PATCH] feat(vpn): fail-fast reachability check + timeouts for VPN-gated targets (ADR-011) Operators connect the VPN tunnel manually on the controller before triggering a deploy - Ansible/Jenkins never drives openconnect itself (see Virtual-Sutra/openspecimen-ansible#143). This adds the pieces that belong in the core playbook for that model: - inventory/customers/ocserv-openspecimen/hosts: ansible_timeout + ansible_become_timeout 10s -> 60s, scoped to this VPN-gated customer only (not a global ansible.cfg change - direct/EICE customers should keep failing fast on a genuinely unreachable host). The ocserv PoC found VPN tunnel latency (~200ms+) tight enough to fail privilege escalation before the connection even finished setting up. - site.yml: a new pre-flight play (vpn_required opt-in, default off) that waits for the target's SSH port before the main play's implicit fact gathering would otherwise be the first thing to hit a down tunnel, then fails with a clear "check the tunnel" message (matching verify-customer.yml's existing assert/fail_msg convention) instead of a deep, generic SSH timeout. - inventory/customers/ocserv-openspecimen/: test profile for the ocserv AnyConnect PoC target, vpn_required: true. --- .../group_vars/openspecimen.yml | 15 ++++++++ inventory/customers/ocserv-openspecimen/hosts | 32 +++++++++++++++++ site.yml | 36 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 inventory/customers/ocserv-openspecimen/group_vars/openspecimen.yml create mode 100644 inventory/customers/ocserv-openspecimen/hosts diff --git a/inventory/customers/ocserv-openspecimen/group_vars/openspecimen.yml b/inventory/customers/ocserv-openspecimen/group_vars/openspecimen.yml new file mode 100644 index 0000000000..90ec01966b --- /dev/null +++ b/inventory/customers/ocserv-openspecimen/group_vars/openspecimen.yml @@ -0,0 +1,15 @@ +--- +# ── Test profile: ocserv AnyConnect PoC target (Ubuntu 22.04, ADR-011) ─────── +# Same Debian/Ubuntu code path as the other local test profiles - the only +# thing this profile validates differently is reachability: this host is only +# reachable through an operator-connected VPN tunnel, not directly. See ./hosts +# for connection notes. +# +# openspecimen_release is NOT set here - always supplied at run time +# (Jenkins RELEASE_FILE dropdown, or -e openspecimen_release= on the CLI). + +# vpn_required tells site.yml's pre-flight play (ADR-011) to verify the target's +# SSH port answers before the main play starts, instead of failing deep inside +# the implicit "Gathering Facts" step with a generic timeout. It's purely a +# fail-fast signal - nothing here drives the VPN tunnel itself. +vpn_required: true diff --git a/inventory/customers/ocserv-openspecimen/hosts b/inventory/customers/ocserv-openspecimen/hosts new file mode 100644 index 0000000000..fba8084278 --- /dev/null +++ b/inventory/customers/ocserv-openspecimen/hosts @@ -0,0 +1,32 @@ +# ── VPN-gated test profile: ocserv AnyConnect PoC target (ADR-011) ────────── +# Validates the "reach a target only reachable through a VPN tunnel" shape +# from ADR-011 / .planning/design/011-vpn-connectivity-design.md (in the +# openspecimen-ansible repo). Unlike the other hosts-*.sample profiles, this +# target's firewall only allows SSH from the ocserv VPN server's own IP - it's +# only reachable while an operator has manually connected an openconnect +# tunnel wherever this playbook runs from. Ansible/Jenkins never brings the +# tunnel up itself - see infra-orbstack's vpn/ component and +# docs/vpn/ocserv-anyconnect-poc.md for how to connect it. +# +# ansible_host is the target's real OrbStack IP (not .orb.local). Re-check +# with `orbctl list` if the VM is ever recreated. +# +# Usage (after the operator has connected the VPN tunnel): +# ansible-playbook -i inventory/customers/ocserv-openspecimen/ site.yml \ +# -e mysql_db_password=localtest +# ───────────────────────────────────────────────────────────────────────────── + +[openspecimen] +ocserv-openspecimen ansible_host=192.168.139.125 ansible_user=ubuntu + +[all:vars] +ansible_python_interpreter = /usr/bin/python3 +ansible_ssh_private_key_file = ~/.orbstack/ssh/id_ed25519 + +# 60s (default 10s): this target's VPN tunnel sees materially higher round-trip +# latency than a direct/EICE connection - the default was tight enough to fail +# privilege escalation before the connection even finished setting up. Scoped to +# this customer only, not a global ansible.cfg change - direct/EICE customers +# should keep failing fast on a genuinely unreachable host. +ansible_timeout = 60 +ansible_become_timeout = 60 diff --git a/site.yml b/site.yml index f16d655763..7734bd3a81 100644 --- a/site.yml +++ b/site.yml @@ -25,6 +25,42 @@ # server.xml/context.xml over the shared conf - the reuse safety-net catches the # lost datasource and fails with guidance; re-onboard such a host. +# ── VPN-gated targets (ADR-011) - fail fast, not deep in the run ─────────────── +# vpn_required (customer group_vars, opt-in, default false) marks a target that's +# only reachable once an operator has manually connected a VPN tunnel on the +# controller - Ansible/Jenkins never drives the tunnel itself (see docs/vpn/). +# `connection: local` + `gather_facts: false` runs this BEFORE the main play's +# implicit fact-gathering would otherwise be the first thing to hit the target - +# without it, a down tunnel fails deep inside "Gathering Facts" with a generic +# SSH timeout instead of this play's explicit message. This only probes the raw +# TCP port (cheap, no SSH auth/become involved) - it catches "tunnel isn't up +# at all", not slow-but-connected auth/privilege-escalation, which the +# ansible_become_timeout override (see inventory/customers/*/hosts) covers. +- name: Pre-flight - verify VPN-gated targets are reachable + hosts: openspecimen + gather_facts: false + connection: local + tags: [always] + tasks: + - name: Wait for the target's SSH port to answer + ansible.builtin.wait_for: + host: "{{ ansible_host | default(inventory_hostname) }}" + port: "{{ ansible_port | default(22) }}" + timeout: "{{ vpn_preflight_timeout | default(15) }}" + when: vpn_required | default(false) | bool + register: _vpn_preflight + ignore_errors: true + + - name: Fail with guidance when the VPN-gated target is unreachable + ansible.builtin.fail: + msg: >- + Cannot reach {{ inventory_hostname }} ({{ ansible_host | default(inventory_hostname) }}:{{ ansible_port | default(22) }}). + vpn_required is set for this customer - confirm an operator has + connected the VPN tunnel on this controller, then re-run. See docs/vpn/. + when: + - vpn_required | default(false) | bool + - _vpn_preflight is failed + - name: Deploy OpenSpecimen hosts: openspecimen become: true