{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving #-} module Propellor.Property.ControlHeir ( ControlHeir(..), ControlList(..), addControlHeir, ControllerOf(..), ) where import Propellor.Base import Propellor.Spin (spin, SpinMode(..)) import Propellor.Types.Info import qualified Propellor.Property.Ssh as Ssh -- | A hierarchy of control. When propellor is run on a host that -- is a Controller, it in turn spins each of the hosts in its control -- list. -- -- There can be multiple levels of controllers in the hierarchy. -- -- Multiple controllers can control the same hosts. However, when -- propellor is already running on a host, a controller will fail -- to spin it. So, if two controllers both try to control the same -- host at the same time, one will fail. -- -- (Loops in the hierarchy, such as a host controlling itself, -- are detected and automatically broken.) data ControlHeir = Controller Host ControlList | Controlled Host instance Show ControlHeir where show (Controller h l) = "Controller " ++ hostName h ++ " (" ++ show l ++ ")" show (Controlled h) = "Controlled " ++ hostName h data ControlList -- | A list of hosts to control. Failure to spin one host does not -- prevent spinning later hosts in the list. = ControlList [ControlHeir] -- | Requires the first host to be successfully spinned before -- proceeding to spin the hosts in the ControlList. | ControlReq ControlHeir ControlList deriving (Show) listHeir :: ControlList -> [ControlHeir] listHeir (ControlList l) = l listHeir (ControlReq h l) = h : listHeir l class DirectlyControlled a where directlyControlled :: a -> [Host] instance DirectlyControlled ControlHeir where directlyControlled (Controlled h) = [h] directlyControlled (Controller h _) = [h] instance DirectlyControlled ControlList where directlyControlled = concatMap directlyControlled . listHeir -- Removes any loops that may be present in the ControlHeir involving -- the passed Host. This is a simple matter of removing the Host from any -- sub-hierarchies. deloop :: Host -> ControlHeir -> ControlHeir deloop _ (Controlled h) = Controlled h deloop thehost (Controller h cl) = Controller h (removeh cl) where removeh (ControlList l) = ControlList (mapMaybe removeh' l) removeh (ControlReq ch cl') = case removeh' ch of Just ch' -> ControlReq ch' (removeh cl') Nothing -> removeh cl' removeh' (Controlled h') | hostName h' == hostName thehost = Nothing | otherwise = Just (Controlled h') removeh' (Controller h' cl') | hostName h' == hostName thehost = Nothing | otherwise = Just (Controller h' (removeh cl')) -- | Applies a ControlHeir to a list of hosts. -- -- This 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 each of the controlled Hosts. -- -- The controller needs to be able to ssh to the hosts it controls, -- and run propellor, as root. To this end, -- the `Propellor.Property.Ssh.knownHost` property is added to the -- controller, so it knows the host keys of the hosts it controlls. -- -- Each controlled host is configured to let its controller -- ssh in as root. This is done by adding the -- `Propellor.Property.Ssh.authorizedKeysFrom` property, with -- `User "root"`. -- -- It's left up to you to use `Propellor.Property.Ssh.userKeys` to -- configure the ssh keys for the root user on controller hosts, -- and to use `Ssh.hostKeys` to configure the host keys for the controlled -- hosts. -- -- For example, if you have some webservers and a dnsserver, -- and want a master that runs propellor on all of them: -- -- > import Propellor -- > import Propellor.Property.ControlHeir -- > import qualified Propellor.Property.Ssh as Ssh -- > import qualified Propellor.Property.Cron as Cron -- > -- > main = defaultMain (hosts `addControlHeir` control) -- > -- > hosts = -- > [ master -- > , dnsserver -- > ] ++ webservers -- > -- > control = Controller master (ControlList (map Controlled (dnsserver:webservers))) -- > -- > dnsserver = host "dns.example.com" -- > & Ssh.hostKeys hostContext [(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB3BJ2GqZiTR2LEoDXyYFgh/BduWefjdKXAsAtzS9zeI")] -- > & ... -- > -- > webservers = -- > [ host "www1.example.com" -- > & Ssh.hostKeys hostContext [(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICfFntnesZcYz2B2T41ay45igfckXRSh5uVffkuCQkLv")] -- > & ... -- > , ... -- > ] -- > -- > master = host "master.example.com" -- > & Ssh.userKeys (User "root") [(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFWD0Hau5FDLeNrDHKilNMKm9c68R3WD+NJOp2jPWvJV")] -- > & Cron.runPropellor -- -- Note that a controller can see all PrivData of the hosts below it in -- the ControlHeir. addControlHeir :: [Host] -> ControlHeir -> [Host] addControlHeir hs (Controlled _) = hs addControlHeir hs c@(Controller _ _) | any isController hs = error "Detected repeated applications of addControlHeir. Since loop prevention only works within a single application, repeated application is unsafe and not allowed." | otherwise = map (\h -> addControlHeir' h (deloop h c)) hs -- Walk through the ControlHeir, and add properties to the Host -- depending on where it appears in the ControlHeir. -- (Loops are already removed before this point.) addControlHeir' :: Host -> ControlHeir -> Host addControlHeir' h (Controlled _) = h addControlHeir' h (Controller controller l) | hn == hostName controller = cont $ h & mkcontroller l | hn `elem` map hostName (directlyControlled l) = cont $ h & controlledBy controller | otherwise = cont h where hn = hostName h cont h' = foldl addControlHeir' h' (listHeir l) mkcontroller (ControlList l') = mkcontroller' (concatMap directlyControlled l') mkcontroller (ControlReq h' l') = mkcontroller' (directlyControlled h') `before` mkcontroller l' mkcontroller' l' = propertyList (cdesc $ unwords $ map hostName l') (map controllerFor l') -- | The host this property is added to becomes the controller for the -- specified Host. controllerFor :: Host -> Property HasInfo controllerFor h = infoProperty desc go (mkControllingInfo h <> privinfo) [] `requires` Ssh.knownHost [h] (hostName h) (User "root") `requires` Ssh.installed where desc = cdesc (hostName h) go = 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) -- | 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 -- | Each Host's info contains a list of the names of hosts it's controlling. newtype ControllerOf = ControllerOf [HostName] deriving (Typeable, Monoid, Show) instance IsInfo ControllerOf where propagateInfo _ = True mkControllingInfo :: Host -> Info mkControllingInfo controlled = addInfo mempty (ControllerOf [hostName controlled]) isController :: Host -> Bool isController h = case getInfo (hostInfo h) of ControllerOf [] -> False _ -> True