MultiHost: Ansible Testing Infrastructure Automation
MultiHost: Ansible Testing Infrastructure Automation
In this post, I’ll share insights from my MultiHost project, which demonstrates advanced infrastructure automation practices through a Ruby-based tool for creating multi-host environments specifically designed for Ansible testing.
Project Overview
MultiHost is a Ruby-based automation tool that simplifies the creation and management of multi-host testing environments for Ansible playbooks. It provides a streamlined approach to infrastructure testing by automating the setup of complex multi-node environments.
Why Multi-Host Testing?
Infrastructure Testing Challenges
- Complex Dependencies: Multi-node applications require coordinated testing
- Environment Consistency: Ensuring consistent test environments across different hosts
- Scalability Testing: Testing applications under various load conditions
- Network Configuration: Testing network-dependent functionality
- Service Orchestration: Validating service interactions across hosts
Benefits of Multi-Host Testing
- Realistic Testing: Test in environments that mirror production
- Integration Validation: Verify component interactions
- Performance Testing: Test under realistic network conditions
- Failure Simulation: Test failure scenarios across multiple nodes
- Deployment Validation: Validate deployment strategies
Technical Architecture
Project Structure
multihost/
├── lib/
│ ├── multihost/
│ │ ├── host_manager.rb
│ │ ├── network_config.rb
│ │ ├── service_orchestrator.rb
│ │ └── test_runner.rb
│ └── multihost.rb
├── config/
│ ├── default.yml
│ ├── environments/
│ │ ├── development.yml
│ │ ├── staging.yml
│ │ └── production.yml
│ └── hosts/
│ ├── web_servers.yml
│ ├── database_servers.yml
│ └── load_balancers.yml
├── scripts/
│ ├── setup.sh
│ ├── teardown.sh
│ └── health_check.sh
├── tests/
│ ├── integration/
│ ├── unit/
│ └── fixtures/
├── Gemfile
├── Rakefile
└── README.mdCore Components
Host Manager
# lib/multihost/host_manager.rb
require 'net/ssh'
require 'yaml'
require 'json'
class HostManager
attr_reader :hosts, :config
def initialize(config_path = 'config/default.yml')
@config = load_config(config_path)
@hosts = {}
@ssh_sessions = {}
end
def add_host(name, host_config)
@hosts[name] = Host.new(name, host_config)
end
def provision_hosts
@hosts.each do |name, host|
puts "Provisioning host: #{name}"
provision_host(host)
end
end
def execute_on_hosts(command, host_filter = nil)
target_hosts = host_filter ? filter_hosts(host_filter) : @hosts.values
results = {}
target_hosts.each do |host|
puts "Executing on #{host.name}: #{command}"
results[host.name] = execute_command(host, command)
end
results
end
def copy_file_to_hosts(source_path, dest_path, host_filter = nil)
target_hosts = host_filter ? filter_hosts(host_filter) : @hosts.values
target_hosts.each do |host|
puts "Copying #{source_path} to #{host.name}:#{dest_path}"
copy_file(host, source_path, dest_path)
end
end
def health_check
puts "Performing health check on all hosts..."
health_status = {}
@hosts.each do |name, host|
health_status[name] = check_host_health(host)
end
health_status
end
def cleanup
puts "Cleaning up hosts..."
@hosts.each do |name, host|
cleanup_host(host)
end
@ssh_sessions.each do |name, session|
session.close if session
end
@ssh_sessions.clear
end
private
def load_config(config_path)
YAML.load_file(config_path)
end
def provision_host(host)
case host.provider
when 'docker'
provision_docker_host(host)
when 'vagrant'
provision_vagrant_host(host)
when 'aws'
provision_aws_host(host)
else
raise "Unsupported provider: #{host.provider}"
end
end
def provision_docker_host(host)
# Docker provisioning logic
docker_cmd = "docker run -d --name #{host.name} #{host.image}"
docker_cmd += " -p #{host.port}:#{host.internal_port}" if host.port
docker_cmd += " #{host.extra_args}" if host.extra_args
system(docker_cmd)
# Wait for container to be ready
wait_for_host(host)
end
def provision_vagrant_host(host)
# Vagrant provisioning logic
vagrant_dir = File.join(Dir.pwd, 'vagrant', host.name)
Dir.mkdir(vagrant_dir) unless Dir.exist?(vagrant_dir)
# Generate Vagrantfile
generate_vagrantfile(host, vagrant_dir)
# Start Vagrant box
Dir.chdir(vagrant_dir) do
system("vagrant up")
end
end
def provision_aws_host(host)
# AWS EC2 provisioning logic
aws_cmd = "aws ec2 run-instances"
aws_cmd += " --image-id #{host.ami_id}"
aws_cmd += " --instance-type #{host.instance_type}"
aws_cmd += " --key-name #{host.key_name}"
aws_cmd += " --security-group-ids #{host.security_groups.join(',')}"
aws_cmd += " --subnet-id #{host.subnet_id}" if host.subnet_id
aws_cmd += " --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=#{host.name}}]'"
result = `#{aws_cmd}`
instance_data = JSON.parse(result)
instance_id = instance_data['Instances'][0]['InstanceId']
host.instance_id = instance_id
# Wait for instance to be running
wait_for_aws_instance(instance_id)
# Get public IP
host.public_ip = get_instance_public_ip(instance_id)
end
def execute_command(host, command)
session = get_ssh_session(host)
if session
result = session.exec!(command)
{ success: true, output: result }
else
{ success: false, error: "Failed to establish SSH connection" }
end
end
def copy_file(host, source_path, dest_path)
session = get_ssh_session(host)
if session
session.scp.upload!(source_path, dest_path)
{ success: true }
else
{ success: false, error: "Failed to establish SSH connection" }
end
end
def get_ssh_session(host)
return @ssh_sessions[host.name] if @ssh_sessions[host.name]
begin
session = Net::SSH.start(
host.ip_address,
host.username,
password: host.password,
key_data: host.private_key,
timeout: 30
)
@ssh_sessions[host.name] = session
session
rescue => e
puts "SSH connection failed for #{host.name}: #{e.message}"
nil
end
end
def check_host_health(host)
health_checks = [
{ name: 'ping', command: 'ping -c 1 8.8.8.8' },
{ name: 'disk_space', command: 'df -h' },
{ name: 'memory', command: 'free -m' },
{ name: 'load_average', command: 'uptime' }
]
health_status = { host: host.name, checks: {} }
health_checks.each do |check|
result = execute_command(host, check[:command])
health_status[:checks][check[:name]] = result
end
health_status
end
def wait_for_host(host, timeout = 300)
start_time = Time.now
while Time.now - start_time < timeout
if host_ready?(host)
puts "Host #{host.name} is ready"
return true
end
sleep 5
end
raise "Timeout waiting for host #{host.name} to be ready"
end
def host_ready?(host)
session = get_ssh_session(host)
return false unless session
result = session.exec!('echo "ready"')
result.strip == 'ready'
rescue
false
end
end
# Host class
class Host
attr_accessor :name, :ip_address, :username, :password, :private_key
attr_accessor :provider, :image, :port, :internal_port, :extra_args
attr_accessor :instance_id, :public_ip, :instance_type, :ami_id
attr_accessor :key_name, :security_groups, :subnet_id
def initialize(name, config)
@name = name
@ip_address = config['ip_address']
@username = config['username'] || 'root'
@password = config['password']
@private_key = config['private_key']
@provider = config['provider'] || 'docker'
@image = config['image'] || 'ubuntu:latest'
@port = config['port']
@internal_port = config['internal_port'] || 22
@extra_args = config['extra_args']
@instance_type = config['instance_type'] || 't2.micro'
@ami_id = config['ami_id']
@key_name = config['key_name']
@security_groups = config['security_groups'] || []
@subnet_id = config['subnet_id']
end
endNetwork Configuration
# lib/multihost/network_config.rb
require 'ipaddr'
class NetworkConfig
attr_reader :networks, :hosts
def initialize(config_path = 'config/networks.yml')
@networks = load_network_config(config_path)
@hosts = {}
end
def create_network(name, cidr, options = {})
network = Network.new(name, cidr, options)
@networks[name] = network
network
end
def assign_ip_to_host(host_name, network_name)
network = @networks[network_name]
raise "Network #{network_name} not found" unless network
ip = network.allocate_ip
@hosts[host_name] = { network: network_name, ip: ip }
ip
end
def configure_host_networking(host_name)
host_config = @hosts[host_name]
return unless host_config
network = @networks[host_config[:network]]
ip = host_config[:ip]
# Configure network interface
configure_interface(host_name, network, ip)
# Update routing table
configure_routing(host_name, network)
# Configure DNS
configure_dns(host_name, network)
end
def test_connectivity(host1, host2)
host1_config = @hosts[host1]
host2_config = @hosts[host2]
return false unless host1_config && host2_config
ip1 = host1_config[:ip]
ip2 = host2_config[:ip]
# Test ping connectivity
test_ping_connectivity(host1, ip2)
end
def generate_inventory_file(output_path = 'inventory.ini')
File.open(output_path, 'w') do |file|
@networks.each do |network_name, network|
file.puts "[#{network_name}]"
@hosts.each do |host_name, host_config|
if host_config[:network] == network_name
file.puts "#{host_name} ansible_host=#{host_config[:ip]}"
end
end
file.puts
end
end
end
private
def load_network_config(config_path)
return {} unless File.exist?(config_path)
YAML.load_file(config_path)
end
def configure_interface(host_name, network, ip)
# Configure network interface based on provider
case network.provider
when 'docker'
configure_docker_interface(host_name, network, ip)
when 'vagrant'
configure_vagrant_interface(host_name, network, ip)
when 'aws'
configure_aws_interface(host_name, network, ip)
end
end
def configure_docker_interface(host_name, network, ip)
# Create Docker network if it doesn't exist
network_name = "multihost_#{network.name}"
system("docker network create --subnet=#{network.cidr} #{network_name}")
# Connect container to network
system("docker network connect #{network_name} #{host_name}")
end
def test_ping_connectivity(host_name, target_ip)
# Execute ping test
result = `docker exec #{host_name} ping -c 1 #{target_ip}`
$?.success?
end
end
# Network class
class Network
attr_reader :name, :cidr, :provider, :allocated_ips
def initialize(name, cidr, options = {})
@name = name
@cidr = cidr
@provider = options[:provider] || 'docker'
@allocated_ips = Set.new
@ip_range = IPAddr.new(cidr)
end
def allocate_ip
# Find next available IP
@ip_range.each do |ip|
next if @allocated_ips.include?(ip.to_s)
next if ip == @ip_range.first # Skip network address
next if ip == @ip_range.last # Skip broadcast address
@allocated_ips.add(ip.to_s)
return ip.to_s
end
raise "No available IPs in network #{@name}"
end
def release_ip(ip)
@allocated_ips.delete(ip)
end
endService Orchestrator
# lib/multihost/service_orchestrator.rb
require 'json'
class ServiceOrchestrator
attr_reader :services, :dependencies
def initialize
@services = {}
@dependencies = {}
end
def define_service(name, config)
@services[name] = Service.new(name, config)
@dependencies[name] = config[:depends_on] || []
end
def deploy_services(host_manager)
# Sort services by dependencies
deployment_order = resolve_dependencies
deployment_order.each do |service_name|
service = @services[service_name]
puts "Deploying service: #{service_name}"
deploy_service(service, host_manager)
# Wait for service to be ready
wait_for_service_ready(service, host_manager)
end
end
def health_check_services(host_manager)
health_status = {}
@services.each do |name, service|
puts "Health checking service: #{name}"
health_status[name] = check_service_health(service, host_manager)
end
health_status
end
def scale_service(service_name, target_count, host_manager)
service = @services[service_name]
raise "Service #{service_name} not found" unless service
current_count = get_service_instance_count(service, host_manager)
if target_count > current_count
scale_up_service(service, target_count - current_count, host_manager)
elsif target_count < current_count
scale_down_service(service, current_count - target_count, host_manager)
end
end
def rolling_update(service_name, new_image, host_manager)
service = @services[service_name]
raise "Service #{service_name} not found" unless service
instances = get_service_instances(service, host_manager)
instances.each do |instance|
puts "Updating instance: #{instance}"
# Deploy new version
deploy_service_instance(service, instance, new_image, host_manager)
# Health check
wait_for_service_ready(service, host_manager, instance)
# Update load balancer if needed
update_load_balancer(service, instance, host_manager)
end
end
private
def resolve_dependencies
visited = Set.new
temp_visited = Set.new
result = []
@services.keys.each do |service|
visit_service(service, visited, temp_visited, result)
end
result
end
def visit_service(service, visited, temp_visited, result)
return if visited.include?(service)
raise "Circular dependency detected" if temp_visited.include?(service)
temp_visited.add(service)
@dependencies[service].each do |dependency|
visit_service(dependency, visited, temp_visited, result)
end
temp_visited.delete(service)
visited.add(service)
result << service
end
def deploy_service(service, host_manager)
case service.type
when 'docker'
deploy_docker_service(service, host_manager)
when 'systemd'
deploy_systemd_service(service, host_manager)
when 'kubernetes'
deploy_kubernetes_service(service, host_manager)
end
end
def deploy_docker_service(service, host_manager)
service.hosts.each do |host_name|
docker_cmd = build_docker_command(service)
host_manager.execute_on_hosts(
docker_cmd,
host_name
)
end
end
def build_docker_command(service)
cmd = "docker run -d --name #{service.name}"
cmd += " -p #{service.port}:#{service.internal_port}" if service.port
cmd += " #{service.environment_vars.map { |k, v| "-e #{k}=#{v}" }.join(' ')}" if service.environment_vars
cmd += " #{service.image}"
cmd += " #{service.command}" if service.command
cmd
end
def wait_for_service_ready(service, host_manager, instance = nil)
timeout = service.health_check[:timeout] || 300
interval = service.health_check[:interval] || 5
start_time = Time.now
while Time.now - start_time < timeout
if service_ready?(service, host_manager, instance)
puts "Service #{service.name} is ready"
return true
end
sleep interval
end
raise "Timeout waiting for service #{service.name} to be ready"
end
def service_ready?(service, host_manager, instance = nil)
health_check = service.health_check
case health_check[:type]
when 'http'
check_http_health(service, host_manager, instance)
when 'tcp'
check_tcp_health(service, host_manager, instance)
when 'command'
check_command_health(service, host_manager, instance)
end
end
def check_http_health(service, host_manager, instance)
url = health_check[:url] || "http://localhost:#{service.port}/health"
service.hosts.each do |host_name|
result = host_manager.execute_on_hosts(
"curl -f #{url}",
host_name
)
return false unless result[host_name][:success]
end
true
end
end
# Service class
class Service
attr_reader :name, :type, :image, :port, :internal_port
attr_reader :hosts, :environment_vars, :command, :health_check
def initialize(name, config)
@name = name
@type = config[:type] || 'docker'
@image = config[:image]
@port = config[:port]
@internal_port = config[:internal_port] || @port
@hosts = config[:hosts] || []
@environment_vars = config[:environment_vars] || {}
@command = config[:command]
@health_check = config[:health_check] || { type: 'tcp', timeout: 300 }
end
endConfiguration Management
Host Configuration
# config/hosts/web_servers.yml
web_servers:
- name: web1
provider: docker
image: nginx:latest
port: 8080
internal_port: 80
environment_vars:
- NGINX_HOST: web1
- NGINX_PORT: 80
- name: web2
provider: docker
image: nginx:latest
port: 8081
internal_port: 80
environment_vars:
- NGINX_HOST: web2
- NGINX_PORT: 80
# config/hosts/database_servers.yml
database_servers:
- name: db1
provider: docker
image: postgres:13
port: 5432
environment_vars:
- POSTGRES_DB: testdb
- POSTGRES_USER: testuser
- POSTGRES_PASSWORD: testpass
- name: db2
provider: docker
image: postgres:13
port: 5433
environment_vars:
- POSTGRES_DB: testdb
- POSTGRES_USER: testuser
- POSTGRES_PASSWORD: testpassService Configuration
# config/services.yml
services:
nginx:
type: docker
image: nginx:latest
hosts: [web1, web2]
port: 80
health_check:
type: http
url: http://localhost:80/health
timeout: 60
postgres:
type: docker
image: postgres:13
hosts: [db1, db2]
port: 5432
environment_vars:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
health_check:
type: tcp
timeout: 120
redis:
type: docker
image: redis:6
hosts: [cache1]
port: 6379
health_check:
type: tcp
timeout: 60Testing Framework
Integration Tests
# tests/integration/multihost_test.rb
require 'minitest/autorun'
require 'minitest/mock'
require_relative '../../lib/multihost'
class MultiHostIntegrationTest < Minitest::Test
def setup
@host_manager = HostManager.new('config/test.yml')
@network_config = NetworkConfig.new('config/test_networks.yml')
@service_orchestrator = ServiceOrchestrator.new
end
def teardown
@host_manager.cleanup
end
def test_host_provisioning
# Add test hosts
@host_manager.add_host('test1', {
'provider' => 'docker',
'image' => 'ubuntu:latest'
})
@host_manager.add_host('test2', {
'provider' => 'docker',
'image' => 'ubuntu:latest'
})
# Provision hosts
@host_manager.provision_hosts
# Verify hosts are running
health_status = @host_manager.health_check
assert health_status['test1'][:checks]['ping'][:success]
assert health_status['test2'][:checks]['ping'][:success]
end
def test_network_configuration
# Create test network
network = @network_config.create_network('test_net', '192.168.100.0/24')
# Assign IPs to hosts
ip1 = @network_config.assign_ip_to_host('test1', 'test_net')
ip2 = @network_config.assign_ip_to_host('test2', 'test_net')
assert_equal '192.168.100.2', ip1
assert_equal '192.168.100.3', ip2
# Test connectivity
@network_config.configure_host_networking('test1')
@network_config.configure_host_networking('test2')
assert @network_config.test_connectivity('test1', 'test2')
end
def test_service_orchestration
# Define services
@service_orchestrator.define_service('web', {
type: 'docker',
image: 'nginx:latest',
hosts: ['test1'],
port: 80,
health_check: { type: 'http', url: 'http://localhost:80' }
})
@service_orchestrator.define_service('db', {
type: 'docker',
image: 'postgres:13',
hosts: ['test2'],
port: 5432,
depends_on: ['web'],
health_check: { type: 'tcp' }
})
# Deploy services
@service_orchestrator.deploy_services(@host_manager)
# Verify services are running
health_status = @service_orchestrator.health_check_services(@host_manager)
assert health_status['web'][:healthy]
assert health_status['db'][:healthy]
end
def test_ansible_integration
# Generate Ansible inventory
@network_config.generate_inventory_file('test_inventory.ini')
# Verify inventory file exists
assert File.exist?('test_inventory.ini')
# Verify inventory content
inventory_content = File.read('test_inventory.ini')
assert_includes inventory_content, 'test1 ansible_host='
assert_includes inventory_content, 'test2 ansible_host='
end
endRake Tasks
# Rakefile
require 'rake'
require 'yaml'
desc "Setup multi-host environment"
task :setup do
puts "Setting up multi-host environment..."
# Load configuration
config = YAML.load_file('config/default.yml')
# Initialize components
host_manager = HostManager.new
network_config = NetworkConfig.new
service_orchestrator = ServiceOrchestrator.new
# Provision hosts
config['hosts'].each do |host_config|
host_manager.add_host(host_config['name'], host_config)
end
host_manager.provision_hosts
# Configure networks
config['networks'].each do |network_config|
network_config.create_network(
network_config['name'],
network_config['cidr'],
network_config
)
end
# Deploy services
config['services'].each do |service_name, service_config|
service_orchestrator.define_service(service_name, service_config)
end
service_orchestrator.deploy_services(host_manager)
puts "Multi-host environment setup complete!"
end
desc "Run health checks"
task :health_check do
puts "Running health checks..."
host_manager = HostManager.new
health_status = host_manager.health_check
health_status.each do |host_name, status|
puts "#{host_name}: #{status[:checks]['ping'][:success] ? 'OK' : 'FAIL'}"
end
end
desc "Generate Ansible inventory"
task :generate_inventory do
puts "Generating Ansible inventory..."
network_config = NetworkConfig.new
network_config.generate_inventory_file('inventory.ini')
puts "Inventory file generated: inventory.ini"
end
desc "Cleanup environment"
task :cleanup do
puts "Cleaning up environment..."
host_manager = HostManager.new
host_manager.cleanup
puts "Cleanup complete!"
end
desc "Run tests"
task :test do
system("ruby -Ilib:test tests/integration/multihost_test.rb")
endAnsible Integration
Ansible Playbook Example
# playbooks/multihost_deployment.yml
---
- name: Deploy application to multi-host environment
hosts: all
become: yes
vars:
app_name: "multihost-app"
app_version: "1.0.0"
tasks:
- name: Update system packages
apt:
update_cache: yes
upgrade: yes
when: ansible_os_family == "Debian"
- name: Install required packages
package:
name:
- python3
- python3-pip
- docker.io
state: present
- name: Start and enable Docker
systemd:
name: docker
state: started
enabled: yes
- name: Create application directory
file:
path: /opt/{{ app_name }}
state: directory
mode: '0755'
- name: Copy application files
copy:
src: "{{ item }}"
dest: /opt/{{ app_name }}/
with_fileglob:
- "app/*"
- name: Install Python dependencies
pip:
requirements: /opt/{{ app_name }}/requirements.txt
state: present
- name: Create systemd service
template:
src: app.service.j2
dest: /etc/systemd/system/{{ app_name }}.service
notify: restart app
- name: Start and enable application
systemd:
name: "{{ app_name }}"
state: started
enabled: yes
daemon_reload: yes
handlers:
- name: restart app
systemd:
name: "{{ app_name }}"
state: restartedCustom Ansible Modules
# ansible/modules/multihost_info.py
from ansible.module_utils.basic import AnsibleModule
import json
import subprocess
def get_host_info():
"""Get host information for multi-host environment."""
try:
# Get hostname
hostname = subprocess.check_output(['hostname']).decode().strip()
# Get IP addresses
ip_output = subprocess.check_output(['ip', 'addr', 'show']).decode()
# Get container info if running in Docker
container_info = {}
try:
container_id = subprocess.check_output(['cat', '/proc/self/cgroup']).decode()
if 'docker' in container_id:
container_info['is_container'] = True
container_info['container_id'] = container_id.split('/')[-1].strip()
except:
container_info['is_container'] = False
return {
'hostname': hostname,
'ip_addresses': ip_output,
'container_info': container_info
}
except Exception as e:
return {'error': str(e)}
def main():
module = AnsibleModule(
argument_spec=dict(),
supports_check_mode=True
)
result = get_host_info()
if 'error' in result:
module.fail_json(msg=result['error'])
else:
module.exit_json(changed=False, ansible_facts=result)
if __name__ == '__main__':
main()Performance Monitoring
Monitoring Integration
# lib/multihost/monitoring.rb
require 'json'
require 'time'
class Monitoring
def initialize(config = {})
@config = config
@metrics = {}
end
def collect_metrics(host_manager)
puts "Collecting metrics from all hosts..."
@metrics = {}
host_manager.hosts.each do |name, host|
@metrics[name] = collect_host_metrics(host, host_manager)
end
@metrics
end
def generate_report(output_path = 'monitoring_report.json')
report = {
timestamp: Time.now.iso8601,
hosts: @metrics,
summary: generate_summary
}
File.write(output_path, JSON.pretty_generate(report))
puts "Monitoring report generated: #{output_path}"
end
def alert_on_thresholds
alerts = []
@metrics.each do |host_name, metrics|
# CPU threshold
if metrics[:cpu_usage] > 80
alerts << {
host: host_name,
type: 'cpu',
message: "High CPU usage: #{metrics[:cpu_usage]}%"
}
end
# Memory threshold
if metrics[:memory_usage] > 90
alerts << {
host: host_name,
type: 'memory',
message: "High memory usage: #{metrics[:memory_usage]}%"
}
end
# Disk threshold
if metrics[:disk_usage] > 85
alerts << {
host: host_name,
type: 'disk',
message: "High disk usage: #{metrics[:disk_usage]}%"
}
end
end
alerts
end
private
def collect_host_metrics(host, host_manager)
metrics = {}
# CPU usage
cpu_result = host_manager.execute_on_hosts(
"top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1",
host.name
)
metrics[:cpu_usage] = cpu_result[host.name][:output].to_f
# Memory usage
memory_result = host_manager.execute_on_hosts(
"free | grep Mem | awk '{printf \"%.2f\", $3/$2 * 100.0}'",
host.name
)
metrics[:memory_usage] = memory_result[host.name][:output].to_f
# Disk usage
disk_result = host_manager.execute_on_hosts(
"df -h / | awk 'NR==2{print $5}' | cut -d'%' -f1",
host.name
)
metrics[:disk_usage] = disk_result[host.name][:output].to_f
# Network stats
network_result = host_manager.execute_on_hosts(
"cat /proc/net/dev | grep eth0 | awk '{print $2, $10}'",
host.name
)
network_data = network_result[host.name][:output].split
metrics[:bytes_received] = network_data[0].to_i
metrics[:bytes_transmitted] = network_data[1].to_i
metrics
end
def generate_summary
total_hosts = @metrics.size
avg_cpu = @metrics.values.map { |m| m[:cpu_usage] }.sum / total_hosts
avg_memory = @metrics.values.map { |m| m[:memory_usage] }.sum / total_hosts
avg_disk = @metrics.values.map { |m| m[:disk_usage] }.sum / total_hosts
{
total_hosts: total_hosts,
average_cpu_usage: avg_cpu.round(2),
average_memory_usage: avg_memory.round(2),
average_disk_usage: avg_disk.round(2)
}
end
endLessons Learned
Infrastructure Automation
- Environment Consistency: Automated provisioning ensures consistent test environments
- Dependency Management: Proper dependency resolution prevents deployment failures
- Resource Management: Efficient resource allocation and cleanup
- Monitoring: Comprehensive monitoring for infrastructure health
Testing Best Practices
- Integration Testing: Multi-host testing validates real-world scenarios
- Automated Setup: Automated environment setup reduces manual errors
- Cleanup Procedures: Proper cleanup prevents resource leaks
- Health Checks: Regular health checks ensure system reliability
Ruby Development
- Object-Oriented Design: Clean class hierarchy and separation of concerns
- Error Handling: Robust error handling and recovery mechanisms
- Configuration Management: Flexible configuration system
- Testing: Comprehensive test coverage for reliability
Future Enhancements
Advanced Features
- Kubernetes Integration: Support for Kubernetes-based multi-host environments
- Cloud Providers: Enhanced support for AWS, Azure, and GCP
- Service Mesh: Integration with service mesh technologies
- Advanced Monitoring: Prometheus and Grafana integration
Performance Improvements
- Parallel Execution: Concurrent host provisioning and service deployment
- Caching: Intelligent caching for faster subsequent deployments
- Resource Optimization: Dynamic resource allocation based on workload
- Load Balancing: Built-in load balancing capabilities
Conclusion
The MultiHost project demonstrates advanced infrastructure automation practices using Ruby. Key achievements include:
- Infrastructure Automation: Automated multi-host environment provisioning
- Service Orchestration: Coordinated service deployment and management
- Network Configuration: Automated network setup and connectivity testing
- Ansible Integration: Seamless integration with Ansible for configuration management
- Monitoring: Comprehensive monitoring and alerting capabilities
- Testing: Robust testing framework for infrastructure validation
The project is available on GitHub and serves as a comprehensive example of infrastructure automation and testing practices.
This project represents my exploration into infrastructure automation and demonstrates how Ruby can be used to build sophisticated tools for managing complex multi-host environments. The lessons learned here continue to influence my approach to infrastructure testing and DevOps automation.