packages feed

propellor 2.8.1 → 2.9.0

raw patch · 87 files changed

+1240/−584 lines, 87 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Propellor: localdir :: FilePath
- Propellor.PropAccum: data PropList
- Propellor.PropAccum: instance Propellor.PropAccum.PropAccum Propellor.PropAccum.PropList
- Propellor.PropAccum: propigateContainer :: (PropAccum container) => String -> container -> Property HasInfo -> Property HasInfo
- Propellor.PropAccum: props :: PropList
- Propellor.Property.Chroot: UsingDeboostrap :: DebootstrapConfig -> BuilderConf
- Propellor.Property.Chroot: data BuilderConf
- Propellor.Property.Chroot: instance GHC.Show.Show Propellor.Property.Chroot.BuilderConf
- Propellor.Property.Chroot: propigateChrootInfo :: (IsProp (Property i)) => Chroot -> Property i -> Property HasInfo
- Propellor.Property.List: instance Propellor.Property.List.PropertyList Propellor.PropAccum.PropList
- Propellor.Property.Ssh: getPubKey :: Propellor (Map SshKeyType PubKeyText)
- Propellor.Property.Ssh: instance GHC.Base.Monoid Propellor.Property.Ssh.SshPubKeyInfo
- Propellor.Property.Ssh: instance GHC.Classes.Eq Propellor.Property.Ssh.SshPubKeyInfo
- Propellor.Property.Ssh: instance GHC.Classes.Ord Propellor.Property.Ssh.SshPubKeyInfo
- Propellor.Property.Ssh: instance Propellor.Types.Info.IsInfo Propellor.Property.Ssh.SshPubKeyInfo
- Propellor.Property.Ssh: keyImported :: IsContext c => SshKeyType -> User -> c -> Property HasInfo
- Propellor.Property.Ssh: keyImported' :: IsContext c => Maybe FilePath -> SshKeyType -> User -> c -> Property HasInfo
- Propellor.Property.Ssh: pubKey :: SshKeyType -> PubKeyText -> Property HasInfo
- Propellor.Types: before :: (IsProp x, Combines y x, IsProp (CombinedType y x)) => x -> y -> CombinedType y x
- Propellor.Types: describe :: IsProp p => p -> Desc -> p
- Propellor.Types: requires :: Combines x y => x -> y -> CombinedType x y
- Propellor.Types.Info: instance Data.Typeable.Internal.Typeable v => Propellor.Types.Info.IsInfo (Propellor.Types.Info.InfoVal v)
- Propellor.Types.Info: propigatableInfo :: Info -> Info
- Propellor.Types.Info: propigateInfo :: IsInfo v => v -> Bool
+ Propellor: (!) :: PropAccum h => h -> RevertableProperty -> h
+ Propellor: (&) :: (PropAccum h, IsProp p) => h -> p -> h
+ Propellor: (<!>) :: Property i1 -> Property i2 -> RevertableProperty
+ Propellor: Host :: HostName -> [Property HasInfo] -> Info -> Host
+ Propellor: RevertableProperty :: (Property HasInfo) -> (Property HasInfo) -> RevertableProperty
+ Propellor: [hostInfo] :: Host -> Info
+ Propellor: [hostName] :: Host -> HostName
+ Propellor: [hostProperties] :: Host -> [Property HasInfo]
+ Propellor: before :: (IsProp x, Combines y x, IsProp (CombinedType y x)) => x -> y -> CombinedType y x
+ Propellor: data Host
+ Propellor: data Property i
+ Propellor: data RevertableProperty
+ Propellor: defaultMain :: [Host] -> IO ()
+ Propellor: describe :: IsProp p => p -> Desc -> p
+ Propellor: host :: HostName -> Host
+ Propellor: onChange :: (Combines (Property x) (Property y)) => Property x -> Property y -> CombinedType (Property x) (Property y)
+ Propellor: requires :: Combines x y => x -> y -> CombinedType x y
+ Propellor.Engine: fromHost' :: Host -> Propellor a -> Propellor a
+ Propellor.Info: addHostInfo :: IsInfo v => Host -> v -> Host
+ Propellor.Location: localdir :: FilePath
+ Propellor.PrivData: readPrivData :: String -> PrivMap
+ Propellor.PrivData: unsetPrivDataUnused :: [Host] -> IO ()
+ Propellor.PropAccum: addProp :: (PropAccum h, IsProp p) => h -> p -> h
+ Propellor.PropAccum: addPropFront :: (PropAccum h, IsProp p) => h -> p -> h
+ Propellor.PropAccum: propagateContainer :: (PropAccum container) => String -> container -> Property HasInfo -> Property HasInfo
+ Propellor.Property: before :: (IsProp x, Combines y x, IsProp (CombinedType y x)) => x -> y -> CombinedType y x
+ Propellor.Property: data Propellor p
+ Propellor.Property: describe :: IsProp p => p -> Desc -> p
+ Propellor.Property: ensureProperty :: Property NoInfo -> Propellor Result
+ Propellor.Property: requires :: Combines x y => x -> y -> CombinedType x y
+ Propellor.Property.Apache: listenPorts :: [Port] -> Property NoInfo
+ Propellor.Property.Apache: siteDisabled :: HostName -> Property NoInfo
+ Propellor.Property.Apache: virtualHost :: HostName -> Port -> FilePath -> RevertableProperty
+ Propellor.Property.Apt: hasForeignArch :: String -> Property NoInfo
+ Propellor.Property.Chroot: Debootstrapped :: DebootstrapConfig -> Debootstrapped
+ Propellor.Property.Chroot: bootstrapped :: ChrootBootstrapper b => System -> b -> FilePath -> Chroot
+ Propellor.Property.Chroot: buildchroot :: ChrootBootstrapper b => b -> System -> FilePath -> Maybe (Property HasInfo)
+ Propellor.Property.Chroot: chrootSystem :: Chroot -> System
+ Propellor.Property.Chroot: class ChrootBootstrapper b
+ Propellor.Property.Chroot: data Debootstrapped
+ Propellor.Property.Chroot: instance Propellor.Property.Chroot.ChrootBootstrapper Propellor.Property.Chroot.Debootstrapped
+ Propellor.Property.Chroot: propagateChrootInfo :: (IsProp (Property i)) => Chroot -> Property i -> Property HasInfo
+ Propellor.Property.File: LinkTarget :: FilePath -> LinkTarget
+ Propellor.Property.File: isSymlinkedTo :: FilePath -> LinkTarget -> Property NoInfo
+ Propellor.Property.File: newtype LinkTarget
+ Propellor.Property.File: stableTmpFor :: FilePath -> FilePath
+ Propellor.Property.File: viaStableTmp :: (MonadMask m, MonadIO m) => (FilePath -> m ()) -> FilePath -> m ()
+ Propellor.Property.List: instance Propellor.PropAccum.PropAccum Propellor.Property.List.PropList
+ Propellor.Property.List: instance Propellor.Property.List.PropertyList Propellor.Property.List.PropList
+ Propellor.Property.List: props :: PropList
+ Propellor.Property.SiteSpecific.IABak: sshKeys :: [(SshKeyType, PubKeyText)]
+ Propellor.Property.Spin: class Spinnable t
+ Propellor.Property.Spin: controlledBy :: Host -> Property NoInfo
+ Propellor.Property.Spin: controllerFor :: Spinnable h => h -> Property HasInfo
+ Propellor.Property.Spin: controllerKeys :: [(SshKeyType, PubKeyText)] -> Property HasInfo
+ Propellor.Property.Spin: instance GHC.Base.Monoid Propellor.Property.Spin.Controlling
+ Propellor.Property.Spin: instance GHC.Show.Show Propellor.Property.Spin.Controlling
+ Propellor.Property.Spin: instance Propellor.Property.Spin.Spinnable Propellor.Types.Host
+ Propellor.Property.Spin: instance Propellor.Property.Spin.Spinnable [Propellor.Types.Host]
+ Propellor.Property.Spin: instance Propellor.Types.Info.IsInfo Propellor.Property.Spin.Controlling
+ Propellor.Property.Spin: toSpin :: Spinnable t => t -> Property HasInfo
+ Propellor.Property.Ssh: authorizedKeysFrom :: User -> (User, Host) -> Property NoInfo
+ Propellor.Property.Ssh: getHostPubKey :: Propellor (Map SshKeyType PubKeyText)
+ Propellor.Property.Ssh: getUserPubKeys :: User -> Propellor [(SshKeyType, PubKeyText)]
+ Propellor.Property.Ssh: hostPubKey :: SshKeyType -> PubKeyText -> Property HasInfo
+ Propellor.Property.Ssh: installed :: Property NoInfo
+ Propellor.Property.Ssh: instance GHC.Base.Monoid Propellor.Property.Ssh.HostKeyInfo
+ Propellor.Property.Ssh: instance GHC.Base.Monoid Propellor.Property.Ssh.UserKeyInfo
+ Propellor.Property.Ssh: instance GHC.Classes.Eq Propellor.Property.Ssh.HostKeyInfo
+ Propellor.Property.Ssh: instance GHC.Classes.Eq Propellor.Property.Ssh.UserKeyInfo
+ Propellor.Property.Ssh: instance GHC.Classes.Ord Propellor.Property.Ssh.HostKeyInfo
+ Propellor.Property.Ssh: instance GHC.Classes.Ord Propellor.Property.Ssh.UserKeyInfo
+ Propellor.Property.Ssh: instance GHC.Show.Show Propellor.Property.Ssh.HostKeyInfo
+ Propellor.Property.Ssh: instance GHC.Show.Show Propellor.Property.Ssh.UserKeyInfo
+ Propellor.Property.Ssh: instance Propellor.Types.Info.IsInfo Propellor.Property.Ssh.HostKeyInfo
+ Propellor.Property.Ssh: instance Propellor.Types.Info.IsInfo Propellor.Property.Ssh.UserKeyInfo
+ Propellor.Property.Ssh: userKeyAt :: IsContext c => Maybe FilePath -> User -> c -> (SshKeyType, PubKeyText) -> Property HasInfo
+ Propellor.Property.Ssh: userKeys :: IsContext c => User -> c -> [(SshKeyType, PubKeyText)] -> Property HasInfo
+ Propellor.Property.Uwsgi: appAvailable :: AppName -> ConfigFile -> Property NoInfo
+ Propellor.Property.Uwsgi: appCfg :: AppName -> FilePath
+ Propellor.Property.Uwsgi: appEnabled :: AppName -> ConfigFile -> RevertableProperty
+ Propellor.Property.Uwsgi: appVal :: AppName -> FilePath
+ Propellor.Property.Uwsgi: appValRelativeCfg :: AppName -> LinkTarget
+ Propellor.Property.Uwsgi: installed :: Property NoInfo
+ Propellor.Property.Uwsgi: reloaded :: Property NoInfo
+ Propellor.Property.Uwsgi: restarted :: Property NoInfo
+ Propellor.Property.Uwsgi: type AppName = String
+ Propellor.Property.Uwsgi: type ConfigFile = [String]
+ Propellor.Types: (<<>>) :: Combines x y => x -> y -> CombinedType x y
+ Propellor.Types: setDesc :: IsProp p => p -> Desc -> p
+ Propellor.Types.CmdLine: UnsetUnused :: CmdLine
+ Propellor.Types.Info: instance (Data.Typeable.Internal.Typeable v, GHC.Show.Show v) => Propellor.Types.Info.IsInfo (Propellor.Types.Info.InfoVal v)
+ Propellor.Types.Info: instance GHC.Show.Show Propellor.Types.Info.InfoEntry
+ Propellor.Types.Info: instance GHC.Show.Show v => GHC.Show.Show (Propellor.Types.Info.InfoVal v)
+ Propellor.Types.Info: propagatableInfo :: Info -> Info
+ Propellor.Types.Info: propagateInfo :: IsInfo v => v -> Bool
+ Propellor.Types.OS: instance GHC.Classes.Eq Propellor.Types.OS.Group
+ Propellor.Types.OS: instance GHC.Classes.Eq Propellor.Types.OS.User
+ Propellor.Types.OS: instance GHC.Classes.Ord Propellor.Types.OS.Group
+ Propellor.Types.OS: instance GHC.Classes.Ord Propellor.Types.OS.User
+ Propellor.Types.OS: instance GHC.Show.Show Propellor.Types.OS.Group
+ Propellor.Types.OS: instance GHC.Show.Show Propellor.Types.OS.User
+ Propellor.Utilities: (<&&>) :: Monad m => m Bool -> m Bool -> m Bool
+ Propellor.Utilities: (<||>) :: Monad m => m Bool -> m Bool -> m Bool
+ Propellor.Utilities: CreateProcess :: CmdSpec -> Maybe FilePath -> Maybe [(String, String)] -> StdStream -> StdStream -> StdStream -> Bool -> Bool -> Bool -> CreateProcess
+ Propellor.Utilities: DirectoryHandle :: IsOpen -> DirStream -> DirectoryHandle
+ Propellor.Utilities: StderrHandle :: StdHandle
+ Propellor.Utilities: StdinHandle :: StdHandle
+ Propellor.Utilities: StdoutHandle :: StdHandle
+ Propellor.Utilities: [close_fds] :: CreateProcess -> Bool
+ Propellor.Utilities: [cmdspec] :: CreateProcess -> CmdSpec
+ Propellor.Utilities: [create_group] :: CreateProcess -> Bool
+ Propellor.Utilities: [cwd] :: CreateProcess -> Maybe FilePath
+ Propellor.Utilities: [delegate_ctlc] :: CreateProcess -> Bool
+ Propellor.Utilities: [env] :: CreateProcess -> Maybe [(String, String)]
+ Propellor.Utilities: [std_err] :: CreateProcess -> StdStream
+ Propellor.Utilities: [std_in] :: CreateProcess -> StdStream
+ Propellor.Utilities: [std_out] :: CreateProcess -> StdStream
+ Propellor.Utilities: addEntries :: Eq k => [(k, v)] -> [(k, v)] -> [(k, v)]
+ Propellor.Utilities: addEntry :: Eq k => k -> v -> [(k, v)] -> [(k, v)]
+ Propellor.Utilities: after :: Monad m => m b -> m a -> m a
+ Propellor.Utilities: allM :: Monad m => (a -> m Bool) -> [a] -> m Bool
+ Propellor.Utilities: anyM :: Monad m => (a -> m Bool) -> [a] -> m Bool
+ Propellor.Utilities: beginning :: [a] -> [a]
+ Propellor.Utilities: bracketIO :: (MonadMask m, MonadIO m) => IO v -> (v -> IO b) -> (v -> m a) -> m a
+ Propellor.Utilities: catchBoolIO :: MonadCatch m => m Bool -> m Bool
+ Propellor.Utilities: catchDefaultIO :: MonadCatch m => a -> m a -> m a
+ Propellor.Utilities: catchHardwareFault :: MonadCatch m => m a -> (IOException -> m a) -> m a
+ Propellor.Utilities: catchIO :: MonadCatch m => m a -> (IOException -> m a) -> m a
+ Propellor.Utilities: catchMaybeIO :: MonadCatch m => m a -> m (Maybe a)
+ Propellor.Utilities: catchMsgIO :: MonadCatch m => m a -> m (Either String a)
+ Propellor.Utilities: catchNonAsync :: MonadCatch m => m a -> (SomeException -> m a) -> m a
+ Propellor.Utilities: checkSuccessProcess :: ProcessHandle -> IO Bool
+ Propellor.Utilities: closeDirectory :: DirectoryHandle -> IO ()
+ Propellor.Utilities: createBackgroundProcess :: CreateProcessRunner
+ Propellor.Utilities: createProcess :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
+ Propellor.Utilities: createProcessChecked :: (ProcessHandle -> IO b) -> CreateProcessRunner
+ Propellor.Utilities: createProcessSuccess :: CreateProcessRunner
+ Propellor.Utilities: data CreateProcess :: *
+ Propellor.Utilities: data DirectoryHandle
+ Propellor.Utilities: data StdHandle
+ Propellor.Utilities: delEntry :: Eq k => k -> [(k, v)] -> [(k, v)]
+ Propellor.Utilities: devNull :: FilePath
+ Propellor.Utilities: dirContents :: FilePath -> IO [FilePath]
+ Propellor.Utilities: dirContentsRecursive :: FilePath -> IO [FilePath]
+ Propellor.Utilities: dirContentsRecursiveSkipping :: (FilePath -> Bool) -> Bool -> FilePath -> IO [FilePath]
+ Propellor.Utilities: dirCruft :: FilePath -> Bool
+ Propellor.Utilities: dirTreeRecursiveSkipping :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
+ Propellor.Utilities: end :: [a] -> [a]
+ Propellor.Utilities: exitBool :: Bool -> IO a
+ Propellor.Utilities: feedWithQuietOutput :: CreateProcessRunner -> CreateProcess -> (Handle -> IO a) -> IO a
+ Propellor.Utilities: firstLine :: String -> String
+ Propellor.Utilities: firstM :: Monad m => (a -> m Bool) -> [a] -> m (Maybe a)
+ Propellor.Utilities: forceSuccessProcess :: CreateProcess -> ProcessHandle -> IO ()
+ Propellor.Utilities: getEnv :: String -> IO (Maybe String)
+ Propellor.Utilities: getEnvDefault :: String -> String -> IO String
+ Propellor.Utilities: getEnvironment :: IO [(String, String)]
+ Propellor.Utilities: getM :: Monad m => (a -> m (Maybe b)) -> [a] -> m (Maybe b)
+ Propellor.Utilities: hGetContentsStrict :: Handle -> IO String
+ Propellor.Utilities: hGetSomeString :: Handle -> Int -> IO String
+ Propellor.Utilities: head :: [a] -> a
+ Propellor.Utilities: headMaybe :: [a] -> Maybe a
+ Propellor.Utilities: ifM :: Monad m => m Bool -> (m a, m a) -> m a
+ Propellor.Utilities: ignoreFailureProcess :: ProcessHandle -> IO Bool
+ Propellor.Utilities: init :: [a] -> [a]
+ Propellor.Utilities: ioHandles :: (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> (Handle, Handle)
+ Propellor.Utilities: isDirectoryEmpty :: FilePath -> IO Bool
+ Propellor.Utilities: last :: [a] -> a
+ Propellor.Utilities: lastMaybe :: [a] -> Maybe a
+ Propellor.Utilities: massReplace :: [(String, String)] -> String -> String
+ Propellor.Utilities: moveFile :: FilePath -> FilePath -> IO ()
+ Propellor.Utilities: noop :: Monad m => m ()
+ Propellor.Utilities: nukeFile :: FilePath -> IO ()
+ Propellor.Utilities: observe :: Monad m => (a -> m b) -> m a -> m a
+ Propellor.Utilities: openDirectory :: FilePath -> IO DirectoryHandle
+ Propellor.Utilities: processHandle :: (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> ProcessHandle
+ Propellor.Utilities: processTranscript :: String -> [String] -> (Maybe String) -> IO (String, Bool)
+ Propellor.Utilities: processTranscript' :: String -> [String] -> Maybe [(String, String)] -> (Maybe String) -> IO (String, Bool)
+ Propellor.Utilities: prop_segment_regressionTest :: Bool
+ Propellor.Utilities: read :: Read a => String -> a
+ Propellor.Utilities: readDirectory :: DirectoryHandle -> IO (Maybe FilePath)
+ Propellor.Utilities: readFileStrict :: FilePath -> IO String
+ Propellor.Utilities: readFileStrictAnyEncoding :: FilePath -> IO String
+ Propellor.Utilities: readProcess :: FilePath -> [String] -> IO String
+ Propellor.Utilities: readProcess' :: CreateProcess -> IO String
+ Propellor.Utilities: readProcessEnv :: FilePath -> [String] -> Maybe [(String, String)] -> IO String
+ Propellor.Utilities: readish :: Read a => String -> Maybe a
+ Propellor.Utilities: reapZombies :: IO ()
+ Propellor.Utilities: relatedTemplate :: FilePath -> FilePath
+ Propellor.Utilities: segment :: (a -> Bool) -> [a] -> [[a]]
+ Propellor.Utilities: segmentDelim :: (a -> Bool) -> [a] -> [[a]]
+ Propellor.Utilities: separate :: (a -> Bool) -> [a] -> ([a], [a])
+ Propellor.Utilities: setEnv :: String -> String -> Bool -> IO ()
+ Propellor.Utilities: startInteractiveProcess :: FilePath -> [String] -> Maybe [(String, String)] -> IO (ProcessHandle, Handle, Handle)
+ Propellor.Utilities: stderrHandle :: HandleExtractor
+ Propellor.Utilities: stdinHandle :: HandleExtractor
+ Propellor.Utilities: stdoutHandle :: HandleExtractor
+ Propellor.Utilities: tail :: [a] -> [a]
+ Propellor.Utilities: tryIO :: MonadCatch m => m a -> m (Either IOException a)
+ Propellor.Utilities: tryNonAsync :: MonadCatch m => m a -> m (Either SomeException a)
+ Propellor.Utilities: tryWhenExists :: MonadCatch m => m a -> m (Maybe a)
+ Propellor.Utilities: type IsOpen = MVar ()
+ Propellor.Utilities: type Template = String
+ Propellor.Utilities: unsetEnv :: String -> IO ()
+ Propellor.Utilities: untilTrue :: Monad m => [a] -> (a -> m Bool) -> m Bool
+ Propellor.Utilities: viaTmp :: (MonadMask m, MonadIO m) => (FilePath -> String -> m ()) -> FilePath -> String -> m ()
+ Propellor.Utilities: waitForProcess :: ProcessHandle -> IO ExitCode
+ Propellor.Utilities: withHandle :: StdHandle -> CreateProcessRunner -> CreateProcess -> (Handle -> IO a) -> IO a
+ Propellor.Utilities: withIOHandles :: CreateProcessRunner -> CreateProcess -> ((Handle, Handle) -> IO a) -> IO a
+ Propellor.Utilities: withOEHandles :: CreateProcessRunner -> CreateProcess -> ((Handle, Handle) -> IO a) -> IO a
+ Propellor.Utilities: withQuietOutput :: CreateProcessRunner -> CreateProcess -> IO ()
+ Propellor.Utilities: withTmpDir :: (MonadMask m, MonadIO m) => Template -> (FilePath -> m a) -> m a
+ Propellor.Utilities: withTmpDirIn :: (MonadMask m, MonadIO m) => FilePath -> Template -> (FilePath -> m a) -> m a
+ Propellor.Utilities: withTmpFile :: (MonadIO m, MonadMask m) => Template -> (FilePath -> Handle -> m a) -> m a
+ Propellor.Utilities: withTmpFileIn :: (MonadIO m, MonadMask m) => FilePath -> Template -> (FilePath -> Handle -> m a) -> m a
+ Propellor.Utilities: writeFileAnyEncoding :: FilePath -> String -> IO ()
+ Propellor.Utilities: writeReadProcessEnv :: FilePath -> [String] -> Maybe [(String, String)] -> (Maybe (Handle -> IO ())) -> (Maybe (Handle -> IO ())) -> IO String
- Propellor.Property.Chroot: Chroot :: FilePath -> System -> BuilderConf -> Host -> Chroot
+ Propellor.Property.Chroot: Chroot :: FilePath -> System -> b -> Host -> Chroot
- Propellor.Property.Debootstrap: built :: FilePath -> System -> DebootstrapConfig -> RevertableProperty
+ Propellor.Property.Debootstrap: built :: FilePath -> System -> DebootstrapConfig -> Property HasInfo
- Propellor.Property.Nginx: siteValRelativeCfg :: HostName -> FilePath
+ Propellor.Property.Nginx: siteValRelativeCfg :: HostName -> LinkTarget
- Propellor.Property.OpenId: providerFor :: [User] -> String -> Property HasInfo
+ Propellor.Property.OpenId: providerFor :: [User] -> HostName -> Maybe Port -> Property HasInfo
- Propellor.Types.Info: class (Typeable v, Monoid v) => IsInfo v
+ Propellor.Types.Info: class (Typeable v, Monoid v, Show v) => IsInfo v

Files

CHANGELOG view
@@ -1,3 +1,41 @@+propellor (2.9.0) unstable; urgency=medium++  * Added basic Uwsgi module, maintained by Félix Sipma.+  * Add Apt.hasForeignArch. Thanks, Per Olofsson.+  * Improved documentation, particularly of the Propellor module.+  * The Propellor module no longer exports many of the things it used to,+    being now focused on only what's needed to write config.hs.+    Use Propellor.Base to get all the things exported by Propellor before.+    (API change)+  * Some renaming of instance methods, and moving of functions to more+    appropriate modules. (API change)+  * Added File.isSymlinkedTo. Thanks, Per Olofsson.+  * fileProperty, and properties derived from it now write the new+    file content via origfile.propellor-new~, instead of to a randomly named+    temp file. This allows them to clean up any temp file that may have+    been left by an interrupted run of propellor.+  * Added Propellor.Property.Spin, which can be used to make a host be a+    controller of other hosts, which will automatically spin them each time+    propellor is run.+  * Ssh.keyImported is replaced with Ssh.userKeys. (API change)+    The new property only gets the private key from the privdata; the+    public key is provided as a parameter, and so is available as+    Info that other properties can use.+  * Ssh.keyImported' is renamed to Ssh.userKeyAt, and also changed+    to only import the private key from the privdata. (API change)+  * While Ssh.keyImported and Ssh.keyImported' avoided updating existing+    keys, the new Ssh.userKeys and Ssh.userKeyAt properties will+    always update out of date key files.+  * Ssh.pubKey renamed to Ssh.hostPubKey. (API change)+  * Added --unset-unused +  * Fix typo: propigate → propagate. Thanks, Felix Gruber.+    (A minor API change)+  * Chroot: Converted to use a ChrootBootstrapper type class, so+    other ways to bootstrap chroots can easily be added in separate+    modules. (API change)++ -- Joey Hess <id@joeyh.name>  Tue, 20 Oct 2015 15:43:12 -0400+ propellor (2.8.1) unstable; urgency=medium    * Guard against power loss etc when building propellor, by updating@@ -272,7 +310,7 @@ propellor (1.2.2) unstable; urgency=medium    * Revert ensureProperty warning message, too many false positives in places-    where Info is correctly propigated. Better approach needed.+    where Info is correctly propagated. Better approach needed.   -- Joey Hess <id@joeyh.name>  Sun, 21 Dec 2014 21:41:11 -0400 
config-joey.hs view
@@ -3,7 +3,6 @@ module Main where  import Propellor-import Propellor.CmdLine import Propellor.Property.Scheduled import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt@@ -17,9 +16,9 @@ import qualified Propellor.Property.Tor as Tor import qualified Propellor.Property.Dns as Dns import qualified Propellor.Property.OpenId as OpenId-import qualified Propellor.Property.Docker as Docker import qualified Propellor.Property.Git as Git import qualified Propellor.Property.Postfix as Postfix+import qualified Propellor.Property.Apache as Apache import qualified Propellor.Property.Grub as Grub import qualified Propellor.Property.Obnam as Obnam import qualified Propellor.Property.Gpg as Gpg@@ -104,7 +103,11 @@ 	& ipv4 "167.88.41.194"  	& CloudAtCost.decruft-	& Ssh.randomHostKeys+	& Ssh.hostKeys hostContext+		[ (SshDsa, "ssh-dss AAAAB3NzaC1kc3MAAACBAI3WUq0RaigLlcUivgNG4sXpso2ORZkMvfqKz6zkc60L6dpxvWDNmZVEH8hEjxRSYG07NehcuOgQqeyFnS++xw1hdeGjf37JqCUH49i02lra3Zxv8oPpRxyeqe5MmuzUJhlWvBdlc3O/nqZ4bTUfnxMzSYWyy6++s/BpSHttZplNAAAAFQC1DE0vzgVeNAv9smHLObQWZFe2VQAAAIBECtpJry3GC8NVTFsTHDGWksluoFPIbKiZUFFztZGdM0AO2VwAbiJ6Au6M3VddGFANgTlni6d2/9yS919zO90TaFoIjywZeXhxE2CSuRfU7sx2hqDBk73jlycem/ER0sanFhzpHVpwmLfWneTXImWyq37vhAxatJANOtbj81vQ3AAAAIBV3lcyTT9xWg1Q4vERJbvyF8mCliwZmnIPa7ohveKkxlcgUk5d6dnaqFfjVaiXBPN3Qd08WXoQ/a9k3chBPT9nW2vWgzzM8l36j2MbHLmaxGwevAc9+vx4MXqvnGHzd2ex950mC33ct3j0fzMZlO6vqEsgD4CYmiASxhfefj+JCQ==")+		, (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJybAjUPUWIhvVMmer8K5ZgdfI54DM6vc8Mzw+5KmVKL0TwkvzbR1HAB4heyMGtN1F8YzkWhsI3/Txh+MQUJ+i4u8SvSYc6D1q3j3ZyCi06wZ3DJS25tZrOM/thOOA1DFA4Hhb0uI/1Kg8PguNNNSMXn8F7q3F6cFQizYgszs6z6ktiST/BTC+IXWovhcnn2vQXXU8FTcTsqBFqA5dEjZbp1WDzqp3km84ZyXGmoVlpqzXeMvlkWTIshYiQjXIwPOkALzlGYjp1lw1OaxPVI1IGFcgCbIWQQWoCReb+genX2VaR+odAYXjaOdRx0lQj7UCPTBCpqMyzBMLtT5Yiaqh")+		, (SshEcdsa, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPhfvcOuw0Yt+MnsFc4TI2gWkKi62Eajxz+TgbHMO/uRTYF8c5V8fOI3o+J/3m5+lT0S5o8j8a7xIC3COvi+AVw=")+		] 	& Apt.unattendedUpgrades 	& Network.ipv6to4 @@ -221,8 +224,10 @@ 		, "--exclude=.*/tmp/" 		, "--one-file-system" 		] Obnam.OnlyClient (Gpg.GpgKeyId "98147487")-		`requires` Ssh.keyImported SshRsa (User "root")+		`requires` Ssh.userKeys (User "root") 			(Context "kite.kitenet.net")+			[ (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC5Gza2sNqSKfNtUN4dN/Z3rlqw18nijmXFx6df2GtBoZbkIak73uQfDuZLP+AXlyfHocwdkdHEf/zrxgXS4EokQMGLZhJ37Pr3edrEn/NEnqroiffw7kyd7EqaziA6UOezcLTjWGv+Zqg9JhitYs4WWTpNzrPH3yQf1V9FunZnkzb4gJGndts13wGmPEwSuf+QHbgQvjMOMCJwWSNcJGdhDR66hFlxfG26xx50uIczXYAbgLfHp5W6WuR/lcaS9J6i7HAPwcsPDA04XDinrcpl29QwsMW1HyGS/4FSCgrDqNZ2jzP49Bka78iCLRqfl1efyYas/Zo1jQ0x+pxq2RMr root@kite")+			] 		`requires` Ssh.knownHost hosts "eubackup.kitenet.net" (User "root") 	& Apt.serviceInstalledRunning "ntp" 	& "/etc/timezone" `File.hasContent` ["US/Eastern"]@@ -299,7 +304,9 @@ 	& Apt.unattendedUpgrades 	& Systemd.installed 	& Systemd.persistentJournal-	& Ssh.keyImported SshRsa (User "joey") hostContext+	& Ssh.userKeys (User "joey") hostContext+		[ (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4wJuQEGno+nJvtE75IKL6JQ08sJHZ9Bzs9Dvu0zuxSEZE30MWK98/twNwCH9PVf2N9m4apfN7f9GHgHTUongfo8xnLAk4PuBSTV74YgKyOCvNYqANuKKa+76PsS/vFf/or3ct++uTEWsRyYD29cQndufwKA4rthAqHG+fifbLDC53AjcldI0zI1RckpPzT+AMazlnSBFMlpKvGD2uzSXALVRXa3vSqWkWd0z7qmIkpmpq0AAgbDLwrGBcUGV/h0rOa2s8zSeirA0tLmHNROl4cZsX0T/6VBGfBRkrHSxL67xJziATw4WPq6spYlxg84pC/5qJVr9SC5HosppbDqgj joey@elephant")+		]  	& Apt.serviceInstalledRunning "swapspace"  	& alias "eubackup.kitenet.net"@@ -322,14 +329,10 @@ 	& alias "ns3.kitenet.net" 	& myDnsSecondary 	-	& Docker.configured-	& Docker.docked openidProvider-		`requires` Apt.serviceInstalledRunning "ntp"-	& Docker.docked ancientKitenet-	& Docker.docked jerryPlay-	& Docker.garbageCollected `period` (Weekly (Just 1))-	 	& Systemd.nspawned oldusenetShellBox+	& Systemd.nspawned ancientKitenet+	& Systemd.nspawned openidProvider+	 	`requires` Apt.serviceInstalledRunning "ntp" 	 	& JoeySites.scrollBox 	& alias "scroll.joeyh.name"@@ -349,7 +352,7 @@ 	& ipv6 "2001:4830:1600:195::2" 	& Apt.serviceInstalledRunning "aiccu" 	& Apt.installed ["ssh"]-	& Ssh.pubKey SshDsa "ssh-dss AAAAB3NzaC1kc3MAAACBAIrLX260fY0Jjj/p0syNhX8OyR8hcr6feDPGOj87bMad0k/w/taDSOzpXe0Wet7rvUTbxUjH+Q5wPd4R9zkaSDiR/tCb45OdG6JsaIkmqncwe8yrU+pqSRCxttwbcFe+UU+4AAcinjVedZjVRDj2rRaFPc9BXkPt7ffk8GwEJ31/AAAAFQCG/gOjObsr86vvldUZHCteaJttNQAAAIB5nomvcqOk/TD07DLaWKyG7gAcW5WnfY3WtnvLRAFk09aq1EuiJ6Yba99Zkb+bsxXv89FWjWDg/Z3Psa22JMyi0HEDVsOevy/1sEQ96AGH5ijLzFInfXAM7gaJKXASD7hPbVdjySbgRCdwu0dzmQWHtH+8i1CMVmA2/a5Y/wtlJAAAAIAUZj2US2D378jBwyX1Py7e4sJfea3WSGYZjn4DLlsLGsB88POuh32aOChd1yzF6r6C2sdoPBHQcWBgNGXcx4gF0B5UmyVHg3lIX2NVSG1ZmfuLNJs9iKNu4cHXUmqBbwFYQJBvB69EEtrOw4jSbiTKwHFmqdA/mw1VsMB+khUaVw=="+	& Ssh.hostPubKey SshDsa "ssh-dss AAAAB3NzaC1kc3MAAACBAIrLX260fY0Jjj/p0syNhX8OyR8hcr6feDPGOj87bMad0k/w/taDSOzpXe0Wet7rvUTbxUjH+Q5wPd4R9zkaSDiR/tCb45OdG6JsaIkmqncwe8yrU+pqSRCxttwbcFe+UU+4AAcinjVedZjVRDj2rRaFPc9BXkPt7ffk8GwEJ31/AAAAFQCG/gOjObsr86vvldUZHCteaJttNQAAAIB5nomvcqOk/TD07DLaWKyG7gAcW5WnfY3WtnvLRAFk09aq1EuiJ6Yba99Zkb+bsxXv89FWjWDg/Z3Psa22JMyi0HEDVsOevy/1sEQ96AGH5ijLzFInfXAM7gaJKXASD7hPbVdjySbgRCdwu0dzmQWHtH+8i1CMVmA2/a5Y/wtlJAAAAIAUZj2US2D378jBwyX1Py7e4sJfea3WSGYZjn4DLlsLGsB88POuh32aOChd1yzF6r6C2sdoPBHQcWBgNGXcx4gF0B5UmyVHg3lIX2NVSG1ZmfuLNJs9iKNu4cHXUmqBbwFYQJBvB69EEtrOw4jSbiTKwHFmqdA/mw1VsMB+khUaVw==" 	& alias "usbackup.kitenet.net" 	& JoeySites.backupsBackedupFrom hosts "eubackup.kitenet.net" "/home/joey/lib/backup" 	& Apt.serviceInstalledRunning "anacron"@@ -424,40 +427,36 @@ webserver :: Systemd.Container webserver = standardStableContainer "webserver" 	& Systemd.bind "/var/www"-	& Apt.serviceInstalledRunning "apache2"+	& Apache.installed  -- My own openid provider. Uses php, so containerized for security -- and administrative sanity.-openidProvider :: Docker.Container-openidProvider = standardStableDockerContainer "openid-provider"-	& alias "openid.kitenet.net"-	& Docker.publish "8081:80"-	& OpenId.providerFor [User "joey", User "liw"]-		"openid.kitenet.net:8081"+openidProvider :: Systemd.Container+openidProvider = standardStableContainer "openid-provider"+	& alias hn+	& OpenId.providerFor [User "joey", User "liw"] hn (Just (Port 8081))+  where+	hn = "openid.kitenet.net" --- Exhibit: kite's 90's website.-ancientKitenet :: Docker.Container-ancientKitenet = standardStableDockerContainer "ancient-kitenet"-	& alias "ancient.kitenet.net"-	& Docker.publish "1994:80"-	& Apt.serviceInstalledRunning "apache2"+-- Exhibit: kite's 90's website on port 1994.+ancientKitenet :: Systemd.Container+ancientKitenet = standardStableContainer "ancient-kitenet"+	& alias hn 	& Git.cloned (User "root") "git://kitenet-net.branchable.com/" "/var/www/html" 		(Just "remotes/origin/old-kitenet.net")+	& Apache.installed+	& Apache.listenPorts [p]+	& Apache.virtualHost hn p "/var/www/html"+	& Apache.siteDisabled "000-default"+  where+	p = Port 1994+	hn = "ancient.kitenet.net"  oldusenetShellBox :: Systemd.Container oldusenetShellBox = standardStableContainer "oldusenet-shellbox" 	& alias "shell.olduse.net" 	& JoeySites.oldUseNetShellBox -jerryPlay :: Docker.Container-jerryPlay = standardDockerContainer "jerryplay" Unstable "amd64"-	& alias "jerryplay.kitenet.net"-	& Docker.publish "2202:22"-	& Docker.publish "8001:80"-	& Apt.installed ["ssh"]-	& User.hasPassword (User "root")-	& Ssh.permitRootLogin (Ssh.RootLogin True)- kiteShellBox :: Systemd.Container kiteShellBox = standardStableContainer "kiteshellbox" 	& JoeySites.kiteShellBox@@ -505,26 +504,6 @@ standardStableContainer :: Systemd.MachineName -> Systemd.Container standardStableContainer name = standardContainer name (Stable "jessie") "amd64" -standardStableDockerContainer :: Docker.ContainerName -> Docker.Container-standardStableDockerContainer name = standardDockerContainer name (Stable "jessie") "amd64"--standardDockerContainer :: Docker.ContainerName -> DebianSuite -> Architecture -> Docker.Container-standardDockerContainer name suite arch = Docker.container name (dockerImage system)-	& os system-	& Apt.stdSourcesList `onChange` Apt.upgrade-	& Apt.unattendedUpgrades-	& Apt.cacheCleaned-	& Docker.tweaked-  where-	system = System (Debian suite) arch---- Docker images I prefer to use.-dockerImage :: System -> Docker.Image-dockerImage (System (Debian Unstable) arch) = Docker.latestImage ("joeyh/debian-unstable-" ++ arch)-dockerImage (System (Debian Testing) arch) = Docker.latestImage ("joeyh/debian-unstable-" ++ arch)-dockerImage (System (Debian (Stable _)) arch) = Docker.latestImage ("joeyh/debian-stable-" ++ arch)-dockerImage _ = Docker.latestImage "debian-stable-official" -- does not currently exist!- myDnsSecondary :: Property HasInfo myDnsSecondary = propertyList "dns secondary for all my domains" $ props 	& Dns.secondary hosts "kitenet.net"@@ -553,11 +532,11 @@ monsters :: [Host]    -- Systems I don't manage with propellor, monsters =            -- but do want to track their public keys etc. 	[ host "usw-s002.rsync.net"-		& Ssh.pubKey SshDsa "ssh-dss AAAAB3NzaC1kc3MAAAEBAI6ZsoW8a+Zl6NqUf9a4xXSMcV1akJHDEKKBzlI2YZo9gb9YoCf5p9oby8THUSgfh4kse7LJeY7Nb64NR6Y/X7I2/QzbE1HGGl5mMwB6LeUcJ74T3TQAlNEZkGt/MOIVLolJHk049hC09zLpkUDtX8K0t1yaCirC9SxDGLTCLEhvU9+vVdVrdQlKZ9wpLUNbdAzvbra+O/IVvExxDZ9WCHrnfNA8ddVZIGEWMqsoNgiuCxiXpi8qL+noghsSQNFTXwo7W2Vp9zj1JkCt3GtSz5IzEpARQaXEAWNEM0n1nJ686YUOhou64iRM8bPC1lp3QXvvZNgj3m+QHhIempx+de8AAAAVAKB5vUDaZOg14gRn7Bp81ja/ik+RAAABACPH/bPbW912x1NxNiikzGR6clLh+bLpIp8Qie3J7DwOr8oC1QOKjNDK+UgQ7mDQEgr4nGjNKSvpDi4c1QCw4sbLqQgx1y2VhT0SmUPHf5NQFldRQyR/jcevSSwOBxszz3aq9AwHiv9OWaO3XY18suXPouiuPTpIcZwc2BLDNHFnDURQeGEtmgqj6gZLIkTY0iw7q9Tj5FOyl4AkvEJC5B4CSzaWgey93Wqn1Imt7KI8+H9lApMKziVL1q+K7xAuNkGmx5YOSNlE6rKAPtsIPHZGxR7dch0GURv2jhh0NQYvBRn3ukCjuIO5gx56HLgilq59/o50zZ4NcT7iASF76TcAAAEAC6YxX7rrs8pp13W4YGiJHwFvIO1yXLGOdqu66JM0plO4J1ItV1AQcazOXLiliny3p2/W+wXZZKd5HIRt52YafCA8YNyMk/sF7JcTR4d4z9CfKaAxh0UpzKiAk+0j/Wu3iPoTOsyt7N0j1+dIyrFodY2sKKuBMT4TQ0yqQpbC+IDQv2i1IlZAPneYGfd5MIGygs2QMfaMQ1jWAKJvEO0vstZ7GB6nDAcg4in3ZiBHtomx3PL5w+zg48S4Ed69BiFXLZ1f6MnjpUOP75pD4MP6toS0rgK9b93xCrEQLgm4oD/7TCHHBo2xR7wwcsN2OddtwWsEM2QgOkt/jdCAoVCqwQ=="+		& Ssh.hostPubKey SshDsa "ssh-dss AAAAB3NzaC1kc3MAAAEBAI6ZsoW8a+Zl6NqUf9a4xXSMcV1akJHDEKKBzlI2YZo9gb9YoCf5p9oby8THUSgfh4kse7LJeY7Nb64NR6Y/X7I2/QzbE1HGGl5mMwB6LeUcJ74T3TQAlNEZkGt/MOIVLolJHk049hC09zLpkUDtX8K0t1yaCirC9SxDGLTCLEhvU9+vVdVrdQlKZ9wpLUNbdAzvbra+O/IVvExxDZ9WCHrnfNA8ddVZIGEWMqsoNgiuCxiXpi8qL+noghsSQNFTXwo7W2Vp9zj1JkCt3GtSz5IzEpARQaXEAWNEM0n1nJ686YUOhou64iRM8bPC1lp3QXvvZNgj3m+QHhIempx+de8AAAAVAKB5vUDaZOg14gRn7Bp81ja/ik+RAAABACPH/bPbW912x1NxNiikzGR6clLh+bLpIp8Qie3J7DwOr8oC1QOKjNDK+UgQ7mDQEgr4nGjNKSvpDi4c1QCw4sbLqQgx1y2VhT0SmUPHf5NQFldRQyR/jcevSSwOBxszz3aq9AwHiv9OWaO3XY18suXPouiuPTpIcZwc2BLDNHFnDURQeGEtmgqj6gZLIkTY0iw7q9Tj5FOyl4AkvEJC5B4CSzaWgey93Wqn1Imt7KI8+H9lApMKziVL1q+K7xAuNkGmx5YOSNlE6rKAPtsIPHZGxR7dch0GURv2jhh0NQYvBRn3ukCjuIO5gx56HLgilq59/o50zZ4NcT7iASF76TcAAAEAC6YxX7rrs8pp13W4YGiJHwFvIO1yXLGOdqu66JM0plO4J1ItV1AQcazOXLiliny3p2/W+wXZZKd5HIRt52YafCA8YNyMk/sF7JcTR4d4z9CfKaAxh0UpzKiAk+0j/Wu3iPoTOsyt7N0j1+dIyrFodY2sKKuBMT4TQ0yqQpbC+IDQv2i1IlZAPneYGfd5MIGygs2QMfaMQ1jWAKJvEO0vstZ7GB6nDAcg4in3ZiBHtomx3PL5w+zg48S4Ed69BiFXLZ1f6MnjpUOP75pD4MP6toS0rgK9b93xCrEQLgm4oD/7TCHHBo2xR7wwcsN2OddtwWsEM2QgOkt/jdCAoVCqwQ==" 	, host "github.com" -		& Ssh.pubKey SshRsa "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ=="+		& Ssh.hostPubKey SshRsa "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==" 	, host "gitlab.com"-		& Ssh.pubKey SshEcdsa "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY="+		& Ssh.hostPubKey SshEcdsa "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=" 	, host "ns6.gandi.net" 		& ipv4 "217.70.177.40" 	, host "turtle.kitenet.net"
config-simple.hs view
@@ -2,13 +2,12 @@ -- the propellor program.  import Propellor-import Propellor.CmdLine-import Propellor.Property.Scheduled import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Network as Network --import qualified Propellor.Property.Ssh as Ssh import qualified Propellor.Property.Cron as Cron+import Propellor.Property.Scheduled --import qualified Propellor.Property.Sudo as Sudo import qualified Propellor.Property.User as User --import qualified Propellor.Property.Hostname as Hostname@@ -19,25 +18,25 @@ main = defaultMain hosts  -- The hosts propellor knows about.--- Edit this to configure propellor! hosts :: [Host] hosts =-	[ host "mybox.example.com"-		& os (System (Debian Unstable) "amd64")-		& Apt.stdSourcesList-		& Apt.unattendedUpgrades-		& Apt.installed ["etckeeper"]-		& Apt.installed ["ssh"]-		& User.hasSomePassword (User "root")-		& Network.ipv6to4-		& File.dirExists "/var/www"-		& Docker.docked webserverContainer-		& Docker.garbageCollected `period` Daily-		& Cron.runPropellor (Cron.Times "30 * * * *")--	-- add more hosts here...-	--, host "foo.example.com" = ...+	[ mybox 	]++-- An example host.+mybox :: Host+mybox = host "mybox.example.com"+	& os (System (Debian Unstable) "amd64")+	& Apt.stdSourcesList+	& Apt.unattendedUpgrades+	& Apt.installed ["etckeeper"]+	& Apt.installed ["ssh"]+	& User.hasSomePassword (User "root")+	& Network.ipv6to4+	& File.dirExists "/var/www"+	& Docker.docked webserverContainer+	& Docker.garbageCollected `period` Daily+	& Cron.runPropellor (Cron.Times "30 * * * *")  -- A generic webserver in a Docker container. webserverContainer :: Docker.Container
config.hs view
@@ -2,13 +2,12 @@ -- the propellor program.  import Propellor-import Propellor.CmdLine-import Propellor.Property.Scheduled import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Network as Network --import qualified Propellor.Property.Ssh as Ssh import qualified Propellor.Property.Cron as Cron+import Propellor.Property.Scheduled --import qualified Propellor.Property.Sudo as Sudo import qualified Propellor.Property.User as User --import qualified Propellor.Property.Hostname as Hostname@@ -19,25 +18,25 @@ main = defaultMain hosts  -- The hosts propellor knows about.--- Edit this to configure propellor! hosts :: [Host] hosts =-	[ host "mybox.example.com"-		& os (System (Debian Unstable) "amd64")-		& Apt.stdSourcesList-		& Apt.unattendedUpgrades-		& Apt.installed ["etckeeper"]-		& Apt.installed ["ssh"]-		& User.hasSomePassword (User "root")-		& Network.ipv6to4-		& File.dirExists "/var/www"-		& Docker.docked webserverContainer-		& Docker.garbageCollected `period` Daily-		& Cron.runPropellor (Cron.Times "30 * * * *")--	-- add more hosts here...-	--, host "foo.example.com" = ...+	[ mybox 	]++-- An example host.+mybox :: Host+mybox = host "mybox.example.com"+	& os (System (Debian Unstable) "amd64")+	& Apt.stdSourcesList+	& Apt.unattendedUpgrades+	& Apt.installed ["etckeeper"]+	& Apt.installed ["ssh"]+	& User.hasSomePassword (User "root")+	& Network.ipv6to4+	& File.dirExists "/var/www"+	& Docker.docked webserverContainer+	& Docker.garbageCollected `period` Daily+	& Cron.runPropellor (Cron.Times "30 * * * *")  -- A generic webserver in a Docker container. webserverContainer :: Docker.Container
debian/changelog view
@@ -1,3 +1,41 @@+propellor (2.9.0) unstable; urgency=medium++  * Added basic Uwsgi module, maintained by Félix Sipma.+  * Add Apt.hasForeignArch. Thanks, Per Olofsson.+  * Improved documentation, particularly of the Propellor module.+  * The Propellor module no longer exports many of the things it used to,+    being now focused on only what's needed to write config.hs.+    Use Propellor.Base to get all the things exported by Propellor before.+    (API change)+  * Some renaming of instance methods, and moving of functions to more+    appropriate modules. (API change)+  * Added File.isSymlinkedTo. Thanks, Per Olofsson.+  * fileProperty, and properties derived from it now write the new+    file content via origfile.propellor-new~, instead of to a randomly named+    temp file. This allows them to clean up any temp file that may have+    been left by an interrupted run of propellor.+  * Added Propellor.Property.Spin, which can be used to make a host be a+    controller of other hosts, which will automatically spin them each time+    propellor is run.+  * Ssh.keyImported is replaced with Ssh.userKeys. (API change)+    The new property only gets the private key from the privdata; the+    public key is provided as a parameter, and so is available as+    Info that other properties can use.+  * Ssh.keyImported' is renamed to Ssh.userKeyAt, and also changed+    to only import the private key from the privdata. (API change)+  * While Ssh.keyImported and Ssh.keyImported' avoided updating existing+    keys, the new Ssh.userKeys and Ssh.userKeyAt properties will+    always update out of date key files.+  * Ssh.pubKey renamed to Ssh.hostPubKey. (API change)+  * Added --unset-unused +  * Fix typo: propigate → propagate. Thanks, Felix Gruber.+    (A minor API change)+  * Chroot: Converted to use a ChrootBootstrapper type class, so+    other ways to bootstrap chroots can easily be added in separate+    modules. (API change)++ -- Joey Hess <id@joeyh.name>  Tue, 20 Oct 2015 15:43:12 -0400+ propellor (2.8.1) unstable; urgency=medium    * Guard against power loss etc when building propellor, by updating@@ -272,7 +310,7 @@ propellor (1.2.2) unstable; urgency=medium    * Revert ensureProperty warning message, too many false positives in places-    where Info is correctly propigated. Better approach needed.+    where Info is correctly propagated. Better approach needed.   -- Joey Hess <id@joeyh.name>  Sun, 21 Dec 2014 21:41:11 -0400 
propellor.cabal view
@@ -1,5 +1,5 @@ Name: propellor-Version: 2.8.1+Version: 2.9.0 Cabal-Version: >= 1.8 License: BSD3 Maintainer: Joey Hess <id@joeyh.name>@@ -69,6 +69,8 @@    Exposed-Modules:     Propellor+    Propellor.Base+    Propellor.Location     Propellor.Property     Propellor.Property.Aiccu     Propellor.Property.Apache@@ -92,6 +94,9 @@     Propellor.Property.Grub     Propellor.Property.Journald     Propellor.Property.Kerberos+    Propellor.Property.List+    Propellor.Property.LightDM+    Propellor.Property.Logcheck     Propellor.Property.Mount     Propellor.Property.Network     Propellor.Property.Nginx@@ -104,9 +109,7 @@     Propellor.Property.Prosody     Propellor.Property.Reboot     Propellor.Property.Rsync-    Propellor.Property.List-    Propellor.Property.LightDM-    Propellor.Property.Logcheck+    Propellor.Property.Spin     Propellor.Property.Scheduled     Propellor.Property.Service     Propellor.Property.Ssh@@ -116,6 +119,7 @@     Propellor.Property.Tor     Propellor.Property.Unbound     Propellor.Property.User+    Propellor.Property.Uwsgi     Propellor.Property.HostingProvider.CloudAtCost     Propellor.Property.HostingProvider.DigitalOcean     Propellor.Property.HostingProvider.Linode@@ -125,6 +129,7 @@     Propellor.Property.SiteSpecific.Branchable     Propellor.Property.SiteSpecific.IABak     Propellor.PropAccum+    Propellor.Utilities     Propellor.CmdLine     Propellor.Info     Propellor.Message
src/Propellor.hs view
@@ -1,15 +1,12 @@-{-# LANGUAGE PackageImports #-}+{-# OPTIONS_GHC -fno-warn-duplicate-exports #-} --- | Pulls in lots of useful modules for building and using Properties.--- --- When propellor runs on a Host, it ensures that its list of Properties--- is satisfied, taking action as necessary when a Property is not+-- | When propellor runs on a Host, it ensures that its Properties+-- are satisfied, taking action as necessary when a Property is not -- currently satisfied. -- -- A simple propellor program example: -- -- > import Propellor--- > import Propellor.CmdLine -- > import qualified Propellor.Property.File as File -- > import qualified Propellor.Property.Apt as Apt -- > @@ -30,54 +27,49 @@ -- git clone <git://git.joeyh.name/propellor>  module Propellor (-	  module Propellor.Types+	-- * Core data types+	  Host(..)+	, Property+	, RevertableProperty+	, (<!>)+	, module Propellor.Types+	-- * Config file+	, defaultMain+	, host+	, (&)+	, (!)+	-- * Propertries+	, describe+	-- | Properties are often combined together in your propellor+	-- configuration. For example:+	--+	-- > "/etc/foo/config" `File.containsLine` "bar=1"+	-- > 	`requires` File.dirExists "/etc/foo"+	, requires+	, before+	, onChange 	, module Propellor.Property-	, module Propellor.Property.List+	-- | Everything you need to build your own properties,+	-- and useful property combinators 	, module Propellor.Property.Cmd-	, module Propellor.PropAccum+	-- | Properties to run shell commands 	, module Propellor.Info-	, module Propellor.PrivData+	-- | Properties that set `Info`+	, module Propellor.Property.List+	-- | Combining a list of properties into a single property 	, module Propellor.Types.PrivData-	, module Propellor.Engine-	, module Propellor.Exception-	, module Propellor.Message-	, localdir+	-- | Private data access for properties  	, module X ) where  import Propellor.Types+import Propellor.CmdLine (defaultMain) import Propellor.Property-import Propellor.Engine import Propellor.Property.List import Propellor.Property.Cmd-import Propellor.PrivData import Propellor.Types.PrivData-import Propellor.Message-import Propellor.Exception import Propellor.Info import Propellor.PropAccum -import Utility.PartialPrelude as X-import Utility.Process as X-import Utility.Exception as X-import Utility.Env as X-import Utility.Directory as X-import Utility.Tmp as X-import Utility.Monad as X-import Utility.Misc as X--import System.Directory as X-import System.IO as X-import System.FilePath as X-import Data.Maybe as X-import Data.Either as X-import Control.Applicative as X-import Control.Monad as X import Data.Monoid as X-import Control.Monad.IfElse as X-import "mtl" Control.Monad.Reader as X---- | This is where propellor installs itself when deploying a host.-localdir :: FilePath-localdir = "/usr/local/propellor"
+ src/Propellor/Base.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE PackageImports #-}++-- | Pulls in lots of useful modules for building and using Properties.++module Propellor.Base (+	-- * Propellor modules+	  module Propellor.Types+	, module Propellor.Property+	, module Propellor.Property.Cmd+	, module Propellor.Property.List+	, module Propellor.Types.PrivData+	, module Propellor.PropAccum+	, module Propellor.Info+	, module Propellor.PrivData+	, module Propellor.Engine+	, module Propellor.Exception+	, module Propellor.Message+	, module Propellor.Location+	, module Propellor.Utilities++	-- * System modules+	, module System.Directory+	, module System.IO+	, module System.FilePath+	, module Data.Maybe+	, module Data.Either+	, module Control.Applicative+	, module Control.Monad+	, module Data.Monoid+	, module Control.Monad.IfElse+	, module Control.Monad.Reader+) where++import Propellor.Types+import Propellor.Property+import Propellor.Engine+import Propellor.Property.List+import Propellor.Property.Cmd+import Propellor.PrivData+import Propellor.Types.PrivData+import Propellor.Message+import Propellor.Exception+import Propellor.Info+import Propellor.PropAccum+import Propellor.Location+import Propellor.Utilities++import System.Directory+import System.IO+import System.FilePath+import Data.Maybe+import Data.Either+import Control.Applicative+import Control.Monad+import Data.Monoid+import Control.Monad.IfElse+import "mtl" Control.Monad.Reader
src/Propellor/Bootstrap.hs view
@@ -5,7 +5,7 @@ 	buildPropellor, ) where -import Propellor+import Propellor.Base  import System.Posix.Files import Data.List
src/Propellor/CmdLine.hs view
@@ -9,7 +9,7 @@ import System.PosixCompat import Network.Socket -import Propellor+import Propellor.Base import Propellor.Gpg import Propellor.Git import Propellor.Bootstrap@@ -27,10 +27,12 @@ 	, "  propellor --spin targethost [--via relayhost]" 	, "  propellor --add-key keyid" 	, "  propellor --rm-key keyid"-	, "  propellor --set field context"+	, "  propellor --list-fields" 	, "  propellor --dump field context" 	, "  propellor --edit field context"-	, "  propellor --list-fields"+	, "  propellor --set field context"+	, "  propellor --unset field context"+	, "  propellor --unset-unused" 	, "  propellor --merge" 	, "  propellor --build" 	, "  propellor --check"@@ -54,6 +56,7 @@ 	go ("--rm-key":k:[]) = return $ RmKey k 	go ("--set":f:c:[]) = withprivfield f c Set 	go ("--unset":f:c:[]) = withprivfield f c Unset+	go ("--unset-unused":[]) = return UnsetUnused 	go ("--dump":f:c:[]) = withprivfield f c Dump 	go ("--edit":f:c:[]) = withprivfield f c Edit 	go ("--list-fields":[]) = return ListFields@@ -98,6 +101,7 @@ 	go _ Check = return () 	go _ (Set field context) = setPrivData field context 	go _ (Unset field context) = unsetPrivData field context+	go _ (UnsetUnused) = unsetPrivDataUnused hostlist 	go _ (Dump field context) = dumpPrivData field context 	go _ (Edit field context) = editPrivData field context 	go _ ListFields = listPrivDataFields hostlist@@ -113,9 +117,10 @@ 	go _ Merge = mergeSpin 	go True cmdline@(Spin _ _) = buildFirst cmdline $ go False cmdline 	go True cmdline = updateFirst cmdline $ go False cmdline-	go False (Spin hs r) = do+	go False (Spin hs mrelay) = do 		commitSpin-		forM_ hs $ \hn -> withhost hn $ spin hn r+		forM_ hs $ \hn -> withhost hn $+			spin (maybe RegularSpin RelaySpin mrelay) hn 	go False cmdline@(SimpleRun hn) = buildFirst cmdline $ 		go False (Run hn) 	go False (Run hn) = ifM ((==) 0 <$> getRealUserID)
src/Propellor/Engine.hs view
@@ -7,6 +7,7 @@ 	ensureProperty, 	ensureProperties, 	fromHost,+	fromHost', 	onlyProcess, 	processChainOutput, ) where@@ -26,6 +27,7 @@ import Propellor.Message import Propellor.Exception import Propellor.Info+import Propellor.Property import Utility.Exception import Utility.PartialPrelude import Utility.Monad@@ -62,13 +64,6 @@ 	(ret, _s, _) <- runRWST (runWithHost (catchPropellor (a res))) host () 	return ret --- | For when code running in the Propellor monad needs to ensure a--- Property.------ This can only be used on a Property that has NoInfo.-ensureProperty :: Property NoInfo -> Propellor Result-ensureProperty = catchPropellor . propertySatisfy- -- | Ensures a list of Properties, with a display of each as it runs. ensureProperties :: [Property NoInfo] -> Propellor Result ensureProperties ps = ensure ps NoChange@@ -79,17 +74,19 @@ 		r <- actionMessageOn hn (propertyDesc p) (ensureProperty p) 		ensure ls (r <> rs) --- | Lifts an action into a different host.+-- | Lifts an action into the context of a different host. ----- > fromHost hosts "otherhost" getPubKey+-- > fromHost hosts "otherhost" Ssh.getHostPubKey fromHost :: [Host] -> HostName -> Propellor a -> Propellor (Maybe a) fromHost l hn getter = case findHost l hn of 	Nothing -> return Nothing-	Just h -> do-		(ret, _s, runlog) <- liftIO $-			runRWST (runWithHost getter) h ()-		tell runlog-		return (Just ret)+	Just h -> Just <$> fromHost' h getter++fromHost' :: Host -> Propellor a -> Propellor a+fromHost' h getter = do+	(ret, _s, runlog) <- liftIO $ runRWST (runWithHost getter) h ()+	tell runlog+	return ret  onlyProcess :: FilePath -> IO a -> IO a onlyProcess lockfile a = bracket lock unlock (const a)
src/Propellor/Git.hs view
@@ -1,6 +1,6 @@ module Propellor.Git where -import Propellor+import Propellor.Base import Propellor.PrivData.Paths import Propellor.Gpg import Utility.FileMode
src/Propellor/Info.hs view
@@ -101,3 +101,6 @@  hostAddresses :: HostName -> [Host] -> [IPAddr] hostAddresses hn hosts = maybe [] (getAddresses . hostInfo) (findHost hosts hn)++addHostInfo ::IsInfo v => Host -> v -> Host+addHostInfo h v = h { hostInfo = addInfo (hostInfo h) v }
+ src/Propellor/Location.hs view
@@ -0,0 +1,5 @@+module Propellor.Location where++-- | This is where propellor installs itself when deploying a host.+localdir :: FilePath+localdir = "/usr/local/propellor"
src/Propellor/PrivData.hs view
@@ -9,12 +9,14 @@ 	addPrivData, 	setPrivData, 	unsetPrivData,+	unsetPrivDataUnused, 	dumpPrivData, 	editPrivData, 	filterPrivData, 	listPrivDataFields, 	makePrivDataDir, 	decryptPrivData,+	readPrivData, 	PrivMap, 	PrivInfo, 	forceHostContext,@@ -158,8 +160,21 @@ unsetPrivData :: PrivDataField -> Context -> IO () unsetPrivData field context = do 	modifyPrivData $ M.delete (field, context)-	putStrLn "Private data unset."+	descUnset field context +descUnset :: PrivDataField -> Context -> IO ()+descUnset field context =+	putStrLn $ "Private data unset: " ++ show field ++ " " ++ show context++unsetPrivDataUnused :: [Host] -> IO ()+unsetPrivDataUnused hosts = do+	deleted <- modifyPrivData' $ \m ->+		let (keep, del) = M.partitionWithKey (\k _ -> k `M.member` usedby) m+		in (keep, M.keys del)+	mapM_ (uncurry descUnset) deleted+  where+	usedby = mkUsedByMap hosts+ dumpPrivData :: PrivDataField -> Context -> IO () dumpPrivData field context = do 	maybe (error "Requested privdata is not set.")@@ -199,15 +214,21 @@ 		, shellEscape context 		, intercalate ", " $ sort $ fromMaybe [] $ M.lookup k usedby 		]-	mkhostmap host mkv = M.fromList $ map (\(f, d, c) -> ((f, mkHostContext c (hostName host)), mkv d)) $-		S.toList $ fromPrivInfo $ getInfo $ hostInfo host-	usedby = M.unionsWith (++) $ map (\h -> mkhostmap h $ const [hostName h]) hosts+	usedby = mkUsedByMap hosts 	wantedmap = M.fromList $ zip (M.keys usedby) (repeat "")-	descmap = M.unions $ map (`mkhostmap` id) hosts+	descmap = M.unions $ map (`mkPrivDataMap` id) hosts 	section desc = putStrLn $ "\n" ++ desc 	showtable rows = do 		putStr $ unlines $ formatTable $ tableWithHeader header rows +mkUsedByMap :: [Host] -> M.Map (PrivDataField, Context) [HostName]+mkUsedByMap = M.unionsWith (++) . map (\h -> mkPrivDataMap h $ const [hostName h])++mkPrivDataMap :: Host -> (Maybe PrivDataSourceDesc -> a) -> M.Map (PrivDataField, Context) a+mkPrivDataMap host mkv = M.fromList $+	map (\(f, d, c) -> ((f, mkHostContext c (hostName host)), mkv d))+		(S.toList $ fromPrivInfo $ getInfo $ hostInfo host)+ setPrivDataTo :: PrivDataField -> Context -> PrivData -> IO () setPrivDataTo field context (PrivData value) = do 	modifyPrivData set@@ -216,16 +237,23 @@ 	set = M.insert (field, context) value  modifyPrivData :: (PrivMap -> PrivMap) -> IO ()-modifyPrivData f = do+modifyPrivData f = modifyPrivData' (\m -> (f m, ()))++modifyPrivData' :: (PrivMap -> (PrivMap, a)) -> IO a+modifyPrivData' f = do 	makePrivDataDir 	m <- decryptPrivData-	let m' = f m+	let (m', r) = f m 	gpgEncrypt privDataFile (show m') 	void $ boolSystem "git" [Param "add", File privDataFile]+	return r  decryptPrivData :: IO PrivMap-decryptPrivData = fromMaybe M.empty . readish <$> gpgDecrypt privDataFile+decryptPrivData = readPrivData <$> gpgDecrypt privDataFile +readPrivData :: String -> PrivMap+readPrivData = fromMaybe M.empty . readish+ makePrivDataDir :: IO () makePrivDataDir = createDirectoryIfMissing False privDataDir @@ -233,10 +261,10 @@ 	{ fromPrivInfo :: S.Set (PrivDataField, Maybe PrivDataSourceDesc, HostContext) } 	deriving (Eq, Ord, Show, Typeable, Monoid) --- PrivInfo is propigated out of containers, so that propellor can see which+-- PrivInfo is propagated out of containers, so that propellor can see which -- hosts need it. instance IsInfo PrivInfo where-	propigateInfo _ = True+	propagateInfo _ = True  -- | Sets the context of any privdata that uses HostContext to the -- provided name.
src/Propellor/PropAccum.hs view
@@ -2,11 +2,11 @@  module Propellor.PropAccum 	( host-	, props 	, PropAccum(..)+	, (&)+	, (&^) 	, (!)-	, PropList-	, propigateContainer+	, propagateContainer 	) where  import Data.Monoid@@ -25,64 +25,56 @@ host :: HostName -> Host host hn = Host hn [] mempty --- | Starts accumulating a list of properties.------ > propertyList "foo" $ props--- > 	& someproperty--- > 	! oldproperty--- > 	& otherproperty-props :: PropList-props = PropList []- -- | Something that can accumulate properties. class PropAccum h where 	-- | Adds a property.-	---	-- Can add Properties and RevertableProperties-	(&) :: IsProp p => h -> p -> h+	addProp :: IsProp p => h -> p -> h -	-- | Like (&), but adds the property at the front of the list.-	(&^) :: IsProp p => h -> p -> h+	-- | Like addProp, but adds the property at the front of the list.+	addPropFront :: IsProp p => h -> p -> h  	getProperties :: h -> [Property HasInfo] -instance PropAccum Host where-	(Host hn ps is) &  p = Host hn (ps ++ [toProp p])-		(is <> getInfoRecursive p)-	(Host hn ps is) &^ p = Host hn (toProp p : ps)-		(getInfoRecursive p <> is)-	getProperties = hostProperties--data PropList = PropList [Property HasInfo]+-- | Adds a property to a `Host` or other `PropAccum`+--+-- Can add Properties and RevertableProperties+(&) :: (PropAccum h, IsProp p) => h -> p -> h+(&) = addProp -instance PropAccum PropList where-	PropList l &  p = PropList (toProp p : l)-	PropList l &^ p = PropList (l ++ [toProp p])-	getProperties (PropList l) = reverse l+-- | Adds a property before any other properties.+(&^) :: (PropAccum h, IsProp p) => h -> p -> h+(&^) = addPropFront  -- | Adds a property in reverted form. (!) :: PropAccum h => h -> RevertableProperty -> h h ! p = h & revert p -infixl 1 &^ infixl 1 &+infixl 1 &^ infixl 1 ! +instance PropAccum Host where+	(Host hn ps is) `addProp`  p = Host hn (ps ++ [toProp p])+		(is <> getInfoRecursive p)+	(Host hn ps is) `addPropFront` p = Host hn (toProp p : ps)+		(getInfoRecursive p <> is)+	getProperties = hostProperties+ -- | Adjust the provided Property, adding to its -- propertyChidren the properties of the provided container. --  -- The Info of the propertyChildren is adjusted to only include --- info that should be propigated out to the Property.+-- info that should be propagated out to the Property. -- -- Any PrivInfo that uses HostContext is adjusted to use the name -- of the container as its context.-propigateContainer+propagateContainer 	:: (PropAccum container) 	=> String 	-> container 	-> Property HasInfo 	-> Property HasInfo-propigateContainer containername c prop = infoProperty+propagateContainer containername c prop = infoProperty 	(propertyDesc prop) 	(propertySatisfy prop) 	(propertyInfo prop)@@ -91,6 +83,6 @@ 	hostprops = map go $ getProperties c 	go p =  		let i = mapInfo (forceHostContext containername)-			(propigatableInfo (propertyInfo p))+			(propagatableInfo (propertyInfo p)) 		    cs = map go (propertyChildren p) 		in infoProperty (propertyDesc p) (propertySatisfy p) i cs
src/Propellor/Property.hs view
@@ -1,7 +1,31 @@ {-# LANGUAGE PackageImports #-} {-# LANGUAGE FlexibleContexts #-} -module Propellor.Property where+module Propellor.Property (+	-- * Property combinators+	  requires+	, before+	, onChange+	, onChangeFlagOnFail+	, flagFile+	, flagFile'+	, check+	, fallback+	, trivial+	, revert+	-- * Property descriptions+	, describe+	, (==>)+	-- * Constructing properties+	, Propellor+	, property+	, ensureProperty+	, withOS+	, makeChange+	, noChange+	, doNothing+	, endAction+) where  import System.Directory import System.FilePath@@ -12,6 +36,7 @@  import Propellor.Types import Propellor.Info+import Propellor.Exception import Utility.Monad  -- | Constructs a Property, from a description and an action to run to@@ -39,6 +64,18 @@ 				writeFile flagfile "" 		return r +-- | Indicates that the first property depends on the second,+-- so before the first is ensured, the second must be ensured.+requires :: Combines x y => x -> y -> CombinedType x y+requires = (<<>>)++-- | Combines together two properties, resulting in one property+-- that ensures the first, and if the first succeeds, ensures the second.+--+-- The combined property uses the description of the first property.+before :: (IsProp x, Combines y x, IsProp (CombinedType y x)) => x -> y -> CombinedType y x+before x y = (y `requires` x) `describe` getDesc x+ -- | Whenever a change has to be made for a Property, causes a hook -- Property to also be run, but not otherwise. onChange@@ -88,11 +125,22 @@ 		writeFile flagfile "" 	removeFlagFile = whenM (doesFileExist flagfile) $ removeFile flagfile +-- | Changes the description of a property.+describe :: IsProp p => p -> Desc -> p+describe = setDesc+ -- | Alias for @flip describe@ (==>) :: IsProp (Property i) => Desc -> Property i -> Property i (==>) = flip describe infixl 1 ==> +-- | For when code running in the Propellor monad needs to ensure a+-- Property.+--+-- This can only be used on a Property that has NoInfo.+ensureProperty :: Property NoInfo -> Propellor Result+ensureProperty = catchPropellor . propertySatisfy+ -- | Makes a Property only need to do anything when a test succeeds. check :: IO Bool -> Property i -> Property i check c p = adjustPropertySatisfy p $ \satisfy -> ifM (liftIO c)@@ -125,11 +173,16 @@ -- | Makes a property that is satisfied differently depending on the host's -- operating system.  ----- Note that the operating system may not be declared for some hosts.+-- Note that the operating system may not be declared for all hosts.+--+-- > myproperty = withOS "foo installed" $ \o -> case o of+-- > 	(Just (System (Debian suite) arch)) -> ...+-- > 	(Just (System (Ubuntu release) arch)) -> ...+-- >	Nothing -> ... withOS :: Desc -> (Maybe System -> Propellor Result) -> Property NoInfo withOS desc a = property desc $ a =<< getOS --- | Undoes the effect of a property.+-- | Undoes the effect of a RevertableProperty. revert :: RevertableProperty -> RevertableProperty revert (RevertableProperty p1 p2) = RevertableProperty p2 p1 @@ -142,6 +195,7 @@ doNothing :: Property NoInfo doNothing = property "noop property" noChange --- | Registers an action that should be run at the very end,+-- | Registers an action that should be run at the very end, after+-- propellor has checks all the properties of a host. endAction :: Desc -> (Result -> Propellor Result) -> Propellor () endAction desc a = tell [EndAction desc a]
src/Propellor/Property/Aiccu.hs view
@@ -9,7 +9,7 @@ 	hasConfig, ) where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Service as Service import qualified Propellor.Property.File as File
src/Propellor/Property/Apache.hs view
@@ -1,10 +1,33 @@ module Propellor.Property.Apache where -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Service as Service +installed :: Property NoInfo+installed = Apt.installed ["apache2"]++restarted :: Property NoInfo+restarted = Service.restarted "apache2"++reloaded :: Property NoInfo+reloaded = Service.reloaded "apache2"++-- | A basic virtual host, publishing a directory, and logging to+-- the combined apache log file.+virtualHost :: HostName -> Port -> FilePath -> RevertableProperty+virtualHost hn (Port p) docroot = siteEnabled hn+	[ "<VirtualHost *:"++show p++">"+	, "ServerName "++hn++":"++show p+	, "DocumentRoot " ++ docroot+	, "ErrorLog /var/log/apache2/error.log"+	, "LogLevel warn"+	, "CustomLog /var/log/apache2/access.log combined"+	, "ServerSignature On"+	, "</VirtualHost>"+	]+ type ConfigFile = [String]  siteEnabled :: HostName -> ConfigFile -> RevertableProperty@@ -19,13 +42,16 @@ 				`requires` installed 				`onChange` reloaded 		]-	disable = combineProperties-		("apache site disabled " ++ hn) -		(map File.notPresent (siteCfg hn))+	disable = siteDisabled hn+	isenabled = boolSystem "a2query" [Param "-q", Param "-s", Param hn]++siteDisabled :: HostName -> Property NoInfo+siteDisabled hn = combineProperties+	("apache site disabled " ++ hn) +	(map File.notPresent (siteCfg hn)) 		`onChange` cmdProperty "a2dissite" ["--quiet", hn] 		`requires` installed 		`onChange` reloaded-	isenabled = boolSystem "a2query" [Param "-q", Param "-s", Param hn]  siteAvailable :: HostName -> ConfigFile -> Property NoInfo siteAvailable hn cf = combineProperties ("apache site available " ++ hn) $@@ -48,6 +74,16 @@ 			`onChange` reloaded 	isenabled = boolSystem "a2query" [Param "-q", Param "-m", Param modname] +-- | Make apache listen on the specified ports.+--+-- Note that ports are also specified inside a site's config file,+-- so that also needs to be changed.+listenPorts :: [Port] -> Property NoInfo+listenPorts ps = "/etc/apache2/ports.conf" `File.hasContent` map portline ps+	`onChange` restarted+  where+	portline (Port n) = "Listen " ++ show n+ -- This is a list of config files because different versions of apache -- use different filenames. Propellor simply writes them all. siteCfg :: HostName -> [FilePath]@@ -57,15 +93,6 @@ 	-- Debian 2.4+ 	, "/etc/apache2/sites-available/" ++ hn ++ ".conf" 	] --installed :: Property NoInfo-installed = Apt.installed ["apache2"]--restarted :: Property NoInfo-restarted = Service.restarted "apache2"--reloaded :: Property NoInfo-reloaded = Service.reloaded "apache2"  -- | Configure apache to use SNI to differentiate between -- https hosts.
src/Propellor/Property/Apt.hs view
@@ -8,7 +8,7 @@ import System.IO import Control.Monad -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File import qualified Propellor.Property.Service as Service import Propellor.Property.File (Line)@@ -297,3 +297,11 @@ cacheCleaned :: Property NoInfo cacheCleaned = trivial $ cmdProperty "apt-get" ["clean"] 	`describe` "apt cache cleaned"++-- | Add a foreign architecture to dpkg and apt.+hasForeignArch :: String -> Property NoInfo+hasForeignArch arch = check notAdded add+	`describe` ("dpkg has foreign architecture " ++ arch)+  where+	notAdded = (not . elem arch . lines) <$> readProcess "dpkg" ["--print-foreign-architectures"]+	add = cmdProperty "dpkg" ["--add-architecture", arch] `before` update
src/Propellor/Property/Chroot.hs view
@@ -1,18 +1,21 @@-{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleContexts, GADTs #-}  module Propellor.Property.Chroot ( 	Chroot(..),-	BuilderConf(..),+	ChrootBootstrapper(..),+	Debootstrapped(..), 	debootstrapped,+	bootstrapped, 	provisioned, 	-- * Internal use 	provisioned',-	propigateChrootInfo,+	propagateChrootInfo, 	propellChroot, 	chain,+	chrootSystem, ) where -import Propellor+import Propellor.Base import Propellor.Types.CmdLine import Propellor.Types.Chroot import Propellor.Types.Info@@ -26,33 +29,57 @@ import Data.List.Utils import System.Posix.Directory -data Chroot = Chroot FilePath System BuilderConf Host-	deriving (Show)+-- | Specification of a chroot. Normally you'll use `debootstrapped` or+-- `bootstrapped` to construct a Chroot value.+data Chroot where+	Chroot :: ChrootBootstrapper b => FilePath -> System -> b -> Host -> Chroot -data BuilderConf-	= UsingDeboostrap Debootstrap.DebootstrapConfig-	deriving (Show)+chrootSystem :: Chroot -> System+chrootSystem (Chroot _ system _ _) = system +instance Show Chroot where+	show (Chroot loc system _ _) = "Chroot " ++ loc ++ " " ++ show system+ instance PropAccum Chroot where-	(Chroot l s c h) & p = Chroot l s c (h & p)-	(Chroot l s c h) &^ p = Chroot l s c (h &^ p)+	(Chroot l s c h) `addProp` p = Chroot l s c (h & p)+	(Chroot l s c h) `addPropFront` p = Chroot l s c (h `addPropFront` p) 	getProperties (Chroot _ _ _ h) = hostProperties h +-- | Class of things that can do initial bootstrapping of an operating+-- System in a chroot.+class ChrootBootstrapper b where+	-- | Do initial bootstrapping of an operating system in a chroot.+	-- If the operating System is not supported, return Nothing.+	buildchroot :: b -> System -> FilePath -> Maybe (Property HasInfo)++-- | Use to bootstrap a chroot with debootstrap.+data Debootstrapped = Debootstrapped Debootstrap.DebootstrapConfig++instance ChrootBootstrapper Debootstrapped where+	buildchroot (Debootstrapped cf) system loc = case system of+		(System (Debian _) _) -> Just debootstrap+		(System (Ubuntu _) _) -> Just debootstrap+	  where+		debootstrap = Debootstrap.built loc system cf+ -- | Defines a Chroot at the given location, built with debootstrap. -- -- Properties can be added to configure the Chroot. -- -- > debootstrapped (System (Debian Unstable) "amd64") Debootstrap.BuildD "/srv/chroot/ghc-dev"--- >    & Apt.installed ["ghc", "haskell-platform"]--- >	& ...+-- > 	& Apt.installed ["ghc", "haskell-platform"]+-- > 	& ... debootstrapped :: System -> Debootstrap.DebootstrapConfig -> FilePath -> Chroot-debootstrapped system conf location = case system of-	(System (Debian _) _) -> mk-	(System (Ubuntu _) _) -> mk+debootstrapped system conf = bootstrapped system (Debootstrapped conf)++-- | Defines a Chroot at the given location, bootstrapped with the+-- specified ChrootBootstrapper.+bootstrapped :: ChrootBootstrapper b => System -> b -> FilePath -> Chroot+bootstrapped system bootstrapper location =+	Chroot location system bootstrapper h+		& os system   where 	h = Host location [] mempty-	mk = Chroot location system (UsingDeboostrap conf) h-		& os system  -- | Ensures that the chroot exists and is provisioned according to its -- properties.@@ -61,29 +88,29 @@ -- is first unmounted. Note that it does not ensure that any processes -- that might be running inside the chroot are stopped. provisioned :: Chroot -> RevertableProperty-provisioned c = provisioned' (propigateChrootInfo c) c False+provisioned c = provisioned' (propagateChrootInfo c) c False  provisioned' :: (Property HasInfo -> Property HasInfo) -> Chroot -> Bool -> RevertableProperty-provisioned' propigator c@(Chroot loc system builderconf _) systemdonly =-	(propigator $ go "exists" setup)+provisioned' propigator c@(Chroot loc system bootstrapper _) systemdonly =+	(propigator $ propertyList (chrootDesc c "exists") [setup]) 		<!>-	(go "removed" teardown)+	(propertyList (chrootDesc c "removed") [teardown])   where-	go desc a = propertyList (chrootDesc c desc) [a]- 	setup = propellChroot c (inChrootProcess (not systemdonly) c) systemdonly 		`requires` toProp built 	-	built = case (system, builderconf) of-		((System (Debian _) _), UsingDeboostrap cf) -> debootstrap cf-		((System (Ubuntu _) _), UsingDeboostrap cf) -> debootstrap cf+	built = case buildchroot bootstrapper system loc of+		Just p -> p+		Nothing -> cantbuild -	debootstrap = Debootstrap.built loc system+	cantbuild = infoProperty (chrootDesc c "built") (error $ "cannot bootstrap " ++ show system ++ " using supplied ChrootBootstrapper") mempty [] -	teardown = toProp (revert built)+	teardown = check (not <$> unpopulated loc) $+		property ("removed " ++ loc) $+			makeChange (removeChroot loc) -propigateChrootInfo :: (IsProp (Property i)) => Chroot -> Property i -> Property HasInfo-propigateChrootInfo c@(Chroot location _ _ _) p = propigateContainer location c p'+propagateChrootInfo :: (IsProp (Property i)) => Chroot -> Property i -> Property HasInfo+propagateChrootInfo c@(Chroot location _ _ _) p = propagateContainer location c p'   where 	p' = infoProperty 		(propertyDesc p)
src/Propellor/Property/Chroot/Util.hs view
@@ -2,11 +2,14 @@  import Propellor.Property.Mount +import Utility.Exception import Utility.Env+import Utility.Directory+ import Control.Applicative import System.Directory --- When chrooting, it's useful to ensure that PATH has all the standard+-- | When chrooting, it's useful to ensure that PATH has all the standard -- directories in it. This adds those directories to whatever PATH is -- already set. standardPathEnv :: IO [(String, String)]@@ -18,9 +21,13 @@ stdPATH :: String stdPATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" --- Removes the contents of a chroot. First, unmounts any filesystems+-- | Removes the contents of a chroot. First, unmounts any filesystems -- mounted within it. removeChroot :: FilePath -> IO () removeChroot c = do 	unmountBelow c 	removeDirectoryRecursive c++-- | Returns true if a chroot directory is empty.+unpopulated :: FilePath -> IO Bool+unpopulated d = null <$> catchDefaultIO [] (dirContents d)
src/Propellor/Property/ConfFile.hs view
@@ -12,7 +12,7 @@ 	lacksIniSection, ) where -import Propellor+import Propellor.Base import Propellor.Property.File  import Data.List (isPrefixOf, foldl')
src/Propellor/Property/Cron.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Cron where -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt import Propellor.Bootstrap
src/Propellor/Property/DebianMirror.hs view
@@ -7,7 +7,7 @@ 	, mirrorCdn 	) where -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Cron as Cron
src/Propellor/Property/Debootstrap.hs view
@@ -10,7 +10,7 @@ 	programPath, ) where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import Propellor.Property.Chroot.Util import Utility.Path@@ -48,19 +48,8 @@ -- -- The System can be any OS and architecture that debootstrap -- and the kernel support.------ Reverting this property deletes the chroot and all its contents.--- Anything mounted under the filesystem is first unmounted.------ Note that reverting this property does not stop any processes--- currently running in the chroot.-built :: FilePath -> System -> DebootstrapConfig -> RevertableProperty-built target system config = built' (toProp installed) target system config <!> teardown-  where-	teardown = check (not <$> unpopulated target) teardownprop-	-	teardownprop = property ("removed debootstrapped " ++ target) $-		makeChange (removeChroot target)+built :: FilePath -> System -> DebootstrapConfig -> Property HasInfo+built target system config = built' (toProp installed) target system config  built' :: (Combines (Property NoInfo) (Property i)) => Property i -> FilePath -> System -> DebootstrapConfig -> Property (CInfo NoInfo i) built' installprop target system@(System _ arch) config = @@ -100,9 +89,6 @@ 		, return False 		) 	-unpopulated :: FilePath -> IO Bool-unpopulated d = null <$> catchDefaultIO [] (dirContents d)	- extractSuite :: System -> Maybe String extractSuite (System (Debian s) _) = Just $ Apt.showSuite s extractSuite (System (Ubuntu r) _) = Just r
src/Propellor/Property/DiskImage.hs view
@@ -36,7 +36,7 @@ 	noFinalization, ) where -import Propellor+import Propellor.Base import Propellor.Property.Chroot (Chroot) import Propellor.Property.Chroot.Util (removeChroot) import qualified Propellor.Property.Chroot as Chroot
src/Propellor/Property/Dns.hs view
@@ -13,7 +13,7 @@ 	genZone, ) where -import Propellor+import Propellor.Base import Propellor.Types.Dns import Propellor.Types.Info import Propellor.Property.File@@ -524,7 +524,7 @@ genSSHFP :: Domain -> Host -> Propellor [(BindDomain, Record)] genSSHFP domain h = concatMap mk . concat <$> (gen =<< get)   where-	get = fromHost [h] hostname Ssh.getPubKey+	get = fromHost [h] hostname Ssh.getHostPubKey 	gen = liftIO . mapM genSSHFP' . M.elems . fromMaybe M.empty 	mk r = mapMaybe (\d -> if inDomain domain d then Just (d, r) else Nothing) 		(AbsDomain hostname : cnames)
src/Propellor/Property/DnsSec.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.DnsSec where -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File  -- | Puts the DNSSEC key files in place from PrivData.@@ -71,7 +71,7 @@ 		, "-3", salt 		-- The serial number needs to be increased each time the 		-- zone is resigned, even if there are no other changes,-		-- so that it will propigate to secondaries. So, use the+		-- so that it will propagate to secondaries. So, use the 		-- unixtime serial format. 		, "-N", "unixtime" 		, "-o", domain
src/Propellor/Property/Docker.hs view
@@ -45,7 +45,7 @@ 	chain, ) where -import Propellor hiding (init)+import Propellor.Base hiding (init) import Propellor.Types.Docker import Propellor.Types.Container import Propellor.Types.CmdLine@@ -97,8 +97,8 @@ 	getImageName (Container i _) = i  instance PropAccum Container where-	(Container i h) & p = Container i (h & p)-	(Container i h) &^ p = Container i (h &^ p)+	(Container i h) `addProp` p = Container i (h `addProp` p)+	(Container i h) `addPropFront` p = Container i (h `addPropFront` p) 	getProperties (Container _ h) = hostProperties h  -- | Defines a Container with a given name, image, and properties.@@ -119,13 +119,13 @@ -- propellor inside the container. -- -- When the container's Properties include DNS info, such as a CNAME,--- that is propigated to the Info of the Host it's docked in.+-- that is propagated to the Info of the Host it's docked in. -- -- Reverting this property ensures that the container is stopped and -- removed. docked :: Container -> RevertableProperty docked ctr@(Container _ h) =-	(propigateContainerInfo ctr (go "docked" setup))+	(propagateContainerInfo ctr (go "docked" setup)) 		<!> 	(go "undocked" teardown)   where@@ -170,8 +170,8 @@ 	pulled = Cmd.cmdProperty dockercmd ["pull", imageIdentifier image] 	image = getImageName ctr -propigateContainerInfo :: (IsProp (Property i)) => Container -> Property i -> Property HasInfo-propigateContainerInfo ctr@(Container _ h) p = propigateContainer cn ctr p'+propagateContainerInfo :: (IsProp (Property i)) => Container -> Property i -> Property HasInfo+propagateContainerInfo ctr@(Container _ h) p = propagateContainer cn ctr p'   where 	p' = infoProperty 		(propertyDesc p)
src/Propellor/Property/File.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.File where -import Propellor+import Propellor.Base import Utility.FileMode  import System.Posix.Files@@ -82,12 +82,11 @@ 		let new = unlines (a (lines old)) 		if old == new 			then noChange-			else makeChange $ viaTmp updatefile f new+			else makeChange $ updatefile new `viaStableTmp` f 	go False = makeChange $ writer f (unlines $ a []) -	-- viaTmp makes the temp file mode 600. 	-- Replicate the original file's owner and mode.-	updatefile f' content = do+	updatefile content f' = do 		writer f' content 		s <- getFileStatus f 		setFileMode f' (fileMode s)@@ -98,6 +97,33 @@ dirExists d = check (not <$> doesDirectoryExist d) $ property (d ++ " exists") $ 	makeChange $ createDirectoryIfMissing True d +-- | The location that a symbolic link points to.+newtype LinkTarget = LinkTarget FilePath++-- | Creates or atomically updates a symbolic link.+--+-- Does not overwrite regular files or directories.+isSymlinkedTo :: FilePath -> LinkTarget -> Property NoInfo+link `isSymlinkedTo` (LinkTarget target) = property desc $+	go =<< (liftIO $ tryIO $ getSymbolicLinkStatus link)+  where+	desc = link ++ " is symlinked to " ++ target+	go (Right stat) =+		if isSymbolicLink stat+			then checkLink+			else nonSymlinkExists+	go (Left _) = makeChange $ createSymbolicLink target link++	nonSymlinkExists = do+		warningMessage $ link ++ " exists and is not a symlink"+		return FailedChange+	checkLink = do+		target' <- liftIO $ readSymbolicLink link+		if target == target'+			then noChange+			else makeChange updateLink+	updateLink = createSymbolicLink target `viaStableTmp` link+ -- | Ensures that a file/dir has the specified owner and group. ownerGroup :: FilePath -> User -> Group -> Property NoInfo ownerGroup f (User owner) (Group group) = property (f ++ " owner " ++ og) $ do@@ -113,3 +139,27 @@ mode f v = property (f ++ " mode " ++ show v) $ do 	liftIO $ modifyFileMode f (const v) 	noChange++-- | A temp file to use when writing new content for a file.+--+-- This is a stable name so it can be removed idempotently.+--+-- It ends with "~" so that programs that read many config files from a+-- directory will treat it as an editor backup file, and not read it.+stableTmpFor :: FilePath -> FilePath+stableTmpFor f = f ++ ".propellor-new~"++-- | Creates/updates a file atomically, running the action to create the+-- stable tmp file, and then renaming it into place.+viaStableTmp :: (MonadMask m, MonadIO m) => (FilePath -> m ()) -> FilePath -> m ()+viaStableTmp a f = bracketIO setup cleanup go+  where+	setup = do+		createDirectoryIfMissing True (takeDirectory f)+		let tmpfile = stableTmpFor f+		nukeFile tmpfile+		return tmpfile+	cleanup tmpfile = tryIO $ removeFile tmpfile+	go tmpfile = do+		a tmpfile+		liftIO $ rename tmpfile f
src/Propellor/Property/Firewall.hs view
@@ -16,7 +16,7 @@ import Data.Char import Data.List -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Network as Network 
src/Propellor/Property/Git.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Git where -import Propellor+import Propellor.Base import Propellor.Property.File import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Service as Service
src/Propellor/Property/Gpg.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Gpg where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import Utility.FileSystemEncoding 
src/Propellor/Property/Group.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Group where -import Propellor+import Propellor.Base  type GID = Int 
src/Propellor/Property/Grub.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Grub where -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt 
src/Propellor/Property/HostingProvider/CloudAtCost.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.HostingProvider.CloudAtCost where -import Propellor+import Propellor.Base import qualified Propellor.Property.Hostname as Hostname import qualified Propellor.Property.File as File import qualified Propellor.Property.User as User
src/Propellor/Property/HostingProvider/DigitalOcean.hs view
@@ -2,7 +2,7 @@ 	distroKernel ) where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.File as File import qualified Propellor.Property.Reboot as Reboot
src/Propellor/Property/HostingProvider/Linode.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.HostingProvider.Linode where -import Propellor+import Propellor.Base import qualified Propellor.Property.Grub as Grub import qualified Propellor.Property.File as File import Utility.FileMode
src/Propellor/Property/Hostname.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Hostname where -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File  import Data.List
src/Propellor/Property/Journald.hs view
@@ -1,5 +1,6 @@ module Propellor.Property.Journald where-import Propellor++import Propellor.Base import qualified Propellor.Property.Systemd as Systemd import Utility.DataUnits 
src/Propellor/Property/Kerberos.hs view
@@ -4,7 +4,7 @@  import Utility.Process -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.File as File import Propellor.Property.User
src/Propellor/Property/LightDM.hs view
@@ -4,7 +4,7 @@  module Propellor.Property.LightDM where -import Propellor+import Propellor.Base import qualified Propellor.Property.ConfFile as ConfFile  -- | Configures LightDM to skip the login screen and autologin as a user.
src/Propellor/Property/List.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE FlexibleInstances #-}  module Propellor.Property.List (+	props, 	PropertyList(..), 	PropertyListType, ) where@@ -12,21 +13,32 @@  import Data.Monoid +-- | Starts accumulating a list of properties.+--+-- > propertyList "foo" $ props+-- > 	& someproperty+-- > 	! oldproperty+-- > 	& otherproperty+props :: PropList+props = PropList []++data PropList = PropList [Property HasInfo]++instance PropAccum PropList where+	PropList l `addProp` p = PropList (toProp p : l)+	PropList l `addPropFront` p = PropList (l ++ [toProp p])+	getProperties (PropList l) = reverse l+ class PropertyList l where 	-- | Combines a list of properties, resulting in a single property 	-- that when run will run each property in the list in turn, 	-- and print out the description of each as it's run. Does not stop-	-- on failure; does propigate overall success/failure.+	-- on failure; does propagate overall success/failure. 	-- 	-- Note that Property HasInfo and Property NoInfo are not the same 	-- type, and so cannot be mixed in a list. To make a list of 	-- mixed types, which can also include RevertableProperty,-	-- use `props`:-	---	-- > propertyList "foo" $ props-	-- >	& someproperty-	-- >	! oldproperty-	-- > 	& otherproperty+	-- use `props` 	propertyList :: Desc -> l -> Property (PropertyListType l)  	-- | Combines a list of properties, resulting in one property that
src/Propellor/Property/Logcheck.hs view
@@ -9,7 +9,7 @@ 	installed, ) where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.File as File 
src/Propellor/Property/Mount.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Mount where -import Propellor+import Propellor.Base import Utility.Path  type FsType = String -- ^ type of filesystem to mount ("auto" to autodetect)
src/Propellor/Property/Network.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Network where -import Propellor+import Propellor.Base import Propellor.Property.File  type Interface = String
src/Propellor/Property/Nginx.hs view
@@ -2,28 +2,21 @@  module Propellor.Property.Nginx where -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Service as Service-import System.Posix.Files  type ConfigFile = [String]  siteEnabled :: HostName -> ConfigFile -> RevertableProperty siteEnabled hn cf = enable <!> disable   where-	enable = check test prop+	enable = siteVal hn `File.isSymlinkedTo` siteValRelativeCfg hn 		`describe` ("nginx site enabled " ++ hn) 		`requires` siteAvailable hn cf 		`requires` installed 		`onChange` reloaded-	  where-		test = not <$> doesFileExist (siteVal hn)-		prop = property "nginx site in place" $ makeChange $-			createSymbolicLink target dir-		target = siteValRelativeCfg hn-		dir = siteVal hn 	disable = trivial $ File.notPresent (siteVal hn) 		`describe` ("nginx site disable" ++ hn) 		`requires` installed@@ -41,8 +34,8 @@ siteVal :: HostName -> FilePath siteVal hn = "/etc/nginx/sites-enabled/" ++ hn -siteValRelativeCfg :: HostName -> FilePath-siteValRelativeCfg hn = "../sites-available/" ++ hn+siteValRelativeCfg :: HostName -> File.LinkTarget+siteValRelativeCfg hn = File.LinkTarget ("../sites-available/" ++ hn)  installed :: Property NoInfo installed = Apt.installed ["nginx"]
src/Propellor/Property/OS.hs view
@@ -7,7 +7,7 @@ 	oldOSRemoved, ) where -import Propellor+import Propellor.Base import qualified Propellor.Property.Debootstrap as Debootstrap import qualified Propellor.Property.Ssh as Ssh import qualified Propellor.Property.Network as Network
src/Propellor/Property/Obnam.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Obnam where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Cron as Cron import qualified Propellor.Property.Gpg as Gpg
src/Propellor/Property/OpenId.hs view
@@ -1,27 +1,47 @@ module Propellor.Property.OpenId where -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt-import qualified Propellor.Property.Service as Service+import qualified Propellor.Property.Apache as Apache  import Data.List -providerFor :: [User] -> String -> Property HasInfo-providerFor users baseurl = propertyList desc $ map toProp-	[ Apt.serviceInstalledRunning "apache2"-	, Apt.installed ["simpleid"]-		`onChange` Service.restarted "apache2"-	, File.fileProperty (desc ++ " configured")+-- | Openid provider, using the simpleid PHP CGI, with apache.+--+-- Runs on usual port by default. When a nonstandard port is specified,+-- apache is limited to listening only on that port. Warning: Specifying+-- a port won't compose well with other apache properties on the same+-- host.+--+-- It's probably a good idea to put this property inside a docker or+-- systemd-nspawn container.+providerFor :: [User] -> HostName -> Maybe Port -> Property HasInfo+providerFor users hn mp = propertyList desc $ props+	& Apt.serviceInstalledRunning "apache2"+	& apacheconfigured+	& Apt.installed ["simpleid"]+		`onChange` Apache.restarted+	& File.fileProperty (desc ++ " configured") 		(map setbaseurl) "/etc/simpleid/config.inc"-	] ++ map identfile users+	& propertyList desc (map identfile users)   where+	baseurl = hn ++ case mp of+		Nothing -> ""+		Just (Port p) -> ':' : show p 	url = "http://"++baseurl++"/simpleid" 	desc = "openid provider " ++ url 	setbaseurl l 		| "SIMPLEID_BASE_URL" `isInfixOf` l =  			"define('SIMPLEID_BASE_URL', '"++url++"');" 		| otherwise = l+	+	apacheconfigured = case mp of+		Nothing -> toProp $+			Apache.virtualHost hn (Port 80) "/var/www/html"+		Just p -> propertyList desc $ props+			& Apache.listenPorts [p]+			& Apache.virtualHost hn p "/var/www/html" 	 	-- the identities directory controls access, so open up 	-- file mode
src/Propellor/Property/Parted.hs view
@@ -21,7 +21,7 @@ 	installed, ) where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Partition as Partition import Utility.DataUnits
src/Propellor/Property/Partition.hs view
@@ -2,7 +2,7 @@  module Propellor.Property.Partition where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt  -- | Filesystems etc that can be used for a partition.
src/Propellor/Property/Postfix.hs view
@@ -2,7 +2,7 @@  module Propellor.Property.Postfix where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.File as File import qualified Propellor.Property.Service as Service
src/Propellor/Property/Prosody.hs view
@@ -2,11 +2,10 @@  module Propellor.Property.Prosody where -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Service as Service-import System.Posix.Files  type ConfigFile = [String] @@ -15,18 +14,16 @@ confEnabled :: Conf -> ConfigFile -> RevertableProperty confEnabled conf cf = enable <!> disable   where-	enable = check test prop+	enable = dir `File.isSymlinkedTo` target 		`describe` ("prosody conf enabled " ++ conf) 		`requires` confAvailable conf cf 		`requires` installed 		`onChange` reloaded 	  where-		test = not <$> doesFileExist (confValPath conf)-		prop = property "prosody conf in place" $ makeChange $-			createSymbolicLink target dir 		target = confValRelativePath conf 		dir = confValPath conf-		confValRelativePath conf' = "../conf.avail" </> conf' <.> "cfg.lua"+		confValRelativePath conf' = File.LinkTarget $+			"../conf.avail" </> conf' <.> "cfg.lua" 	disable = trivial $ File.notPresent (confValPath conf) 		`describe` ("prosody conf disabled " ++ conf) 		`requires` installed
src/Propellor/Property/Reboot.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Reboot where -import Propellor+import Propellor.Base  now :: Property NoInfo now = cmdProperty "reboot" []
src/Propellor/Property/Rsync.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Rsync where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt  type Src = FilePath
src/Propellor/Property/Scheduled.hs view
@@ -9,7 +9,7 @@ 	, YearDay 	) where -import Propellor+import Propellor.Base import Utility.Scheduled  import Data.Time.Clock
src/Propellor/Property/Service.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Service where -import Propellor+import Propellor.Base  type ServiceName = String 
src/Propellor/Property/SiteSpecific/Branchable.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.SiteSpecific.Branchable where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.File as File import qualified Propellor.Property.User as User@@ -50,7 +50,9 @@ 		, "lru-size = 128" 		] 	& Gpg.keyImported (Gpg.GpgKeyId obnamkey) (User "root")-	& Ssh.keyImported SshRsa (User "root") (Context "branchable.com")+	& Ssh.userKeys (User "root") (Context "branchable.com")+		[ (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC2PqTSupwncqeffNwZQXacdEWp7L+TxllIxH7WjfRMb3U74mQxWI0lwqLVW6Fox430DvhSqF1y5rJBvTHh4i49Tc9lZ7mwAxA6jNOP6bmdfteaKKYmUw5qwtJW0vISBFu28qBO11Nq3uJ1D3Oj6N+b3mM/0D3Y3NoGgF8+2dLdi81u9+l6AQ5Jsnozi2Ni/Osx2oVGZa+IQDO6gX8VEP4OrcJFNJe8qdnvItcGwoivhjbIfzaqNNvswKgGzhYLOAS5KT8HsjvIpYHWkyQ5QUX7W/lqGSbjP+6B8C3tkvm8VLXbmaD+aSkyCaYbuoXC2BoJdS7Jh8phKMwPJmdYVepn")+		] 	& Ssh.knownHost hosts "eubackup.kitenet.net" (User "root") 	& Ssh.knownHost hosts "usw-s002.rsync.net" (User "root") 
src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs view
@@ -2,7 +2,7 @@  module Propellor.Property.SiteSpecific.GitAnnexBuilder where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.User as User import qualified Propellor.Property.Cron as Cron
src/Propellor/Property/SiteSpecific/GitHome.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.SiteSpecific.GitHome where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import Propellor.Property.User 
src/Propellor/Property/SiteSpecific/IABak.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.SiteSpecific.IABak where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Git as Git import qualified Propellor.Property.Cron as Cron@@ -26,7 +26,7 @@ gitServer knownhosts = propertyList "iabak git server" $ props 	& Git.cloned (User "root") repo "/usr/local/IA.BAK" (Just "server") 	& Git.cloned (User "root") repo "/usr/local/IA.BAK/client" (Just "master")-	& Ssh.keyImported SshRsa (User "root") (Context "IA.bak.users.git")+	& Ssh.userKeys (User "root") (Context "IA.bak.users.git") sshKeys 	& Ssh.knownHost knownhosts "gitlab.com" (User "root") 	& Git.cloned (User "root") userrepo "/usr/local/IA.BAK/pubkeys" (Just "master") 	& Apt.serviceInstalledRunning "apache2"@@ -45,7 +45,7 @@ registrationServer :: [Host] -> Property HasInfo registrationServer knownhosts = propertyList "iabak registration server" $ props 	& User.accountFor (User "registrar")-	& Ssh.keyImported SshRsa (User "registrar") (Context "IA.bak.users.git")+	& Ssh.userKeys (User "registrar") (Context "IA.bak.users.git") sshKeys 	& Ssh.knownHost knownhosts "gitlab.com" (User "registrar") 	& Git.cloned (User "registrar") repo "/home/registrar/IA.BAK" (Just "server") 	& Git.cloned (User "registrar") userrepo "/home/registrar/users" (Just "master")@@ -59,6 +59,11 @@ 		`requires` File.dirExists "/etc/kgb-bot/"   where 	link = "/usr/lib/cgi-bin/register.cgi"++sshKeys :: [(SshKeyType, Ssh.PubKeyText)]+sshKeys = +	[ (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCoiE+CPiIQyfWnl/E9iKG3eo4QzlH30vi7xAgKolGaTu6qKy4XPtl+8MNm2Dqn9QEYRVyyOT/XH0yP5dRc6uyReT8dBy03MmLkVbj8Q+nKCz5YOMTxrY3sX6RRXU1zVGjeVd0DtC+rKRT7reoCxef42LAJTm8nCyZu/enAuso5qHqBbqulFz2YXEKfU1SEEXLawtvgGck1KmCyg+pqazeI1eHWXrojQf5isTBKfPQLWVppBkWAf5cA4wP5U1vN9dVirIdw66ds1M8vnGlkTBjxP/HLGBWGYhZHE7QXjXRsk2RIXlHN9q6GdNu8+F3HXS22mst47E4UAeRoiXSMMtF5")+	]  graphiteServer :: Property HasInfo graphiteServer = propertyList "iabak graphite server" $ props
src/Propellor/Property/SiteSpecific/JoeySites.hs view
@@ -3,7 +3,7 @@  module Propellor.Property.SiteSpecific.JoeySites where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.File as File import qualified Propellor.Property.Gpg as Gpg@@ -137,7 +137,10 @@ 		, "--client-name=spool" 		, "--ssh-key=" ++ keyfile 		] Obnam.OnlyClient-		`requires` Ssh.keyImported' (Just keyfile) SshRsa (User "root") (Context "olduse.net")+		`requires` Ssh.userKeyAt (Just keyfile)+			(User "root")+			(Context "olduse.net")+			(SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD0F6L76SChMCIGmeyGhlFMUTgZ3BoTbATiOSs0A7KXQoI1LTE5ZtDzzUkrQRJVpJ640pfMR7cQZyBm8tv+kYIPp0238GrX43c1vgm0L78agDnBU7r2iNMyWIwhssK8O3ZAhp8Q4KCz1r8hP2nIiD0y1D1VWW8h4KWOS7I1XCEAjOTvFvEjTh6a9MyHrcIkv7teUUzTBRjNrsyijCFRk1+pEET54RueoOmEjQcWd/sK1tYRiMZjegRLBOus2wUWsUOvznJ2iniLONUTGAWRnEV+O7hLN6CD44osJ+wkZk8bPAumTS0zcSLckX1jpdHJicmAyeniWSd4FCqm1YE6/xDD") 		`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root") 	keyfile = "/root/.ssh/olduse.net.key" @@ -183,13 +186,18 @@ 	& Apt.serviceInstalledRunning "mumble-server" 	& Obnam.backup "/var/lib/mumble-server" (Cron.Times "55 5 * * *") 		[ "--repository=sftp://2318@usw-s002.rsync.net/~/" ++ hn ++ ".obnam"+		, "--ssh-key=" ++ sshkey 		, "--client-name=mumble" 		] Obnam.OnlyClient-		`requires` Ssh.keyImported SshRsa (User "root") (Context hn)+		`requires` Ssh.userKeyAt (Just sshkey)+			(User "root")+ 			(Context hn)+			(SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSXXSM3mM8SNu+qel9R/LkDIkjpV3bfpUtRtYv2PTNqicHP+DdoThrr0ColFCtLH+k2vQJvR2n8uMzHn53Dq2IO3TtD27+7rJSsJwAZ8oftNzuTir8IjAwX5g6JYJs+L0Ny4RB0ausd+An0k/CPMRl79zKxpZd2MBMDNXt8hyqu0vS0v1ohq5VBEVhBBvRvmNQvWOCj7PdrKQXpUBHruZOeVVEdUUXZkVc1H0t7LVfJnE+nGKyWbw2jM+7r3Rn5Semc4R1DxsfaF8lKkZyE88/5uZQ/ddomv8ptz6YZ5b+Bg6wfooWPC3RWAALjxnHaC2yN1VONAvHmT0uNn1o6v0b") 		`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root") 	& trivial (cmdProperty "chown" ["-R", "mumble-server:mumble-server", "/var/lib/mumble-server"])   where 	hn = "mumble.debian.net"+	sshkey = "/root/.ssh/mumble.debian.net.key"  -- git.kitenet.net and git.joeyh.name gitServer :: [Host] -> Property HasInfo@@ -199,7 +207,10 @@ 		, "--ssh-key=" ++ sshkey 		, "--client-name=wren" -- historical 		] Obnam.OnlyClient (Gpg.GpgKeyId "1B169BE1")-		`requires` Ssh.keyImported' (Just sshkey) SshRsa (User "root") (Context "git.kitenet.net")+		`requires` Ssh.userKeyAt (Just sshkey)+			(User "root")+			(Context "git.kitenet.net")+			(SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD0F6L76SChMCIGmeyGhlFMUTgZ3BoTbATiOSs0A7KXQoI1LTE5ZtDzzUkrQRJVpJ640pfMR7cQZyBm8tv+kYIPp0238GrX43c1vgm0L78agDnBU7r2iNMyWIwhssK8O3ZAhp8Q4KCz1r8hP2nIiD0y1D1VWW8h4KWOS7I1XCEAjOTvFvEjTh6a9MyHrcIkv7teUUzTBRjNrsyijCFRk1+pEET54RueoOmEjQcWd/sK1tYRiMZjegRLBOus2wUWsUOvznJ2iniLONUTGAWRnEV+O7hLN6CD44osJ+wkZk8bPAumTS0zcSLckX1jpdHJicmAyeniWSd4FCqm1YE6/xDD") 		`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root") 		`requires` Ssh.authorizedKeys (User "family") (Context "git.kitenet.net") 		`requires` User.accountFor (User "family")
+ src/Propellor/Property/Spin.hs view
@@ -0,0 +1,160 @@+{-# LANGUAGE FlexibleInstances, DeriveDataTypeable, GeneralizedNewtypeDeriving #-}++module Propellor.Property.Spin (+	Spinnable(..),+	controllerFor,+	controllerKeys,+	controlledBy,+) where++import Propellor.Base+import Propellor.Spin (spin, SpinMode(..))+import Propellor.Types.Info+import qualified Propellor.Property.Ssh as Ssh++import qualified Data.Set as S++-- | A class of things that can be spinned.+class Spinnable t where+	toSpin :: t -> Property HasInfo++instance Spinnable Host where+	toSpin h = infoProperty desc go (mkControllingInfo h <> privinfo) []+		`requires` Ssh.knownHost [h] (hostName h) (User "root")+	  where+		desc = cdesc (hostName h)+		go = do+			thishost <- ask+			if isControllerLoop thishost h+				then errorMessage $ unwords+					[ "controller loop detected involving"+					, hostName thishost+					, "and"+					, hostName h+					]+				else do+					liftIO $ spin ControllingSpin (hostName h) h+					-- Don't know if the spin made a+					-- change to the remote host or not,+					-- but in any case, the+					-- local host was not changed.+					noChange+		-- Make the controlling host have all the remote host's+		-- PrivData, so it can send it on to the remote host+		-- when spinning it.+		privinfo = addInfo mempty $+			forceHostContext (hostName h) $+				getInfo (hostInfo h)++-- | Each Host in the list is spinned in turn. Does not stop on spin+-- failure; does propagate overall success/failure.+instance Spinnable [Host] where+	toSpin l = propertyList (cdesc $ unwords $ map hostName l) (map toSpin l)++-- | The Host that has this Property is in control of running propellor on+-- some other Hosts.+--+-- Making a host a controller eliminates the need to manually run+-- propellor --spin to update the controlled hosts. Each time+-- propellor is run on the controller host, it will in turn run+-- propellor on the controlled Hosts.+--+-- The controller needs to be able to ssh to the hosts it controls,+-- and run propellor, as root. The controller is automatically configured+-- with `Propellor.Property.Ssh.knownHost` to know the host keys of the +-- hosts that it will ssh to. It's up to you to use `controllerKeys`+-- and `controlledBy` to set up the ssh keys that will let the controller+-- log into the hosts it controls.+--+-- For example, if you have some webservers and a dnsserver,+-- and want a master that runs propellor on all of them:+--+-- > import Propellor+-- > import qualified Propellor.Property.Spin as Spin+-- > import qualified Propellor.Property.Ssh as Ssh+-- > import qualified Propellor.Property.Cron as Cron+-- > +-- > main = defaultMain hosts+-- >+-- > hosts =+-- > 	[ master+-- >	, dnsserver+-- >	] ++ webservers+-- > +-- > dnsserver = host "dns.example.com"+-- >	& Ssh.hostKeys hostContext [(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB3BJ2GqZiTR2LEoDXyYFgh/BduWefjdKXAsAtzS9zeI")]+-- >    & Spin.controlledBy master+-- >	& ...+-- > +-- > webservers =+-- >    [ host "www1.example.com"+-- >		& Ssh.hostKeys hostContext [(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICfFntnesZcYz2B2T41ay45igfckXRSh5uVffkuCQkLv")]+-- > 		& Spin.controlledBy master+-- >		& ...+-- >	, ...+-- >	]+-- >+-- > master = host "master.example.com"+-- >	& Spin.controllerKeys [(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFWD0Hau5FDLeNrDHKilNMKm9c68R3WD+NJOp2jPWvJV")]+-- > 	-- Only update dnsserver once all webservers are successfully updated.+-- >	& Spin.controllerFor dnsserver+-- >		`requires` Spin.controllerFor webservers+-- >	& Cron.runPropellor+--+-- Multiple controllers can control the same hosts. However, when+-- propellor is already running on a host, a controller will fail+-- to run it. So, if two controllers both try to control the same+-- host at the same time, one will fail.+--+-- Chains of controllers are supported; host A can control host B which+-- controls host C. Loops of controllers are automatically prevented.+--+-- Note that a controller can see all PrivInfo of the hosts it controls.+controllerFor :: Spinnable h => h -> Property HasInfo+controllerFor h = toSpin h+	`requires` Ssh.installed++-- | Uses `Propellor.Property.Ssh.keysImported` to set up the ssh keys+-- for the root user on a controller. +--+-- (The corresponding private keys come from the privdata.)+controllerKeys :: [(SshKeyType, Ssh.PubKeyText)] -> Property HasInfo+controllerKeys ks = Ssh.userKeys (User "root") hostContext ks+	`requires` Ssh.installed++-- | Use this property to let the specified controller Host ssh in+-- and run propellor.+controlledBy :: Host -> Property NoInfo+controlledBy h = User "root" `Ssh.authorizedKeysFrom` (User "root", h)+	`requires` Ssh.installed++cdesc :: String -> Desc+cdesc n = "controller for " ++ n++-- To detect loops of controlled hosts, each Host's info contains a list+-- of the hosts it's controlling.+newtype Controlling = Controlled [Host]+	deriving (Typeable, Monoid, Show)++isControlledBy :: Host -> Controlling -> Bool+h `isControlledBy` (Controlled hs) = any (== hostName h) (map hostName hs)++instance IsInfo Controlling where+	propagateInfo _ = True++mkControllingInfo :: Host -> Info+mkControllingInfo controlled = addInfo mempty (Controlled [controlled])++getControlledBy :: Host -> Controlling+getControlledBy = getInfo . hostInfo++isControllerLoop :: Host -> Host -> Bool+isControllerLoop controller controlled = go S.empty controlled+  where+	go checked h+		| controller `isControlledBy` c = True+		-- avoid checking loops that have been checked before+		| hostName h `S.member` checked = False+		| otherwise = any (go (S.insert (hostName h) checked)) l+	  where+		c@(Controlled l) = getControlledBy h
src/Propellor/Property/Ssh.hs view
@@ -1,7 +1,10 @@ {-# LANGUAGE DeriveDataTypeable #-}  module Propellor.Property.Ssh (+	installed,+	restarted, 	PubKeyText,+	-- * Daemon configuration 	sshdConfig, 	ConfigKeyword, 	setSshdConfigBool,@@ -10,34 +13,43 @@ 	permitRootLogin, 	passwordAuthentication, 	noPasswords,-	hasAuthorizedKeys,-	authorizedKey,-	restarted,+	listenPort,+	-- * Host keys 	randomHostKeys, 	hostKeys, 	hostKey,-	pubKey,-	getPubKey,-	keyImported,-	keyImported',+	hostPubKey,+	getHostPubKey,+	-- * User keys and configuration+	userKeys,+	userKeyAt, 	knownHost,+	authorizedKeysFrom, 	authorizedKeys,-	listenPort+	authorizedKey,+	hasAuthorizedKeys,+	getUserPubKeys, ) where -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File import qualified Propellor.Property.Service as Service+import qualified Propellor.Property.Apt as Apt import Propellor.Property.User import Propellor.Types.Info import Utility.FileMode  import System.PosixCompat import qualified Data.Map as M+import qualified Data.Set as S import Data.List -type PubKeyText = String+installed :: Property NoInfo+installed = Apt.installed ["ssh"] +restarted :: Property NoInfo+restarted = Service.restarted "ssh"+ sshBool :: Bool -> String sshBool True = "yes" sshBool False = "no"@@ -95,14 +107,26 @@ 	d <- dotDir user 	return $ d </> f +-- | Makes the ssh server listen on a given port, in addition to any other+-- ports it is configured to listen on.+--+-- Revert to prevent it listening on a particular port.+listenPort :: Int -> RevertableProperty+listenPort port = enable <!> disable+  where+	portline = "Port " ++ show port+	enable = sshdConfig `File.containsLine` portline+		`describe` ("ssh listening on " ++ portline)+		`onChange` restarted+	disable = sshdConfig `File.lacksLine` portline+		`describe` ("ssh not listening on " ++ portline)+		`onChange` restarted+ hasAuthorizedKeys :: User -> IO Bool hasAuthorizedKeys = go <=< dotFile "authorized_keys"   where 	go f = not . null <$> catchDefaultIO "" (readFile f) -restarted :: Property NoInfo-restarted = Service.restarted "ssh"- -- | Blows away existing host keys and make new ones. -- Useful for systems installed from an image that might reuse host keys. -- A flag file is used to only ever do this once.@@ -118,6 +142,9 @@ 		ensureProperty $ scriptProperty  			[ "DPKG_MAINTSCRIPT_NAME=postinst DPKG_MAINTSCRIPT_PACKAGE=openssh-server /var/lib/dpkg/info/openssh-server.postinst configure" ] +-- | The text of a ssh public key, for example, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB3BJ2GqZiTR2LEoDXyYFgh/BduWefjdKXAsAtzS9zeI"+type PubKeyText = String+ -- | Installs the specified list of ssh host keys. -- -- The corresponding private keys come from the privdata.@@ -146,29 +173,25 @@ -- the private key comes from the privdata;  hostKey :: IsContext c => c -> SshKeyType -> PubKeyText -> Property HasInfo hostKey context keytype pub = combineProperties desc-	[ pubKey keytype pub-	, toProp $ property desc $ install writeFile True (lines pub)+	[ hostPubKey keytype pub+	, toProp $ property desc $ install File.hasContent True (lines pub) 	, withPrivData (keysrc "" (SshPrivKey keytype "")) context $ \getkey -> 		property desc $ getkey $-			install writeFileProtected False . privDataLines+			install File.hasContentProtected False . privDataLines 	] 	`onChange` restarted   where 	desc = "ssh host key configured (" ++ fromKeyType keytype ++ ")" 	install writer ispub keylines = do 		let f = keyFile keytype ispub-		have <- liftIO $ catchDefaultIO "" $ readFileStrict f-		let want = keyFileContent keylines-		if have == want-			then noChange-			else makeChange $ writer f want+		ensureProperty $ writer f (keyFileContent keylines) 	keysrc ext field = PrivDataSourceFileFromCommand field ("sshkey"++ext) 		("ssh-keygen -t " ++ sshKeyTypeParam keytype ++ " -f sshkey")  -- Make sure that there is a newline at the end; -- ssh requires this for some types of private keys.-keyFileContent :: [String] -> String-keyFileContent keylines = unlines (keylines ++ [""])+keyFileContent :: [String] -> [File.Line]+keyFileContent keylines = keylines ++ [""]  keyFile :: SshKeyType -> Bool -> FilePath keyFile keytype ispub = "/etc/ssh/ssh_host_" ++ fromKeyType keytype ++ "_key" ++ ext@@ -178,80 +201,106 @@ -- | Indicates the host key that is used by a Host, but does not actually -- configure the host to use it. Normally this does not need to be used; -- use 'hostKey' instead.-pubKey :: SshKeyType -> PubKeyText -> Property HasInfo-pubKey t = pureInfoProperty "ssh pubkey known" . SshPubKeyInfo . M.singleton t+hostPubKey :: SshKeyType -> PubKeyText -> Property HasInfo+hostPubKey t = pureInfoProperty "ssh pubkey known" . HostKeyInfo . M.singleton t -getPubKey :: Propellor (M.Map SshKeyType PubKeyText)-getPubKey = fromSshPubKeyInfo <$> askInfo+getHostPubKey :: Propellor (M.Map SshKeyType PubKeyText)+getHostPubKey = fromHostKeyInfo <$> askInfo -newtype SshPubKeyInfo = SshPubKeyInfo -	{ fromSshPubKeyInfo :: M.Map SshKeyType PubKeyText }-	deriving (Eq, Ord, Typeable)+newtype HostKeyInfo = HostKeyInfo +	{ fromHostKeyInfo :: M.Map SshKeyType PubKeyText }+	deriving (Eq, Ord, Typeable, Show) -instance IsInfo SshPubKeyInfo where-	propigateInfo _ = False+instance IsInfo HostKeyInfo where+	propagateInfo _ = False -instance Monoid SshPubKeyInfo where-	mempty = SshPubKeyInfo M.empty-	mappend (SshPubKeyInfo old) (SshPubKeyInfo new) = +instance Monoid HostKeyInfo where+	mempty = HostKeyInfo M.empty+	mappend (HostKeyInfo old) (HostKeyInfo new) =  		-- new first because union prefers values from the first 		-- parameter when there is a duplicate key-		SshPubKeyInfo (new `M.union` old)+		HostKeyInfo (new `M.union` old) --- | Sets up a user with a ssh private key and public key pair from the--- PrivData.------ If the user already has a private/public key, it is left unchanged.-keyImported :: IsContext c => SshKeyType -> User -> c -> Property HasInfo-keyImported = keyImported' Nothing+userPubKeys :: User -> [(SshKeyType, PubKeyText)] -> Property HasInfo+userPubKeys u@(User n) l = pureInfoProperty ("ssh pubkey for " ++ n) $+	UserKeyInfo (M.singleton u (S.fromList l)) --- | A file can be speficied to write the key to somewhere other than--- usual. Allows a user to have multiple keys for different roles.-keyImported' :: IsContext c => Maybe FilePath -> SshKeyType -> User -> c -> Property HasInfo-keyImported' dest keytype user@(User u) context = combineProperties desc-	[ installkey (SshPubKey keytype u) (install writeFile ".pub")-	, installkey (SshPrivKey keytype u) (install writeFileProtected "")-	]+getUserPubKeys :: User -> Propellor [(SshKeyType, PubKeyText)]+getUserPubKeys u = maybe [] S.toList . M.lookup u . fromUserKeyInfo <$> askInfo++newtype UserKeyInfo = UserKeyInfo+	{ fromUserKeyInfo :: M.Map User (S.Set (SshKeyType, PubKeyText)) }+	deriving (Eq, Ord, Typeable, Show)++instance IsInfo UserKeyInfo where+	propagateInfo _ = False++instance Monoid UserKeyInfo where+	mempty = UserKeyInfo M.empty+	mappend (UserKeyInfo old) (UserKeyInfo new) = +		UserKeyInfo (M.unionWith S.union old new)++-- | Sets up a user with the specified public keys, and the corresponding+-- private keys from the privdata.+-- +-- The public keys are added to the Info, so other properties like+-- `authorizedKeysFrom` can use them.+userKeys :: IsContext c => User -> c -> [(SshKeyType, PubKeyText)] -> Property HasInfo+userKeys user@(User name) context ks = combineProperties desc $+	userPubKeys user ks : map (userKeyAt Nothing user context) ks   where+	desc = unwords+		[ name+		, "has ssh key"+		, "(" ++ unwords (map (fromKeyType . fst) ks) ++ ")"+		]++-- | Sets up a user with the specified pubic key, and a private+-- key from the privdata.+--+-- A file can be specified to write the key to somewhere other than+-- the default locations. Allows a user to have multiple keys for+-- different roles.+userKeyAt :: IsContext c => Maybe FilePath -> User -> c -> (SshKeyType, PubKeyText) -> Property HasInfo+userKeyAt dest user@(User u) context (keytype, pubkeytext) =+	combineProperties desc $ props+		& pubkey+		& privkey+  where 	desc = unwords $ catMaybes 		[ Just u 		, Just "has ssh key" 		, dest 		, Just $ "(" ++ fromKeyType keytype ++ ")" 		]-	installkey p a = withPrivData p context $ \getkey ->-		property desc $ getkey a+	pubkey = property desc $ install File.hasContent ".pub" [pubkeytext]+	privkey = withPrivData (SshPrivKey keytype u) context $ \getkey -> +		property desc $ getkey $+			install File.hasContentProtected "" . privDataLines 	install writer ext key = do 		f <- liftIO $ keyfile ext-		ifM (liftIO $ doesFileExist f)-			( noChange-			, ensureProperties-				[ property desc $ makeChange $ do-					createDirectoryIfMissing True (takeDirectory f)-					writer f (keyFileContent (privDataLines key))-				, File.ownerGroup f user (userGroup user)-				, File.ownerGroup (takeDirectory f) user (userGroup user)-				]-			)+		ensureProperty $ combineProperties desc+			[ writer f (keyFileContent key)+			, File.ownerGroup f user (userGroup user)+			, File.ownerGroup (takeDirectory f) user (userGroup user)+			] 	keyfile ext = case dest of 		Nothing -> do 			home <- homeDirectory <$> getUserEntryForName u 			return $ home </> ".ssh" </> "id_" ++ fromKeyType keytype ++ ext 		Just f -> return $ f ++ ext -- fromKeyType :: SshKeyType -> String fromKeyType SshRsa = "rsa" fromKeyType SshDsa = "dsa" fromKeyType SshEcdsa = "ecdsa" fromKeyType SshEd25519 = "ed25519" --- | Puts some host's ssh public key(s), as set using 'pubKey' or 'hostKey'--- into the known_hosts file for a user.+-- | Puts some host's ssh public key(s), as set using `hostPubKey`+-- or `hostKey` into the known_hosts file for a user. knownHost :: [Host] -> HostName -> User -> Property NoInfo knownHost hosts hn user@(User u) = property desc $-	go =<< fromHost hosts hn getPubKey+	go =<< fromHost hosts hn getHostPubKey   where 	desc = u ++ " knows ssh key for " ++ hn 	go (Just m) | not (M.null m) = do@@ -264,23 +313,41 @@ 			, File.ownerGroup (takeDirectory f) user (userGroup user) 			] 	go _ = do-		warningMessage $ "no configred pubKey for " ++ hn+		warningMessage $ "no configured ssh host keys for " ++ hn 		return FailedChange +-- | Ensures that a local user's authorized keys contains a line allowing+-- logins from a remote user on the specified Host.+--+-- The ssh keys of the remote user can be set using `keysImported`+--+-- Any other lines in the authorized_keys file are preserved as-is.+authorizedKeysFrom :: User -> (User, Host) -> Property NoInfo+localuser@(User ln) `authorizedKeysFrom` (remoteuser@(User rn), remotehost) = +	property desc $ go =<< fromHost' remotehost (getUserPubKeys remoteuser)+  where+	remote = rn ++ "@" ++ hostName remotehost+	desc = ln ++ " authorized_keys from " ++ remote+	go [] = do+		warningMessage $ "no configured ssh user keys for " ++ remote+		return FailedChange+	go ks = ensureProperty $ combineProperties desc $+		map (authorizedKey localuser . snd) ks+ -- | Makes a user have authorized_keys from the PrivData -- -- This removes any other lines from the file. authorizedKeys :: IsContext c => User -> c -> Property HasInfo authorizedKeys user@(User u) context = withPrivData (SshAuthorizedKeys u) context $ \get ->-	property (u ++ " has authorized_keys") $ get $ \v -> do+	property desc $ get $ \v -> do 		f <- liftIO $ dotFile "authorized_keys" user-		liftIO $ do-			createDirectoryIfMissing True (takeDirectory f)-			writeFileProtected f (keyFileContent (privDataLines v))-		ensureProperties -			[ File.ownerGroup f user (userGroup user)+		ensureProperty $ combineProperties desc+			[ File.hasContentProtected f (keyFileContent (privDataLines v))+			, File.ownerGroup f user (userGroup user) 			, File.ownerGroup (takeDirectory f) user (userGroup user)-			] +			]+  where+	desc = u ++ " has authorized_keys"  -- | Ensures that a user's authorized_keys contains a line. -- Any other lines in the file are preserved as-is.@@ -295,19 +362,4 @@ 		, File.ownerGroup (takeDirectory f) user (userGroup user) 		]   where-	desc = u ++ " has autorized_keys"---- | Makes the ssh server listen on a given port, in addition to any other--- ports it is configured to listen on.------ Revert to prevent it listening on a particular port.-listenPort :: Int -> RevertableProperty-listenPort port = enable <!> disable-  where-	portline = "Port " ++ show port-	enable = sshdConfig `File.containsLine` portline-		`describe` ("ssh listening on " ++ portline)-		`onChange` restarted-	disable = sshdConfig `File.lacksLine` portline-		`describe` ("ssh not listening on " ++ portline)-		`onChange` restarted+	desc = u ++ " has authorized_keys"
src/Propellor/Property/Sudo.hs view
@@ -2,7 +2,7 @@  import Data.List -import Propellor+import Propellor.Base import Propellor.Property.File import qualified Propellor.Property.Apt as Apt import Propellor.Property.User
src/Propellor/Property/Systemd.hs view
@@ -40,7 +40,7 @@ 	bindRo, ) where -import Propellor+import Propellor.Base import Propellor.Types.Chroot import Propellor.Types.Container import Propellor.Types.Info@@ -62,8 +62,8 @@ 	deriving (Show)  instance PropAccum Container where-	(Container n c h) & p = Container n c (h & p)-	(Container n c h) &^ p = Container n c (h &^ p)+	(Container n c h) `addProp` p = Container n c (h `addProp` p)+	(Container n c h) `addPropFront` p = Container n c (h `addPropFront` p) 	getProperties (Container _ _ h) = hostProperties h  -- | Starts a systemd service.@@ -187,7 +187,8 @@ 	& resolvConfed 	& linkJournal   where-	c@(Chroot.Chroot _ system _ _) = mkchroot (containerDir name)+	c = mkchroot (containerDir name)+	system = Chroot.chrootSystem c 	h = Host name [] mempty  -- | Runs a container using systemd-nspawn.@@ -205,7 +206,7 @@ -- Reverting this property stops the container, removes the systemd unit, -- and deletes the chroot and all its contents. nspawned :: Container -> RevertableProperty-nspawned c@(Container name (Chroot.Chroot loc system builderconf _) h) =+nspawned c@(Container name (Chroot.Chroot loc system builder _) h) = 	p `describe` ("nspawned " ++ name)   where 	p = enterScript c@@ -216,7 +217,7 @@ 	-- Chroot provisioning is run in systemd-only mode, 	-- which sets up the chroot and ensures systemd and dbus are 	-- installed, but does not handle the other provisions.-	chrootprovisioned = Chroot.provisioned' (Chroot.propigateChrootInfo chroot) chroot True+	chrootprovisioned = Chroot.provisioned' (Chroot.propagateChrootInfo chroot) chroot True  	-- Use nsenter to enter container and and run propellor to 	-- finish provisioning.@@ -225,7 +226,7 @@ 			<!> 		doNothing -	chroot = Chroot.Chroot loc system builderconf h+	chroot = Chroot.Chroot loc system builder h  -- | Sets up the service file for the container, and then starts -- it running.@@ -254,8 +255,9 @@ 		<$> servicefilecontent 		<*> catchDefaultIO "" (readFile servicefile) -	writeservicefile = property servicefile $ makeChange $-		viaTmp writeFile servicefile =<< servicefilecontent+	writeservicefile = property servicefile $ makeChange $ do+		c <- servicefilecontent+		File.viaStableTmp (\t -> writeFile t c) servicefile  	setupservicefile = check (not <$> goodservicefile) $ 		-- if it's running, it has the wrong configuration,@@ -376,8 +378,8 @@ -- -- > foo :: Host -- > foo = host "foo.example.com"--- >	& Systemd.running Systemd.networkd -- >	& Systemd.nspawned webserver+-- > 		`requires` Systemd.running Systemd.networkd -- > -- > webserver :: Systemd.container -- > webserver = Systemd.container "webserver" (Chroot.debootstrapped (System (Debian Testing) "amd64") mempty)
src/Propellor/Property/Systemd/Core.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Systemd.Core where -import Propellor+import Propellor.Base import qualified Propellor.Property.Apt as Apt  -- dbus is only a Recommends of systemd, but is needed for communication
src/Propellor/Property/Tor.hs view
@@ -1,6 +1,6 @@ module Propellor.Property.Tor where -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Service as Service
src/Propellor/Property/Unbound.hs view
@@ -17,7 +17,7 @@ 	, cachingDnsServer 	) where -import Propellor+import Propellor.Base import Propellor.Property.File import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Service as Service
src/Propellor/Property/User.hs view
@@ -2,7 +2,7 @@  import System.Posix -import Propellor+import Propellor.Base import qualified Propellor.Property.File as File  data Eep = YesReallyDeleteHome
+ src/Propellor/Property/Uwsgi.hs view
@@ -0,0 +1,49 @@+-- | Maintainer: Félix Sipma <felix+propellor@gueux.org>++module Propellor.Property.Uwsgi where++import Propellor.Base+import qualified Propellor.Property.File as File+import qualified Propellor.Property.Apt as Apt+import qualified Propellor.Property.Service as Service++type ConfigFile = [String]++type AppName = String++appEnabled :: AppName -> ConfigFile -> RevertableProperty+appEnabled an cf = enable <!> disable+  where+	enable = appVal an `File.isSymlinkedTo` appValRelativeCfg an+		`describe` ("uwsgi app enabled " ++ an)+		`requires` appAvailable an cf+		`requires` installed+		`onChange` reloaded+	disable = trivial $ File.notPresent (appVal an)+		`describe` ("uwsgi app disable" ++ an)+		`requires` installed+		`onChange` reloaded++appAvailable :: AppName -> ConfigFile -> Property NoInfo+appAvailable an cf = ("uwsgi app available " ++ an) ==>+	appCfg an `File.hasContent` (comment : cf)+  where+	comment = "# deployed with propellor, do not modify"++appCfg :: AppName -> FilePath+appCfg an = "/etc/uwsgi/apps-available/" ++ an++appVal :: AppName -> FilePath+appVal an = "/etc/uwsgi/apps-enabled/" ++ an++appValRelativeCfg :: AppName -> File.LinkTarget+appValRelativeCfg an = File.LinkTarget $ "../apps-available/" ++ an++installed :: Property NoInfo+installed = Apt.installed ["uwsgi"]++restarted :: Property NoInfo+restarted = Service.restarted "uwsgi"++reloaded :: Property NoInfo+reloaded = Service.reloaded "uwsgi"
src/Propellor/Protocol.hs view
@@ -11,7 +11,7 @@  import Data.List -import Propellor+import Propellor.Base  data Stage = NeedGitClone | NeedRepoUrl | NeedPrivData | NeedGitPush | NeedPrecompiled 	deriving (Read, Show, Eq)
src/Propellor/Shim.hs view
@@ -6,7 +6,7 @@  module Propellor.Shim (setup, cleanEnv, file) where -import Propellor+import Propellor.Base import Utility.LinuxMkLibs import Utility.FileMode import Utility.FileSystemEncoding
src/Propellor/Spin.hs view
@@ -1,5 +1,6 @@ module Propellor.Spin ( 	commitSpin,+	SpinMode(..), 	spin, 	update, 	gitPushHelper,@@ -16,7 +17,7 @@ import qualified Data.Set as S import Network.Socket (getAddrInfo, defaultHints, AddrInfo(..), AddrInfoFlag(..), SockAddr) -import Propellor+import Propellor.Base import Propellor.Protocol import Propellor.PrivData.Paths import Propellor.Git@@ -40,20 +41,26 @@ 		void $ actionMessage "Push to central git repository" $ 			boolSystem "git" [Param "push"] -spin :: HostName -> Maybe HostName -> Host -> IO ()-spin target relay hst = do+data SpinMode+	= RegularSpin+	| RelaySpin HostName+	| ControllingSpin+	deriving (Eq)++spin :: SpinMode -> HostName -> Host -> IO ()+spin spinmode target hst = do 	cacheparams <- if viarelay 		then pure ["-A"] 		else toCommand <$> sshCachingParams hn 	when viarelay $ 		void $ boolSystem "ssh-add" [] -	sshtarget <- ("root@" ++) <$> case relay of-		Just r -> pure r-		Nothing -> getSshTarget target hst+	sshtarget <- ("root@" ++) <$> case spinmode of+		RelaySpin r -> pure r+		_ -> getSshTarget target hst  	-- Install, or update the remote propellor.-	updateServer target relay hst+	updateServer target spinmode hst 		(proc "ssh" $ cacheparams ++ [sshtarget, shellWrap probecmd]) 		(proc "ssh" $ cacheparams ++ [sshtarget, shellWrap updatecmd]) @@ -61,10 +68,14 @@ 	unlessM (boolSystem "ssh" (map Param $ cacheparams ++ ["-t", sshtarget, shellWrap runcmd])) $ 		error "remote propellor failed"   where-	hn = fromMaybe target relay+	hn = case spinmode of+		RelaySpin h -> h+		_ -> target -	relaying = relay == Just target-	viarelay = isJust relay && not relaying+	relaying = spinmode == RelaySpin target+	viarelay = not relaying && case spinmode of+		RelaySpin _ -> True+		_ -> False  	probecmd = intercalate " ; " 		[ "if [ ! -d " ++ localdir ++ "/.git ]"@@ -169,20 +180,22 @@  updateServer 	:: HostName-	-> Maybe HostName+	-> SpinMode 	-> Host 	-> CreateProcess 	-> CreateProcess 	-> IO ()-updateServer target relay hst connect haveprecompiled =+updateServer target spinmode hst connect haveprecompiled = 	withIOHandles createProcessSuccess connect go   where-	hn = fromMaybe target relay-	relaying = relay == Just target+	hn = case spinmode of+		RelaySpin h -> h+		_ -> target+	relaying = spinmode == RelaySpin target  	go (toh, fromh) = do 		let loop = go (toh, fromh)-		let restart = updateServer hn relay hst connect haveprecompiled+		let restart = updateServer hn spinmode hst connect haveprecompiled 		let done = return () 		v <- maybe Nothing readish <$> getMarked fromh statusMarker 		case v of@@ -190,7 +203,7 @@ 				sendRepoUrl toh 				loop 			(Just NeedPrivData) -> do-				sendPrivData hn hst toh relaying+				sendPrivData hn toh =<< getprivdata 				loop 			(Just NeedGitClone) -> do 				hClose toh@@ -201,31 +214,36 @@ 				hClose toh 				hClose fromh 				sendPrecompiled hn-				updateServer hn relay hst haveprecompiled (error "loop")+				updateServer hn spinmode hst haveprecompiled (error "loop") 			(Just NeedGitPush) -> do 				sendGitUpdate hn fromh toh 				hClose fromh 				hClose toh 				done 			Nothing -> done--sendRepoUrl :: Handle -> IO ()-sendRepoUrl toh = sendMarked toh repoUrlMarker =<< (fromMaybe "" <$> getRepoUrl)--sendPrivData :: HostName -> Host -> Handle -> Bool -> IO ()-sendPrivData hn hst toh relaying = do-	privdata <- getdata-	void $ actionMessage ("Sending privdata (" ++ show (length privdata) ++ " bytes) to " ++ hn) $ do-		sendMarked toh privDataMarker privdata-		return True-  where-	getdata+	getprivdata 		| relaying = do 			let f = privDataRelay hn 			d <- readFileStrictAnyEncoding f 			nukeFile f 			return d-		| otherwise = show . filterPrivData hst <$> decryptPrivData+		| otherwise = case spinmode of+			-- When one host is controlling another,+			-- the controlling host's privdata includes the+			-- privdata of the controlled host.+			ControllingSpin -> show . filterPrivData hst . readPrivData+				<$> readFileStrictAnyEncoding privDataLocal+			_ -> show . filterPrivData hst <$> decryptPrivData++sendRepoUrl :: Handle -> IO ()+sendRepoUrl toh = sendMarked toh repoUrlMarker =<< (fromMaybe "" <$> getRepoUrl)++sendPrivData :: HostName -> Handle -> String -> IO ()+sendPrivData hn toh privdata = void $ actionMessage msg $ do+	sendMarked toh privDataMarker privdata+	return True+  where+	msg = "Sending privdata (" ++ show (length privdata) ++ " bytes) to " ++ hn  sendGitUpdate :: HostName -> Handle -> Handle -> IO () sendGitUpdate hn fromh toh =
src/Propellor/Ssh.hs view
@@ -1,6 +1,6 @@ module Propellor.Ssh where -import Propellor+import Propellor.Base import Utility.UserInfo  import System.PosixCompat
src/Propellor/Types.hs view
@@ -10,12 +10,12 @@  module Propellor.Types 	( Host(..)-	, Desc 	, Property 	, Info 	, HasInfo 	, NoInfo 	, CInfo+	, Desc 	, infoProperty 	, simpleProperty 	, adjustPropertySatisfy@@ -27,7 +27,6 @@ 	, IsProp(..) 	, Combines(..) 	, CombinedType-	, before 	, combineWith 	, Propellor(..) 	, EndAction(..)@@ -93,6 +92,12 @@ -- | The core data type of Propellor, this represents a property -- that the system should have, and an action to ensure it has the -- property.+--+-- A property can have associated `Info` or not. This is tracked at the+-- type level with Property `NoInfo` and Property `HasInfo`.+--+-- There are many instances and type families, which are mostly used+-- internally, so you needn't worry about them. data Property i where 	IProperty :: Desc -> Propellor Result -> Info -> [Property HasInfo] -> Property HasInfo 	SProperty :: Desc -> Propellor Result -> [Property NoInfo] -> Property NoInfo@@ -164,17 +169,17 @@ propertyChildren (IProperty _ _ _ cs) = cs propertyChildren (SProperty _ _ cs) = cs --- | A property that can be reverted.+-- | A property that can be reverted. The first Property is run+-- normally and the second is run when it's reverted. data RevertableProperty = RevertableProperty (Property HasInfo) (Property HasInfo) --- | Makes a revertable property; the first Property is run--- normally and the second is run when it's reverted.+-- | Shorthand to construct a revertable property. (<!>) :: Property i1 -> Property i2 -> RevertableProperty p1 <!> p2 = RevertableProperty (toIProperty p1) (toIProperty p2) +-- | Class of types that can be used as properties of a host. class IsProp p where-	-- | Sets description.-	describe :: p -> Desc -> p+	setDesc :: p -> Desc -> p 	toProp :: p -> Property HasInfo 	getDesc :: p -> Desc 	-- | Gets the info of the property, combined with all info@@ -182,28 +187,28 @@ 	getInfoRecursive :: p -> Info  instance IsProp (Property HasInfo) where-	describe (IProperty _ a i cs) d = IProperty d a i cs+	setDesc (IProperty _ a i cs) d = IProperty d a i cs 	toProp = id 	getDesc = propertyDesc 	getInfoRecursive (IProperty _ _ i cs) =  		i <> mconcat (map getInfoRecursive cs) instance IsProp (Property NoInfo) where-	describe (SProperty _ a cs) d = SProperty d a cs+	setDesc (SProperty _ a cs) d = SProperty d a cs 	toProp = toIProperty 	getDesc = propertyDesc 	getInfoRecursive _ = mempty  instance IsProp RevertableProperty where 	-- | Sets the description of both sides.-	describe (RevertableProperty p1 p2) d = -		RevertableProperty (describe p1 d) (describe p2 ("not " ++ d))+	setDesc (RevertableProperty p1 p2) d = +		RevertableProperty (setDesc p1 d) (setDesc p2 ("not " ++ d)) 	getDesc (RevertableProperty p1 _) = getDesc p1 	toProp (RevertableProperty p1 _) = p1 	-- | Return the Info of the currently active side. 	getInfoRecursive (RevertableProperty p1 _p2) = getInfoRecursive p1 --- | Type level calculation of the type that results from combining two types--- with `requires`.+-- | Type level calculation of the type that results from combining two+-- types of properties. type family CombinedType x y type instance CombinedType (Property x) (Property y) = Property (CInfo x y) type instance CombinedType RevertableProperty (Property NoInfo) = RevertableProperty@@ -211,15 +216,11 @@ type instance CombinedType RevertableProperty RevertableProperty = RevertableProperty  class Combines x y where-	-- | Indicates that the first property depends on the second,-	-- so before the first is ensured, the second will be ensured.-	requires :: x -> y -> CombinedType x y-	--- | Combines together two properties, resulting in one property--- that ensures the first, and if the first succeeds, ensures the second.--- The property uses the description of the first property.-before :: (IsProp x, Combines y x, IsProp (CombinedType y x)) => x -> y -> CombinedType y x-before x y = (y `requires` x) `describe` getDesc x+	-- | Combines two properties. The second property is ensured+	-- first, and only once it is successfully ensures will the first+	-- be ensured. The combined property will have the description of+	-- the first property.+	(<<>>) :: x -> y -> CombinedType x y  -- | Combines together two properties, yielding a property that -- has the description and info of the first, and that has the second@@ -231,36 +232,36 @@ 	-> Property x 	-> Property y 	-> CombinedType (Property x) (Property y)-combineWith f x y = adjustPropertySatisfy (x `requires` y) $ \_ ->+combineWith f x y = adjustPropertySatisfy (x <<>> y) $ \_ -> 	f (propertySatisfy $ toSProperty x) (propertySatisfy $ toSProperty y)  instance Combines (Property HasInfo) (Property HasInfo) where-	requires (IProperty d1 a1 i1 cs1) y@(IProperty _d2 a2 _i2 _cs2) =+	(IProperty d1 a1 i1 cs1) <<>> y@(IProperty _d2 a2 _i2 _cs2) = 		IProperty d1 (a2 <> a1) i1 (y : cs1)  instance Combines (Property HasInfo) (Property NoInfo) where-	requires (IProperty d1 a1 i1 cs1) y@(SProperty _d2 a2 _cs2) =+	(IProperty d1 a1 i1 cs1) <<>> y@(SProperty _d2 a2 _cs2) = 		IProperty d1 (a2 <> a1) i1 (toIProperty y : cs1)  instance Combines (Property NoInfo) (Property HasInfo) where-	requires (SProperty d1 a1 cs1) y@(IProperty _d2 a2 _i2 _cs2) =+	(SProperty d1 a1 cs1) <<>> y@(IProperty _d2 a2 _i2 _cs2) = 		IProperty d1 (a2 <> a1) mempty (y : map toIProperty cs1)  instance Combines (Property NoInfo) (Property NoInfo) where-	requires (SProperty d1 a1  cs1) y@(SProperty _d2 a2 _cs2) =+	(SProperty d1 a1  cs1) <<>> y@(SProperty _d2 a2 _cs2) = 		SProperty d1 (a2 <> a1) (y : cs1)  instance Combines RevertableProperty (Property HasInfo) where-	requires (RevertableProperty p1 p2) y =-		RevertableProperty (p1 `requires` y) p2+	(RevertableProperty p1 p2) <<>> y =+		RevertableProperty (p1 <<>> y) p2  instance Combines RevertableProperty (Property NoInfo) where-	requires (RevertableProperty p1 p2) y =-		RevertableProperty (p1 `requires` toIProperty y) p2+	(RevertableProperty p1 p2) <<>> y =+		RevertableProperty (p1 <<>> toIProperty y) p2  instance Combines RevertableProperty RevertableProperty where-	requires (RevertableProperty x1 x2) (RevertableProperty y1 y2) =+	(RevertableProperty x1 x2) <<>> (RevertableProperty y1 y2) = 		RevertableProperty-			(x1 `requires` y1)+			(x1 <<>> y1) 			-- when reverting, run actions in reverse order-			(y2 `requires` x2)+			(y2 <<>> x2)
src/Propellor/Types/Chroot.hs view
@@ -16,7 +16,7 @@ 	deriving (Show, Typeable)  instance IsInfo ChrootInfo where-	propigateInfo _ = False+	propagateInfo _ = False  instance Monoid ChrootInfo where 	mempty = ChrootInfo mempty mempty
src/Propellor/Types/CmdLine.hs view
@@ -5,12 +5,14 @@  import System.Posix.Types +-- | All the command line actions that propellor can perform. data CmdLine 	= Run HostName 	| Spin [HostName] (Maybe HostName) 	| SimpleRun HostName 	| Set PrivDataField Context 	| Unset PrivDataField Context+	| UnsetUnused 	| Dump PrivDataField Context 	| Edit PrivDataField Context 	| ListFields@@ -27,4 +29,3 @@ 	| GitPush Fd Fd 	| Check 	deriving (Read, Show, Eq)-
src/Propellor/Types/Dns.hs view
@@ -26,7 +26,7 @@ 	deriving (Show, Eq, Ord, Monoid, Typeable)  instance IsInfo AliasesInfo where-	propigateInfo _ = False+	propagateInfo _ = False  toAliasesInfo :: [HostName] -> AliasesInfo toAliasesInfo l = AliasesInfo (S.fromList l)@@ -40,10 +40,10 @@ toDnsInfo :: S.Set Record -> DnsInfo toDnsInfo = DnsInfo --- | DNS Info is propigated, so that eg, aliases of a container+-- | DNS Info is propagated, so that eg, aliases of a container -- are reflected in the dns for the host where it runs. instance IsInfo DnsInfo where-	propigateInfo _ = True+	propagateInfo _ = True  -- | Represents a bind 9 named.conf file. data NamedConf = NamedConf@@ -155,7 +155,7 @@ 	deriving (Eq, Ord, Show, Typeable)  instance IsInfo NamedConfMap where-	propigateInfo _ = False+	propagateInfo _ = False  -- | Adding a Master NamedConf stanza for a particulr domain always -- overrides an existing Secondary stanza for that domain, while a
src/Propellor/Types/Docker.hs view
@@ -16,7 +16,7 @@ 	deriving (Show, Typeable)  instance IsInfo DockerInfo where-	propigateInfo _ = False+	propagateInfo _ = False  instance Monoid DockerInfo where 	mempty = DockerInfo mempty mempty
src/Propellor/Types/Info.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GADTs, DeriveDataTypeable, GeneralizedNewtypeDeriving #-}  module Propellor.Types.Info ( 	Info,@@ -6,7 +6,7 @@ 	addInfo, 	getInfo, 	mapInfo,-	propigatableInfo,+	propagatableInfo, 	InfoVal(..), 	fromInfoVal, 	Typeable,@@ -17,59 +17,65 @@ import Data.Maybe  -- | Information about a Host, which can be provided by its properties.-data Info = Info [(Dynamic, Bool)]+newtype Info = Info [InfoEntry]+	deriving (Monoid, Show) -instance Show Info where-	show (Info l) = "Info " ++ show (map (dynTypeRep . fst) l)+data InfoEntry where+	InfoEntry :: (IsInfo v, Typeable v) => v -> InfoEntry -instance Monoid Info where- 	mempty = Info []-	mappend (Info a) (Info b) = Info (a <> b)+instance Show InfoEntry where+	show (InfoEntry v) = show v +-- Extracts the value from an InfoEntry but only when+-- it's of the requested type.+extractInfoEntry :: Typeable v => InfoEntry -> Maybe v+extractInfoEntry (InfoEntry v) = cast v+ -- | Values stored in Info must be members of this class. -- -- This is used to avoid accidentially using other data types -- as info, especially type aliases which coud easily lead to bugs. -- We want a little bit of dynamic types here, but not too far..-class (Typeable v, Monoid v) => IsInfo v where-	-- | Should info of this type be propigated out of a+class (Typeable v, Monoid v, Show v) => IsInfo v where+	-- | Should info of this type be propagated out of a 	-- container to its Host?-	propigateInfo :: v -> Bool+	propagateInfo :: v -> Bool  -- | Any value in the `IsInfo` type class can be added to an Info. addInfo :: IsInfo v => Info -> v -> Info-addInfo (Info l) v = Info ((toDyn v, propigateInfo v):l)+addInfo (Info l) v = Info (InfoEntry v:l) +-- The list is reversed here because addInfo builds it up in reverse order. getInfo :: IsInfo v => Info -> v-getInfo (Info l) = mconcat (mapMaybe (fromDynamic . fst) (reverse l))+getInfo (Info l) = mconcat (mapMaybe extractInfoEntry (reverse l))  -- | Maps a function over all values stored in the Info that are of the -- appropriate type. mapInfo :: IsInfo v => (v -> v) -> Info -> Info mapInfo f (Info l) = Info (map go l)   where-	go (i, p) = case fromDynamic i of-		Nothing -> (i, p)-		Just v -> (toDyn (f v), p)+	go i = case extractInfoEntry i of+		Nothing -> i+		Just v -> InfoEntry (f v) --- | Filters out parts of the Info that should not propigate out of a+-- | Filters out parts of the Info that should not propagate out of a -- container.-propigatableInfo :: Info -> Info-propigatableInfo (Info l) = Info (filter snd l)+propagatableInfo :: Info -> Info+propagatableInfo (Info l) = Info (filter (\(InfoEntry a) -> propagateInfo a) l)  -- | Use this to put a value in Info that is not a monoid.--- The last value set will be used. This info does not propigate+-- The last value set will be used. This info does not propagate -- out of a container. data InfoVal v = NoInfoVal | InfoVal v-	deriving (Typeable)+	deriving (Typeable, Show)  instance Monoid (InfoVal v) where 	mempty = NoInfoVal 	mappend _ v@(InfoVal _) = v 	mappend v NoInfoVal = v -instance Typeable v => IsInfo (InfoVal v) where-	propigateInfo _ = False+instance (Typeable v, Show v) => IsInfo (InfoVal v) where+	propagateInfo _ = False  fromInfoVal :: InfoVal v -> Maybe v fromInfoVal NoInfoVal = Nothing
src/Propellor/Types/OS.hs view
@@ -40,8 +40,12 @@ type Architecture = String  type UserName = String+ newtype User = User UserName+	deriving (Eq, Ord, Show)+ newtype Group = Group String+	deriving (Eq, Ord, Show)  -- | Makes a Group with the same name as the User. userGroup :: User -> Group
src/Propellor/Types/PrivData.hs view
@@ -12,7 +12,7 @@ -- It's fine to add new constructors. data PrivDataField 	= DockerAuthentication-	| SshPubKey SshKeyType UserName+	| SshPubKey SshKeyType UserName -- ^ Not used anymore, but retained to avoid breaking serialization of old files 	| SshPrivKey SshKeyType UserName -- ^ For host key, use empty UserName 	| SshAuthorizedKeys UserName 	| Password UserName
+ src/Propellor/Utilities.hs view
@@ -0,0 +1,27 @@+-- | Re-exports some of propellor's internal utility modules.+--+-- These are used in the implementation of propellor, including some of its+-- properties. However, there is no API stability; any of these can change+-- or be removed without a major version number increase. +--+-- Use outside propellor at your own risk.++module Propellor.Utilities (+	  module Utility.PartialPrelude+	, module Utility.Process+	, module Utility.Exception+	, module Utility.Env+	, module Utility.Directory+	, module Utility.Tmp+	, module Utility.Monad+	, module Utility.Misc+) where++import Utility.PartialPrelude+import Utility.Process+import Utility.Exception+import Utility.Env+import Utility.Directory+import Utility.Tmp+import Utility.Monad+import Utility.Misc
src/config.hs view
@@ -2,13 +2,12 @@ -- the propellor program.  import Propellor-import Propellor.CmdLine-import Propellor.Property.Scheduled import qualified Propellor.Property.File as File import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.Network as Network --import qualified Propellor.Property.Ssh as Ssh import qualified Propellor.Property.Cron as Cron+import Propellor.Property.Scheduled --import qualified Propellor.Property.Sudo as Sudo import qualified Propellor.Property.User as User --import qualified Propellor.Property.Hostname as Hostname@@ -19,25 +18,25 @@ main = defaultMain hosts  -- The hosts propellor knows about.--- Edit this to configure propellor! hosts :: [Host] hosts =-	[ host "mybox.example.com"-		& os (System (Debian Unstable) "amd64")-		& Apt.stdSourcesList-		& Apt.unattendedUpgrades-		& Apt.installed ["etckeeper"]-		& Apt.installed ["ssh"]-		& User.hasSomePassword (User "root")-		& Network.ipv6to4-		& File.dirExists "/var/www"-		& Docker.docked webserverContainer-		& Docker.garbageCollected `period` Daily-		& Cron.runPropellor (Cron.Times "30 * * * *")--	-- add more hosts here...-	--, host "foo.example.com" = ...+	[ mybox 	]++-- An example host.+mybox :: Host+mybox = host "mybox.example.com"+	& os (System (Debian Unstable) "amd64")+	& Apt.stdSourcesList+	& Apt.unattendedUpgrades+	& Apt.installed ["etckeeper"]+	& Apt.installed ["ssh"]+	& User.hasSomePassword (User "root")+	& Network.ipv6to4+	& File.dirExists "/var/www"+	& Docker.docked webserverContainer+	& Docker.garbageCollected `period` Daily+	& Cron.runPropellor (Cron.Times "30 * * * *")  -- A generic webserver in a Docker container. webserverContainer :: Docker.Container