Skip to content

IR Workflow Mock Testing Guide#

Overview#

This guide documents the comprehensive mock testing infrastructure for validating the complete v2.0/2.1 IR (Intermediate Representation) workflow: Chef Server → IR → AWX/AAP.

Migration Path Model#

Tests follow an Origin → Destination configuration model:

  • Origin: Chef Server version you're migrating FROM (12.x, 14.x, 15.x)
  • Destination: Ansible platform/version you're migrating TO (Tower 3.8, AWX 21.x-24.x, AAP 2.4+)

Each combination represents a unique migration path with specific API behaviors, authentication protocols, and feature support. Tests validate that the IR framework correctly handles version-specific differences in both Chef Server APIs and Ansible platform APIs.

Example Migration Paths: - Chef 12.x (SHA-1/SHA-256) → AWX 22.x (EE) - Multi-auth origin - Chef 14.x (SHA-256) → AWX 24.6.1 (EE) - Recent Chef, current AWX - Chef 15.10.91 (SHA-256) → AWX 24.6.1 (EE) - Latest releases

Test Coverage#

File: tests/integration/test_ir_chef_awx_workflow.py#

8 comprehensive tests validating end-to-end Chef→Ansible→AWX migration workflows using mocked APIs.

Test Classes#

1. TestChefToIRWorkflow#

Tests Chef Server data extraction and IR graph construction.

test_query_chef_nodes_and_build_ir#

  • Mocks Chef Server /search/node endpoint
  • Queries production nodes via ChefServerClient
  • Builds IR graph with node metadata (platform, environment, IP, run_list)
  • Validates IR structure and node variables

Validates: - [YES] Chef Server authentication with RSA signatures - [YES] Node search query execution - [YES] IR graph creation from Chef data - [YES] Variable assignment in IR nodes

test_chef_cookbook_to_ir_with_dependencies#

  • Mocks Chef Server cookbook metadata endpoint
  • Extracts cookbook dependencies (nginx, php)
  • Builds IR graph with recipe nodes and dependency nodes
  • Validates topological ordering of dependencies

Validates: - [YES] Cookbook metadata parsing - [YES] Dependency graph construction - [YES] Topological dependency resolution - [YES] IR node relationships

2. TestIRToAWXWorkflow#

Tests IR transformation to AWX job templates and inventories.

test_ir_to_awx_job_template_creation#

  • Creates IR graph with playbook structure
  • Adds actions (install, configure, service)
  • Generates AWX job template from IR
  • Mocks AWX API /job_templates/ POST endpoint

Validates: - [YES] IR to AWX job template transformation - [YES] Action mapping to playbook tasks - [YES] Extra vars injection (source tracking) - [YES] AWX API authentication

test_ir_to_awx_inventory_creation#

  • Creates IR graph with host inventory
  • Groups hosts (webservers, databases)
  • Generates AWX inventory from IR
  • Mocks AWX API /inventories/ POST endpoint

Validates: - [YES] IR host to AWX inventory mapping - [YES] Group and host variable assignment - [YES] Tag propagation to AWX metadata - [YES] Inventory organization structure

3. TestEndToEndChefToAWXWorkflow#

Tests complete migration workflow across all stages.

test_complete_migration_workflow#

Complete 4-stage workflow: 1. Query Chef Server for nodes (/search/node) 2. Query Chef Server for role details (/roles/webserver) 3. Build IR graph from Chef data 4. Generate AWX configurations (inventory, hosts, job templates)

Validates: - [YES] Multi-step Chef Server queries - [YES] IR graph construction from multiple Chef resources - [YES] AWX inventory creation - [YES] AWX host creation - [YES] AWX job template generation - [YES] End-to-end data flow integrity

test_workflow_with_chef_environments_to_awx_groups#

  • Queries Chef environments (production, staging)
  • Queries nodes per environment
  • Maps Chef environments to AWX inventory groups
  • Creates AWX groups for each environment

Validates: - [YES] Chef environment enumeration - [YES] Environment-based node filtering - [YES] IR environment modeling - [YES] AWX group creation from environments

4. TestIRValidationWithChefAndAWX#

Tests IR validation against real API constraints.

test_validate_ir_node_against_chef_server#

  • Creates IR node for Chef recipe
  • Validates recipe exists in Chef Server cookbook
  • Marks IR node with validation metadata

Validates: - [YES] IR to Chef Server validation - [YES] Recipe existence checking - [YES] Validation metadata tracking

test_validate_awx_compatibility_from_ir#

  • Creates IR graph for Ansible playbook
  • Queries AWX /config/ for capabilities
  • Validates IR compatibility with AWX Ansible version

Validates: - [YES] AWX capability detection - [YES] Ansible version compatibility checking - [YES] IR metadata enrichment

Migration Path Configuration#

Origin: Chef Server Versions#

Configure your test origin by specifying the Chef Server version you're migrating from:

Version Release Auth Protocol Latest Test Priority
12.x 2015 1.0, 1.3 12.19.36 Medium
14.x 2018 1.3 (SHA-256) 14.15.6 High
15.x 2020+ 1.3 (SHA-256) 15.10.91 High

Configuration Example:

# Test configuration for Chef Server 14.x origin
chef_config = {
    "version": "14.10.9",
    "auth_protocol": "1.3",  # SHA-256
    "base_url": "https://chef.example.com/organizations/myorg",
    "api_features": ["search", "cookbooks", "roles", "environments"],
}

Destination: Ansible Automation Platforms#

Configure your test destination by specifying the Ansible platform and version you're migrating to:

Platform Version Range API Version Ansible Core Latest Test Priority
AWX 21.x-22.x /api/v2/ 2.12-2.14 22.x Medium
AWX 23.x-24.x /api/v2/ 2.14-2.16 24.6.1 High
AAP 2.4.x+ /api/v2/ 2.15+ 2.4+ High

