---
# ============================================================================
# generate-exec-node-bundle.yml
# ============================================================================
# Run this ON the controller node (as the AAP service user, e.g. ec2-user).
# It generates a self-contained tarball with everything needed to set up
# a receptor execution/hop node in a disconnected environment.
#
# Place this file in the AAP installer bundle directory alongside setup.sh
#
# Usage:
#   ansible-playbook generate-exec-node-bundle.yml \
#     -e exec_node_hostname=exec-node-1.example.com \
#     -e exec_node_type=execution \
#     -e controller_hostname=$(hostname -f)
#
# Optional overrides:
#   -e receptor_port=27199
#   -e peering_direction=exec_to_controller   # or controller_to_exec
#   -e include_ee_images=true
# ============================================================================

- name: "Generate execution/hop node bundle"
  hosts: localhost
  connection: local
  gather_facts: true

  vars:
    # ---- Required (pass via -e) ----
    # exec_node_hostname: exec-node-1.example.com
    # controller_hostname: controller.example.com
    # exec_node_type: execution  # or 'hop'

    # ---- Optional (sensible defaults) ----
    receptor_port: 27199
    receptor_protocol: "tcp"
    receptor_log_level: "info"
    receptor_mintls13: false
    receptor_disable_tls: false
    receptor_disable_signing: false
    # exec_to_controller = exec node dials out to controller (preferred for hop to controller for disconnected)
    # controller_to_exec = controller dials out to exec node (exec node must be reachable from controller)
    # exec_to_hop = exec dials out to hop node (preferred for disconnected)
    peering_direction: "exec_to_controller"
    include_ee_images: true

    # Certificate subject (matches AAP installer defaults)
    cert_country: "US"
    cert_state: "North Carolina"
    cert_locality: "Raleigh"
    cert_org: "Red Hat"
    cert_ou: "Ansible"

    # Paths on the controller (AAP 2.6 containerized defaults)
    # _ca_tls_dir is set by common/tasks/facts.yml to ~/aap/tls
    aap_base_dir: "{{ ansible_user_dir }}/aap"
    ca_tls_dir: "{{ aap_base_dir }}/tls"
    # receptor_conf_dir is where all receptor config + certs live on the controller host
    receptor_conf_dir: "{{ aap_base_dir }}/receptor/etc"

    # Where to stage the bundle
    bundle_staging_dir: "/tmp/receptor-bundle-{{ exec_node_hostname }}"
    bundle_output: "{{ ansible_user_dir }}/receptor-bundle-{{ exec_node_hostname }}.tar.gz"

    # Receptor image — auto-detected from running container, or override
    receptor_image: "registry.redhat.io/ansible-automation-platform-26/receptor-rhel9:latest"

    # Container log driver (must match what the exec node will use)
    container_log_driver: "journald"

  pre_tasks:

    - name: Validate required variables
      ansible.builtin.assert:
        that:
          - exec_node_hostname is defined and exec_node_hostname | length > 0
          - controller_hostname is defined and controller_hostname | length > 0
          - exec_node_type in ['execution', 'hop']
        fail_msg: >-
          Required variables: exec_node_hostname, controller_hostname,
          exec_node_type (execution|hop)

    - name: Validate mesh CA exists
      ansible.builtin.stat:
        path: "{{ item }}"
      register: _ca_files
      loop:
        - "{{ ca_tls_dir }}/ca.key"
        - "{{ ca_tls_dir }}/ca.cert"

    - name: Fail if mesh CA is missing
      ansible.builtin.fail:
        msg: "Mesh CA not found at {{ ca_tls_dir }}. Is this the AAP controller node?"
      when: _ca_files.results | selectattr('stat.exists', 'equalto', false) | list | length > 0

    - name: Validate signing public key exists
      ansible.builtin.stat:
        path: "{{ receptor_conf_dir }}/signing_public.pem"
      register: _signing_key_stat

    - name: Fail if signing public key is missing
      ansible.builtin.fail:
        msg: >-
          Work signing public key not found at {{ receptor_conf_dir }}/signing_public.pem.
          Ensure the AAP installer has completed successfully on this controller.
      when: not _signing_key_stat.stat.exists

    - name: Detect receptor image from running container
      ansible.builtin.command:
        cmd: podman inspect receptor --format "{{ '{{' }}.ImageName{{ '}}' }}"
      register: _running_receptor_image
      changed_when: false
      failed_when: false

    - name: Use detected receptor image if available
      ansible.builtin.set_fact:
        receptor_image: "{{ _running_receptor_image.stdout | trim }}"
      when:
        - _running_receptor_image.rc == 0
        - _running_receptor_image.stdout | trim | length > 0

    - name: Show receptor image that will be used
      ansible.builtin.debug:
        msg: "Using receptor image: {{ receptor_image }}"

  tasks:

    # ------------------------------------------------------------------
    # 1. Create staging directory
    # ------------------------------------------------------------------
    - name: Create bundle staging directory
      ansible.builtin.file:
        path: "{{ bundle_staging_dir }}"
        state: directory
        mode: '0700'

    - name: Create bundle subdirectories
      ansible.builtin.file:
        path: "{{ bundle_staging_dir }}/{{ item }}"
        state: directory
        mode: '0755'
      loop:
        - certs
        - images

    # ------------------------------------------------------------------
    # 2. Generate TLS certificate for the execution node
    #    Replicates roles/receptor/tasks/tls.yml from the installer.
    #    The cert is signed with the mesh CA (ownca provider) so that
    #    mutual TLS within the receptor mesh works out of the box.
    # ------------------------------------------------------------------
    - name: Ensure python3-cryptography is installed (required for cert generation)
      ansible.builtin.package:
        name: python3-cryptography
        state: present
      become: true

    - name: Generate execution node private key
      community.crypto.openssl_privatekey:
        path: "{{ bundle_staging_dir }}/certs/receptor.key"
        mode: '0400'

    - name: Generate CSR for execution node
      community.crypto.openssl_csr_pipe:
        common_name: "{{ exec_node_hostname }}"
        country_name: "{{ cert_country }}"
        state_or_province_name: "{{ cert_state }}"
        locality_name: "{{ cert_locality }}"
        organization_name: "{{ cert_org }}"
        organizational_unit_name: "{{ cert_ou }}"
        subject_alt_name:
          # DNS or IP SAN — use the same filter as the installer
          - "{{ exec_node_hostname | ansible.containerized_installer.subject_alt_name }}"
          # Receptor-specific OID for node identity (required by AAP mesh)
          - "otherName:1.3.6.1.4.1.2312.19.1;UTF8:{{ exec_node_hostname }}"
        privatekey_path: "{{ bundle_staging_dir }}/certs/receptor.key"
      register: _receptor_tls_csr

    - name: Sign execution node certificate with mesh CA
      community.crypto.x509_certificate:
        path: "{{ bundle_staging_dir }}/certs/receptor.crt"
        csr_content: "{{ _receptor_tls_csr.csr }}"
        privatekey_path: "{{ bundle_staging_dir }}/certs/receptor.key"
        provider: ownca
        ownca_path: "{{ ca_tls_dir }}/ca.cert"
        ownca_privatekey_path: "{{ ca_tls_dir }}/ca.key"
        mode: '0640'

    - name: Copy mesh CA certificate into bundle
      ansible.builtin.copy:
        src: "{{ ca_tls_dir }}/ca.cert"
        dest: "{{ bundle_staging_dir }}/certs/mesh-CA.crt"
        mode: '0644'
        remote_src: true

    - name: Copy work signing public key into bundle
      ansible.builtin.copy:
        # signing_public.pem lives in receptor_conf_dir (~/aap/receptor/etc/)
        # alongside receptor.conf — this is where the installer places it
        src: "{{ receptor_conf_dir }}/signing_public.pem"
        dest: "{{ bundle_staging_dir }}/certs/signing_public.pem"
        mode: '0644'
        remote_src: true

    # ------------------------------------------------------------------
    # 3. Copy the CA trust extracted directory into the bundle
    #    The receptor container needs this mounted at
    #    /etc/pki/ca-trust/extracted so it trusts the mesh CA.
    #    This is the same extracted/ dir produced by update-ca-trust on
    #    the controller, mounted from ~/aap/tls/extracted.
    # ------------------------------------------------------------------
    - name: Archive CA trust extracted directory for bundle
      ansible.builtin.command:
        cmd: >-
          tar -czf {{ bundle_staging_dir }}/ca-trust-extracted.tar.gz
          -C {{ ca_tls_dir }}
          extracted
      changed_when: true

    # ------------------------------------------------------------------
    # 4. Query the AAP database for the controller's hostname and canonical
    #    receptor address. This must happen before receptor.conf is rendered
    #    because the peer address in the config must match the IP/hostname
    #    that the controller's TLS cert SAN was issued for.
    # ------------------------------------------------------------------
    - name: "DB: Detect controller hostname and canonical receptor address"
      ansible.builtin.command:
        cmd: >-
          podman exec automation-controller-task
          awx-manage shell -c
          "from awx.main.models import Instance, ReceptorAddress;
          i = Instance.objects.filter(node_type__in=['control','hybrid']).first();
          addr = ReceptorAddress.objects.filter(instance=i, canonical=True).first();
          print(i.hostname);
          print(addr.address);
          print(addr.port)"
      register: _controller_db_info
      changed_when: false

    - name: Set controller DB facts
      ansible.builtin.set_fact:
        # hostname is what the DB uses to identify the instance (used in register_peers)
        _controller_db_hostname_str: "{{ _controller_db_info.stdout_lines[0] | trim }}"
        # canonical receptor address is what the TLS cert was issued for — use this
        # as the peer address in receptor.conf so TLS verification succeeds.
        # This is typically the IP (ansible_host) rather than the FQDN, because the
        # installer uses routable_hostname | default(ansible_host) for the cert SAN.
        _controller_receptor_address: "{{ _controller_db_info.stdout_lines[1] | trim }}"
        _controller_receptor_port: "{{ _controller_db_info.stdout_lines[2] | trim }}"

    - name: Display controller receptor details
      ansible.builtin.debug:
        msg: >-
          Controller DB hostname: {{ _controller_db_hostname_str }}
          Canonical receptor address: {{ _controller_receptor_address }}:{{ _controller_receptor_port }}
          (peer address in receptor.conf will use the canonical address to match the TLS cert SAN)

    # ------------------------------------------------------------------
    # 5. Generate receptor.conf using the official installer template
    #    (roles/receptor/templates/receptor.conf.j2).
    #
    #    The template uses two Ansible magic variables that cannot be
    #    overridden with set_fact — they are always resolved from the live
    #    inventory:
    #
    #      groups['automationcontroller'] — length check decides whether to
    #        emit 'local-only:' (single node, no exec_nodes) or a listener.
    #        We always need a listener, so we add a second placeholder host
    #        to automationcontroller so length > 1.
    #
    #      hostvars[node] — used in the peers loop to get _receptor_hostname,
    #        _receptor_protocol, _receptor_port for each peer.
    #        We add the controller as a real in-memory inventory host with
    #        those vars set so hostvars resolves correctly.
    #
    #    add_host puts both into the live inventory for this play run only.
    #    Nothing is written to disk; it is ephemeral.
    # ------------------------------------------------------------------
    - name: Add controller host to in-memory inventory for template rendering
      ansible.builtin.add_host:
        name: "{{ _controller_db_hostname_str }}"
        groups:
          - automationcontroller
        # Use the canonical receptor address (IP) — this is what the controller's
        # TLS cert SAN was issued for, so TLS verification on the exec node succeeds.
        _receptor_hostname: "{{ _controller_receptor_address }}"
        _receptor_protocol: "{{ receptor_protocol }}"
        _receptor_port: "{{ _controller_receptor_port }}"

    - name: Add placeholder to automationcontroller group (ensures listener stanza)
      ansible.builtin.add_host:
        name: "__aap_placeholder__"
        groups:
          - automationcontroller

    - name: Set template rendering variables
      ansible.builtin.set_fact:
        # Node identity — mirrors what the installer sets in receptor/tasks/facts.yml.
        # The template uses BOTH receptor_type (role default var) and _receptor_type
        # (computed fact from facts.yml), so both must be set.
        receptor_type: "{{ exec_node_type }}"
        _receptor_hostname: "{{ exec_node_hostname }}"
        _receptor_type: "{{ exec_node_type }}"
        _receptor_port: "{{ receptor_port }}"
        _receptor_protocol: "{{ receptor_protocol }}"
        # exec_to_controller: exec node dials the controller's canonical receptor address.
        # controller_to_exec: exec node only listens → no outbound peers.
        _receptor_peers: "{{ [_controller_db_hostname_str] if peering_direction == 'exec_to_controller' else [] }}"

    - name: Generate receptor.conf from installer template
      ansible.builtin.template:
        src: "{{ playbook_dir }}/collections/ansible_collections/ansible/containerized_installer/roles/receptor/templates/receptor.conf.j2"
        dest: "{{ bundle_staging_dir }}/receptor.conf"
        mode: '0644'

    - name: "DB: Provision the {{ exec_node_type }} instance"
      ansible.builtin.command:
        cmd: >-
          podman exec automation-controller-task
          awx-manage provision_instance
          --hostname={{ exec_node_hostname }}
          --node_type={{ exec_node_type }}
          {% if exec_node_uuid is defined and exec_node_uuid | length > 0 %}--uuid={{ exec_node_uuid }}{% endif %}
      register: _provision
      changed_when: "'(changed: True)' in _provision.stdout"

    - name: "DB: Register receptor listener address for exec node"
      ansible.builtin.command:
        cmd: >-
          podman exec automation-controller-task
          awx-manage add_receptor_address
          --instance={{ exec_node_hostname }}
          --address={{ exec_node_hostname }}
          --port={{ receptor_port }}
          --protocol={{ receptor_protocol }}
          --canonical
          {% if peering_direction == 'exec_to_controller' %}--peers_from_control_nodes{% endif %}
      register: _receptor_addr
      changed_when: "'(changed: True)' in _receptor_addr.stdout"

    - name: "DB: Register peering — controller dials exec node"
      ansible.builtin.command:
        cmd: >-
          podman exec automation-controller-task
          awx-manage register_peers {{ _controller_db_hostname_str }}
          --peers {{ exec_node_hostname }}
      register: _register_peers
      changed_when: true
      when: peering_direction == 'controller_to_exec'

    - name: "DB: Add exec node to default instance group"
      ansible.builtin.command:
        cmd: >-
          podman exec automation-controller-task
          awx-manage register_queue
          --queuename=default
          --hostnames={{ exec_node_hostname }}
      register: _queue_default
      changed_when: "'(changed: True)' in _queue_default.stdout"
      when: exec_node_type == 'execution'

    - name: "DB: Add exec node to executionplane instance group"
      ansible.builtin.command:
        cmd: >-
          podman exec automation-controller-task
          awx-manage register_queue
          --queuename=executionplane
          --hostnames={{ exec_node_hostname }}
      register: _queue_exec
      changed_when: "'(changed: True)' in _queue_exec.stdout"
      when: exec_node_type == 'execution'

    - name: "DB: Verify instance registered"
      ansible.builtin.command:
        cmd: >-
          podman exec automation-controller-task
          awx-manage shell -c
          "from awx.main.models import Instance;
          i = Instance.objects.get(hostname='{{ exec_node_hostname }}');
          print(f'hostname={i.hostname} type={i.node_type} enabled={i.enabled}')"
      register: _verify
      changed_when: false

    - name: Display registered instance details
      ansible.builtin.debug:
        msg: "{{ _verify.stdout }}"

    # ------------------------------------------------------------------
    # 6. Copy receptor container image from the installer bundle
    #    The bundle/images/ dir already has the pre-built .tar.gz files.
    # ------------------------------------------------------------------
    - name: Set installer bundle images path
      ansible.builtin.set_fact:
        _installer_bundle_images: "{{ playbook_dir }}/bundle/images"

    - name: Check for bundled receptor image
      ansible.builtin.stat:
        path: "{{ _installer_bundle_images }}/receptor-rhel9.tar.gz"
      register: _bundled_receptor

    - name: Copy receptor image from installer bundle
      ansible.builtin.copy:
        src: "{{ _installer_bundle_images }}/receptor-rhel9.tar.gz"
        dest: "{{ bundle_staging_dir }}/images/receptor-rhel9.tar.gz"
        remote_src: true
      when: _bundled_receptor.stat.exists

    - name: Fall back to podman save if bundled image not found
      ansible.builtin.shell:
        cmd: "podman save {{ receptor_image }} | gzip > {{ bundle_staging_dir }}/images/receptor-rhel9.tar.gz"
      when: not _bundled_receptor.stat.exists
      changed_when: true

    # ------------------------------------------------------------------
    # 7. Copy EE images for execution nodes
    #    Hop nodes do not execute jobs so they don't need EE images.
    # ------------------------------------------------------------------
    - name: Copy EE images from installer bundle
      ansible.builtin.copy:
        src: "{{ _installer_bundle_images }}/{{ item }}"
        dest: "{{ bundle_staging_dir }}/images/{{ item }}"
        remote_src: true
      loop:
        - ee-minimal-rhel9.tar.gz
        - ee-supported-rhel9.tar.gz
      when:
        - include_ee_images | bool
        - exec_node_type == 'execution'
      failed_when: false

    # ------------------------------------------------------------------
    # 8. Copy the install playbook into the bundle
    # ------------------------------------------------------------------
    - name: Copy install-exec-node.yml into bundle
      ansible.builtin.copy:
        src: "{{ playbook_dir }}/install-exec-node.yml"
        dest: "{{ bundle_staging_dir }}/install-exec-node.yml"
        mode: '0644'
        remote_src: true

    # ------------------------------------------------------------------
    # 9. Generate inventory.yml for the execution node
    # ------------------------------------------------------------------
    - name: Generate inventory.yml for execution node
      ansible.builtin.copy:
        dest: "{{ bundle_staging_dir }}/inventory.yml"
        mode: '0644'
        content: |
          ---
          # Inventory for receptor node installation.
          # Generated by generate-exec-node-bundle.yml
          #
          # By default this runs locally on the exec node itself.
          # To run from a jump host, set ansible_host to the exec node IP/FQDN,
          # set ansible_connection: ssh, and provide ansible_user / ssh key.
          all:
            hosts:
              {{ exec_node_hostname }}:
                ansible_host: localhost
                ansible_connection: local
                # ---- Uncomment for remote execution ----
                # ansible_host: {{ exec_node_hostname }}
                # ansible_connection: ssh
                # ansible_user: ec2-user
                # ansible_ssh_private_key_file: ~/.ssh/your-key.pem

            vars:
              # Node identity
              exec_node_hostname: "{{ exec_node_hostname }}"
              exec_node_type: "{{ exec_node_type }}"
              controller_hostname: "{{ controller_hostname }}"

              # Receptor settings
              receptor_port: {{ receptor_port }}
              receptor_protocol: "{{ receptor_protocol }}"
              receptor_image: "{{ receptor_image }}"
              container_log_driver: "{{ container_log_driver }}"

              # Paths resolved at install time (set to playbook_dir automatically)
              bundle_dir: "{{ '{{' }} playbook_dir {{ '}}' }}"

    # ------------------------------------------------------------------
    # 10. Create the tarball
    # ------------------------------------------------------------------
    - name: Create bundle tarball
      ansible.builtin.command:
        cmd: >-
          tar -czf {{ bundle_output }}
          -C /tmp
          receptor-bundle-{{ exec_node_hostname }}
      changed_when: true

    # ------------------------------------------------------------------
    # 11. Cleanup staging directory
    # ------------------------------------------------------------------
    - name: Remove staging directory
      ansible.builtin.file:
        path: "{{ bundle_staging_dir }}"
        state: absent

    # ------------------------------------------------------------------
    # Summary
    # ------------------------------------------------------------------
    - name: Display bundle summary
      ansible.builtin.debug:
        msg: |
          ===============================================================
          Bundle generated successfully
          ===============================================================

          Tarball : {{ bundle_output }}
          Node    : {{ exec_node_hostname }} ({{ exec_node_type }})
          DB      : {{ _verify.stdout }}
          Image   : {{ receptor_image }}

          Bundle contents:
            certs/receptor.crt          TLS certificate (signed by mesh CA)
            certs/receptor.key          TLS private key
            certs/mesh-CA.crt           Mesh CA certificate (mutual TLS trust)
            certs/signing_public.pem    Work signing public key
            ca-trust-extracted.tar.gz   CA trust bundle (mounted into container)
            receptor.conf               Receptor configuration
            images/receptor-rhel9.tar.gz  Receptor container image
            install-exec-node.yml       Installation playbook
            inventory.yml               Ansible inventory

          Next steps on the execution node:
            1. Transfer {{ bundle_output | basename }} to the execution node
            2. tar -xzf {{ bundle_output | basename }}
            3. cd receptor-bundle-{{ exec_node_hostname }}/
            4. ansible-playbook install-exec-node.yml -i inventory.yml
          ===============================================================
