23 Inventar-Plugins und Erweiterungen

23.1 Überblick über verfügbare Inventar-Plugins in Ansible

Inventar-Plugins in Ansible erweitern die Flexibilität und Skalierbarkeit von Inventories, indem sie es ermöglichen, dynamische Inventories aus verschiedenen Quellen zu generieren. Diese Plugins können auf eine Vielzahl von Datenquellen zugreifen, darunter Cloud-Dienste, Datenbanken und externe APIs.

Wichtiger Hinweis: Ab Ansible 2.8 müssen Inventar-Plugins nicht mehr manuell aktiviert werden, sofern sie korrekt mit plugin: ... in der YAML-Konfiguration spezifiziert sind und die Plugin-Dateien im erkannten Pfad (inventory/, ./, etc.) liegen.

23.1.1 Verfügbare Plugins ermitteln

Um alle verfügbaren Inventar-Plugins anzuzeigen:

ansible-doc -t inventory -l

Für detaillierte Dokumentation eines spezifischen Plugins:

ansible-doc -t inventory aws_ec2

23.1.2 Gängige Inventar-Plugins

  1. aws_ec2:
  2. gcp_compute:
  3. azure_rm:
  4. vmware_vm_inventory:
  5. openstack:

23.2 Installation und Konfiguration von Inventar-Plugins

23.2.1 Projektstruktur für dynamische Inventories

ansible-project/
├── ansible.cfg
├── inventory/
│   ├── aws_ec2.yml
│   ├── gcp_compute.yml
│   └── group_vars/
├── inventory_plugins/
│   └── custom_plugin.py
└── playbooks/

23.2.2 Beispiel: Erweiterte AWS EC2 Konfiguration

23.2.2.1 1. Installation der erforderlichen Abhängigkeiten

pip install boto3 botocore

23.2.2.2 2. Konfigurationsdatei inventory/aws_ec2.yml

plugin: aws_ec2

# Regionen definieren
regions:
  - us-west-1
  - us-west-2
  - eu-central-1

# Filter für Instance-Status
filters:
  instance-state-name: running
  # Zusätzliche Filter möglich:
  # "tag:Environment": production

# Hostnamen-Mapping (wichtig für dynamische Umgebungen)
hostnames:
  - dns-name
  - private-dns-name
  - tag:Name
  - instance-id

# Variablen zusammenstellen
compose:
  ansible_host: public_ip_address | default(private_ip_address)
  ec2_arch: architecture
  ec2_instance_type: instance_type
  ec2_placement_region: placement.region
  ec2_security_groups: security_groups | map(attribute='group_name') | list
  ec2_vpc_id: vpc_id

# Dynamische Gruppenerstellung
groups:
  # Gruppen basierend auf Instance-Typ
  ec2_micro: instance_type == "t2.micro"
  ec2_small: instance_type == "t2.small"
  
  # Gruppen basierend auf Tags
  webservers: "'web' in (tags.Role | default(''))"
  databases: "'db' in (tags.Role | default(''))"
  
  # Gruppen basierend auf Availability Zone
  us_west_1a: placement.availability_zone == "us-west-1a"

# Gruppierung nach Tags mit Präfix
keyed_groups:
  - key: tags.Environment | default('unknown')
    prefix: env
  - key: tags.Application | default('misc')
    prefix: app
  - key: placement.availability_zone
    prefix: az

# Strenge Validierung aktivieren
strict: false

# Caching für bessere Performance
cache: true
cache_plugin: memory
cache_timeout: 3600

23.2.2.3 3. Verwendung und Validierung

# Inventory anzeigen
ansible-inventory -i inventory/aws_ec2.yml --list

# Spezifische Gruppe anzeigen
ansible-inventory -i inventory/aws_ec2.yml --list --host webserver-01

# Graph-Darstellung
ansible-inventory -i inventory/aws_ec2.yml --graph

23.2.3 Beispiel: Google Cloud Platform Konfiguration

23.2.3.1 1. Installation der Abhängigkeiten

pip install google-auth google-api-python-client

23.2.3.2 2. Konfigurationsdatei inventory/gcp_compute.yml

plugin: gcp_compute

# Projekte und Zonen
projects:
  - my-production-project
  - my-staging-project

zones:
  - us-central1-a
  - us-central1-b
  - europe-west1-a

# Service Account Key (alternativ: Application Default Credentials)
auth_kind: serviceaccount
service_account_file: /path/to/service-account.json

# Filter
filters:
  - status = RUNNING
  - machineType:n1-standard-*

