Ansible Firefighting Hotline Series (22): Automated Diagnosis of FTP Failures

πŸ“ Ansible Firefighting Hotline | Struggling with FTP Service Failures? One-Click Automated Diagnosis Turns You into a File Transfer Expert!

Tired of blindly troubleshooting FTP service failures? Today, we bring you a comprehensive automated analysis solution for FTP service failures on RHEL8/9 & CentOS8/9, allowing you to say goodbye to the nightmare of manually typing commands!

🎯 Pain Points Addressed

The daily routine of an operations engineer: FTP connection failure β†’ manually checking vsftpd status β†’ checking port listening β†’ checking firewall configuration β†’ analyzing service logs β†’ troubleshooting network connectivity β†’ verifying user permissions… After a series of actions, several hours may pass, and the problem could still be elusive.

Even worse: FTP service issues often involve dual-sided checks on both the server and client, making it easy to overlook key information during manual troubleshooting, lacking systematic analysis, and unable to quickly pinpoint the root cause. Have you ever thought that if there were an automated FTP diagnosis solution, all these problems would be resolved?

✨ Solution Preview

Today, we share an automated analysis solution for FTP service failures on RHEL8/9 & CentOS8/9 using Ansible, which includes six core diagnostic modules, standardizing, automating, and intelligentizing your FTP troubleshooting!

Results Preview

🧾 Sample Original Diagnosis Report (results only)

======= FTP Diagnosis for ftp-server.example.com ======
Host: ftp-server.example.com  (addr: 192.168.1.100)
OS: RedHat 9.6 (x86_64)
--------------------------------------------------------
[Server Checks]
Package (vsftpd): vsftpd-3.0.3-34.el9.x86_64
Service (vsftpd) - systemctl is-active: active
Port Listening (21): tcp   LISTEN 0      32          0.0.0.0:21       0.0.0.0:*    users:(("vsftpd",pid=12345,fd=3))
Firewall Info:
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources: 
  services: cockpit dhcpv6-client ftp ssh
  ports: 
  protocols: 
  masquerade: no
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules: 
vsftpd.conf anonymous_enable:
anonymous_enable=NO

[Client Connectivity Checks]
Ping results:
 - PING 192.168.1.100 (192.168.1.100) 56(84) bytes of data.
   64 bytes from 192.168.1.100: icmp_seq=1 ttl=64 time=0.123 ms
   64 bytes from 192.168.1.100: icmp_seq=2 ttl=64 time=0.098 ms
TCP connect (port 21) results:
 - Connected
 - Connected

-------------------------------------------------
Diagnostic Hint:
- Basic checks performed. Consider checking user permissions, passive ports range, FTP over TLS settings, or packet captures if needed.
Generated at: 2025-09-22T12:13:01Z
==================================================

πŸ€” Design Philosophy: Why Our Playbook is Best Practice?

A professional automation solution is not just a simple pile of commands. Our design philosophy incorporates the core practices advocated by Red Hat, elevating your automation solution from “just works” to “professional and reliable”!

1

Role Separation, Precise Diagnosis ✨ We adopt a design concept of separating server and client, executing different diagnostic tasks based on the host’s role in the inventory (ftp_servers vs ftp_clients). Server-side checks include package installation, service status, port listening, and firewall configuration; client-side checks include network connectivity and TCP connection tests. This design makes diagnosis more precise and efficient!

2

Variable Driven, Flexible Adaptation πŸ’» We centralize all configurable parameters (such as FTP service name, port number, report path) in the <span><span>vars</span></span> section at the top of the Playbook. This means that when you need to adjust the scope of diagnosis, you only need to modify these variables without touching any core automation task logic.

3

Idempotency Assurance, Safe and Secure βœ… All our Playbooks strictly adhere to Ansible’s core principleβ€”idempotency. You can confidently execute this Playbook repeatedly; Ansible will automatically detect the current state and only perform necessary checks.

4

Closed-Loop Verification, Visible Results 🎯 The last step of the Playbook is to generate a complete diagnostic report and automatically pull it to the control node. This forms a check-analyze-report-summarize closed loop. Not only do you execute automation, but you can also immediately see the diagnostic results, ensuring that the problem is under control!

