Spaces:
Sleeping
Sleeping
| """ | |
| 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!") | |