# Hostnamen-Konfiguration
hostnames:
  - name
  - networkInterfaces[0].accessConfigs[0].natIP
  - networkInterfaces[0].networkIP

# Variablen-Mapping
compose:
  ansible_host: networkInterfaces[0].accessConfigs[0].natIP | default(networkInterfaces[0].networkIP)
  gcp_machine_type: machineType | regex_replace('.*/', '')
  gcp_zone: zone | regex_replace('.*/', '')
  gcp_project: selfLink | regex_replace('.*/projects/([^/]*)/.*', '\\1')

# Gruppierung
groups:
  gcp_preemptible: scheduling.preemptible | default(false)
  gcp_production: "'production' in (labels.environment | default(''))"

keyed_groups:
  - key: labels.environment | default('unknown')
    prefix: env
  - key: zone | regex_replace('.*/', '')
    prefix: zone

23.3 Erstellen benutzerdefinierter Plugins

23.3.1 Plugin-Struktur und Implementierung

23.3.1.1 1. Plugin-Datei inventory_plugins/database_inventory.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.module_utils.six.moves import configparser
import sqlite3
import json

DOCUMENTATION = '''
    name: database_inventory
    plugin_type: inventory
    short_description: Database-basiertes Inventory Plugin
    description:
        - Lädt Host-Informationen aus einer SQLite-Datenbank
        - Unterstützt dynamische Gruppenerstellung
    requirements:
        - sqlite3
    options:
        database_path:
            description: Pfad zur SQLite-Datenbank
            required: true
            type: str
        host_table:
            description: Name der Hosts-Tabelle
            required: false
            default: hosts
            type: str
        group_table:
            description: Name der Groups-Tabelle
            required: false
            default: groups
            type: str
'''

class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
    NAME = 'database_inventory'

    def verify_file(self, path):
        """Prüft, ob die Datei von diesem Plugin verarbeitet werden kann"""
        if super(InventoryModule, self).verify_file(path):
            if path.endswith(('database_inventory.yml', 'database_inventory.yaml')):
                return True
        return False

    def parse(self, inventory, loader, path, cache=True):
        """Hauptmethode zum Parsen des Inventories"""
        # Basis-Parser aufrufen
        super(InventoryModule, self).parse(inventory, loader, path, cache)
        
        # Konfiguration laden
        try:
            config = self._read_config_data(path)
        except Exception as e:
            raise AnsibleParserError(f"Fehler beim Lesen der Konfiguration: {e}")
        
        # Plugin-spezifische Konfiguration
        database_path = self.get_option('database_path')
        host_table = self.get_option('host_table')
        group_table = self.get_option('group_table')
        
        if not database_path:
            raise AnsibleError("database_path ist erforderlich")
        
        # Cache-Schlüssel generieren
        cache_key = self.get_cache_key(path)
        
        # Versuche aus Cache zu laden
        if cache:
            cache_data = self.cache.get(cache_key)
            if cache_data:
                self._populate_from_cache(cache_data)
                return
        
        try:
            # Datenbankverbindung
            conn = sqlite3.connect(database_path)
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            
            # Hosts laden
            self._load_hosts(cursor, host_table)
            
            # Gruppen laden
            self._load_groups(cursor, group_table)
            
            # Cache aktualisieren
            if cache:
                cache_data = {
                    'hosts': dict(self.inventory.hosts),
                    'groups': dict(self.inventory.groups)
                }
                self.cache.set(cache_key, cache_data)
                
        except sqlite3.Error as e:
            raise AnsibleError(f"Datenbankfehler: {e}")
        except Exception as e:
            raise AnsibleError(f"Unerwarteter Fehler: {e}")
        finally:
            if 'conn' in locals():
                conn.close()

    def _load_hosts(self, cursor, table_name):
        """Lädt Hosts aus der Datenbank"""
        try:
            cursor.execute(f"""
                SELECT hostname, ip_address, variables 
                FROM {table_name} 
                WHERE active = 1
            """)
            
            for row in cursor.fetchall():
                hostname = row['hostname']
                ip_address = row['ip_address']
                variables_json = row['variables'] or '{}'
                
                # Host hinzufügen
                self.inventory.add_host(hostname)
                
                # IP-Adresse setzen
                if ip_address:
                    self.inventory.set_variable(hostname, 'ansible_host', ip_address)
                
                # Zusätzliche Variablen aus JSON
                try:
                    variables = json.loads(variables_json)
                    for key, value in variables.items():
                        self.inventory.set_variable(hostname, key, value)
                except json.JSONDecodeError:
                    self.display.warning(f"Ungültiges JSON für Host {hostname}")
                    
        except sqlite3.Error as e:
            raise AnsibleError(f"Fehler beim Laden der Hosts: {e}")

    def _load_groups(self, cursor, table_name):
        """Lädt Gruppen aus der Datenbank"""
        try:
            cursor.execute(f"""
                SELECT g.groupname, g.variables, h.hostname
                FROM {table_name} g
                LEFT JOIN host_groups hg ON g.id = hg.group_id
                LEFT JOIN hosts h ON hg.host_id = h.id
                WHERE g.active = 1
            """)
            
            groups_data = {}
            for row in cursor.fetchall():
                groupname = row['groupname']
                hostname = row['hostname']
                variables_json = row['variables'] or '{}'
                
                # Gruppe erstellen falls nicht vorhanden
                if groupname not in groups_data:
                    groups_data[groupname] = {
                        'hosts': [],
                        'variables': {}
                    }
                    self.inventory.add_group(groupname)
                    
                    # Gruppenvariablen setzen
                    try:
                        variables = json.loads(variables_json)
                        for key, value in variables.items():
                            self.inventory.set_variable(groupname, key, value)
                    except json.JSONDecodeError:
                        self.display.warning(f"Ungültiges JSON für Gruppe {groupname}")
                
                # Host zur Gruppe hinzufügen
                if hostname:
                    self.inventory.add_host(hostname, group=groupname)
                    
        except sqlite3.Error as e:
            raise AnsibleError(f"Fehler beim Laden der Gruppen: {e}")

    def _populate_from_cache(self, cache_data):
        """Lädt Daten aus dem Cache"""
        # Implementierung für Cache-basierte Wiederherstellung
        pass