⭐ Automation Scenario Rating

Rating Dimension Rating Description
Ease of Use ⭐⭐ One-click execution, detailed comments, beginner-friendly
Reusability ⭐⭐⭐⭐⭐ Variable configuration, supports multi-host parallel execution
Stability ⭐⭐⭐⭐⭐ Idempotent design, comprehensive error handling
Scalability ⭐⭐⭐⭐ Modular design, easy to extend functionality
Best Practice Compliance ⭐⭐⭐⭐⭐ Follows Ansible best practices, code standards

πŸ—‚οΈ Project Directory Structure

09_FTP_Automated_Diagnosis/
β”œβ”€β”€ troubleshooting02_ftp.yml    # Main diagnostic Playbook

πŸ“„ Core File Content Overview

🎯 Main Diagnostic Playbook (troubleshooting02_ftp.yml)

---
- name: "FTP Troubleshooting (single play for all hosts; per-host remote report + fetch to controller)"
  hosts: all
  gather_facts: yes
  become: yes
  vars:
    ftp_service: vsftpd
    ftp_port: 21
    remote_report_path: "/tmp/ftp_diagnosis_{{ inventory_hostname }}.txt"
    controller_reports_dir: "/tmp/ftp_reports"

  tasks:

    - name: "Ensure the aggregation directory on the control node exists (run only once)"
      ansible.builtin.file:
        path: "{{ controller_reports_dir }}"
        state: directory
        mode: "0755"
      delegate_to: localhost
      run_once: true

    #####################################################################
    # Server-side common checks (only run if host in ftp_servers)
    #####################################################################
    - name: "Check if FTP package (vsftpd) is installed - only server"
      ansible.builtin.shell: "rpm -qa | grep -i '^{{ ftp_service }}' || true"
      register: ftp_pkg
      changed_when: false
      ignore_errors: yes
      when: "'ftp_servers' in group_names"

    - name: "Collect service facts - only server"
      ansible.builtin.service_facts:
      when: "'ftp_servers' in group_names"

    - name: "Check if FTP service is active (only check, no modification) - only server"
      ansible.builtin.shell: "systemctl is-active {{ ftp_service }} || true"
      register: ftp_service_state
      changed_when: false
      when: "'ftp_servers' in group_names"

    - name: "Check if port {{ ftp_port }} is listening - server"
      ansible.builtin.shell: "ss -tulpn 2>/dev/null | grep :{{ ftp_port }} || true"
      register: ftp_port_status
      changed_when: false
      ignore_errors: yes
      when: "'ftp_servers' in group_names"

    - name: "Check firewall (firewalld/iptables) - server"
      ansible.builtin.shell: |
        if command -v firewall-cmd >/dev/null; then
          firewall-cmd --list-all || true
        elif command -v iptables >/dev/null; then
          iptables -L -n || true
        else
          echo "No firewall tool found"
        fi
      register: firewall_status
      changed_when: false
      ignore_errors: yes
      when: "'ftp_servers' in group_names"

    - name: "Check vsftpd configuration anonymous_enable - server"
      ansible.builtin.shell: "grep '^anonymous_enable' /etc/vsftpd/vsftpd.conf || true"
      register: vsftpd_conf
      changed_when: false
      ignore_errors: yes
      when: "'ftp_servers' in group_names"

    #####################################################################
    # Client-side checks (only run if host in ftp_clients)
    #####################################################################
    - name: "Prepare FTP server address list for clients (for ping/tcp tests) - client"
      ansible.builtin.set_fact:
        ftp_server_addrs: >-
          {{ groups['ftp_servers']
             | map('extract', hostvars, 'ansible_host')
             | map('default', '')
             | map('regex_replace','^$','')
             | list }}
      when: "'ftp_clients' in group_names"

    - name: "Client ping each FTP server - client"
      ansible.builtin.shell: "ping -c 2 {{ hostvars[item].ansible_host | default(item) }} || true"
      loop: "{{ groups['ftp_servers'] }}"
      loop_control:
        label: "{{ item }}"
      register: client_ping_results
      changed_when: false
      ignore_errors: yes
      when: "'ftp_clients' in group_names"

    - name: "Client TCP connection test to each FTP server (port {{ ftp_port }}) - client"
      ansible.builtin.shell: "timeout 5 bash -c 'cat < /dev/null > /dev/tcp/{{ hostvars[item].ansible_host | default(item) }}/{{ ftp_port }}' && echo 'Connected' || echo 'Failed'"
      loop: "{{ groups['ftp_servers'] }}"
      loop_control:
        label: "{{ item }}"
      register: client_tcp_results
      changed_when: false
      ignore_errors: yes
      when: "'ftp_clients' in group_names"

    #####################################################################
    # Generate per-host report content (as a string variable)
    #####################################################################
    - name: "Generate diagnostic report content (variable ftp_report)"
      ansible.builtin.set_fact:
        ftp_report: |
          ================= FTP Diagnosis for {{ inventory_hostname }} ===============
          Host: {{ inventory_hostname }}  (addr: {{ ansible_default_ipv4.address | default(ansible_host | default('N/A')) }})
          OS: {{ ansible_distribution }} {{ ansible_distribution_version }} ({{ ansible_architecture }})
          -------------------------------------------------------------------------
          {% if 'ftp_servers' in group_names %}
          [Server Checks]
          Package (vsftpd): {{ ftp_pkg.stdout | default('Not Checked or Not Installed') }}
          Service ({{ ftp_service }}) - systemctl is-active: {{ ftp_service_state.stdout | default('Unknown') }}
          Port Listening ({{ ftp_port }}): {{ ftp_port_status.stdout | default('No listening info') }}
          Firewall Info:
          {{ firewall_status.stdout | default('No firewall tool found or no output') }}
          vsftpd.conf anonymous_enable:
          {{ vsftpd_conf.stdout | default('Not Found or Not Checked') }}
          {% endif %}
          {% if 'ftp_clients' in group_names %}
          [Client Connectivity Checks]
          Ping results:
          {% for r in client_ping_results.results %} - {{ (r.stdout | default('')) | trim }} {% endfor %}
          TCP connect (port {{ ftp_port }}) results:
          {% for r in client_tcp_results.results %} - {{ (r.stdout | default('')) | trim }} {% endfor %}
          {% endif %}
          -------------------------------------------------------------------------
          Diagnostic Hint:
          {% if ('ftp_servers' in group_names) and (ftp_pkg.stdout | default('')  '') %}
          - FTP package not installed (vsftpd).
          {% elif ('ftp_servers' in group_names) and (ftp_port_status.stdout | default('')  '') %}
          - Package present but port {{ ftp_port }} not listening. Check service / selinux / firewall / passive ports.
          {% else %}
          - Basic checks performed. Consider checking user permissions, passive ports range, FTP over TLS settings, or packet captures if needed.
          {% endif %}
          Generated at: {{ ansible_date_time.iso8601 }}
          ========================================================================

    #####################################################################
    # Write a report to the remote host at /tmp/ftp_diagnosis_<host>.txt (ensure the file exists on remote)
    #####################################################################
    - name: "Write diagnostic report to remote {{ remote_report_path }}"
      ansible.builtin.copy:
        dest: "{{ remote_report_path }}"
        content: "{{ ftp_report }}"
        mode: "0644"

    #####################################################################
    # Fetch the remote report to the control node's controller_reports_dir
    # Using fetch: will generate /tmp/ftp_reports/<inventory_hostname>/ftp_diagnosis_<inventory_hostname>.txt on the control node
    #####################################################################
    - name: "Fetch remote report to control node directory {{ controller_reports_dir }} (each host has its own file)"
      ansible.builtin.fetch:
        src: "{{ remote_report_path }}"
        dest: "{{ controller_reports_dir }}/"
        flat: no
      ignore_errors: yes

    - name: "Display the path of fetched files on the control node (debug info)"
      ansible.builtin.debug:
        msg: "Fetched to control node: {{ controller_reports_dir }}/{{ inventory_hostname }}/{{ remote_report_path | basename }}"
      run_once: false

    - name: "Debug: Output a brief diagnostic summary (for runtime viewing)"
      ansible.builtin.debug:
        msg: |
          Host {{ inventory_hostname }} checked.
          Server role: {{ 'yes' if 'ftp_servers' in group_names else 'no' }}
          Client role: {{ 'yes' if 'ftp_clients' in group_names else 'no' }}
      changed_when: false

