Merge branch 'joeyconfig'
This commit is contained in:
commit
5dd316a0ad
|
@ -8,38 +8,65 @@ import Propellor.Types.Attr
|
||||||
import "mtl" Control.Monad.Reader
|
import "mtl" Control.Monad.Reader
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
|
import Data.Maybe
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
|
|
||||||
pureAttrProperty :: Desc -> (Attr -> Attr) -> AttrProperty
|
pureAttrProperty :: Desc -> SetAttr -> Property
|
||||||
pureAttrProperty desc = AttrProperty $ Property ("has " ++ desc)
|
pureAttrProperty desc = Property ("has " ++ desc) (return NoChange)
|
||||||
(return NoChange)
|
|
||||||
|
|
||||||
hostname :: HostName -> AttrProperty
|
hostname :: HostName -> Property
|
||||||
hostname name = pureAttrProperty ("hostname " ++ name) $
|
hostname name = pureAttrProperty ("hostname " ++ name) $
|
||||||
\d -> d { _hostname = name }
|
\d -> d { _hostname = name }
|
||||||
|
|
||||||
getHostName :: Propellor HostName
|
getHostName :: Propellor HostName
|
||||||
getHostName = asks _hostname
|
getHostName = asks _hostname
|
||||||
|
|
||||||
os :: System -> AttrProperty
|
os :: System -> Property
|
||||||
os system = pureAttrProperty ("Operating " ++ show system) $
|
os system = pureAttrProperty ("Operating " ++ show system) $
|
||||||
\d -> d { _os = Just system }
|
\d -> d { _os = Just system }
|
||||||
|
|
||||||
getOS :: Propellor (Maybe System)
|
getOS :: Propellor (Maybe System)
|
||||||
getOS = asks _os
|
getOS = asks _os
|
||||||
|
|
||||||
cname :: Domain -> AttrProperty
|
-- | Indidate that a host has an A record in the DNS.
|
||||||
cname domain = pureAttrProperty ("cname " ++ domain) (addCName domain)
|
--
|
||||||
|
-- TODO check at run time if the host really has this address.
|
||||||
|
-- (Can't change the host's address, but as a sanity check.)
|
||||||
|
ipv4 :: String -> Property
|
||||||
|
ipv4 addr = pureAttrProperty ("ipv4 " ++ addr)
|
||||||
|
(addDNS $ Address $ IPv4 addr)
|
||||||
|
|
||||||
cnameFor :: IsProp p => Domain -> (Domain -> p) -> AttrProperty
|
-- | Indidate that a host has an AAAA record in the DNS.
|
||||||
cnameFor domain mkp =
|
ipv6 :: String -> Property
|
||||||
let p = mkp domain
|
ipv6 addr = pureAttrProperty ("ipv6 " ++ addr)
|
||||||
in AttrProperty p (addCName domain)
|
(addDNS $ Address $ IPv6 addr)
|
||||||
|
|
||||||
addCName :: HostName -> Attr -> Attr
|
-- | Indicates another name for the host in the DNS.
|
||||||
addCName domain d = d { _cnames = S.insert domain (_cnames d) }
|
alias :: Domain -> Property
|
||||||
|
alias domain = pureAttrProperty ("aka " ++ domain)
|
||||||
|
(addDNS $ CNAME $ AbsDomain domain)
|
||||||
|
|
||||||
sshPubKey :: String -> AttrProperty
|
addDNS :: Record -> SetAttr
|
||||||
|
addDNS record d = d { _dns = S.insert record (_dns d) }
|
||||||
|
|
||||||
|
-- | Adds a DNS NamedConf stanza.
|
||||||
|
--
|
||||||
|
-- Note that adding a Master stanza for a domain always overrides an
|
||||||
|
-- existing Secondary stanza, while a Secondary stanza is only added
|
||||||
|
-- when there is no existing Master stanza.
|
||||||
|
addNamedConf :: NamedConf -> SetAttr
|
||||||
|
addNamedConf conf d = d { _namedconf = new }
|
||||||
|
where
|
||||||
|
m = _namedconf d
|
||||||
|
domain = confDomain conf
|
||||||
|
new = case (confType conf, confType <$> M.lookup domain m) of
|
||||||
|
(Secondary, Just Master) -> m
|
||||||
|
_ -> M.insert domain conf m
|
||||||
|
|
||||||
|
getNamedConf :: Propellor (M.Map Domain NamedConf)
|
||||||
|
getNamedConf = asks _namedconf
|
||||||
|
|
||||||
|
sshPubKey :: String -> Property
|
||||||
sshPubKey k = pureAttrProperty ("ssh pubkey known") $
|
sshPubKey k = pureAttrProperty ("ssh pubkey known") $
|
||||||
\d -> d { _sshPubKey = Just k }
|
\d -> d { _sshPubKey = Just k }
|
||||||
|
|
||||||
|
@ -58,9 +85,22 @@ hostProperties (Host ps _) = ps
|
||||||
hostMap :: [Host] -> M.Map HostName Host
|
hostMap :: [Host] -> M.Map HostName Host
|
||||||
hostMap l = M.fromList $ zip (map (_hostname . hostAttr) l) l
|
hostMap l = M.fromList $ zip (map (_hostname . hostAttr) l) l
|
||||||
|
|
||||||
|
hostAttrMap :: [Host] -> M.Map HostName Attr
|
||||||
|
hostAttrMap l = M.fromList $ zip (map _hostname attrs) attrs
|
||||||
|
where
|
||||||
|
attrs = map hostAttr l
|
||||||
|
|
||||||
findHost :: [Host] -> HostName -> Maybe Host
|
findHost :: [Host] -> HostName -> Maybe Host
|
||||||
findHost l hn = M.lookup hn (hostMap l)
|
findHost l hn = M.lookup hn (hostMap l)
|
||||||
|
|
||||||
|
getAddresses :: Attr -> [IPAddr]
|
||||||
|
getAddresses = mapMaybe getIPAddr . S.toList . _dns
|
||||||
|
|
||||||
|
hostAddresses :: HostName -> [Host] -> [IPAddr]
|
||||||
|
hostAddresses hn hosts = case hostAttr <$> findHost hosts hn of
|
||||||
|
Nothing -> []
|
||||||
|
Just attr -> mapMaybe getIPAddr $ S.toList $ _dns attr
|
||||||
|
|
||||||
-- | Lifts an action into a different host.
|
-- | Lifts an action into a different host.
|
||||||
--
|
--
|
||||||
-- For example, `fromHost hosts "otherhost" getSshPubKey`
|
-- For example, `fromHost hosts "otherhost" getSshPubKey`
|
||||||
|
|
|
@ -18,7 +18,7 @@ runPropellor attr a = runReaderT (runWithAttr a) attr
|
||||||
mainProperties :: Attr -> [Property] -> IO ()
|
mainProperties :: Attr -> [Property] -> IO ()
|
||||||
mainProperties attr ps = do
|
mainProperties attr ps = do
|
||||||
r <- runPropellor attr $
|
r <- runPropellor attr $
|
||||||
ensureProperties [Property "overall" $ ensureProperties ps]
|
ensureProperties [Property "overall" (ensureProperties ps) id]
|
||||||
setTitle "propellor: done"
|
setTitle "propellor: done"
|
||||||
hFlush stdout
|
hFlush stdout
|
||||||
case r of
|
case r of
|
||||||
|
|
|
@ -5,6 +5,7 @@ module Propellor.Property where
|
||||||
import System.Directory
|
import System.Directory
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
import Data.List
|
||||||
import Control.Monad.IfElse
|
import Control.Monad.IfElse
|
||||||
import "mtl" Control.Monad.Reader
|
import "mtl" Control.Monad.Reader
|
||||||
|
|
||||||
|
@ -15,23 +16,21 @@ import Propellor.Engine
|
||||||
import Utility.Monad
|
import Utility.Monad
|
||||||
import System.FilePath
|
import System.FilePath
|
||||||
|
|
||||||
makeChange :: IO () -> Propellor Result
|
-- Constructs a Property.
|
||||||
makeChange a = liftIO a >> return MadeChange
|
property :: Desc -> Propellor Result -> Property
|
||||||
|
property d s = Property d s id
|
||||||
noChange :: Propellor Result
|
|
||||||
noChange = return NoChange
|
|
||||||
|
|
||||||
-- | Combines a list of properties, resulting in a single property
|
-- | Combines a list of properties, resulting in a single property
|
||||||
-- that when run will run each property in the list in turn,
|
-- 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
|
-- and print out the description of each as it's run. Does not stop
|
||||||
-- on failure; does propigate overall success/failure.
|
-- on failure; does propigate overall success/failure.
|
||||||
propertyList :: Desc -> [Property] -> Property
|
propertyList :: Desc -> [Property] -> Property
|
||||||
propertyList desc ps = Property desc $ ensureProperties ps
|
propertyList desc ps = Property desc (ensureProperties ps) (combineSetAttrs ps)
|
||||||
|
|
||||||
-- | Combines a list of properties, resulting in one property that
|
-- | Combines a list of properties, resulting in one property that
|
||||||
-- ensures each in turn, stopping on failure.
|
-- ensures each in turn, stopping on failure.
|
||||||
combineProperties :: Desc -> [Property] -> Property
|
combineProperties :: Desc -> [Property] -> Property
|
||||||
combineProperties desc ps = Property desc $ go ps NoChange
|
combineProperties desc ps = Property desc (go ps NoChange) (combineSetAttrs ps)
|
||||||
where
|
where
|
||||||
go [] rs = return rs
|
go [] rs = return rs
|
||||||
go (l:ls) rs = do
|
go (l:ls) rs = do
|
||||||
|
@ -44,26 +43,23 @@ combineProperties desc ps = Property desc $ go ps NoChange
|
||||||
-- that ensures the first, and if the first succeeds, ensures the second.
|
-- that ensures the first, and if the first succeeds, ensures the second.
|
||||||
-- The property uses the description of the first property.
|
-- The property uses the description of the first property.
|
||||||
before :: Property -> Property -> Property
|
before :: Property -> Property -> Property
|
||||||
p1 `before` p2 = Property (propertyDesc p1) $ do
|
p1 `before` p2 = p2 `requires` p1
|
||||||
r <- ensureProperty p1
|
`describe` (propertyDesc p1)
|
||||||
case r of
|
|
||||||
FailedChange -> return FailedChange
|
|
||||||
_ -> ensureProperty p2
|
|
||||||
|
|
||||||
-- | Makes a perhaps non-idempotent Property be idempotent by using a flag
|
-- | Makes a perhaps non-idempotent Property be idempotent by using a flag
|
||||||
-- file to indicate whether it has run before.
|
-- file to indicate whether it has run before.
|
||||||
-- Use with caution.
|
-- Use with caution.
|
||||||
flagFile :: Property -> FilePath -> Property
|
flagFile :: Property -> FilePath -> Property
|
||||||
flagFile property = flagFile' property . return
|
flagFile p = flagFile' p . return
|
||||||
|
|
||||||
flagFile' :: Property -> IO FilePath -> Property
|
flagFile' :: Property -> IO FilePath -> Property
|
||||||
flagFile' property getflagfile = Property (propertyDesc property) $ do
|
flagFile' p getflagfile = adjustProperty p $ \satisfy -> do
|
||||||
flagfile <- liftIO getflagfile
|
flagfile <- liftIO getflagfile
|
||||||
go flagfile =<< liftIO (doesFileExist flagfile)
|
go satisfy flagfile =<< liftIO (doesFileExist flagfile)
|
||||||
where
|
where
|
||||||
go _ True = return NoChange
|
go _ _ True = return NoChange
|
||||||
go flagfile False = do
|
go satisfy flagfile False = do
|
||||||
r <- ensureProperty property
|
r <- satisfy
|
||||||
when (r == MadeChange) $ liftIO $
|
when (r == MadeChange) $ liftIO $
|
||||||
unlessM (doesFileExist flagfile) $ do
|
unlessM (doesFileExist flagfile) $ do
|
||||||
createDirectoryIfMissing True (takeDirectory flagfile)
|
createDirectoryIfMissing True (takeDirectory flagfile)
|
||||||
|
@ -73,8 +69,10 @@ flagFile' property getflagfile = Property (propertyDesc property) $ do
|
||||||
--- | Whenever a change has to be made for a Property, causes a hook
|
--- | Whenever a change has to be made for a Property, causes a hook
|
||||||
-- Property to also be run, but not otherwise.
|
-- Property to also be run, but not otherwise.
|
||||||
onChange :: Property -> Property -> Property
|
onChange :: Property -> Property -> Property
|
||||||
property `onChange` hook = Property (propertyDesc property) $ do
|
p `onChange` hook = Property (propertyDesc p) satisfy (combineSetAttr p hook)
|
||||||
r <- ensureProperty property
|
where
|
||||||
|
satisfy = do
|
||||||
|
r <- ensureProperty p
|
||||||
case r of
|
case r of
|
||||||
MadeChange -> do
|
MadeChange -> do
|
||||||
r' <- ensureProperty hook
|
r' <- ensureProperty hook
|
||||||
|
@ -85,10 +83,10 @@ property `onChange` hook = Property (propertyDesc property) $ do
|
||||||
(==>) = flip describe
|
(==>) = flip describe
|
||||||
infixl 1 ==>
|
infixl 1 ==>
|
||||||
|
|
||||||
-- | Makes a Property only be performed when a test succeeds.
|
-- | Makes a Property only need to do anything when a test succeeds.
|
||||||
check :: IO Bool -> Property -> Property
|
check :: IO Bool -> Property -> Property
|
||||||
check c property = Property (propertyDesc property) $ ifM (liftIO c)
|
check c p = adjustProperty p $ \satisfy -> ifM (liftIO c)
|
||||||
( ensureProperty property
|
( satisfy
|
||||||
, return NoChange
|
, return NoChange
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -99,8 +97,8 @@ check c property = Property (propertyDesc property) $ ifM (liftIO c)
|
||||||
-- to be made as it is to just idempotently assure the property is
|
-- to be made as it is to just idempotently assure the property is
|
||||||
-- satisfied. For example, chmodding a file.
|
-- satisfied. For example, chmodding a file.
|
||||||
trivial :: Property -> Property
|
trivial :: Property -> Property
|
||||||
trivial p = Property (propertyDesc p) $ do
|
trivial p = adjustProperty p $ \satisfy -> do
|
||||||
r <- ensureProperty p
|
r <- satisfy
|
||||||
if r == MadeChange
|
if r == MadeChange
|
||||||
then return NoChange
|
then return NoChange
|
||||||
else return r
|
else return r
|
||||||
|
@ -110,10 +108,10 @@ trivial p = Property (propertyDesc p) $ do
|
||||||
--
|
--
|
||||||
-- Note that the operating system may not be declared for some hosts.
|
-- Note that the operating system may not be declared for some hosts.
|
||||||
withOS :: Desc -> (Maybe System -> Propellor Result) -> Property
|
withOS :: Desc -> (Maybe System -> Propellor Result) -> Property
|
||||||
withOS desc a = Property desc $ a =<< getOS
|
withOS desc a = property desc $ a =<< getOS
|
||||||
|
|
||||||
boolProperty :: Desc -> IO Bool -> Property
|
boolProperty :: Desc -> IO Bool -> Property
|
||||||
boolProperty desc a = Property desc $ ifM (liftIO a)
|
boolProperty desc a = property desc $ ifM (liftIO a)
|
||||||
( return MadeChange
|
( return MadeChange
|
||||||
, return FailedChange
|
, return FailedChange
|
||||||
)
|
)
|
||||||
|
@ -133,16 +131,33 @@ host hn = Host [] (\_ -> newAttr hn)
|
||||||
|
|
||||||
-- | Adds a property to a Host
|
-- | Adds a property to a Host
|
||||||
--
|
--
|
||||||
-- Can add Properties, RevertableProperties, and AttrProperties
|
-- Can add Properties and RevertableProperties
|
||||||
(&) :: IsProp p => Host -> p -> Host
|
(&) :: IsProp p => Host -> p -> Host
|
||||||
(Host ps as) & p = Host (ps ++ [toProp p]) (getAttr p . as)
|
(Host ps as) & p = Host (ps ++ [toProp p]) (setAttr p . as)
|
||||||
|
|
||||||
infixl 1 &
|
infixl 1 &
|
||||||
|
|
||||||
-- | Adds a property to the Host in reverted form.
|
-- | Adds a property to the Host in reverted form.
|
||||||
(!) :: Host -> RevertableProperty -> Host
|
(!) :: Host -> RevertableProperty -> Host
|
||||||
(Host ps as) ! p = Host (ps ++ [toProp q]) (getAttr q . as)
|
(Host ps as) ! p = Host (ps ++ [toProp q]) (setAttr q . as)
|
||||||
where
|
where
|
||||||
q = revert p
|
q = revert p
|
||||||
|
|
||||||
infixl 1 !
|
infixl 1 !
|
||||||
|
|
||||||
|
-- Changes the action that is performed to satisfy a property.
|
||||||
|
adjustProperty :: Property -> (Propellor Result -> Propellor Result) -> Property
|
||||||
|
adjustProperty p f = p { propertySatisfy = f (propertySatisfy p) }
|
||||||
|
|
||||||
|
-- Combines the Attr settings of two properties.
|
||||||
|
combineSetAttr :: (IsProp p, IsProp q) => p -> q -> SetAttr
|
||||||
|
combineSetAttr p q = setAttr p . setAttr q
|
||||||
|
|
||||||
|
combineSetAttrs :: IsProp p => [p] -> SetAttr
|
||||||
|
combineSetAttrs = foldl' (.) id . map setAttr
|
||||||
|
|
||||||
|
makeChange :: IO () -> Propellor Result
|
||||||
|
makeChange a = liftIO a >> return MadeChange
|
||||||
|
|
||||||
|
noChange :: Propellor Result
|
||||||
|
noChange = return NoChange
|
||||||
|
|
|
@ -157,8 +157,8 @@ buildDepIn dir = go `requires` installedMin ["devscripts", "equivs"]
|
||||||
-- | Package installation may fail becuse the archive has changed.
|
-- | Package installation may fail becuse the archive has changed.
|
||||||
-- Run an update in that case and retry.
|
-- Run an update in that case and retry.
|
||||||
robustly :: Property -> Property
|
robustly :: Property -> Property
|
||||||
robustly p = Property (propertyDesc p) $ do
|
robustly p = adjustProperty p $ \satisfy -> do
|
||||||
r <- ensureProperty p
|
r <- satisfy
|
||||||
if r == FailedChange
|
if r == FailedChange
|
||||||
then ensureProperty $ p `requires` update
|
then ensureProperty $ p `requires` update
|
||||||
else return r
|
else return r
|
||||||
|
@ -210,7 +210,7 @@ reConfigure :: Package -> [(String, String, String)] -> Property
|
||||||
reConfigure package vals = reconfigure `requires` setselections
|
reConfigure package vals = reconfigure `requires` setselections
|
||||||
`describe` ("reconfigure " ++ package)
|
`describe` ("reconfigure " ++ package)
|
||||||
where
|
where
|
||||||
setselections = Property "preseed" $ makeChange $
|
setselections = property "preseed" $ makeChange $
|
||||||
withHandle StdinHandle createProcessSuccess
|
withHandle StdinHandle createProcessSuccess
|
||||||
(proc "debconf-set-selections" []) $ \h -> do
|
(proc "debconf-set-selections" []) $ \h -> do
|
||||||
forM_ vals $ \(tmpl, tmpltype, value) ->
|
forM_ vals $ \(tmpl, tmpltype, value) ->
|
||||||
|
@ -236,7 +236,7 @@ trustsKey k = RevertableProperty trust untrust
|
||||||
desc = "apt trusts key " ++ keyname k
|
desc = "apt trusts key " ++ keyname k
|
||||||
f = "/etc/apt/trusted.gpg.d" </> keyname k ++ ".gpg"
|
f = "/etc/apt/trusted.gpg.d" </> keyname k ++ ".gpg"
|
||||||
untrust = File.notPresent f
|
untrust = File.notPresent f
|
||||||
trust = check (not <$> doesFileExist f) $ Property desc $ makeChange $ do
|
trust = check (not <$> doesFileExist f) $ property desc $ makeChange $ do
|
||||||
withHandle StdinHandle createProcessSuccess
|
withHandle StdinHandle createProcessSuccess
|
||||||
(proc "gpg" ["--no-default-keyring", "--keyring", f, "--import", "-"]) $ \h -> do
|
(proc "gpg" ["--no-default-keyring", "--keyring", f, "--import", "-"]) $ \h -> do
|
||||||
hPutStr h (pubkey k)
|
hPutStr h (pubkey k)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Data.List
|
||||||
import "mtl" Control.Monad.Reader
|
import "mtl" Control.Monad.Reader
|
||||||
|
|
||||||
import Propellor.Types
|
import Propellor.Types
|
||||||
|
import Propellor.Property
|
||||||
import Utility.Monad
|
import Utility.Monad
|
||||||
import Utility.SafeCommand
|
import Utility.SafeCommand
|
||||||
import Utility.Env
|
import Utility.Env
|
||||||
|
@ -25,7 +26,7 @@ cmdProperty cmd params = cmdProperty' cmd params []
|
||||||
-- | A property that can be satisfied by running a command,
|
-- | A property that can be satisfied by running a command,
|
||||||
-- with added environment.
|
-- with added environment.
|
||||||
cmdProperty' :: String -> [String] -> [(String, String)] -> Property
|
cmdProperty' :: String -> [String] -> [(String, String)] -> Property
|
||||||
cmdProperty' cmd params env = Property desc $ liftIO $ do
|
cmdProperty' cmd params env = property desc $ liftIO $ do
|
||||||
env' <- addEntries env <$> getEnvironment
|
env' <- addEntries env <$> getEnvironment
|
||||||
ifM (boolSystemEnv cmd (map Param params) (Just env'))
|
ifM (boolSystemEnv cmd (map Param params) (Just env'))
|
||||||
( return MadeChange
|
( return MadeChange
|
||||||
|
|
|
@ -33,7 +33,7 @@ job desc times user cddir command = cronjobfile `File.hasContent`
|
||||||
`requires` Apt.installed ["util-linux", "moreutils"]
|
`requires` Apt.installed ["util-linux", "moreutils"]
|
||||||
`describe` ("cronned " ++ desc)
|
`describe` ("cronned " ++ desc)
|
||||||
where
|
where
|
||||||
cmdline = "cd " ++ cddir ++ " && " ++ command
|
cmdline = "cd " ++ cddir ++ " && ( " ++ command ++ " )"
|
||||||
cronjobfile = "/etc/cron.d/" ++ map sanitize desc
|
cronjobfile = "/etc/cron.d/" ++ map sanitize desc
|
||||||
sanitize c
|
sanitize c
|
||||||
| isAlphaNum c = c
|
| isAlphaNum c = c
|
||||||
|
|
|
@ -1,49 +1,128 @@
|
||||||
module Propellor.Property.Dns where
|
module Propellor.Property.Dns (
|
||||||
|
module Propellor.Types.Dns,
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
secondaryFor,
|
||||||
|
mkSOA,
|
||||||
|
rootAddressesFrom,
|
||||||
|
writeZoneFile,
|
||||||
|
nextSerialNumber,
|
||||||
|
adjustSerialNumber,
|
||||||
|
serialNumberOffset,
|
||||||
|
genZone,
|
||||||
|
) where
|
||||||
|
|
||||||
import Propellor
|
import Propellor
|
||||||
|
import Propellor.Types.Dns
|
||||||
import Propellor.Property.File
|
import Propellor.Property.File
|
||||||
|
import Propellor.Types.Attr
|
||||||
import qualified Propellor.Property.Apt as Apt
|
import qualified Propellor.Property.Apt as Apt
|
||||||
import qualified Propellor.Property.Service as Service
|
import qualified Propellor.Property.Service as Service
|
||||||
|
import Utility.Applicative
|
||||||
|
|
||||||
namedconf :: FilePath
|
import qualified Data.Map as M
|
||||||
namedconf = "/etc/bind/named.conf.local"
|
import qualified Data.Set as S
|
||||||
|
import Data.List
|
||||||
|
|
||||||
data Zone = Zone
|
-- | Primary dns server for a domain.
|
||||||
{ zdomain :: Domain
|
--
|
||||||
, ztype :: Type
|
-- Most of the content of the zone file is configured by setting properties
|
||||||
, zfile :: FilePath
|
-- of hosts. For example,
|
||||||
, zmasters :: [IPAddr]
|
--
|
||||||
, zconfiglines :: [String]
|
-- > host "foo.example.com"
|
||||||
|
-- > & ipv4 "192.168.1.1"
|
||||||
|
-- > & alias "mail.exmaple.com"
|
||||||
|
--
|
||||||
|
-- Will cause that hostmame and its alias to appear in the zone file,
|
||||||
|
-- with the configured IP address.
|
||||||
|
--
|
||||||
|
-- The [(Domain, Record)] list can be used for additional records
|
||||||
|
-- that cannot be configured elsewhere. For example, it might contain
|
||||||
|
-- CNAMEs pointing at hosts that propellor does not control.
|
||||||
|
primary :: [Host] -> Domain -> SOA -> [(BindDomain, Record)] -> Property
|
||||||
|
primary hosts domain soa rs = withwarnings (check needupdate baseprop)
|
||||||
|
`requires` servingZones
|
||||||
|
`onChange` Service.reloaded "bind9"
|
||||||
|
where
|
||||||
|
(partialzone, warnings) = genZone hosts domain soa
|
||||||
|
zone = partialzone { zHosts = zHosts partialzone ++ rs }
|
||||||
|
zonefile = "/etc/bind/propellor/db." ++ domain
|
||||||
|
baseprop = Property ("dns primary for " ++ domain)
|
||||||
|
(makeChange $ writeZoneFile zone zonefile)
|
||||||
|
(addNamedConf conf)
|
||||||
|
withwarnings p = adjustProperty p $ \satisfy -> do
|
||||||
|
mapM_ warningMessage warnings
|
||||||
|
satisfy
|
||||||
|
conf = NamedConf
|
||||||
|
{ confDomain = domain
|
||||||
|
, confType = Master
|
||||||
|
, confFile = zonefile
|
||||||
|
, confMasters = []
|
||||||
|
, confLines = []
|
||||||
|
}
|
||||||
|
needupdate = do
|
||||||
|
v <- readZonePropellorFile zonefile
|
||||||
|
return $ case v of
|
||||||
|
Nothing -> True
|
||||||
|
Just oldzone ->
|
||||||
|
-- compare everything except serial
|
||||||
|
let oldserial = sSerial (zSOA oldzone)
|
||||||
|
z = zone { zSOA = (zSOA zone) { sSerial = oldserial } }
|
||||||
|
in z /= oldzone || oldserial < sSerial (zSOA zone)
|
||||||
|
|
||||||
|
-- | Secondary dns server for a domain.
|
||||||
|
--
|
||||||
|
-- The primary server is determined by looking at the properties of other
|
||||||
|
-- hosts to find which one is configured as the primary.
|
||||||
|
--
|
||||||
|
-- Note that if a host is declared to be a primary and a secondary dns
|
||||||
|
-- server for the same domain, the primary server config always wins.
|
||||||
|
secondary :: [Host] -> Domain -> Property
|
||||||
|
secondary hosts domain = secondaryFor masters hosts domain
|
||||||
|
where
|
||||||
|
masters = M.keys $ M.filter ismaster $ hostAttrMap hosts
|
||||||
|
ismaster attr = case M.lookup domain (_namedconf attr) of
|
||||||
|
Nothing -> False
|
||||||
|
Just conf -> confType conf == Master && confDomain conf == domain
|
||||||
|
|
||||||
|
-- | This variant is useful if the primary server does not have its DNS
|
||||||
|
-- configured via propellor.
|
||||||
|
secondaryFor :: [HostName] -> [Host] -> Domain -> Property
|
||||||
|
secondaryFor masters hosts domain = pureAttrProperty desc (addNamedConf conf)
|
||||||
|
`requires` servingZones
|
||||||
|
where
|
||||||
|
desc = "dns secondary for " ++ domain
|
||||||
|
conf = NamedConf
|
||||||
|
{ confDomain = domain
|
||||||
|
, confType = Secondary
|
||||||
|
, confFile = "db." ++ domain
|
||||||
|
, confMasters = concatMap (\m -> hostAddresses m hosts) masters
|
||||||
|
, confLines = ["allow-transfer { }"]
|
||||||
}
|
}
|
||||||
|
|
||||||
zoneDesc :: Zone -> String
|
-- | Rewrites the whole named.conf.local file to serve the zones
|
||||||
zoneDesc z = zdomain z ++ " (" ++ show (ztype z) ++ ")"
|
-- configured by `primary` and `secondary`, and ensures that bind9 is
|
||||||
|
-- running.
|
||||||
|
servingZones :: Property
|
||||||
|
servingZones = property "serving configured dns zones" go
|
||||||
|
`requires` Apt.serviceInstalledRunning "bind9"
|
||||||
|
`onChange` Service.reloaded "bind9"
|
||||||
|
where
|
||||||
|
go = do
|
||||||
|
zs <- getNamedConf
|
||||||
|
ensureProperty $
|
||||||
|
hasContent namedConfFile $
|
||||||
|
concatMap confStanza $ M.elems zs
|
||||||
|
|
||||||
type IPAddr = String
|
confStanza :: NamedConf -> [Line]
|
||||||
|
confStanza c =
|
||||||
type Domain = String
|
|
||||||
|
|
||||||
data Type = Master | Secondary
|
|
||||||
deriving (Show, Eq)
|
|
||||||
|
|
||||||
secondary :: Domain -> [IPAddr] -> Zone
|
|
||||||
secondary domain masters = Zone
|
|
||||||
{ zdomain = domain
|
|
||||||
, ztype = Secondary
|
|
||||||
, zfile = "db." ++ domain
|
|
||||||
, zmasters = masters
|
|
||||||
, zconfiglines = ["allow-transfer { }"]
|
|
||||||
}
|
|
||||||
|
|
||||||
zoneStanza :: Zone -> [Line]
|
|
||||||
zoneStanza z =
|
|
||||||
[ "// automatically generated by propellor"
|
[ "// automatically generated by propellor"
|
||||||
, "zone \"" ++ zdomain z ++ "\" {"
|
, "zone \"" ++ confDomain c ++ "\" {"
|
||||||
, cfgline "type" (if ztype z == Master then "master" else "slave")
|
, cfgline "type" (if confType c == Master then "master" else "slave")
|
||||||
, cfgline "file" ("\"" ++ zfile z ++ "\"")
|
, cfgline "file" ("\"" ++ confFile c ++ "\"")
|
||||||
] ++
|
] ++
|
||||||
(if null (zmasters z) then [] else mastersblock) ++
|
(if null (confMasters c) then [] else mastersblock) ++
|
||||||
(map (\l -> "\t" ++ l ++ ";") (zconfiglines z)) ++
|
(map (\l -> "\t" ++ l ++ ";") (confLines c)) ++
|
||||||
[ "};"
|
[ "};"
|
||||||
, ""
|
, ""
|
||||||
]
|
]
|
||||||
|
@ -51,13 +130,242 @@ zoneStanza z =
|
||||||
cfgline f v = "\t" ++ f ++ " " ++ v ++ ";"
|
cfgline f v = "\t" ++ f ++ " " ++ v ++ ";"
|
||||||
mastersblock =
|
mastersblock =
|
||||||
[ "\tmasters {" ] ++
|
[ "\tmasters {" ] ++
|
||||||
(map (\ip -> "\t\t" ++ ip ++ ";") (zmasters z)) ++
|
(map (\ip -> "\t\t" ++ fromIPAddr ip ++ ";") (confMasters c)) ++
|
||||||
[ "\t};" ]
|
[ "\t};" ]
|
||||||
|
|
||||||
-- | Rewrites the whole named.conf.local file to serve the specificed
|
namedConfFile :: FilePath
|
||||||
-- zones.
|
namedConfFile = "/etc/bind/named.conf.local"
|
||||||
zones :: [Zone] -> Property
|
|
||||||
zones zs = hasContent namedconf (concatMap zoneStanza zs)
|
-- | Generates a SOA with some fairly sane numbers in it.
|
||||||
`describe` ("dns server for zones: " ++ unwords (map zoneDesc zs))
|
--
|
||||||
`requires` Apt.serviceInstalledRunning "bind9"
|
-- The Domain is the domain to use in the SOA record. Typically
|
||||||
`onChange` Service.reloaded "bind9"
|
-- something like ns1.example.com. So, not the domain that this is the SOA
|
||||||
|
-- record for.
|
||||||
|
--
|
||||||
|
-- The SerialNumber can be whatever serial number was used by the domain
|
||||||
|
-- before propellor started managing it. Or 0 if the domain has only ever
|
||||||
|
-- been managed by propellor.
|
||||||
|
--
|
||||||
|
-- You do not need to increment the SerialNumber when making changes!
|
||||||
|
-- Propellor will automatically add the number of commits in the git
|
||||||
|
-- repository to the SerialNumber.
|
||||||
|
--
|
||||||
|
-- Handy trick: You don't need to list IPAddrs in the [Record],
|
||||||
|
-- just make some Host sets its `alias` to the root of domain.
|
||||||
|
mkSOA :: Domain -> SerialNumber -> [Record] -> SOA
|
||||||
|
mkSOA d sn rs = SOA
|
||||||
|
{ sDomain = AbsDomain d
|
||||||
|
, sSerial = sn
|
||||||
|
, sRefresh = hours 4
|
||||||
|
, sRetry = hours 1
|
||||||
|
, sExpire = 2419200 -- 4 weeks
|
||||||
|
, sNegativeCacheTTL = hours 8
|
||||||
|
, sRecord = rs
|
||||||
|
}
|
||||||
|
where
|
||||||
|
hours n = n * 60 * 60
|
||||||
|
|
||||||
|
rootAddressesFrom :: [Host] -> HostName -> [Record]
|
||||||
|
rootAddressesFrom hosts hn = map Address (hostAddresses hn hosts)
|
||||||
|
|
||||||
|
dValue :: BindDomain -> String
|
||||||
|
dValue (RelDomain d) = d
|
||||||
|
dValue (AbsDomain d) = d ++ "."
|
||||||
|
dValue (SOADomain) = "@"
|
||||||
|
|
||||||
|
rField :: Record -> String
|
||||||
|
rField (Address (IPv4 _)) = "A"
|
||||||
|
rField (Address (IPv6 _)) = "AAAA"
|
||||||
|
rField (CNAME _) = "CNAME"
|
||||||
|
rField (MX _ _) = "MX"
|
||||||
|
rField (NS _) = "NS"
|
||||||
|
rField (TXT _) = "TXT"
|
||||||
|
rField (SRV _ _ _ _) = "SRV"
|
||||||
|
|
||||||
|
rValue :: Record -> String
|
||||||
|
rValue (Address (IPv4 addr)) = addr
|
||||||
|
rValue (Address (IPv6 addr)) = addr
|
||||||
|
rValue (CNAME d) = dValue d
|
||||||
|
rValue (MX pri d) = show pri ++ " " ++ dValue d
|
||||||
|
rValue (NS d) = dValue d
|
||||||
|
rValue (SRV priority weight port target) = unwords
|
||||||
|
[ show priority
|
||||||
|
, show weight
|
||||||
|
, show port
|
||||||
|
, dValue target
|
||||||
|
]
|
||||||
|
rValue (TXT s) = [q] ++ filter (/= q) s ++ [q]
|
||||||
|
where
|
||||||
|
q = '"'
|
||||||
|
|
||||||
|
-- | Adjusts the serial number of the zone to
|
||||||
|
--
|
||||||
|
-- * Always be larger than the serial number in the Zone record.
|
||||||
|
-- * Always be larger than the passed SerialNumber
|
||||||
|
nextSerialNumber :: Zone -> SerialNumber -> Zone
|
||||||
|
nextSerialNumber z serial = adjustSerialNumber z $ \sn -> succ $ max sn serial
|
||||||
|
|
||||||
|
adjustSerialNumber :: Zone -> (SerialNumber -> SerialNumber) -> Zone
|
||||||
|
adjustSerialNumber (Zone d soa l) f = Zone d soa' l
|
||||||
|
where
|
||||||
|
soa' = soa { sSerial = f (sSerial soa) }
|
||||||
|
|
||||||
|
-- | Count the number of git commits made to the current branch.
|
||||||
|
serialNumberOffset :: IO SerialNumber
|
||||||
|
serialNumberOffset = fromIntegral . length . lines
|
||||||
|
<$> readProcess "git" ["log", "--pretty=%H"]
|
||||||
|
|
||||||
|
-- | Write a Zone out to a to a file.
|
||||||
|
--
|
||||||
|
-- The serial number in the Zone automatically has the serialNumberOffset
|
||||||
|
-- added to it. Also, just in case, the old serial number used in the zone
|
||||||
|
-- file is checked, and if it is somehow larger, its succ is used.
|
||||||
|
writeZoneFile :: Zone -> FilePath -> IO ()
|
||||||
|
writeZoneFile z f = do
|
||||||
|
oldserial <- oldZoneFileSerialNumber f
|
||||||
|
offset <- serialNumberOffset
|
||||||
|
let z' = nextSerialNumber
|
||||||
|
(adjustSerialNumber z (+ offset))
|
||||||
|
oldserial
|
||||||
|
createDirectoryIfMissing True (takeDirectory f)
|
||||||
|
writeFile f (genZoneFile z')
|
||||||
|
writeZonePropellorFile f z'
|
||||||
|
|
||||||
|
-- | Next to the zone file, is a ".propellor" file, which contains
|
||||||
|
-- the serialized Zone. This saves the bother of parsing
|
||||||
|
-- the horrible bind zone file format.
|
||||||
|
zonePropellorFile :: FilePath -> FilePath
|
||||||
|
zonePropellorFile f = f ++ ".propellor"
|
||||||
|
|
||||||
|
oldZoneFileSerialNumber :: FilePath -> IO SerialNumber
|
||||||
|
oldZoneFileSerialNumber = maybe 0 (sSerial . zSOA) <$$> readZonePropellorFile
|
||||||
|
|
||||||
|
writeZonePropellorFile :: FilePath -> Zone -> IO ()
|
||||||
|
writeZonePropellorFile f z = writeFile (zonePropellorFile f) (show z)
|
||||||
|
|
||||||
|
readZonePropellorFile :: FilePath -> IO (Maybe Zone)
|
||||||
|
readZonePropellorFile f = catchDefaultIO Nothing $
|
||||||
|
readish <$> readFileStrict (zonePropellorFile f)
|
||||||
|
|
||||||
|
-- | Generating a zone file.
|
||||||
|
genZoneFile :: Zone -> String
|
||||||
|
genZoneFile (Zone zdomain soa rs) = unlines $
|
||||||
|
header : genSOA zdomain soa ++ map genr rs
|
||||||
|
where
|
||||||
|
header = com $ "BIND zone file for " ++ zdomain ++ ". Generated by propellor, do not edit."
|
||||||
|
|
||||||
|
genr (d, r) = genRecord zdomain (Just d, r)
|
||||||
|
|
||||||
|
genRecord :: Domain -> (Maybe BindDomain, Record) -> String
|
||||||
|
genRecord zdomain (mdomain, record) = intercalate "\t"
|
||||||
|
[ hn
|
||||||
|
, "IN"
|
||||||
|
, rField record
|
||||||
|
, rValue record
|
||||||
|
]
|
||||||
|
where
|
||||||
|
hn = maybe "" (domainHost zdomain) mdomain
|
||||||
|
|
||||||
|
genSOA :: Domain -> SOA -> [String]
|
||||||
|
genSOA zdomain soa =
|
||||||
|
header ++ map (genRecord zdomain) (zip (repeat Nothing) (sRecord soa))
|
||||||
|
where
|
||||||
|
header =
|
||||||
|
-- "@ IN SOA ns1.example.com. root ("
|
||||||
|
[ intercalate "\t"
|
||||||
|
[ dValue SOADomain
|
||||||
|
, "IN"
|
||||||
|
, "SOA"
|
||||||
|
, dValue (sDomain soa)
|
||||||
|
, "root"
|
||||||
|
, "("
|
||||||
|
]
|
||||||
|
, headerline sSerial "Serial"
|
||||||
|
, headerline sRefresh "Refresh"
|
||||||
|
, headerline sRetry "Retry"
|
||||||
|
, headerline sExpire "Expire"
|
||||||
|
, headerline sNegativeCacheTTL "Negative Cache TTL"
|
||||||
|
, inheader ")"
|
||||||
|
]
|
||||||
|
headerline r comment = inheader $ show (r soa) ++ "\t\t" ++ com comment
|
||||||
|
inheader l = "\t\t\t" ++ l
|
||||||
|
|
||||||
|
-- | Comment line in a zone file.
|
||||||
|
com :: String -> String
|
||||||
|
com s = "; " ++ s
|
||||||
|
|
||||||
|
type WarningMessage = String
|
||||||
|
|
||||||
|
-- | Generates a Zone for a particular Domain from the DNS properies of all
|
||||||
|
-- hosts that propellor knows about that are in that Domain.
|
||||||
|
genZone :: [Host] -> Domain -> SOA -> (Zone, [WarningMessage])
|
||||||
|
genZone hosts zdomain soa =
|
||||||
|
let (warnings, zhosts) = partitionEithers $ concat $ map concat
|
||||||
|
[ map hostips inzdomain
|
||||||
|
, map hostrecords inzdomain
|
||||||
|
, map addcnames (M.elems m)
|
||||||
|
]
|
||||||
|
in (Zone zdomain soa (nub zhosts), warnings)
|
||||||
|
where
|
||||||
|
m = hostAttrMap hosts
|
||||||
|
-- Known hosts with hostname located in the zone's domain.
|
||||||
|
inzdomain = M.elems $ M.filterWithKey (\hn _ -> inDomain zdomain $ AbsDomain $ hn) m
|
||||||
|
|
||||||
|
-- Each host with a hostname located in the zdomain
|
||||||
|
-- should have 1 or more IPAddrs in its Attr.
|
||||||
|
--
|
||||||
|
-- If a host lacks any IPAddr, it's probably a misconfiguration,
|
||||||
|
-- so warn.
|
||||||
|
hostips :: Attr -> [Either WarningMessage (BindDomain, Record)]
|
||||||
|
hostips attr
|
||||||
|
| null l = [Left $ "no IP address defined for host " ++ _hostname attr]
|
||||||
|
| otherwise = map Right l
|
||||||
|
where
|
||||||
|
l = zip (repeat $ AbsDomain $ _hostname attr)
|
||||||
|
(map Address $ getAddresses attr)
|
||||||
|
|
||||||
|
-- Any host, whether its hostname is in the zdomain or not,
|
||||||
|
-- may have cnames which are in the zdomain. The cname may even be
|
||||||
|
-- the same as the root of the zdomain, which is a nice way to
|
||||||
|
-- specify IP addresses for a SOA record.
|
||||||
|
--
|
||||||
|
-- Add Records for those.. But not actually, usually, cnames!
|
||||||
|
-- Why not? Well, using cnames doesn't allow doing some things,
|
||||||
|
-- including MX and round robin DNS, and certianly CNAMES
|
||||||
|
-- shouldn't be used in SOA records.
|
||||||
|
--
|
||||||
|
-- We typically know the host's IPAddrs anyway.
|
||||||
|
-- So we can just use the IPAddrs.
|
||||||
|
addcnames :: Attr -> [Either WarningMessage (BindDomain, Record)]
|
||||||
|
addcnames attr = concatMap gen $ filter (inDomain zdomain) $
|
||||||
|
mapMaybe getCNAME $ S.toList (_dns attr)
|
||||||
|
where
|
||||||
|
gen c = case getAddresses attr of
|
||||||
|
[] -> [ret (CNAME c)]
|
||||||
|
l -> map (ret . Address) l
|
||||||
|
where
|
||||||
|
ret record = Right (c, record)
|
||||||
|
|
||||||
|
-- Adds any other DNS records for a host located in the zdomain.
|
||||||
|
hostrecords :: Attr -> [Either WarningMessage (BindDomain, Record)]
|
||||||
|
hostrecords attr = map Right l
|
||||||
|
where
|
||||||
|
l = zip (repeat $ AbsDomain $ _hostname attr)
|
||||||
|
(S.toList $ S.filter (\r -> isNothing (getIPAddr r) && isNothing (getCNAME r)) (_dns attr))
|
||||||
|
|
||||||
|
inDomain :: Domain -> BindDomain -> Bool
|
||||||
|
inDomain domain (AbsDomain d) = domain == d || ('.':domain) `isSuffixOf` d
|
||||||
|
inDomain _ _ = False -- can't tell, so assume not
|
||||||
|
|
||||||
|
-- | Gets the hostname of the second domain, relative to the first domain,
|
||||||
|
-- suitable for using in a zone file.
|
||||||
|
domainHost :: Domain -> BindDomain -> String
|
||||||
|
domainHost _ (RelDomain d) = d
|
||||||
|
domainHost _ SOADomain = "@"
|
||||||
|
domainHost base (AbsDomain d)
|
||||||
|
| dotbase `isSuffixOf` d = take (length d - length dotbase) d
|
||||||
|
| base == d = "@"
|
||||||
|
| otherwise = d
|
||||||
|
where
|
||||||
|
dotbase = '.':base
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ import Data.List.Utils
|
||||||
-- | Configures docker with an authentication file, so that images can be
|
-- | Configures docker with an authentication file, so that images can be
|
||||||
-- pushed to index.docker.io.
|
-- pushed to index.docker.io.
|
||||||
configured :: Property
|
configured :: Property
|
||||||
configured = Property "docker configured" go `requires` installed
|
configured = property "docker configured" go `requires` installed
|
||||||
where
|
where
|
||||||
go = withPrivData DockerAuthentication $ \cfg -> ensureProperty $
|
go = withPrivData DockerAuthentication $ \cfg -> ensureProperty $
|
||||||
"/root/.dockercfg" `File.hasContent` (lines cfg)
|
"/root/.dockercfg" `File.hasContent` (lines cfg)
|
||||||
|
@ -64,7 +64,7 @@ docked
|
||||||
-> RevertableProperty
|
-> RevertableProperty
|
||||||
docked hosts cn = RevertableProperty (go "docked" setup) (go "undocked" teardown)
|
docked hosts cn = RevertableProperty (go "docked" setup) (go "undocked" teardown)
|
||||||
where
|
where
|
||||||
go desc a = Property (desc ++ " " ++ cn) $ do
|
go desc a = property (desc ++ " " ++ cn) $ do
|
||||||
hn <- getHostName
|
hn <- getHostName
|
||||||
let cid = ContainerId hn cn
|
let cid = ContainerId hn cn
|
||||||
ensureProperties [findContainer hosts cid cn $ a cid]
|
ensureProperties [findContainer hosts cid cn $ a cid]
|
||||||
|
@ -79,7 +79,7 @@ docked hosts cn = RevertableProperty (go "docked" setup) (go "undocked" teardown
|
||||||
teardown cid (Container image _runparams) =
|
teardown cid (Container image _runparams) =
|
||||||
combineProperties ("undocked " ++ fromContainerId cid)
|
combineProperties ("undocked " ++ fromContainerId cid)
|
||||||
[ stoppedContainer cid
|
[ stoppedContainer cid
|
||||||
, Property ("cleaned up " ++ fromContainerId cid) $
|
, property ("cleaned up " ++ fromContainerId cid) $
|
||||||
liftIO $ report <$> mapM id
|
liftIO $ report <$> mapM id
|
||||||
[ removeContainer cid
|
[ removeContainer cid
|
||||||
, removeImage image
|
, removeImage image
|
||||||
|
@ -96,7 +96,7 @@ findContainer hosts cid cn mk = case findHost hosts (cn2hn cn) of
|
||||||
Nothing -> cantfind
|
Nothing -> cantfind
|
||||||
Just h -> maybe cantfind mk (mkContainer cid h)
|
Just h -> maybe cantfind mk (mkContainer cid h)
|
||||||
where
|
where
|
||||||
cantfind = containerDesc cid $ Property "" $ do
|
cantfind = containerDesc cid $ property "" $ do
|
||||||
liftIO $ warningMessage $
|
liftIO $ warningMessage $
|
||||||
"missing definition for docker container \"" ++ cn2hn cn
|
"missing definition for docker container \"" ++ cn2hn cn
|
||||||
return FailedChange
|
return FailedChange
|
||||||
|
@ -126,9 +126,9 @@ garbageCollected = propertyList "docker garbage collected"
|
||||||
, gcimages
|
, gcimages
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
gccontainers = Property "docker containers garbage collected" $
|
gccontainers = property "docker containers garbage collected" $
|
||||||
liftIO $ report <$> (mapM removeContainer =<< listContainers AllContainers)
|
liftIO $ report <$> (mapM removeContainer =<< listContainers AllContainers)
|
||||||
gcimages = Property "docker images garbage collected" $ do
|
gcimages = property "docker images garbage collected" $ do
|
||||||
liftIO $ report <$> (mapM removeImage =<< listImages)
|
liftIO $ report <$> (mapM removeImage =<< listImages)
|
||||||
|
|
||||||
data Container = Container Image [RunParam]
|
data Container = Container Image [RunParam]
|
||||||
|
@ -140,51 +140,51 @@ type RunParam = String
|
||||||
type Image = String
|
type Image = String
|
||||||
|
|
||||||
-- | Set custom dns server for container.
|
-- | Set custom dns server for container.
|
||||||
dns :: String -> AttrProperty
|
dns :: String -> Property
|
||||||
dns = runProp "dns"
|
dns = runProp "dns"
|
||||||
|
|
||||||
-- | Set container host name.
|
-- | Set container host name.
|
||||||
hostname :: String -> AttrProperty
|
hostname :: String -> Property
|
||||||
hostname = runProp "hostname"
|
hostname = runProp "hostname"
|
||||||
|
|
||||||
-- | Set name for container. (Normally done automatically.)
|
-- | Set name for container. (Normally done automatically.)
|
||||||
name :: String -> AttrProperty
|
name :: String -> Property
|
||||||
name = runProp "name"
|
name = runProp "name"
|
||||||
|
|
||||||
-- | Publish a container's port to the host
|
-- | Publish a container's port to the host
|
||||||
-- (format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort)
|
-- (format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort)
|
||||||
publish :: String -> AttrProperty
|
publish :: String -> Property
|
||||||
publish = runProp "publish"
|
publish = runProp "publish"
|
||||||
|
|
||||||
-- | Username or UID for container.
|
-- | Username or UID for container.
|
||||||
user :: String -> AttrProperty
|
user :: String -> Property
|
||||||
user = runProp "user"
|
user = runProp "user"
|
||||||
|
|
||||||
-- | Mount a volume
|
-- | Mount a volume
|
||||||
-- Create a bind mount with: [host-dir]:[container-dir]:[rw|ro]
|
-- Create a bind mount with: [host-dir]:[container-dir]:[rw|ro]
|
||||||
-- With just a directory, creates a volume in the container.
|
-- With just a directory, creates a volume in the container.
|
||||||
volume :: String -> AttrProperty
|
volume :: String -> Property
|
||||||
volume = runProp "volume"
|
volume = runProp "volume"
|
||||||
|
|
||||||
-- | Mount a volume from the specified container into the current
|
-- | Mount a volume from the specified container into the current
|
||||||
-- container.
|
-- container.
|
||||||
volumes_from :: ContainerName -> AttrProperty
|
volumes_from :: ContainerName -> Property
|
||||||
volumes_from cn = genProp "volumes-from" $ \hn ->
|
volumes_from cn = genProp "volumes-from" $ \hn ->
|
||||||
fromContainerId (ContainerId hn cn)
|
fromContainerId (ContainerId hn cn)
|
||||||
|
|
||||||
-- | Work dir inside the container.
|
-- | Work dir inside the container.
|
||||||
workdir :: String -> AttrProperty
|
workdir :: String -> Property
|
||||||
workdir = runProp "workdir"
|
workdir = runProp "workdir"
|
||||||
|
|
||||||
-- | Memory limit for container.
|
-- | Memory limit for container.
|
||||||
--Format: <number><optional unit>, where unit = b, k, m or g
|
--Format: <number><optional unit>, where unit = b, k, m or g
|
||||||
memory :: String -> AttrProperty
|
memory :: String -> Property
|
||||||
memory = runProp "memory"
|
memory = runProp "memory"
|
||||||
|
|
||||||
-- | Link with another container on the same host.
|
-- | Link with another container on the same host.
|
||||||
link :: ContainerName -> ContainerAlias -> AttrProperty
|
link :: ContainerName -> ContainerAlias -> Property
|
||||||
link linkwith alias = genProp "link" $ \hn ->
|
link linkwith calias = genProp "link" $ \hn ->
|
||||||
fromContainerId (ContainerId hn linkwith) ++ ":" ++ alias
|
fromContainerId (ContainerId hn linkwith) ++ ":" ++ calias
|
||||||
|
|
||||||
-- | A short alias for a linked container.
|
-- | A short alias for a linked container.
|
||||||
-- Each container has its own alias namespace.
|
-- Each container has its own alias namespace.
|
||||||
|
@ -230,7 +230,7 @@ containerDesc cid p = p `describe` desc
|
||||||
desc = "[" ++ fromContainerId cid ++ "] " ++ propertyDesc p
|
desc = "[" ++ fromContainerId cid ++ "] " ++ propertyDesc p
|
||||||
|
|
||||||
runningContainer :: ContainerId -> Image -> [RunParam] -> Property
|
runningContainer :: ContainerId -> Image -> [RunParam] -> Property
|
||||||
runningContainer cid@(ContainerId hn cn) image runps = containerDesc cid $ Property "running" $ do
|
runningContainer cid@(ContainerId hn cn) image runps = containerDesc cid $ property "running" $ do
|
||||||
l <- liftIO $ listContainers RunningContainers
|
l <- liftIO $ listContainers RunningContainers
|
||||||
if cid `elem` l
|
if cid `elem` l
|
||||||
then do
|
then do
|
||||||
|
@ -324,7 +324,7 @@ chain s = case toContainerId s of
|
||||||
-- being run. So, retry connections to the client for up to
|
-- being run. So, retry connections to the client for up to
|
||||||
-- 1 minute.
|
-- 1 minute.
|
||||||
provisionContainer :: ContainerId -> Property
|
provisionContainer :: ContainerId -> Property
|
||||||
provisionContainer cid = containerDesc cid $ Property "provision" $ liftIO $ do
|
provisionContainer cid = containerDesc cid $ property "provision" $ liftIO $ do
|
||||||
let shim = Shim.file (localdir </> "propellor") (localdir </> shimdir cid)
|
let shim = Shim.file (localdir </> "propellor") (localdir </> shimdir cid)
|
||||||
r <- simpleShClientRetry 60 (namedPipe cid) shim params (go Nothing)
|
r <- simpleShClientRetry 60 (namedPipe cid) shim params (go Nothing)
|
||||||
when (r /= FailedChange) $
|
when (r /= FailedChange) $
|
||||||
|
@ -356,7 +356,7 @@ stopContainer :: ContainerId -> IO Bool
|
||||||
stopContainer cid = boolSystem dockercmd [Param "stop", Param $ fromContainerId cid ]
|
stopContainer cid = boolSystem dockercmd [Param "stop", Param $ fromContainerId cid ]
|
||||||
|
|
||||||
stoppedContainer :: ContainerId -> Property
|
stoppedContainer :: ContainerId -> Property
|
||||||
stoppedContainer cid = containerDesc cid $ Property desc $
|
stoppedContainer cid = containerDesc cid $ property desc $
|
||||||
ifM (liftIO $ elem cid <$> listContainers RunningContainers)
|
ifM (liftIO $ elem cid <$> listContainers RunningContainers)
|
||||||
( liftIO cleanup `after` ensureProperty
|
( liftIO cleanup `after` ensureProperty
|
||||||
(boolProperty desc $ stopContainer cid)
|
(boolProperty desc $ stopContainer cid)
|
||||||
|
@ -405,18 +405,15 @@ listContainers status =
|
||||||
listImages :: IO [Image]
|
listImages :: IO [Image]
|
||||||
listImages = lines <$> readProcess dockercmd ["images", "--all", "--quiet"]
|
listImages = lines <$> readProcess dockercmd ["images", "--all", "--quiet"]
|
||||||
|
|
||||||
runProp :: String -> RunParam -> AttrProperty
|
runProp :: String -> RunParam -> Property
|
||||||
runProp field val = AttrProperty prop $ \attr ->
|
runProp field val = pureAttrProperty (param) $ \attr ->
|
||||||
attr { _dockerRunParams = _dockerRunParams attr ++ [\_ -> "--"++param] }
|
attr { _dockerRunParams = _dockerRunParams attr ++ [\_ -> "--"++param] }
|
||||||
where
|
where
|
||||||
param = field++"="++val
|
param = field++"="++val
|
||||||
prop = Property (param) (return NoChange)
|
|
||||||
|
|
||||||
genProp :: String -> (HostName -> RunParam) -> AttrProperty
|
genProp :: String -> (HostName -> RunParam) -> Property
|
||||||
genProp field mkval = AttrProperty prop $ \attr ->
|
genProp field mkval = pureAttrProperty field $ \attr ->
|
||||||
attr { _dockerRunParams = _dockerRunParams attr ++ [\hn -> "--"++field++"=" ++ mkval hn] }
|
attr { _dockerRunParams = _dockerRunParams attr ++ [\hn -> "--"++field++"=" ++ mkval hn] }
|
||||||
where
|
|
||||||
prop = Property field (return NoChange)
|
|
||||||
|
|
||||||
-- | The ContainerIdent of a container is written to
|
-- | The ContainerIdent of a container is written to
|
||||||
-- /.propellor-ident inside it. This can be checked to see if
|
-- /.propellor-ident inside it. This can be checked to see if
|
||||||
|
|
|
@ -18,7 +18,7 @@ f `hasContent` newcontent = fileProperty ("replace " ++ f)
|
||||||
-- The file's permissions are preserved if the file already existed.
|
-- The file's permissions are preserved if the file already existed.
|
||||||
-- Otherwise, they're set to 600.
|
-- Otherwise, they're set to 600.
|
||||||
hasPrivContent :: FilePath -> Property
|
hasPrivContent :: FilePath -> Property
|
||||||
hasPrivContent f = Property desc $ withPrivData (PrivFile f) $ \privcontent ->
|
hasPrivContent f = property desc $ withPrivData (PrivFile f) $ \privcontent ->
|
||||||
ensureProperty $ fileProperty' writeFileProtected desc
|
ensureProperty $ fileProperty' writeFileProtected desc
|
||||||
(\_oldcontent -> lines privcontent) f
|
(\_oldcontent -> lines privcontent) f
|
||||||
where
|
where
|
||||||
|
@ -48,13 +48,13 @@ f `lacksLine` l = fileProperty (f ++ " remove: " ++ l) (filter (/= l)) f
|
||||||
|
|
||||||
-- | Removes a file. Does not remove symlinks or non-plain-files.
|
-- | Removes a file. Does not remove symlinks or non-plain-files.
|
||||||
notPresent :: FilePath -> Property
|
notPresent :: FilePath -> Property
|
||||||
notPresent f = check (doesFileExist f) $ Property (f ++ " not present") $
|
notPresent f = check (doesFileExist f) $ property (f ++ " not present") $
|
||||||
makeChange $ nukeFile f
|
makeChange $ nukeFile f
|
||||||
|
|
||||||
fileProperty :: Desc -> ([Line] -> [Line]) -> FilePath -> Property
|
fileProperty :: Desc -> ([Line] -> [Line]) -> FilePath -> Property
|
||||||
fileProperty = fileProperty' writeFile
|
fileProperty = fileProperty' writeFile
|
||||||
fileProperty' :: (FilePath -> String -> IO ()) -> Desc -> ([Line] -> [Line]) -> FilePath -> Property
|
fileProperty' :: (FilePath -> String -> IO ()) -> Desc -> ([Line] -> [Line]) -> FilePath -> Property
|
||||||
fileProperty' writer desc a f = Property desc $ go =<< liftIO (doesFileExist f)
|
fileProperty' writer desc a f = property desc $ go =<< liftIO (doesFileExist f)
|
||||||
where
|
where
|
||||||
go True = do
|
go True = do
|
||||||
ls <- liftIO $ lines <$> readFile f
|
ls <- liftIO $ lines <$> readFile f
|
||||||
|
@ -74,12 +74,12 @@ fileProperty' writer desc a f = Property desc $ go =<< liftIO (doesFileExist f)
|
||||||
|
|
||||||
-- | Ensures a directory exists.
|
-- | Ensures a directory exists.
|
||||||
dirExists :: FilePath -> Property
|
dirExists :: FilePath -> Property
|
||||||
dirExists d = check (not <$> doesDirectoryExist d) $ Property (d ++ " exists") $
|
dirExists d = check (not <$> doesDirectoryExist d) $ property (d ++ " exists") $
|
||||||
makeChange $ createDirectoryIfMissing True d
|
makeChange $ createDirectoryIfMissing True d
|
||||||
|
|
||||||
-- | Ensures that a file/dir has the specified owner and group.
|
-- | Ensures that a file/dir has the specified owner and group.
|
||||||
ownerGroup :: FilePath -> UserName -> GroupName -> Property
|
ownerGroup :: FilePath -> UserName -> GroupName -> Property
|
||||||
ownerGroup f owner group = Property (f ++ " owner " ++ og) $ do
|
ownerGroup f owner group = property (f ++ " owner " ++ og) $ do
|
||||||
r <- ensureProperty $ cmdProperty "chown" [og, f]
|
r <- ensureProperty $ cmdProperty "chown" [og, f]
|
||||||
if r == FailedChange
|
if r == FailedChange
|
||||||
then return r
|
then return r
|
||||||
|
@ -89,6 +89,6 @@ ownerGroup f owner group = Property (f ++ " owner " ++ og) $ do
|
||||||
|
|
||||||
-- | Ensures that a file/dir has the specfied mode.
|
-- | Ensures that a file/dir has the specfied mode.
|
||||||
mode :: FilePath -> FileMode -> Property
|
mode :: FilePath -> FileMode -> Property
|
||||||
mode f v = Property (f ++ " mode " ++ show v) $ do
|
mode f v = property (f ++ " mode " ++ show v) $ do
|
||||||
liftIO $ modifyFileMode f (\_old -> v)
|
liftIO $ modifyFileMode f (\_old -> v)
|
||||||
noChange
|
noChange
|
||||||
|
|
|
@ -62,7 +62,7 @@ type Branch = String
|
||||||
--
|
--
|
||||||
-- A branch can be specified, to check out.
|
-- A branch can be specified, to check out.
|
||||||
cloned :: UserName -> RepoUrl -> FilePath -> Maybe Branch -> Property
|
cloned :: UserName -> RepoUrl -> FilePath -> Maybe Branch -> Property
|
||||||
cloned owner url dir mbranch = check originurl (Property desc checkout)
|
cloned owner url dir mbranch = check originurl (property desc checkout)
|
||||||
`requires` installed
|
`requires` installed
|
||||||
where
|
where
|
||||||
desc = "git cloned " ++ url ++ " to " ++ dir
|
desc = "git cloned " ++ url ++ " to " ++ dir
|
||||||
|
|
|
@ -21,7 +21,7 @@ installed = Apt.installed ["gnupg"]
|
||||||
-- The GpgKeyId does not have to be a numeric id; it can just as easily
|
-- The GpgKeyId does not have to be a numeric id; it can just as easily
|
||||||
-- be a description of the key.
|
-- be a description of the key.
|
||||||
keyImported :: GpgKeyId -> UserName -> Property
|
keyImported :: GpgKeyId -> UserName -> Property
|
||||||
keyImported keyid user = flagFile' (Property desc go) genflag
|
keyImported keyid user = flagFile' (property desc go) genflag
|
||||||
`requires` installed
|
`requires` installed
|
||||||
where
|
where
|
||||||
desc = user ++ " has gpg key " ++ show keyid
|
desc = user ++ " has gpg key " ++ show keyid
|
||||||
|
|
|
@ -9,7 +9,7 @@ import qualified Propellor.Property.File as File
|
||||||
-- A FQDN also configures /etc/hosts, with an entry for 127.0.1.1, which is
|
-- A FQDN also configures /etc/hosts, with an entry for 127.0.1.1, which is
|
||||||
-- standard at least on Debian to set the FDQN (127.0.0.1 is localhost).
|
-- standard at least on Debian to set the FDQN (127.0.0.1 is localhost).
|
||||||
sane :: Property
|
sane :: Property
|
||||||
sane = Property ("sane hostname") (ensureProperty . setTo =<< getHostName)
|
sane = property ("sane hostname") (ensureProperty . setTo =<< getHostName)
|
||||||
|
|
||||||
setTo :: HostName -> Property
|
setTo :: HostName -> Property
|
||||||
setTo hn = combineProperties desc go
|
setTo hn = combineProperties desc go
|
||||||
|
|
|
@ -65,7 +65,7 @@ backup dir crontimes params numclients = cronjob `describe` desc
|
||||||
-- The restore is performed atomically; restoring to a temp directory
|
-- The restore is performed atomically; restoring to a temp directory
|
||||||
-- and then moving it to the directory.
|
-- and then moving it to the directory.
|
||||||
restored :: FilePath -> [ObnamParam] -> Property
|
restored :: FilePath -> [ObnamParam] -> Property
|
||||||
restored dir params = Property (dir ++ " restored by obnam") go
|
restored dir params = property (dir ++ " restored by obnam") go
|
||||||
`requires` installed
|
`requires` installed
|
||||||
where
|
where
|
||||||
go = ifM (liftIO needsRestore)
|
go = ifM (liftIO needsRestore)
|
||||||
|
@ -97,14 +97,17 @@ installed = Apt.installed ["obnam"]
|
||||||
|
|
||||||
-- | Ensures that a recent version of obnam gets installed.
|
-- | Ensures that a recent version of obnam gets installed.
|
||||||
--
|
--
|
||||||
-- Only useful on Stable.
|
-- Only does anything for Debian Stable.
|
||||||
latestVersion :: Property
|
latestVersion :: Property
|
||||||
latestVersion = propertyList "obnam latest version"
|
latestVersion = withOS "obnam latest version" $ \o -> case o of
|
||||||
[ toProp $ Apt.trustsKey key
|
(Just (System (Debian suite) _)) | isStable suite -> ensureProperty $
|
||||||
, Apt.setSourcesListD sources "obnam"
|
Apt.setSourcesListD (sources suite) "obnam"
|
||||||
]
|
`requires` toProp (Apt.trustsKey key)
|
||||||
|
_ -> noChange
|
||||||
where
|
where
|
||||||
sources = ["deb http://code.liw.fi/debian wheezy main"]
|
sources suite =
|
||||||
|
[ "deb http://code.liw.fi/debian " ++ Apt.showSuite suite ++ " main"
|
||||||
|
]
|
||||||
-- gpg key used by the code.liw.fi repository.
|
-- gpg key used by the code.liw.fi repository.
|
||||||
key = Apt.AptKey "obnam" $ unlines
|
key = Apt.AptKey "obnam" $ unlines
|
||||||
[ "-----BEGIN PGP PUBLIC KEY BLOCK-----"
|
[ "-----BEGIN PGP PUBLIC KEY BLOCK-----"
|
||||||
|
|
|
@ -15,7 +15,7 @@ installed = Apt.serviceInstalledRunning "postfix"
|
||||||
satellite :: Property
|
satellite :: Property
|
||||||
satellite = setup `requires` installed
|
satellite = setup `requires` installed
|
||||||
where
|
where
|
||||||
setup = trivial $ Property "postfix satellite system" $ do
|
setup = trivial $ property "postfix satellite system" $ do
|
||||||
hn <- getHostName
|
hn <- getHostName
|
||||||
ensureProperty $ Apt.reConfigure "postfix"
|
ensureProperty $ Apt.reConfigure "postfix"
|
||||||
[ ("postfix/main_mailer_type", "select", "Satellite system")
|
[ ("postfix/main_mailer_type", "select", "Satellite system")
|
||||||
|
|
|
@ -19,13 +19,13 @@ import qualified Data.Map as M
|
||||||
-- This uses the description of the Property to keep track of when it was
|
-- This uses the description of the Property to keep track of when it was
|
||||||
-- last run.
|
-- last run.
|
||||||
period :: Property -> Recurrance -> Property
|
period :: Property -> Recurrance -> Property
|
||||||
period prop recurrance = Property desc $ do
|
period prop recurrance = flip describe desc $ adjustProperty prop $ \satisfy -> do
|
||||||
lasttime <- liftIO $ getLastChecked (propertyDesc prop)
|
lasttime <- liftIO $ getLastChecked (propertyDesc prop)
|
||||||
nexttime <- liftIO $ fmap startTime <$> nextTime schedule lasttime
|
nexttime <- liftIO $ fmap startTime <$> nextTime schedule lasttime
|
||||||
t <- liftIO localNow
|
t <- liftIO localNow
|
||||||
if Just t >= nexttime
|
if Just t >= nexttime
|
||||||
then do
|
then do
|
||||||
r <- ensureProperty prop
|
r <- satisfy
|
||||||
liftIO $ setLastChecked t (propertyDesc prop)
|
liftIO $ setLastChecked t (propertyDesc prop)
|
||||||
return r
|
return r
|
||||||
else noChange
|
else noChange
|
||||||
|
@ -37,7 +37,7 @@ period prop recurrance = Property desc $ do
|
||||||
periodParse :: Property -> String -> Property
|
periodParse :: Property -> String -> Property
|
||||||
periodParse prop s = case toRecurrance s of
|
periodParse prop s = case toRecurrance s of
|
||||||
Just recurrance -> period prop recurrance
|
Just recurrance -> period prop recurrance
|
||||||
Nothing -> Property "periodParse" $ do
|
Nothing -> property "periodParse" $ do
|
||||||
liftIO $ warningMessage $ "failed periodParse: " ++ s
|
liftIO $ warningMessage $ "failed periodParse: " ++ s
|
||||||
noChange
|
noChange
|
||||||
|
|
||||||
|
|
|
@ -13,19 +13,19 @@ type ServiceName = String
|
||||||
-- we can do is try to start the service, and if it fails, assume
|
-- we can do is try to start the service, and if it fails, assume
|
||||||
-- this means it's already running.
|
-- this means it's already running.
|
||||||
running :: ServiceName -> Property
|
running :: ServiceName -> Property
|
||||||
running svc = Property ("running " ++ svc) $ do
|
running svc = property ("running " ++ svc) $ do
|
||||||
void $ ensureProperty $
|
void $ ensureProperty $
|
||||||
scriptProperty ["service " ++ shellEscape svc ++ " start >/dev/null 2>&1 || true"]
|
scriptProperty ["service " ++ shellEscape svc ++ " start >/dev/null 2>&1 || true"]
|
||||||
return NoChange
|
return NoChange
|
||||||
|
|
||||||
restarted :: ServiceName -> Property
|
restarted :: ServiceName -> Property
|
||||||
restarted svc = Property ("restarted " ++ svc) $ do
|
restarted svc = property ("restarted " ++ svc) $ do
|
||||||
void $ ensureProperty $
|
void $ ensureProperty $
|
||||||
scriptProperty ["service " ++ shellEscape svc ++ " restart >/dev/null 2>&1 || true"]
|
scriptProperty ["service " ++ shellEscape svc ++ " restart >/dev/null 2>&1 || true"]
|
||||||
return NoChange
|
return NoChange
|
||||||
|
|
||||||
reloaded :: ServiceName -> Property
|
reloaded :: ServiceName -> Property
|
||||||
reloaded svc = Property ("reloaded " ++ svc) $ do
|
reloaded svc = property ("reloaded " ++ svc) $ do
|
||||||
void $ ensureProperty $
|
void $ ensureProperty $
|
||||||
scriptProperty ["service " ++ shellEscape svc ++ " reload >/dev/null 2>&1 || true"]
|
scriptProperty ["service " ++ shellEscape svc ++ " reload >/dev/null 2>&1 || true"]
|
||||||
return NoChange
|
return NoChange
|
||||||
|
|
|
@ -40,7 +40,7 @@ builder arch crontimes rsyncupload = combineProperties "gitannexbuilder"
|
||||||
-- The builduser account does not have a password set,
|
-- The builduser account does not have a password set,
|
||||||
-- instead use the password privdata to hold the rsync server
|
-- instead use the password privdata to hold the rsync server
|
||||||
-- password used to upload the built image.
|
-- password used to upload the built image.
|
||||||
, Property "rsync password" $ do
|
, property "rsync password" $ do
|
||||||
let f = homedir </> "rsyncpassword"
|
let f = homedir </> "rsyncpassword"
|
||||||
if rsyncupload
|
if rsyncupload
|
||||||
then withPrivData (Password builduser) $ \p -> do
|
then withPrivData (Password builduser) $ \p -> do
|
||||||
|
|
|
@ -8,16 +8,16 @@ import Utility.SafeCommand
|
||||||
-- | Clones Joey Hess's git home directory, and runs its fixups script.
|
-- | Clones Joey Hess's git home directory, and runs its fixups script.
|
||||||
installedFor :: UserName -> Property
|
installedFor :: UserName -> Property
|
||||||
installedFor user = check (not <$> hasGitDir user) $
|
installedFor user = check (not <$> hasGitDir user) $
|
||||||
Property ("githome " ++ user) (go =<< liftIO (homedir user))
|
property ("githome " ++ user) (go =<< liftIO (homedir user))
|
||||||
`requires` Apt.installed ["git"]
|
`requires` Apt.installed ["git"]
|
||||||
where
|
where
|
||||||
go home = do
|
go home = do
|
||||||
let tmpdir = home </> "githome"
|
let tmpdir = home </> "githome"
|
||||||
ensureProperty $ combineProperties "githome setup"
|
ensureProperty $ combineProperties "githome setup"
|
||||||
[ userScriptProperty user ["git clone " ++ url ++ " " ++ tmpdir]
|
[ userScriptProperty user ["git clone " ++ url ++ " " ++ tmpdir]
|
||||||
, Property "moveout" $ makeChange $ void $
|
, property "moveout" $ makeChange $ void $
|
||||||
moveout tmpdir home
|
moveout tmpdir home
|
||||||
, Property "rmdir" $ makeChange $ void $
|
, property "rmdir" $ makeChange $ void $
|
||||||
catchMaybeIO $ removeDirectory tmpdir
|
catchMaybeIO $ removeDirectory tmpdir
|
||||||
, userScriptProperty user ["rm -rf .aptitude/ .bashrc .profile; bin/mr checkout; bin/fixups"]
|
, userScriptProperty user ["rm -rf .aptitude/ .bashrc .profile; bin/mr checkout; bin/fixups"]
|
||||||
]
|
]
|
||||||
|
|
|
@ -16,9 +16,63 @@ import qualified Propellor.Property.Obnam as Obnam
|
||||||
import qualified Propellor.Property.Apache as Apache
|
import qualified Propellor.Property.Apache as Apache
|
||||||
import Utility.SafeCommand
|
import Utility.SafeCommand
|
||||||
|
|
||||||
|
import Data.List
|
||||||
|
import System.Posix.Files
|
||||||
|
|
||||||
|
oldUseNetServer :: [Host] -> Property
|
||||||
|
oldUseNetServer hosts = propertyList ("olduse.net server")
|
||||||
|
[ oldUseNetInstalled "oldusenet-server"
|
||||||
|
, Obnam.latestVersion
|
||||||
|
, Obnam.backup datadir "33 4 * * *"
|
||||||
|
[ "--repository=sftp://2318@usw-s002.rsync.net/~/olduse.net"
|
||||||
|
, "--client-name=spool"
|
||||||
|
] Obnam.OnlyClient
|
||||||
|
`requires` Ssh.keyImported SshRsa "root"
|
||||||
|
`requires` Ssh.knownHost hosts "usw-s002.rsync.net" "root"
|
||||||
|
, check (not . isSymbolicLink <$> getSymbolicLinkStatus newsspool) $
|
||||||
|
property "olduse.net spool in place" $ makeChange $ do
|
||||||
|
removeDirectoryRecursive newsspool
|
||||||
|
createSymbolicLink (datadir </> "news") newsspool
|
||||||
|
, Apt.installed ["leafnode"]
|
||||||
|
, "/etc/news/leafnode/config" `File.hasContent`
|
||||||
|
[ "# olduse.net configuration (deployed by propellor)"
|
||||||
|
, "expire = 1000000" -- no expiry via texpire
|
||||||
|
, "server = " -- no upstream server
|
||||||
|
, "debugmode = 1"
|
||||||
|
, "allowSTRANGERS = 42" -- lets anyone connect
|
||||||
|
, "nopost = 1" -- no new posting (just gather them)
|
||||||
|
]
|
||||||
|
, "/etc/hosts.deny" `File.lacksLine` "leafnode: ALL"
|
||||||
|
, Apt.serviceInstalledRunning "openbsd-inetd"
|
||||||
|
, File.notPresent "/etc/cron.daily/leafnode"
|
||||||
|
, File.notPresent "/etc/cron.d/leafnode"
|
||||||
|
, Cron.niceJob "oldusenet-expire" "11 1 * * *" "news" newsspool $ intercalate ";"
|
||||||
|
[ "find \\( -path ./out.going -or -path ./interesting.groups -or -path './*/.overview' \\) -prune -or -type f -ctime +60 -print | xargs --no-run-if-empty rm"
|
||||||
|
, "find -type d -empty | xargs --no-run-if-empty rmdir"
|
||||||
|
]
|
||||||
|
, Cron.niceJob "oldusenet-uucp" "*/5 * * * *" "news" "/" $
|
||||||
|
"/usr/bin/uucp " ++ datadir
|
||||||
|
, toProp $ Apache.siteEnabled "nntp.olduse.net" $ apachecfg "nntp.olduse.net" False
|
||||||
|
[ " DocumentRoot " ++ datadir ++ "/"
|
||||||
|
, " <Directory " ++ datadir ++ "/>"
|
||||||
|
, " Options Indexes FollowSymlinks"
|
||||||
|
, " AllowOverride None"
|
||||||
|
-- I had this in the file before.
|
||||||
|
-- This may be needed by a newer version of apache?
|
||||||
|
--, " Require all granted"
|
||||||
|
, " </Directory>"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
where
|
||||||
|
newsspool = "/var/spool/news"
|
||||||
|
datadir = "/var/spool/oldusenet"
|
||||||
|
|
||||||
oldUseNetShellBox :: Property
|
oldUseNetShellBox :: Property
|
||||||
oldUseNetShellBox = check (not <$> Apt.isInstalled "oldusenet") $
|
oldUseNetShellBox = oldUseNetInstalled "oldusenet"
|
||||||
propertyList ("olduse.net shellbox")
|
|
||||||
|
oldUseNetInstalled :: Apt.Package -> Property
|
||||||
|
oldUseNetInstalled pkg = check (not <$> Apt.isInstalled pkg) $
|
||||||
|
propertyList ("olduse.net " ++ pkg)
|
||||||
[ Apt.installed (words "build-essential devscripts debhelper git libncursesw5-dev libpcre3-dev pkg-config bison libicu-dev libidn11-dev libcanlock2-dev libuu-dev ghc libghc-strptime-dev libghc-hamlet-dev libghc-ifelse-dev libghc-hxt-dev libghc-utf8-string-dev libghc-missingh-dev libghc-sha-dev")
|
[ Apt.installed (words "build-essential devscripts debhelper git libncursesw5-dev libpcre3-dev pkg-config bison libicu-dev libidn11-dev libcanlock2-dev libuu-dev ghc libghc-strptime-dev libghc-hamlet-dev libghc-ifelse-dev libghc-hxt-dev libghc-utf8-string-dev libghc-missingh-dev libghc-sha-dev")
|
||||||
`describe` "olduse.net build deps"
|
`describe` "olduse.net build deps"
|
||||||
, scriptProperty
|
, scriptProperty
|
||||||
|
@ -26,12 +80,13 @@ oldUseNetShellBox = check (not <$> Apt.isInstalled "oldusenet") $
|
||||||
, "git clone git://olduse.net/ /root/tmp/oldusenet/source"
|
, "git clone git://olduse.net/ /root/tmp/oldusenet/source"
|
||||||
, "cd /root/tmp/oldusenet/source/"
|
, "cd /root/tmp/oldusenet/source/"
|
||||||
, "dpkg-buildpackage -us -uc"
|
, "dpkg-buildpackage -us -uc"
|
||||||
, "dpkg -i ../oldusenet*.deb || true"
|
, "dpkg -i ../" ++ pkg ++ "_*.deb || true"
|
||||||
, "apt-get -fy install" -- dependencies
|
, "apt-get -fy install" -- dependencies
|
||||||
, "rm -rf /root/tmp/oldusenet"
|
, "rm -rf /root/tmp/oldusenet"
|
||||||
] `describe` "olduse.net built"
|
] `describe` "olduse.net built"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
kgbServer :: Property
|
kgbServer :: Property
|
||||||
kgbServer = withOS desc $ \o -> case o of
|
kgbServer = withOS desc $ \o -> case o of
|
||||||
(Just (System (Debian Unstable) _)) ->
|
(Just (System (Debian Unstable) _)) ->
|
||||||
|
|
|
@ -67,7 +67,7 @@ randomHostKeys :: Property
|
||||||
randomHostKeys = flagFile prop "/etc/ssh/.unique_host_keys"
|
randomHostKeys = flagFile prop "/etc/ssh/.unique_host_keys"
|
||||||
`onChange` restartSshd
|
`onChange` restartSshd
|
||||||
where
|
where
|
||||||
prop = Property "ssh random host keys" $ do
|
prop = property "ssh random host keys" $ do
|
||||||
void $ liftIO $ boolSystem "sh"
|
void $ liftIO $ boolSystem "sh"
|
||||||
[ Param "-c"
|
[ Param "-c"
|
||||||
, Param "rm -f /etc/ssh/ssh_host_*"
|
, Param "rm -f /etc/ssh/ssh_host_*"
|
||||||
|
@ -81,8 +81,8 @@ randomHostKeys = flagFile prop "/etc/ssh/.unique_host_keys"
|
||||||
-- (Uses a null username for host keys.)
|
-- (Uses a null username for host keys.)
|
||||||
hostKey :: SshKeyType -> Property
|
hostKey :: SshKeyType -> Property
|
||||||
hostKey keytype = combineProperties desc
|
hostKey keytype = combineProperties desc
|
||||||
[ Property desc (install writeFile (SshPubKey keytype "") ".pub")
|
[ property desc (install writeFile (SshPubKey keytype "") ".pub")
|
||||||
, Property desc (install writeFileProtected (SshPrivKey keytype "") "")
|
, property desc (install writeFileProtected (SshPrivKey keytype "") "")
|
||||||
]
|
]
|
||||||
`onChange` restartSshd
|
`onChange` restartSshd
|
||||||
where
|
where
|
||||||
|
@ -98,8 +98,8 @@ hostKey keytype = combineProperties desc
|
||||||
-- from the site's PrivData.
|
-- from the site's PrivData.
|
||||||
keyImported :: SshKeyType -> UserName -> Property
|
keyImported :: SshKeyType -> UserName -> Property
|
||||||
keyImported keytype user = combineProperties desc
|
keyImported keytype user = combineProperties desc
|
||||||
[ Property desc (install writeFile (SshPubKey keytype user) ".pub")
|
[ property desc (install writeFile (SshPubKey keytype user) ".pub")
|
||||||
, Property desc (install writeFileProtected (SshPrivKey keytype user) "")
|
, property desc (install writeFileProtected (SshPrivKey keytype user) "")
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
desc = user ++ " has ssh key (" ++ fromKeyType keytype ++ ")"
|
desc = user ++ " has ssh key (" ++ fromKeyType keytype ++ ")"
|
||||||
|
@ -108,7 +108,7 @@ keyImported keytype user = combineProperties desc
|
||||||
ifM (liftIO $ doesFileExist f)
|
ifM (liftIO $ doesFileExist f)
|
||||||
( noChange
|
( noChange
|
||||||
, ensureProperty $ combineProperties desc
|
, ensureProperty $ combineProperties desc
|
||||||
[ Property desc $
|
[ property desc $
|
||||||
withPrivData p $ \key -> makeChange $
|
withPrivData p $ \key -> makeChange $
|
||||||
writer f key
|
writer f key
|
||||||
, File.ownerGroup f user user
|
, File.ownerGroup f user user
|
||||||
|
@ -126,7 +126,7 @@ fromKeyType SshEd25519 = "ed25519"
|
||||||
|
|
||||||
-- | Puts some host's ssh public key into the known_hosts file for a user.
|
-- | Puts some host's ssh public key into the known_hosts file for a user.
|
||||||
knownHost :: [Host] -> HostName -> UserName -> Property
|
knownHost :: [Host] -> HostName -> UserName -> Property
|
||||||
knownHost hosts hn user = Property desc $
|
knownHost hosts hn user = property desc $
|
||||||
go =<< fromHost hosts hn getSshPubKey
|
go =<< fromHost hosts hn getSshPubKey
|
||||||
where
|
where
|
||||||
desc = user ++ " knows ssh key for " ++ hn
|
desc = user ++ " knows ssh key for " ++ hn
|
||||||
|
@ -143,7 +143,7 @@ knownHost hosts hn user = Property desc $
|
||||||
|
|
||||||
-- | Makes a user have authorized_keys from the PrivData
|
-- | Makes a user have authorized_keys from the PrivData
|
||||||
authorizedKeys :: UserName -> Property
|
authorizedKeys :: UserName -> Property
|
||||||
authorizedKeys user = Property (user ++ " has authorized_keys") $
|
authorizedKeys user = property (user ++ " has authorized_keys") $
|
||||||
withPrivData (SshAuthorizedKeys user) $ \v -> do
|
withPrivData (SshAuthorizedKeys user) $ \v -> do
|
||||||
f <- liftIO $ dotFile "authorized_keys" user
|
f <- liftIO $ dotFile "authorized_keys" user
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Propellor.Property.User
|
||||||
-- | Allows a user to sudo. If the user has a password, sudo is configured
|
-- | Allows a user to sudo. If the user has a password, sudo is configured
|
||||||
-- to require it. If not, NOPASSWORD is enabled for the user.
|
-- to require it. If not, NOPASSWORD is enabled for the user.
|
||||||
enabledFor :: UserName -> Property
|
enabledFor :: UserName -> Property
|
||||||
enabledFor user = Property desc go `requires` Apt.installed ["sudo"]
|
enabledFor user = property desc go `requires` Apt.installed ["sudo"]
|
||||||
where
|
where
|
||||||
go = do
|
go = do
|
||||||
locked <- liftIO $ isLockedPassword user
|
locked <- liftIO $ isLockedPassword user
|
||||||
|
|
|
@ -29,7 +29,7 @@ hasSomePassword user = check ((/= HasPassword) <$> getPasswordStatus user) $
|
||||||
hasPassword user
|
hasPassword user
|
||||||
|
|
||||||
hasPassword :: UserName -> Property
|
hasPassword :: UserName -> Property
|
||||||
hasPassword user = Property (user ++ " has password") $
|
hasPassword user = property (user ++ " has password") $
|
||||||
withPrivData (Password user) $ \password -> makeChange $
|
withPrivData (Password user) $ \password -> makeChange $
|
||||||
withHandle StdinHandle createProcessSuccess
|
withHandle StdinHandle createProcessSuccess
|
||||||
(proc "chpasswd" []) $ \h -> do
|
(proc "chpasswd" []) $ \h -> do
|
||||||
|
|
|
@ -5,15 +5,13 @@
|
||||||
module Propellor.Types
|
module Propellor.Types
|
||||||
( Host(..)
|
( Host(..)
|
||||||
, Attr
|
, Attr
|
||||||
, HostName
|
|
||||||
, Propellor(..)
|
, Propellor(..)
|
||||||
, Property(..)
|
, Property(..)
|
||||||
, RevertableProperty(..)
|
, RevertableProperty(..)
|
||||||
, AttrProperty(..)
|
|
||||||
, IsProp
|
, IsProp
|
||||||
, describe
|
, describe
|
||||||
, toProp
|
, toProp
|
||||||
, getAttr
|
, setAttr
|
||||||
, requires
|
, requires
|
||||||
, Desc
|
, Desc
|
||||||
, Result(..)
|
, Result(..)
|
||||||
|
@ -23,6 +21,7 @@ module Propellor.Types
|
||||||
, GpgKeyId
|
, GpgKeyId
|
||||||
, SshKeyType(..)
|
, SshKeyType(..)
|
||||||
, module Propellor.Types.OS
|
, module Propellor.Types.OS
|
||||||
|
, module Propellor.Types.Dns
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
@ -33,8 +32,9 @@ import "MonadCatchIO-transformers" Control.Monad.CatchIO
|
||||||
|
|
||||||
import Propellor.Types.Attr
|
import Propellor.Types.Attr
|
||||||
import Propellor.Types.OS
|
import Propellor.Types.OS
|
||||||
|
import Propellor.Types.Dns
|
||||||
|
|
||||||
data Host = Host [Property] (Attr -> Attr)
|
data Host = Host [Property] SetAttr
|
||||||
|
|
||||||
-- | Propellor's monad provides read-only access to attributes of the
|
-- | Propellor's monad provides read-only access to attributes of the
|
||||||
-- system.
|
-- system.
|
||||||
|
@ -53,16 +53,15 @@ newtype Propellor p = Propellor { runWithAttr :: ReaderT Attr IO p }
|
||||||
-- property.
|
-- property.
|
||||||
data Property = Property
|
data Property = Property
|
||||||
{ propertyDesc :: Desc
|
{ propertyDesc :: Desc
|
||||||
-- | must be idempotent; may run repeatedly
|
|
||||||
, propertySatisfy :: Propellor Result
|
, propertySatisfy :: Propellor Result
|
||||||
|
-- ^ must be idempotent; may run repeatedly
|
||||||
|
, propertyAttr :: SetAttr
|
||||||
|
-- ^ a property can affect the overall Attr
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | A property that can be reverted.
|
-- | A property that can be reverted.
|
||||||
data RevertableProperty = RevertableProperty Property Property
|
data RevertableProperty = RevertableProperty Property Property
|
||||||
|
|
||||||
-- | A property that affects the Attr.
|
|
||||||
data AttrProperty = forall p. IsProp p => AttrProperty p (Attr -> Attr)
|
|
||||||
|
|
||||||
class IsProp p where
|
class IsProp p where
|
||||||
-- | Sets description.
|
-- | Sets description.
|
||||||
describe :: p -> Desc -> p
|
describe :: p -> Desc -> p
|
||||||
|
@ -70,17 +69,21 @@ class IsProp p where
|
||||||
-- | Indicates that the first property can only be satisfied
|
-- | Indicates that the first property can only be satisfied
|
||||||
-- once the second one is.
|
-- once the second one is.
|
||||||
requires :: p -> Property -> p
|
requires :: p -> Property -> p
|
||||||
getAttr :: p -> (Attr -> Attr)
|
setAttr :: p -> SetAttr
|
||||||
|
|
||||||
instance IsProp Property where
|
instance IsProp Property where
|
||||||
describe p d = p { propertyDesc = d }
|
describe p d = p { propertyDesc = d }
|
||||||
toProp p = p
|
toProp p = p
|
||||||
x `requires` y = Property (propertyDesc x) $ do
|
setAttr = propertyAttr
|
||||||
|
x `requires` y = Property (propertyDesc x) satisfy attr
|
||||||
|
where
|
||||||
|
attr = propertyAttr x . propertyAttr y
|
||||||
|
satisfy = do
|
||||||
r <- propertySatisfy y
|
r <- propertySatisfy y
|
||||||
case r of
|
case r of
|
||||||
FailedChange -> return FailedChange
|
FailedChange -> return FailedChange
|
||||||
_ -> propertySatisfy x
|
_ -> propertySatisfy x
|
||||||
getAttr _ = id
|
|
||||||
|
|
||||||
instance IsProp RevertableProperty where
|
instance IsProp RevertableProperty where
|
||||||
-- | Sets the description of both sides.
|
-- | Sets the description of both sides.
|
||||||
|
@ -89,13 +92,8 @@ instance IsProp RevertableProperty where
|
||||||
toProp (RevertableProperty p1 _) = p1
|
toProp (RevertableProperty p1 _) = p1
|
||||||
(RevertableProperty p1 p2) `requires` y =
|
(RevertableProperty p1 p2) `requires` y =
|
||||||
RevertableProperty (p1 `requires` y) p2
|
RevertableProperty (p1 `requires` y) p2
|
||||||
getAttr _ = id
|
-- | Return the SetAttr of the currently active side.
|
||||||
|
setAttr (RevertableProperty p1 _p2) = setAttr p1
|
||||||
instance IsProp AttrProperty where
|
|
||||||
describe (AttrProperty p a) d = AttrProperty (describe p d) a
|
|
||||||
toProp (AttrProperty p _) = toProp p
|
|
||||||
(AttrProperty p a) `requires` y = AttrProperty (p `requires` y) a
|
|
||||||
getAttr (AttrProperty _ a) = a
|
|
||||||
|
|
||||||
type Desc = String
|
type Desc = String
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
module Propellor.Types.Attr where
|
module Propellor.Types.Attr where
|
||||||
|
|
||||||
import Propellor.Types.OS
|
import Propellor.Types.OS
|
||||||
|
import qualified Propellor.Types.Dns as Dns
|
||||||
|
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
|
import qualified Data.Map as M
|
||||||
|
|
||||||
-- | The attributes of a host. For example, its hostname.
|
-- | The attributes of a host. For example, its hostname.
|
||||||
data Attr = Attr
|
data Attr = Attr
|
||||||
{ _hostname :: HostName
|
{ _hostname :: HostName
|
||||||
, _cnames :: S.Set Domain
|
|
||||||
, _os :: Maybe System
|
, _os :: Maybe System
|
||||||
, _sshPubKey :: Maybe String
|
, _sshPubKey :: Maybe String
|
||||||
|
, _dns :: S.Set Dns.Record
|
||||||
|
, _namedconf :: M.Map Dns.Domain Dns.NamedConf
|
||||||
|
|
||||||
, _dockerImage :: Maybe String
|
, _dockerImage :: Maybe String
|
||||||
, _dockerRunParams :: [HostName -> String]
|
, _dockerRunParams :: [HostName -> String]
|
||||||
|
@ -18,8 +21,9 @@ data Attr = Attr
|
||||||
instance Eq Attr where
|
instance Eq Attr where
|
||||||
x == y = and
|
x == y = and
|
||||||
[ _hostname x == _hostname y
|
[ _hostname x == _hostname y
|
||||||
, _cnames x == _cnames y
|
|
||||||
, _os x == _os y
|
, _os x == _os y
|
||||||
|
, _dns x == _dns y
|
||||||
|
, _namedconf x == _namedconf y
|
||||||
, _sshPubKey x == _sshPubKey y
|
, _sshPubKey x == _sshPubKey y
|
||||||
|
|
||||||
, _dockerImage x == _dockerImage y
|
, _dockerImage x == _dockerImage y
|
||||||
|
@ -30,15 +34,15 @@ instance Eq Attr where
|
||||||
instance Show Attr where
|
instance Show Attr where
|
||||||
show a = unlines
|
show a = unlines
|
||||||
[ "hostname " ++ _hostname a
|
[ "hostname " ++ _hostname a
|
||||||
, "cnames " ++ show (_cnames a)
|
|
||||||
, "OS " ++ show (_os a)
|
, "OS " ++ show (_os a)
|
||||||
, "sshPubKey " ++ show (_sshPubKey a)
|
, "sshPubKey " ++ show (_sshPubKey a)
|
||||||
|
, "dns " ++ show (_dns a)
|
||||||
|
, "namedconf " ++ show (_namedconf a)
|
||||||
, "docker image " ++ show (_dockerImage a)
|
, "docker image " ++ show (_dockerImage a)
|
||||||
, "docker run params " ++ show (map (\mk -> mk "") (_dockerRunParams a))
|
, "docker run params " ++ show (map (\mk -> mk "") (_dockerRunParams a))
|
||||||
]
|
]
|
||||||
|
|
||||||
newAttr :: HostName -> Attr
|
newAttr :: HostName -> Attr
|
||||||
newAttr hn = Attr hn S.empty Nothing Nothing Nothing []
|
newAttr hn = Attr hn Nothing Nothing S.empty M.empty Nothing []
|
||||||
|
|
||||||
type HostName = String
|
type SetAttr = Attr -> Attr
|
||||||
type Domain = String
|
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
module Propellor.Types.Dns where
|
||||||
|
|
||||||
|
import Data.Word
|
||||||
|
|
||||||
|
type Domain = String
|
||||||
|
|
||||||
|
data IPAddr = IPv4 String | IPv6 String
|
||||||
|
deriving (Read, Show, Eq, Ord)
|
||||||
|
|
||||||
|
fromIPAddr :: IPAddr -> String
|
||||||
|
fromIPAddr (IPv4 addr) = addr
|
||||||
|
fromIPAddr (IPv6 addr) = addr
|
||||||
|
|
||||||
|
-- | Represents a bind 9 named.conf file.
|
||||||
|
data NamedConf = NamedConf
|
||||||
|
{ confDomain :: Domain
|
||||||
|
, confType :: Type
|
||||||
|
, confFile :: FilePath
|
||||||
|
, confMasters :: [IPAddr]
|
||||||
|
, confLines :: [String]
|
||||||
|
}
|
||||||
|
deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
|
data Type = Master | Secondary
|
||||||
|
deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
|
-- | Represents a bind 9 zone file.
|
||||||
|
data Zone = Zone
|
||||||
|
{ zDomain :: Domain
|
||||||
|
, zSOA :: SOA
|
||||||
|
, zHosts :: [(BindDomain, Record)]
|
||||||
|
}
|
||||||
|
deriving (Read, Show, Eq)
|
||||||
|
|
||||||
|
-- | Every domain has a SOA record, which is big and complicated.
|
||||||
|
data SOA = SOA
|
||||||
|
{ sDomain :: BindDomain
|
||||||
|
-- ^ Typically ns1.your.domain
|
||||||
|
, sSerial :: SerialNumber
|
||||||
|
-- ^ The most important parameter is the serial number,
|
||||||
|
-- which must increase after each change.
|
||||||
|
, sRefresh :: Integer
|
||||||
|
, sRetry :: Integer
|
||||||
|
, sExpire :: Integer
|
||||||
|
, sNegativeCacheTTL :: Integer
|
||||||
|
, sRecord :: [Record]
|
||||||
|
-- ^ Records for the root of the domain. Typically NS, A, TXT
|
||||||
|
}
|
||||||
|
deriving (Read, Show, Eq)
|
||||||
|
|
||||||
|
-- | Types of DNS records.
|
||||||
|
--
|
||||||
|
-- This is not a complete list, more can be added.
|
||||||
|
data Record
|
||||||
|
= Address IPAddr
|
||||||
|
| CNAME BindDomain
|
||||||
|
| MX Int BindDomain
|
||||||
|
| NS BindDomain
|
||||||
|
| TXT String
|
||||||
|
| SRV Word16 Word16 Word16 BindDomain
|
||||||
|
deriving (Read, Show, Eq, Ord)
|
||||||
|
|
||||||
|
getIPAddr :: Record -> Maybe IPAddr
|
||||||
|
getIPAddr (Address addr) = Just addr
|
||||||
|
getIPAddr _ = Nothing
|
||||||
|
|
||||||
|
getCNAME :: Record -> Maybe BindDomain
|
||||||
|
getCNAME (CNAME d) = Just d
|
||||||
|
getCNAME _ = Nothing
|
||||||
|
|
||||||
|
-- | Bind serial numbers are unsigned, 32 bit integers.
|
||||||
|
type SerialNumber = Word32
|
||||||
|
|
||||||
|
-- | Domains in the zone file must end with a period if they are absolute.
|
||||||
|
--
|
||||||
|
-- Let's use a type to keep absolute domains straight from relative
|
||||||
|
-- domains.
|
||||||
|
--
|
||||||
|
-- The SOADomain refers to the root SOA record.
|
||||||
|
data BindDomain = RelDomain Domain | AbsDomain Domain | SOADomain
|
||||||
|
deriving (Read, Show, Eq, Ord)
|
|
@ -1,5 +1,6 @@
|
||||||
module Propellor.Types.OS where
|
module Propellor.Types.OS where
|
||||||
|
|
||||||
|
type HostName = String
|
||||||
type UserName = String
|
type UserName = String
|
||||||
type GroupName = String
|
type GroupName = String
|
||||||
|
|
||||||
|
|
13
TODO
13
TODO
|
@ -15,7 +15,12 @@
|
||||||
* There is no way for a property of a docker container to require
|
* There is no way for a property of a docker container to require
|
||||||
some property be met outside the container. For example, some servers
|
some property be met outside the container. For example, some servers
|
||||||
need ntp installed for a good date source.
|
need ntp installed for a good date source.
|
||||||
* Attributes can only be set in the top level property list for a Host.
|
* Docking a container in a host should add to the host any cnames that
|
||||||
If an attribute is set inside a propertyList, it won't propigate out.
|
are assigned to the container.
|
||||||
Fix this. Probably the fix involves combining AttrProperty into Property.
|
* Either `Ssh.hostKey` should set the sshPubKey attr
|
||||||
Then propertyList can gather the attributes from its list.
|
(which seems hard, as attrs need to be able to be calculated without
|
||||||
|
running any IO code, and here IO is needed along with decrypting the
|
||||||
|
PrivData..), or the public key should not be stored in
|
||||||
|
the PrivData, and instead configured using the attr.
|
||||||
|
Getting the ssh host key into the attr will allow automatically
|
||||||
|
exporting it via DNS (SSHFP record)
|
||||||
|
|
|
@ -32,11 +32,15 @@ hosts :: [Host] -- * \ | | '--------'
|
||||||
hosts = -- (o) `
|
hosts = -- (o) `
|
||||||
-- My laptop
|
-- My laptop
|
||||||
[ host "darkstar.kitenet.net"
|
[ host "darkstar.kitenet.net"
|
||||||
|
& ipv6 "2001:4830:1600:187::2" -- sixxs tunnel
|
||||||
& Docker.configured
|
& Docker.configured
|
||||||
& Apt.buildDep ["git-annex"] `period` Daily
|
& Apt.buildDep ["git-annex"] `period` Daily
|
||||||
|
|
||||||
-- Nothing super-important lives here.
|
-- Nothing super-important lives here.
|
||||||
, standardSystem "clam.kitenet.net" Unstable "amd64"
|
, standardSystem "clam.kitenet.net" Unstable "amd64"
|
||||||
|
& ipv4 "162.248.143.249"
|
||||||
|
& ipv6 "2002:5044:5531::1"
|
||||||
|
|
||||||
& cleanCloudAtCost
|
& cleanCloudAtCost
|
||||||
& Apt.unattendedUpgrades
|
& Apt.unattendedUpgrades
|
||||||
& Network.ipv6to4
|
& Network.ipv6to4
|
||||||
|
@ -44,18 +48,18 @@ hosts = -- (o) `
|
||||||
& Postfix.satellite
|
& Postfix.satellite
|
||||||
& Docker.configured
|
& Docker.configured
|
||||||
|
|
||||||
& cname "shell.olduse.net"
|
& alias "shell.olduse.net"
|
||||||
& JoeySites.oldUseNetShellBox
|
& JoeySites.oldUseNetShellBox
|
||||||
|
|
||||||
& cname "openid.kitenet.net"
|
& alias "openid.kitenet.net"
|
||||||
& Docker.docked hosts "openid-provider"
|
& Docker.docked hosts "openid-provider"
|
||||||
`requires` Apt.installed ["ntp"]
|
`requires` Apt.installed ["ntp"]
|
||||||
|
|
||||||
& cname "ancient.kitenet.net"
|
& alias "ancient.kitenet.net"
|
||||||
& Docker.docked hosts "ancient-kitenet"
|
& Docker.docked hosts "ancient-kitenet"
|
||||||
|
|
||||||
-- I'd rather this were on diatom, but it needs unstable.
|
-- I'd rather this were on diatom, but it needs unstable.
|
||||||
& cname "kgb.kitenet.net"
|
& alias "kgb.kitenet.net"
|
||||||
& JoeySites.kgbServer
|
& JoeySites.kgbServer
|
||||||
|
|
||||||
& Docker.garbageCollected `period` Daily
|
& Docker.garbageCollected `period` Daily
|
||||||
|
@ -63,6 +67,8 @@ hosts = -- (o) `
|
||||||
|
|
||||||
-- Orca is the main git-annex build box.
|
-- Orca is the main git-annex build box.
|
||||||
, standardSystem "orca.kitenet.net" Unstable "amd64"
|
, standardSystem "orca.kitenet.net" Unstable "amd64"
|
||||||
|
& ipv4 "138.38.108.179"
|
||||||
|
|
||||||
& Hostname.sane
|
& Hostname.sane
|
||||||
& Apt.unattendedUpgrades
|
& Apt.unattendedUpgrades
|
||||||
& Postfix.satellite
|
& Postfix.satellite
|
||||||
|
@ -76,13 +82,14 @@ hosts = -- (o) `
|
||||||
|
|
||||||
-- Important stuff that needs not too much memory or CPU.
|
-- Important stuff that needs not too much memory or CPU.
|
||||||
, standardSystem "diatom.kitenet.net" Stable "amd64"
|
, standardSystem "diatom.kitenet.net" Stable "amd64"
|
||||||
|
& ipv4 "107.170.31.195"
|
||||||
|
|
||||||
& Hostname.sane
|
& Hostname.sane
|
||||||
& Ssh.hostKey SshDsa
|
& Ssh.hostKey SshDsa
|
||||||
& Ssh.hostKey SshRsa
|
& Ssh.hostKey SshRsa
|
||||||
& Ssh.hostKey SshEcdsa
|
& Ssh.hostKey SshEcdsa
|
||||||
& Apt.unattendedUpgrades
|
& Apt.unattendedUpgrades
|
||||||
& Apt.serviceInstalledRunning "ntp"
|
& Apt.serviceInstalledRunning "ntp"
|
||||||
& Dns.zones myDnsSecondary
|
|
||||||
& Postfix.satellite
|
& Postfix.satellite
|
||||||
|
|
||||||
& Apt.serviceInstalledRunning "apache2"
|
& Apt.serviceInstalledRunning "apache2"
|
||||||
|
@ -93,24 +100,40 @@ hosts = -- (o) `
|
||||||
& Apache.multiSSL
|
& Apache.multiSSL
|
||||||
& File.ownerGroup "/srv/web" "joey" "joey"
|
& File.ownerGroup "/srv/web" "joey" "joey"
|
||||||
|
|
||||||
& cname "git.kitenet.net"
|
& alias "git.kitenet.net"
|
||||||
& cname "git.joeyh.name"
|
& alias "git.joeyh.name"
|
||||||
& JoeySites.gitServer hosts
|
& JoeySites.gitServer hosts
|
||||||
|
|
||||||
& cname "downloads.kitenet.net"
|
& alias "downloads.kitenet.net"
|
||||||
& JoeySites.annexWebSite hosts "/srv/git/downloads.git"
|
& JoeySites.annexWebSite hosts "/srv/git/downloads.git"
|
||||||
"downloads.kitenet.net"
|
"downloads.kitenet.net"
|
||||||
"840760dc-08f0-11e2-8c61-576b7e66acfd"
|
"840760dc-08f0-11e2-8c61-576b7e66acfd"
|
||||||
[("turtle", "ssh://turtle.kitenet.net/~/lib/downloads/")]
|
[("turtle", "ssh://turtle.kitenet.net/~/lib/downloads/")]
|
||||||
& JoeySites.annexRsyncServer
|
& JoeySites.annexRsyncServer
|
||||||
|
|
||||||
& cname "tmp.kitenet.net"
|
& alias "tmp.kitenet.net"
|
||||||
& JoeySites.annexWebSite hosts "/srv/git/joey/tmp.git"
|
& JoeySites.annexWebSite hosts "/srv/git/joey/tmp.git"
|
||||||
"tmp.kitenet.net"
|
"tmp.kitenet.net"
|
||||||
"26fd6e38-1226-11e2-a75f-ff007033bdba"
|
"26fd6e38-1226-11e2-a75f-ff007033bdba"
|
||||||
[]
|
[]
|
||||||
& JoeySites.twitRss
|
& JoeySites.twitRss
|
||||||
|
|
||||||
|
& alias "nntp.olduse.net"
|
||||||
|
& alias "resources.olduse.net"
|
||||||
|
& JoeySites.oldUseNetServer hosts
|
||||||
|
|
||||||
|
& myDnsSecondary
|
||||||
|
& Dns.primary hosts "olduse.net"
|
||||||
|
( Dns.mkSOA "ns1.kitenet.net" 100
|
||||||
|
[ NS (AbsDomain "ns1.kitenet.net")
|
||||||
|
, NS (AbsDomain "ns6.gandi.net")
|
||||||
|
, NS (AbsDomain "ns2.kitenet.net")
|
||||||
|
, MX 0 (AbsDomain "kitenet.net")
|
||||||
|
, TXT "v=spf1 a -all"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
[ (RelDomain "article", CNAME $ AbsDomain "virgil.koldfront.dk") ]
|
||||||
|
|
||||||
& Apt.installed ["ntop"]
|
& Apt.installed ["ntop"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -222,17 +245,17 @@ cleanCloudAtCost = propertyList "cloudatcost cleanup"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
myDnsSecondary :: [Dns.Zone]
|
myDnsSecondary :: Property
|
||||||
myDnsSecondary =
|
myDnsSecondary = propertyList "dns secondary for all my domains"
|
||||||
[ Dns.secondary "kitenet.net" master
|
[ Dns.secondaryFor wren hosts "kitenet.net"
|
||||||
, Dns.secondary "joeyh.name" master
|
, Dns.secondaryFor wren hosts "joeyh.name"
|
||||||
, Dns.secondary "ikiwiki.info" master
|
, Dns.secondaryFor wren hosts "ikiwiki.info"
|
||||||
, Dns.secondary "olduse.net" master
|
, Dns.secondary hosts "olduse.net"
|
||||||
, Dns.secondary "branchable.com" branchablemaster
|
, Dns.secondaryFor branchable hosts "branchable.com"
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
master = ["80.68.85.49", "2001:41c8:125:49::10"] -- wren
|
wren = ["wren.kitenet.net"]
|
||||||
branchablemaster = ["66.228.46.55", "2600:3c03::f03c:91ff:fedf:c0e5"]
|
branchable = ["branchable.com"]
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = defaultMain hosts
|
main = defaultMain hosts
|
||||||
|
@ -251,11 +274,23 @@ main = defaultMain hosts
|
||||||
|
|
||||||
|
|
||||||
monsters :: [Host] -- Systems I don't manage with propellor,
|
monsters :: [Host] -- Systems I don't manage with propellor,
|
||||||
monsters = -- but do want to track their public keys.
|
monsters = -- but do want to track their public keys etc.
|
||||||
[ host "usw-s002.rsync.net"
|
[ host "usw-s002.rsync.net"
|
||||||
& sshPubKey "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=="
|
& sshPubKey "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 "turtle.kitenet.net"
|
|
||||||
& sshPubKey "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAokMXQiX/NZjA1UbhMdgAscnS5dsmy+Q7bWrQ6tsTZ/o+6N/T5cbjoBHOdpypXJI3y/PiJTDJaQtXIhLa8gFg/EvxMnMz/KG9skADW1361JmfCc4BxicQIO2IOOe6eilPr+YsnOwiHwL0vpUnuty39cppuMWVD25GzxXlS6KQsLCvXLzxLLuNnGC43UAM0q4UwQxDtAZEK1dH2o3HMWhgMP2qEQupc24dbhpO3ecxh2C9678a3oGDuDuNf7mLp3s7ptj5qF3onitpJ82U5o7VajaHoygMaSRFeWxP2c13eM57j3bLdLwxVXFhePcKXARu1iuFTLS5uUf3hN6MkQcOGw=="
|
|
||||||
, host "github.com"
|
, host "github.com"
|
||||||
& sshPubKey "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ=="
|
& sshPubKey "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ=="
|
||||||
|
, host "turtle.kitenet.net"
|
||||||
|
& ipv4 "67.223.19.96"
|
||||||
|
& ipv6 "2001:4978:f:2d9::2"
|
||||||
|
& sshPubKey "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAokMXQiX/NZjA1UbhMdgAscnS5dsmy+Q7bWrQ6tsTZ/o+6N/T5cbjoBHOdpypXJI3y/PiJTDJaQtXIhLa8gFg/EvxMnMz/KG9skADW1361JmfCc4BxicQIO2IOOe6eilPr+YsnOwiHwL0vpUnuty39cppuMWVD25GzxXlS6KQsLCvXLzxLLuNnGC43UAM0q4UwQxDtAZEK1dH2o3HMWhgMP2qEQupc24dbhpO3ecxh2C9678a3oGDuDuNf7mLp3s7ptj5qF3onitpJ82U5o7VajaHoygMaSRFeWxP2c13eM57j3bLdLwxVXFhePcKXARu1iuFTLS5uUf3hN6MkQcOGw=="
|
||||||
|
, host "wren.kitenet.net"
|
||||||
|
& ipv4 "80.68.85.49"
|
||||||
|
& ipv6 "2001:41c8:125:49::10"
|
||||||
|
& alias "kite.kitenet.net"
|
||||||
|
, host "branchable.com"
|
||||||
|
& ipv4 "66.228.46.55"
|
||||||
|
& ipv6 "2600:3c03::f03c:91ff:fedf:c0e5"
|
||||||
|
& alias "olduse.net"
|
||||||
|
& alias "www.olduse.net"
|
||||||
|
& alias "git.olduse.net"
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
propellor (0.3.2) UNRELEASED; urgency=medium
|
propellor (0.4.0) unstable; urgency=medium
|
||||||
|
|
||||||
* Run all cron jobs under chronic from moreutils to avoid unnecessary mails.
|
* Propellor can configure primary DNS servers, including generating
|
||||||
|
zone files, which is done by looking at the properties of hosts
|
||||||
|
in a domain.
|
||||||
|
* The `cname` property was renamed to `alias` as it does not always
|
||||||
|
generate CNAME in the DNS.
|
||||||
|
* Constructor of Property has changed (use `property` function instead).
|
||||||
|
* All Property combinators now combine together their Attr settings.
|
||||||
|
So Attr settings can be made inside a propertyList, for example.
|
||||||
|
* Run all cron jobs under chronic from moreutils to avoid unnecessary
|
||||||
|
mails.
|
||||||
|
|
||||||
-- Joey Hess <joeyh@debian.org> Thu, 17 Apr 2014 21:00:43 -0400
|
-- Joey Hess <joeyh@debian.org> Sat, 19 Apr 2014 02:09:56 -0400
|
||||||
|
|
||||||
propellor (0.3.1) unstable; urgency=medium
|
propellor (0.3.1) unstable; urgency=medium
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
Name: propellor
|
Name: propellor
|
||||||
Version: 0.3.1
|
Version: 0.4.0
|
||||||
Cabal-Version: >= 1.6
|
Cabal-Version: >= 1.6
|
||||||
License: GPL
|
License: GPL
|
||||||
Maintainer: Joey Hess <joey@kitenet.net>
|
Maintainer: Joey Hess <joey@kitenet.net>
|
||||||
|
@ -99,6 +99,7 @@ Library
|
||||||
Propellor.Exception
|
Propellor.Exception
|
||||||
Propellor.Types
|
Propellor.Types
|
||||||
Propellor.Types.OS
|
Propellor.Types.OS
|
||||||
|
Propellor.Types.Dns
|
||||||
Other-Modules:
|
Other-Modules:
|
||||||
Propellor.Types.Attr
|
Propellor.Types.Attr
|
||||||
Propellor.CmdLine
|
Propellor.CmdLine
|
||||||
|
|
Loading…
Reference in New Issue