packages feed

propellor 0.7.0 → 0.8.0

raw patch · 22 files changed

+457/−245 lines, 22 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Propellor.Types: instance Eq PrivDataField
- Propellor.Types: instance Eq SshKeyType
- Propellor.Types: instance Ord PrivDataField
- Propellor.Types: instance Ord SshKeyType
- Propellor.Types: instance Read PrivDataField
- Propellor.Types: instance Read SshKeyType
- Propellor.Types: instance Show PrivDataField
- Propellor.Types: instance Show SshKeyType
- Propellor.Types: type GpgKeyId = String
+ Propellor.PrivData: addPrivDataField :: (PrivDataField, Context) -> Property
+ Propellor.PrivData: editPrivData :: PrivDataField -> Context -> IO ()
+ Propellor.PrivData: filterPrivData :: Host -> PrivMap -> PrivMap
+ Propellor.PrivData: getLocalPrivData :: PrivDataField -> Context -> IO (Maybe PrivData)
+ Propellor.PrivData: listPrivDataFields :: [Host] -> IO ()
+ Propellor.PrivData: setPrivDataTo :: PrivDataField -> Context -> PrivData -> IO ()
+ Propellor.PrivData: type PrivMap = Map (PrivDataField, Context) PrivData
+ Propellor.Property.Gpg: type GpgKeyId = String
+ Propellor.Types: Context :: String -> Context
+ Propellor.Types: Edit :: PrivDataField -> Context -> CmdLine
+ Propellor.Types: ListFields :: CmdLine
+ Propellor.Types: anyContext :: Context
+ Propellor.Types: newtype Context
+ Propellor.Types: type PrivData = String
+ Propellor.Types.PrivData: Context :: String -> Context
+ Propellor.Types.PrivData: DockerAuthentication :: PrivDataField
+ Propellor.Types.PrivData: GpgKey :: PrivDataField
+ Propellor.Types.PrivData: Password :: UserName -> PrivDataField
+ Propellor.Types.PrivData: PrivFile :: FilePath -> PrivDataField
+ Propellor.Types.PrivData: SshAuthorizedKeys :: UserName -> PrivDataField
+ Propellor.Types.PrivData: SshDsa :: SshKeyType
+ Propellor.Types.PrivData: SshEcdsa :: SshKeyType
+ Propellor.Types.PrivData: SshEd25519 :: SshKeyType
+ Propellor.Types.PrivData: SshPrivKey :: SshKeyType -> UserName -> PrivDataField
+ Propellor.Types.PrivData: SshPubKey :: SshKeyType -> UserName -> PrivDataField
+ Propellor.Types.PrivData: SshRsa :: SshKeyType
+ Propellor.Types.PrivData: anyContext :: Context
+ Propellor.Types.PrivData: data PrivDataField
+ Propellor.Types.PrivData: data SshKeyType
+ Propellor.Types.PrivData: instance Eq Context
+ Propellor.Types.PrivData: instance Eq PrivDataField
+ Propellor.Types.PrivData: instance Eq SshKeyType
+ Propellor.Types.PrivData: instance Ord Context
+ Propellor.Types.PrivData: instance Ord PrivDataField
+ Propellor.Types.PrivData: instance Ord SshKeyType
+ Propellor.Types.PrivData: instance Read Context
+ Propellor.Types.PrivData: instance Read PrivDataField
+ Propellor.Types.PrivData: instance Read SshKeyType
+ Propellor.Types.PrivData: instance Show Context
+ Propellor.Types.PrivData: instance Show PrivDataField
+ Propellor.Types.PrivData: instance Show SshKeyType
+ Propellor.Types.PrivData: newtype Context
+ Propellor.Types.PrivData: type PrivData = String
- Propellor.PrivData: decryptPrivData :: HostName -> IO (Map PrivDataField String)
+ Propellor.PrivData: decryptPrivData :: IO PrivMap
- Propellor.PrivData: dumpPrivData :: HostName -> PrivDataField -> IO ()
+ Propellor.PrivData: dumpPrivData :: PrivDataField -> Context -> IO ()
- Propellor.PrivData: getPrivData :: PrivDataField -> IO (Maybe String)
+ Propellor.PrivData: getPrivData :: PrivDataField -> Context -> PrivMap -> Maybe PrivData
- Propellor.PrivData: privDataFile :: HostName -> FilePath
+ Propellor.PrivData: privDataFile :: FilePath
- Propellor.PrivData: setPrivData :: HostName -> PrivDataField -> IO ()
+ Propellor.PrivData: setPrivData :: PrivDataField -> Context -> IO ()
- Propellor.PrivData: withPrivData :: PrivDataField -> (String -> Propellor Result) -> Propellor Result
+ Propellor.PrivData: withPrivData :: PrivDataField -> Context -> (((PrivData -> Propellor Result) -> Propellor Result) -> Property) -> Property
- Propellor.Property.File: hasPrivContent :: FilePath -> Property
+ Propellor.Property.File: hasPrivContent :: FilePath -> Context -> Property
- Propellor.Property.File: hasPrivContentExposed :: FilePath -> Property
+ Propellor.Property.File: hasPrivContentExposed :: FilePath -> Context -> Property
- Propellor.Property.SiteSpecific.GitAnnexBuilder: autobuilder :: CronTimes -> TimeOut -> Bool -> Property
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: autobuilder :: Architecture -> CronTimes -> TimeOut -> Property
- Propellor.Property.Ssh: authorizedKeys :: UserName -> Property
+ Propellor.Property.Ssh: authorizedKeys :: UserName -> Context -> Property
- Propellor.Property.Ssh: hostKey :: SshKeyType -> Property
+ Propellor.Property.Ssh: hostKey :: SshKeyType -> Context -> Property
- Propellor.Property.Ssh: keyImported :: SshKeyType -> UserName -> Property
+ Propellor.Property.Ssh: keyImported :: SshKeyType -> UserName -> Context -> Property
- Propellor.Property.User: hasPassword :: UserName -> Property
+ Propellor.Property.User: hasPassword :: UserName -> Context -> Property
- Propellor.Property.User: hasSomePassword :: UserName -> Property
+ Propellor.Property.User: hasSomePassword :: UserName -> Context -> Property
- Propellor.Types: Dump :: HostName -> PrivDataField -> CmdLine
+ Propellor.Types: Dump :: PrivDataField -> Context -> CmdLine
- Propellor.Types: GpgKey :: GpgKeyId -> PrivDataField
+ Propellor.Types: GpgKey :: PrivDataField
- Propellor.Types: Set :: HostName -> PrivDataField -> CmdLine
+ Propellor.Types: Set :: PrivDataField -> Context -> CmdLine