#  Summary Step: Merge all fetched per-host reports into a unified report on the control node
- name: "Merge all fetched reports into a unified report on the control node"
  hosts: localhost
  gather_facts: no
  vars:
    controller_reports_dir: "/tmp/ftp_reports"
    merged_report: "/tmp/ftp_reports_merged/ftp_diagnosis_report.txt"
  tasks:
    - name: "Create merge directory"
      ansible.builtin.file:
        path: "/tmp/ftp_reports_merged"
        state: directory
        mode: "0755"

    - name: "Merge all host reports from /tmp/ftp_reports (if any) into a unified report"
      ansible.builtin.shell: |
        echo "============== FTP Troubleshooting Unified Report ================" > {{ merged_report }}
        echo "Generated at: $(date --iso-8601=seconds)" >> {{ merged_report }}
        echo "======================================================================" >> {{ merged_report }}

        # If the directory does not exist or is empty, still generate an empty report and explain
        if [ ! -d "{{ controller_reports_dir }}" ]; then
          echo "No per-host reports found in {{ controller_reports_dir }}" >> {{ merged_report }}
          exit 0
        fi

        found=0
        for hostdir in {{ controller_reports_dir }}/*; do
          [ -d "$hostdir" ] || continue
          for f in "$hostdir"/*; do
            [ -f "$f" ] || continue
            found=1
            echo "" >> {{ merged_report }}
            echo "---- Report from: $(basename $hostdir) ----" >> {{ merged_report }}
            cat "$f" >> {{ merged_report }}
            echo "" >> {{ merged_report }}
          done
        done

        if [ "$found" -eq 0 ]; then
          echo "" >> {{ merged_report }}
          echo "No per-host report files found under {{ controller_reports_dir }}/*/*" >> {{ merged_report }}
        fi

        echo "======================= End of Unified Report =====================" >> {{ merged_report }}
      args:
        executable: /bin/bash

    - name: "Display unified report path"
      ansible.builtin.debug:
        msg: "Unified diagnostic report generated: {{ merged_report }}"

