packages feed

salmon-ops-0.1.0.0: fixtures/QemuHostSetupFixture.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedRecordDot #-}

{- | One-time, privileged host setup for the Layer 3 qemu test tier (see
@specs/qemu-test-vms.md@\/@specs/qemu-test-vms-progress.md@): grants
"Salmon.Builtin.Nodes.Capabilities" to @capsh@\/@qemu-system-x86_64@ and
hands ownership of each rootfs's @etc\/ssh@ subtree to an unprivileged
user, so that routine test runs (@cabal test salmon-ops-recipes@,
"Test.Harness".'Test.Harness.hasVmPrivileges') no longer need to run as
root at all — only this one-off setup does. Grants @capsh@, not @ip@
itself: see "Salmon.Builtin.Nodes.LinuxBridge".'Salmon.Builtin.Nodes.LinuxBridge.ipLinkCommand's
haddock for why a direct grant on @ip@ doesn't work (iproute2
unconditionally drops its own capability set at startup and only trusts
the ambient set, which only @capsh@-mediated exec can populate).

Meant to be run once per machine, as root (or under @sudo@), and again
after any @apt upgrade@ of @libcap2-bin@\/@qemu-system-x86@ (package
upgrades replace the binary, wiping its capabilities — see
"Salmon.Builtin.Nodes.Capabilities".'Salmon.Builtin.Nodes.Capabilities.grantCapabilities'
haddock) or after debootstrapping a new rootfs:

> cabal build salmon-qemu-host-setup-fixture
> sudo dist-newstyle/build/*/*/salmon-ops-0.1.0.0/x/salmon-qemu-host-setup-fixture/build/salmon-qemu-host-setup-fixture/salmon-qemu-host-setup-fixture \
>     lucas /var/lib/salmon-test-vms/smoke/root /var/lib/salmon-test-vms/pg-primary/root /var/lib/salmon-test-vms/pg-standby/root

Idempotent (same conventions as every other node in this codebase): safe to
rerun, and every step it didn't need to redo is reported 'Skip'/no-op.
-}
module Main (main) where

import Control.Monad (unless)
import Control.Monad.Identity (runIdentity)
import Data.List (foldl')
import qualified Data.Text as Text
import System.Directory (canonicalizePath, findExecutable)
import System.Environment (getArgs)
import System.Exit (die, exitFailure)
import System.FilePath ((</>))

import Salmon.Actions.UpDown (upTree)
import Salmon.Builtin.Extension
import qualified Salmon.Builtin.Nodes.Capabilities as Capabilities
import qualified Salmon.Builtin.Nodes.Debian.OS as Debian
import qualified Salmon.Builtin.Nodes.User as User
import Salmon.Op.OpGraph (overlaid)
import Salmon.Reporter (reportPrint)

-- | The capabilities each binary needs — see 'Salmon.Builtin.Nodes.Capabilities.grantCapabilities'.
capshCapabilities, qemuCapabilities :: [Capabilities.Capability]
capshCapabilities = ["cap_net_admin"]
qemuCapabilities = ["cap_dac_override", "cap_chown", "cap_fowner"]

-- | Grants a resolved binary path its needed capabilities, or dies loudly
-- if the binary isn't found — same "fail loudly, don't hang" spirit as
-- "Test.Harness".'Test.Harness.requireExecutable'. Canonicalizes past any
-- symlink first (e.g. Debian's usrmerge makes @\/usr\/sbin\/ip@ a symlink to
-- @\/bin\/ip@) — @setcap@ refuses to operate on a symlink at all
-- ("Invalid file for capability operation"), it needs the real inode.
capabilityOp :: String -> [Capabilities.Capability] -> IO Op
capabilityOp exe caps = do
    mPath <- findExecutable exe
    case mPath of
        Nothing -> die (exe <> " not found on PATH")
        Just linkedPath -> do
            path <- canonicalizePath linkedPath
            pure (Capabilities.grantCapabilities reportPrint Debian.setcap path caps)

-- | Hands ownership of one rootfs's @etc\/ssh@ subtree to the unprivileged
-- test user — see "Test.Harness".'Test.Harness.ensureVmSshAccess', which
-- writes a fresh per-boot SSH CA there directly on the host filesystem.
sshDirOwnershipOp :: User.Owner -> FilePath -> Op
sshDirOwnershipOp owner rootfs =
    User.chown reportPrint Debian.chown True owner (rootfs </> "etc/ssh")

main :: IO ()
main = do
    args <- getArgs
    case args of
        (user : rootfsPaths) -> do
            capshOp <- capabilityOp "capsh" capshCapabilities
            qemuOp <- capabilityOp "qemu-system-x86_64" qemuCapabilities
            let owner = User.Owner (User.User (Text.pack user)) (User.Group (Text.pack user))
                sshOps = map (sshDirOwnershipOp owner) rootfsPaths
                allOps = foldl' overlaid capshOp (qemuOp : sshOps)
            ok <- upTree reportPrint (pure . runIdentity) allOps
            unless ok exitFailure
        _ -> die "usage: salmon-qemu-host-setup-fixture <unprivileged-user> <rootfs-path>..."