Volume 4 — Cross-Platform System Management — contents
  1. Chapter 1 — Why a Contract Layer
  2. Chapter 2 — The 52 Cross-OS Contracts
  3. Chapter 3 — featurebase and the Per-OS Implementation Pattern
  4. Chapter 4 — RHEL Implementation
  5. Chapter 5 — Ubuntu / Debian Implementation
  6. Chapter 6 — macOS Implementation
  7. Chapter 7 — Windows Implementation
  8. Chapter 8 — UEFI Firmware Implementation
  9. Chapter 9 — Spec-Driven Development for System Administration
  10. Chapter 10 — Building Cross-OS Test Parity

Chapter 5 — Ubuntu / Debian Implementation

Ubuntu came after RHEL. It established the patterns RHEL grandfathered: the OS-prefixed class names (Convention 4), the shared featurebase imported from codecraftai-sysman-common (rather than locally defined), and the per-feature config extensions for tool quirks (DEBIAN_FRONTEND=noninteractive, the single-zone ufw abstraction).

codecraftai-sysman-linux-ubuntu is the second sysman package. Its existence is what forced the prefixing convention — two packages with PackageManager classes was the collision that prompted Convention 4. Reading this chapter is reading the generalisation of the patterns RHEL established.

Layout

codecraftai-sysman-linux-ubuntu/source/packages/ccai/sysman/linux/ubuntu/
├── __init__.py
├── packagemanagement.py
├── servicemanagement.py
├── firewallmanagement.py
├── networkconfiguration.py
├── bootloadermanagement.py    # covers EFI variants RHEL doesn't
├── ... (35 more feature modules)
└── (no local featurebase — imports from codecraftai-sysman-common)

40 Python files. Notably no local featurebase.py — the Ubuntu package imports BaseFeatureManager, FeatureConfig, FeatureActionResult, and ActionStatus directly from ccai.sysman.common.featurebase.