23.3.1.2 2. Plugin-Konfiguration inventory/database_inventory.yml

plugin: database_inventory
database_path: /path/to/inventory.db
host_table: hosts
group_table: groups

# Optional: Caching aktivieren
cache: true
cache_plugin: jsonfile
cache_timeout: 300
cache_connection: /tmp/ansible_inventory_cache

23.3.1.3 3. Datenbankschema (SQLite)

-- Hosts-Tabelle
CREATE TABLE hosts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    hostname VARCHAR(255) UNIQUE NOT NULL,
    ip_address VARCHAR(45),
    variables TEXT,  -- JSON-String
    active INTEGER DEFAULT 1,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Groups-Tabelle
CREATE TABLE groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    groupname VARCHAR(255) UNIQUE NOT NULL,
    variables TEXT,  -- JSON-String
    active INTEGER DEFAULT 1,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Host-Group-Zuordnungen
CREATE TABLE host_groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    host_id INTEGER,
    group_id INTEGER,
    FOREIGN KEY (host_id) REFERENCES hosts(id),
    FOREIGN KEY (group_id) REFERENCES groups(id),
    UNIQUE(host_id, group_id)
);

-- Beispieldaten
INSERT INTO hosts (hostname, ip_address, variables) VALUES 
('web-01', '192.168.1.10', '{"server_role": "web", "nginx_version": "1.18"}'),
('web-02', '192.168.1.11', '{"server_role": "web", "nginx_version": "1.18"}'),
('db-01', '192.168.1.20', '{"server_role": "database", "mysql_version": "8.0"}');

INSERT INTO groups (groupname, variables) VALUES 
('webservers', '{"http_port": 80, "https_port": 443}'),
('databases', '{"mysql_port": 3306}');

INSERT INTO host_groups (host_id, group_id) VALUES 
(1, 1), (2, 1), (3, 2);

23.3.2 Plugin-Installation und -Konfiguration

23.3.2.1 1. ansible.cfg erweitern

[defaults]
inventory_plugins = ./inventory_plugins
enable_plugins = auto, host_list, script, yaml, ini, database_inventory

[inventory]
cache = True
cache_plugin = jsonfile
cache_timeout = 3600
cache_connection = /tmp/ansible_inventory_cache

23.3.2.2 2. Plugin verwenden

# Plugin testen
ansible-inventory -i inventory/database_inventory.yml --list

# Spezifischen Host anzeigen
ansible-inventory -i inventory/database_inventory.yml --host web-01

# Plugin-Dokumentation anzeigen
ansible-doc -t inventory database_inventory

23.4 Integration in bestehende Ansible-Umgebungen

23.4.1 Best Practices für Produktionsumgebungen

23.4.1.1 1. Schrittweise Migration

