From 42594d6b4c14a21efc42e262e52c2c67f30c67c3 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 19 Jun 2014 14:41:55 -0400 Subject: Add --edit to edit a privdata value in $EDITOR --- src/Propellor/CmdLine.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/Propellor/CmdLine.hs') diff --git a/src/Propellor/CmdLine.hs b/src/Propellor/CmdLine.hs index 32e97316..c084355b 100644 --- a/src/Propellor/CmdLine.hs +++ b/src/Propellor/CmdLine.hs @@ -26,9 +26,10 @@ usage = do , " propellor" , " propellor hostname" , " propellor --spin hostname" + , " propellor --add-key keyid" , " propellor --set hostname field" , " propellor --dump hostname field" - , " propellor --add-key keyid" + , " propellor --edit hostname field" ] exitFailure @@ -41,6 +42,7 @@ processCmdLine = go =<< getArgs go ("--add-key":k:[]) = return $ AddKey k go ("--set":h:f:[]) = withprivfield f (return . Set h) go ("--dump":h:f:[]) = withprivfield f (return . Dump h) + go ("--edit":h:f:[]) = withprivfield f (return . Edit h) go ("--continue":s:[]) = case readish s of Just cmdline -> return $ Continue cmdline Nothing -> errorMessage "--continue serialization failure" @@ -71,6 +73,7 @@ defaultMain hostlist = do go _ (Continue cmdline) = go False cmdline go _ (Set hn field) = setPrivData hn field go _ (Dump hn field) = dumpPrivData hn field + go _ (Edit hn field) = editPrivData hn field go _ (AddKey keyid) = addKey keyid go _ (Chain hn) = withhost hn $ \h -> do r <- runPropellor h $ ensureProperties $ hostProperties h -- cgit v1.2.3 From f674c56119bd9b1bb9af53d5063c57275be77827 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 19 Jun 2014 14:56:50 -0400 Subject: Add --list-fields to list a host's currently set privdata fields. --- debian/changelog | 1 + src/Propellor/CmdLine.hs | 3 +++ src/Propellor/PrivData.hs | 7 +++++++ src/Propellor/Types.hs | 1 + 4 files changed, 12 insertions(+) (limited to 'src/Propellor/CmdLine.hs') diff --git a/debian/changelog b/debian/changelog index 95d0b232..8e9a2b48 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,7 @@ propellor (0.7.1) UNRELEASED; urgency=medium * Add --edit to edit a privdata value in $EDITOR. + * Add --list-fields to list a host's currently set privdata fields. -- Joey Hess Thu, 19 Jun 2014 14:27:21 -0400 diff --git a/src/Propellor/CmdLine.hs b/src/Propellor/CmdLine.hs index c084355b..1027fd8d 100644 --- a/src/Propellor/CmdLine.hs +++ b/src/Propellor/CmdLine.hs @@ -30,6 +30,7 @@ usage = do , " propellor --set hostname field" , " propellor --dump hostname field" , " propellor --edit hostname field" + , " propellor --list-fields hostname" ] exitFailure @@ -43,6 +44,7 @@ processCmdLine = go =<< getArgs go ("--set":h:f:[]) = withprivfield f (return . Set h) go ("--dump":h:f:[]) = withprivfield f (return . Dump h) go ("--edit":h:f:[]) = withprivfield f (return . Edit h) + go ("--list-fields":h:[]) = return $ ListFields h go ("--continue":s:[]) = case readish s of Just cmdline -> return $ Continue cmdline Nothing -> errorMessage "--continue serialization failure" @@ -74,6 +76,7 @@ defaultMain hostlist = do go _ (Set hn field) = setPrivData hn field go _ (Dump hn field) = dumpPrivData hn field go _ (Edit hn field) = editPrivData hn field + go _ (ListFields hn) = listPrivDataFields hn go _ (AddKey keyid) = addKey keyid go _ (Chain hn) = withhost hn $ \h -> do r <- runPropellor h $ ensureProperties $ hostProperties h diff --git a/src/Propellor/PrivData.hs b/src/Propellor/PrivData.hs index fec6acc3..c2af4284 100644 --- a/src/Propellor/PrivData.hs +++ b/src/Propellor/PrivData.hs @@ -68,6 +68,13 @@ editPrivData host field = do readFile f setPrivDataTo host field v' +listPrivDataFields :: HostName -> IO () +listPrivDataFields host = do + putStrLn (host ++ "'s currently set privdata fields:") + mapM_ list . M.keys =<< decryptPrivData host + where + list = putStrLn . ("\t" ++) . shellEscape . show + setPrivDataTo :: HostName -> PrivDataField -> String -> IO () setPrivDataTo host field value = do makePrivDataDir diff --git a/src/Propellor/Types.hs b/src/Propellor/Types.hs index 740996be..59652f66 100644 --- a/src/Propellor/Types.hs +++ b/src/Propellor/Types.hs @@ -138,6 +138,7 @@ data CmdLine | Set HostName PrivDataField | Dump HostName PrivDataField | Edit HostName PrivDataField + | ListFields HostName | AddKey String | Continue CmdLine | Chain HostName -- cgit v1.2.3 From 58f79c12aad3511b70f2233226d3f0afc5214b10 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Sun, 6 Jul 2014 15:56:56 -0400 Subject: propellor spin --- config-joey.hs | 30 +++--- debian/changelog | 11 +- doc/security.mdwn | 15 +-- propellor.cabal | 1 + src/Propellor/CmdLine.hs | 32 +++--- src/Propellor/PrivData.hs | 116 ++++++++++++--------- src/Propellor/Property/Docker.hs | 7 +- src/Propellor/Property/File.hs | 13 +-- src/Propellor/Property/Gpg.hs | 17 +-- src/Propellor/Property/OpenId.hs | 5 +- .../Property/SiteSpecific/GitAnnexBuilder.hs | 38 +++---- src/Propellor/Property/SiteSpecific/JoeySites.hs | 28 ++--- src/Propellor/Property/Ssh.hs | 45 ++++---- src/Propellor/Property/User.hs | 23 ++-- src/Propellor/Types.hs | 31 ++---- src/Propellor/Types/Info.hs | 5 +- src/Propellor/Types/PrivData.hs | 34 ++++++ 17 files changed, 252 insertions(+), 199 deletions(-) create mode 100644 src/Propellor/Types/PrivData.hs (limited to 'src/Propellor/CmdLine.hs') diff --git a/config-joey.hs b/config-joey.hs index 86117070..31ea685c 100644 --- a/config-joey.hs +++ b/config-joey.hs @@ -72,14 +72,15 @@ hosts = -- (o) ` & Apt.buildDep ["git-annex"] `period` Daily -- Important stuff that needs not too much memory or CPU. - , standardSystem "diatom.kitenet.net" Stable "amd64" + , let ctx = Context "diatom.kitenet.net " + in standardSystem "diatom.kitenet.net" Stable "amd64" & ipv4 "107.170.31.195" & DigitalOcean.distroKernel & Hostname.sane - & Ssh.hostKey SshDsa - & Ssh.hostKey SshRsa - & Ssh.hostKey SshEcdsa + & Ssh.hostKey SshDsa ctx + & Ssh.hostKey SshRsa ctx + & Ssh.hostKey SshEcdsa ctx & Apt.unattendedUpgrades & Apt.serviceInstalledRunning "ntp" & Postfix.satellite @@ -89,9 +90,9 @@ hosts = -- (o) ` & Apt.serviceInstalledRunning "swapspace" & Apt.serviceInstalledRunning "apache2" - & File.hasPrivContent "/etc/ssl/certs/web.pem" - & File.hasPrivContent "/etc/ssl/private/web.pem" - & File.hasPrivContent "/etc/ssl/certs/startssl.pem" + & File.hasPrivContent "/etc/ssl/certs/web.pem" (Context "kitenet.net") + & File.hasPrivContent "/etc/ssl/private/web.pem" (Context "kitenet.net") + & File.hasPrivContent "/etc/ssl/certs/startssl.pem" (Context "kitenet.net") & Apache.modEnabled "ssl" & Apache.multiSSL & File.ownerGroup "/srv/web" "joey" "joey" @@ -133,16 +134,17 @@ hosts = -- (o) ` & Dns.secondaryFor ["animx"] hosts "animx.eu.org" -- storage and backup server - , standardSystem "elephant.kitenet.net" Unstable "amd64" + , let ctx = Context "elephant.kitenet.net" + in standardSystem "elephant.kitenet.net" Unstable "amd64" & ipv4 "193.234.225.114" & Hostname.sane & Postfix.satellite & Apt.unattendedUpgrades - & Ssh.hostKey SshDsa - & Ssh.hostKey SshRsa - & Ssh.hostKey SshEcdsa - & Ssh.keyImported SshRsa "joey" + & Ssh.hostKey SshDsa ctx + & Ssh.hostKey SshRsa ctx + & Ssh.hostKey SshEcdsa ctx + & Ssh.keyImported SshRsa "joey" ctx -- PV-grub chaining -- http://notes.pault.ag/linode-pv-grub-chainning/ @@ -263,13 +265,13 @@ standardSystem hn suite arch = host hn & Apt.installed ["etckeeper"] & Apt.installed ["ssh"] & GitHome.installedFor "root" - & User.hasSomePassword "root" + & User.hasSomePassword "root" (Context hn) -- Harden the system, but only once root's authorized_keys -- is safely in place. & check (Ssh.hasAuthorizedKeys "root") (Ssh.passwordAuthentication False) & User.accountFor "joey" - & User.hasSomePassword "joey" + & User.hasSomePassword "joey" (Context hn) & Sudo.enabledFor "joey" & GitHome.installedFor "joey" & Apt.installed ["vim", "screen", "less"] diff --git a/debian/changelog b/debian/changelog index 83a9a767..5530e5c8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,7 +1,14 @@ -propellor (0.7.1) UNRELEASED; urgency=medium +propellor (0.8.0) UNRELEASED; urgency=medium + + * Completely reworked privdata storage. There is now a single file, + and each host is sent only the privdata that its Properties actually use. + + To transition existing privdata, run propellor against a host and + watch out for the red failure messages, and run the suggested commands + to store the privdata using the new storage scheme. * Add --edit to edit a privdata value in $EDITOR. - * Add --list-fields to list a host's currently set privdata fields. + * Add --list-fields to list all currently set privdata fields. * Fix randomHostKeys property to run openssh-server's postinst in a non-failing way. * Hostname.sane now cleans up the 127.0.0.1 localhost line in /etc/hosts, diff --git a/doc/security.mdwn b/doc/security.mdwn index 5576bf06..075d68ec 100644 --- a/doc/security.mdwn +++ b/doc/security.mdwn @@ -27,10 +27,11 @@ Since the propoellor git repository is public, you can't store in cleartext private data such as passwords, ssh private keys, etc. Instead, `propellor --spin $host` looks for a -`~/.propellor/privdata/$host.gpg` file and if found decrypts it and sends -it to the remote host using ssh. This lets a remote host know its own -private data, without seeing all the rest. - -To securely store private data, use: `propellor --set $host $field` -The field name will be something like 'Password "root"'; see PrivData.hs -for available fields. +`~/.propellor/privdata/privdata.gpg` file and if found decrypts it, +extracts the private that that the $host needs, and sends it to to the +$host using ssh. This lets a host know its own private data, without +seeing all the rest. + +To securely store private data, use: `propellor --set $field $context` +Propellor will tell you the details when you use a Property that needs +PrivData. diff --git a/propellor.cabal b/propellor.cabal index 2ac8a44a..1f606d31 100644 --- a/propellor.cabal +++ b/propellor.cabal @@ -105,6 +105,7 @@ Library Propellor.Types Propellor.Types.OS Propellor.Types.Dns + Propellor.Types.PrivData Other-Modules: Propellor.Types.Info Propellor.CmdLine diff --git a/src/Propellor/CmdLine.hs b/src/Propellor/CmdLine.hs index 1027fd8d..b6dd2bc1 100644 --- a/src/Propellor/CmdLine.hs +++ b/src/Propellor/CmdLine.hs @@ -27,10 +27,10 @@ usage = do , " propellor hostname" , " propellor --spin hostname" , " propellor --add-key keyid" - , " propellor --set hostname field" - , " propellor --dump hostname field" - , " propellor --edit hostname field" - , " propellor --list-fields hostname" + , " propellor --set field context" + , " propellor --dump field context" + , " propellor --edit field context" + , " propellor --list-fields" ] exitFailure @@ -41,10 +41,10 @@ processCmdLine = go =<< getArgs go ("--spin":h:[]) = return $ Spin h go ("--boot":h:[]) = return $ Boot h go ("--add-key":k:[]) = return $ AddKey k - go ("--set":h:f:[]) = withprivfield f (return . Set h) - go ("--dump":h:f:[]) = withprivfield f (return . Dump h) - go ("--edit":h:f:[]) = withprivfield f (return . Edit h) - go ("--list-fields":h:[]) = return $ ListFields h + go ("--set":f:c:[]) = withprivfield f c Set + go ("--dump":f:c:[]) = withprivfield f c Dump + go ("--edit":f:c:[]) = withprivfield f c Edit + go ("--list-fields":[]) = return ListFields go ("--continue":s:[]) = case readish s of Just cmdline -> return $ Continue cmdline Nothing -> errorMessage "--continue serialization failure" @@ -60,8 +60,8 @@ processCmdLine = go =<< getArgs else return $ Run s go _ = usage - withprivfield s f = case readish s of - Just pf -> f pf + withprivfield s c f = case readish s of + Just pf -> return $ f pf (Context c) Nothing -> errorMessage $ "Unknown privdata field " ++ s defaultMain :: [Host] -> IO () @@ -73,10 +73,10 @@ defaultMain hostlist = do go True cmdline where go _ (Continue cmdline) = go False cmdline - go _ (Set hn field) = setPrivData hn field - go _ (Dump hn field) = dumpPrivData hn field - go _ (Edit hn field) = editPrivData hn field - go _ (ListFields hn) = listPrivDataFields hn + go _ (Set field context) = setPrivData field context + go _ (Dump field context) = dumpPrivData field context + go _ (Edit field context) = editPrivData field context + go _ ListFields = listPrivDataFields go _ (AddKey keyid) = addKey keyid go _ (Chain hn) = withhost hn $ \h -> do r <- runPropellor h $ ensureProperties $ hostProperties h @@ -182,11 +182,11 @@ spin hn = do void $ gitCommit [Param "--allow-empty", Param "-a", Param "-m", Param "propellor spin"] void $ boolSystem "git" [Param "push"] cacheparams <- toCommand <$> sshCachingParams hn - go cacheparams url =<< gpgDecrypt (privDataFile hn) + go cacheparams url =<< gpgDecrypt privDataFile where go cacheparams url privdata = withBothHandles createProcessSuccess (proc "ssh" $ cacheparams ++ [user, bootstrapcmd]) $ \(toh, fromh) -> do let finish = do - senddata toh (privDataFile hn) privDataMarker privdata + senddata toh privDataLocal privDataMarker privdata hClose toh -- Display remaining output. diff --git a/src/Propellor/PrivData.hs b/src/Propellor/PrivData.hs index c2af4284..d57b2e6f 100644 --- a/src/Propellor/PrivData.hs +++ b/src/Propellor/PrivData.hs @@ -2,18 +2,20 @@ module Propellor.PrivData where -import qualified Data.Map as M import Control.Applicative import System.FilePath import System.IO import System.Directory import Data.Maybe -import Data.List +import Data.Monoid import Control.Monad import Control.Monad.IfElse import "mtl" Control.Monad.Reader +import qualified Data.Map as M +import qualified Data.Set as S import Propellor.Types +import Propellor.Types.Info import Propellor.Message import Utility.Monad import Utility.PartialPrelude @@ -25,40 +27,57 @@ import Utility.Misc import Utility.FileMode import Utility.Env --- | When the specified PrivDataField is available on the host Propellor --- is provisioning, it provies the data to the action. Otherwise, it prints --- a message to help the user make the necessary private data available. -withPrivData :: PrivDataField -> (String -> Propellor Result) -> Propellor Result -withPrivData field a = maybe missing a =<< liftIO (getPrivData field) +-- | Allows a Property to access the value of a specific PrivDataField, +-- for use in a specific Context. +-- +-- Example use: +-- +-- > withPrivData (PrivFile pemfile) (Context "joeyh.name") $ \getdata -> +-- > property "joeyh.name ssl cert" $ getdata $ \privdata -> +-- > liftIO $ writeFile pemfile privdata +-- > where pemfile = "/etc/ssl/certs/web.pem" +-- +-- Note that if the value is not available, the action is not run +-- and instead it prints a message to help the user make the necessary +-- private data available. +withPrivData + :: PrivDataField + -> Context + -> (((PrivData -> Propellor Result) -> Propellor Result) -> Property) + -> Property +withPrivData field context@(Context cname) mkprop = addinfo $ mkprop $ \a -> + maybe missing a =<< liftIO (getLocalPrivData field context) + where + missing = liftIO $ do + warningMessage $ "Missing privdata " ++ show field ++ " (for " ++ cname ++ ")" + putStrLn $ "Fix this by running: propellor --set '" ++ show field ++ "' '" ++ cname ++ "'" + return FailedChange + addinfo p = p { propertyInfo = propertyInfo p <> mempty { _privDataFields = S.singleton (field, context) } } + +{- Gets the requested field's value, in the specified context if it's + - available, from the host's local privdata cache. -} +getLocalPrivData :: PrivDataField -> Context -> IO (Maybe PrivData) +getLocalPrivData field context = + getPrivData field context . fromMaybe M.empty <$> localcache where - missing = do - host <- asks hostName - let host' = if ".docker" `isSuffixOf` host - then "$parent_host" - else host - 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 - m <- catchDefaultIO Nothing $ readish <$> readFile privDataLocal - return $ maybe Nothing (M.lookup field) m - -setPrivData :: HostName -> PrivDataField -> IO () -setPrivData host field = do + localcache = catchDefaultIO Nothing $ readish <$> readFile privDataLocal + +getPrivData :: PrivDataField -> Context -> (M.Map (PrivDataField, Context) PrivData) -> Maybe PrivData +getPrivData field context = M.lookup (field, context) + +setPrivData :: PrivDataField -> Context -> IO () +setPrivData field context = do putStrLn "Enter private data on stdin; ctrl-D when done:" - setPrivDataTo host field =<< hGetContentsStrict stdin + setPrivDataTo field context =<< hGetContentsStrict stdin -dumpPrivData :: HostName -> PrivDataField -> IO () -dumpPrivData host field = +dumpPrivData :: PrivDataField -> Context -> IO () +dumpPrivData field context = maybe (error "Requested privdata is not set.") putStrLn - =<< getPrivDataFor host field + =<< (getPrivData field context <$> decryptPrivData) -editPrivData :: HostName -> PrivDataField -> IO () -editPrivData host field = do - v <- getPrivDataFor host field +editPrivData :: PrivDataField -> Context -> IO () +editPrivData field context = do + v <- getPrivData field context <$> decryptPrivData v' <- withTmpFile "propellorXXXX" $ \f h -> do hClose h maybe noop (writeFileProtected f) v @@ -66,35 +85,30 @@ editPrivData host field = do unlessM (boolSystem editor [File f]) $ error "Editor failed; aborting." readFile f - setPrivDataTo host field v' + setPrivDataTo field context v' -listPrivDataFields :: HostName -> IO () -listPrivDataFields host = do - putStrLn (host ++ "'s currently set privdata fields:") - mapM_ list . M.keys =<< decryptPrivData host +listPrivDataFields :: IO () +listPrivDataFields = do + putStrLn ("All currently set privdata fields:") + mapM_ list . M.keys =<< decryptPrivData where list = putStrLn . ("\t" ++) . shellEscape . show -setPrivDataTo :: HostName -> PrivDataField -> String -> IO () -setPrivDataTo host field value = do +setPrivDataTo :: PrivDataField -> Context -> PrivData -> IO () +setPrivDataTo field context value = do makePrivDataDir - let f = privDataFile host - m <- decryptPrivData host - let m' = M.insert field (chomp value) m - gpgEncrypt f (show m') + m <- decryptPrivData + let m' = M.insert (field, context) (chomp value) m + gpgEncrypt privDataFile (show m') putStrLn "Private data set." - void $ boolSystem "git" [Param "add", File f] + void $ boolSystem "git" [Param "add", File privDataFile] where chomp s | end s == "\n" = chomp (beginning s) | otherwise = s -getPrivDataFor :: HostName -> PrivDataField -> IO (Maybe String) -getPrivDataFor host field = M.lookup field <$> decryptPrivData host - -decryptPrivData :: HostName -> IO (M.Map PrivDataField String) -decryptPrivData host = fromMaybe M.empty . readish - <$> gpgDecrypt (privDataFile host) +decryptPrivData :: IO (M.Map (PrivDataField, Context) PrivData) +decryptPrivData = fromMaybe M.empty . readish <$> gpgDecrypt privDataFile makePrivDataDir :: IO () makePrivDataDir = createDirectoryIfMissing False privDataDir @@ -102,8 +116,8 @@ makePrivDataDir = createDirectoryIfMissing False privDataDir privDataDir :: FilePath privDataDir = "privdata" -privDataFile :: HostName -> FilePath -privDataFile host = privDataDir host ++ ".gpg" +privDataFile :: FilePath +privDataFile = privDataDir "privdata.gpg" privDataLocal :: FilePath privDataLocal = privDataDir "local" diff --git a/src/Propellor/Property/Docker.hs b/src/Propellor/Property/Docker.hs index 1521eb65..4d443986 100644 --- a/src/Propellor/Property/Docker.hs +++ b/src/Propellor/Property/Docker.hs @@ -55,10 +55,11 @@ installed = Apt.installed ["docker.io"] -- | Configures docker with an authentication file, so that images can be -- pushed to index.docker.io. Optional. configured :: Property -configured = property "docker configured" go `requires` installed +configured = prop `requires` installed where - go = withPrivData DockerAuthentication $ \cfg -> ensureProperty $ - "/root/.dockercfg" `File.hasContent` (lines cfg) + prop = withPrivData DockerAuthentication anyContext $ \getcfg -> + property "docker configured" $ getcfg $ \cfg -> ensureProperty $ + "/root/.dockercfg" `File.hasContent` (lines cfg) -- | A short descriptive name for a container. -- Should not contain whitespace or other unusual characters, diff --git a/src/Propellor/Property/File.hs b/src/Propellor/Property/File.hs index 0b060177..0e738f25 100644 --- a/src/Propellor/Property/File.hs +++ b/src/Propellor/Property/File.hs @@ -17,16 +17,17 @@ f `hasContent` newcontent = fileProperty ("replace " ++ f) -- -- The file's permissions are preserved if the file already existed. -- Otherwise, they're set to 600. -hasPrivContent :: FilePath -> Property -hasPrivContent f = property desc $ withPrivData (PrivFile f) $ \privcontent -> - ensureProperty $ fileProperty' writeFileProtected desc - (\_oldcontent -> lines privcontent) f +hasPrivContent :: FilePath -> Context -> Property +hasPrivContent f context = withPrivData (PrivFile f) context $ \getcontent -> + property desc $ getcontent $ \privcontent -> + ensureProperty $ fileProperty' writeFileProtected desc + (\_oldcontent -> lines privcontent) f where desc = "privcontent " ++ f -- | Leaves the file world-readable. -hasPrivContentExposed :: FilePath -> Property -hasPrivContentExposed f = hasPrivContent f `onChange` +hasPrivContentExposed :: FilePath -> Context -> Property +hasPrivContentExposed f context = hasPrivContent f context `onChange` mode f (combineModes (ownerWriteMode:readModes)) -- | Ensures that a line is present in a file, adding it to the end if not. diff --git a/src/Propellor/Property/Gpg.hs b/src/Propellor/Property/Gpg.hs index 64ea9fea..b4698663 100644 --- a/src/Propellor/Property/Gpg.hs +++ b/src/Propellor/Property/Gpg.hs @@ -9,6 +9,8 @@ import System.PosixCompat installed :: Property installed = Apt.installed ["gnupg"] +type GpgKeyId = String + -- | Sets up a user with a gpg key from the privdata. -- -- Note that if a secret key is exported using gpg -a --export-secret-key, @@ -21,19 +23,20 @@ installed = Apt.installed ["gnupg"] -- The GpgKeyId does not have to be a numeric id; it can just as easily -- be a description of the key. keyImported :: GpgKeyId -> UserName -> Property -keyImported keyid user = flagFile' (property desc go) genflag +keyImported keyid user = flagFile' prop genflag `requires` installed where desc = user ++ " has gpg key " ++ show keyid genflag = do d <- dotDir user return $ d ".propellor-imported-keyid-" ++ keyid - go = withPrivData (GpgKey keyid) $ \key -> makeChange $ - withHandle StdinHandle createProcessSuccess - (proc "su" ["-c", "gpg --import", user]) $ \h -> do - fileEncoding h - hPutStr h key - hClose h + prop = withPrivData GpgKey (Context keyid) $ \getkey -> + property desc $ getkey $ \key -> makeChange $ + withHandle StdinHandle createProcessSuccess + (proc "su" ["-c", "gpg --import", user]) $ \h -> do + fileEncoding h + hPutStr h key + hClose h dotDir :: UserName -> IO FilePath dotDir user = do diff --git a/src/Propellor/Property/OpenId.hs b/src/Propellor/Property/OpenId.hs index 051d6425..39cb6ff0 100644 --- a/src/Propellor/Property/OpenId.hs +++ b/src/Propellor/Property/OpenId.hs @@ -25,5 +25,6 @@ providerFor users baseurl = propertyList desc $ -- the identitites directory controls access, so open up -- file mode - identfile u = File.hasPrivContentExposed $ - concat $ [ "/var/lib/simpleid/identities/", u, ".identity" ] + identfile u = File.hasPrivContentExposed + (concat [ "/var/lib/simpleid/identities/", u, ".identity" ]) + (Context baseurl) diff --git a/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs b/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs index 85584e43..4cb26a50 100644 --- a/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs +++ b/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs @@ -23,29 +23,25 @@ builddir = gitbuilderdir "build" type TimeOut = String -- eg, 5h -autobuilder :: CronTimes -> TimeOut -> Bool -> Property -autobuilder crontimes timeout rsyncupload = combineProperties "gitannexbuilder" +autobuilder :: Architecture -> CronTimes -> TimeOut -> Property +autobuilder arch crontimes timeout = combineProperties "gitannexbuilder" [ Apt.serviceInstalledRunning "cron" , Cron.niceJob "gitannexbuilder" crontimes builduser gitbuilderdir $ "git pull ; timeout " ++ timeout ++ " ./autobuild" -- The builduser account does not have a password set, -- instead use the password privdata to hold the rsync server -- password used to upload the built image. - , property "rsync password" $ do - let f = homedir "rsyncpassword" - if rsyncupload - then withPrivData (Password builduser) $ \p -> do - oldp <- liftIO $ catchDefaultIO "" $ - readFileStrict f - if p /= oldp - then makeChange $ writeFile f p - else noChange - else do - ifM (liftIO $ doesFileExist f) - ( noChange - , makeChange $ writeFile f "no password configured" - ) + , withPrivData (Password builduser) context $ \getpw -> + property "rsync password" $ getpw $ \pw -> do + oldpw <- liftIO $ catchDefaultIO "" $ + readFileStrict pwfile + if pw /= oldpw + then makeChange $ writeFile pwfile pw + else noChange ] + where + context = Context ("gitannexbuilder " ++ arch) + pwfile = homedir "rsyncpassword" tree :: Architecture -> Property tree buildarch = combineProperties "gitannexbuilder tree" @@ -101,13 +97,13 @@ standardAutoBuilderContainer dockerImage arch buildminute timeout = Docker.conta & User.accountFor builduser & tree arch & buildDepsApt - & autobuilder (show buildminute ++ " * * * *") timeout True + & autobuilder arch (show buildminute ++ " * * * *") timeout androidAutoBuilderContainer :: (System -> Docker.Image) -> Cron.CronTimes -> TimeOut -> Host androidAutoBuilderContainer dockerImage crontimes timeout = androidContainer dockerImage "android-git-annex-builder" (tree "android") builddir & Apt.unattendedUpgrades - & autobuilder crontimes timeout True + & autobuilder "android" crontimes timeout -- Android is cross-built in a Debian i386 container, using the Android NDK. androidContainer :: (System -> Docker.Image) -> Docker.ContainerName -> Property -> FilePath -> Host @@ -154,7 +150,7 @@ armelCompanionContainer dockerImage = Docker.container "armel-git-annex-builder- -- The armel builder can ssh to this companion. & Docker.expose "22" & Apt.serviceInstalledRunning "ssh" - & Ssh.authorizedKeys builduser + & Ssh.authorizedKeys builduser (Context "armel-git-annex-builder") armelAutoBuilderContainer :: (System -> Docker.Image) -> Cron.CronTimes -> TimeOut -> Host armelAutoBuilderContainer dockerImage crontimes timeout = Docker.container "armel-git-annex-builder" @@ -172,9 +168,9 @@ armelAutoBuilderContainer dockerImage crontimes timeout = Docker.container "arme -- git-annex/standalone/linux/install-haskell-packages -- which is not fully automated.) & buildDepsNoHaskellLibs - & autobuilder crontimes timeout True + & autobuilder "armel" crontimes timeout `requires` tree "armel" - & Ssh.keyImported SshRsa builduser + & Ssh.keyImported SshRsa builduser (Context "armel-git-annex-builder") & trivial writecompanionaddress where writecompanionaddress = scriptProperty diff --git a/src/Propellor/Property/SiteSpecific/JoeySites.hs b/src/Propellor/Property/SiteSpecific/JoeySites.hs index 57023cb5..bffc8a30 100644 --- a/src/Propellor/Property/SiteSpecific/JoeySites.hs +++ b/src/Propellor/Property/SiteSpecific/JoeySites.hs @@ -29,7 +29,7 @@ oldUseNetServer hosts = propertyList ("olduse.net server") [ "--repository=sftp://2318@usw-s002.rsync.net/~/olduse.net" , "--client-name=spool" ] Obnam.OnlyClient - `requires` Ssh.keyImported SshRsa "root" + `requires` Ssh.keyImported SshRsa "root" (Context "olduse.net") `requires` Ssh.knownHost hosts "usw-s002.rsync.net" "root" , check (not . isSymbolicLink <$> getSymbolicLinkStatus newsspool) $ property "olduse.net spool in place" $ makeChange $ do @@ -97,7 +97,7 @@ kgbServer = withOS desc $ \o -> case o of (Just (System (Debian Unstable) _)) -> ensureProperty $ propertyList desc [ Apt.serviceInstalledRunning "kgb-bot" - , File.hasPrivContent "/etc/kgb-bot/kgb.conf" + , File.hasPrivContent "/etc/kgb-bot/kgb.conf" anyContext `onChange` Service.restarted "kgb-bot" , "/etc/default/kgb-bot" `File.containsLine` "BOT_ENABLED=1" `describe` "kgb bot enabled" @@ -108,17 +108,19 @@ kgbServer = withOS desc $ \o -> case o of desc = "kgb.kitenet.net setup" mumbleServer :: [Host] -> Property -mumbleServer hosts = combineProperties "mumble.debian.net" +mumbleServer hosts = combineProperties hn [ Apt.serviceInstalledRunning "mumble-server" , Obnam.latestVersion , Obnam.backup "/var/lib/mumble-server" "55 5 * * *" - [ "--repository=sftp://joey@turtle.kitenet.net/~/lib/backup/mumble.debian.net.obnam" + [ "--repository=sftp://joey@turtle.kitenet.net/~/lib/backup/" ++ hn ++ ".obnam" , "--client-name=mumble" ] Obnam.OnlyClient - `requires` Ssh.keyImported SshRsa "root" + `requires` Ssh.keyImported SshRsa "root" (Context hn) `requires` Ssh.knownHost hosts "turtle.kitenet.net" "root" , trivial $ cmdProperty "chown" ["-R", "mumble-server:mumble-server", "/var/lib/mumble-server"] ] + where + hn = "mumble.debian.net" obnamLowMem :: Property obnamLowMem = combineProperties "obnam tuned for low memory use" @@ -141,16 +143,16 @@ gitServer hosts = propertyList "git.kitenet.net setup" , "--client-name=wren" ] Obnam.OnlyClient `requires` Gpg.keyImported "1B169BE1" "root" - `requires` Ssh.keyImported SshRsa "root" + `requires` Ssh.keyImported SshRsa "root" (Context "git.kitenet.net") `requires` Ssh.knownHost hosts "usw-s002.rsync.net" "root" - `requires` Ssh.authorizedKeys "family" + `requires` Ssh.authorizedKeys "family" (Context "git.kitenet.net") `requires` User.accountFor "family" , Apt.installed ["git", "rsync", "gitweb"] -- backport avoids channel flooding on branch merge , Apt.installedBackport ["kgb-client"] -- backport supports ssh event notification , Apt.installedBackport ["git-annex"] - , File.hasPrivContentExposed "/etc/kgb-bot/kgb-client.conf" + , File.hasPrivContentExposed "/etc/kgb-bot/kgb-client.conf" anyContext , toProp $ Git.daemonRunning "/srv/git" , "/etc/gitweb.conf" `File.containsLines` [ "$projectroot = '/srv/git';" @@ -202,7 +204,7 @@ annexWebSite hosts origin hn uuid remotes = propertyList (hn ++" website using g dir = "/srv/web/" ++ hn postupdatehook = dir ".git/hooks/post-update" setup = userScriptProperty "joey" setupscript - `requires` Ssh.keyImported SshRsa "joey" + `requires` Ssh.keyImported SshRsa "joey" (Context hn) `requires` Ssh.knownHost hosts "turtle.kitenet.net" "joey" setupscript = [ "cd " ++ shellEscape dir @@ -270,9 +272,9 @@ mainhttpscert True = gitAnnexDistributor :: Property gitAnnexDistributor = combineProperties "git-annex distributor, including rsync server and signer" [ Apt.installed ["rsync"] - , File.hasPrivContent "/etc/rsyncd.conf" + , File.hasPrivContent "/etc/rsyncd.conf" (Context "git-annex distributor") `onChange` Service.restarted "rsync" - , File.hasPrivContent "/etc/rsyncd.secrets" + , File.hasPrivContent "/etc/rsyncd.secrets" (Context "git-annex distributor") `onChange` Service.restarted "rsync" , "/etc/default/rsync" `File.containsLine` "RSYNC_ENABLE=true" `onChange` Service.running "rsync" @@ -315,7 +317,7 @@ ircBouncer = propertyList "IRC bouncer" [ Apt.installed ["znc"] , User.accountFor "znc" , File.dirExists (parentDir conf) - , File.hasPrivContent conf + , File.hasPrivContent conf anyContext , File.ownerGroup conf "znc" "znc" , Cron.job "znconboot" "@reboot" "znc" "~" "znc" -- ensure running if it was not already @@ -341,7 +343,7 @@ githubBackup :: Property githubBackup = propertyList "github-backup box" [ Apt.installed ["github-backup", "moreutils"] , let f = "/home/joey/.github-keys" - in File.hasPrivContent f + in File.hasPrivContent f anyContext `onChange` File.ownerGroup f "joey" "joey" ] diff --git a/src/Propellor/Property/Ssh.hs b/src/Propellor/Property/Ssh.hs index bc0e7cab..6785ede6 100644 --- a/src/Propellor/Property/Ssh.hs +++ b/src/Propellor/Property/Ssh.hs @@ -75,42 +75,43 @@ randomHostKeys = flagFile prop "/etc/ssh/.unique_host_keys" ensureProperty $ scriptProperty [ "DPKG_MAINTSCRIPT_NAME=postinst DPKG_MAINTSCRIPT_PACKAGE=openssh-server /var/lib/dpkg/info/openssh-server.postinst configure" ] --- | Sets ssh host keys from the site's PrivData. --- --- (Uses a null username for host keys.) -hostKey :: SshKeyType -> Property -hostKey keytype = combineProperties desc - [ property desc (install writeFile (SshPubKey keytype "") ".pub") - , property desc (install writeFileProtected (SshPrivKey keytype "") "") +-- | Sets ssh host keys. +hostKey :: SshKeyType -> Context -> Property +hostKey keytype context = combineProperties desc + [ installkey (SshPubKey keytype "") (install writeFile ".pub") + , installkey (SshPrivKey keytype "") (install writeFileProtected "") ] `onChange` restartSshd where desc = "known ssh host key (" ++ fromKeyType keytype ++ ")" - install writer p ext = withPrivData p $ \key -> do + installkey p a = withPrivData p context $ \getkey -> + property desc $ getkey a + install writer ext key = do let f = "/etc/ssh/ssh_host_" ++ fromKeyType keytype ++ "_key" ++ ext s <- liftIO $ readFileStrict f if s == key then noChange else makeChange $ writer f key --- | Sets up a user with a ssh private key and public key pair --- from the site's PrivData. -keyImported :: SshKeyType -> UserName -> Property -keyImported keytype user = combineProperties desc - [ property desc (install writeFile (SshPubKey keytype user) ".pub") - , property desc (install writeFileProtected (SshPrivKey keytype user) "") +-- | Sets up a user with a ssh private key and public key pair from the +-- PrivData. +keyImported :: SshKeyType -> UserName -> Context -> Property +keyImported keytype user context = combineProperties desc + [ installkey (SshPubKey keytype user) (install writeFile ".pub") + , installkey (SshPrivKey keytype user) (install writeFileProtected "") ] where desc = user ++ " has ssh key (" ++ fromKeyType keytype ++ ")" - install writer p ext = do + installkey p a = withPrivData p context $ \getkey -> + property desc $ getkey a + install writer ext key = do f <- liftIO $ keyfile ext ifM (liftIO $ doesFileExist f) ( noChange , ensureProperties - [ property desc $ - withPrivData p $ \key -> makeChange $ do - createDirectoryIfMissing True (takeDirectory f) - writer f key + [ property desc $ makeChange $ do + createDirectoryIfMissing True (takeDirectory f) + writer f key , File.ownerGroup f user user , File.ownerGroup (takeDirectory f) user user ] @@ -143,9 +144,9 @@ knownHost hosts hn user = property desc $ return FailedChange -- | Makes a user have authorized_keys from the PrivData -authorizedKeys :: UserName -> Property -authorizedKeys user = property (user ++ " has authorized_keys") $ - withPrivData (SshAuthorizedKeys user) $ \v -> do +authorizedKeys :: UserName -> Context -> Property +authorizedKeys user context = withPrivData (SshAuthorizedKeys user) context $ \get -> + property (user ++ " has authorized_keys") $ get $ \v -> do f <- liftIO $ dotFile "authorized_keys" user liftIO $ do createDirectoryIfMissing True (takeDirectory f) diff --git a/src/Propellor/Property/User.hs b/src/Propellor/Property/User.hs index eef2a57e..f9c400a8 100644 --- a/src/Propellor/Property/User.hs +++ b/src/Propellor/Property/User.hs @@ -24,17 +24,18 @@ nuked user _ = check (isJust <$> catchMaybeIO (homedir user)) $ cmdProperty "use -- | Only ensures that the user has some password set. It may or may -- not be the password from the PrivData. -hasSomePassword :: UserName -> Property -hasSomePassword user = check ((/= HasPassword) <$> getPasswordStatus user) $ - hasPassword user - -hasPassword :: UserName -> Property -hasPassword user = property (user ++ " has password") $ - withPrivData (Password user) $ \password -> makeChange $ - withHandle StdinHandle createProcessSuccess - (proc "chpasswd" []) $ \h -> do - hPutStrLn h $ user ++ ":" ++ password - hClose h +hasSomePassword :: UserName -> Context -> Property +hasSomePassword user context = check ((/= HasPassword) <$> getPasswordStatus user) $ + hasPassword user context + +hasPassword :: UserName -> Context -> Property +hasPassword user context = withPrivData (Password user) context $ \getpassword -> + property (user ++ " has password") $ + getpassword $ \password -> makeChange $ + withHandle StdinHandle createProcessSuccess + (proc "chpasswd" []) $ \h -> do + hPutStrLn h $ user ++ ":" ++ password + hClose h lockedPassword :: UserName -> Property lockedPassword user = check (not <$> isLockedPassword user) $ cmdProperty "passwd" diff --git a/src/Propellor/Types.hs b/src/Propellor/Types.hs index 59652f66..037cd962 100644 --- a/src/Propellor/Types.hs +++ b/src/Propellor/Types.hs @@ -17,7 +17,9 @@ module Propellor.Types , ActionResult(..) , CmdLine(..) , PrivDataField(..) - , GpgKeyId + , PrivData + , Context(..) + , anyContext , SshKeyType(..) , module Propellor.Types.OS , module Propellor.Types.Dns @@ -32,6 +34,7 @@ import "MonadCatchIO-transformers" Control.Monad.CatchIO import Propellor.Types.Info import Propellor.Types.OS import Propellor.Types.Dns +import Propellor.Types.PrivData -- | Everything Propellor knows about a system: Its hostname, -- properties and other info. @@ -135,30 +138,12 @@ data CmdLine = Run HostName | Spin HostName | Boot HostName - | Set HostName PrivDataField - | Dump HostName PrivDataField - | Edit HostName PrivDataField - | ListFields HostName + | Set PrivDataField Context + | Dump PrivDataField Context + | Edit PrivDataField Context + | ListFields | AddKey String | Continue CmdLine | Chain HostName | Docker HostName deriving (Read, Show, Eq) - --- | Note that removing or changing field names will break the --- serialized privdata files, so don't do that! --- It's fine to add new fields. -data PrivDataField - = DockerAuthentication - | SshPubKey SshKeyType UserName - | SshPrivKey SshKeyType UserName - | SshAuthorizedKeys UserName - | Password UserName - | PrivFile FilePath - | GpgKey GpgKeyId - deriving (Read, Show, Ord, Eq) - -type GpgKeyId = String - -data SshKeyType = SshRsa | SshDsa | SshEcdsa | SshEd25519 - deriving (Read, Show, Ord, Eq) diff --git a/src/Propellor/Types/Info.hs b/src/Propellor/Types/Info.hs index 5f034492..8856e06f 100644 --- a/src/Propellor/Types/Info.hs +++ b/src/Propellor/Types/Info.hs @@ -1,6 +1,7 @@ module Propellor.Types.Info where import Propellor.Types.OS +import Propellor.Types.PrivData import qualified Propellor.Types.Dns as Dns import qualified Data.Set as S @@ -9,6 +10,7 @@ import Data.Monoid -- | Information about a host. data Info = Info { _os :: Val System + , _privDataFields :: S.Set (PrivDataField, Context) , _sshPubKey :: Val String , _dns :: S.Set Dns.Record , _namedconf :: Dns.NamedConfMap @@ -17,9 +19,10 @@ data Info = Info deriving (Eq, Show) instance Monoid Info where - mempty = Info mempty mempty mempty mempty mempty + mempty = Info mempty mempty mempty mempty mempty mempty mappend old new = Info { _os = _os old <> _os new + , _privDataFields = _privDataFields old <> _privDataFields new , _sshPubKey = _sshPubKey old <> _sshPubKey new , _dns = _dns old <> _dns new , _namedconf = _namedconf old <> _namedconf new diff --git a/src/Propellor/Types/PrivData.hs b/src/Propellor/Types/PrivData.hs new file mode 100644 index 00000000..16d6cdb1 --- /dev/null +++ b/src/Propellor/Types/PrivData.hs @@ -0,0 +1,34 @@ +module Propellor.Types.PrivData where + +import Propellor.Types.OS + +-- | Note that removing or changing field names will break the +-- serialized privdata files, so don't do that! +-- It's fine to add new fields. +data PrivDataField + = DockerAuthentication + | SshPubKey SshKeyType UserName + | SshPrivKey SshKeyType UserName + | SshAuthorizedKeys UserName + | Password UserName + | PrivFile FilePath + | GpgKey + deriving (Read, Show, Ord, Eq) + +-- | Context in which a PrivDataField is used. +-- +-- Often this will be a domain name. For example, +-- Context "www.example.com" could be used for the SSL cert +-- for the web server serving that domain. Multiple hosts might +-- use that privdata. +newtype Context = Context String + deriving (Read, Show, Ord, Eq) + +-- | Use when a PrivDataField is not dependent on any paricular context. +anyContext :: Context +anyContext = Context "any" + +type PrivData = String + +data SshKeyType = SshRsa | SshDsa | SshEcdsa | SshEd25519 + deriving (Read, Show, Ord, Eq) -- cgit v1.2.3 From 20df4170b096a92a038ed8fb6fc6d44f71c42f0e Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Sun, 6 Jul 2014 16:44:13 -0400 Subject: beautiful table for --list-fields, with the hostnames --- src/Propellor/CmdLine.hs | 2 +- src/Propellor/PrivData.hs | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) (limited to 'src/Propellor/CmdLine.hs') diff --git a/src/Propellor/CmdLine.hs b/src/Propellor/CmdLine.hs index b6dd2bc1..c4a3afec 100644 --- a/src/Propellor/CmdLine.hs +++ b/src/Propellor/CmdLine.hs @@ -76,7 +76,7 @@ defaultMain hostlist = do go _ (Set field context) = setPrivData field context go _ (Dump field context) = dumpPrivData field context go _ (Edit field context) = editPrivData field context - go _ ListFields = listPrivDataFields + go _ ListFields = listPrivDataFields hostlist go _ (AddKey keyid) = addKey keyid go _ (Chain hn) = withhost hn $ \h -> do r <- runPropellor h $ ensureProperties $ hostProperties h diff --git a/src/Propellor/PrivData.hs b/src/Propellor/PrivData.hs index 10965fe3..27d49926 100644 --- a/src/Propellor/PrivData.hs +++ b/src/Propellor/PrivData.hs @@ -8,6 +8,7 @@ import System.IO import System.Directory import Data.Maybe import Data.Monoid +import Data.List import Control.Monad import Control.Monad.IfElse import "mtl" Control.Monad.Reader @@ -88,20 +89,23 @@ editPrivData field context = do readFile f setPrivDataTo field context v' -listPrivDataFields :: IO () -listPrivDataFields = do +listPrivDataFields :: [Host] -> IO () +listPrivDataFields hosts = do m <- decryptPrivData putStrLn "\n" - let rows = map mkrow (M.keys m) + let usedby = M.unionsWith (++) $ map mkhostmap hosts + let rows = map (mkrow usedby) (M.keys m) let table = tableWithHeader header rows putStr $ unlines $ formatTable table where - header = ["Field", "Context", "Hosts"] - mkrow (field, (Context context)) = + header = ["Field", "Context", "Used by"] + mkrow usedby k@(field, (Context context)) = [ shellEscape $ show field , shellEscape context - , "xxx" + , intercalate ", " $ sort $ fromMaybe [] $ M.lookup k usedby ] + mkhostmap host = M.fromList $ map (\k -> (k, [hostName host])) $ + S.toList $ _privDataFields $ hostInfo host setPrivDataTo :: PrivDataField -> Context -> PrivData -> IO () setPrivDataTo field context value = do -- cgit v1.2.3 From e6ce744078aed2840ea51cc49ac6149ac6f4418d Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Sun, 6 Jul 2014 17:15:27 -0400 Subject: move privdata Property to toplevel so its Info propigates Ugh, that's a nasty gotcha but I cannot see a way to fix it generally right now. --- privdata/privdata.gpg | 215 +++++++++++++++-------- src/Propellor/CmdLine.hs | 6 +- src/Propellor/PrivData.hs | 4 + src/Propellor/Property/SiteSpecific/JoeySites.hs | 24 +-- 4 files changed, 160 insertions(+), 89 deletions(-) (limited to 'src/Propellor/CmdLine.hs') diff --git a/privdata/privdata.gpg b/privdata/privdata.gpg index ec561944..7af6b51b 100644 --- a/privdata/privdata.gpg +++ b/privdata/privdata.gpg @@ -1,79 +1,144 @@ -----BEGIN PGP MESSAGE----- Version: GnuPG v1 -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 -=WDbE +hQIMA7ODiaEXBlRZARAAkA78JupS2jpSQFUxuBoKxLrt5UpLJ92iat3BDO/wcmN+ +vvXVSKd7JhpYzG0nRk7j6RQ3w43zdR/zUgbbnmq+6BKvXm6yVbIPPwi28YPChviw +NKf5Lo02V707EZjr5V9ptPTAO01RPIFEn0UfwG3tZePEBseK0dBxGBVopXD7ZNvD +uMgVlOJGazGn+dK8snFRc4YbnBXYY4pPr4tkzu2MhPCSlbbQ13tEI/w/sWJ+4YLa +qOrsMvILmm7eS8ml3mw7OVVYr+E1SWkotVw9qyXbvfqYX/B4WwlhMX9E7Q/HNqIn +bOFcZN+/TZGBNIJ0Jj/KndJtArj0BiqPD+4kXKf2yKLe1QWbgRzIvlF31pwhiRYs +8JIYi7Bq0SSXBINS5SpOpbIAuZcQpVkbc8RoDJXyXqypWUlt68Kju/l/FpDd0+BD +Z4rIeWgy0taF0JI3ye5Hxgx2Z7s4COFo8P3zO91G+VQjTA1n6tOXWVyb/EJvmaVe +/UQgmsyxoWbUzG5OpRQzV5lu0JApR6nyHPgP8g4Xh0aFqDsVffyf8FKAJPTNL0ui +oi1zkbGMhqXIxrnaez3dXwAkA+tePVxKlvC6SWoWMk25p4z8I7GvtslH3ilx5I24 +nXXwsaLaLUzT0VIyKDKp0zwn0sblEBQrJUGvM2gSNXBtKOxKY2RwIaXLNk5AwAPS +7AF3V5X7regX6tNEEz1HzXGWniIPdLVb6jGokvC1LGAUGf3imp91bRTF9Ev+FGqe +6MfGF7CEUtaxMiRGFAW7FaJyP6a2nE4PGFU1XF0aglFKeMQdlwNPhjMKrKHbUhwb +7fFDHpKzb0P9VJqgxxB6DWgr4JiTgTgfycMxsvtMYv0/0u3AOMh2/4RMLJs9GFi7 +l37tdX0QGzsyBv1uj8LI4kK5rx+z9wHrlSX8Do8lmdctzrrrwq74ItTaUU5wHEJ3 +dfnaNXA+mtFNANSoF4fTvX8vsAoBWZq5vYi5zLS4z+lFKVaO3AIHDbGgfXTWutaL +/bmAreywopziiOJUYaoPjLtWE4AADI6sa8pwtPnLW5RtpfgQeRrRD/fCcCDonfxD +otmZfvLUKZe/QMEMxfOu6eDHsfFh+PFwD4m7O4yC25eb/poc7RibCHB6vstz8YdA +NfT5FR3e713BDqAefSfvLlMpr3He6AGUFJ0wSfuvDmanAK3GxMdeLhH9CA4wS29Y +8Y0Abfb2901MCotCT8twr3NvU/mY5MFtWA2rX0qOd8r+k7ySZ5Dmrd/6qkW8HRvz +Q34c5VXdSmvLt9m9qCmdtr2ui2nVQXYXH+jwIULCIXqTAyvtUTpb9qGHR6s57h8q +zpSkiylL/7Lbd+Hy2Rdc5fqoYLgZ5MShVfT2wWYNllsljaFuYoZ+8Pv91grpbQ/5 +gaNTiXKc3OLd1ZOIFskcR8epc92hvFLgfCYBFMovLsBonaAYY6hKoVcgAbPUFbNq +hURWXeeji5/2p42ThRa0tTGZI1y9NJvzT7DdV+WFK5VFtmRQN5uAKv+MpXzsE1Yf +c7QLMPvQ+81naMi9Dod+Z90VuwtN5Cl2RP8a2NchWZZQLTMm6wCq6rNFW0s+C+Qz +84+MtNNxDKlxFcc0nzCSpI86WYm1BtcS/SEgD6+6ZqJADuhKUQZUsqF0byLqVY/Y +weLRbndg/5WVOyetZ9nMktxYx88pZAkdDascKEsV+ui+21TA2ovnWg06pu+SLoFI +orjVjUk39SmWhuy0+fZb/48jamNKf6uHLYVATeIQ/kKQi9lSstppK4K3XVsjdKoT ++wFyAVmq8/Wc4mqIQ2wLCzo19OgOfK6yOWCKDVxfxPOaQWZy8sd9Dz9pioMy5BQs +rWophGRIYMcC5d6cCi0mXyJHAi6ji25u5OqI9sEtpZhnsJWWaLxgyRxZVTr0gdWd +VVU3pyIgS6t8Fwx9ypilnXR5EdVEh9f6/YrMc4uDMzBKV+bn+fiHIj9O4Md/vZhh +xxwWW9metWF5KaJbCGfo2GPZFDIBB+JLrypbxuciAPTZlwBV4E6wnHU4ITzI0sw+ +g0GvtcrD8L3gZR7H5oeRrpIuql+ExF9gGSVBlcYQnRa7VJDtKNPuXBD03bYYrUCx +qF0xECc6kboAObRAPDXwLPylbo/Iz6ETolVkZo+KQgfAv09q2ju6gAfk5aHR5UN3 +9NJcG6u/m6GOLfbi0sTdCWvDK1mX1rHyq482brbkR5UhFz1iE8JPXlDeA1ITZCft +0KNP/bQDeAbfD8eBqNuHAgQ2LAm+UrPbX2rmEfufDeQi/41HrCiUNuYkmO3L306h +KEJYtpX3NlP14/mxD1Cxx37+qG30U3moDz6IMkMmWGdZus45OSNQlEl3zEFtzqHX +ruk0DLlHaZAbGHUM2ISlNG5+FC+4DsiqR7MSCiCYzc53mHIpLaxJOF5rAWnZQpfb +63Q9wtmpChfNliCQIqUK4WNq81m9xly0TxnppojpEVmzfb8i4nrHO90C8gHeM/+B +TwwAOhJG6wz8RtwIkfbYsnSnntCuMMjMSwo8XB1y12PlMu69fQjEazgtuXHYnqh6 +2NFIW4dIriCl8QciT6qvYa385uPQmimDRdXVbr6cbq37zPXQ5D1ds9pHpRw1K7db +PvjU63t1sESA0vmuZ9SOZxCGDFmGLDIVeGBUT62U7cCc2gND7tOBlv5LIvy904Nz +tYuwf3XPVzTFNOeFn/NXuQaAFnS2QuETwq/CKxsDHUtYDZx3te2p5iV1o/oOPCwO +twM8+RPG/G9eWCv6WOG3POeU6CLcmx+JboMb0qIjrKt55QcbexVCisu8LDB0/qct +wm0fTGKXJPzZjladN/mpm+pcWMuvyeJsD6iP7mFwPQxp2tk5hZWn11vxE50lReAv +cD3p5pzaJPzKARL/mr/h8/nFrIdIlYrx0O6SiA4UVwhn2xh5PaRhqQGWckMg2lrT +Eoa3qDQ6VxPTlSmaxdzgx89r3OOAchYrhiYgSwdD3JIi4vrPHrucvWWP6UONekd+ +2wA09v9zt0B2wuMpqXwyw3KL9VaO9usQbtNRbm/RqFdvSzn0BUT5Xn9Lp45INyGI +uk5lA+HL4z6EgZDZtvK6U//DLxFaimo074EpRtV/qxT+qXBnF+EFZcFi0SQwjFsz +66tSukQcF86VCMZE1SK07YMJn0LvO/3iRBFauL1ZpJh1bw0YNTZs/qIQVNrR/jab +fXpEWeEl3FqohagwjHxPpEMWDtYbfkhmqzjp7yNcjFPYw1DuTDfAVaW1qQEhkEhR +cZ79N26c7fQAiqCqXdrX/2lfCJcr2fHzrhSHW6oxg3R1Kkf3MLxj/k3u7Tir7e/Q +JoqvmkYxWhC2XL3ixHu0V/AoO2Jb0zmPLAJZJ/llwiUeBGRueeR+KUP5uZDhNZAP +jUg+wy5YkuM0/oS/7IWkE7ibYb+YNLpkaOp39rzhqFWRHDc83SgzzzgeeYoXOPr9 +AccXUZYEY3o7C/LKwueOrkOwADQMuaCkB9qiBT2rXOhl9I0fjrNqhvVkGWBYH6jz +vei8g6p0kkkE3SAZV8si6/SybJ709qUdGlWPzI9lrZ9AWDdDhNrI2QsQvSKxR7qc +08fkMs+rkcEKlEuC2FRYDf1C2kcPLmF5IfMZPgvFbvA3v5EI+hINq1WXoizqKhcf +B/3KaIW1vWZRZGCHlICu5o1uSDBEOpTIOV39M4miLLYuemX6kLHJX6MYUzMxMBtD +2cNrz4I1Mjgw2Eg9T9nsaPP35XxDtlc3w3HTOOGP54wF66Mwlk2U/o4xINmLE2rs +IL0/f5ZA0EyQ/TQuyIYG5peC4jHfcvgnZdnlosPAO16dx+/nD+3sDE4nSXsUSr9m +aslnGa9HT3z6hnCELfE0G0yQ1eqPWnPw71dlaFzW8sXSo680id/P2cZdNz/cBMsZ +VehAKYoPjz33GjCGa8x/wf7nKKUD/mSztyI8AceqY48fe7XTlopDU3MwCkvcVqro +bqOrqmoR4MQbunO+MbkG0raqEgEgjn6iPo6bbROxXcUM5CUAErJ6+yHVXijmVZ1T +pDKVevl5rKhO33Rwhtp0IBFeBkrwXgiHuItyVyPfcoT+gpaS4GU45kB9IdRG3BKd +nbQYsYqMRRjDCPdZ4LvVjAGlllTFbNybxWv/j8NUO5bco8HteZHIocSokqIQnktM +RlI/p7Kyw5gWefclI8h4RM0h1ll7zCFD3ovhUKoD3Y8u4LBkfjJfwEilCJvIiMMc +cHHkSHWd5KN1kJR2iiblLR2tccPWz+cqwNDmhta4MTzLfKXwo+zYaetOfFamyooO +WpzXM6EeN0hjDLov/idxXxLkxjF3sdEzqmypKYz+DpFMfqlSTd38czf+v72cc+fl +imB61EaJQvLNklgoAgeLBGxFVw7OOfUECNwq5HRo3p6cIEgn6uV3WFUEY4aVn6tL +4l2mo/jO1zBKrSS/rUL9Hb7cCHq0wWUhJa4ntNwh+YtFmIaAM+uDQIkS1EucHPF8 +Ek8sZG4vKK/tVlRSb/amGlxkvSdKYfsxG0fdm+IVf2m5FWifynqh73XFhKB0RZkD +s7acHQZlTz9h/CjQasbcNW4AyL4nf/ly7Fps5WiKqR3l5GRQXKDdpKrLHJb1jpGF +w2oVOssabYY5NGe2LjheaIOudN735HS76jgauKWO3GhQibM7kknPWwEWZUNVBJMR +TGSErpqertSAdhC5gjfOrQUCE9QYT6Clr+JMFvqWDl3wDl/QLuqPruVKzSLePk4H +YmmCEV5Ksym38QHT1gbslLsmmdgQAoF7r3zQ+BVNI0QQCyITTyFK9xVAJtXlaHMK +BmibVYO0pJAeoE1t07QZiFPsrb8wsImYgnaNbRZbcZKQNZgYoxqPH24VpUOFvrlP +uazRjIvtH5u9eykx3SeOqqTvW17xbNI584xn4haT7kP6zoM96ydEeKdDtysSZFYI +I/hooi+1g9eiem2sOtfKLh8XvlcbeJ18GMM+NOOBA/u1yJtlAdPJtomorLMy7lCz +hVKbeG1NsdEaQftG83pJtSmLiD6B6QOhwSFL8iRmByfWHEw6dbHar7Yv9ojx0HQW ++ok5Ua1jlVn6VVe0+IjnWHW+RCxXpUqlDuDm6b0Gxv3TJDu0CEQHruyJAKWJOpx1 +WWN4z/joItf3EVou+grmrq4RiitglwEUoM1jKD+Mbvb7sSVlvrNzv2ypgsRk05aC +BNQiRNWJYQiCDpatCoZ5eeVtFtEdn+gRDC+bYxQHDB5RmQ2Hz1od8b73jistUEaL +4LTR6Gh8HRNDN3mtySJAGM7LlibkH9ifAYxGNdQpLSI2O0X1q8wCSI+fUfZN5wVw +MOHDjyR2/2UVHw6K9FBnHpe3qvMh4BPmXOGdPxWF6X92tA1Y3gj9vLiXOcfRV1jf +HonyMW19AI/BB9PmmSmfdaIQE4vUtHL85URz/ySHf23BlYSzJeN1FH1Pac2+UGi4 +SY8Pu2fIaLOUanbrbdLjMm2OqMHlVWTtz+CkK/jhlzvF0/nxbzb3Mx0Cfn8EK7Ol +bcyPDXJBNrNDQOONYD4xhHXoEkqO6it7TMTkhgiB2EIp87jWcfnzifpEDh8yus9q +0ihtDGEWmV+BSpvxUXuLkL50gOP1PtUFalxakGZFgFGVTt9biWCV6DaMJZ2lJyrm +eFD49YtP0GTYugYwt9GwJUN/z92/Y97NxNvN1BcieJ4oQF82ak/sa8GTRmTGQo5D +7zzpSmoWhW76P1yMYO8x8QaQUpwp+74vc/E3PQX7oMwBV1Zwty6eU65iYYIMBUUU +5dNyBDkpCtpPgLclmLaXpu1EH3PVMXhb0PyDHnA7E8N5CfRVF8emwai9zoi1Zrtr +AIGcW2dyunimsdHO+kQz1XAzXlzoj11VuXxfIBjqXWfuFI+ywVIvWUuFJu9e4SAY +KGdI12Py0s7s7U5bh0XE7OBiVRkWlpQWPfNhuvz6j136/soKi2lEDfpfC59pFp5K +zCWHpvsUwiQQLmhi3mCysLgiRBvfBXIJnn1p2n6+sj67MliJ+7XvfS9UT/znC8x9 +mGifa0/nLOc714b4LdGCjdajifxtsnWWLNGwurbSEO9LXYE48ZA0sQx2278HBCbX +3fAgcDIDdCDAMVw55sGZrGSpLUdDvY4toJ1TEMzbfOSIcRZtDT2VAGDIVso3/W4u +uSmWRhmiF0njFp9QTLALQu7qGvwmfBbnwoM4dBUDed995ik6RljiwmqPXI6icJG8 +V0m5sRT8ryyn8BGHb3p1GKMNBotWW9WBLsu843I0ySsKA2SRwXVVctFDz5yjcK0v +l8fLns300CthgRPE4oJoW5KvMujALKQL3dc9Ci2LEYHOGb5ZWA1JYfSH6lGUNjqr +x2bumBCR/yeHElQuWqVLVt5myKRneakki7Nim7/sYRgy9TUYEBR1apQVmwrZLnuh +EGAO0hGgdo1bnG4IV2eWkoNQn17QJY26LxuyD1QsdBjkp+eQ8zfNbsWAxtwGcZUp +s/hmEwD3OTSX8rslhjJxR74Wlx6ZoOBuTzxzCXrN/XT2EHSnjmChyTT8XoWBtzGR +6Prbmy4noIlYSBAwkCItMJ+i5bhBjxEfHVbPLjf/vq6XlYXpXDH6vN4tc+DiufmL +Ia5KuxuE6C0wZmr52OshRTUAnXQeWEhwoEuqzYIglqqbZtH9Hnh6PKiX3AAhD69T +Ppl++jP+1Q2/WkoZun9nQu/l7pHLZ0poXkk9USlYJ3AD8Fu+kqYdWAi8AdLqhKg/ +yoJlW5tLuwFqLj9/U//UqSPxyM3XHvO5abJjOzYBynHV0lk1ez1R+I7k62dgIVHc +zmCSBw+YypWS3ati2rN6tYoVUENVo3ehMQ2SP77LDO1GuCy+8jq35NROpWgyFm3I +Ttn0uC5DOjftsyYFwTpr3wvPFpjvOlK4HtWZycSvvUa4WM152ctdutB0CABvjHPa +wM8e0eDQ8T0jqv8hsZrS63r/lwIKeqmBmxQVWIJz95Op+kJz7QAYJThBsVk4jptq +Gf6dy19VaXsf7t7yI2/1VfhIgZmlm6PpYj4t0mqguSbR4xTTYc5wiejQiAR9p/Ft +GGrtaFK+u+Kglt1aelh2I/Jd9jy/Dgt0x42mj2Q1mJyv9DfB+LB/iJov7syKdO1t +iMv529zMQWgpar3gCRmRCr0EGGtYIGsDhK3lWTSBa9PpmA+aHjdGdIZylc29orfk +FOs6JD/VolJbWyoTIe6mz+zndSp4Cdhn5e0tut7MzSkJi+0KttuT/kalC7+UzKBh +ZWj1aJlgO98SGrYWLoU5BF/C8gr81fY/k2xrrRMsSCbV8tfmYL5Kzj6Iys88nJyf +hlcJMm9cF94W13rB53CK8C8vhAxgxknWWi1gl5OZNu9sU0QPCUA4BtwfloTvNo0N +VvCPAsAVeLVdpItoVhy6/2zxL1TPJ+5HlTFEhktTAz4FAMMvKH7wj7NAbpLoVdDr +cpkkJMpNBEBJluac8Eiwl9Dsky4tnB+DtPvuP3LGD8NVp/vgoUnirMQFCGH7vPlN +Ak2Xz7fVGdZ/pewDnoqOcNE+zHglOmLIu2nDH3tQ41ama+nNLjo1ijDO/w0wVoE/ +3gZGJBza3LznzzpoKvfPwPI6u0IkEuOREHaP56lii8su4mMSDPrM2ggns1o1YgPp +xJ/oNAvCs4ihPGzIXet3ceoXxp/jgq+LEVEXlh6MAlHZ4k3Ltrxk0whiAuWpato5 +MknOtzoQRHR4bsU+5JhmC8F7GhZKL8e5R/bD53yUVf6c2hF5713Cevj16RW6DfW+ +Z3lZST1l6Sz0sNK7i7A2YJOvJz4qJ6Uuo7klbSZ2gO0AVqwp97aH2oHsFP1f9OVy +LMO6bG+JfeCMQ5zSvSeTJsMPnUYcGs5CVbn2EAQj3op7vdS4F4gVnQ/EdyXCZA1+ +M+ZX6TqEdMVYyih4EO7/DW7Fu74Ezry3lZ/lpijg3718LbA5qTwNQEicrEefOJHf +casaufj2+sGBq5lMuzN2YPzIT4AQQSdxc1ZMQFLcW9IZwxEapX7vcWBOx+25Sn5x +YHgG2xXmlCHf2WQRuXS8rSsVZ/Xnovy6tnM7GGs3m2kIB6i5U+8o7sxJp+z/NbEU +HwwFXcYbASwUsWUir1z/BnR6Ckt0QmXSP41KheK3e7oCz1Lh0sjwg8b+RBU3ONzR +1Q6XZuH955bBYaA2Avo2+CkXA5ZveQ03lw2tfEztZG78ncb2Bn6y+hAiYFX0RmkX +sTHzVYx6+wo/3WgVyRRmDleSaMEYysbH26WsaaNlN6KWVVA/sFA0Ysfdblc7dvaE +CVMnJ6N2iJ7ii0t/TrTrRh0nY7y4AgukkxpO448pMCnnl6KwuGaCThS+2cY9dP7A +s/0PQ88MZfKHk6Wut1AF+L4nff9pWKq1c+hSx17ZJ7MDJ8JhVfdhvNNyt8ArS4Fz +QBLdRmc/dVUZWvHAFa+pN+COV/UsO2usRd3wVdyz3yw0l1zBnNXVuh4tjFs9O1p8 +dF0GWziYFOU5mXIn8LRDkddZi/DI/BtO5Kewt8kXvEC7JOwv6vbztHheuu2JzZqn +rVOjp2mHWlUGH8Vgxe2jEcBV2dZsE3yqUEL9uJNVjHm5MY4IU5/rzhr4E41GRl56 +hlsfNuM8rKz8tpF+rfJeO/w3mLr9/SrKN1pJzjoZMSTe5rOnW9OlmqlEHUyXhWiv +1yl0Uar0K9ERIdiGUBqTXzbNB5YOI1fwbdwWS73JOYOLZLY8//vgYNZa4ynDqZOQ +uZd3/Oko+Jj8/PetCSbOCoTc4uHeVSsSV/g+sgoVKBIAcAuQazJULU1pM1XR77GA +HsRTxx1LhRYCGgMdRTWoUxPeSkgCu4QwB5c/T9o7ce+WOqO2G5WYrghDKihonFKq +3YaY1bDsqV+tJmeicQ== +=G40h -----END PGP MESSAGE----- diff --git a/src/Propellor/CmdLine.hs b/src/Propellor/CmdLine.hs index c4a3afec..5e20427e 100644 --- a/src/Propellor/CmdLine.hs +++ b/src/Propellor/CmdLine.hs @@ -186,7 +186,7 @@ spin hn = do where go cacheparams url privdata = withBothHandles createProcessSuccess (proc "ssh" $ cacheparams ++ [user, bootstrapcmd]) $ \(toh, fromh) -> do let finish = do - senddata toh privDataLocal privDataMarker privdata + senddata toh "privdata" privDataMarker privdata hClose toh -- Display remaining output. @@ -228,8 +228,8 @@ spin hn = do Just status -> return status showremote s = putStrLn s - senddata toh f marker s = void $ - actionMessage ("Sending " ++ f ++ " (" ++ show (length s) ++ " bytes) to " ++ hn) $ do + senddata toh desc marker s = void $ + actionMessage ("Sending " ++ desc ++ " (" ++ show (length s) ++ " bytes) to " ++ hn) $ do sendMarked toh marker s return True diff --git a/src/Propellor/PrivData.hs b/src/Propellor/PrivData.hs index 27d49926..0194c969 100644 --- a/src/Propellor/PrivData.hs +++ b/src/Propellor/PrivData.hs @@ -42,6 +42,10 @@ import Utility.Table -- Note that if the value is not available, the action is not run -- and instead it prints a message to help the user make the necessary -- private data available. +-- +-- The resulting Property includes Info about the PrivDataField +-- being used, which is necessary to ensure that the privdata is sent to +-- the remote host by propellor. withPrivData :: PrivDataField -> Context diff --git a/src/Propellor/Property/SiteSpecific/JoeySites.hs b/src/Propellor/Property/SiteSpecific/JoeySites.hs index bffc8a30..803b726e 100644 --- a/src/Propellor/Property/SiteSpecific/JoeySites.hs +++ b/src/Propellor/Property/SiteSpecific/JoeySites.hs @@ -93,17 +93,19 @@ oldUseNetInstalled pkg = check (not <$> Apt.isInstalled pkg) $ kgbServer :: Property -kgbServer = withOS desc $ \o -> case o of - (Just (System (Debian Unstable) _)) -> - ensureProperty $ propertyList desc - [ Apt.serviceInstalledRunning "kgb-bot" - , File.hasPrivContent "/etc/kgb-bot/kgb.conf" anyContext - `onChange` Service.restarted "kgb-bot" - , "/etc/default/kgb-bot" `File.containsLine` "BOT_ENABLED=1" - `describe` "kgb bot enabled" - `onChange` Service.running "kgb-bot" - ] - _ -> error "kgb server needs Debian unstable (for kgb-bot 1.31+)" +kgbServer = propertyList desc + [ withOS desc $ \o -> case o of + (Just (System (Debian Unstable) _)) -> + ensureProperty $ propertyList desc + [ Apt.serviceInstalledRunning "kgb-bot" + , "/etc/default/kgb-bot" `File.containsLine` "BOT_ENABLED=1" + `describe` "kgb bot enabled" + `onChange` Service.running "kgb-bot" + ] + _ -> error "kgb server needs Debian unstable (for kgb-bot 1.31+)" + , File.hasPrivContent "/etc/kgb-bot/kgb.conf" anyContext + `onChange` Service.restarted "kgb-bot" + ] where desc = "kgb.kitenet.net setup" -- cgit v1.2.3 From e2019aa7a8e18549df359bac39b325a86f448ccc Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Sun, 6 Jul 2014 17:37:10 -0400 Subject: propellor spin --- src/Propellor/CmdLine.hs | 10 ++++++---- src/Propellor/PrivData.hs | 12 ++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) (limited to 'src/Propellor/CmdLine.hs') diff --git a/src/Propellor/CmdLine.hs b/src/Propellor/CmdLine.hs index 5e20427e..448e70d2 100644 --- a/src/Propellor/CmdLine.hs +++ b/src/Propellor/CmdLine.hs @@ -84,7 +84,7 @@ defaultMain hostlist = do go _ (Docker hn) = Docker.chain hn go True cmdline@(Spin _) = buildFirst cmdline $ go False cmdline go True cmdline = updateFirst cmdline $ go False cmdline - go False (Spin hn) = withhost hn $ const $ spin hn + go False (Spin hn) = withhost hn $ spin hn go False (Run hn) = ifM ((==) 0 <$> getRealUserID) ( onlyProcess $ withhost hn mainProperties , go True (Spin hn) @@ -176,14 +176,16 @@ updateFirst cmdline next = do getCurrentGitSha1 :: String -> IO String getCurrentGitSha1 branchref = readProcess "git" ["show-ref", "--hash", branchref] -spin :: HostName -> IO () -spin hn = do +spin :: HostName -> Host -> IO () +spin hn hst = do url <- getUrl void $ gitCommit [Param "--allow-empty", Param "-a", Param "-m", Param "propellor spin"] void $ boolSystem "git" [Param "push"] cacheparams <- toCommand <$> sshCachingParams hn - go cacheparams url =<< gpgDecrypt privDataFile + go cacheparams url =<< hostprivdata where + hostprivdata = show . filterPrivData hst <$> decryptPrivData + go cacheparams url privdata = withBothHandles createProcessSuccess (proc "ssh" $ cacheparams ++ [user, bootstrapcmd]) $ \(toh, fromh) -> do let finish = do senddata toh "privdata" privDataMarker privdata diff --git a/src/Propellor/PrivData.hs b/src/Propellor/PrivData.hs index 0194c969..e9e7e47f 100644 --- a/src/Propellor/PrivData.hs +++ b/src/Propellor/PrivData.hs @@ -29,6 +29,8 @@ import Utility.FileMode import Utility.Env import Utility.Table +type PrivMap = M.Map (PrivDataField, Context) PrivData + -- | Allows a Property to access the value of a specific PrivDataField, -- for use in a specific Context. -- @@ -68,7 +70,13 @@ getLocalPrivData field context = where localcache = catchDefaultIO Nothing $ readish <$> readFile privDataLocal -getPrivData :: PrivDataField -> Context -> (M.Map (PrivDataField, Context) PrivData) -> Maybe PrivData +{- Get only the set of PrivData that the Host's Info says it uses. -} +filterPrivData :: Host -> PrivMap -> PrivMap +filterPrivData host = M.filterWithKey (\k _v -> S.member k used) + where + used = _privDataFields $ hostInfo host + +getPrivData :: PrivDataField -> Context -> PrivMap -> Maybe PrivData getPrivData field context = M.lookup (field, context) setPrivData :: PrivDataField -> Context -> IO () @@ -124,7 +132,7 @@ setPrivDataTo field context value = do | end s == "\n" = chomp (beginning s) | otherwise = s -decryptPrivData :: IO (M.Map (PrivDataField, Context) PrivData) +decryptPrivData :: IO PrivMap decryptPrivData = fromMaybe M.empty . readish <$> gpgDecrypt privDataFile makePrivDataDir :: IO () -- cgit v1.2.3