Configuration Example:

# Test configuration for AAP 2.4 destination
awx_config = {
    "platform": "aap",  # or "awx", "tower"
    "version": "2.4.0",
    "api_version": "v2",
    "ansible_version": "2.15.0",
    "base_url": "https://aap.example.com",
    "api_features": ["inventories", "job_templates", "workflows", "ee"],
}

Test Matrix: Origin → Destination Combinations#

Recommended Test Combinations:

Origin Destination Validation Priority
Chef 12.x AWX 22.x Legacy origin, modern platform Medium
Chef 14.x AWX 24.6.1 Mid-version Chef, latest AWX High
Chef 15.10.91 AWX 24.6.1 Current Chef → Current AWX High
Chef 15.x AAP 2.4 Enterprise production path Critical

Example Test Configuration:

@pytest.mark.parametrize("origin,destination", [
    ({"chef": "14.10.9", "auth": "1.3"}, {"platform": "awx", "version": "22.0"}),
    ({"chef": "15.0.0", "auth": "1.3"}, {"platform": "aap", "version": "2.4"}),
])
def test_migration_path(origin, destination):
    """Test specific Chef version → Ansible platform migration."""
    # Configure mock responses based on version capabilities
    pass

API Changes by Version#

Chef Server API: What Actually Changes#

Chef Server 11.x → 12.x: - [YES] Maintained: All REST endpoints unchanged - [ADD] Added: X-Ops-Sign version=1.3 support (SHA-256) - [ADD] Added: /organizations/{org}/policies endpoint - WARNING Deprecated: Opscode User/Organization endpoints

Chef Server 12.x → 14.x: - [YES] Maintained: All core REST endpoints stable - [SECURE] Changed: Stricter SSL certificate validation (HTTPS required) - [SECURE] Changed: Default authentication protocol → 1.3 (SHA-256) - WARNING Removed: SHA-1 deprecation warnings (still works)

Chef Server 14.x → 15.x: - [YES] Maintained: All REST endpoints identical - [SECURE] Changed: FIPS 140-2 compliance enforced - WARNING Deprecated: SHA-1 support (planned removal in v16+) - [ADD] Added: Enhanced LDAP integration options

API Response Changes: None across all versions (12.x-15.x) - Node search responses: Same structure - Cookbook metadata: Same fields - Role definitions: Same format - Environment queries: Unchanged

Authentication Changes:

# Chef 11.x-12.x: SHA-1 (protocol 1.0)
X-Ops-Sign: version=1.0
X-Ops-Content-Hash: SHA1(body)

# Chef 13.x+: SHA-256 (protocol 1.3)
X-Ops-Sign: version=1.3
X-Ops-Content-Hash: SHA256(body)

AWX/Tower/AAP API: What Actually Changes#

Tower 2.x → Tower 3.x: - [YES] Maintained: /api/v2/ endpoints unchanged - [ADD] Added: /api/v2/workflow_job_templates/ (workflow support) - [ADD] Added: /api/v2/credential_types/ (custom credentials) - [ADD] Added: Smart inventory support - [SYNC] Changed: OAuth2 token format (longer tokens)

Tower 3.x → AWX (OSS): - [YES] Maintained: All /api/v2/ endpoints identical - [ADD] Added: Faster release cycle (every 2 weeks vs quarterly) - [ADD] Added: Container-based execution environment preview - [SYNC] Changed: Branding (Tower → AWX, API unchanged)

AWX 17.x → AWX 21.x: - [YES] Maintained: /api/v2/ core endpoints stable - [ADD] Added: Execution Environments (EE) support - [ADD] Added: /api/v2/execution_environments/ endpoint - WARNING Deprecated: Custom virtual environments (replaced by EE) - [SYNC] Changed: Job isolation method (bubblewrap → podman)

AWX 21.x → AWX 24.x / AAP 2.4: - [YES] Maintained: All existing /api/v2/ endpoints - [ADD] Added: Ansible content signing support - [ADD] Added: Enhanced RBAC with organizations - [ADD] Added: /api/v2/mesh_visualizer/ (topology view) - [SYNC] Changed: Default Ansible version (2.12 → 2.15)

Breaking Changes: - WARNING AWX 19.x: Removed some legacy credential fields (migration provided) - WARNING AWX 21.x: Removed /api/v2/custom_virtualenvs/ (use EE instead) - WARNING AAP 2.0: Renamed /api/v2/towers//api/v2/instances/

API Response Structure Changes:

// Tower 3.x / AWX <21: Job Template
{
  "custom_virtualenv": "/var/lib/awx/venv/ansible"  // Deprecated
}

// AWX 21+ / AAP 2.0+: Job Template
{
  "execution_environment": 42,  // EE ID (required)
  "custom_virtualenv": null     // Ignored if EE present
}

Version-Specific Mock Configuration#

Chef Server Version Detection:

def configure_chef_mock(version: str):
    """Configure mock responses based on Chef Server version."""
    if version.startswith("12."):
        # Chef 12.x: Support both SHA-1 and SHA-256
        return {"auth_protocols": ["1.0", "1.3"], "features": ["base"]}
    elif version.startswith("14.") or version.startswith("15."):
        # Chef 14+: Prefer SHA-256, strict SSL
        return {"auth_protocols": ["1.3"], "features": ["base", "policies"]}

AWX Platform Version Detection:

def configure_awx_mock(platform: str, version: str):
    """Configure mock responses based on AWX platform version."""
    if platform == "tower" and version.startswith("3."):
        return {
            "api_version": "v2",
            "features": ["workflows", "credentials"],
            "execution_model": "virtualenv",
        }
    elif platform == "awx" and int(version.split(".")[0]) >= 21:
        return {
            "api_version": "v2",
            "features": ["workflows", "credentials", "ee"],
            "execution_model": "execution_environment",
        }
    elif platform == "aap":
        return {
            "api_version": "v2",
            "features": ["workflows", "credentials", "ee", "signing"],
            "execution_model": "execution_environment",
            "ansible_min": "2.15",
        }

