--- # 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 %}