playbook.ymlยถ

---
# Lab-only node provisioning for the simulated cluster; never a production
# role. Runs over ansible_connection: docker right after `compose up` (python +
# ansible + the CLI are baked into the node image via the distro's infinito
# image, so no .deb bootstrap is needed): waits for systemd, wires lab DNS
# (sys-hostname skips /etc/hosts on DOCKER_IN_CONTAINER hosts; the dnsmasq
# wildcard points the app domain at the manager's ingress), then unpacks the
# current repo onto every node. Host-side concerns (image build, compose up)
# live in scripts/tests/deploy/swarm/routine/01_bootstrap.sh.
- name: provision the simulated cluster nodes
  hosts: all
  gather_facts: false
  vars_files:
    - "{{ playbook_dir }}/../../group_vars/all/08_networks.yml" # nocheck: path-absolute - lab-only playbook, playbook_dir is stable and path_absolute plugin loading is fragile in a standalone -c docker run
  vars:
    lab_repo_root: "{{ playbook_dir }}/../.." # nocheck: path-absolute - see above
    lab_mgr: "{{ lookup('env', 'MGR') }}"
    lab_wrk1: "{{ lookup('env', 'WRK1') }}"
    lab_wrk2: "{{ lookup('env', 'WRK2') }}"
    lab_nfs: "{{ lookup('env', 'NFS_SERVER') }}"
    lab_backup: "{{ lookup('env', 'BACKUP_NODE') }}"
    lab_domain: "{{ lookup('env', 'INFINITO_DOMAIN') }}"
    lab_export_base: "{{ lookup('env', 'NFS_EXPORT_BASE') }}"
    lab_node_src: "{{ lookup('env', 'INFINITO_NODE_SRC_DIR') }}"
    lab_runner_temp: "{{ lookup('env', 'RUNNER_TEMP') }}"
    lab_sampler_log: "{{ lookup('env', 'INFINITO_DNS53_SAMPLER_LOG') }}"
    lab_systemd_timeout: "{{ lookup('env', 'SWARM_PILOT_SYSTEMD_TIMEOUT') | default('180', true) }}"
    lab_is_mgr: "{{ inventory_hostname == lab_mgr }}"
    lab_is_worker: "{{ inventory_hostname in [lab_wrk1, lab_wrk2] }}"
    lab_dns_node: "{{ lab_is_mgr or lab_is_worker or inventory_hostname == lab_nfs }}"
    lab_domain_target: "{{ lookup('env', 'INFINITO_SWARM_MGR_IP') }}"
    lab_filesystem: "{{ lookup('env', 'INFINITO_DOCKER_FILESYSTEM') }}"
    lab_filesystem_required: "{{ lookup('env', 'INFINITO_DOCKER_FILESYSTEM_REQUIRED') | default('false', true) }}"
    lab_dataroot_size: "{{ '60G' if lab_is_mgr else '30G' }}"
    lab_shared_cache: "{{ lookup('env', 'INFINITO_CACHE_SHARED_HOST_PATH') }}"
    lab_host_pacman_cache: "{{ lookup('env', 'INFINITO_CACHE_HOST_PACMAN_PATH') }}"
    lab_no_proxy: "{{ (NETWORK_LOOPBACK_HOSTS + ['infinito-registry-cache', lab_mgr, lab_wrk1, lab_wrk2, lab_nfs, lab_backup, 'gcr.io', 'storage.googleapis.com',
      'googleusercontent.com']) | join(',') }}"
  tasks:
    - name: require the lab manager IP (lookup('env') yields '' for a missing key, and an empty dnsmasq address= is valid syntax)
      assert:
        that: lab_domain_target | length > 0
        fail_msg: INFINITO_SWARM_MGR_IP is empty; it comes from scripts/tests/deploy/swarm/utils/topology/base.sh
      run_once: true

    - block:
        - name: wait for systemd ready (status from stdout; rc is nonzero when degraded)
          command: systemctl is-system-running
          timeout: 15
          register: lab_systemd
          until: lab_systemd.stdout is search('running|degraded|starting|maintenance')
          failed_when: lab_systemd.stdout is not search('running|degraded|starting|maintenance')
          retries: "{{ lab_systemd_timeout }}"
          delay: 1
          changed_when: false
      rescue:
        - name: dump docker logs
          command:
            argv: [docker, logs, "{{ inventory_hostname }}"]
          timeout: 30
          delegate_to: localhost
          ignore_errors: true
          changed_when: false
        - name: dump journal + pending jobs
          command: "{{ item }}"
          loop:
            - journalctl --no-pager -n 50
            - systemctl list-jobs --no-pager
          timeout: 30
          ignore_errors: true
          changed_when: false
        - name: dump container state
          command:
            argv: [docker, inspect, --format, "{{ '{{json .State}}' }}", "{{ inventory_hostname }}"]
          timeout: 30
          delegate_to: localhost
          ignore_errors: true
          changed_when: false
        - name: fail the bring-up
          fail:
            msg: "{{ inventory_hostname }} systemd did not become responsive within {{ lab_systemd_timeout }}s"

    - name: permit user logins (degraded lab boot never clears /run/nologin)
      systemd:
        name: systemd-user-sessions
        state: started
      timeout: 30

    - name: probe the pacman cache script (mounted by shared-cache.override.yml, absent without a shared cache folder)
      stat:
        path: /usr/local/bin/pacman-cache.sh
      timeout: 30
      register: lab_pacman_cache_script

    - name: layer the node pacman caches before the first install, host cache read-only first, the node download dir second and the shared cache third
      command:
        argv:
          - /usr/local/bin/pacman-cache.sh
          - /etc/pacman.conf
          - "{{ lab_shared_cache }}"
          - "{{ lab_host_pacman_cache }}"
      timeout: 60
      when: lab_pacman_cache_script.stat.exists
      changed_when: true

    - name: put the node docker data root on the stated filesystem (before anything is pulled)
      script: >-
        {{ lab_repo_root }}/scripts/tests/deploy/utils/filesystem/docker_dataroot.sh
        {{ lab_filesystem }} {{ lab_filesystem_required }} {{ lab_dataroot_size }} {{ inventory_hostname }}
      timeout: 600
      when: lab_filesystem | length > 0
      changed_when: true
      register: lab_dataroot

    - name: surface the node's effective data root filesystem
      debug:
        msg: "{{ lab_dataroot.stdout_lines | select('search', '^docker-dataroot-filesystem: status=') | list }}"
      when: not (lab_dataroot is skipped)

    - name: probe the registry-cache CA (mounted by cache.override.yml, absent without a local cache stack)
      stat:
        path: /opt/registry-cache-ca/ca.crt
      timeout: 30
      register: lab_registry_ca

    - name: wire the node docker daemon to the host registry cache
      include_tasks: registry_cache.yml
      when: lab_registry_ca.stat.exists

    - name: wipe stale NFS export state (as root in-node; the act runner cannot)
      command: find {{ lab_export_base }} -mindepth 1 -delete
      timeout: 120
      when: inventory_hostname == lab_nfs
      changed_when: true

    - name: render the app-domain hosts entries
      command: python3 -m utils.tests.swarm.write.hosts_entries
      args:
        chdir: "{{ lab_repo_root }}"
      environment:
        HOSTS_ENTRY_IP: "{{ lab_domain_target }}"
      timeout: 60
      register: lab_hosts_entries
      when: lab_is_mgr or inventory_hostname in [lab_nfs, lab_backup]
      delegate_to: localhost
      changed_when: false

    - name: seed /etc/hosts with the app domains
      blockinfile:
        path: /etc/hosts
        marker: "# {mark} INFINITO LAB DOMAINS"
        block: "{{ lab_hosts_entries.stdout }}"
        unsafe_writes: true
      timeout: 30
      when: lab_is_mgr or inventory_hostname in [lab_nfs, lab_backup]

    - name: own the whole dnsmasq config (the packaging appends --local-service unconditionally and only an interface, except-interface, listen-address or auth-server directive resets it; without that reset a failed interface re-enumeration empties dnsmasq's local-subnet list and it rejects every query, loopback included, until an address event rebuilds it)
      template: # nocheck: stack-host-template - lab node system config, every DNS node writes its own /etc/dnsmasq.conf and IS_STACK_HOST does not exist in this play
        src: dnsmasq.conf.j2
        dest: /etc/dnsmasq.conf
      timeout: 30
      when: lab_dns_node

    - name: enable and restart dnsmasq (arch is Type=dbus, so systemd waits its full 90s DefaultTimeoutStartSec before reporting)
      systemd:
        name: dnsmasq
        enabled: true
        state: restarted
      timeout: 120
      when: lab_dns_node

    - name: point resolv.conf at dnsmasq
      copy:
        dest: /etc/resolv.conf
        content: "nameserver 127.0.0.1\n"
        unsafe_writes: true
      timeout: 30
      when: lab_dns_node

    - name: verify the app domain resolves to its lab target
      command: getent ahosts {{ ('test.' ~ lab_domain) if lab_is_worker else lab_domain }}
      timeout: 10
      register: lab_resolve
      until: lab_resolve.stdout is search('^' ~ (lab_domain_target | regex_escape) ~ ' ', multiline=True)
      retries: 30
      delay: 1
      changed_when: false
      when: lab_dns_node

    - name: pack the repo once on the controller
      command: tar -C {{ lab_repo_root }} -cf {{ lab_runner_temp }}/infinito-src.tar .
      timeout: 300
      delegate_to: localhost
      run_once: true
      changed_when: true

    - name: create the node source dir
      file:
        path: "{{ lab_node_src }}"
        state: directory
        mode: "0755"
      timeout: 30

    - name: unpack the repo onto the node
      unarchive:
        src: "{{ lab_runner_temp }}/infinito-src.tar"
        dest: "{{ lab_node_src }}"
      timeout: 300

    - name: sample what the resolver answers, not just whether it listens
      shell: |
        set -eu
        sampler={{ lab_node_src }}/scripts/tests/deploy/ci/dns53-sampler.sh
        INFINITO_DNS53_SAMPLER_LOG={{ lab_sampler_log }} \
          setsid sh "${sampler}" </dev/null >/dev/null 2>&1 &
      args:
        creates: "{{ lab_sampler_log }}"
      timeout: 30
      when: lab_dns_node