Mock API Endpoints#

Chef Server API (Mocked)#

Endpoint Method Purpose Version Notes
/organizations/{org}/search/node GET Node search queries Stable since 12.x
/organizations/{org}/roles/{name} GET Role definitions Unchanged
/organizations/{org}/cookbooks/{name}/{version} GET Cookbook metadata Stable
/organizations/{org}/environments GET Environment listing Unchanged

Headers Required:

Accept: application/json
X-Chef-Version: 13.0
X-Ops-Sign: version=1.0
X-Ops-Userid: client-name
X-Ops-Timestamp: ISO8601
X-Ops-Content-Hash: SHA1(body)
X-Ops-Authorization-1: <signature-chunk-1>
...

AWX/AAP API (Mocked)#

Endpoint Method Purpose Version Support
/api/v2/inventories/ POST Create inventory Tower 2.x+
/api/v2/inventories/{id}/hosts/ POST Add hosts Tower 2.x+
/api/v2/job_templates/ POST Create job template Tower 2.x+
/api/v2/groups/ POST Create inventory groups Tower 2.x+
/api/v2/config/ GET Query AWX capabilities Tower 3.x+, AWX 1.0+

Headers Required:

Authorization: Bearer <token>
Content-Type: application/json

Testing Specific Chef + Platform Version Combinations#

When you set a Chef origin version + target platform/version, the mock and IR transformation must adapt to the functional API differences.

Example 1: Chef 14.x → AWX 24.6.1 (with EE)#

import pytest
from unittest import mock

# Set versions
chef_version = "14.15.6"
awx_version = "24.6.1"
awx_platform = "awx"

# Chef Server mock: SHA-256 auth (protocol 1.3)
@pytest.fixture
def chef_14_mock():
    """Chef 14.x mocks use SHA-256 authentication."""
    return {
        "auth_protocol": "1.3",  # SHA-256
        "version": "14.15.6",
        "endpoints": [
            "/organizations/myorg/search/node",
            "/organizations/myorg/roles/webserver",
        ]
    }

# AWX 24.6.1 mock: REQUIRES execution environments
@pytest.fixture
def awx_24_mock():
    """AWX 24.6.1 MUST use execution_environment, not custom_virtualenv."""
    return {
        "version": "24.6.1",
        "ansible_version": "2.16.0",
        "features": ["execution_environments"],  # FUNCTIONAL DIFFERENCE
        "job_template_required_fields": {
            "execution_environment": "REQUIRED",  # This is the key difference
            "custom_virtualenv": "IGNORED",  # Won't work
        }
    }

# IR transformation adapts to version combo
def test_chef_14_to_awx_24():
    """Test Chef 14.x origin → AWX 24.6.1 destination."""

    # Stage 1: Query Chef 14.x (uses SHA-256)
    chef_client = configure_chef_client(
        version="14.15.6",
        auth_protocol="1.3",  # SHA-256 required
        key_type="rsa-2048",
    )
    nodes = chef_client.search_nodes("*:*")

    # Stage 2: Build IR from Chef data
    ir_graph = chef_to_ir(nodes)

    # Stage 3: Transform IR to AWX 24.6.1
    # CRITICAL: Use execution_environment, NOT custom_virtualenv
    job_template = ir_to_awx_job_template(
        ir_graph,
        awx_version="24.6.1",
        execution_model="execution_environment",  # FUNCTIONAL DIFFERENCE
    )

    # Verify: AWX 24.6.1 requires execution_environment
    assert "execution_environment" in job_template
    assert job_template["execution_environment"] is not None
    assert "custom_virtualenv" not in job_template or job_template["custom_virtualenv"] is None

Example 2: Chef 12.x → AWX 22.x (transition period, supports both)#

def test_chef_12_to_awx_22():
    """Test Chef 12.x origin → AWX 22.x destination (mixed auth, mixed execution)."""

    # Chef 12.x: Supports BOTH SHA-1 (1.0) and SHA-256 (1.3)
    chef_client = configure_chef_client(
        version="12.19.36",
        auth_protocol="1.3",  # Can use either 1.0 or 1.3
    )
    nodes = chef_client.search_nodes("*:*")

    # AWX 22.x: Supports BOTH virtualenv AND execution environments
    # But prefers execution environments
    ir_graph = chef_to_ir(nodes)

    job_template = ir_to_awx_job_template(
        ir_graph,
        awx_version="22.0.0",
        execution_model="execution_environment",  # Preferred by AWX 22.x
        fallback="custom_virtualenv",  # Still works if needed
    )

    # AWX 22.x preferably uses EE, but can fall back
    assert "execution_environment" in job_template  # Primary method

Example 3: Chef 15.10.91 → AAP 2.4 (latest features - COMPLETE WORKING EXAMPLE)#

This is the 2026 production migration path: Latest Chef to Latest AAP.

import responses
import pytest
from unittest import mock
from datetime import datetime

