Volume 2 — Distributed Automation — contents
  1. Chapter 1 — Why Distributed Automation Is Genuinely Hard
  2. Chapter 2 — Single Actor, Multiple Observers — The Determinism Model
  3. Chapter 3 — The Pod Model — Where Parallelism Actually Lives
  4. Chapter 4 — The TestJob Inheritance Chain — The Customisation Cascade
  5. Chapter 5 — Tandem Testing in Depth
  6. Chapter 6 — Landscape Activation
  7. Chapter 7 — The CI → Job Hand-off as a Verified Contract
  8. Chapter 8 — The DistributedTestSequencer
  9. Chapter 9 — job_seed and Reproducibility
  10. Chapter 10 — The Remote-Execution Wire
  11. Chapter 11 — Credentials at Scale
  12. Chapter 12 — Data Profiles in Remote Workers
  13. Chapter 13 — Result Aggregation
  14. Chapter 14 — Partial-Run Semantics
  15. Chapter 15 — Debugging a Distributed Failure
  16. Chapter 16 — Case Study: A 5-Host Integration Test for a Sysman Feature

Chapter 6 — Landscape Activation

Volume 1 chapter 9 carried the landscape boundary at the contract level: TestJob's five fire_* hooks are no-ops on single-host; subclasses give them bodies. This chapter walks the bodies. When the framework "activates" a landscape, what actually happens? Three layers, each with a job, each with failure modes the next layer can't recover.

Landscape.activate_configuration(), Landscape.activate_integration(), and Landscape.activate_operations() are the three method calls InteropLibraryTestJob makes during a distributed test run. Each delegates to a corresponding layer on the Landscape class:

  • activate_configuration delegates to LandscapeConfigurationLayer.
  • activate_integration delegates to LandscapeIntegrationLayer.
  • activate_operations delegates to LandscapeOperationalLayer.

The three layers are the framework's separation of concerns for landscape activation: load files → instantiate objects → bring online and verify. Each layer has a clear responsibility and a clear failure mode.

The three layers at a glance

Layer Responsibility Output Failure mode
Configuration Load Landscape and Topology files; initialise credentials; let integration couplings inspect config landscape_info, topology_info, CredentialManager populated Missing files; schema errors; unresolved credentials
Integration Instantiate device, service, power, serial objects from the loaded config _integrated_devices, _integrated_services, etc. populated Unknown deviceTypes; misconfigured Features; coordinator-init errors
Operational Activate coordinators; attach extensions; verify connectivity Devices online and reachable Unreachable devices; failed authentication; service-side errors

The order matters. Each layer requires the previous layer's output. An attempted operational activation against a landscape whose integration layer didn't produce devices fails immediately. An integration activation against a landscape whose configuration layer didn't load fails immediately. The framework enforces the order through the Landscape class's three methods, each of which checks its predecessor's state.

Layer 1 — LandscapeConfigurationLayer

File: codecraftai-landscaping/source/packages/ccai/landscaping/layers/landscapeconfigurationlayer.py:51

The configuration layer is the "load and validate" phase. It owns the on-disk representation of the landscape and topology, plus the credentials and runtime references they contain.

