2026-02-28 22:27:58 -07:00
|
|
|
---
|
|
|
|
|
# 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
|
|
|
|
|
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
|
2026-03-01 00:44:21 -07:00
|
|
|
become: false
|
2026-02-28 22:27:58 -07:00
|
|
|
when: agent_deployed is changed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Play 3: Register Docker hosts in Portainer
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
- 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
|
|
|
|
|
register: existing_endpoints
|
|
|
|
|
delegate_to: localhost
|
2026-03-01 00:44:21 -07:00
|
|
|
become: false
|
2026-02-28 22:27:58 -07:00
|
|
|
run_once: false
|
|
|
|
|
|
|
|
|
|
- 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
|
|
|
|
|
register: portainer_enroll
|
|
|
|
|
delegate_to: localhost
|
2026-03-01 00:44:21 -07:00
|
|
|
become: false
|
2026-02-28 22:27:58 -07:00
|
|
|
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: Generate local report
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
- name: Generate Docker host discovery report
|
|
|
|
|
hosts: localhost
|
|
|
|
|
gather_facts: false
|
2026-03-01 00:44:21 -07:00
|
|
|
become: false
|
2026-02-28 22:27:58 -07:00
|
|
|
vars:
|
|
|
|
|
report_path: "{{ playbook_dir }}/../semaphore/reports/docker_hosts_{{ ansible_date_time.date }}.txt"
|
|
|
|
|
tasks:
|
|
|
|
|
|
|
|
|
|
- name: Ensure reports directory exists
|
|
|
|
|
ansible.builtin.file:
|
|
|
|
|
path: "{{ playbook_dir }}/../semaphore/reports"
|
|
|
|
|
state: directory
|
|
|
|
|
mode: "0755"
|
|
|
|
|
|
|
|
|
|
- name: Collect results from all hosts
|
|
|
|
|
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
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
- name: Write report to file
|
|
|
|
|
ansible.builtin.copy:
|
|
|
|
|
dest: "{{ report_path }}"
|
|
|
|
|
mode: "0644"
|
|
|
|
|
content: |
|
|
|
|
|
Docker Host Discovery Report
|
|
|
|
|
============================
|
|
|
|
|
Generated: {{ ansible_date_time.iso8601 }}
|
|
|
|
|
Portainer: {{ portainer_url }}
|
|
|
|
|
|
|
|
|
|
DOCKER HOSTS FOUND ({{ docker_hosts_found | length }})
|
|
|
|
|
{% for h in docker_hosts_found %}
|
|
|
|
|
- {{ h }}
|
|
|
|
|
IP: {{ hostvars[h].ansible_host | default('unknown') }}
|
|
|
|
|
Docker version: {{ hostvars[h].docker_version | default('unknown') }}
|
|
|
|
|
Portainer ID: {{ hostvars[h].portainer_endpoint_id | default('not enrolled') }}
|
|
|
|
|
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 %}
|
|
|
|
|
|
|
|
|
|
- name: Print report path
|
|
|
|
|
ansible.builtin.debug:
|
|
|
|
|
msg: "Report written to {{ report_path }}"
|
|
|
|
|
|
|
|
|
|
- name: Print Docker hosts summary
|
|
|
|
|
ansible.builtin.debug:
|
|
|
|
|
msg: |
|
|
|
|
|
Docker hosts found ({{ docker_hosts_found | length }}):
|
|
|
|
|
{% for h in docker_hosts_found %}
|
|
|
|
|
- {{ h }} ({{ hostvars[h].ansible_host | default('?') }}) Docker {{ hostvars[h].docker_version | default('?') }}
|
|
|
|
|
{% endfor %}
|