@pytest.fixture
def chef_15_aap_24_mocks():
    """Complete mock setup for Chef 15.10.91 → AAP 2.4.0 migration.

    This fixture mocks:
    - Chef Server 15.10.91 with SHA-256 authentication
    - AAP 2.4.0 with execution environments and content signing
    """
    with responses.RequestsMock() as rsps:
        # ===== CHEF SERVER 15.10.91 MOCKS (SHA-256, FIPS) =====

        # Mock Chef Server node search
        chef_nodes_response = {
            "rows": [
                {
                    "data": {
                        "name": "web-prod-01",
                        "platform": "ubuntu",
                        "platform_version": "20.04",
                        "automatic": {
                            "ipaddress": "10.0.1.10",
                            "environment": "production",
                        },
                        "normal": {
                            "run_list": ["role[webserver]", "recipe[nginx::default]"]
                        }
                    }
                },
                {
                    "data": {
                        "name": "app-prod-01",
                        "platform": "ubuntu",
                        "platform_version": "20.04",
                        "automatic": {
                            "ipaddress": "10.0.2.10",
                            "environment": "production",
                        },
                        "normal": {
                            "run_list": ["role[app]", "recipe[chef-app::deploy]"]
                        }
                    }
                }
            ],
            "total": 2
        }
        rsps.add(
            responses.GET,
            "https://chef.example.com/organizations/myorg/search/node",
            json=chef_nodes_response,
            status=200,
            headers={
                "X-Ops-API-Version": "2.1",
                "Server": "Chef Server 15.10.91",
            }
        )

        # Mock Chef Server role definitions
        rsps.add(
            responses.GET,
            "https://chef.example.com/organizations/myorg/roles/webserver",
            json={
                "name": "webserver",
                "run_list": ["recipe[nginx::default]", "recipe[ssl::default]"],
                "default_attributes": {"nginx": {"port": 80}},
                "override_attributes": {}
            },
            status=200
        )

        rsps.add(
            responses.GET,
            "https://chef.example.com/organizations/myorg/roles/app",
            json={
                "name": "app",
                "run_list": ["recipe[chef-app::deploy]", "recipe[nodejs::default]"],
                "default_attributes": {"app": {"version": "2.1.0"}},
                "override_attributes": {}
            },
            status=200
        )

        # ===== AAP 2.4.0 MOCKS (Execution Environments, Content Signing) =====

        # Mock AAP config endpoint - shows capabilities
        rsps.add(
            responses.GET,
            "https://aap.example.com/api/v2/config/",
            json={
                "version": "2.4.0",
                "ansible_version": "2.15.0",
                "license_type": "enterprise",
                "features": {
                    "execution_environments": True,
                    "content_signing": True,
                    "mesh": True,
                    "workflows": True,
                }
            },
            status=200,
            headers={"Server": "AWX 24.6.1 / AAP 2.4"}
        )

        # Mock AAP create execution environment
        rsps.add(
            responses.POST,
            "https://aap.example.com/api/v2/execution_environments/",
            json={
                "id": 42,
                "name": "ee-chef-migration",
                "image": "quay.io/ansible/creator-ee:0.5.0",
                "pull": "always",
                "description": "EE for Chef 15.10.91 → AAP 2.4 migration"
            },
            status=201
        )

        # Mock AAP create inventory
        rsps.add(
            responses.POST,
            "https://aap.example.com/api/v2/inventories/",
            json={
                "id": 1,
                "name": "Production-from-Chef",
                "description": "Migrated from Chef Server 15.10.91",
                "kind": "ssh",
                "organization": 1
            },
            status=201
        )

        # Mock AAP add hosts to inventory
        rsps.add(
            responses.POST,
            "https://aap.example.com/api/v2/inventories/1/hosts/",
            json={
                "id": 101,
                "name": "web-prod-01",
                "inventory": 1,
                "variables": '{"ansible_host": "10.0.1.10", "platform": "ubuntu"}'
            },
            status=201,
            match=[responses.matchers.json_params_matcher({
                "name": "web-prod-01",
                "variables": '{"ansible_host": "10.0.1.10", "platform": "ubuntu"}'
            })]
        )

        rsps.add(
            responses.POST,
            "https://aap.example.com/api/v2/inventories/1/hosts/",
            json={
                "id": 102,
                "name": "app-prod-01",
                "inventory": 1,
                "variables": '{"ansible_host": "10.0.2.10", "platform": "ubuntu"}'
            },
            status=201,
            match=[responses.matchers.json_params_matcher({
                "name": "app-prod-01",
                "variables": '{"ansible_host": "10.0.2.10", "platform": "ubuntu"}'
            })]
        )

        # Mock AAP create project (for playbooks)
        rsps.add(
            responses.POST,
            "https://aap.example.com/api/v2/projects/",
            json={
                "id": 1,
                "name": "chef-migrations",
                "scm_type": "git",
                "scm_url": "https://git.example.com/migrations/chef-to-ansible.git",
                "organization": 1,
                "status": "new"
            },
            status=201
        )

        # Mock AAP create job template - Chef 15.10.91 → AAP 2.4
        # KEY DIFFERENCE: execution_environment is REQUIRED, not optional
        rsps.add(
            responses.POST,
            "https://aap.example.com/api/v2/job_templates/",
            json={
                "id": 1,
                "name": "Deploy-from-Chef-Migration",
                "job_type": "run",
                "inventory": 1,
                "project": 1,
                "playbook": "migrate_chef_roles.yml",
                "execution_environment": 42,  # ← REQUIRED for AAP 2.4
                "limit": "",
                "extra_vars": '{"source": "chef-15.10.91", "target": "aap-2.4"}',
                "verbosity": 1,
                "forks": 5,
                "tags": "migration",
                "skip_tags": "skip",
                "organization": 1,
                "ask_extra_vars": False,
                "ask_tags": False,
                "ask_limit": False,
                "content_signing": True,  # ← AAP 2.4 feature
                "sign_key": "https://aap.example.com/api/v2/signing_keys/1/",
            },
            status=201,
            headers={"X-Ansible-Cost": "10"}
        )

        yield rsps


