propellor/src/Propellor/Property/Systemd.hs

273 lines
8.9 KiB
Haskell
Raw Normal View History

2014-11-20 21:18:26 +00:00
module Propellor.Property.Systemd (
2014-11-21 21:11:26 +00:00
module Propellor.Property.Systemd.Core,
2015-01-19 18:26:18 +00:00
ServiceName,
MachineName,
2014-11-21 16:35:07 +00:00
started,
stopped,
enabled,
2014-11-21 18:37:19 +00:00
disabled,
restarted,
2014-11-20 21:18:26 +00:00
persistentJournal,
Option,
configured,
journaldConfigured,
daemonReloaded,
2014-11-21 16:18:03 +00:00
Container,
2014-11-20 21:18:26 +00:00
container,
nspawned,
containerCfg,
2014-11-22 00:53:38 +00:00
resolvConfed,
2014-11-20 21:18:26 +00:00
) where
import Propellor
import Propellor.Types.Chroot
2014-11-20 21:18:26 +00:00
import qualified Propellor.Property.Chroot as Chroot
import qualified Propellor.Property.Apt as Apt
import qualified Propellor.Property.File as File
2014-11-21 21:11:26 +00:00
import Propellor.Property.Systemd.Core
2014-11-20 21:18:26 +00:00
import Utility.SafeCommand
import Utility.FileMode
2014-11-20 21:18:26 +00:00
import Data.List
2014-11-20 21:18:26 +00:00
import Data.List.Utils
import qualified Data.Map as M
2014-11-20 21:18:26 +00:00
2014-11-21 16:35:07 +00:00
type ServiceName = String
2014-11-20 21:18:26 +00:00
type MachineName = String
2014-11-21 19:55:27 +00:00
data Container = Container MachineName Chroot.Chroot Host
2014-11-22 00:53:38 +00:00
deriving (Show)
2014-11-20 21:18:26 +00:00
instance PropAccum Container where
(Container n c h) & p = Container n c (h & p)
(Container n c h) &^ p = Container n c (h &^ p)
getProperties (Container _ _ h) = hostProperties h
2014-11-20 21:18:26 +00:00
2014-11-21 16:35:07 +00:00
-- | Starts a systemd service.
started :: ServiceName -> Property
started n = trivial $ cmdProperty "systemctl" ["start", n]
`describe` ("service " ++ n ++ " started")
-- | Stops a systemd service.
stopped :: ServiceName -> Property
stopped n = trivial $ cmdProperty "systemctl" ["stop", n]
`describe` ("service " ++ n ++ " stopped")
-- | Enables a systemd service.
enabled :: ServiceName -> Property
enabled n = trivial $ cmdProperty "systemctl" ["enable", n]
`describe` ("service " ++ n ++ " enabled")
2014-11-21 18:37:19 +00:00
-- | Disables a systemd service.
disabled :: ServiceName -> Property
2014-11-21 20:02:10 +00:00
disabled n = trivial $ cmdProperty "systemctl" ["disable", n]
2014-11-21 18:37:19 +00:00
`describe` ("service " ++ n ++ " disabled")
-- | Restarts a systemd service.
restarted :: ServiceName -> Property
restarted n = trivial $ cmdProperty "systemctl" ["restart", n]
`describe` ("service " ++ n ++ " restarted")
-- | Enables persistent storage of the journal.
2014-11-20 21:18:26 +00:00
persistentJournal :: Property
persistentJournal = check (not <$> doesDirectoryExist dir) $
2014-11-21 16:35:07 +00:00
combineProperties "persistent systemd journal"
2014-11-20 21:18:26 +00:00
[ cmdProperty "install" ["-d", "-g", "systemd-journal", dir]
, cmdProperty "setfacl" ["-R", "-nm", "g:adm:rx,d:g:adm:rx", dir]
2014-11-21 16:35:07 +00:00
, started "systemd-journal-flush"
2014-11-20 21:18:26 +00:00
]
`requires` Apt.installed ["acl"]
where
dir = "/var/log/journal"
type Option = String
-- | Ensures that an option is configured in one of systemd's config files.
-- Does not ensure that the relevant daemon notices the change immediately.
--
-- This assumes that there is only one [Header] per file, which is
-- currently the case. And it assumes the file already exists with
-- the right [Header], so new lines can just be appended to the end.
configured :: FilePath -> Option -> String -> Property
configured cfgfile option value = combineProperties desc
[ File.fileProperty desc (mapMaybe removeother) cfgfile
, File.containsLine cfgfile line
]
where
setting = option ++ "="
line = setting ++ value
desc = cfgfile ++ " " ++ line
removeother l
| setting `isPrefixOf` l = Nothing
| otherwise = Just l
-- | Configures journald, restarting it so the changes take effect.
journaldConfigured :: Option -> String -> Property
journaldConfigured option value =
configured "/etc/systemd/journald.conf" option value
`onChange` restarted "systemd-journald"
-- | Causes systemd to reload its configuration files.
daemonReloaded :: Property
daemonReloaded = trivial $ cmdProperty "systemctl" ["daemon-reload"]
2014-11-21 19:55:27 +00:00
-- | Defines a container with a given machine name.
2014-11-20 21:18:26 +00:00
--
2014-11-21 19:55:27 +00:00
-- Properties can be added to configure the Container.
--
-- > container "webserver" (Chroot.debootstrapped (System (Debian Unstable) "amd64") mempty)
-- > & Apt.installedRunning "apache2"
-- > & ...
2014-11-21 19:55:27 +00:00
container :: MachineName -> (FilePath -> Chroot.Chroot) -> Container
container name mkchroot = Container name c h
& os system
& resolvConfed
2014-11-21 19:55:27 +00:00
where
c@(Chroot.Chroot _ system _ _) = mkchroot (containerDir name)
h = Host name [] mempty
2014-11-20 21:18:26 +00:00
-- | Runs a container using systemd-nspawn.
--
-- A systemd unit is set up for the container, so it will automatically
-- be started on boot.
--
-- Systemd is automatically installed inside the container, and will
-- communicate with the host's systemd. This allows systemctl to be used to
-- examine the status of services running inside the container.
--
-- When the host system has persistentJournal enabled, journactl can be
-- used to examine logs forwarded from the container.
--
-- Reverting this property stops the container, removes the systemd unit,
-- and deletes the chroot and all its contents.
nspawned :: Container -> RevertableProperty
2014-11-21 19:55:27 +00:00
nspawned c@(Container name (Chroot.Chroot loc system builderconf _) h) =
RevertableProperty setup teardown
2014-11-20 21:18:26 +00:00
where
setup = combineProperties ("nspawned " ++ name) $
2014-11-21 17:55:42 +00:00
map toProp steps ++ [containerprovisioned]
teardown = combineProperties ("not nspawned " ++ name) $
2014-11-21 17:55:42 +00:00
map (toProp . revert) (reverse steps)
2014-11-21 17:49:17 +00:00
steps =
[ enterScript c
, chrootprovisioned
, nspawnService c (_chrootCfg $ _chrootinfo $ hostInfo h)
2014-11-21 17:49:17 +00:00
]
2014-11-20 21:18:26 +00:00
2014-11-21 21:11:26 +00:00
-- Chroot provisioning is run in systemd-only mode,
-- which sets up the chroot and ensures systemd and dbus are
-- installed, but does not handle the other provisions.
chrootprovisioned = Chroot.provisioned'
(Chroot.propigateChrootInfo chroot) chroot True
-- Use nsenter to enter container and and run propellor to
-- finish provisioning.
containerprovisioned = Chroot.propellChroot chroot
2014-11-21 21:11:26 +00:00
(enterContainerProcess c) False
2014-11-21 21:11:26 +00:00
chroot = Chroot.Chroot loc system builderconf h
2014-11-20 21:18:26 +00:00
-- | Sets up the service file for the container, and then starts
-- it running.
nspawnService :: Container -> ChrootCfg -> RevertableProperty
nspawnService (Container name _ _) cfg = RevertableProperty setup teardown
2014-11-20 21:18:26 +00:00
where
service = nspawnServiceName name
servicefile = "/etc/systemd/system/multi-user.target.wants" </> service
servicefilecontent = do
2014-11-22 00:19:20 +00:00
ls <- lines <$> readFile "/lib/systemd/system/systemd-nspawn@.service"
return $ unlines $
"# deployed by propellor" : map addparams ls
addparams l
| "ExecStart=" `isPrefixOf` l =
l ++ " " ++ unwords (nspawnServiceParams cfg)
| otherwise = l
goodservicefile = (==)
<$> servicefilecontent
<*> catchDefaultIO "" (readFile servicefile)
2014-12-07 18:49:12 +00:00
writeservicefile = property servicefile $ makeChange $
viaTmp writeFile servicefile =<< servicefilecontent
setupservicefile = check (not <$> goodservicefile) $
-- if it's running, it has the wrong configuration,
-- so stop it
stopped service
`requires` daemonReloaded
`requires` writeservicefile
setup = started service `requires` setupservicefile
2014-11-20 21:18:26 +00:00
2014-11-21 18:37:19 +00:00
teardown = check (doesFileExist servicefile) $
disabled service `requires` stopped service
nspawnServiceParams :: ChrootCfg -> [String]
2014-11-22 00:53:38 +00:00
nspawnServiceParams NoChrootCfg = []
nspawnServiceParams (SystemdNspawnCfg ps) =
M.keys $ M.filter id $ M.fromList ps
2014-11-20 21:18:26 +00:00
-- | Installs a "enter-machinename" script that root can use to run a
-- command inside the container.
--
-- This uses nsenter to enter the container, by looking up the pid of the
-- container's init process and using its namespace.
enterScript :: Container -> RevertableProperty
enterScript c@(Container name _ _) = RevertableProperty setup teardown
where
setup = combineProperties ("generated " ++ enterScriptFile c)
[ scriptfile `File.hasContent`
[ "#!/bin/sh"
, "# Generated by propellor"
, "pid=\"$(machinectl show " ++ shellEscape name ++ " -p Leader | cut -d= -f2)\" || true"
, "if [ -n \"$pid\" ]; then"
2014-11-21 17:57:58 +00:00
, "\tnsenter -p -u -n -i -m -t \"$pid\" \"$@\""
, "else"
2014-11-21 17:55:42 +00:00
, "\techo container not running >&2"
, "\texit 1"
, "fi"
]
, scriptfile `File.mode` combineModes (readModes ++ executeModes)
]
teardown = File.notPresent scriptfile
scriptfile = enterScriptFile c
enterScriptFile :: Container -> FilePath
enterScriptFile (Container name _ _ ) = "/usr/local/bin/enter-" ++ mungename name
enterContainerProcess :: Container -> [String] -> CreateProcess
enterContainerProcess = proc . enterScriptFile
2014-11-21 16:35:07 +00:00
nspawnServiceName :: MachineName -> ServiceName
2014-11-20 21:18:26 +00:00
nspawnServiceName name = "systemd-nspawn@" ++ name ++ ".service"
containerDir :: MachineName -> FilePath
2014-11-21 16:35:07 +00:00
containerDir name = "/var/lib/container" </> mungename name
mungename :: MachineName -> String
mungename = replace "/" "_"
-- | This configures how systemd-nspawn(1) starts the container,
-- by specifying a parameter, such as "--private-network", or
-- "--link-journal=guest"
--
-- When there is no leading dash, "--" is prepended to the parameter.
--
-- Reverting the property will remove a parameter, if it's present.
containerCfg :: String -> RevertableProperty
containerCfg p = RevertableProperty (mk True) (mk False)
where
2014-11-22 00:53:38 +00:00
mk b = pureInfoProperty ("container configuration " ++ (if b then "" else "without ") ++ p') $
mempty { _chrootinfo = mempty { _chrootCfg = SystemdNspawnCfg [(p', b)] } }
p' = case p of
('-':_) -> p
_ -> "--" ++ p
2014-12-09 18:22:37 +00:00
-- | Bind mounts </etc/resolv.conf> from the host into the container.
--
-- This property is enabled by default. Revert it to disable it.
resolvConfed :: RevertableProperty
resolvConfed = containerCfg "bind=/etc/resolv.conf"