πŸ› οΈ Foolproof Deployment Guide

Theory can be seen a thousand times, but nothing beats hands-on practice!

Prerequisites

1One Ansible control node.2The target server is configured with SSH trust, and the user executing Ansible has<span><span>sudo</span></span> privileges.3The control node has Ansible installed.

Project Directory Structure

This is a very simple project; you only need a few files!

09_FTP_Automated_Diagnosis/
β”œβ”€β”€ troubleshooting02_ftp.yml    # Main diagnostic Playbook
└── inventory                    # Host inventory (needs to be created)

How to Use?

1

Create Host Inventory πŸ“: Create a <span><span>inventory</span></span> file and fill in the hostnames or IP addresses of your FTP servers and clients.

[ftp_servers]
ftp-server1.example.com
ftp-server2.example.com

[ftp_clients]
client1.example.com
client2.example.com

2

Modify Variables ✏️: Open the <span><span>troubleshooting02_ftp.yml</span></span> file and modify the variable section according to your needs, such as FTP service name, port number, report output path, etc.

3

Execute Automation ▢️: Run the following command, then go make a cup of coffee β˜•οΈ!

ansible-playbook -i inventory troubleshooting02_ftp.yml

πŸ” Diagnostic Coverage

βœ… Server-Side Checks (ftp_servers group)

β€’Package Check: vsftpd package installation status and versionβ€’Service Status Analysis: systemctl is-active check for service running statusβ€’Port Listening Check: ss command checks if port 21 is listeningβ€’Firewall Configuration: firewalld/iptables status and rules checkβ€’Configuration File Check: vsftpd.conf key configuration items check