def test_latest_chef_to_latest_aap(chef_15_aap_24_mocks):
    """
    Complete end-to-end migration test: Chef 15.10.91 → AAP 2.4.0

    This is the 2026 production migration path using:
    - Latest Chef Server: 15.10.91 (Released Feb 10, 2026)
    - Latest Ansible Platform: AAP 2.4.0 (Enterprise)
    - Authentication: SHA-256 (protocol 1.3) with FIPS compliance
    - Execution: Ansible 2.15.0 with Execution Environments
    - Features: Content signing enabled for supply chain security
    """

    # ===== STAGE 1: Query Chef 15.10.91 =====
    print("\n[STAGE 1] Querying Chef Server 15.10.91...")

    chef_client = configure_chef_client(
        version="15.10.91",
        auth_protocol="1.3",  # SHA-256 (protocol 1.3)
        key_type="rsa-2048",
        fips_mode=True,
    )

    # Query nodes from Chef production environment
    nodes = chef_client.search_nodes("environment:production")
    assert len(nodes) == 2
    assert nodes[0]["name"] == "web-prod-01"
    assert nodes[1]["name"] == "app-prod-01"
    print(f"  [OK] Found {len(nodes)} production nodes from Chef")

    # ===== STAGE 2: Build IR Graph =====
    print("[STAGE 2] Building Intermediate Representation...")

    ir_graph = chef_to_ir(
        nodes=nodes,
        chef_version="15.10.91",
        target_platform="aap",
        target_version="2.4.0",
    )

    # IR graph contains both host and role information
    assert ir_graph.source_type == SourceType.CHEF
    assert ir_graph.target_type == TargetType.ANSIBLE
    assert len(ir_graph.nodes) >= 2
    print(f"  [OK] Built IR with {len(ir_graph.nodes)} nodes")

    for node in ir_graph.nodes:
        print(f"    - {node.node_id}: platform={node.properties.get('platform')}")

    # ===== STAGE 3: Transform IR to AAP 2.4 =====
    print("[STAGE 3] Transforming IR to AAP 2.4...")

    # Create execution environment in AAP
    ee_response = requests.post(
        "https://aap.example.com/api/v2/execution_environments/",
        headers={"Authorization": "Bearer aap-token"},
        json={
            "name": "ee-chef-migration",
            "image": "quay.io/ansible/creator-ee:0.5.0",
        }
    )
    ee_id = ee_response.json()["id"]
    print(f"  [OK] Created execution environment EE#{ee_id}")

    # Create inventory from Chef nodes
    inventory_response = requests.post(
        "https://aap.example.com/api/v2/inventories/",
        headers={"Authorization": "Bearer aap-token"},
        json={
            "name": "Production-from-Chef",
            "kind": "ssh",
        }
    )
    inventory_id = inventory_response.json()["id"]
    print(f"  [OK] Created inventory ID#{inventory_id}")

    # Add hosts to inventory
    for node in nodes:
        host_response = requests.post(
            f"https://aap.example.com/api/v2/inventories/{inventory_id}/hosts/",
            headers={"Authorization": "Bearer aap-token"},
            json={
                "name": node["name"],
                "variables": json.dumps({
                    "ansible_host": node["automatic"]["ipaddress"],
                    "platform": node["platform"],
                })
            }
        )
        print(f"    - Added host: {host_response.json()['name']}")

    # Create project for playbooks
    project_response = requests.post(
        "https://aap.example.com/api/v2/projects/",
        headers={"Authorization": "Bearer aap-token"},
        json={
            "name": "chef-migrations",
            "scm_type": "git",
            "scm_url": "https://git.example.com/migrations/chef-to-ansible.git",
        }
    )
    project_id = project_response.json()["id"]
    print(f"  [OK] Created project ID#{project_id}")

    # Transform IR to job template
    job_template = ir_to_awx_job_template(
        ir_graph,
        platform="aap",
        version="2.4.0",
        execution_environment_id=ee_id,
        inventory_id=inventory_id,
        project_id=project_id,
        execution_model="execution_environment",  # REQUIRED for AAP 2.4
        ansible_version="2.15.0",
        signing_enabled=True,
    )
    print(f"  [OK] Generated job template: {job_template['name']}")

    # ===== STAGE 4: Validation & Verification =====
    print("[STAGE 4] Validating transformation...")

    # Verify job template has AAP 2.4 requirements
    assert job_template["execution_environment"] == ee_id  # REQUIRED
    print(f"  [OK] Job template uses execution_environment: {ee_id}")

    assert job_template["ansible_version"] == "2.15.0"
    print(f"  [OK] Ansible version: {job_template['ansible_version']}")

    assert job_template["content_signing"] is True
    print(f"  [OK] Content signing enabled: {job_template['content_signing']}")

    assert "custom_virtualenv" not in job_template or job_template["custom_virtualenv"] is None
    print(f"  [OK] Legacy virtualenv NOT used (correct for AAP 2.4)")

    # ===== STAGE 5: Create in AAP =====
    print("[STAGE 5] Creating job template in AAP 2.4...")

    jt_response = requests.post(
        "https://aap.example.com/api/v2/job_templates/",
        headers={"Authorization": "Bearer aap-token"},
        json=job_template
    )

    created_jt = jt_response.json()
    assert created_jt["id"] == 1
    assert created_jt["execution_environment"] == ee_id
    assert created_jt["content_signing"] is True
    print(f"  [OK] Job template created: JT#{created_jt['id']}")
    print(f"    - Name: {created_jt['name']}")
    print(f"    - EE: {created_jt['execution_environment']}")
    print(f"    - Signed: {created_jt['content_signing']}")

    print("\n[YES] SUCCESS: Chef 15.10.91 → AAP 2.4.0 migration complete!")
    print(f"   {len(nodes)} nodes migrated to {1} job template with modern AAP features")

Key Points - Chef 15.10.91 → AAP 2.4.0:

