Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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=<name> 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
32 changes: 32 additions & 0 deletions inventory/customers/ocserv-openspecimen/hosts
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down