ansible-automation/playbooks/find_docker_enroll_portainer.yml

279 lines
9.6 KiB
YAML
Raw Normal View History

---
# 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
env:
AGENT_PORT: "9001"
# Ensures the agent's self-signed TLS cert covers the host's real IP,
# not just the Docker bridge (172.17.0.x) IP.
AGENT_HOST: "{{ ansible_host }}"
when: "'Up' not in (agent_status.stdout | default(''))"
register: agent_deployed
ignore_errors: true
- name: Wait for Portainer Agent to be ready
ansible.builtin.wait_for:
port: "{{ portainer_agent_port }}"
host: "127.0.0.1"
delay: 3
timeout: 30
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
ignore_errors: true
- name: Determine if this host is already enrolled
ansible.builtin.set_fact:
already_enrolled: >-
{{
(existing_endpoints.json | default([]))
| 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 }}"
TLS: "true"
TLSSkipVerify: "true"
TLSSkipClientVerify: "true"
status_code: [200, 201]
return_content: true
validate_certs: false
register: portainer_enroll
when: not already_enrolled
ignore_errors: true
- 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 is not failed and portainer_enroll.json is defined)
else (
(existing_endpoints.json | default([]))
| selectattr('Name', 'equalto', inventory_hostname)
| map(attribute='Id') | list | first | default('unknown') | string
)
}}
portainer_enrolled_now: "{{ portainer_enroll is changed and portainer_enroll is not failed }}"
# ---------------------------------------------------------------------------
# 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