Chapter 5 — Ubuntu / Debian Implementation
Ubuntu came after RHEL. It established the patterns RHEL grandfathered: the OS-prefixed class names (Convention 4), the shared
featurebaseimported fromcodecraftai-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-recommendsfor stricter installs setsconfig = UbuntuPackageManagerConfig(apt_options=["--no-install-recommends"]).noninteractive_env— when True (the default), every write verb runs with theDEBIAN_FRONTEND=noninteractiveenv 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" ispublic). - 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:
- Reads the existing drop-in file (if it exists).
- Computes the desired post-state as a YAML dict.
- If post-state matches existing, return SKIPPED.
- Otherwise, render the YAML and write the file via
tee(per Convention 1's file-I/O-through-runner rule). - Run
netplan applyto 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-executionandcommand-runner-architecture— these are in RHEL's registry as Layer-1 features, but Ubuntu inherits the runner fromcodecraftai-sysman-commonand 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).