summaryrefslogtreecommitdiff
path: root/src/Propellor/Property
diff options
context:
space:
mode:
Diffstat (limited to 'src/Propellor/Property')
-rw-r--r--src/Propellor/Property/Apache.hs8
-rw-r--r--src/Propellor/Property/Apt/PPA.hs3
-rw-r--r--src/Propellor/Property/Bootstrap.hs15
-rw-r--r--src/Propellor/Property/Chroot.hs20
-rw-r--r--src/Propellor/Property/ConfFile.hs11
-rw-r--r--src/Propellor/Property/DiskImage.hs144
-rw-r--r--src/Propellor/Property/DiskImage/PartSpec.hs8
-rw-r--r--src/Propellor/Property/Docker.hs12
-rw-r--r--src/Propellor/Property/File.hs50
-rw-r--r--src/Propellor/Property/FreeDesktop.hs29
-rw-r--r--src/Propellor/Property/Grub.hs18
-rw-r--r--src/Propellor/Property/HostingProvider/Linode.hs5
-rw-r--r--src/Propellor/Property/Hostname.hs2
-rw-r--r--src/Propellor/Property/LightDM.hs3
-rw-r--r--src/Propellor/Property/Sbuild.hs4
-rw-r--r--src/Propellor/Property/SiteSpecific/JoeySites.hs27
-rw-r--r--src/Propellor/Property/Systemd.hs2
-rw-r--r--src/Propellor/Property/User.hs8
-rw-r--r--src/Propellor/Property/XFCE.hs41
-rw-r--r--src/Propellor/Property/ZFS/Process.hs3
20 files changed, 284 insertions, 129 deletions
diff --git a/src/Propellor/Property/Apache.hs b/src/Propellor/Property/Apache.hs
index 554a5837..854d0eaa 100644
--- a/src/Propellor/Property/Apache.hs
+++ b/src/Propellor/Property/Apache.hs
@@ -189,7 +189,7 @@ httpsVirtualHost' domain docroot letos addedcfg = setup <!> teardown
`requires` modEnabled "ssl"
`before` setuphttps
teardown = siteDisabled domain
- setuphttp = siteEnabled' domain $
+ setuphttp = (siteEnabled' domain $
-- The sslconffile is only created after letsencrypt gets
-- the cert. The "*" is needed to make apache not error
-- when the file doesn't exist.
@@ -201,23 +201,23 @@ httpsVirtualHost' domain docroot letos addedcfg = setup <!> teardown
, "RewriteRule ^/.well-known/(.*) - [L]"
-- Everything else redirects to https
, "RewriteRule ^/(.*) https://" ++ domain ++ "/$1 [L,R,NE]"
- ]
+ ])
+ `requires` File.dirExists (takeDirectory cf)
setuphttps = LetsEncrypt.letsEncrypt letos domain docroot
`onChange` postsetuphttps
postsetuphttps = combineProperties (domain ++ " ssl cert installed") $ props
- & File.dirExists (takeDirectory cf)
& File.hasContent cf sslvhost
`onChange` reloaded
-- always reload since the cert has changed
& reloaded
where
- cf = sslconffile "letsencrypt"
sslvhost = vhost (Port 443)
[ "SSLEngine on"
, "SSLCertificateFile " ++ LetsEncrypt.certFile domain
, "SSLCertificateKeyFile " ++ LetsEncrypt.privKeyFile domain
, "SSLCertificateChainFile " ++ LetsEncrypt.chainFile domain
]
+ cf = sslconffile "letsencrypt"
sslconffile s = "/etc/apache2/sites-available/ssl/" ++ domain ++ "/" ++ s ++ ".conf"
vhost p ls =
[ "<VirtualHost *:" ++ val p ++">"
diff --git a/src/Propellor/Property/Apt/PPA.hs b/src/Propellor/Property/Apt/PPA.hs
index 346125ff..a8f7db15 100644
--- a/src/Propellor/Property/Apt/PPA.hs
+++ b/src/Propellor/Property/Apt/PPA.hs
@@ -6,10 +6,11 @@ module Propellor.Property.Apt.PPA where
import Data.List
import Control.Applicative
import Prelude
-import Data.String.Utils
import Data.String (IsString(..))
+
import Propellor.Base
import qualified Propellor.Property.Apt as Apt
+import Utility.Split
-- | Ensure software-properties-common is installed.
installed :: Property DebianLike
diff --git a/src/Propellor/Property/Bootstrap.hs b/src/Propellor/Property/Bootstrap.hs
index 5678a865..767d6ef7 100644
--- a/src/Propellor/Property/Bootstrap.hs
+++ b/src/Propellor/Property/Bootstrap.hs
@@ -17,17 +17,17 @@ data RepoSource
-- | Bootstraps a propellor installation into
-- /usr/local/propellor/
--
--- Normally, propellor is already bootstrapped when it runs, so this
--- property is not useful. However, this can be useful inside a
--- chroot used to build a disk image, to make the disk image
--- have propellor installed.
+-- This property only does anything when used inside a chroot.
+-- This is particularly useful inside a chroot used to build a
+-- disk image, to make the disk image have propellor installed.
--
-- The git repository is cloned (or pulled to update if it already exists).
--
-- All build dependencies are installed, using distribution packages
-- or falling back to using cabal.
bootstrappedFrom :: RepoSource -> Property Linux
-bootstrappedFrom reposource = go `requires` clonedFrom reposource
+bootstrappedFrom reposource = check inChroot $
+ go `requires` clonedFrom reposource
where
go :: Property Linux
go = property "Propellor bootstrapped" $ do
@@ -35,7 +35,8 @@ bootstrappedFrom reposource = go `requires` clonedFrom reposource
assumeChange $ exposeTrueLocaldir $ const $
runShellCommand $ buildShellCommand
[ "cd " ++ localdir
- , bootstrapPropellorCommand system
+ , checkDepsCommand system
+ , buildCommand
]
-- | Clones the propellor repeository into /usr/local/propellor/
@@ -83,7 +84,7 @@ clonedFrom reposource = case reposource of
-- configuration.
copygitconfig :: Property Linux
copygitconfig = property ("Propellor repo git config copied from outside the chroot") $ do
- let gitconfig = localdir <> ".git" <> "config"
+ let gitconfig = localdir </> ".git" </> "config"
cfg <- liftIO $ B.readFile gitconfig
exposeTrueLocaldir $ const $
liftIO $ B.writeFile gitconfig cfg
diff --git a/src/Propellor/Property/Chroot.hs b/src/Propellor/Property/Chroot.hs
index 5f764d47..65749e34 100644
--- a/src/Propellor/Property/Chroot.hs
+++ b/src/Propellor/Property/Chroot.hs
@@ -33,11 +33,10 @@ import qualified Propellor.Property.File as File
import qualified Propellor.Shim as Shim
import Propellor.Property.Mount
import Utility.FileMode
+import Utility.Split
import qualified Data.Map as M
-import Data.List.Utils
import System.Posix.Directory
-import System.Console.Concurrent
-- | Specification of a chroot. Normally you'll use `debootstrapped` or
-- `bootstrapped` to construct a Chroot value.
@@ -201,9 +200,7 @@ propellChroot c@(Chroot loc _ _ _) mkproc systemdonly = property (chrootDesc c "
, "--continue"
, show cmd
]
- let p' = p { env = Just pe }
- r <- liftIO $ withHandle StdoutHandle createProcessSuccess p'
- processChainOutput
+ r <- liftIO $ chainPropellor (p { env = Just pe })
liftIO cleanup
return r
@@ -223,13 +220,12 @@ chain hostlist (ChrootChain hn loc systemdonly onconsole) =
go h = do
changeWorkingDirectory localdir
when onconsole forceConsole
- onlyProcess (provisioningLock loc) $ do
- r <- runPropellor (setInChroot h) $ ensureChildProperties $
- if systemdonly
- then [toChildProperty Systemd.installed]
- else hostProperties h
- flushConcurrentOutput
- putStrLn $ "\n" ++ show r
+ onlyProcess (provisioningLock loc) $
+ runChainPropellor (setInChroot h) $
+ ensureChildProperties $
+ if systemdonly
+ then [toChildProperty Systemd.installed]
+ else hostProperties h
chain _ _ = errorMessage "bad chain command"
inChrootProcess :: Bool -> Chroot -> [String] -> IO (CreateProcess, IO ())
diff --git a/src/Propellor/Property/ConfFile.hs b/src/Propellor/Property/ConfFile.hs
index b49c626e..ce092ec9 100644
--- a/src/Propellor/Property/ConfFile.hs
+++ b/src/Propellor/Property/ConfFile.hs
@@ -11,6 +11,7 @@ module Propellor.Property.ConfFile (
containsIniSetting,
hasIniSection,
lacksIniSection,
+ iniFileContains,
) where
import Propellor.Base
@@ -114,3 +115,13 @@ lacksIniSection f header = adjustIniSection
(const []) -- remove all lines of section
id -- add no lines if section is missing
f
+
+-- | Specifies the whole content of a .ini file.
+--
+-- Revertijg this causes the file not to exist.
+iniFileContains :: FilePath -> [(IniSection, [(IniKey, String)])] -> RevertableProperty UnixLike UnixLike
+iniFileContains f l = f `hasContent` content <!> notPresent f
+ where
+ content = concatMap sectioncontent l
+ sectioncontent (section, keyvalues) = iniHeader section :
+ map (\(key, value) -> key ++ "=" ++ value) keyvalues
diff --git a/src/Propellor/Property/DiskImage.hs b/src/Propellor/Property/DiskImage.hs
index 90b7010b..9300b201 100644
--- a/src/Propellor/Property/DiskImage.hs
+++ b/src/Propellor/Property/DiskImage.hs
@@ -13,11 +13,7 @@ module Propellor.Property.DiskImage (
imageRebuilt,
imageBuiltFrom,
imageExists,
- -- * Finalization
- Finalization,
- grubBooted,
Grub.BIOS(..),
- noFinalization,
) where
import Propellor.Base
@@ -33,6 +29,8 @@ import Propellor.Property.Mount
import Propellor.Property.Fstab (SwapPartition(..), genFstab)
import Propellor.Property.Partition
import Propellor.Property.Rsync
+import Propellor.Types.Info
+import Propellor.Types.Bootloader
import Propellor.Container
import Utility.Path
@@ -49,7 +47,11 @@ type DiskImage = FilePath
-- First the specified Chroot is set up, and its properties are satisfied.
--
-- Then, the disk image is set up, and the chroot is copied into the
--- appropriate partition(s) of it.
+-- appropriate partition(s) of it.
+--
+-- The partitions default to being sized just large enough to fit the files
+-- from the chroot. You can use `addFreeSpace` to make them a bit larger
+-- than that, or `setSize` to use a fixed size.
--
-- Note that the disk image file is reused if it already exists,
-- to avoid expensive IO to generate a new one. And, it's updated in-place,
@@ -67,7 +69,7 @@ type DiskImage = FilePath
-- >
-- > foo = host "foo.example.com" $ props
-- > & imageBuilt "/srv/diskimages/disk.img" mychroot
--- > MSDOS (grubBooted PC)
+-- > MSDOS
-- > [ partition EXT2 `mountedAt` "/boot"
-- > `setFlag` BootFlag
-- > , partition EXT4 `mountedAt` "/"
@@ -79,6 +81,7 @@ type DiskImage = FilePath
-- > mychroot d = debootstrapped mempty d $ props
-- > & osDebian Unstable X86_64
-- > & Apt.installed ["linux-image-amd64"]
+-- > & Grub.installed PC
-- > & User.hasPassword (User "root")
-- > & User.accountFor (User "demo")
-- > & User.hasPassword (User "demo")
@@ -92,7 +95,7 @@ type DiskImage = FilePath
-- > foo = host "foo.example.com" $ props
-- > & imageBuilt "/srv/diskimages/bar-disk.img"
-- > (hostChroot bar (Debootstrapped mempty))
--- > MSDOS (grubBooted PC)
+-- > MSDOS
-- > [ partition EXT2 `mountedAt` "/boot"
-- > `setFlag` BootFlag
-- > , partition EXT4 `mountedAt` "/"
@@ -104,18 +107,19 @@ type DiskImage = FilePath
-- > bar = host "bar.example.com" $ props
-- > & osDebian Unstable X86_64
-- > & Apt.installed ["linux-image-amd64"]
+-- > & Grub.installed PC
-- > & hasPassword (User "root")
-imageBuilt :: DiskImage -> (FilePath -> Chroot) -> TableType -> Finalization -> [PartSpec] -> RevertableProperty (HasInfo + DebianLike) Linux
+imageBuilt :: DiskImage -> (FilePath -> Chroot) -> TableType -> [PartSpec] -> RevertableProperty (HasInfo + DebianLike) Linux
imageBuilt = imageBuilt' False
-- | Like 'built', but the chroot is deleted and rebuilt from scratch each
-- time. This is more expensive, but useful to ensure reproducible results
-- when the properties of the chroot have been changed.
-imageRebuilt :: DiskImage -> (FilePath -> Chroot) -> TableType -> Finalization -> [PartSpec] -> RevertableProperty (HasInfo + DebianLike) Linux
+imageRebuilt :: DiskImage -> (FilePath -> Chroot) -> TableType -> [PartSpec] -> RevertableProperty (HasInfo + DebianLike) Linux
imageRebuilt = imageBuilt' True
-imageBuilt' :: Bool -> DiskImage -> (FilePath -> Chroot) -> TableType -> Finalization -> [PartSpec] -> RevertableProperty (HasInfo + DebianLike) Linux
-imageBuilt' rebuild img mkchroot tabletype final partspec =
+imageBuilt' :: Bool -> DiskImage -> (FilePath -> Chroot) -> TableType -> [PartSpec] -> RevertableProperty (HasInfo + DebianLike) Linux
+imageBuilt' rebuild img mkchroot tabletype partspec =
imageBuiltFrom img chrootdir tabletype final partspec
`requires` Chroot.provisioned chroot
`requires` (cleanrebuild <!> (doNothing :: Property UnixLike))
@@ -135,12 +139,16 @@ imageBuilt' rebuild img mkchroot tabletype final partspec =
-- Before ensuring any other properties of the chroot,
-- avoid starting services. Reverted by imageFinalized.
&^ Chroot.noServices
- -- First stage finalization.
- & fst final
& cachesCleaned
-- Only propagate privdata Info from this chroot, nothing else.
propprivdataonly (Chroot.Chroot d b ip h) =
Chroot.Chroot d b (\c _ -> ip c onlyPrivData) h
+ -- Pick boot loader finalization based on which bootloader is
+ -- installed.
+ final = case fromInfo (containerInfo chroot) of
+ [GrubInstalled] -> grubBooted
+ [] -> unbootable "no bootloader is installed"
+ _ -> unbootable "multiple bootloaders are installed; don't know which to use"
-- | This property is automatically added to the chroot when building a
-- disk image. It cleans any caches of information that can be omitted;
@@ -247,7 +255,7 @@ getMountSz szm l (Just mntpt) =
--
-- If the file is too large, truncates it down to the specified size.
imageExists :: FilePath -> ByteSize -> Property Linux
-imageExists img sz = property ("disk image exists" ++ img) $ liftIO $ do
+imageExists img isz = property ("disk image exists" ++ img) $ liftIO $ do
ms <- catchMaybeIO $ getFileStatus img
case ms of
Just s
@@ -258,21 +266,24 @@ imageExists img sz = property ("disk image exists" ++ img) $ liftIO $ do
_ -> do
L.writeFile img (L.replicate (fromIntegral sz) 0)
return MadeChange
-
--- | A pair of properties. The first property is satisfied within the
--- chroot, and is typically used to download the boot loader.
---
--- The second property is run after the disk image is created,
--- with its populated partition tree mounted in the provided
--- location from the provided loop devices. This will typically
--- take care of installing the boot loader to the image.
+ where
+ sz = ceiling (fromInteger isz / sectorsize) * ceiling sectorsize
+ -- Disks have a sector size, and making a disk image not
+ -- aligned to a sector size will confuse some programs.
+ -- Common sector sizes are 512 and 4096; use 4096 as it's larger.
+ sectorsize = 4096 :: Double
+
+-- | A property that is run after the disk image is created, with
+-- its populated partition tree mounted in the provided
+-- location from the provided loop devices. This is typically used to
+-- install a boot loader in the image's superblock.
--
--- It's ok if the second property leaves additional things mounted
+-- It's ok if the property leaves additional things mounted
-- in the partition tree.
-type Finalization = (Property Linux, (FilePath -> [LoopDev] -> Property Linux))
+type Finalization = (FilePath -> [LoopDev] -> Property Linux)
imageFinalized :: Finalization -> [Maybe MountPoint] -> [MountOpts] -> [LoopDev] -> PartTable -> Property Linux
-imageFinalized (_, final) mnts mntopts devs (PartTable _ parts) =
+imageFinalized final mnts mntopts devs (PartTable _ parts) =
property' "disk image finalized" $ \w ->
withTmpDir "mnt" $ \top ->
go w top `finally` liftIO (unmountall top)
@@ -316,48 +327,53 @@ imageFinalized (_, final) mnts mntopts devs (PartTable _ parts) =
allowservices top = nukeFile (top ++ "/usr/sbin/policy-rc.d")
-noFinalization :: Finalization
-noFinalization = (doNothing, \_ _ -> doNothing)
+unbootable :: String -> Finalization
+unbootable msg = \_ _ -> property desc $ do
+ warningMessage (desc ++ ": " ++ msg)
+ return FailedChange
+ where
+ desc = "image is not bootable"
-- | Makes grub be the boot loader of the disk image.
-grubBooted :: Grub.BIOS -> Finalization
-grubBooted bios = (Grub.installed' bios, boots)
+--
+-- This does not install the grub package. You will need to add
+-- the `Grub.installed` property to the chroot.
+grubBooted :: Finalization
+grubBooted mnt loopdevs = combineProperties "disk image boots using grub" $ props
+ -- bind mount host /dev so grub can access the loop devices
+ & bindMount "/dev" (inmnt "/dev")
+ & mounted "proc" "proc" (inmnt "/proc") mempty
+ & mounted "sysfs" "sys" (inmnt "/sys") mempty
+ -- update the initramfs so it gets the uuid of the root partition
+ & inchroot "update-initramfs" ["-u"]
+ `assume` MadeChange
+ -- work around for http://bugs.debian.org/802717
+ & check haveosprober (inchroot "chmod" ["-x", osprober])
+ & inchroot "update-grub" []
+ `assume` MadeChange
+ & check haveosprober (inchroot "chmod" ["+x", osprober])
+ & inchroot "grub-install" [wholediskloopdev]
+ `assume` MadeChange
+ -- sync all buffered changes out to the disk image
+ -- may not be necessary, but seemed needed sometimes
+ -- when using the disk image right away.
+ & cmdProperty "sync" []
+ `assume` NoChange
where
- boots mnt loopdevs = combineProperties "disk image boots using grub" $ props
- -- bind mount host /dev so grub can access the loop devices
- & bindMount "/dev" (inmnt "/dev")
- & mounted "proc" "proc" (inmnt "/proc") mempty
- & mounted "sysfs" "sys" (inmnt "/sys") mempty
- -- update the initramfs so it gets the uuid of the root partition
- & inchroot "update-initramfs" ["-u"]
- `assume` MadeChange
- -- work around for http://bugs.debian.org/802717
- & check haveosprober (inchroot "chmod" ["-x", osprober])
- & inchroot "update-grub" []
- `assume` MadeChange
- & check haveosprober (inchroot "chmod" ["+x", osprober])
- & inchroot "grub-install" [wholediskloopdev]
- `assume` MadeChange
- -- sync all buffered changes out to the disk image
- -- may not be necessary, but seemed needed sometimes
- -- when using the disk image right away.
- & cmdProperty "sync" []
- `assume` NoChange
- where
- -- cannot use </> since the filepath is absolute
- inmnt f = mnt ++ f
-
- inchroot cmd ps = cmdProperty "chroot" ([mnt, cmd] ++ ps)
-
- haveosprober = doesFileExist (inmnt osprober)
- osprober = "/etc/grub.d/30_os-prober"
-
- -- It doesn't matter which loopdev we use; all
- -- come from the same disk image, and it's the loop dev
- -- for the whole disk image we seek.
- wholediskloopdev = case loopdevs of
- (l:_) -> wholeDiskLoopDev l
- [] -> error "No loop devs provided!"
+ -- cannot use </> since the filepath is absolute
+ inmnt f = mnt ++ f
+
+ inchroot cmd ps = cmdProperty "chroot" ([mnt, cmd] ++ ps)
+
+ haveosprober = doesFileExist (inmnt osprober)
+ osprober = "/etc/grub.d/30_os-prober"
+
+ -- It doesn't matter which loopdev we use; all
+ -- come from the same disk image, and it's the loop dev
+ -- for the whole disk image we seek.
+ wholediskloopdev = case loopdevs of
+ (l:_) -> wholeDiskLoopDev l
+ [] -> error "No loop devs provided!"
isChild :: FilePath -> Maybe MountPoint -> Bool
isChild mntpt (Just d)
diff --git a/src/Propellor/Property/DiskImage/PartSpec.hs b/src/Propellor/Property/DiskImage/PartSpec.hs
index 4b05df03..2b14baa0 100644
--- a/src/Propellor/Property/DiskImage/PartSpec.hs
+++ b/src/Propellor/Property/DiskImage/PartSpec.hs
@@ -69,6 +69,14 @@ addFreeSpace (mp, o, p) freesz = (mp, o, \sz -> p (sz <> freesz))
setSize :: PartSpec -> PartSize -> PartSpec
setSize (mp, o, p) sz = (mp, o, const (p sz))
+-- | Sets the percent of the filesystem blocks reserved for the super-user.
+--
+-- The default is 5% for ext2 and ext4. Some filesystems may not support
+-- this.
+reservedSpacePercentage :: PartSpec -> Int -> PartSpec
+reservedSpacePercentage s percent = adjustp s $ \p ->
+ p { partMkFsOpts = ("-m"):show percent:partMkFsOpts p }
+
-- | Sets a flag on the partition.
setFlag :: PartSpec -> PartFlag -> PartSpec
setFlag s f = adjustp s $ \p -> p { partFlags = (f, True):partFlags p }
diff --git a/src/Propellor/Property/Docker.hs b/src/Propellor/Property/Docker.hs
index 1080418b..66418253 100644
--- a/src/Propellor/Property/Docker.hs
+++ b/src/Propellor/Property/Docker.hs
@@ -59,13 +59,13 @@ import qualified Propellor.Property.Pacman as Pacman
import qualified Propellor.Shim as Shim
import Utility.Path
import Utility.ThreadScheduler
+import Utility.Split
import Control.Concurrent.Async hiding (link)
import System.Posix.Directory
import System.Posix.Process
import Prelude hiding (init)
import Data.List hiding (init)
-import Data.List.Utils
import qualified Data.Map as M
import System.Console.Concurrent
@@ -576,8 +576,7 @@ provisionContainer cid = containerDesc cid $ property "provisioned" $ liftIO $ d
let p = inContainerProcess cid
(if isConsole msgh then ["-it"] else [])
(shim : params)
- r <- withHandle StdoutHandle createProcessSuccess p $
- processChainOutput
+ r <- chainPropellor p
when (r /= FailedChange) $
setProvisionedFlag cid
return r
@@ -596,10 +595,9 @@ chain hostlist hn s = case toContainerId s of
where
go cid h = do
changeWorkingDirectory localdir
- onlyProcess (provisioningLock cid) $ do
- r <- runPropellor h $ ensureChildProperties $ hostProperties h
- flushConcurrentOutput
- putStrLn $ "\n" ++ show r
+ onlyProcess (provisioningLock cid) $
+ runChainPropellor h $
+ ensureChildProperties $ hostProperties h
stopContainer :: ContainerId -> IO Bool
stopContainer cid = boolSystem dockercmd [Param "stop", Param $ fromContainerId cid ]
diff --git a/src/Propellor/Property/File.hs b/src/Propellor/Property/File.hs
index 459fe2c7..3293599a 100644
--- a/src/Propellor/Property/File.hs
+++ b/src/Propellor/Property/File.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FlexibleInstances, FlexibleContexts #-}
module Propellor.Property.File where
@@ -105,11 +105,11 @@ hasPrivContent' writemode source f context =
-- | Replaces the content of a file with the transformed content of another file
basedOn :: FilePath -> (FilePath, [Line] -> [Line]) -> Property UnixLike
-f `basedOn` (f', a) = property' desc $ \o -> do
- tmpl <- liftIO $ readFile f'
+f `basedOn` (src, a) = property' desc $ \o -> do
+ tmpl <- liftIO $ readFile src
ensureProperty o $ fileProperty desc (\_ -> a $ lines $ tmpl) f
where
- desc = f ++ " is based on " ++ f'
+ desc = f ++ " is based on " ++ src
-- | Removes a file. Does not remove symlinks or non-plain-files.
notPresent :: FilePath -> Property UnixLike
@@ -150,23 +150,26 @@ link `isSymlinkedTo` (LinkTarget target) = property desc $
-- | Ensures that a file is a copy of another (regular) file.
isCopyOf :: FilePath -> FilePath -> Property UnixLike
-f `isCopyOf` f' = property desc $ go =<< (liftIO $ tryIO $ getFileStatus f')
+f `isCopyOf` src = property desc $ go =<< (liftIO $ tryIO $ getFileStatus src)
where
- desc = f ++ " is copy of " ++ f'
+ desc = f ++ " is copy of " ++ src
go (Right stat) = if isRegularFile stat
- then gocmp =<< (liftIO $ cmp)
- else warningMessage (f' ++ " is not a regular file") >>
+ then ifM (liftIO $ doesFileExist f)
+ ( gocmp =<< (liftIO $ cmp)
+ , doit
+ )
+ else warningMessage (src ++ " is not a regular file") >>
return FailedChange
go (Left e) = warningMessage (show e) >> return FailedChange
- cmp = safeSystem "cmp" [Param "-s", Param "--", File f, File f']
+ cmp = safeSystem "cmp" [Param "-s", Param "--", File f, File src]
gocmp ExitSuccess = noChange
gocmp (ExitFailure 1) = doit
gocmp _ = warningMessage "cmp failed" >> return FailedChange
- doit = makeChange $ copy f' `viaStableTmp` f
- copy src dest = unlessM (runcp src dest) $ errorMessage "cp failed"
- runcp src dest = boolSystem "cp"
+ doit = makeChange $ copy `viaStableTmp` f
+ copy dest = unlessM (runcp dest) $ errorMessage "cp failed"
+ runcp dest = boolSystem "cp"
[Param "--preserve=all", Param "--", File src, File dest]
-- | Ensures that a file/dir has the specified owner and group.
@@ -177,6 +180,20 @@ ownerGroup f (User owner) (Group group) = p `describe` (f ++ " owner " ++ og)
`changesFile` f
og = owner ++ ":" ++ group
+-- | Given a base directory, and a relative path under that
+-- directory, applies a property to each component of the path in turn,
+-- starting with the base directory.
+--
+-- For example, to make a file owned by a user, making sure their home
+-- directory and the subdirectories to it are also owned by them:
+--
+-- > "/home/user/program/file" `hasContent` ["foo"]
+-- > `before` applyPath "/home/user" ".config/program/file"
+-- > (\f -> ownerGroup f (User "user") (Group "user"))
+applyPath :: Monoid (Property metatypes) => FilePath -> FilePath -> (FilePath -> Property metatypes) -> Property metatypes
+applyPath basedir relpath mkp = mconcat $
+ map mkp (scanl (</>) basedir (splitPath relpath))
+
-- | Ensures that a file/dir has the specfied mode.
mode :: FilePath -> FileMode -> Property UnixLike
mode f v = p `changesFile` f
@@ -290,3 +307,12 @@ readConfigFileName = readish . unescape
Nothing -> '_' : ns ++ unescape cs'
Just n -> chr n : unescape cs'
unescape (c:cs) = c : unescape cs
+
+data Overwrite = OverwriteExisting | PreserveExisting
+
+-- | When passed PreserveExisting, only ensures the property when the file
+-- does not exist.
+checkOverwrite :: Overwrite -> FilePath -> (FilePath -> Property i) -> Property i
+checkOverwrite OverwriteExisting f mkp = mkp f
+checkOverwrite PreserveExisting f mkp =
+ check (not <$> doesFileExist f) (mkp f)
diff --git a/src/Propellor/Property/FreeDesktop.hs b/src/Propellor/Property/FreeDesktop.hs
new file mode 100644
index 00000000..75dcbdfa
--- /dev/null
+++ b/src/Propellor/Property/FreeDesktop.hs
@@ -0,0 +1,29 @@
+-- | Freedesktop.org configuration file properties.
+
+module Propellor.Property.FreeDesktop where
+
+import Propellor.Base
+import Propellor.Property.ConfFile
+
+desktopFile :: String -> FilePath
+desktopFile s = s ++ ".desktop"
+
+-- | Name used in a desktop file; user visible.
+type Name = String
+
+-- | Command that a dekstop file runs. May include parameters.
+type Exec = String
+
+-- | Specifies an autostart file. By default it will be located in the
+-- system-wide autostart directory.
+autostart :: FilePath -> Name -> Exec -> RevertableProperty UnixLike UnixLike
+autostart f n e = ("/etc/xdg/autostart" </> f) `iniFileContains`
+ [ ("Desktop Entry",
+ [ ("Type", "Application")
+ , ("Version", "1.0")
+ , ("Name", n)
+ , ("Comment", "Autostart")
+ , ("Terminal", "False")
+ , ("Exec", e)
+ ] )
+ ]
diff --git a/src/Propellor/Property/Grub.hs b/src/Propellor/Property/Grub.hs
index 9dd5e8e1..4bad7b2b 100644
--- a/src/Propellor/Property/Grub.hs
+++ b/src/Propellor/Property/Grub.hs
@@ -3,6 +3,9 @@ module Propellor.Property.Grub where
import Propellor.Base
import qualified Propellor.Property.File as File
import qualified Propellor.Property.Apt as Apt
+import Propellor.Property.Chroot (inChroot)
+import Propellor.Types.Info
+import Propellor.Types.Bootloader
-- | Eg, \"hd0,0\" or \"xen/xvda1\"
type GrubDevice = String
@@ -18,9 +21,10 @@ data BIOS = PC | EFI64 | EFI32 | Coreboot | Xen
-- | Installs the grub package. This does not make grub be used as the
-- bootloader.
--
--- This includes running update-grub.
-installed :: BIOS -> Property DebianLike
-installed bios = installed' bios `onChange` mkConfig
+-- This includes running update-grub, unless it's run in a chroot.
+installed :: BIOS -> Property (HasInfo + DebianLike)
+installed bios = installed' bios
+ `onChange` (check (not <$> inChroot) mkConfig)
-- Run update-grub, to generate the grub boot menu. It will be
-- automatically updated when kernel packages are installed.
@@ -29,11 +33,11 @@ mkConfig = tightenTargets $ cmdProperty "update-grub" []
`assume` MadeChange
-- | Installs grub; does not run update-grub.
-installed' :: BIOS -> Property Linux
-installed' bios = (aptinstall `pickOS` unsupportedOS)
+installed' :: BIOS -> Property (HasInfo + DebianLike)
+installed' bios = setInfoProperty aptinstall
+ (toInfo [GrubInstalled])
`describe` "grub package installed"
where
- aptinstall :: Property DebianLike
aptinstall = Apt.installed [debpkg]
debpkg = case bios of
PC -> "grub-pc"
@@ -64,7 +68,7 @@ boots dev = tightenTargets $ cmdProperty "grub-install" [dev]
--
-- The rootdev should be in the form "hd0", while the bootdev is in the form
-- "xen/xvda".
-chainPVGrub :: GrubDevice -> GrubDevice -> TimeoutSecs -> Property DebianLike
+chainPVGrub :: GrubDevice -> GrubDevice -> TimeoutSecs -> Property (HasInfo + DebianLike)
chainPVGrub rootdev bootdev timeout = combineProperties desc $ props
& File.dirExists "/boot/grub"
& "/boot/grub/menu.lst" `File.hasContent`
diff --git a/src/Propellor/Property/HostingProvider/Linode.hs b/src/Propellor/Property/HostingProvider/Linode.hs
index fca3df63..ebe8d261 100644
--- a/src/Propellor/Property/HostingProvider/Linode.hs
+++ b/src/Propellor/Property/HostingProvider/Linode.hs
@@ -8,7 +8,7 @@ import Utility.FileMode
-- | Configures grub to use the serial console as set up by Linode.
-- Useful when running a distribution supplied kernel.
-- <https://www.linode.com/docs/tools-reference/custom-kernels-distros/run-a-distribution-supplied-kernel-with-kvm>
-serialGrub :: Property DebianLike
+serialGrub :: Property (HasInfo + DebianLike)
serialGrub = "/etc/default/grub" `File.containsLines`
[ "GRUB_CMDLINE_LINUX=\"console=ttyS0,19200n8\""
, "GRUB_DISABLE_LINUX_UUID=true"
@@ -17,11 +17,12 @@ serialGrub = "/etc/default/grub" `File.containsLines`
]
`onChange` Grub.mkConfig
`requires` Grub.installed Grub.PC
+ `describe` "GRUB configured for Linode serial console"
-- | Linode's pv-grub-x86_64 (only used for its older XEN instances)
-- does not support booting recent Debian kernels compressed
-- with xz. This sets up pv-grub chaining to enable it.
-chainPVGrub :: Grub.TimeoutSecs -> Property DebianLike
+chainPVGrub :: Grub.TimeoutSecs -> Property (HasInfo + DebianLike)
chainPVGrub = Grub.chainPVGrub "hd0" "xen/xvda"
-- | Linode disables mlocate's cron job's execute permissions,
diff --git a/src/Propellor/Property/Hostname.hs b/src/Propellor/Property/Hostname.hs
index e1342d91..1eb9d690 100644
--- a/src/Propellor/Property/Hostname.hs
+++ b/src/Propellor/Property/Hostname.hs
@@ -3,9 +3,9 @@ module Propellor.Property.Hostname where
import Propellor.Base
import qualified Propellor.Property.File as File
import Propellor.Property.Chroot (inChroot)
+import Utility.Split
import Data.List
-import Data.List.Utils
-- | Ensures that the hostname is set using best practices, to whatever
-- name the `Host` has.
diff --git a/src/Propellor/Property/LightDM.hs b/src/Propellor/Property/LightDM.hs
index 339fa9a3..69538d89 100644
--- a/src/Propellor/Property/LightDM.hs
+++ b/src/Propellor/Property/LightDM.hs
@@ -10,7 +10,8 @@ installed :: Property DebianLike
installed = Apt.installed ["lightdm"]
-- | Configures LightDM to skip the login screen and autologin as a user.
-autoLogin :: User -> Property UnixLike
+autoLogin :: User -> Property DebianLike
autoLogin (User u) = "/etc/lightdm/lightdm.conf" `ConfFile.containsIniSetting`
("SeatDefaults", "autologin-user", u)
`describe` "lightdm autologin"
+ `requires` installed
diff --git a/src/Propellor/Property/Sbuild.hs b/src/Propellor/Property/Sbuild.hs
index 00109381..460d0b16 100644
--- a/src/Propellor/Property/Sbuild.hs
+++ b/src/Propellor/Property/Sbuild.hs
@@ -98,10 +98,10 @@ import qualified Propellor.Property.File as File
import qualified Propellor.Property.Schroot as Schroot
import qualified Propellor.Property.Reboot as Reboot
import qualified Propellor.Property.User as User
-
import Utility.FileMode
+import Utility.Split
+
import Data.List
-import Data.List.Utils
type Suite = String
diff --git a/src/Propellor/Property/SiteSpecific/JoeySites.hs b/src/Propellor/Property/SiteSpecific/JoeySites.hs
index 063a2eda..6e0d6c4e 100644
--- a/src/Propellor/Property/SiteSpecific/JoeySites.hs
+++ b/src/Propellor/Property/SiteSpecific/JoeySites.hs
@@ -22,10 +22,10 @@ import qualified Propellor.Property.Systemd as Systemd
import qualified Propellor.Property.Fail2Ban as Fail2Ban
import qualified Propellor.Property.LetsEncrypt as LetsEncrypt
import Utility.FileMode
+import Utility.Split
import Data.List
import System.Posix.Files
-import Data.String.Utils
scrollBox :: Property (HasInfo + DebianLike)
scrollBox = propertyList "scroll server" $ props
@@ -248,7 +248,7 @@ gitServer hosts = propertyList "git.kitenet.net setup" $ props
]
`describe` "cgit configured"
-- I keep the website used for git.kitenet.net/git.joeyh.name checked into git..
- & Git.cloned (User "root") "/srv/git/joey/git.kitenet.net.git" "/srv/web/git.kitenet.net" Nothing
+ & Git.cloned (User "joey") "/srv/git/joey/git.kitenet.net.git" "/srv/web/git.kitenet.net" Nothing
-- Don't need global apache configuration for cgit.
! Apache.confEnabled "cgit"
& website "git.kitenet.net"
@@ -681,6 +681,10 @@ dkimInstalled = go `onChange` Service.restarted "opendkim"
& File.ownerGroup "/etc/mail/dkim.key" (User "opendkim") (Group "opendkim")
& "/etc/default/opendkim" `File.containsLine`
"SOCKET=\"inet:8891@localhost\""
+ `onChange`
+ (cmdProperty "/lib/opendkim/opendkim.service.generate" []
+ `assume` MadeChange)
+ `onChange` Service.restarted "opendkim"
& "/etc/opendkim.conf" `File.containsLines`
[ "KeyFile /etc/mail/dkim.key"
, "SubDomains yes"
@@ -694,9 +698,22 @@ dkimInstalled = go `onChange` Service.restarted "opendkim"
domainKey :: (BindDomain, Record)
domainKey = (RelDomain "mail._domainkey", TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCc+/rfzNdt5DseBBmfB3C6sVM7FgVvf4h1FeCfyfwPpVcmPdW6M2I+NtJsbRkNbEICxiP6QY2UM0uoo9TmPqLgiCCG2vtuiG6XMsS0Y/gGwqKM7ntg/7vT1Go9vcquOFFuLa5PnzpVf8hB9+PMFdS4NPTvWL2c5xxshl/RJzICnQIDAQAB")
-hasJoeyCAChain :: Property (HasInfo + UnixLike)
-hasJoeyCAChain = "/etc/ssl/certs/joeyca.pem" `File.hasPrivContentExposed`
- Context "joeyca.pem"
+postfixSaslPasswordClient :: Property (HasInfo + DebianLike)
+postfixSaslPasswordClient = combineProperties "postfix uses SASL password to authenticate with smarthost" $ props
+ & Postfix.satellite
+ & Postfix.mappedFile "/etc/postfix/sasl_passwd"
+ (`File.hasPrivContent` (Context "kitenet.net"))
+ & Postfix.mainCfFile `File.containsLines`
+ [ "# TLS setup for SASL auth to kite"
+ , "smtp_sasl_auth_enable = yes"
+ , "smtp_tls_security_level = encrypt"
+ , "smtp_sasl_tls_security_options = noanonymous"
+ , "relayhost = [kitenet.net]"
+ , "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd"
+ , "# kite's fingerprint"
+ , "smtp_tls_fingerprint_cert_match = 13:B0:0C:F3:11:83:A5:EB:A9:37:C6:C5:ED:16:60:86"
+ ]
+ `onChange` Postfix.reloaded
hasPostfixCert :: Context -> Property (HasInfo + UnixLike)
hasPostfixCert ctx = combineProperties "postfix tls cert installed" $ props
diff --git a/src/Propellor/Property/Systemd.hs b/src/Propellor/Property/Systemd.hs
index 7c40bd16..d1a94aa8 100644
--- a/src/Propellor/Property/Systemd.hs
+++ b/src/Propellor/Property/Systemd.hs
@@ -55,9 +55,9 @@ import qualified Propellor.Property.Apt as Apt
import qualified Propellor.Property.File as File
import Propellor.Property.Systemd.Core
import Utility.FileMode
+import Utility.Split
import Data.List
-import Data.List.Utils
import qualified Data.Map as M
type ServiceName = String
diff --git a/src/Propellor/Property/User.hs b/src/Propellor/Property/User.hs
index 0c7e48f2..ce2611bc 100644
--- a/src/Propellor/Property/User.hs
+++ b/src/Propellor/Property/User.hs
@@ -97,8 +97,12 @@ setPassword getpassword = getpassword $ go
-- | Makes a user's password be the passed String. Highly insecure:
-- The password is right there in your config file for anyone to see!
hasInsecurePassword :: User -> String -> Property DebianLike
-hasInsecurePassword u@(User n) p = property (n ++ " has insecure password") $
- chpasswd u p []
+hasInsecurePassword u@(User n) p = go
+ `requires` shadowConfig True
+ where
+ go :: Property DebianLike
+ go = property (n ++ " has insecure password") $
+ chpasswd u p []
chpasswd :: User -> String -> [String] -> Propellor Result
chpasswd (User user) v ps = makeChange $ withHandle StdinHandle createProcessSuccess
diff --git a/src/Propellor/Property/XFCE.hs b/src/Propellor/Property/XFCE.hs
new file mode 100644
index 00000000..dc57660f
--- /dev/null
+++ b/src/Propellor/Property/XFCE.hs
@@ -0,0 +1,41 @@
+module Propellor.Property.XFCE where
+
+import Propellor.Base
+import qualified Propellor.Property.Apt as Apt
+import qualified Propellor.Property.File as File
+import qualified Propellor.Property.User as User
+
+installed :: Property DebianLike
+installed = Apt.installed ["task-xfce-desktop"]
+ `describe` "XFCE desktop installed"
+
+-- | Minimal install of XFCE, with a terminal emulator and panel,
+-- and X and network-manager, but not any of the extra apps.
+installedMin :: Property DebianLike
+installedMin = Apt.installedMin ["xfce4", "xfce4-terminal", "task-desktop"]
+ `describe` "minimal XFCE desktop installed"
+
+-- | Installs network-manager-gnome, which is the way to get
+-- network-manager to manage networking in XFCE too.
+networkManager :: Property DebianLike
+networkManager = Apt.installedMin ["network-manager-gnome"]
+
+-- | Normally at first login, XFCE asks what kind of panel the user wants.
+-- This enables the default configuration noninteractively.
+defaultPanelFor :: User -> File.Overwrite -> Property DebianLike
+defaultPanelFor u@(User username) overwrite = property' desc $ \w -> do
+ home <- liftIO $ User.homedir u
+ ensureProperty w (go home)
+ where
+ desc = "default XFCE panel for " ++ username
+ basecf = ".config" </> "xfce4" </> "xfconf"
+ </> "xfce-perchannel-xml" </> "xfce4-panel.xml"
+ -- This location is probably Debian-specific.
+ defcf = "/etc/xdg/xfce4/panel/default.xml"
+ go :: FilePath -> Property DebianLike
+ go home = tightenTargets $
+ File.checkOverwrite overwrite (home </> basecf) $ \cf ->
+ cf `File.isCopyOf` defcf
+ `before` File.applyPath home basecf
+ (\f -> File.ownerGroup f u (userGroup u))
+ `requires` Apt.installed ["xfce4-panel"]
diff --git a/src/Propellor/Property/ZFS/Process.hs b/src/Propellor/Property/ZFS/Process.hs
index 372bac6d..42b23df2 100644
--- a/src/Propellor/Property/ZFS/Process.hs
+++ b/src/Propellor/Property/ZFS/Process.hs
@@ -5,7 +5,8 @@
module Propellor.Property.ZFS.Process where
import Propellor.Base
-import Data.String.Utils (split)
+import Utility.Split
+
import Data.List
-- | Gets the properties of a ZFS volume.