βœ… Client-Side Checks (ftp_clients group)

β€’Network Connectivity: ping test to all FTP serversβ€’TCP Connection Test: TCP connection test on port 21β€’Multi-Server Support: Automatically iterate through all FTP servers for testing

βœ… Intelligent Report Generation

β€’Per-Host Reports: Generate independent diagnostic reports for each hostβ€’Unified Summary Report: Automatically merge all host reports into a unified reportβ€’Intelligent Diagnostic Hints: Provide targeted troubleshooting suggestions based on check results

βœ… Report Management

β€’Remote Reports: Generate local report files on each hostβ€’Automatic Fetching: Automatically pull to the control node using the fetch moduleβ€’Unified Summary: Generate a unified diagnostic report on the control node

βœ… Comprehensive Error Handling

β€’Idempotent Designβ€’Fault Tolerance for Failed Tasks (ignore_errors: yes)β€’Role Separation Execution (when condition checks)β€’Automatic Creation of Report Directory

πŸ’‘ Tips for Use

🎯 Batch Diagnosis

# Add multiple servers and clients in the inventory
[ftp_servers]
server1 ansible_host=192.168.1.100
server2 ansible_host=192.168.1.101

[ftp_clients]
client1 ansible_host=192.168.1.200
client2 ansible_host=192.168.1.201

# Parallel execution, doubling efficiency
ansible-playbook troubleshooting02_ftp.yml -i inventory --forks 10

πŸ”§ Custom Configuration

Edit the <span><span>troubleshooting02_ftp.yml</span></span> file to adjust according to your environment:

β€’Modify FTP service name (vsftpd/proftpd, etc.)β€’Adjust FTP port number (21/2121, etc.)β€’Configure report output pathβ€’Customize diagnostic scope

πŸ› Troubleshooting

If you encounter issues, check the generated diagnostic reports:

β€’Per-host report:<span><span>/tmp/ftp_reports/[hostname]/ftp_diagnosis_[hostname].txt</span></span>β€’Unified report:<span><span>/tmp/ftp_reports_merged/ftp_diagnosis_report.txt</span></span>β€’Contains complete troubleshooting hints and diagnostic suggestions

⚠️ Reminder on the Importance of FTP Services

FTP service issues often affect file transfer and business continuity; it is recommended to:

β€’Regularly check FTP service statusβ€’Set up FTP service monitoring alertsβ€’Establish standard procedures for handling FTP failures

🎯 Advanced Usage Scenarios

Enterprise-Level Deployment

β€’Multi-Environment Support: Unified FTP service diagnosis for development, testing, and production environmentsβ€’Compliance Checks: Meet corporate FTP service audit requirementsβ€’Monitoring Integration: Seamlessly integrate with existing monitoring systemsβ€’Cluster Management: Unified management of FTP service status across the entire cluster

Fault Prevention

β€’Regular Checks: Set scheduled tasks to proactively discover potential FTP service issuesβ€’Trend Analysis: Analyze the health trends of FTP services through historical reportsβ€’Alert Mechanism: Set FTP service alert thresholds based on diagnostic resultsβ€’Automatic Repair: Implement FTP service automatic repair in conjunction with other tools

Team Collaboration

β€’Standardized Processes: Unify team FTP service failure troubleshooting standardsβ€’Knowledge Accumulation: Solidify expert experience into automation scriptsβ€’New Employee Training: Quickly enhance the overall FTP service level of the teamβ€’Document Management: Establish a knowledge base for handling FTP service failures

Extended Applications

β€’Other FTP Services: Support for vsftpd, proftpd, pure-ftpd, etc.β€’SFTP/FTPS: Extend support for secure file transfer protocolsβ€’WebDAV: Extend to web distributed authoring and version controlβ€’NFS/SMB: Extend to other file sharing protocols

Tags:#Ansible #Automation Operations #FTP Diagnosis #vsftpd #RHEL8 #CentOS8 #File Transfer #Operational Efficiency

Leave a Comment