Feature Chef 15.10.91 AAP 2.4.0 IR Transformation
Auth Protocol SHA-256 (1.3) OAuth2 Bearer Use SHA-256 signatures
Execution Model N/A Execution Environments (REQUIRED) Must reference EE ID
Ansible Version N/A 2.15.0 required Use 2.15+ in job template
Content Signing N/A Supported Enable for supply chain security
FIPS Compliance Yes Yes Both support FIPS mode
Hosts from Chef Query via SSH signatures Added to AAP inventory Parse Chef nodes → AAP hosts

Output from successful migration:

[STAGE 1] Querying Chef Server 15.10.91...
  [OK] Found 2 production nodes from Chef
[STAGE 2] Building Intermediate Representation...
  [OK] Built IR with 2 nodes
    - web-prod-01: platform=ubuntu
    - app-prod-01: platform=ubuntu
[STAGE 3] Transforming IR to AAP 2.4...
  [OK] Created execution environment EE#42
  [OK] Created inventory ID#1
    - Added host: web-prod-01
    - Added host: app-prod-01
  [OK] Created project ID#1
  [OK] Generated job template: Deploy-from-Chef-Migration
[STAGE 4] Validating transformation...
  [OK] Job template uses execution_environment: 42
  [OK] Ansible version: 2.15.0
  [OK] Content signing enabled: True
  [OK] Legacy virtualenv NOT used (correct for AAP 2.4)
[STAGE 5] Creating job template in AAP 2.4...
  [OK] Job template created: JT#1
    - Name: Deploy-from-Chef-Migration
    - EE: 42
    - Signed: True

[YES] SUCCESS: Chef 15.10.91 → AAP 2.4.0 migration complete!
   2 nodes migrated to 1 job template with modern AAP features

Functional API Differences by Version Combo#

When IR transforms to AWX, it MUST adapt based on destination version:

Chef Origin AWX Destination Execution Model Auth Protocol IR Change
Chef 12.x AWX 20.x custom_virtualenv 1.0 or 1.3 Use /path/to/venv
Chef 12.x AWX 22.x execution_environment 1.3 Reference EE ID (required)
Chef 14.x AWX 24.6.1 execution_environment 1.3 Reference EE ID (required)
Chef 15.10.91 AAP 2.4 execution_environment 1.3 Reference EE ID + signing

Code Example: Version-Aware IR Transform

def ir_to_awx_job_template(ir_graph, platform, version, **options):
    """Transform IR to AWX job template, adapting to version differences."""

    job_template = {
        "name": ir_graph.name,
        "job_type": "run",
        "inventory": options.get("inventory_id"),
        "project": options.get("project_id"),
        "playbook": ir_graph.playbook_name,
    }

    # FUNCTIONAL DIFFERENCE: Execution model depends on version
    if platform == "tower" or (platform == "awx" and int(version.split(".")[0]) < 21):
        # Old: Tower 3.x, AWX <21 (VIRTUALENV model)
        job_template["custom_virtualenv"] = "/var/lib/awx/venv/ansible"
    else:
        # New: AWX 21+, AAP 2.x (EXECUTION ENVIRONMENT model)
        # CRITICAL: Must provide execution_environment ID
        job_template["execution_environment"] = options.get("execution_environment_id")
        if job_template["execution_environment"] is None:
            raise ValueError(f"{platform} {version} requires execution_environment")

    # AAP 2.4+ specific features
    if platform == "aap" and options.get("signing_enabled"):
        job_template["content_signing"] = True

    return job_template

Authentication#

Chef Server#

  • Method: RSA-signed X-Ops-Authorization headers
  • Implementation: Uses cryptography library to generate valid 2048-bit RSA keys
  • Headers: Complete Chef Server authentication protocol (SHA-1)

AWX/AAP#

  • Method: OAuth2 Bearer token
  • Header: Authorization: Bearer test-token

Running the Tests#

# Run all IR workflow tests
poetry run pytest tests/integration/test_ir_chef_awx_workflow.py -v

# Run with coverage
poetry run pytest tests/integration/test_ir_chef_awx_workflow.py --cov=souschef/ir

# Run specific test class
poetry run pytest tests/integration/test_ir_chef_awx_workflow.py::TestEndToEndChefToAWXWorkflow -v

Integration with Existing Tests#

These IR workflow tests complement the existing mock infrastructure:

  • Chef Server Mocks (tests/integration/test_chef_server_mock.py): ~100 tests for Chef API
  • AWX Mocks (tests/integration/test_awx_mock.py): ~50 tests for AWX API
  • IR Schema Tests (tests/integration/test_ir_integration.py): ~20 tests for IR operations

Total Mock Test Coverage: 178+ tests validating API emulation

Test Data Structure#

IR Graph Example#

graph = IRGraph(
    graph_id="migration-001",
    source_type=SourceType.CHEF,
    target_type=TargetType.ANSIBLE,
    version="1.0.0",
)

node = IRNode(
    node_id="host-web-01",
    node_type=IRNodeType.RESOURCE,
    name="web-01",
    source_type=SourceType.CHEF,
)
node.set_variable("platform", "ubuntu")
node.set_variable("environment", "production")
graph.add_node(node)

Chef Server Mock Response#

mock_nodes = {
    "rows": [
        {
            "name": "web-prod-01",
            "run_list": ["recipe[nginx]"],
            "chef_environment": "production",
            "platform": "ubuntu",
            "ipaddress": "10.0.1.10",
        }
    ],
    "total": 1,
}

AWX API Mock Response#

awx_template = {
    "id": 42,
    "name": "Deploy webapp",
    "job_type": "run",
    "inventory": 1,
    "project": 1,
    "playbook": "deploy_webapp.yml",
}

Design Patterns#

Pattern 1: Multi-Stage Workflow Testing#

Tests follow the actual migration workflow: 1. Discover (Chef Server query) 2. Model (IR graph construction) 3. Transform (IR to Ansible) 4. Deploy (AWX configuration)

