""" Comprehensive tests for Stage 6 - Autonomous Deployment Engine. Tests cover: - Multi-vendor device connectivity - Config template generation - Pre/post validation checks - Automatic rollback - Parallel deployment - Error handling """ import pytest import sys from pathlib import Path # Add parent directory to path for imports sys.path.insert(0, str(Path(__file__).parent.parent)) from agent.device_driver import ( DeviceDriver, DeviceCredentials, DeviceType, ConnectionStatus, CommandResult ) from agent.config_templates import ConfigTemplateEngine from agent.deployment_engine import ( DeploymentEngine, DeploymentTask, DeploymentStatus ) class TestDeviceDriver: """Test multi-vendor device connectivity""" def test_driver_initialization(self): """Test driver initializes correctly""" driver = DeviceDriver(use_napalm=True) assert driver is not None assert hasattr(driver, 'connections') def test_cisco_ios_connection(self): """Test Cisco IOS device connection""" driver = DeviceDriver(use_napalm=True) creds = DeviceCredentials( hostname="192.168.1.1", username="admin", password="cisco", device_type=DeviceType.CISCO_IOS, port=22 ) conn = driver.connect(creds) assert conn is not None assert conn.status in [ConnectionStatus.CONNECTED, ConnectionStatus.FAILED] def test_arista_eos_connection(self): """Test Arista EOS device connection""" driver = DeviceDriver(use_napalm=True) creds = DeviceCredentials( hostname="192.168.1.10", username="admin", password="arista", device_type=DeviceType.ARISTA_EOS, port=22 ) conn = driver.connect(creds) assert conn is not None def test_connection_pooling(self): """Test connection reuse""" driver = DeviceDriver(use_napalm=True) creds = DeviceCredentials( hostname="192.168.1.1", username="admin", password="cisco", device_type=DeviceType.CISCO_IOS ) conn1 = driver.connect(creds) conn2 = driver.connect(creds) # Should reuse same connection if first one succeeded # In mock mode or on success, connections are pooled if conn1.status == ConnectionStatus.CONNECTED: assert conn1 is conn2 else: # Failed connections don't get pooled, they're retried assert conn1 is not None and conn2 is not None def test_send_command(self): """Test command execution""" driver = DeviceDriver(use_napalm=True) creds = DeviceCredentials( hostname="192.168.1.1", username="admin", password="cisco", device_type=DeviceType.CISCO_IOS ) conn = driver.connect(creds) if conn.status == ConnectionStatus.CONNECTED: result = driver.send_command(creds, "show version") assert isinstance(result, CommandResult) assert result.command == "show version" class TestConfigTemplates: """Test configuration template generation""" def test_template_engine_initialization(self): """Test template engine initializes""" engine = ConfigTemplateEngine() assert engine is not None def test_cisco_ios_l2_template(self): """Test Cisco IOS L2 switch template""" engine = ConfigTemplateEngine() device = { 'name': 'SW1', 'mgmt_ip': '192.168.1.10', 'interfaces': [ {'name': 'GigabitEthernet0/1', 'description': 'Uplink'}, {'name': 'GigabitEthernet0/2', 'description': 'Access'} ] } vlans = [ {'id': 10, 'name': 'Data'}, {'id': 20, 'name': 'Voice'} ] context = { 'device': device, 'vlans': vlans, 'domain_name': 'lab.local', 'ntp_servers': ['192.168.1.1'], 'dns_servers': ['8.8.8.8', '8.8.4.4'] } config = engine.render_template('cisco_ios_l2_switch', context) assert config is not None assert 'hostname SW1' in config assert 'vlan 10' in config assert 'ip domain-name lab.local' in config def test_cisco_ios_l3_template(self): """Test Cisco IOS L3 router template""" engine = ConfigTemplateEngine() device = { 'name': 'R1', 'mgmt_ip': '192.168.1.1', 'interfaces': [ {'name': 'GigabitEthernet0/0', 'ip': '10.0.0.1/24'}, {'name': 'GigabitEthernet0/1', 'ip': '10.0.1.1/24'} ] } routing = { 'protocol': 'ospf', 'process_id': 1, 'networks': [ {'network': '10.0.0.0', 'wildcard': '0.0.0.255', 'area': 0}, {'network': '10.0.1.0', 'wildcard': '0.0.0.255', 'area': 0} ] } context = { 'device': device, 'routing': routing, 'domain_name': 'lab.local' } config = engine.render_template('cisco_ios_l3_router', context) assert config is not None assert 'hostname R1' in config assert 'router ospf 1' in config def test_arista_eos_template(self): """Test Arista EOS template""" engine = ConfigTemplateEngine() device = { 'name': 'ARISTA1', 'mgmt_ip': '192.168.1.20', 'interfaces': [] } context = { 'device': device, 'domain_name': 'lab.local' } config = engine.render_template('arista_eos', context) assert config is not None assert 'hostname ARISTA1' in config def test_juniper_junos_template(self): """Test Juniper JunOS template""" engine = ConfigTemplateEngine() device = { 'name': 'JUNIPER1', 'mgmt_ip': '192.168.1.30', 'interfaces': [] } context = { 'device': device, 'domain_name': 'lab.local' } config = engine.render_template('juniper_junos', context) assert config is not None assert 'host-name JUNIPER1' in config # JunOS uses hierarchical syntax def test_generate_device_config(self): """Test automatic template selection""" engine = ConfigTemplateEngine() device = { 'name': 'AUTO1', 'vendor': 'cisco', 'model': 'catalyst', 'role': 'switch', 'mgmt_ip': '192.168.1.40', 'interfaces': [] } context = {'device': device} config = engine.generate_device_config(device, context) # Should auto-select cisco_ios_l2_switch template assert config is not None assert 'hostname AUTO1' in config class TestDeploymentEngine: """Test deployment orchestration""" def test_engine_initialization(self): """Test deployment engine initializes""" engine = DeploymentEngine(use_napalm=True) assert engine is not None assert hasattr(engine, 'driver') assert hasattr(engine, 'template_engine') def test_single_device_dry_run(self): """Test single device deployment in dry-run mode""" engine = DeploymentEngine(use_napalm=True) task = DeploymentTask( device_id="SW1", device_type=DeviceType.CISCO_IOS, hostname="192.168.1.10", username="admin", password="cisco", config="hostname SW1\nip domain-name lab.local", dry_run=True, pre_checks=["command:show version"], post_checks=["interface:GigabitEthernet0/1"] ) result = engine.deploy_single_device(task) assert result is not None assert result.device_id == "SW1" assert result.status in [DeploymentStatus.SUCCESS, DeploymentStatus.FAILED] def test_validation_checks(self): """Test pre/post validation checks""" engine = DeploymentEngine(use_napalm=True) task = DeploymentTask( device_id="R1", device_type=DeviceType.CISCO_IOS, hostname="192.168.1.1", username="admin", password="cisco", config="hostname R1", dry_run=True, pre_checks=[ "ping:192.168.1.1", "command:show version" ], post_checks=[ "interface:GigabitEthernet0/0", "command:show ip interface brief" ] ) result = engine.deploy_single_device(task) assert result is not None assert 'pre_check_results' in result.__dict__ assert 'post_check_results' in result.__dict__ def test_multiple_device_deployment(self): """Test deploying to multiple devices""" engine = DeploymentEngine(use_napalm=True) tasks = [ DeploymentTask( device_id=f"SW{i}", device_type=DeviceType.CISCO_IOS, hostname=f"192.168.1.{10+i}", username="admin", password="cisco", config=f"hostname SW{i}", dry_run=True ) for i in range(1, 4) ] results = engine.deploy_multiple_devices(tasks, parallel=False) assert len(results) == 3 assert all(isinstance(r.device_id, str) for r in results) def test_generate_and_deploy(self): """Test template generation + deployment""" engine = DeploymentEngine(use_napalm=True) device = { 'name': 'TEST-SW1', 'vendor': 'cisco', 'model': 'catalyst', 'role': 'switch', 'mgmt_ip': '192.168.1.50', 'interfaces': [] } network_context = { 'vlans': [{'id': 10, 'name': 'Test'}], 'domain_name': 'test.local' } credentials = { 'username': 'admin', 'password': 'cisco', 'device_type': DeviceType.CISCO_IOS } result = engine.generate_and_deploy( device=device, network_context=network_context, credentials=credentials, dry_run=True ) assert result is not None assert result.device_id == 'TEST-SW1' def test_rollback_on_failure(self): """Test automatic rollback when deployment fails""" engine = DeploymentEngine(use_napalm=True) # Task with impossible post-check to force failure task = DeploymentTask( device_id="FAIL-TEST", device_type=DeviceType.CISCO_IOS, hostname="192.168.1.99", username="admin", password="cisco", config="hostname FAIL-TEST", dry_run=False, # Real deployment to test rollback post_checks=["command:show impossible-command"] ) result = engine.deploy_single_device(task) # Should either fail or rollback if result.status == DeploymentStatus.FAILED: # Check if rollback was attempted assert result.rolled_back or result.error is not None class TestErrorHandling: """Test error handling and edge cases""" def test_invalid_device_type(self): """Test handling of invalid device type""" driver = DeviceDriver(use_napalm=True) # This should handle gracefully try: creds = DeviceCredentials( hostname="192.168.1.1", username="admin", password="test", device_type=DeviceType.GENERIC_SSH ) conn = driver.connect(creds) assert conn is not None except Exception as e: # Should not crash assert True def test_unreachable_device(self): """Test handling unreachable device""" driver = DeviceDriver(use_napalm=True) creds = DeviceCredentials( hostname="192.168.255.255", # Unreachable username="admin", password="test", device_type=DeviceType.CISCO_IOS, timeout=2 # Short timeout ) conn = driver.connect(creds) # Should handle timeout gracefully if driver.mock_mode: assert conn.status == ConnectionStatus.CONNECTED else: assert conn.status == ConnectionStatus.FAILED assert conn.last_error is not None def test_authentication_failure(self): """Test handling authentication failure""" driver = DeviceDriver(use_napalm=True) creds = DeviceCredentials( hostname="192.168.1.1", username="wrong", password="wrong", device_type=DeviceType.CISCO_IOS ) conn = driver.connect(creds) # Should handle auth failure assert conn is not None def test_empty_config_deployment(self): """Test deploying empty config""" engine = DeploymentEngine(use_napalm=True) task = DeploymentTask( device_id="EMPTY", device_type=DeviceType.CISCO_IOS, hostname="192.168.1.1", username="admin", password="cisco", config="", # Empty config dry_run=True ) result = engine.deploy_single_device(task) assert result is not None class TestIntegration: """Integration tests for complete workflows""" def test_full_deployment_workflow(self): """Test complete deployment workflow end-to-end""" # 1. Initialize components driver = DeviceDriver(use_napalm=True) template_engine = ConfigTemplateEngine() deployment_engine = DeploymentEngine(use_napalm=True) # 2. Define device device = { 'name': 'INTEGRATION-SW1', 'vendor': 'cisco', 'model': 'catalyst', 'role': 'switch', 'mgmt_ip': '192.168.1.100', 'interfaces': [ {'name': 'GigabitEthernet0/1', 'description': 'Test'} ] } # 3. Generate config from template context = { 'device': device, 'vlans': [{'id': 100, 'name': 'Integration-Test'}], 'domain_name': 'integration.test' } config = template_engine.generate_device_config(device, context) assert config is not None assert 'hostname INTEGRATION-SW1' in config # 4. Create deployment task task = DeploymentTask( device_id=device['name'], device_type=DeviceType.CISCO_IOS, hostname=device['mgmt_ip'], username='admin', password='cisco', config=config, dry_run=True, pre_checks=['command:show version'], post_checks=['interface:GigabitEthernet0/1'] ) # 5. Deploy result = deployment_engine.deploy_single_device(task) # 6. Verify result assert result is not None assert result.device_id == 'INTEGRATION-SW1' assert result.config_deployed is not None or result.error is not None def test_multi_vendor_deployment(self): """Test deploying to multiple vendor devices""" engine = DeploymentEngine(use_napalm=True) devices = [ { 'name': 'CISCO-SW1', 'vendor': 'cisco', 'mgmt_ip': '192.168.1.10', 'device_type': DeviceType.CISCO_IOS }, { 'name': 'ARISTA-SW1', 'vendor': 'arista', 'mgmt_ip': '192.168.1.20', 'device_type': DeviceType.ARISTA_EOS }, { 'name': 'JUNIPER-R1', 'vendor': 'juniper', 'mgmt_ip': '192.168.1.30', 'device_type': DeviceType.JUNIPER_JUNOS } ] tasks = [] for dev in devices: task = DeploymentTask( device_id=dev['name'], device_type=dev['device_type'], hostname=dev['mgmt_ip'], username='admin', password='admin', config=f"hostname {dev['name']}", dry_run=True ) tasks.append(task) results = engine.deploy_multiple_devices(tasks, parallel=False) assert len(results) == 3 assert all(r.device_id in [d['name'] for d in devices] for r in results) if __name__ == '__main__': # Run tests print("Running Stage 6 Deployment Engine Tests...") print("=" * 60) # Run with pytest if available, otherwise run basic tests try: import pytest pytest.main([__file__, '-v', '--tb=short']) except ImportError: print("pytest not installed, running basic tests...") # Run basic initialization tests print("\n1. Testing DeviceDriver initialization...") driver = DeviceDriver(use_napalm=True) print(f" ✓ DeviceDriver: netmiko={driver.use_netmiko}, napalm={driver.use_napalm}") print("\n2. Testing ConfigTemplateEngine...") engine = ConfigTemplateEngine() print(f" ✓ ConfigTemplateEngine initialized") print("\n3. Testing DeploymentEngine...") deployer = DeploymentEngine(use_napalm=True) print(f" ✓ DeploymentEngine initialized") print("\n4. Testing template generation...") device = {'name': 'TEST-SW1', 'mgmt_ip': '192.168.1.1', 'interfaces': []} context = {'device': device, 'vlans': [{'id': 10, 'name': 'Test'}]} config = engine.render_template('cisco_ios_l2_switch', context) print(f" ✓ Generated {len(config)} chars of config") assert 'hostname TEST-SW1' in config print("\n5. Testing dry-run deployment...") task = DeploymentTask( device_id="TEST-SW1", device_type=DeviceType.CISCO_IOS, hostname="192.168.1.1", username="admin", password="cisco", config=config, dry_run=True ) result = deployer.deploy_single_device(task) print(f" ✓ Deployment result: {result.status.value}") print("\n" + "=" * 60) print("✅ All basic tests passed!")