Key methods

  • load_landscape(activation_params) — reads the LANDSCAPE_CONFIGURATION_MAP (populated earlier by codecraftai-configuration's loader) and stores it as self._landscape_info.

  • load_topology(activation_params) — same for TOPOLOGY_CONFIGURATION_MAP, stored as self._topology_info. Line 267.

  • initialize_credentials() — instantiates CredentialManager for the loaded credentials. Anything the test job will need to authenticate against must resolve here.

  • attach_to_environment() (lines 201–219) — gives every installed integration coupling a chance to inspect the configuration before device creation. Quoted:

    def attach_to_environment(self):
        lscape = self.landscape
    
        # During the landscape initialization and before we start creating devices, give
        # all of the installed coordinator couplings and opportunity to peek at the configuration
        # and perform any special validation or internal initialization, this also provides the
        # integrations with a first glance at the test landscape after the configuration files
        # have been loaded.
        integ_coupling_table = lscape.installed_integration_couplings
        for integ_key in integ_coupling_table:
            integ_coupling = integ_coupling_table[integ_key]
            integ_coupling.attach_to_environment(lscape)
    
        return
    
  • validate_landscape() and validate_topology() — structural validation hooks. The shipped validate_topology() (line 536) is a stub returning empty errors / warnings; the user overrides it in a Landscape subclass to enforce project-specific schema.

What can go wrong here

  • Missing files. If AM_CONFIG_LANDSCAPE_NAMES references a name that ConfigurationLoader cannot find, load fails before any device is created. This is the cleanest failure to diagnose — the path is explicit, the resolver explicitly reports what it tried.
  • Schema errors. YAML or JSON parse errors land here.
  • Credential resolution failures. A credential name in the Landscape that doesn't resolve through any configured backend fails here. The test job sees a ConfigurationError and refuses to proceed.

The configuration layer is fast. Loading is filesystem + parse + dict-walk. No network calls. No device probing. Failure here means the CI system handed the job configuration that doesn't even parse — a clear "your inputs are wrong" signal.

Layer 2 — LandscapeIntegrationLayer

File: codecraftai-landscaping/source/packages/ccai/landscaping/layers/landscapeintegrationlayer.py:43

The integration layer is the "instantiate objects" phase. It walks the loaded landscape_info and constructs in-memory representations of every device, service, power controller, and serial coordinator the landscape declares.

Key method

initialize_landscape(activation_params) at lines 295–341 is the layer's entry point. Its body:

def initialize_landscape(self, activation_params: LandscapeActivationParams):
    lscape = self.landscape

    devices = None
    with lscape.begin_locked_landscape_scope() as locked:

        layer_config = lscape.layer_configuration

        if layer_config.landscape_info is not None:

            if not activation_params.disable_device_activation:
                # Initialize the devices so we know what they are...
                devices = self._initialize_landscape_devices(layer_config)

                if self._power_request_count > 0:
                    self._initialize_landscape_power(layer_config)

                if self._serial_request_count > 0:
                    self._initialize_landscape_serial(layer_config)
            else:
                self.logger.info("LandscapeIntegrationLayer: 'Device Activation' was disabled.")
                devices = {}

            if not activation_params.disable_service_activation:
                services = self._initialize_landscape_services(layer_config)
            else:
                self.logger.info("LandscapeIntegrationLayer: 'Service Activation' was disabled.")
                services = {}

            self._integrated_devices = devices.copy()
            self._integrated_services = services.copy()

Three things to notice:

  1. The lock. begin_locked_landscape_scope() is held during initialisation to prevent concurrent activation from multiple threads (a real concern with the LandscapeSingleton pattern).
  2. The activation flags. disable_device_activation and disable_service_activation are LandscapeActivationParams fields letting the test job skip device/service initialisation when only the configuration layer is needed (e.g., for inspection or audit). The default is to activate both.
  3. The on-demand power and serial init. The integration layer only initialises power and serial subsystems if at least one device requested them (tracked via _power_request_count and _serial_request_count). Pods without power-control devices skip the power init entirely.

What gets created

After initialize_landscape returns:

  • integrated_devices — a thread-safe dict of device_name → LandscapeDevice instances. Each device knows its deviceType, host, credentials, Features, and the ProtocolExtensions it should expose.
  • integrated_services — same for services.
  • integrated_power — power-controller objects.
  • integrated_serial — serial-coordinator objects.

The objects exist but are not yet online. No SSH session has been opened. No serial port has been probed. The integration layer is a graph-construction phase; the operational layer is the first-contact phase.

What can go wrong here

  • Unknown deviceTypes. A Landscape declaring deviceType: network/specialdevice against a framework that has no Coordinator registered for network/specialdevice fails here. The Volume 0 chapter 7 factory model is what determines whether a deviceType is known; a project adding a new deviceType registers a Coordinator subclass through the factory protocol.
  • Misconfigured Features. A Feature that references a service-name that doesn't exist in the services section fails here. The integration layer's _initialize_landscape_devices walks every device's Features and looks up service references; a missing service is a ConfigurationError.
  • Coordinator-init errors. A Coordinator subclass's own __init__ failing surfaces here.

Layer 3 — LandscapeOperationalLayer

File: codecraftai-landscaping/source/packages/ccai/landscaping/layers/landscapeoperationallayer.py:41

The operational layer is where landscape activation contacts the real world. It activates each Coordinator (which brings its devices online), attaches ProtocolExtensions to devices, and verifies connectivity.

Key method

establish_connectivity(activation_params) at lines 391–480 is the layer's verification entry point. Excerpted:

def establish_connectivity(self, activation_params: LandscapeActivationParams):
    lscape = self.landscape
    layer_integ = lscape.layer_integration

    failures = []

    coordinators_for_services = layer_integ.coordinators_for_services
    for coord in coordinators_for_services.values():
        results = coord.establish_connectivity(activation_params)
        for item in results:
            host, ipaddr, status, stdout, stderr, exc = item
            if exc is not None:
                failures.append(item)
            elif status is not None:
                if isinstance(status, bool):
                    if not status:
                        failures.append(item)
                elif status != 0:
                    failures.append(item)

    # ... similar loops for power, serial, devices ...

    if len(failures) > 0:
        errmsg_lines = [
            "Errors encountered while establishing connectivity with the landscape.",
            "FAILURES:"
        ]

        for item in failures:
            host, ipaddr, status, stdout, stderr, exc = item
            if exc is not None:
                errmsg_lines.append(f"    Host: {host} IP: {ipaddr} Exception: {exc}")
            else:
                errmsg_lines.append(f"    Host: {host} IP: {ipaddr} Status: {status} Stderr: {stderr}")

        errmsg = os.linesep.join(errmsg_lines)

        if not activation_params.allow_missing_devices:
            raise RuntimeError(errmsg)
        else:
            self.logger.error(errmsg)

    return

Three properties of the verification:

  1. The result tuple shape. Each coordinator's establish_connectivity returns a list of (host, ipaddr, status, stdout, stderr, exc) tuples. The tuple captures: which host, what IP it resolved to, what status code the probe returned, stdout / stderr from the probe, and any exception caught. This is the primary forensic record for "why is host X unreachable."
  2. The allow_missing_devices flag. When False (the default), any failure raises RuntimeError and the test job refuses to proceed. When True (typical for smoke runs), failures are logged and the run continues with missing devices.
  3. The error message construction. The framework follows the Volume 0 chapter 2 multi-line-error-message discipline: build a list of lines, format with os.linesep.join, raise. An SDET reading the error gets a structured failure report with the host, IP, status, and stderr — enough to triage without re-running.

What gets activated

activate_coordinators(activation_params) runs before establish_connectivity. For each Coordinator:

  • The Coordinator's activate() method runs. This typically:
    • Walks each device the Coordinator manages.
    • Probes the device for additional metadata (model number, OS version, hardware info).
    • Attaches the appropriate ProtocolExtensions to the device (device.ssh, device.serial, device.power).
  • Per-device features that need the Coordinator's bridging (e.g., a serial console reachable through a serial concentrator) get their ProtocolExtensions wired through the Coordinator's registry.

After activate_coordinators plus establish_connectivity succeed, the Pod is operationally live. Devices respond to calls; ProtocolExtensions are attached; service references resolve; credentials work.

What can go wrong here

  • Unreachable devices. Network down, host crashed, firewall blocking, wrong port — all surface as failures in the result tuples.
  • Failed authentication. Credentials resolved (layer 1), ProtocolExtensions attached (above), but the actual SSH authentication failed. Surface as SshAuthError in the exc field.
  • Timeout exceeded. The Coordinator's probe took too long — either a long-running connection attempt hung, or the device is up but the probe is slow. Surface as TimeoutError.

The order: why each phase requires the previous

Volume 0 chapter 4's protocol discipline shows up here. The three layers are strictly ordered:

  1. Configuration must succeed before Integration (the integration layer reads landscape_info from the configuration layer).
  2. Integration must succeed before Operational (the operational layer iterates the integrated coordinators from the integration layer).
  3. Operational must succeed before tests run (a test against a Pod whose devices aren't online and reachable is not deterministic — chapter 2's commitment).

The framework enforces the order by checking state at each layer's entry. An out-of-order activation surfaces a SemanticError (Volume 0 chapter 3) — the framework refuses to run the operational layer if integration didn't produce devices, refuses to run integration if configuration didn't load, etc.

Activation parameters

LandscapeActivationParams (codecraftai-landscaping/source/packages/ccai/landscaping/landscapeparameters.py:19) is the dataclass that controls activation:

class LandscapeActivationParams(SimpleNamespace):
    disable_device_activation: bool=False
    disable_service_activation: bool=False
    allow_missing_devices: bool=False
    allow_unknown_devices: bool=False
    allow_missing_services: bool=False
    allow_unknown_services: bool=False
    validate_features: bool=True
    validate_topology: bool=True

Each flag answers a real question:

  • disable_device_activation — skip device activation entirely. Useful for test jobs that only inspect configuration.
  • allow_missing_devices — proceed even when some devices fail to come online. Useful for smoke runs.
  • validate_topology — run the topology validator before proceeding. Default True; chapter 7 walks the verification contract that hangs on this flag.

The default values are strict: activation requires every device and service to come up, every Feature to validate, every Topology assertion to verify. A project running a deliberate smoke / partial run flips the flags explicitly; the strict defaults ensure no production run accidentally runs against a half-broken Pod.

What this means for the verification contract

Chapter 7 walks the verification contract as it appears at the orchestration / job seam — what the CI system has promised the job, and how the job verifies the promise. The mechanism of verification is what this chapter just walked. Specifically:

  • Landscape verification = establish_connectivity returning no failures. Every device declared in the Landscape can be reached at its declared address with its declared protocol using its declared credentials.
  • Topology verification = the user's validate_topology() override returning no errors. The shipped stub returns empty; a project's Topology subclass overrides it to check the project-specific Topology shape.

If either fails (and the activation params are strict), the test job refuses to run. The Pod is not realised. Tests do not start.

What the next chapter does

Chapter 7 walks the verification contract at the orchestration-to-job seam. The CI system says "here are your resources" (the Landscape) and "here is the configured state" (the Topology); the test job verifies both before the first test runs. This chapter walked the layers that do the work; chapter 7 walks the contract the layers honour.