This is the shared-base pattern — the second-mover advantage. RHEL was first and has its own featurebase for historical reasons (the common package didn't exist yet). Ubuntu was second and uses the common package directly. Every per-OS package added after Ubuntu (macOS, Windows, UEFI) follows the shared-base pattern.

OS-prefixed class names

Every public type in the Ubuntu package is prefixed with Ubuntu:

class UbuntuPackageManager(BaseFeatureManager): ...
class UbuntuPackageManagerConfig(FeatureConfig): ...
class UbuntuPackageActionResult(FeatureActionResult): ...
class UbuntuPackageActionStatus(Enum): ...
class UbuntuPackageInfo: ...

class UbuntuServiceManager(BaseFeatureManager): ...
class UbuntuServiceManagerConfig(FeatureConfig): ...
class UbuntuServiceActionResult(FeatureActionResult): ...
class UbuntuServiceScope(Enum): ...

The prefix is mandatory per Convention 4 — no exceptions. A reader importing from Ubuntu sees the prefix in every name, which is what makes cross-package usage unambiguous:

from ccai.sysman.linux.redhat.packagemanagement import PackageManager
from ccai.sysman.linux.ubuntu.packagemanagement import UbuntuPackageManager

# Distinct names; no aliasing needed.
rhel_mgr = PackageManager(runner=rhel_runner, config=...)
ubuntu_mgr = UbuntuPackageManager(runner=ubuntu_runner, config=...)

The historical exception (RHEL retains unprefixed names) is the only exception. Every other package follows.

Three worked features

Package management with apt-get

File: packagemanagement.py:1–79

from ccai.sysman.common.featurebase import (
    ActionStatus,
    BaseFeatureManager,
    FeatureActionResult,
    FeatureConfig,
)


@dataclass
class UbuntuPackageManagerConfig(FeatureConfig):
    """
        Configuration for :class:`UbuntuPackageManager`.  Inherits
        ``dry_run`` and ``elevated`` from :class:`FeatureConfig`.

        :param apt_options: Optional list of additional flags appended to
                            every ``apt-get`` write argv.  Defaults to
                            ``None``.
        :param noninteractive_env: When True (the default), the runner's
                                   env for write verbs includes
                                   ``DEBIAN_FRONTEND=noninteractive``.
    """
    apt_options: Optional[list[str]] = None
    noninteractive_env: bool = True


class UbuntuPackageManager(BaseFeatureManager):
    """
        Ubuntu apt/dpkg package manager.  Every verb is dispatched through
        the injected :class:`ICommandRunner`.
    """

    result_class = UbuntuPackageActionResult
    status_class = UbuntuPackageActionStatus
    feature_name = "package-management"

Two Ubuntu-specific config extensions:

  • apt_options — extra flags appended to every apt-get argv. A test that wants to pass --no-install-recommends for stricter installs sets config = UbuntuPackageManagerConfig(apt_options=["--no-install-recommends"]).
  • noninteractive_env — when True (the default), every write verb runs with the DEBIAN_FRONTEND=noninteractive env var set. This prevents apt from prompting for user input during unattended runs (e.g., when a package's postinst script asks "do you want to keep your existing config file?").

The noninteractive_env flag is the Ubuntu-specific quirk the contract leaves to the implementation. RHEL's dnf doesn't have this concept (dnf is non-interactive by default with the -y flag). The contract specifies the verb's behaviour ("idempotent install at the requested version"); the implementation handles the OS-specific noise that gets in the way.

Firewall management with ufw

File: firewallmanagement.py:167–176

class UbuntuFirewallManager(BaseFeatureManager):
    """
        Ubuntu ufw firewall manager.  Every verb is dispatched through
        the injected :class:`ICommandRunner`.
    """

    result_class = UbuntuFirewallActionResult
    status_class = UbuntuFirewallActionStatus
    feature_name = "firewall-management"

The contract's firewall-management interface includes zone-based methods (add_service(service, zone), etc.). ufw, however, has no zone concept.

The Ubuntu manager handles this via Convention 5 (FAILED with stable detail token):

def add_service(self, service: str, zone: str) -> UbuntuFirewallActionResult:
    if zone != "default":
        return UbuntuFirewallActionResult(
            status=UbuntuFirewallActionStatus.FAILED,
            zone=zone,
            detail="ufw_no_zones",  # stable token
        )
    # ... ufw-specific implementation for the default zone ...

A caller passing any value other than "default" gets FAILED with the stable token ufw_no_zones. Tests written against the contract know to accept this token on Ubuntu; cross-OS tests that target the firewall feature on both RHEL and Ubuntu either:

  • Use zone="default" (works on both — RHEL's "default" is public).
  • Have separate paths for the two OSes when zone-specific behaviour is needed.

Convention 5 is the framework's mechanism for handling this class of divergence cleanly.

Network configuration with netplan

File: networkconfiguration.py:1–80

"""
    Ubuntu network-configuration feature.

    Implements the cross-OS network-configuration contract for Ubuntu using
    the iproute2 family (``ip``), netplan (``netplan apply`` plus a
    declarative YAML drop-in under ``/etc/netplan/``), ``resolvectl`` for
    DNS, and ``hostnamectl`` for the static hostname.
"""


@dataclass
class UbuntuNetworkConfigurationManagerConfig(FeatureConfig):
    """
        :param netplan_dir: Directory holding netplan YAML drop-ins.
                            Defaults to ``/etc/netplan``.
        :param netplan_basename: Filename of the netplan drop-in
                                 maintained by this manager.  Defaults to
                                 ``99-ccai.yaml``.
    """
    netplan_dir: str = "/etc/netplan"
    netplan_basename: str = "99-ccai.yaml"

Persistent IPv4 addressing on Ubuntu happens through netplan YAML drop-in files. The Ubuntu manager:

  1. Reads the existing drop-in file (if it exists).
  2. Computes the desired post-state as a YAML dict.
  3. If post-state matches existing, return SKIPPED.
  4. Otherwise, render the YAML and write the file via tee (per Convention 1's file-I/O-through-runner rule).
  5. Run netplan apply to make the change live.

The netplan_dir and netplan_basename config fields let tests target a custom drop-in path (useful for testing in CI environments where /etc/netplan/ should be left alone).

Compare to RHEL's network-configuration: RHEL uses NetworkManager's KeyFile format under /etc/NetworkManager/system-connections/. Same contract; different tool; different file layout; same idempotency discipline.

The resolvectl and hostnamectl tools handle DNS and hostname respectively — also different from RHEL's NetworkManager-based DNS handling. Each manager does what the OS's idiom requires.

What's NOT in Ubuntu

A reader comparing the Ubuntu and RHEL packages will notice:

RHEL-only features absent from Ubuntu

  • selinux-management — Ubuntu uses AppArmor, not SELinux. The Ubuntu package doesn't ship an AppArmor manager (no contract for AppArmor exists yet).
  • subscription-management — RHEL-specific (Red Hat Subscription Manager).
  • cockpit-management — RHEL-specific.
  • insights-management — RHEL-specific.
  • tuned-management — RHEL-specific.
  • command-execution and command-runner-architecture — these are in RHEL's registry as Layer-1 features, but Ubuntu inherits the runner from codecraftai-sysman-common and doesn't re-register.

Ubuntu-specific feature: bootloader-management

The Ubuntu package ships bootloader-management (covering GRUB2 with EFI variants). RHEL's package has a simpler boot-management feature; the Ubuntu version is more sophisticated because Ubuntu's bootloader story (Secure Boot, EFI, BIOS modes) varies more than RHEL's typical deployment.

The asymmetry is not a contract violation. The contracts are a menu; not every OS implements every feature. What the contracts ensure is that when an OS implements a feature, the implementation honours the contract.

The shared-base benefit visible

A worth-pointing-out detail: when the codecraftai-sysman-common package's featurebase is updated (a Phase-3 audit fix, a new convention added), the Ubuntu package picks up the change automatically on its next dependency resolution. The RHEL package's local featurebase is a copy that has to be updated manually (or the package has to migrate to the shared base, which is a separate maintenance task).

This is the trade-off that prompted the shared-base pattern. The cost: Ubuntu can't customise its base. The benefit: common improvements arrive uniformly. Ubuntu's choice (shared base) is a vote in favour of consistency over flexibility.

Tests

41 test files at source/testroots/testbase/ccai/tests/sysman/linux/ubuntu/.

Slightly fewer than RHEL's 53, reflecting:

  • Fewer features (40 vs 46).
  • Some features have simpler test surfaces (ufw's single-zone reduces the test matrix for firewall vs firewalld's multiple zones).

The test patterns are the same as RHEL's: command-construction, idempotency, dry-run, elevation, output-parsing, error-handling. The tests run against mock runners in unit mode; integration tests against real Ubuntu hosts run periodically.

Cross-OS feature surface — shared and divergent

A summary of where Ubuntu and RHEL converge and diverge on features they both implement:

Feature RHEL tool Ubuntu tool Convergence
package-management dnf + rpm apt-get + dpkg Same contract; different tools; different version-pinning syntax
service-management systemctl systemctl Both systemd; Ubuntu adds UbuntuServiceScope enum for SYSTEM/USER distinction
firewall-management firewalld (zones) ufw (single "default" zone) Same contract; Ubuntu uses Convention 5 for non-default zones
network-configuration NetworkManager KeyFile netplan YAML Both write persistent files, run a "reload" command
storage-management parted, mkfs, LVM tools parted, mkfs, LVM tools Largely identical (Linux-wide tools)
user-group-management useradd, groupadd, usermod useradd, groupadd, usermod Identical
repository-management yum/dnf .repo files apt sources.list.d Same concept; very different file formats
ssh-server-configuration sshd_config edit + restart sshd_config edit + restart Identical

The convergence pattern: when the concept is shared (package install, service start, user add), the contract abstracts over both OSes cleanly. When the concept diverges (firewall zones, network config files), the contract specifies the abstract verb and Convention 5 handles the cases where one OS can't honour the abstraction.

What's next

Chapter 6 walks the macOS implementation (codecraftai-sysman-macos) — 49 features, 45 test files, and the most distinctive set of OS-specific quirks: Homebrew elevation inversion (Convention 11), launchd's gui/<uid> domain mapping for user services, pf's per-anchor file layout, and the macOS hostname triple (HostName / ComputerName / LocalHostName).