-- | Implementation of an execution environment that uses /systemdNspawn/. module B9.SystemdNspawn ( SystemdNspawn (..), ) where import B9.B9Config ( getB9Config, systemdNspawnConfigs, ) import B9.B9Config.SystemdNspawn as X import B9.B9Error import B9.B9Exec import B9.B9Logging import B9.BuildInfo import B9.Container import B9.DiskImages import B9.ExecEnv import B9.ShellScript import Control.Eff import Control.Lens (view) import Control.Monad (when) import Control.Monad.IO.Class ( liftIO, ) import Data.Foldable (traverse_) import Data.List (intercalate, partition) import Data.Maybe (fromMaybe, maybe) import System.Directory import System.FilePath import Text.Printf (printf) newtype SystemdNspawn = SystemdNspawn SystemdNspawnConfig type SudoPrepender = String -> String instance Backend SystemdNspawn where getBackendConfig _ = fmap SystemdNspawn . view systemdNspawnConfigs <$> getB9Config supportedImageTypes _ = [Raw] runInEnvironment (SystemdNspawn dCfg) env scriptIn = if emptyScript scriptIn then return True else do let sudo = if _systemdNspawnUseSudo dCfg then ("sudo " ++) else id containerBuildDirs <- createContainerBuildRootDir containerMounts <- mountLoopbackImages sudo env containerBuildDirs finallyB9 ( do bootScript <- prepareBootScript containerBuildDirs scriptIn execBuild sudo containerMounts (envSharedDirectories env) bootScript dCfg ) ( do umountLoopbackImages sudo containerMounts removeContainerBuildRootDir sudo containerBuildDirs ) createContainerBuildRootDir :: (Member BuildInfoReader e, Member ExcB9 e, CommandIO e) => Eff e ContainerBuildDirectories createContainerBuildRootDir = do buildD <- getBuildDir let loopbackMountDir = root "loopback_mounts" root = buildD "container_build_root" liftIO $ do createDirectoryIfMissing True root createDirectoryIfMissing True loopbackMountDir let res = ContainerBuildDirectories {containerBuildRoot = root, containerLoopbackMountRoot = loopbackMountDir} traceL ("Created container build directories: " ++ show res) return res data ContainerBuildDirectories = ContainerBuildDirectories { containerBuildRoot :: FilePath, containerLoopbackMountRoot :: FilePath } deriving (Show) mountLoopbackImages :: (Member BuildInfoReader e, Member ExcB9 e, CommandIO e) => SudoPrepender -> ExecEnv -> ContainerBuildDirectories -> Eff e ContainerMounts mountLoopbackImages sudo e containerDirs = do let imgMounts0 = [(img, mountPoint) | (img, MountPoint mountPoint) <- envImageMounts e] imgMounts = [(imgPath, mountPoint) | (Image imgPath _ _, mountPoint) <- imgMounts0] invalidImages = [x | x@(Image _ t _, _) <- imgMounts0, t /= Raw] when (not (null invalidImages)) (throwB9Error ("Internal Error: Only 'raw' disk images can be used for container builds, and these images were supposed to be automatically converted: " ++ show invalidImages)) case partition ((== "/") . snd) imgMounts of ([rootImg], otherImgs) -> do rootMount <- mountLoopback rootImg otherMounts <- traverse mountLoopback otherImgs return (ContainerMounts (Right rootMount) otherMounts) ([], _) -> throwB9Error "A containerized build requires that a disk image for the root-, i.e. the '/' directory is configured." (rootImgs, _) -> throwB9Error ("A containerized build requires that only one disk image for the root-, i.e. the '/' directory, instead these were given: " ++ show rootImgs) where mountLoopback (imgPath, containerMountPoint) = do let hostMountPoint = containerLoopbackMountRoot containerDirs printHash (imgPath, containerMountPoint) liftIO $ createDirectoryIfMissing True hostMountPoint hostCmd (sudo (printf "mount -o loop '%s' '%s'" imgPath hostMountPoint)) timeoutFastCmd return ( LoopbackMount { loopbackHost = hostMountPoint, loopbackContainer = containerMountPoint } ) newtype ContainerRootImage = ContainerRootImage FilePath deriving (Show) data ContainerMounts = ContainerMounts { containerRootImage :: Either ContainerRootImage LoopbackMount, containerLoopbackMounts :: [LoopbackMount] } deriving (Show) data LoopbackMount = LoopbackMount {loopbackHost :: FilePath, loopbackContainer :: FilePath} deriving (Show) prepareBootScript :: (Member ExcB9 e, CommandIO e) => ContainerBuildDirectories -> Script -> Eff e BootScript prepareBootScript containerDirs script = do let bs = BootScript { bootScriptHostDir = containerBuildRoot containerDirs "boot_script", bootScriptContainerDir = "/mnt/boot_script", bootScriptContainerCommand = bootScriptContainerDir bs scriptFile } scriptFile = "run.sh" scriptEnv = Begin [ Run "export" ["HOME=/root"], Run "export" ["USER=root"], -- IgnoreErrors True [Run "source" ["/etc/profile"]], script ] liftIO $ do createDirectoryIfMissing True (bootScriptHostDir bs) writeSh (bootScriptHostDir bs scriptFile) scriptEnv traceL ("wrote script: \n" ++ show scriptEnv) traceL ("created boot-script: " ++ show bs) return bs data BootScript = BootScript { bootScriptHostDir :: FilePath, bootScriptContainerDir :: FilePath, bootScriptContainerCommand :: String } deriving (Show) execBuild :: (Member ExcB9 e, CommandIO e) => SudoPrepender -> ContainerMounts -> [SharedDirectory] -> BootScript -> SystemdNspawnConfig -> Eff e Bool execBuild sudo containerMounts sharedDirs bootScript dCfg = do let systemdCmd = unwords ( systemdNspawnExe ++ consoleOptions ++ rootImageOptions ++ capabilityOptions ++ bindMounts ++ extraArgs ++ execOptions ) systemdNspawnExe = [fromMaybe "systemd-nspawn" (_systemdNspawnExecutable dCfg)] consoleOptions = ["--console=" ++ show (_systemdNspawnConsole dCfg)] rootImageOptions = case containerRootImage containerMounts of Left (ContainerRootImage imgPath) -> ["-i", imgPath] Right loopbackMounted -> ["-D", loopbackHost loopbackMounted] capabilityOptions = case _systemdNspawnCapabilities dCfg of [] -> [] caps -> ["--capability=" ++ intercalate "," (map show caps)] bindMounts = map mkBind loopbackMounts ++ map mkBind sharedDirMounts ++ map mkBindRo sharedDirMountsRo ++ [mkBindRo (bootScriptHostDir bootScript, bootScriptContainerDir bootScript)] where mkBind (hostDir, containerDir) = "--bind=" ++ hostDir ++ ":" ++ containerDir mkBindRo (hostDir, containerDir) = "--bind-ro=" ++ hostDir ++ ":" ++ containerDir loopbackMounts = [ (h, c) | LoopbackMount {loopbackHost = h, loopbackContainer = c} <- containerLoopbackMounts containerMounts ] sharedDirMounts = [(h, c) | SharedDirectory h (MountPoint c) <- sharedDirs] sharedDirMountsRo = [(h, c) | SharedDirectoryRO h (MountPoint c) <- sharedDirs] extraArgs = maybe [] (: []) (_systemdNspawnExtraArgs dCfg) execOptions = ["/bin/sh", bootScriptContainerCommand bootScript] timeout = (TimeoutMicros . (* 1000000)) <$> _systemdNspawnMaxLifetimeSeconds dCfg traceL ("executing systemd-nspawn container build") case _systemdNspawnConsole dCfg of SystemdNspawnInteractive -> hostCmdStdIn HostCommandInheritStdin (sudo systemdCmd) Nothing _ -> hostCmd (sudo systemdCmd) timeout umountLoopbackImages :: forall e. (Member ExcB9 e, CommandIO e) => SudoPrepender -> ContainerMounts -> Eff e () umountLoopbackImages sudo c = do case containerRootImage c of Left _ -> return () Right r -> umount r traverse_ umount (containerLoopbackMounts c) where umount :: LoopbackMount -> Eff e () umount l = do traceL $ "unmounting: " ++ show l res <- hostCmd (sudo (printf "umount '%s'" (loopbackHost l))) timeoutFastCmd when (not res) (errorL ("failed to unmount: " ++ show l)) removeContainerBuildRootDir :: forall e. (Member ExcB9 e, CommandIO e) => SudoPrepender -> ContainerBuildDirectories -> Eff e () removeContainerBuildRootDir sudo containerBuildDirs = do let target = containerBuildRoot containerBuildDirs traceL $ "removing: " ++ target res <- hostCmd (sudo (printf "rm -rf '%s'" target)) timeoutFastCmd when (not res) (errorL ("failed to remove: " ++ target)) timeoutFastCmd :: Maybe Timeout timeoutFastCmd = Just (TimeoutMicros 10000000)