π 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