Propellor monad is a Reader for HostAttr

So far, the hostname is only used to improve a message in withPrivData,
but I anticipate using HostAttr for a lot more.
This commit is contained in:
Joey Hess 2014-04-10 17:22:32 -04:00
parent 5acaf8758f
commit 25942fb0cc
18 changed files with 163 additions and 76 deletions

View File

@ -1,3 +1,5 @@
{-# LANGUAGE PackageImports #-}
-- | Pulls in lots of useful modules for building and using Properties.
--
-- Propellor enures that the system it's run in satisfies a list of
@ -31,6 +33,7 @@ module Propellor (
, module Propellor.Property.Cmd
, module Propellor.PrivData
, module Propellor.Engine
, module Propellor.Exception
, module Propellor.Message
, localdir
@ -43,6 +46,7 @@ import Propellor.Engine
import Propellor.Property.Cmd
import Propellor.PrivData
import Propellor.Message
import Propellor.Exception
import Utility.PartialPrelude as X
import Utility.Process as X
@ -62,6 +66,7 @@ import Control.Applicative as X
import Control.Monad as X
import Data.Monoid as X
import Control.Monad.IfElse as X
import "mtl" Control.Monad.Reader as X
-- | This is where propellor installs itself when deploying a host.
localdir :: FilePath

View File

@ -66,21 +66,23 @@ defaultMain getprops = do
go _ (Continue cmdline) = go False cmdline
go _ (Set host field) = setPrivData host field
go _ (AddKey keyid) = addKey keyid
go _ (Chain host) = withprops host $ \ps -> do
r <- ensureProperties' ps
go _ (Chain host) = withprops host $ \hostattr ps -> do
r <- runPropellor hostattr $ ensureProperties ps
putStrLn $ "\n" ++ show r
go _ (Docker host) = Docker.chain host
go True cmdline@(Spin _) = buildFirst cmdline $ go False cmdline
go True cmdline = updateFirst cmdline $ go False cmdline
go False (Spin host) = withprops host $ const $ spin host
go False (Spin host) = withprops host $ const . const $ spin host
go False (Run host) = ifM ((==) 0 <$> getRealUserID)
( onlyProcess $ withprops host ensureProperties
( onlyProcess $ withprops host mainProperties
, go True (Spin host)
)
go False (Boot host) = onlyProcess $ withprops host $ boot
withprops host a = maybe (unknownhost host) a $
withprops host a = maybe (unknownhost host) (a hostattr) $
headMaybe $ catMaybes $ map (\get -> get host) getprops
where
hostattr = mkHostAttr host
onlyProcess :: IO a -> IO a
onlyProcess a = bracket lock unlock (const a)
@ -275,15 +277,15 @@ fromMarked marker s
len = length marker
matches = filter (marker `isPrefixOf`) $ lines s
boot :: [Property] -> IO ()
boot ps = do
boot :: HostAttr -> [Property] -> IO ()
boot hostattr ps = do
sendMarked stdout statusMarker $ show Ready
reply <- hGetContentsStrict stdin
makePrivDataDir
maybe noop (writeFileProtected privDataLocal) $
fromMarked privDataMarker reply
ensureProperties ps
mainProperties hostattr ps
addKey :: String -> IO ()
addKey keyid = exitBool =<< allM id [ gpg, gitadd, gitcommit ]

View File

@ -1,30 +1,37 @@
{-# LANGUAGE PackageImports #-}
module Propellor.Engine where
import System.Exit
import System.IO
import Data.Monoid
import System.Console.ANSI
import "mtl" Control.Monad.Reader
import Propellor.Types
import Propellor.Message
import Utility.Exception
import Propellor.Exception
ensureProperty :: Property -> IO Result
ensureProperty = catchDefaultIO FailedChange . propertySatisfy
runPropellor :: HostAttr -> Propellor a -> IO a
runPropellor hostattr a = runReaderT (runWithHostAttr a) hostattr
ensureProperties :: [Property] -> IO ()
ensureProperties ps = do
r <- ensureProperties' [Property "overall" $ ensureProperties' ps]
mainProperties :: HostAttr -> [Property] -> IO ()
mainProperties hostattr ps = do
r <- runPropellor hostattr $
ensureProperties [Property "overall" $ ensureProperties ps]
setTitle "propellor: done"
hFlush stdout
case r of
FailedChange -> exitWith (ExitFailure 1)
_ -> exitWith ExitSuccess
ensureProperties' :: [Property] -> IO Result
ensureProperties' ps = ensure ps NoChange
ensureProperties :: [Property] -> Propellor Result
ensureProperties ps = ensure ps NoChange
where
ensure [] rs = return rs
ensure (l:ls) rs = do
r <- actionMessage (propertyDesc l) (ensureProperty l)
ensure ls (r <> rs)
ensureProperty :: Property -> Propellor Result
ensureProperty = catchPropellor . propertySatisfy

16
Propellor/Exception.hs Normal file
View File

@ -0,0 +1,16 @@
{-# LANGUAGE PackageImports #-}
module Propellor.Exception where
import qualified "MonadCatchIO-transformers" Control.Monad.CatchIO as M
import Control.Exception
import Control.Applicative
import Propellor.Types
-- | Catches IO exceptions and returns FailedChange.
catchPropellor :: Propellor Result -> Propellor Result
catchPropellor a = either (\_ -> FailedChange) id <$> tryPropellor a
tryPropellor :: Propellor a -> Propellor (Either IOException a)
tryPropellor = M.try

View File

@ -1,30 +1,35 @@
{-# LANGUAGE PackageImports #-}
module Propellor.Message where
import System.Console.ANSI
import System.IO
import System.Log.Logger
import "mtl" Control.Monad.Reader
import Propellor.Types
-- | Shows a message while performing an action, with a colored status
-- display.
actionMessage :: ActionResult r => Desc -> IO r -> IO r
actionMessage :: (MonadIO m, ActionResult r) => Desc -> m r -> m r
actionMessage desc a = do
setTitle $ "propellor: " ++ desc
hFlush stdout
liftIO $ do
setTitle $ "propellor: " ++ desc
hFlush stdout
r <- a
setTitle "propellor: running"
let (msg, intensity, color) = getActionResult r
putStr $ desc ++ " ... "
colorLine intensity color msg
hFlush stdout
liftIO $ do
setTitle "propellor: running"
let (msg, intensity, color) = getActionResult r
putStr $ desc ++ " ... "
colorLine intensity color msg
hFlush stdout
return r
warningMessage :: String -> IO ()
warningMessage s = colorLine Vivid Red $ "** warning: " ++ s
warningMessage :: MonadIO m => String -> m ()
warningMessage s = liftIO $ colorLine Vivid Red $ "** warning: " ++ s
colorLine :: ColorIntensity -> Color -> String -> IO ()
colorLine intensity color msg = do

View File

@ -1,3 +1,5 @@
{-# LANGUAGE PackageImports #-}
module Propellor.PrivData where
import qualified Data.Map as M
@ -7,6 +9,7 @@ import System.IO
import System.Directory
import Data.Maybe
import Control.Monad
import "mtl" Control.Monad.Reader
import Propellor.Types
import Propellor.Message
@ -18,13 +21,15 @@ import Utility.Tmp
import Utility.SafeCommand
import Utility.Misc
withPrivData :: PrivDataField -> (String -> IO Result) -> IO Result
withPrivData field a = maybe missing a =<< getPrivData field
withPrivData :: PrivDataField -> (String -> Propellor Result) -> Propellor Result
withPrivData field a = maybe missing a =<< liftIO (getPrivData field)
where
missing = do
warningMessage $ "Missing privdata " ++ show field
putStrLn $ "Fix this by running: propellor --set $hostname '" ++ show field ++ "'"
return FailedChange
host <- getHostName
liftIO $ do
warningMessage $ "Missing privdata " ++ show field
putStrLn $ "Fix this by running: propellor --set "++host++" '" ++ show field ++ "'"
return FailedChange
getPrivData :: PrivDataField -> IO (Maybe String)
getPrivData field = do

View File

@ -1,18 +1,21 @@
{-# LANGUAGE PackageImports #-}
module Propellor.Property where
import System.Directory
import Control.Monad
import Data.Monoid
import Control.Monad.IfElse
import "mtl" Control.Monad.Reader
import Propellor.Types
import Propellor.Engine
import Utility.Monad
makeChange :: IO () -> IO Result
makeChange a = a >> return MadeChange
makeChange :: IO () -> Propellor Result
makeChange a = liftIO a >> return MadeChange
noChange :: IO Result
noChange :: Propellor Result
noChange = return NoChange
-- | Combines a list of properties, resulting in a single property
@ -20,7 +23,7 @@ noChange = return NoChange
-- and print out the description of each as it's run. Does not stop
-- on failure; does propigate overall success/failure.
propertyList :: Desc -> [Property] -> Property
propertyList desc ps = Property desc $ ensureProperties' ps
propertyList desc ps = Property desc $ ensureProperties ps
-- | Combines a list of properties, resulting in one property that
-- ensures each in turn, stopping on failure.
@ -49,12 +52,12 @@ p1 `before` p2 = Property (propertyDesc p1) $ do
-- Use with caution.
flagFile :: Property -> FilePath -> Property
flagFile property flagfile = Property (propertyDesc property) $
go =<< doesFileExist flagfile
go =<< liftIO (doesFileExist flagfile)
where
go True = return NoChange
go False = do
r <- ensureProperty property
when (r == MadeChange) $
when (r == MadeChange) $ liftIO $
unlessM (doesFileExist flagfile) $
writeFile flagfile ""
return r
@ -76,13 +79,13 @@ infixl 1 ==>
-- | Makes a Property only be performed when a test succeeds.
check :: IO Bool -> Property -> Property
check c property = Property (propertyDesc property) $ ifM c
check c property = Property (propertyDesc property) $ ifM (liftIO c)
( ensureProperty property
, return NoChange
)
boolProperty :: Desc -> IO Bool -> Property
boolProperty desc a = Property desc $ ifM a
boolProperty desc a = Property desc $ ifM (liftIO a)
( return MadeChange
, return FailedChange
)

View File

@ -1,3 +1,5 @@
{-# LANGUAGE PackageImports #-}
module Propellor.Property.Cmd (
cmdProperty,
cmdProperty',
@ -7,6 +9,7 @@ module Propellor.Property.Cmd (
import Control.Applicative
import Data.List
import "mtl" Control.Monad.Reader
import Propellor.Types
import Utility.Monad
@ -22,7 +25,7 @@ cmdProperty cmd params = cmdProperty' cmd params []
-- | A property that can be satisfied by running a command,
-- with added environment.
cmdProperty' :: String -> [String] -> [(String, String)] -> Property
cmdProperty' cmd params env = Property desc $ do
cmdProperty' cmd params env = Property desc $ liftIO $ do
env' <- addEntries env <$> getEnvironment
ifM (boolSystemEnv cmd (map Param params) (Just env'))
( return MadeChange

View File

@ -53,7 +53,7 @@ docked findc hn cn = findContainer findc hn cn $
teardown = combineProperties ("undocked " ++ fromContainerId cid)
[ stoppedContainer cid
, Property ("cleaned up " ++ fromContainerId cid) $
report <$> mapM id
liftIO $ report <$> mapM id
[ removeContainer cid
, removeImage image
]
@ -74,7 +74,7 @@ findContainer findc hn cn mk = case findc hn cn of
where
cid = ContainerId hn cn
cantfind = containerDesc (ContainerId hn cn) $ Property "" $ do
warningMessage $ "missing definition for docker container \"" ++ fromContainerId cid
liftIO $ warningMessage $ "missing definition for docker container \"" ++ fromContainerId cid
return FailedChange
-- | Causes *any* docker images that are not in use by running containers to
@ -90,9 +90,9 @@ garbageCollected = propertyList "docker garbage collected"
]
where
gccontainers = Property "docker containers garbage collected" $
report <$> (mapM removeContainer =<< listContainers AllContainers)
liftIO $ report <$> (mapM removeContainer =<< listContainers AllContainers)
gcimages = Property "docker images garbage collected" $ do
report <$> (mapM removeImage =<< listImages)
liftIO $ report <$> (mapM removeImage =<< listImages)
-- | Pass to defaultMain to add docker containers.
-- You need to provide the function mapping from
@ -239,19 +239,19 @@ containerDesc cid p = p `describe` desc
runningContainer :: ContainerId -> Image -> [Containerized Property] -> Property
runningContainer cid@(ContainerId hn cn) image containerprops = containerDesc cid $ Property "running" $ do
l <- listContainers RunningContainers
l <- liftIO $ listContainers RunningContainers
if cid `elem` l
then do
-- Check if the ident has changed; if so the
-- parameters of the container differ and it must
-- be restarted.
runningident <- getrunningident
runningident <- liftIO $ getrunningident
if runningident == Just ident
then return NoChange
then noChange
else do
void $ stopContainer cid
void $ liftIO $ stopContainer cid
restartcontainer
else ifM (elem cid <$> listContainers AllContainers)
else ifM (liftIO $ elem cid <$> listContainers AllContainers)
( restartcontainer
, go image
)
@ -259,8 +259,8 @@ runningContainer cid@(ContainerId hn cn) image containerprops = containerDesc ci
ident = ContainerIdent image hn cn runps
restartcontainer = do
oldimage <- fromMaybe image <$> commitContainer cid
void $ removeContainer cid
oldimage <- liftIO $ fromMaybe image <$> commitContainer cid
void $ liftIO $ removeContainer cid
go oldimage
getrunningident :: IO (Maybe ContainerIdent)
@ -280,10 +280,11 @@ runningContainer cid@(ContainerId hn cn) image containerprops = containerDesc ci
]
go img = do
clearProvisionedFlag cid
createDirectoryIfMissing True (takeDirectory $ identFile cid)
shim <- Shim.setup (localdir </> "propellor") (localdir </> shimdir cid)
writeFile (identFile cid) (show ident)
liftIO $ do
clearProvisionedFlag cid
createDirectoryIfMissing True (takeDirectory $ identFile cid)
shim <- liftIO $ Shim.setup (localdir </> "propellor") (localdir </> shimdir cid)
liftIO $ writeFile (identFile cid) (show ident)
ensureProperty $ boolProperty "run" $ runContainer img
(runps ++ ["-i", "-d", "-t"])
[shim, "--docker", fromContainerId cid]
@ -339,7 +340,7 @@ chain s = case toContainerId s of
-- being run. So, retry connections to the client for up to
-- 1 minute.
provisionContainer :: ContainerId -> Property
provisionContainer cid = containerDesc cid $ Property "provision" $ do
provisionContainer cid = containerDesc cid $ Property "provision" $ liftIO $ do
let shim = Shim.file (localdir </> "propellor") (localdir </> shimdir cid)
r <- simpleShClientRetry 60 (namedPipe cid) shim params (go Nothing)
when (r /= FailedChange) $
@ -372,8 +373,8 @@ stopContainer cid = boolSystem dockercmd [Param "stop", Param $ fromContainerId
stoppedContainer :: ContainerId -> Property
stoppedContainer cid = containerDesc cid $ Property desc $
ifM (elem cid <$> listContainers RunningContainers)
( cleanup `after` ensureProperty
ifM (liftIO $ elem cid <$> listContainers RunningContainers)
( liftIO cleanup `after` ensureProperty
(boolProperty desc $ stopContainer cid)
, return NoChange
)

View File

@ -38,10 +38,10 @@ notPresent f = check (doesFileExist f) $ Property (f ++ " not present") $
makeChange $ nukeFile f
fileProperty :: Desc -> ([Line] -> [Line]) -> FilePath -> Property
fileProperty desc a f = Property desc $ go =<< doesFileExist f
fileProperty desc a f = Property desc $ go =<< liftIO (doesFileExist f)
where
go True = do
ls <- lines <$> readFile f
ls <- liftIO $ lines <$> readFile f
let ls' = a ls
if ls' == ls
then noChange

View File

@ -20,13 +20,13 @@ import qualified Data.Map as M
-- last run.
period :: Property -> Recurrance -> Property
period prop recurrance = Property desc $ do
lasttime <- getLastChecked (propertyDesc prop)
nexttime <- fmap startTime <$> nextTime schedule lasttime
t <- localNow
lasttime <- liftIO $ getLastChecked (propertyDesc prop)
nexttime <- liftIO $ fmap startTime <$> nextTime schedule lasttime
t <- liftIO localNow
if Just t >= nexttime
then do
r <- ensureProperty prop
setLastChecked t (propertyDesc prop)
liftIO $ setLastChecked t (propertyDesc prop)
return r
else noChange
where
@ -38,7 +38,7 @@ periodParse :: Property -> String -> Property
periodParse prop s = case toRecurrance s of
Just recurrance -> period prop recurrance
Nothing -> Property "periodParse" $ do
warningMessage $ "failed periodParse: " ++ s
liftIO $ warningMessage $ "failed periodParse: " ++ s
noChange
lastCheckedFile :: FilePath

View File

@ -44,12 +44,13 @@ builder arch crontimes rsyncupload = combineProperties "gitannexbuilder"
let f = homedir </> "rsyncpassword"
if rsyncupload
then withPrivData (Password builduser) $ \p -> do
oldp <- catchDefaultIO "" $ readFileStrict f
oldp <- liftIO $ catchDefaultIO "" $
readFileStrict f
if p /= oldp
then makeChange $ writeFile f p
else noChange
else do
ifM (doesFileExist f)
ifM (liftIO $ doesFileExist f)
( noChange
, makeChange $ writeFile f "no password configured"
)

View File

@ -8,7 +8,7 @@ import Utility.SafeCommand
-- | Clones Joey Hess's git home directory, and runs its fixups script.
installedFor :: UserName -> Property
installedFor user = check (not <$> hasGitDir user) $
Property ("githome " ++ user) (go =<< homedir user)
Property ("githome " ++ user) (go =<< liftIO (homedir user))
`requires` Apt.installed ["git"]
where
go Nothing = noChange

View File

@ -53,7 +53,7 @@ uniqueHostKeys = flagFile prop "/etc/ssh/.unique_host_keys"
`onChange` restartSshd
where
prop = Property "ssh unique host keys" $ do
void $ boolSystem "sh"
void $ liftIO $ boolSystem "sh"
[ Param "-c"
, Param "rm -f /etc/ssh/ssh_host_*"
]

View File

@ -13,7 +13,7 @@ enabledFor :: UserName -> Property
enabledFor user = Property desc go `requires` Apt.installed ["sudo"]
where
go = do
locked <- isLockedPassword user
locked <- liftIO $ isLockedPassword user
ensureProperty $
fileProperty desc
(modify locked . filter (wanted locked))

View File

@ -1,20 +1,53 @@
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Propellor.Types where
import Data.Monoid
import Control.Applicative
import System.Console.ANSI
import "mtl" Control.Monad.Reader
import "MonadCatchIO-transformers" Control.Monad.CatchIO
type HostName = String
type GroupName = String
type UserName = String
-- | The core data type of Propellor, this reprecents a property
-- that the system should have, and an action to ensure it has the
-- property.
data Property = Property
{ propertyDesc :: Desc
-- | must be idempotent; may run repeatedly
, propertySatisfy :: IO Result
, propertySatisfy :: Propellor Result
}
-- | A property that can be reverted.
data RevertableProperty = RevertableProperty Property Property
-- | Propellor's monad provides read-only access to attributes of the
-- system.
newtype Propellor a = Propellor { runWithHostAttr :: ReaderT HostAttr IO a }
deriving
( Monad
, Functor
, Applicative
, MonadReader HostAttr
, MonadIO
, MonadCatchIO
)
-- | The attributes of a system. For example, its hostname.
newtype HostAttr = HostAttr
{ _hostname :: HostName
}
mkHostAttr :: HostName -> HostAttr
mkHostAttr = HostAttr
getHostName :: Propellor HostName
getHostName = asks _hostname
class IsProp p where
-- | Sets description.
describe :: p -> Desc -> p

4
debian/changelog vendored
View File

@ -1,4 +1,4 @@
propellor (0.2.4) UNRELEASED; urgency=medium
propellor (0.3.0) UNRELEASED; urgency=medium
* ipv6to4: Ensure interface is brought up automatically on boot.
* Enabling unattended upgrades now ensures that cron is installed and
@ -8,6 +8,8 @@ propellor (0.2.4) UNRELEASED; urgency=medium
* Fix compilation on Debian stable.
* Include security updates in sources.list for stable and testing.
* Use ssh connection caching, especially when bootstrapping.
* Properties now run in a Propellor monad, which provides access to
attributes of the host.
-- Joey Hess <joeyh@debian.org> Tue, 08 Apr 2014 18:07:12 -0400

View File

@ -1,5 +1,5 @@
Name: propellor
Version: 0.2.3
Version: 0.3.0
Cabal-Version: >= 1.6
License: GPL
Maintainer: Joey Hess <joey@kitenet.net>
@ -38,7 +38,8 @@ Executable propellor
GHC-Options: -Wall
Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5,
IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal,
containers, network, async, time, QuickCheck
containers, network, async, time, QuickCheck, mtl,
MonadCatchIO-transformers
if (! os(windows))
Build-Depends: unix
@ -48,7 +49,8 @@ Executable config
GHC-Options: -Wall -threaded
Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5,
IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal,
containers, network, async, time, QuickCheck
containers, network, async, time, QuickCheck, mtl,
MonadCatchIO-transformers
if (! os(windows))
Build-Depends: unix
@ -57,7 +59,8 @@ Library
GHC-Options: -Wall
Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5,
IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal,
containers, network, async, time, QuickCheck
containers, network, async, time, QuickCheck, mtl,
MonadCatchIO-transformers
if (! os(windows))
Build-Depends: unix
@ -88,6 +91,7 @@ Library
Propellor.Message
Propellor.PrivData
Propellor.Engine
Propellor.Exception
Propellor.Types
Other-Modules:
Propellor.CmdLine