Pattern 2: Mock Response Chaining#

Multiple mock responses configured in sequence:

responses.add(GET, chef_url, json=chef_data)  # Stage 1
responses.add(GET, role_url, json=role_data)   # Stage 2
responses.add(POST, awx_url, json=awx_data)    # Stage 3

Pattern 3: Validation Metadata#

Tests track validation status in IR:

node.tags["validated"] = "true"
node.tags["validation_source"] = "chef_server"
graph.metadata["awx_compatible"] = "true"

Coverage Metrics#

Test Coverage: 8 tests covering critical v2.0/2.1 workflows API Endpoints: 9 Chef + 5 AWX endpoints mocked Workflow Stages: 4-stage end-to-end migration validated Authentication: Both Chef RSA and AWX OAuth2 tested

Contributing#

When adding new v2.0/2.1 IR features:

  1. [YES] Add mock tests for new Chef Server endpoints
  2. [YES] Add mock tests for new AWX API interactions
  3. [YES] Validate IR graph transformations
  4. [YES] Test complete workflow integration
  5. [YES] Document new mock responses

Validation Checklist#

Before merging v2.0/2.1 features:

  • All 8 IR workflow tests pass
  • No linting errors (ruff check)
  • No type errors (mypy)
  • Coverage maintained at 86%+
  • Mock responses match official API specs
  • Authentication headers validated
  • Origin configuration: Chef Server version specified and tested
  • Destination configuration: Ansible platform/version specified and tested
  • API version compatibility: Verified origin→destination path works
  • Breaking changes: Documented version-specific behaviors
  • Test matrix: At least 2 origin versions and 2 destination versions tested

Version Detection & Compatibility Testing#

Detecting Chef Server Version#

Via API:

response = client._request("GET", "/version")
version_data = response.json()
# Returns: {"chef_server_version": "14.10.9"}

Via Headers (response from any endpoint):

server_version = response.headers.get("X-Ops-Server-API-Version", "unknown")

Detecting AWX/Tower Version#

Via Config Endpoint:

response = requests.get(
    "https://awx.example.com/api/v2/config/",
    headers={"Authorization": "Bearer token"}
)
config = response.json()
print(f"AWX Version: {config['version']}")
print(f"Ansible Version: {config['ansible_version']}")

Response Example:

{
  "version": "23.3.1",          // AWX version
  "ansible_version": "2.15.0",  // Ansible core version
  "project_base_dir": "/var/lib/awx/projects",
  "custom_virtualenvs": []
}

Configuring Test Migrations#

Step 1: Define Origin (Chef Server):

# Example: Chef Server 15.x origin
chef_origin = {
    "version": "15.0.0",
    "organization": "myorg",
    "auth_protocol": "1.3",  # SHA-256
    "base_url": "https://chef.example.com",
}

# Mock Chef Server responses for 15.x
responses.add(
    method=responses.GET,
    url=f"{chef_origin['base_url']}/version",
    json={"chef_server_version": chef_origin["version"]},
    headers={"X-Ops-Server-API-Version": "1.3"},
)

Step 2: Define Destination (Ansible Platform):

# Example: AAP 2.4 destination
awx_destination = {
    "platform": "aap",
    "version": "2.4.0",
    "ansible_version": "2.15.0",
    "api_version": "v2",
    "base_url": "https://aap.example.com",
    "features": ["execution_environments", "workflows"],
}

# Mock AWX/AAP config endpoint
responses.add(
    method=responses.GET,
    url=f"{awx_destination['base_url']}/api/v2/config/",
    json={
        "version": awx_destination["version"],
        "ansible_version": awx_destination["ansible_version"],
    },
)

Step 3: Configure Version-Specific Behaviors:

# Chef 15.x: Uses SHA-256 authentication
if chef_origin["auth_protocol"] == "1.3":
    auth_headers = generate_chef_auth_sha256(...)
else:
    auth_headers = generate_chef_auth_sha1(...)

# AAP 2.4: Requires execution environments
if "execution_environments" in awx_destination["features"]:
    job_template["execution_environment"] = 1  # Required
else:
    job_template["custom_virtualenv"] = "/venv/ansible"  # Legacy

Testing Cross-Version Compatibility#

Test Origin Compatibility (Chef Server versions):

@pytest.mark.parametrize("chef_version,auth_protocol", [
    ("12.0", "1.0"),  # SHA-1 only
    ("12.19", "1.3"), # First 1.3 support
    ("14.0", "1.3"),  # SHA-256 preferred
    ("15.0", "1.3"),  # SHA-256 default
])
def test_chef_origin_versions(chef_version, auth_protocol):
    """Test Chef Server versions as migration origins."""
    # Configure mock with version-specific auth
    client = ChefServerClient(
        base_url="https://chef.example.com",
        client_name="validator",
        private_key=test_key,
        auth_version=auth_protocol,
    )
    # All versions should return same data structure
    nodes = client.search_nodes("name:*")
    assert "rows" in nodes

Test Destination Compatibility (Ansible platforms):

@pytest.mark.parametrize("platform,version,ansible_version", [
    ("tower", "3.8.6", "2.9.27"),   # Legacy
    ("awx", "21.0", "2.12.0"),      # Transition (virtualenv → EE)
    ("awx", "24.0", "2.16.0"),      # Current
    ("aap", "2.4.0", "2.15.0"),     # Enterprise
])
def test_awx_destination_versions(platform, version, ansible_version):
    """Test Ansible platforms as migration destinations."""
    # Configure version-specific API mock
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/config/",
        json={"version": version, "ansible_version": ansible_version},
    )

    # Generate IR and transform to platform-specific config
    ir_graph = create_test_ir_graph()

    # Version-specific transformations
    if platform == "aap" or (platform == "awx" and int(version.split(".")[0]) >= 21):
        # Use execution environments (required in AAP, AWX 21+)
        job_template = ir_to_awx_job_template(ir_graph, use_ee=True)
        assert "execution_environment" in job_template
    else:
        # Use legacy virtualenv (Tower 3.x, early AWX)
        job_template = ir_to_awx_job_template(ir_graph, use_ee=False)
        assert "custom_virtualenv" in job_template

