ansible-automation/playbooks/find_docker_enroll_portainer.yml
sam 6db20117fd Eliminate localhost tasks to fix sudo issue on Semaphore runner
- Play 3: Run Portainer API calls from remote hosts directly (no
  delegate_to: localhost). Add validate_certs: false for self-signed cert.
- Play 4: Replace localhost file report with debug output using run_once.
  No filesystem writes = no privilege escalation needed on the runner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 01:06:17 -07:00

273 lines
9.2 KiB
YAML

---
# find_docker_enroll_portainer.yml
#
# Discovers which hosts in the inventory are running the Docker engine,
# deploys the Portainer Agent container on each Docker host, registers the
# host as a new environment in Portainer, and writes a local report.
#
# Required variables (set in group_vars/all.yml or Semaphore extra-vars):
# portainer_url - e.g. http://10.40.40.2:9000
# portainer_api_token - Portainer API token (Settings → Users → API key)
# portainer_agent_port - defaults to 9001
#
# Usage:
# ansible-playbook -i semaphore/inventory/hosts.yml \
# playbooks/find_docker_enroll_portainer.yml
#
# From Semaphore: point at this playbook, select the hosts.yml inventory,
# and add portainer_api_token as an extra-var or in group_vars/all.yml.
- name: Discover Docker hosts and collect facts
hosts: all
gather_facts: false
ignore_unreachable: true
tasks:
- name: Test SSH connectivity
ansible.builtin.wait_for_connection:
timeout: 10
register: ssh_check
become: false
ignore_errors: true
- name: Gather minimal facts for reachable hosts
ansible.builtin.setup:
gather_subset:
- "!all"
- network
- distribution
when: ssh_check is succeeded
ignore_errors: true
- name: Check if Docker binary is present
ansible.builtin.command: which docker
register: docker_which
changed_when: false
failed_when: false
when: ssh_check is succeeded
- name: Check if Docker daemon is responding
ansible.builtin.command: docker info --format '{% raw %}{{json .ServerVersion}}{% endraw %}'
register: docker_info
changed_when: false
failed_when: false
become: true
when:
- ssh_check is succeeded
- docker_which.rc is defined
- docker_which.rc == 0
- name: Record Docker status as host fact
ansible.builtin.set_fact:
docker_running: >-
{{
docker_which.rc is defined and docker_which.rc == 0 and
docker_info.rc is defined and docker_info.rc == 0
}}
docker_version: >-
{{
(docker_info.stdout | default('""') | from_json)
if (docker_info.rc is defined and docker_info.rc == 0)
else 'not running'
}}
when: ssh_check is succeeded
- name: Mark unreachable hosts
ansible.builtin.set_fact:
docker_running: false
ssh_reachable: false
when: ssh_check is failed or ssh_check is skipped
# ---------------------------------------------------------------------------
# Play 2: Deploy Portainer Agent on Docker hosts
# ---------------------------------------------------------------------------
- name: Deploy Portainer Agent on Docker hosts
hosts: all
gather_facts: false
ignore_unreachable: true
become: true
vars:
portainer_agent_port: "{{ portainer_agent_port | default(9001) }}"
tasks:
- name: Skip hosts without Docker
ansible.builtin.meta: end_host
when: not (docker_running | default(false))
- name: Check if portainer_agent container already exists
ansible.builtin.command: >
docker ps -a --filter name=portainer_agent --format "{% raw %}{{.Status}}{% endraw %}"
register: agent_status
changed_when: false
failed_when: false
- name: Pull Portainer Agent image
community.docker.docker_image:
name: portainer/agent
tag: latest
source: pull
when: "'Up' not in (agent_status.stdout | default(''))"
- name: Deploy Portainer Agent container
community.docker.docker_container:
name: portainer_agent
image: portainer/agent:latest
state: started
restart_policy: always
ports:
- "{{ portainer_agent_port }}:9001"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
env:
AGENT_PORT: "9001"
when: "'Up' not in (agent_status.stdout | default(''))"
register: agent_deployed
- name: Wait for Portainer Agent to be ready
ansible.builtin.wait_for:
port: "{{ portainer_agent_port }}"
host: "{{ ansible_host }}"
delay: 3
timeout: 30
delegate_to: localhost
become: false
when: agent_deployed is changed
# ---------------------------------------------------------------------------
# Play 3: Register Docker hosts in Portainer
# ---------------------------------------------------------------------------
# Runs the Portainer API calls FROM each remote host (not delegate_to localhost)
# to avoid privilege escalation issues on the Semaphore runner.
# validate_certs: false needed for Portainer's self-signed cert.
- name: Register Docker hosts in Portainer
hosts: all
gather_facts: false
ignore_unreachable: true
vars:
portainer_agent_port: "{{ portainer_agent_port | default(9001) }}"
tasks:
- name: Skip hosts without Docker
ansible.builtin.meta: end_host
when: not (docker_running | default(false))
- name: Check if endpoint already exists in Portainer
ansible.builtin.uri:
url: "{{ portainer_url }}/api/endpoints"
method: GET
headers:
X-API-Key: "{{ portainer_api_token }}"
return_content: true
status_code: 200
validate_certs: false
register: existing_endpoints
- name: Determine if this host is already enrolled
ansible.builtin.set_fact:
already_enrolled: >-
{{
existing_endpoints.json
| selectattr('Name', 'equalto', inventory_hostname)
| list | length > 0
}}
- name: Register host as Portainer Agent endpoint
ansible.builtin.uri:
url: "{{ portainer_url }}/api/endpoints"
method: POST
headers:
X-API-Key: "{{ portainer_api_token }}"
body_format: form-multipart
body:
Name: "{{ inventory_hostname }}"
EndpointCreationType: "2"
URL: "tcp://{{ ansible_host }}:{{ portainer_agent_port }}"
status_code: [200, 201]
return_content: true
validate_certs: false
register: portainer_enroll
when: not already_enrolled
- name: Store enrollment result
ansible.builtin.set_fact:
portainer_endpoint_id: >-
{{
(portainer_enroll.json.Id | string)
if (portainer_enroll is not skipped and portainer_enroll.json is defined)
else (
existing_endpoints.json
| selectattr('Name', 'equalto', inventory_hostname)
| map(attribute='Id') | list | first | string
)
}}
portainer_enrolled_now: "{{ portainer_enroll is changed }}"
# ---------------------------------------------------------------------------
# Play 4: Print discovery report to Semaphore output
# ---------------------------------------------------------------------------
# Runs on all hosts but only executes once (run_once: true per task).
# Uses debug instead of file I/O to avoid any privilege issues on the runner.
- name: Print Docker host discovery report
hosts: all
gather_facts: false
tasks:
- name: Collect discovery results
ansible.builtin.set_fact:
docker_hosts_found: >-
{{
hostvars | dict2items
| selectattr('value.docker_running', 'defined')
| selectattr('value.docker_running', 'equalto', true)
| map(attribute='key') | list | sort
}}
unreachable_hosts: >-
{{
hostvars | dict2items
| selectattr('value.ssh_reachable', 'defined')
| selectattr('value.ssh_reachable', 'equalto', false)
| map(attribute='key') | list | sort
}}
no_docker_hosts: >-
{{
hostvars | dict2items
| selectattr('value.docker_running', 'defined')
| selectattr('value.docker_running', 'equalto', false)
| rejectattr('value.ssh_reachable', 'equalto', false)
| map(attribute='key') | list | sort
}}
run_once: true
- name: Print discovery report
ansible.builtin.debug:
msg: |
============================================================
Docker Host Discovery Report — {{ portainer_url }}
============================================================
DOCKER FOUND ({{ docker_hosts_found | length }}):
{% for h in docker_hosts_found %}
{{ h }} ({{ hostvars[h].ansible_host | default('?') }})
Docker: {{ hostvars[h].docker_version | default('?') }}
Portainer: ID={{ hostvars[h].portainer_endpoint_id | default('n/a') }} enrolled_now={{ hostvars[h].portainer_enrolled_now | default(false) }}
{% endfor %}
NO DOCKER ({{ no_docker_hosts | length }}):
{% for h in no_docker_hosts %}
{{ h }} ({{ hostvars[h].ansible_host | default('?') }})
{% endfor %}
UNREACHABLE ({{ unreachable_hosts | length }}):
{% for h in unreachable_hosts %}
{{ h }} ({{ hostvars[h].ansible_host | default('?') }})
{% endfor %}
============================================================
run_once: true