ansible-automation/playbooks/find_docker_enroll_portainer.yml
sam 017a3a00ee Initial commit: playbooks and inventory for Semaphore automation
- find_docker_enroll_portainer.yml: discover Docker hosts across all VLANs,
  deploy Portainer Agent, register in Portainer, write discovery report
- inventory/hosts.yml: auto-generated from NetBox (31 hosts) + UniFi clients
  (135 unmanaged hosts not in NetBox) across vlan1/vlan40/vlan20

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:27:58 -07:00

289 lines
9.6 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
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
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
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
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
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 %}