Contents

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.md

Core 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
end

Network 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
end

Service 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
end

Configuration 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: testpass

Service 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: 60

Testing 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
end

Rake 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")
end

Ansible 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: restarted

Custom 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
end

Lessons 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.