Test End-to-End Paths:

@pytest.mark.parametrize("origin,destination", [
    # Minimum supported: Chef 12.x → Tower 3.8
    (
        {"chef": "12.19", "auth": "1.3"},
        {"platform": "tower", "version": "3.8.6", "ansible": "2.9"},
    ),
    # Mid-range: Chef 14.x → AWX 22.x
    (
        {"chef": "14.15", "auth": "1.3"},
        {"platform": "awx", "version": "22.0", "ansible": "2.14"},
    ),
    # Current: Chef 15.x → AAP 2.4
    (
        {"chef": "15.0", "auth": "1.3"},
        {"platform": "aap", "version": "2.4", "ansible": "2.15"},
    ),
])
def test_migration_path_compatibility(origin, destination):
    """Test complete Chef → Ansible migration paths."""
    # Stage 1: Query Chef Server (origin version)
    chef_client = configure_chef_client(origin)
    nodes = chef_client.search_nodes("*:*")

    # Stage 2: Build IR from Chef data
    ir_graph = chef_to_ir(nodes)

    # Stage 3: Transform IR to Ansible (destination version)
    awx_config = configure_awx_client(destination)
    inventory = ir_to_awx_inventory(ir_graph, awx_config)
    job_template = ir_to_awx_job_template(ir_graph, awx_config)

    # Stage 4: Validate destination compatibility
    assert validate_awx_compatibility(ir_graph, destination["ansible"])

Version-Specific Breaking Changes#

When migrating FROM these Chef Server versions:

Origin Version Breaking Changes Migration Impact Workaround
Chef 11.x SHA-1 only Must upgrade to 12.x for SHA-256 Use auth protocol 1.0
Chef 12.x Mixed auth support None - fully compatible Prefer protocol 1.3
Chef 14.x Strict SSL/TLS Requires valid certificates Update cert validation
Chef 15.x FIPS compliance Cipher suite restrictions Use approved ciphers

When migrating TO these Ansible platforms:

Destination Version Breaking Changes Migration Impact Workaround
Tower 3.8 EOL (no EE support) Limited to virtualenv Use AWX 19+ for EE
AWX 19.x Removed legacy creds Credential migration needed Remap credential types
AWX 21.x EE required Must provide EE or default Create default EE
AAP 2.x Renamed entities API paths changed Use new paths
AAP 2.4 Ansible 2.15+ Older playbooks may break Test playbook syntax

Tested Migration Paths#

Current Test Coverage (test_ir_chef_awx_workflow.py):

Test Origin Destination Status API Validation
test_query_chef_nodes_and_build_ir Chef 14.x N/A [YES] Passing SHA-256 auth, node search
test_chef_cookbook_to_ir_with_dependencies Chef 12.x+ N/A [YES] Passing Cookbook metadata
test_ir_to_awx_job_template_creation N/A AWX 24.6.1 [YES] Passing EE-based templates
test_ir_to_awx_inventory_creation N/A AWX 24.6.1 [YES] Passing Inventory API v2
test_complete_migration_workflow Chef 14.x AWX 24.6.1 [YES] Passing Full 4-stage pipeline
test_workflow_with_chef_environments_to_awx_groups Chef 14.x AWX 24.6.1 [YES] Passing Environment mapping
test_validate_ir_node_against_chef_server Chef 14.x N/A [YES] Passing Recipe validation
test_validate_awx_compatibility_from_ir N/A AWX 24.6.1 [YES] Passing Ansible version check

Default Test Configuration: - Origin: Chef Server 14.x with SHA-256 authentication (protocol 1.3) - Destination: AWX 24.6.1 with Ansible 2.16 and Execution Environment support - API Versions: Chef Server (no URL version), AWX /api/v2/

# Add tests for other Chef origins
test_chef_12x_mixed_auth()  # Protocol 1.0/1.3 support
test_chef_15_fips()  # FIPS 140-2 compliance

# Add tests for other AWX versions
test_chef_to_awx_22()  # AWX 22.x with EE
test_chef_to_awx_23()  # AWX 23.x releases

# Add tests for AAP
test_chef_to_aap_24()  # AAP 2.4 features

Future Enhancements#

Potential additions to mock coverage:

Expanded Version Testing#

  • Chef 11.x origins: Test SHA-1-only legacy migrations
  • Chef 12.x mixed auth: Test protocol 1.0/1.3 transitions
  • Chef 15.x FIPS: Test FIPS 140-2 compliant origins
  • Tower 3.8 destinations: Test legacy AWX Tower targets
  • AWX 24.x destinations: Test latest AWX features
  • AAP 2.5+ destinations: Test future AAP releases

Additional API Coverage#

  • Chef Policyfiles: Mock policy group queries and policy revisions
  • AWX Workflows: Multi-step workflow node creation and dependencies
  • Chef Search: Advanced search query patterns (boolean, wildcard, range)
  • AWX Surveys: Job template survey generation from Chef attributes
  • AWX Execution Environments: Custom EE image configuration

Error Scenario Testing#

  • 4xx/5xx responses: API error handling across versions
  • Rate limiting: API throttling simulation (429 responses)
  • Authentication failures: Invalid keys, expired tokens
  • Version mismatches: Incompatible origin→destination combinations

Performance Testing#

  • Large-scale migrations: 1000+ nodes, 100+ cookbooks
  • Concurrent API calls: Parallel Chef Server queries
  • AWX batch operations: Bulk inventory/host creation