# Phase 1: Parallel testing
ansible-inventory -i inventory/static.ini,inventory/aws_ec2.yml --list

# Phase 2: Graduelle Übernahme einzelner Hostgruppen
ansible-playbook site.yml -i inventory/aws_ec2.yml --limit webservers

# Phase 3: Vollständige Migration
ansible-playbook site.yml -i inventory/

23.4.1.2 2. Fehlerbehandlung und Debugging

# In Plugin-Konfiguration
strict: false  # Ignoriert Fehler bei Variablen-Zusammensetzung
compose:
  ansible_host: public_ip_address | default(private_ip_address, true)
# Debugging aktivieren
ANSIBLE_DEBUG=1 ansible-inventory -i inventory/aws_ec2.yml --list

# Verbosity erhöhen
ansible-inventory -i inventory/aws_ec2.yml --list -vvv

23.4.1.3 3. Performance-Optimierung

# Caching für bessere Performance
cache: true
cache_plugin: redis
cache_timeout: 1800
cache_connection: redis://localhost:6379/1

# Parallele API-Calls (nur bei unterstützten Plugins)
ansible_host_key_checking: false

23.4.1.4 4. Sicherheitsüberlegungen

# Credentials aus Umgebungsvariablen
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_SESSION_TOKEN="your-session-token"

# Alternativ: AWS CLI Profile
export AWS_PROFILE="production"

# GCP Service Account
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
# In Plugin-Konfiguration: Keine Credentials hardcoden!
# Schlecht:
# access_key: AKIAIOSFODNN7EXAMPLE

# Gut: Umgebungsvariablen oder IAM Roles verwenden
# (Plugin nutzt automatisch AWS CLI/SDK Credential Chain)

23.4.1.5 5. Monitoring und Alerting

# Inventory-Validierung als Pre-Deployment Check
ansible-inventory -i inventory/ --list > /dev/null
if [ $? -ne 0 ]; then
    echo "FEHLER: Inventory konnte nicht geladen werden"
    exit 1
fi

# Anzahl Hosts überwachen
HOST_COUNT=$(ansible-inventory -i inventory/ --list | jq '._meta.hostvars | length')
if [ $HOST_COUNT -lt 10 ]; then
    echo "WARNUNG: Ungewöhnlich wenige Hosts gefunden ($HOST_COUNT)"
fi

23.4.2 Troubleshooting häufiger Probleme

23.4.2.1 Plugin wird nicht erkannt

# Plugin-Pfade prüfen
ansible-config dump | grep INVENTORY_PLUGINS

# Plugin-Liste anzeigen
ansible-doc -t inventory -l | grep your_plugin

23.4.2.2 Authentifizierung fehlgeschlagen

# AWS Credentials testen
aws sts get-caller-identity

# GCP Credentials testen
gcloud auth list

23.4.2.3 Performance-Probleme

# Cache-Status prüfen
ls -la /tmp/ansible_inventory_cache/

# Cache löschen
rm -rf /tmp/ansible_inventory_cache/*

23.5 Erweiterte Funktionen

23.5.1 Multi-Source Inventories

# inventory/combined.yml
plugin: advanced
sources:
  - plugin: aws_ec2
    regions: ['us-west-1', 'us-west-2']
  - plugin: gcp_compute
    projects: ['my-project']
  - plugin: azure_rm
    subscription_id: 'your-subscription-id'

# Namespace-Trennung
keyed_groups:
  - key: inventory_source
    prefix: source

23.5.2 Testing und Validierung

# tests/test_database_inventory.py
import pytest
import tempfile
import sqlite3
from ansible.inventory.manager import InventoryManager
from ansible.parsing.dataloader import DataLoader

def test_database_inventory_plugin():
    # Temporäre Datenbank erstellen
    with tempfile.NamedTemporaryFile(suffix='.db') as db_file:
        # Schema und Testdaten erstellen
        conn = sqlite3.connect(db_file.name)
        conn.execute('''CREATE TABLE hosts (
            hostname TEXT, ip_address TEXT, variables TEXT, active INTEGER
        )''')
        conn.execute('''INSERT INTO hosts VALUES 
            ('test-host', '192.168.1.1', '{}', 1)''')
        conn.commit()
        conn.close()
        
        # Plugin testen
        loader = DataLoader()
        inventory = InventoryManager(loader=loader, sources=[f'database_inventory.yml'])
        
        assert 'test-host' in inventory.hosts
        assert inventory.get_host('test-host').vars['ansible_host'] == '192.168.1.1'