-- | Maintainer: Nicolas Schodet -- -- Support for Icinga2 monitoring system. module Propellor.Property.Icinga2 ( ApiPermissions(..), allApiPermissions, webApiPermissions, installed, restarted, reloaded, idoMysqlConfigured, apiConfigured, apiUserGranted, webInstalled, confAdjustSection, confContainsSetting, confFileContains, confHosts, confServices, confQuote, ) where import Propellor.Base import Data.List import qualified Propellor.Property.Apt as Apt import qualified Propellor.Property.ConfFile as ConfFile import qualified Propellor.Property.File as File import qualified Propellor.Property.Service as Service -- | API User permissions. newtype ApiPermissions = ApiPermissions [String] -- | Allow everything. allApiPermissions :: ApiPermissions allApiPermissions = ApiPermissions ["*"] -- | Needed permissions for Icinga Web 2. webApiPermissions :: ApiPermissions webApiPermissions = ApiPermissions [ "status/query" , "actions/*" , "objects/modify/*" , "objects/query/*" ] -- | A section in configuration, need to be the full section description. type ConfSection = String -- | A key in configuration. type ConfKey = String -- | A configuration statement, can be multiline. type ConfStatement = [String] -- | Make sure Icinga 2 is installed. installed :: Property DebianLike installed = Apt.installed ["icinga2"] -- | Restart Icinga 2. restarted :: Property DebianLike restarted = Service.restarted "icinga2" -- | Reload Icinga 2 configuration. reloaded :: Property DebianLike reloaded = Service.reloaded "icinga2" `before` delay where -- Reloading too fast causes problems, systemd waits forever. delay = cmdProperty "sleep" ["1"] `assume` MadeChange -- | Configure IDO MySQL. idoMysqlConfigured :: Property DebianLike idoMysqlConfigured = enabled `describe` "icinga2 IDO MySQL feature enabled" `requires` installed `requires` Apt.installed ["icinga2-ido-mysql"] `onChange` restarted where enabled = pathEnabled `File.isSymlinkedTo` pathAvailable pathEnabled = "/etc/icinga2/features-enabled/ido-mysql.conf" pathAvailable = File.LinkTarget "../features-available/ido-mysql.conf" -- | Configure and enable the API. apiConfigured :: Property DebianLike apiConfigured = check (not <$> isConfigured) go `requires` installed `onChange` restarted where isConfigured :: IO Bool isConfigured = doesFileExist "/etc/icinga2/features-enabled/api.conf" go = cmdProperty "icinga2" ["api", "setup"] -- | Create an API user and grant permissions. apiUserGranted :: IsContext c => User -> ApiPermissions -> c -> Property (HasInfo + DebianLike) apiUserGranted (User username) (ApiPermissions perms) context = go `requires` apiConfigured where go :: Property (HasInfo + DebianLike) go = withPrivData (Password username) context $ \getpassword -> property' desc $ \w -> getpassword $ \priv -> ensureProperty w $ goprop $ privDataVal priv goprop :: String -> Property DebianLike goprop password = confAdjustSection desc section (adj password) (ins password) usersFile desc = ("user " ++ username ++ " granted API usage") section = "object ApiUser " ++ (confQuote username) usersFile = "conf.d/api-users.conf" adj password = const [ section ++ " {" , " password = " ++ (confQuote password) , " permissions = [ " ++ qperms ++ " ]" ] ins password ls = ls ++ [""] ++ (adj password []) ++ ["}"] qperms = intercalate ", " $ map confQuote $ perms -- | Install Icinga Web 2. webInstalled :: Property DebianLike webInstalled = Apt.installed ["icingaweb2"] -- | Adjust a section of Icinga configuration file. confAdjustSection :: Desc -> ConfSection -> ConfFile.AdjustSection -> ConfFile.InsertSection -> FilePath -> Property DebianLike confAdjustSection desc section adj ins f = ConfFile.adjustSection desc start past adj ins conff `onChange` reloaded where start = (== (section ++ " {")) past = (== "}") conff = "/etc/icinga2" f -- | Ensures that a configuration file contains a section with a key=value -- setting. Works for simple item using a simple format (single line). confContainsSetting :: FilePath -> (ConfSection, ConfKey, String) -> Property DebianLike confContainsSetting f (section, key, value) = confAdjustSection (f ++ " section '" ++ section ++ "' contains " ++ key ++ "=" ++ value) section go (++ confsection) f where confkey = " " ++ key ++ " = " confline = confkey ++ value go [] = [confline] go (l:ls) = if isKeyVal l then confline : ls else l : go ls isKeyVal = (confkey `isPrefixOf`) confsection = ["", section ++ " {", confline, "}"] -- | Replace a full configuration file. confFileContains :: FilePath -> [ConfStatement] -> Property DebianLike confFileContains f stmts = conff `File.hasContent` (concat stmts) `onChange` reloaded where conff = "/etc/icinga2" f -- | Replace the full host definition. confHosts :: [ConfStatement] -> Property DebianLike confHosts hosts = confFileContains "conf.d/hosts.conf" hosts -- | Replace the full services definition. confServices :: [ConfStatement] -> Property DebianLike confServices services = confFileContains "conf.d/services.conf" services -- | Quote a string to be included in configuration. confQuote :: String -> String confQuote s = ['"'] ++ (concatMap escape s) ++ ['"'] where escape c | c == '"' = ['\\', '"'] | c == '\\' = ['\\', '\\'] | c == '\t' = ['\\', 't'] | c == '\r' = ['\\', 'r'] | c == '\n' = ['\\', 'n'] | c == '\b' = ['\\', 'b'] | c == '\f' = ['\\', 'f'] | otherwise = [c]