Files

CHANGELOG view
@@ -1,3 +1,29 @@+propellor (0.8.0) unstable; 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. You may find+    it useful to run the old version of propellor to extract data from the old+    privdata files during this migration.++    Several properties that use privdata now require a context to be+    specified. If in doubt, you can use anyContext, or+    Context "hostname.example.com"++  * Add --edit to edit a privdata value in $EDITOR.+  * Add --list-fields to list all currently set privdata fields, along with+    the hosts that use them.+  * 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,+    to avoid eg, apache complaining "Could not reliably determine the+    server's fully qualified domain name".++ -- Joey Hess <joeyh@debian.org>  Sun, 06 Jul 2014 18:28:08 -0400+ propellor (0.7.0) unstable; urgency=medium    * combineProperties no longer stops when a property fails; now it continues
Makefile view
@@ -1,5 +1,7 @@ CABAL?=cabal +DEBDEPS=gnupg ghc cabal-install libghc-missingh-dev libghc-ansi-terminal-dev libghc-ifelse-dev libghc-unix-compat-dev libghc-hslogger-dev libghc-network-dev libghc-quickcheck2-dev libghc-mtl-dev libghc-monadcatchio-transformers-dev+ run: deps build 	./propellor @@ -10,7 +12,7 @@ 	ln -sf dist/build/propellor-config/propellor-config propellor  deps:-	@if [ $$(whoami) = root ]; then apt-get --no-upgrade --no-install-recommends -y install gnupg ghc cabal-install libghc-missingh-dev libghc-ansi-terminal-dev libghc-ifelse-dev libghc-unix-compat-dev libghc-hslogger-dev libghc-network-dev libghc-quickcheck2-dev libghc-mtl-dev libghc-monadcatchio-transformers-dev; fi || true+	@if [ $$(whoami) = root ]; then apt-get --no-upgrade --no-install-recommends -y install $(DEBDEPS) || (apt-get update && apt-get --no-upgrade --no-install-recommends -y install $(DEBDEPS)); fi || true 	@if [ $$(whoami) = root ]; then apt-get --no-upgrade --no-install-recommends -y install libghc-async-dev || (cabal update; cabal install async); fi || true  dist/setup-config: propellor.cabal
config-joey.hs view
@@ -42,49 +42,18 @@ 		& Docker.configured 		& Docker.docked hosts "android-git-annex" -	-- Nothing super-important lives here and mostly it's docker containers.+	-- Unreliable server. 	, standardSystem "clam.kitenet.net" Unstable "amd64"-		& ipv4 "162.248.143.249"-		& ipv6 "2002:5044:5531::1"+		& ipv4 "162.248.9.29"  		& CloudAtCost.decruft 		& Apt.unattendedUpgrades 		& Network.ipv6to4 		& Tor.isBridge 		& Postfix.satellite-		& Docker.configured -		& Docker.docked hosts "oldusenet-shellbox"-		& Docker.docked hosts "openid-provider"-		 	`requires` Apt.serviceInstalledRunning "ntp"-		& Docker.docked hosts "ancient-kitenet"--		-- I'd rather this were on diatom, but it needs unstable.-		& alias "kgb.kitenet.net"-		& JoeySites.kgbServer--		& alias "mumble.kitenet.net"-		& JoeySites.mumbleServer hosts-		-		& alias "ns9.kitenet.net"-		& myDnsSecondary-		-		& alias "znc.kitenet.net"-		& JoeySites.ircBouncer--		-- For https port 443, shellinabox with ssh login to-		-- kitenet.net-		& alias "shell.kitenet.net"-		& JoeySites.kiteShellBox--		-- Nothing is using http port 80 on clam, so listen on-		-- that port for ssh, for traveling on bad networks that-		-- block 22.-		& "/etc/ssh/sshd_config" `File.containsLine` "Port 80"-			`onChange` Service.restarted "ssh"-+		& Docker.configured 		& Docker.garbageCollected `period` Daily-		& Apt.installed ["git-annex", "mtr", "screen"] 	 	-- Orca is the main git-annex build box. 	, standardSystem "orca.kitenet.net" Unstable "amd64"@@ -103,14 +72,15 @@ 		& 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@@ -120,9 +90,9 @@ 		& 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"@@ -164,16 +134,17 @@ 		& 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/@@ -191,6 +162,7 @@ 			[ "configfile (xen/xvda1)/boot/grub/grub.cfg" ] 		& Apt.installed ["grub-xen"] 		& flagFile (scriptProperty ["update-grub; grub-mkimage --prefix '(xen/xvda1)/boot/grub' -c /boot/load.cf -O x86_64-xen /usr/lib/grub/x86_64-xen/*.mod > /boot/xen-shim"]) "/boot/xen-shim"+			`describe` "/boot-xen-shim"  		& alias "eubackup.kitenet.net" 		& Apt.installed ["obnam", "sshfs", "rsync"]@@ -201,9 +173,39 @@ 		& alias "podcatcher.kitenet.net" 		& Apt.installed ["git-annex"] 		+		& alias "znc.kitenet.net"+		& JoeySites.ircBouncer++		-- I'd rather this were on diatom, but it needs unstable.+		& alias "kgb.kitenet.net"+		& JoeySites.kgbServer++		& alias "mumble.kitenet.net"+		& JoeySites.mumbleServer hosts+		+		& alias "ns3.kitenet.net"+		& myDnsSecondary+		 		& Docker.configured++		& Docker.docked hosts "oldusenet-shellbox"+		& Docker.docked hosts "openid-provider"+		 	`requires` Apt.serviceInstalledRunning "ntp"+		& Docker.docked hosts "ancient-kitenet"+ 		& Docker.garbageCollected `period` (Weekly (Just 1))+		+		-- For https port 443, shellinabox with ssh login to+		-- kitenet.net+		& alias "shell.kitenet.net"+		& JoeySites.kiteShellBox+		-- Nothing is using http port 80, so listen on+		-- that port for ssh, for traveling on bad networks that+		-- block 22.+		& "/etc/ssh/sshd_config" `File.containsLine` "Port 80"+			`onChange` Service.restarted "ssh" + 	    --'                        __|II|      ,. 	  ----                      __|II|II|__   (  \_,/\ 	 ------'\o/-'-.-'-.-'-.- __|II|II|II|II|___/   __/ -'-.-'-.-'-.-'-.-'-@@ -264,13 +266,13 @@ 	& 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"]@@ -309,14 +311,14 @@ branchableSecondary = Dns.secondaryFor ["branchable.com"] hosts "branchable.com"  -- Currently using diatom (ns2) as primary with secondaries--- clam (ns9) and gandi.+-- elephant (ns3) and gandi. -- kite handles all mail. myDnsPrimary :: Domain -> [(BindDomain, Record)] -> RevertableProperty myDnsPrimary domain extras = Dns.primary hosts domain 	(Dns.mkSOA "ns2.kitenet.net" 100) $ 	[ (RootDomain, NS $ AbsDomain "ns2.kitenet.net")+	, (RootDomain, NS $ AbsDomain "ns3.kitenet.net") 	, (RootDomain, NS $ AbsDomain "ns6.gandi.net")-	, (RootDomain, NS $ AbsDomain "ns9.kitenet.net") 	, (RootDomain, MX 0 $ AbsDomain "kitenet.net") 	, (RootDomain, TXT "v=spf1 a ?all") 	] ++ extras@@ -377,7 +379,7 @@ 		 -   some static websites 		 - bitlbee 		 - prosody-		 -   (used by anna and daddy's git-annex)+		 -   (used by daddy's git-annex) 		 - named 		 -   (branchable is still pushing to here 		 -    (thinking it's ns2.branchable.com), but it's no
config-simple.hs view
@@ -29,7 +29,7 @@ 		& Apt.unattendedUpgrades 		& Apt.installed ["etckeeper"] 		& Apt.installed ["ssh"]-		& User.hasSomePassword "root"+		& User.hasSomePassword "root" (Context "mybox.example.com") 		& Network.ipv6to4 		& File.dirExists "/var/www" 		& Docker.docked hosts "webserver"
debian/changelog view
@@ -1,3 +1,29 @@+propellor (0.8.0) unstable; 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. You may find+    it useful to run the old version of propellor to extract data from the old+    privdata files during this migration.++    Several properties that use privdata now require a context to be+    specified. If in doubt, you can use anyContext, or+    Context "hostname.example.com"++  * Add --edit to edit a privdata value in $EDITOR.+  * Add --list-fields to list all currently set privdata fields, along with+    the hosts that use them.+  * 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,+    to avoid eg, apache complaining "Could not reliably determine the+    server's fully qualified domain name".++ -- Joey Hess <joeyh@debian.org>  Sun, 06 Jul 2014 18:28:08 -0400+ propellor (0.7.0) unstable; urgency=medium    * combineProperties no longer stops when a property fails; now it continues
propellor.cabal view
@@ -1,5 +1,5 @@ Name: propellor-Version: 0.7.0+Version: 0.8.0 Cabal-Version: >= 1.6 License: BSD3 Maintainer: Joey Hess <joey@kitenet.net>@@ -105,6 +105,7 @@     Propellor.Types     Propellor.Types.OS     Propellor.Types.Dns+    Propellor.Types.PrivData   Other-Modules:     Propellor.Types.Info     Propellor.CmdLine@@ -126,6 +127,7 @@     Utility.Process     Utility.SafeCommand     Utility.Scheduled+    Utility.Table     Utility.ThreadScheduler     Utility.Tmp     Utility.UserInfo
src/Propellor/CmdLine.hs view
@@ -26,9 +26,11 @@ 		, "  propellor" 		, "  propellor hostname" 		, "  propellor --spin hostname"-		, "  propellor --set hostname field"-		, "  propellor --dump hostname field" 		, "  propellor --add-key keyid"+		, "  propellor --set field context"+		, "  propellor --dump field context"+		, "  propellor --edit field context"+		, "  propellor --list-fields" 		] 	exitFailure @@ -39,8 +41,10 @@   	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 ("--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"@@ -56,8 +60,8 @@ 			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 ()@@ -69,8 +73,10 @@ 	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 _ (Set field context) = setPrivData field context+	go _ (Dump field context) = dumpPrivData field context+	go _ (Edit field context) = editPrivData field context+	go _ ListFields = listPrivDataFields hostlist 	go _ (AddKey keyid) = addKey keyid 	go _ (Chain hn) = withhost hn $ \h -> do 		r <- runPropellor h $ ensureProperties $ hostProperties h@@ -78,7 +84,7 @@ 	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)@@ -170,17 +176,19 @@ 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 hn)+	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 (privDataFile hn) privDataMarker privdata+			senddata toh "privdata" privDataMarker privdata 			hClose toh 			 			-- Display remaining output.@@ -222,8 +230,8 @@ 			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 
src/Propellor/PrivData.hs view
@@ -2,18 +2,23 @@  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.Monoid import Data.List 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 Propellor.Info import Utility.Monad import Utility.PartialPrelude import Utility.Exception@@ -21,53 +26,119 @@ import Utility.Tmp import Utility.SafeCommand import Utility.Misc+import Utility.FileMode+import Utility.Env+import Utility.Table --- | 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)+type PrivMap = M.Map (PrivDataField, Context) PrivData++-- | 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.+--+-- 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+	-> (((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 = 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+	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) } } -getPrivData :: PrivDataField -> IO (Maybe String)-getPrivData field = do-	m <- catchDefaultIO Nothing $ readish <$> readFile privDataLocal-	return $ maybe Nothing (M.lookup field) m+addPrivDataField :: (PrivDataField, Context) -> Property+addPrivDataField v = pureInfoProperty (show v) $+	mempty { _privDataFields = S.singleton v } -setPrivData :: HostName -> PrivDataField -> IO ()-setPrivData host field = do+{- 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+	localcache = catchDefaultIO Nothing $ readish <$> readFile privDataLocal++{- 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 ()+setPrivData field context = do 	putStrLn "Enter private data on stdin; ctrl-D when done:"-	value <- chomp <$> hGetContentsStrict stdin+	setPrivDataTo field context =<< hGetContentsStrict stdin++dumpPrivData :: PrivDataField -> Context -> IO ()+dumpPrivData field context =+	maybe (error "Requested privdata is not set.") putStrLn+		=<< (getPrivData field context <$> decryptPrivData)++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+		editor <- getEnvDefault "EDITOR" "vi"+		unlessM (boolSystem editor [File f]) $+			error "Editor failed; aborting."+		readFile f+	setPrivDataTo field context v'++listPrivDataFields :: [Host] -> IO ()+listPrivDataFields hosts = do+	m <- decryptPrivData+	putStrLn "\n"+	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", "Used by"]+	mkrow usedby k@(field, (Context context)) =+		[ shellEscape $ show field+		, shellEscape context+		, 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 	makePrivDataDir-	let f = privDataFile host-	m <- decryptPrivData host-	let m' = M.insert field 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 -dumpPrivData :: HostName -> PrivDataField -> IO ()-dumpPrivData host field = go . M.lookup field =<< decryptPrivData host-  where-	go Nothing = error "Requested privdata is not set."-	go (Just s) = putStrLn s--decryptPrivData :: HostName -> IO (M.Map PrivDataField String)-decryptPrivData host = fromMaybe M.empty . readish-	<$> gpgDecrypt (privDataFile host)+decryptPrivData :: IO PrivMap+decryptPrivData = fromMaybe M.empty . readish <$> gpgDecrypt privDataFile  makePrivDataDir :: IO () makePrivDataDir = createDirectoryIfMissing False privDataDir@@ -75,8 +146,8 @@ privDataDir :: FilePath privDataDir = "privdata" -privDataFile :: HostName -> FilePath-privDataFile host = privDataDir </> host ++ ".gpg"+privDataFile :: FilePath+privDataFile = privDataDir </> "privdata.gpg"  privDataLocal :: FilePath privDataLocal = privDataDir </> "local"
src/Propellor/Property/Docker.hs view
@@ -55,10 +55,11 @@ -- | 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,@@ -86,8 +87,8 @@ -- The container has its own Properties which are handled by running -- propellor inside the container. ----- Additionally, the container can have DNS info, such as a CNAME.--- These become info of the host(s) it's docked in.+-- When the container's Properties include DNS info, such as a CNAME,+-- that is propigated to the Info of the host(s) it's docked in. -- -- Reverting this property ensures that the container is stopped and -- removed.@@ -96,7 +97,7 @@ 	-> ContainerName 	-> RevertableProperty docked hosts cn = RevertableProperty-	((maybe id exposeDnsInfos mhost) (go "docked" setup))+	((maybe id propigateInfo mhost) (go "docked" setup)) 	(go "undocked" teardown)   where 	go desc a = property (desc ++ " " ++ cn) $ do@@ -123,9 +124,12 @@ 					] 			] -exposeDnsInfos :: Host -> Property -> Property-exposeDnsInfos (Host _ _ containerinfo) p = combineProperties (propertyDesc p) $-	p : map addDNS (S.toList $ _dns containerinfo)+propigateInfo :: Host -> Property -> Property+propigateInfo (Host _ _ containerinfo) p =+	combineProperties (propertyDesc p) $ p : dnsprops ++ privprops+  where+	dnsprops = map addDNS (S.toList $ _dns containerinfo)+	privprops = map addPrivDataField (S.toList $ _privDataFields containerinfo)  findContainer 	:: Maybe Host@@ -390,7 +394,7 @@ -- being run. So, retry connections to the client for up to -- 1 minute. provisionContainer :: ContainerId -> Property-provisionContainer cid = containerDesc cid $ property "provision" $ liftIO $ do+provisionContainer cid = containerDesc cid $ property "provisioned" $ liftIO $ do 	let shim = Shim.file (localdir </> "propellor") (localdir </> shimdir cid) 	r <- simpleShClientRetry 60 (namedPipe cid) shim params (go Nothing) 	when (r /= FailedChange) $
src/Propellor/Property/File.hs view
@@ -17,16 +17,17 @@ -- -- 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.
src/Propellor/Property/Gpg.hs view
@@ -9,6 +9,8 @@ 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 @@ -- 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
src/Propellor/Property/Hostname.hs view
@@ -3,11 +3,16 @@ import Propellor import qualified Propellor.Property.File as File --- | Ensures that the hostname is set to the HostInfo value.+-- | Ensures that the hostname is set using best practices.+-- -- Configures /etc/hostname and the current hostname. ----- A FQDN also configures /etc/hosts, with an entry for 127.0.1.1, which is--- standard at least on Debian to set the FDQN (127.0.0.1 is localhost).+-- /etc/hosts is also configured, with an entry for 127.0.1.1, which is+-- standard at least on Debian to set the FDQN.+--+-- Also, the /etc/hosts 127.0.0.1 line is set to localhost. Putting any+-- other hostnames there is not best practices and can lead to annoying+-- messages from eg, apache. sane :: Property sane = property ("sane hostname") (ensureProperty . setTo =<< asks hostName) @@ -21,13 +26,14 @@ 		[ Just $ "/etc/hostname" `File.hasContent` [basehost] 		, if null domain 			then Nothing -			else Just $ File.fileProperty desc-				addhostline "/etc/hosts"+			else Just $ trivial $ hostsline "127.0.1.1" [hn, basehost]+		, Just $ trivial $ hostsline "127.0.0.1" ["localhost"] 		, Just $ trivial $ cmdProperty "hostname" [basehost] 		] 	-	hostip = "127.0.1.1"-	hostline = hostip ++ "\t" ++ hn ++ " " ++ basehost--	addhostline ls = hostline : filter (not . hashostip) ls-	hashostip l = headMaybe (words l) == Just hostip+	hostsline ip names = File.fileProperty desc+		(addhostsline ip names)+		"/etc/hosts"+	addhostsline ip names ls =+		(ip ++ "\t" ++ (unwords names)) : filter (not . hasip ip) ls+	hasip ip l = headMaybe (words l) == Just ip
src/Propellor/Property/OpenId.hs view
@@ -25,5 +25,6 @@ 	 	-- 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)
src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs view
@@ -23,29 +23,25 @@  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 @@ 	& 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 @@ 	-- 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 @@ 	-- 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
src/Propellor/Property/SiteSpecific/JoeySites.hs view
@@ -16,6 +16,7 @@ import qualified Propellor.Property.Apache as Apache import Utility.SafeCommand import Utility.FileMode+import Utility.Path  import Data.List import System.Posix.Files@@ -28,7 +29,7 @@ 		[ "--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@@ -84,37 +85,44 @@ 			, "dpkg -i ../" ++ pkg ++ "_*.deb || true" 			, "apt-get -fy install" -- dependencies 			, "rm -rf /root/tmp/oldusenet"+			-- screen fails unless the directory has this mode.+			-- not sure what's going on.+			, "chmod 777 /var/run/screen" 			] `describe` "olduse.net built" 		]   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"-				`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"  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"@@ -137,16 +145,16 @@ 		, "--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';"@@ -198,7 +206,7 @@ 	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@@ -266,9 +274,9 @@ 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"@@ -310,10 +318,13 @@ ircBouncer = propertyList "IRC bouncer" 	[ Apt.installed ["znc"] 	, User.accountFor "znc"-	, File.hasPrivContent conf+	, File.dirExists (parentDir conf)+	, File.hasPrivContent conf anyContext 	, File.ownerGroup conf "znc" "znc" 	, Cron.job "znconboot" "@reboot" "znc" "~" "znc"-	, Cron.job "zncrunning" "@hourly" "znc" "~" "znc || true"+	-- ensure running if it was not already+	, trivial $ userScriptProperty "znc" ["znc || true"]+		`describe` "znc running" 	]   where 	conf = "/home/znc/.znc/configs/znc.conf"@@ -335,7 +346,7 @@ 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" 	] 
src/Propellor/Property/Ssh.hs view
@@ -72,46 +72,46 @@ 			[ Param "-c" 			, Param "rm -f /etc/ssh/ssh_host_*" 			]-		ensureProperty $-			cmdProperty "/var/lib/dpkg/info/openssh-server.postinst"-				["configure"]+		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 				]@@ -144,9 +144,9 @@ 		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)
src/Propellor/Property/User.hs view
@@ -24,17 +24,18 @@  -- | 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+hasSomePassword :: UserName -> Context -> Property+hasSomePassword user context = check ((/= HasPassword) <$> getPasswordStatus user) $+	hasPassword user context -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+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"
src/Propellor/Types.hs view
@@ -17,7 +17,9 @@ 	, ActionResult(..) 	, CmdLine(..) 	, PrivDataField(..)-	, GpgKeyId+	, PrivData+	, Context(..)+	, anyContext 	, SshKeyType(..) 	, module Propellor.Types.OS 	, module Propellor.Types.Dns@@ -32,6 +34,7 @@ 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,28 +138,12 @@ 	= Run HostName 	| Spin HostName 	| Boot HostName-	| Set HostName PrivDataField-	| Dump HostName PrivDataField+	| 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)
src/Propellor/Types/Info.hs view
@@ -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 @@ -- | 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 @@ 	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
+ src/Propellor/Types/PrivData.hs view
@@ -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)
+ src/Utility/Table.hs view
@@ -0,0 +1,28 @@+{- text based table generation+ -+ - Copyright 2014 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++module Utility.Table where++type Table = [[String]]++-- | A table with a header that is set off with lines under each+-- header item.+tableWithHeader :: [String] -> [[String]] -> Table+tableWithHeader header rows = header : map linesep header : rows+  where+	linesep = map (const '-')++-- | Formats a table to lines, automatically padding rows to the same size.+formatTable :: Table -> [String]+formatTable table = map (\r -> unwords (map pad (zip r rowsizes))) table+  where+	pad (cell, size) = cell ++ take (size - length cell) padding+	padding = repeat ' '+	rowsizes = sumrows (map (map length) table)+	sumrows [] = repeat 0+	sumrows [r] = r+	sumrows (r1:r2:rs) = sumrows $ map (uncurry max) (zip r1 r2) : rs
src/config.hs view
@@ -29,7 +29,7 @@ 		& Apt.unattendedUpgrades 		& Apt.installed ["etckeeper"] 		& Apt.installed ["ssh"]-		& User.hasSomePassword "root"+		& User.hasSomePassword "root" (Context "mybox.example.com") 		& Network.ipv6to4 		& File.dirExists "/var/www" 		& Docker.docked hosts "webserver"