From f78c2f16d1c93ee6fe2620916b7584d91d116723 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 3 Dec 2014 19:05:18 -0400 Subject: update name of libgnutls dev package --- src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs b/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs index 0208dea6..bf87b210 100644 --- a/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs +++ b/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs @@ -76,7 +76,7 @@ buildDepsNoHaskellLibs = Apt.installed "debhelper", "ghc", "curl", "openssh-client", "git-remote-gcrypt", "liblockfile-simple-perl", "cabal-install", "vim", "less", -- needed by haskell libs - "libxml2-dev", "libidn11-dev", "libgsasl7-dev", "libgnutls-dev", + "libxml2-dev", "libidn11-dev", "libgsasl7-dev", "libgnutls28-dev", "alex", "happy", "c2hs" ] -- cgit v1.3-2-g0d8e From a2537ce16c55871907bce64be108b10c2ee6283b Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 16:14:01 -0400 Subject: propellor spin --- config-joey.hs | 10 +++--- src/Propellor/Property/OS.hs | 78 ++++++++++++++++++++++++++++-------------- src/Propellor/Property/User.hs | 12 +++++++ 3 files changed, 70 insertions(+), 30 deletions(-) diff --git a/config-joey.hs b/config-joey.hs index b617ccfa..03f21330 100644 --- a/config-joey.hs +++ b/config-joey.hs @@ -54,11 +54,11 @@ hosts = -- (o) ` testvm :: Host testvm = host "testvm.kitenet.net" - & Chroot.provisioned (Chroot.debootstrapped (System (Debian Unstable) "amd64") Debootstrap.DefaultConfig "/new-os") - -- & OS.cleanInstall (OS.Confirmed "foo.example.com") [] - -- `onChange` propertyList "fixing up after clean install" - -- [ - -- ] + & os (System (Debian Unstable) "amd64") + & OS.cleanInstallOnce (OS.Confirmed "testvm.kitenet.netno") + `onChange` propertyList "fixing up after clean install" + [ OS.preserveRootSshAuthorized + ] darkstar :: Host darkstar = host "darkstar.kitenet.net" diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 5dddff2c..ce8f44f6 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -1,20 +1,19 @@ module Propellor.Property.OS ( cleanInstallOnce, - Confirmed(..), + Confirmation(..), preserveNetworkInterfaces, preserveRootSshAuthorized, grubBoots, GrubDev(..), + oldOSKernelPreserved, kernelInstalled, oldOSRemoved, ) where import Propellor -import qualified Propellor.Property.Chroot as Chroot import qualified Propellor.Property.Debootstrap as Debootstrap -import qualified Propellor.Property.File as File import qualified Propellor.Property.Ssh as Ssh -import Utility.FileMode +import qualified Propellor.Property.User as User -- | Replaces whatever OS was installed before with a clean installation -- of the OS that the Host is configured to have. @@ -22,8 +21,10 @@ import Utility.FileMode -- This can replace one Linux distribution with different one. -- But, it can also fail and leave the system in an unbootable state. -- +-- The files from the old os will be left in /old-os +-- -- To avoid this property being accidentially used, you have to provide --- a Confirmed containing the name of the host that you intend to apply the +-- a Confirmation containing the name of the host that you intend to apply the -- property to. -- -- This property only runs once. The cleanly installed system will have @@ -35,53 +36,73 @@ import Utility.FileMode -- working system. For example: -- -- > & os (System (Debian Unstable) "amd64") --- > & cleanInstall (Confirmed "foo.example.com") [BackupOldOS, UseOldKernel] +-- > & cleanInstall (Confirmed "foo.example.com") -- > `onChange` propertyList "fixing up after clean install" -- > [ preserveNetworkInterfaces -- > , preserverRootSshAuthorized +-- > , oldOSKernelPreserved -- > -- , kernelInstalled -- > -- , grubBoots "hd0" +-- > -- , oldOsRemoved -- > ] -- > & Apt.installed ["ssh"] -- > & User.hasSomePassword "root" -- > & User.accountFor "joey" -- > & User.hasSomePassword "joey" -- > -- rest of system properties here -cleanInstallOnce :: Confirmed -> [Tweak] -> Property -cleanInstallOnce confirmed tweaks = check (not <$> doesFileExist flagfile) $ - property "OS cleanly installed" $ do - checkConfirmed confirmed +cleanInstallOnce :: Confirmation -> Property +cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ + confirmed + `before` + osbootstrapped + `before` + transitioned + `before` + User.shadowConfig True + `before` + propellorbootstrapped + `before` + finalized + where + confirmed = property "clean install confirmed" $ do + checkConfirmed confirmation + return NoChange + + osbootstrapped = withOS "/new-os bootstrapped" $ \o -> case o of + (Just d@(System (Debian _) _)) -> debootstrap d + (Just u@(System (Ubuntu _) _)) -> debootstrap u + _ -> error "os is not declared to be Debian or Ubuntu" + debootstrap targetos = ensureProperty $ toProp $ + Debootstrap.built "/new-os" targetos Debootstrap.DefaultConfig + + transitioned = property "/new-os moved into place" $ error "TODO" - -- debootstrap /new-os chroot, but don't run propellor - -- inside the chroot. -- unmount all mounts -- move all directories to /old-os, - -- except for /boot and /lib/modules when UseOldKernel - -- (or, delete when not BackupOldOS) -- move /new-os to / -- touch flagfile + + propellorbootstrapped = property "propellor re-debootstrapped in new os" $ + error "TODO" -- re-bootstrap propellor in /usr/local/propellor, -- (using git repo bundle, privdata file, and possibly -- git repo url, which all need to be arranged to -- be present in /old-os's /usr/local/propellor) - -- enable shadow passwords (to avoid foot-shooting) - -- return MadeChange - where + + finalized = property "clean install finalized" $ do + liftIO $ writeFile flagfile "" + return MadeChange + flagfile = "/etc/propellor-cleaninstall" -data Confirmed = Confirmed HostName +data Confirmation = Confirmed HostName -checkConfirmed :: Confirmed -> Propellor () +checkConfirmed :: Confirmation -> Propellor () checkConfirmed (Confirmed c) = do hostname <- asks hostName when (hostname /= c) $ errorMessage "Run with a bad confirmation, not matching hostname." --- | Sometimes you want an almost clean install, but with some tweaks. -data Tweak - = UseOldKernel -- ^ Leave /boot and /lib/modules from old OS, so the system can boot using them as before - | BackupOldOS -- ^ Back up old OS to /old-os, to avoid losing any important files - -- /etc/network/interfaces is configured to bring up all interfaces that -- are currently up, using the same IP addresses. preserveNetworkInterfaces :: Property @@ -103,6 +124,13 @@ preserveRootSshAuthorized = check (doesDirectoryExist oldloc) $ kernelInstalled :: Property kernelInstalled = undefined +-- Copies kernel images, initrds, and modules from /old-os +-- into the new system. +-- +-- TODO: grub config? +oldOSKernelPreserved :: Property +oldOSKernelPreserved = undefined + -- Installs grub onto a device to boot the system. -- -- You may want to install grub to multiple devices; eg for a system @@ -113,7 +141,7 @@ grubBoots = undefined type GrubDev = String -- Removes the old OS's backup from /old-os -oldOSRemoved :: Confirmed -> Property +oldOSRemoved :: Confirmation -> Property oldOSRemoved confirmed = check (doesDirectoryExist oldOsDir) $ property "old OS backup removed" $ do checkConfirmed confirmed diff --git a/src/Propellor/Property/User.hs b/src/Propellor/Property/User.hs index 434a92a3..ccb69b24 100644 --- a/src/Propellor/Property/User.hs +++ b/src/Propellor/Property/User.hs @@ -84,3 +84,15 @@ hasGroup user group' = check test $ cmdProperty "adduser" `describe` unwords ["user", user, "in group", group'] where test = not . elem group' . words <$> readProcess "groups" [user] + +-- | Controls whether shadow passwords are enabled or not. +shadowConfig :: Bool -> Property +shadowConfig True = check (not <$> shadowExists) $ + cmdProperty "shadowconfig" ["on"] + `describe` "shadow passwords enabled" +shadowConfig False = check shadowExists $ + cmdProperty "shadowconfig" ["off"] + `describe` "shadow passwords disabled" + +shadowExists :: IO Bool +shadowExists = doesFileExist "/etc/shadow" -- cgit v1.3-2-g0d8e -- cgit v1.3-2-g0d8e -- cgit v1.3-2-g0d8e -- cgit v1.3-2-g0d8e From 0aa635dbd2412b02e38385376f27adfb7962c974 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 16:21:09 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index ce8f44f6..8f852bbd 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -52,7 +52,7 @@ import qualified Propellor.Property.User as User -- > -- rest of system properties here cleanInstallOnce :: Confirmation -> Property cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ - confirmed + confirmed "clean install confirmed" confirmation `before` osbootstrapped `before` @@ -64,10 +64,6 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ `before` finalized where - confirmed = property "clean install confirmed" $ do - checkConfirmed confirmation - return NoChange - osbootstrapped = withOS "/new-os bootstrapped" $ \o -> case o of (Just d@(System (Debian _) _)) -> debootstrap d (Just u@(System (Ubuntu _) _)) -> debootstrap u @@ -97,11 +93,14 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ data Confirmation = Confirmed HostName -checkConfirmed :: Confirmation -> Propellor () -checkConfirmed (Confirmed c) = do +confirmed :: Desc -> Confirmation -> Property +confirmed desc (Confirmed c) = property desc $ do hostname <- asks hostName - when (hostname /= c) $ - errorMessage "Run with a bad confirmation, not matching hostname." + if hostname /= c + then do + warningMessage "Run with a bad confirmation, not matching hostname." + return FailedChange + else return NoChange -- /etc/network/interfaces is configured to bring up all interfaces that -- are currently up, using the same IP addresses. @@ -142,9 +141,10 @@ type GrubDev = String -- Removes the old OS's backup from /old-os oldOSRemoved :: Confirmation -> Property -oldOSRemoved confirmed = check (doesDirectoryExist oldOsDir) $ - property "old OS backup removed" $ do - checkConfirmed confirmed +oldOSRemoved confirmation = check (doesDirectoryExist oldOsDir) $ + go `requires` confirmed "old OS backup removal confirmed" confirmation + where + go = property "old OS backup removed" $ do liftIO $ removeDirectoryRecursive oldOsDir return MadeChange -- cgit v1.3-2-g0d8e -- cgit v1.3-2-g0d8e From c99e367c3bb1da193c2d8f756b5fe3ec9c490b53 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 16:22:29 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 8f852bbd..0138be9c 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -52,9 +52,9 @@ import qualified Propellor.Property.User as User -- > -- rest of system properties here cleanInstallOnce :: Confirmation -> Property cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ - confirmed "clean install confirmed" confirmation - `before` osbootstrapped + `requires` + confirmed "clean install confirmed" confirmation `before` transitioned `before` -- cgit v1.3-2-g0d8e From 1106c305d05f910795b4b549ef0348ce6ba15b85 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 16:23:00 -0400 Subject: propellor spin --- config-joey.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-joey.hs b/config-joey.hs index 03f21330..a1ea21d1 100644 --- a/config-joey.hs +++ b/config-joey.hs @@ -55,7 +55,7 @@ hosts = -- (o) ` testvm :: Host testvm = host "testvm.kitenet.net" & os (System (Debian Unstable) "amd64") - & OS.cleanInstallOnce (OS.Confirmed "testvm.kitenet.netno") + & OS.cleanInstallOnce (OS.Confirmed "testvm.kitenet.net") `onChange` propertyList "fixing up after clean install" [ OS.preserveRootSshAuthorized ] -- cgit v1.3-2-g0d8e From 5eb980d04851e26ed5cff47d98374de9688b349d Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 16:24:35 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 0138be9c..f06e1b78 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -52,18 +52,19 @@ import qualified Propellor.Property.User as User -- > -- rest of system properties here cleanInstallOnce :: Confirmation -> Property cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ - osbootstrapped - `requires` - confirmed "clean install confirmed" confirmation - `before` - transitioned - `before` - User.shadowConfig True - `before` - propellorbootstrapped - `before` - finalized + go `requires` confirmed "clean install confirmed" confirmation where + go = + osbootstrapped + `before` + transitioned + `before` + User.shadowConfig True + `before` + propellorbootstrapped + `before` + finalized + osbootstrapped = withOS "/new-os bootstrapped" $ \o -> case o of (Just d@(System (Debian _) _)) -> debootstrap d (Just u@(System (Ubuntu _) _)) -> debootstrap u -- cgit v1.3-2-g0d8e From 05ad6ad130139333e6029f4ea7cc05e3c7ed1bf0 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 16:25:12 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index f06e1b78..02d5f47d 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -73,21 +73,21 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ Debootstrap.built "/new-os" targetos Debootstrap.DefaultConfig transitioned = property "/new-os moved into place" $ - error "TODO" + return NoChange -- unmount all mounts -- move all directories to /old-os, -- move /new-os to / -- touch flagfile propellorbootstrapped = property "propellor re-debootstrapped in new os" $ - error "TODO" + return NoChange -- re-bootstrap propellor in /usr/local/propellor, -- (using git repo bundle, privdata file, and possibly -- git repo url, which all need to be arranged to -- be present in /old-os's /usr/local/propellor) finalized = property "clean install finalized" $ do - liftIO $ writeFile flagfile "" + --liftIO $ writeFile flagfile "" return MadeChange flagfile = "/etc/propellor-cleaninstall" -- cgit v1.3-2-g0d8e From 228154cf1f1c9c28f933a7e073f421ba0b876275 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 16:27:54 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 02d5f47d..e09b3d8b 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -73,21 +73,21 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ Debootstrap.built "/new-os" targetos Debootstrap.DefaultConfig transitioned = property "/new-os moved into place" $ - return NoChange + return FailedChange -- unmount all mounts -- move all directories to /old-os, -- move /new-os to / -- touch flagfile propellorbootstrapped = property "propellor re-debootstrapped in new os" $ - return NoChange + return FailedChange -- re-bootstrap propellor in /usr/local/propellor, -- (using git repo bundle, privdata file, and possibly -- git repo url, which all need to be arranged to -- be present in /old-os's /usr/local/propellor) finalized = property "clean install finalized" $ do - --liftIO $ writeFile flagfile "" + liftIO $ writeFile flagfile "" return MadeChange flagfile = "/etc/propellor-cleaninstall" -- cgit v1.3-2-g0d8e -- cgit v1.3-2-g0d8e From e47fbd9b39708e3488e047a5c22565ff23e79d46 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 16:29:33 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index e09b3d8b..20e6e47f 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -55,15 +55,15 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ go `requires` confirmed "clean install confirmed" confirmation where go = - osbootstrapped - `before` - transitioned - `before` - User.shadowConfig True - `before` - propellorbootstrapped - `before` finalized + `requires` + propellorbootstrapped + `requires` + User.shadowConfig True + `requires` + flipped + `requires` + osbootstrapped osbootstrapped = withOS "/new-os bootstrapped" $ \o -> case o of (Just d@(System (Debian _) _)) -> debootstrap d @@ -72,7 +72,7 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ debootstrap targetos = ensureProperty $ toProp $ Debootstrap.built "/new-os" targetos Debootstrap.DefaultConfig - transitioned = property "/new-os moved into place" $ + flipped = property "/new-os moved into place" $ return FailedChange -- unmount all mounts -- move all directories to /old-os, -- cgit v1.3-2-g0d8e From bf4840f341c83f28a53cf80fd7750a661e734d65 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 16:50:00 -0400 Subject: propellor spin --- propellor.cabal | 1 + src/Propellor/Property/Debootstrap.hs | 8 ++---- src/Propellor/Property/Mount.hs | 12 +++++++++ src/Propellor/Property/OS.hs | 46 +++++++++++++++++++++++------------ 4 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 src/Propellor/Property/Mount.hs diff --git a/propellor.cabal b/propellor.cabal index 617a1fc8..91d08bd5 100644 --- a/propellor.cabal +++ b/propellor.cabal @@ -85,6 +85,7 @@ Library Propellor.Property.Gpg Propellor.Property.Group Propellor.Property.Grub + Propellor.Property.Mount Propellor.Property.Network Propellor.Property.Nginx Propellor.Property.Obnam diff --git a/src/Propellor/Property/Debootstrap.hs b/src/Propellor/Property/Debootstrap.hs index ab5bddf4..35d9e472 100644 --- a/src/Propellor/Property/Debootstrap.hs +++ b/src/Propellor/Property/Debootstrap.hs @@ -9,6 +9,7 @@ module Propellor.Property.Debootstrap ( import Propellor import qualified Propellor.Property.Apt as Apt import Propellor.Property.Chroot.Util +import Propellor.Property.Mount import Utility.Path import Utility.SafeCommand import Utility.FileMode @@ -95,9 +96,7 @@ built target system@(System _ arch) config = submnts <- filter (\p -> simplifyPath p /= simplifyPath target) . filter (dirContains target) <$> mountPoints - forM_ submnts $ \mnt -> - unlessM (boolSystem "umount" [ Param "-l", Param mnt ]) $ do - errorMessage $ "failed unmounting " ++ mnt + forM_ submnts umountLazy removeDirectoryRecursive target -- A failed debootstrap run will leave a debootstrap directory; @@ -109,9 +108,6 @@ built target system@(System _ arch) config = , return False ) -mountPoints :: IO [FilePath] -mountPoints = lines <$> readProcess "findmnt" ["-rn", "--output", "target"] - extractSuite :: System -> Maybe String extractSuite (System (Debian s) _) = Just $ Apt.showSuite s extractSuite (System (Ubuntu r) _) = Just r diff --git a/src/Propellor/Property/Mount.hs b/src/Propellor/Property/Mount.hs new file mode 100644 index 00000000..804407e9 --- /dev/null +++ b/src/Propellor/Property/Mount.hs @@ -0,0 +1,12 @@ +module Propellor.Property.Mount where + +import Propellor +import Utility.SafeCommand + +mountPoints :: IO [FilePath] +mountPoints = lines <$> readProcess "findmnt" ["-rn", "--output", "target"] + +umountLazy :: FilePath -> IO () +umountLazy mnt = + unlessM (boolSystem "umount" [ Param "-l", Param mnt ]) $ + errorMessage $ "failed unmounting " ++ mnt diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 20e6e47f..aa304f61 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -14,6 +14,7 @@ import Propellor import qualified Propellor.Property.Debootstrap as Debootstrap import qualified Propellor.Property.Ssh as Ssh import qualified Propellor.Property.User as User +import Propellor.Property.Mount -- | Replaces whatever OS was installed before with a clean installation -- of the OS that the Host is configured to have. @@ -63,30 +64,42 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ `requires` flipped `requires` + umountall + `requires` osbootstrapped - osbootstrapped = withOS "/new-os bootstrapped" $ \o -> case o of + osbootstrapped = withOS (newOSDir ++ " bootstrapped") $ \o -> case o of (Just d@(System (Debian _) _)) -> debootstrap d (Just u@(System (Ubuntu _) _)) -> debootstrap u _ -> error "os is not declared to be Debian or Ubuntu" debootstrap targetos = ensureProperty $ toProp $ - Debootstrap.built "/new-os" targetos Debootstrap.DefaultConfig + Debootstrap.built newOSDir targetos Debootstrap.DefaultConfig - flipped = property "/new-os moved into place" $ - return FailedChange - -- unmount all mounts - -- move all directories to /old-os, - -- move /new-os to / - -- touch flagfile + umountall = property "all mount points unmounted" $ liftIO $ do + mnts <- filter (/= "/") <$> mountPoints + forM_ mnts umountLazy + return $ if null mnts then NoChange else MadeChange + + flipped = property (newOSDir ++ " moved into place") $ liftIO $ do + createDirectoryIfMissing True oldOSDir + rootcontents <- dirContents "/" + forM_ rootcontents $ \d -> + when (d /= oldOSDir && d /= newOSDir) $ + renameDirectory d (oldOSDir ++ d) + newrootcontents <- dirContents newOSDir + forM_ newrootcontents $ \d -> + renameDirectory d ("/" ++ takeFileName d) + removeDirectory newOSDir + return MadeChange propellorbootstrapped = property "propellor re-debootstrapped in new os" $ - return FailedChange + return NoChange -- re-bootstrap propellor in /usr/local/propellor, -- (using git repo bundle, privdata file, and possibly -- git repo url, which all need to be arranged to -- be present in /old-os's /usr/local/propellor) - finalized = property "clean install finalized" $ do + finalized = property "clean OS installed" $ do liftIO $ writeFile flagfile "" return MadeChange @@ -118,7 +131,7 @@ preserveRootSshAuthorized = check (doesDirectoryExist oldloc) $ ensureProperties (map (Ssh.authorizedKey "root") ks) where newloc = "/root/.ssh/authorized_keys" - oldloc = oldOsDir ++ newloc + oldloc = oldOSDir ++ newloc -- Installs an appropriate kernel from the OS distribution. kernelInstalled :: Property @@ -142,12 +155,15 @@ type GrubDev = String -- Removes the old OS's backup from /old-os oldOSRemoved :: Confirmation -> Property -oldOSRemoved confirmation = check (doesDirectoryExist oldOsDir) $ +oldOSRemoved confirmation = check (doesDirectoryExist oldOSDir) $ go `requires` confirmed "old OS backup removal confirmed" confirmation where go = property "old OS backup removed" $ do - liftIO $ removeDirectoryRecursive oldOsDir + liftIO $ removeDirectoryRecursive oldOSDir return MadeChange -oldOsDir :: FilePath -oldOsDir = "/old-os" +oldOSDir :: FilePath +oldOSDir = "/old-os" + +newOSDir :: FilePath +newOSDir = "/new-os" -- cgit v1.3-2-g0d8e From 31cf8e0c24ff25a14ce750bbb1491436c55f07e0 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 16:54:04 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index aa304f61..2888800e 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -75,8 +75,8 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ debootstrap targetos = ensureProperty $ toProp $ Debootstrap.built newOSDir targetos Debootstrap.DefaultConfig - umountall = property "all mount points unmounted" $ liftIO $ do - mnts <- filter (/= "/") <$> mountPoints + umountall = property "mount points unmounted" $ liftIO $ do + mnts <- filter (`notElem` ["/", "/proc"]) <$> mountPoints forM_ mnts umountLazy return $ if null mnts then NoChange else MadeChange @@ -84,11 +84,13 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ createDirectoryIfMissing True oldOSDir rootcontents <- dirContents "/" forM_ rootcontents $ \d -> - when (d /= oldOSDir && d /= newOSDir) $ + when (d `notElem` [oldOSDir, newOSDir, "/proc"]) $ renameDirectory d (oldOSDir ++ d) newrootcontents <- dirContents newOSDir - forM_ newrootcontents $ \d -> - renameDirectory d ("/" ++ takeFileName d) + forM_ newrootcontents $ \d -> do + let dest = "/" ++ takeFileName d + whenM (not <$> doesDirectoryExist dest) $ + renameDirectory d dest removeDirectory newOSDir return MadeChange -- cgit v1.3-2-g0d8e From 61766ff027ffaad3e816c7cda20284f87e16478b Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 16:55:33 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 2888800e..8b221b95 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -77,7 +77,8 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ umountall = property "mount points unmounted" $ liftIO $ do mnts <- filter (`notElem` ["/", "/proc"]) <$> mountPoints - forM_ mnts umountLazy + -- reverse so that deeper mount points come first + forM_ (reverse mnts) umountLazy return $ if null mnts then NoChange else MadeChange flipped = property (newOSDir ++ " moved into place") $ liftIO $ do -- cgit v1.3-2-g0d8e From c57a42388366f4e0bb6195ec4d957f8bc8ebad6e Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 17:01:43 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 8b221b95..603e4593 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -76,7 +76,7 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ Debootstrap.built newOSDir targetos Debootstrap.DefaultConfig umountall = property "mount points unmounted" $ liftIO $ do - mnts <- filter (`notElem` ["/", "/proc"]) <$> mountPoints + mnts <- filter (`notElem` ("/": trickydirs)) <$> mountPoints -- reverse so that deeper mount points come first forM_ (reverse mnts) umountLazy return $ if null mnts then NoChange else MadeChange @@ -85,7 +85,7 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ createDirectoryIfMissing True oldOSDir rootcontents <- dirContents "/" forM_ rootcontents $ \d -> - when (d `notElem` [oldOSDir, newOSDir, "/proc"]) $ + when (d `notElem` (oldOSDir:newOSDir:trickydirs)) $ renameDirectory d (oldOSDir ++ d) newrootcontents <- dirContents newOSDir forM_ newrootcontents $ \d -> do @@ -95,6 +95,14 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ removeDirectory newOSDir return MadeChange + trickydirs = + -- /tmp can contain X's sockets, which prevent moving it + -- so it's left as-is. + [ "/tmp" + -- /proc is left mounted + , "/proc" + ] + propellorbootstrapped = property "propellor re-debootstrapped in new os" $ return NoChange -- re-bootstrap propellor in /usr/local/propellor, -- cgit v1.3-2-g0d8e From 014af4a5b4a21185e16f7564be00859b3bc4c060 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 17:05:03 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 603e4593..14e5d85a 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -16,6 +16,8 @@ import qualified Propellor.Property.Ssh as Ssh import qualified Propellor.Property.User as User import Propellor.Property.Mount +import System.Posix.Files (rename, fileExist) + -- | Replaces whatever OS was installed before with a clean installation -- of the OS that the Host is configured to have. -- @@ -86,12 +88,12 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ rootcontents <- dirContents "/" forM_ rootcontents $ \d -> when (d `notElem` (oldOSDir:newOSDir:trickydirs)) $ - renameDirectory d (oldOSDir ++ d) + rename d (oldOSDir ++ d) newrootcontents <- dirContents newOSDir forM_ newrootcontents $ \d -> do let dest = "/" ++ takeFileName d - whenM (not <$> doesDirectoryExist dest) $ - renameDirectory d dest + whenM (not <$> fileExist dest) $ + rename d dest removeDirectory newOSDir return MadeChange -- cgit v1.3-2-g0d8e From cdf763b8b8deeb9c7e572afe268869e441ef30aa Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 17:11:08 -0400 Subject: propellor spin --- src/Propellor/Property/Chroot/Util.hs | 7 ++++--- src/Propellor/Property/OS.hs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Propellor/Property/Chroot/Util.hs b/src/Propellor/Property/Chroot/Util.hs index feb71d01..382fbab7 100644 --- a/src/Propellor/Property/Chroot/Util.hs +++ b/src/Propellor/Property/Chroot/Util.hs @@ -9,7 +9,8 @@ import Control.Applicative standardPathEnv :: IO [(String, String)] standardPathEnv = do path <- getEnvDefault "PATH" "/bin" - addEntry "PATH" (path ++ std) + addEntry "PATH" (path ++ stdPATH) <$> getEnvironment - where - std = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +stdPATH :: String +stdPATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 14e5d85a..cbdb4d99 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -94,7 +94,7 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ let dest = "/" ++ takeFileName d whenM (not <$> fileExist dest) $ rename d dest - removeDirectory newOSDir + removeDirectoryRecursive newOSDir return MadeChange trickydirs = -- cgit v1.3-2-g0d8e From f1fd75c9ecee5f398a25488c73a541d4135887da Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 17:11:15 -0400 Subject: more work on OS takeover --- config-joey.hs | 10 +-- propellor.cabal | 1 + src/Propellor/Property/Chroot/Util.hs | 7 +- src/Propellor/Property/Debootstrap.hs | 8 +- src/Propellor/Property/OS.hs | 138 ++++++++++++++++++++++++---------- src/Propellor/Property/User.hs | 12 +++ 6 files changed, 122 insertions(+), 54 deletions(-) diff --git a/config-joey.hs b/config-joey.hs index b617ccfa..a1ea21d1 100644 --- a/config-joey.hs +++ b/config-joey.hs @@ -54,11 +54,11 @@ hosts = -- (o) ` testvm :: Host testvm = host "testvm.kitenet.net" - & Chroot.provisioned (Chroot.debootstrapped (System (Debian Unstable) "amd64") Debootstrap.DefaultConfig "/new-os") - -- & OS.cleanInstall (OS.Confirmed "foo.example.com") [] - -- `onChange` propertyList "fixing up after clean install" - -- [ - -- ] + & os (System (Debian Unstable) "amd64") + & OS.cleanInstallOnce (OS.Confirmed "testvm.kitenet.net") + `onChange` propertyList "fixing up after clean install" + [ OS.preserveRootSshAuthorized + ] darkstar :: Host darkstar = host "darkstar.kitenet.net" diff --git a/propellor.cabal b/propellor.cabal index 617a1fc8..91d08bd5 100644 --- a/propellor.cabal +++ b/propellor.cabal @@ -85,6 +85,7 @@ Library Propellor.Property.Gpg Propellor.Property.Group Propellor.Property.Grub + Propellor.Property.Mount Propellor.Property.Network Propellor.Property.Nginx Propellor.Property.Obnam diff --git a/src/Propellor/Property/Chroot/Util.hs b/src/Propellor/Property/Chroot/Util.hs index feb71d01..382fbab7 100644 --- a/src/Propellor/Property/Chroot/Util.hs +++ b/src/Propellor/Property/Chroot/Util.hs @@ -9,7 +9,8 @@ import Control.Applicative standardPathEnv :: IO [(String, String)] standardPathEnv = do path <- getEnvDefault "PATH" "/bin" - addEntry "PATH" (path ++ std) + addEntry "PATH" (path ++ stdPATH) <$> getEnvironment - where - std = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +stdPATH :: String +stdPATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" diff --git a/src/Propellor/Property/Debootstrap.hs b/src/Propellor/Property/Debootstrap.hs index ab5bddf4..35d9e472 100644 --- a/src/Propellor/Property/Debootstrap.hs +++ b/src/Propellor/Property/Debootstrap.hs @@ -9,6 +9,7 @@ module Propellor.Property.Debootstrap ( import Propellor import qualified Propellor.Property.Apt as Apt import Propellor.Property.Chroot.Util +import Propellor.Property.Mount import Utility.Path import Utility.SafeCommand import Utility.FileMode @@ -95,9 +96,7 @@ built target system@(System _ arch) config = submnts <- filter (\p -> simplifyPath p /= simplifyPath target) . filter (dirContains target) <$> mountPoints - forM_ submnts $ \mnt -> - unlessM (boolSystem "umount" [ Param "-l", Param mnt ]) $ do - errorMessage $ "failed unmounting " ++ mnt + forM_ submnts umountLazy removeDirectoryRecursive target -- A failed debootstrap run will leave a debootstrap directory; @@ -109,9 +108,6 @@ built target system@(System _ arch) config = , return False ) -mountPoints :: IO [FilePath] -mountPoints = lines <$> readProcess "findmnt" ["-rn", "--output", "target"] - extractSuite :: System -> Maybe String extractSuite (System (Debian s) _) = Just $ Apt.showSuite s extractSuite (System (Ubuntu r) _) = Just r diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 5dddff2c..cbdb4d99 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -1,20 +1,22 @@ module Propellor.Property.OS ( cleanInstallOnce, - Confirmed(..), + Confirmation(..), preserveNetworkInterfaces, preserveRootSshAuthorized, grubBoots, GrubDev(..), + oldOSKernelPreserved, kernelInstalled, oldOSRemoved, ) where import Propellor -import qualified Propellor.Property.Chroot as Chroot import qualified Propellor.Property.Debootstrap as Debootstrap -import qualified Propellor.Property.File as File import qualified Propellor.Property.Ssh as Ssh -import Utility.FileMode +import qualified Propellor.Property.User as User +import Propellor.Property.Mount + +import System.Posix.Files (rename, fileExist) -- | Replaces whatever OS was installed before with a clean installation -- of the OS that the Host is configured to have. @@ -22,8 +24,10 @@ import Utility.FileMode -- This can replace one Linux distribution with different one. -- But, it can also fail and leave the system in an unbootable state. -- +-- The files from the old os will be left in /old-os +-- -- To avoid this property being accidentially used, you have to provide --- a Confirmed containing the name of the host that you intend to apply the +-- a Confirmation containing the name of the host that you intend to apply the -- property to. -- -- This property only runs once. The cleanly installed system will have @@ -35,52 +39,95 @@ import Utility.FileMode -- working system. For example: -- -- > & os (System (Debian Unstable) "amd64") --- > & cleanInstall (Confirmed "foo.example.com") [BackupOldOS, UseOldKernel] +-- > & cleanInstall (Confirmed "foo.example.com") -- > `onChange` propertyList "fixing up after clean install" -- > [ preserveNetworkInterfaces -- > , preserverRootSshAuthorized +-- > , oldOSKernelPreserved -- > -- , kernelInstalled -- > -- , grubBoots "hd0" +-- > -- , oldOsRemoved -- > ] -- > & Apt.installed ["ssh"] -- > & User.hasSomePassword "root" -- > & User.accountFor "joey" -- > & User.hasSomePassword "joey" -- > -- rest of system properties here -cleanInstallOnce :: Confirmed -> [Tweak] -> Property -cleanInstallOnce confirmed tweaks = check (not <$> doesFileExist flagfile) $ - property "OS cleanly installed" $ do - checkConfirmed confirmed - error "TODO" - -- debootstrap /new-os chroot, but don't run propellor - -- inside the chroot. - -- unmount all mounts - -- move all directories to /old-os, - -- except for /boot and /lib/modules when UseOldKernel - -- (or, delete when not BackupOldOS) - -- move /new-os to / - -- touch flagfile +cleanInstallOnce :: Confirmation -> Property +cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ + go `requires` confirmed "clean install confirmed" confirmation + where + go = + finalized + `requires` + propellorbootstrapped + `requires` + User.shadowConfig True + `requires` + flipped + `requires` + umountall + `requires` + osbootstrapped + + osbootstrapped = withOS (newOSDir ++ " bootstrapped") $ \o -> case o of + (Just d@(System (Debian _) _)) -> debootstrap d + (Just u@(System (Ubuntu _) _)) -> debootstrap u + _ -> error "os is not declared to be Debian or Ubuntu" + debootstrap targetos = ensureProperty $ toProp $ + Debootstrap.built newOSDir targetos Debootstrap.DefaultConfig + + umountall = property "mount points unmounted" $ liftIO $ do + mnts <- filter (`notElem` ("/": trickydirs)) <$> mountPoints + -- reverse so that deeper mount points come first + forM_ (reverse mnts) umountLazy + return $ if null mnts then NoChange else MadeChange + + flipped = property (newOSDir ++ " moved into place") $ liftIO $ do + createDirectoryIfMissing True oldOSDir + rootcontents <- dirContents "/" + forM_ rootcontents $ \d -> + when (d `notElem` (oldOSDir:newOSDir:trickydirs)) $ + rename d (oldOSDir ++ d) + newrootcontents <- dirContents newOSDir + forM_ newrootcontents $ \d -> do + let dest = "/" ++ takeFileName d + whenM (not <$> fileExist dest) $ + rename d dest + removeDirectoryRecursive newOSDir + return MadeChange + + trickydirs = + -- /tmp can contain X's sockets, which prevent moving it + -- so it's left as-is. + [ "/tmp" + -- /proc is left mounted + , "/proc" + ] + + propellorbootstrapped = property "propellor re-debootstrapped in new os" $ + return NoChange -- re-bootstrap propellor in /usr/local/propellor, -- (using git repo bundle, privdata file, and possibly -- git repo url, which all need to be arranged to -- be present in /old-os's /usr/local/propellor) - -- enable shadow passwords (to avoid foot-shooting) - -- return MadeChange - where + + finalized = property "clean OS installed" $ do + liftIO $ writeFile flagfile "" + return MadeChange + flagfile = "/etc/propellor-cleaninstall" -data Confirmed = Confirmed HostName +data Confirmation = Confirmed HostName -checkConfirmed :: Confirmed -> Propellor () -checkConfirmed (Confirmed c) = do +confirmed :: Desc -> Confirmation -> Property +confirmed desc (Confirmed c) = property desc $ do hostname <- asks hostName - when (hostname /= c) $ - errorMessage "Run with a bad confirmation, not matching hostname." - --- | Sometimes you want an almost clean install, but with some tweaks. -data Tweak - = UseOldKernel -- ^ Leave /boot and /lib/modules from old OS, so the system can boot using them as before - | BackupOldOS -- ^ Back up old OS to /old-os, to avoid losing any important files + if hostname /= c + then do + warningMessage "Run with a bad confirmation, not matching hostname." + return FailedChange + else return NoChange -- /etc/network/interfaces is configured to bring up all interfaces that -- are currently up, using the same IP addresses. @@ -97,12 +144,19 @@ preserveRootSshAuthorized = check (doesDirectoryExist oldloc) $ ensureProperties (map (Ssh.authorizedKey "root") ks) where newloc = "/root/.ssh/authorized_keys" - oldloc = oldOsDir ++ newloc + oldloc = oldOSDir ++ newloc -- Installs an appropriate kernel from the OS distribution. kernelInstalled :: Property kernelInstalled = undefined +-- Copies kernel images, initrds, and modules from /old-os +-- into the new system. +-- +-- TODO: grub config? +oldOSKernelPreserved :: Property +oldOSKernelPreserved = undefined + -- Installs grub onto a device to boot the system. -- -- You may want to install grub to multiple devices; eg for a system @@ -113,12 +167,16 @@ grubBoots = undefined type GrubDev = String -- Removes the old OS's backup from /old-os -oldOSRemoved :: Confirmed -> Property -oldOSRemoved confirmed = check (doesDirectoryExist oldOsDir) $ - property "old OS backup removed" $ do - checkConfirmed confirmed - liftIO $ removeDirectoryRecursive oldOsDir +oldOSRemoved :: Confirmation -> Property +oldOSRemoved confirmation = check (doesDirectoryExist oldOSDir) $ + go `requires` confirmed "old OS backup removal confirmed" confirmation + where + go = property "old OS backup removed" $ do + liftIO $ removeDirectoryRecursive oldOSDir return MadeChange -oldOsDir :: FilePath -oldOsDir = "/old-os" +oldOSDir :: FilePath +oldOSDir = "/old-os" + +newOSDir :: FilePath +newOSDir = "/new-os" diff --git a/src/Propellor/Property/User.hs b/src/Propellor/Property/User.hs index 434a92a3..ccb69b24 100644 --- a/src/Propellor/Property/User.hs +++ b/src/Propellor/Property/User.hs @@ -84,3 +84,15 @@ hasGroup user group' = check test $ cmdProperty "adduser" `describe` unwords ["user", user, "in group", group'] where test = not . elem group' . words <$> readProcess "groups" [user] + +-- | Controls whether shadow passwords are enabled or not. +shadowConfig :: Bool -> Property +shadowConfig True = check (not <$> shadowExists) $ + cmdProperty "shadowconfig" ["on"] + `describe` "shadow passwords enabled" +shadowConfig False = check shadowExists $ + cmdProperty "shadowconfig" ["off"] + `describe` "shadow passwords disabled" + +shadowExists :: IO Bool +shadowExists = doesFileExist "/etc/shadow" -- cgit v1.3-2-g0d8e From 4be893f0bf26990b65828841f00062d2acfff5f2 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 17:30:40 -0400 Subject: more improvements to takeover --- src/Propellor/Property/OS.hs | 70 ++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index cbdb4d99..b81b7c4e 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -4,8 +4,8 @@ module Propellor.Property.OS ( preserveNetworkInterfaces, preserveRootSshAuthorized, grubBoots, - GrubDev(..), - oldOSKernelPreserved, + GrubDev, + rebootForced, kernelInstalled, oldOSRemoved, ) where @@ -15,6 +15,7 @@ import qualified Propellor.Property.Debootstrap as Debootstrap import qualified Propellor.Property.Ssh as Ssh import qualified Propellor.Property.User as User import Propellor.Property.Mount +import Propellor.Property.Chroot.Util (stdPATH) import System.Posix.Files (rename, fileExist) @@ -24,29 +25,29 @@ import System.Posix.Files (rename, fileExist) -- This can replace one Linux distribution with different one. -- But, it can also fail and leave the system in an unbootable state. -- --- The files from the old os will be left in /old-os --- -- To avoid this property being accidentially used, you have to provide --- a Confirmation containing the name of the host that you intend to apply the --- property to. +-- a Confirmation containing the name of the host that you intend to apply +-- the property to. -- -- This property only runs once. The cleanly installed system will have -- a file /etc/propellor-cleaninstall, which indicates it was cleanly -- installed. +-- +-- The files from the old os will be left in /old-os -- -- You will typically want to run some more properties after the clean --- install, to bootstrap from the cleanly installed system to a fully --- working system. For example: +-- install succeeds, to bootstrap from the cleanly installed system to +-- a fully working system. For example: -- -- > & os (System (Debian Unstable) "amd64") -- > & cleanInstall (Confirmed "foo.example.com") -- > `onChange` propertyList "fixing up after clean install" -- > [ preserveNetworkInterfaces -- > , preserverRootSshAuthorized --- > , oldOSKernelPreserved -- > -- , kernelInstalled -- > -- , grubBoots "hd0" --- > -- , oldOsRemoved +-- > -- , oldOsRemoved (Confirmed "foo.example.com") +-- > -- , rebootForced -- > ] -- > & Apt.installed ["ssh"] -- > & User.hasSomePassword "root" @@ -95,15 +96,12 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ whenM (not <$> fileExist dest) $ rename d dest removeDirectoryRecursive newOSDir + + -- Prepare environment for running additional properties. + liftIO $ writeFile flagfile "" + void $ setEnv "PATH" stdPATH True + return MadeChange - - trickydirs = - -- /tmp can contain X's sockets, which prevent moving it - -- so it's left as-is. - [ "/tmp" - -- /proc is left mounted - , "/proc" - ] propellorbootstrapped = property "propellor re-debootstrapped in new os" $ return NoChange @@ -111,12 +109,21 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ -- (using git repo bundle, privdata file, and possibly -- git repo url, which all need to be arranged to -- be present in /old-os's /usr/local/propellor) + -- TODO - finalized = property "clean OS installed" $ do - liftIO $ writeFile flagfile "" - return MadeChange + -- Ensure that MadeChange is returned by the overall property, + -- so that anything hooking in onChange will run afterwards. + finalized = property "clean OS installed" $ return MadeChange flagfile = "/etc/propellor-cleaninstall" + + trickydirs = + -- /tmp can contain X's sockets, which prevent moving it + -- so it's left as-is. + [ "/tmp" + -- /proc is left mounted + , "/proc" + ] data Confirmation = Confirmed HostName @@ -129,12 +136,12 @@ confirmed desc (Confirmed c) = property desc $ do return FailedChange else return NoChange --- /etc/network/interfaces is configured to bring up all interfaces that +-- | /etc/network/interfaces is configured to bring up all interfaces that -- are currently up, using the same IP addresses. preserveNetworkInterfaces :: Property preserveNetworkInterfaces = undefined --- Root's .ssh/authorized_keys has added to it any ssh keys that +-- | Root's .ssh/authorized_keys has added to it any ssh keys that -- were authorized in the old OS. Any other contents of the file are -- retained. preserveRootSshAuthorized :: Property @@ -146,18 +153,11 @@ preserveRootSshAuthorized = check (doesDirectoryExist oldloc) $ newloc = "/root/.ssh/authorized_keys" oldloc = oldOSDir ++ newloc --- Installs an appropriate kernel from the OS distribution. +-- | Installs an appropriate kernel from the OS distribution. kernelInstalled :: Property kernelInstalled = undefined --- Copies kernel images, initrds, and modules from /old-os --- into the new system. --- --- TODO: grub config? -oldOSKernelPreserved :: Property -oldOSKernelPreserved = undefined - --- Installs grub onto a device to boot the system. +-- | Installs grub onto a device to boot the system. -- -- You may want to install grub to multiple devices; eg for a system -- that uses software RAID. @@ -166,6 +166,12 @@ grubBoots = undefined type GrubDev = String +-- | Forces an immediate reboot, without contacting the init system. +-- +-- Can be used after cleanInstallOnce. +rebootForced :: Property +rebootForced = cmdProperty "reboot" [ "--force" ] + -- Removes the old OS's backup from /old-os oldOSRemoved :: Confirmation -> Property oldOSRemoved confirmation = check (doesDirectoryExist oldOSDir) $ -- cgit v1.3-2-g0d8e From 573b8b7df866d4801c1ea06edf1195fbeeef9499 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 4 Dec 2014 17:40:26 -0400 Subject: changelog --- debian/changelog | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/debian/changelog b/debian/changelog index aa191650..079a737c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,27 +1,30 @@ propellor (1.1.0) UNRELEASED; urgency=medium - * propellor --spin can now deploy propellor to hosts that do not have - git, ghc, or apt-get. This is accomplished by uploading a fairly - portable precompiled tarball of propellor. * --spin target --via relay causes propellor to bounce through an intermediate relay host, which handles any necessary uploads when provisioning the target host. * --spin can be passed multiple hosts, and it will provision each host in turn. + * Add --merge, to combine multiple --spin commits into a single, more useful + commit. * Hostname parameters not containing dots are looked up in the DNS to find the full hostname. + * propellor --spin can now deploy propellor to hosts that do not have + git, ghc, or apt-get. This is accomplished by uploading a fairly + portable precompiled tarball of propellor. + * Propellor.Property.OS contains properties that can be used to do a clean + reinstall of the OS of an existing host. This can be used, for example, + to do an in-place conversion from Fedora to Debian. Use with caution! * Added group-related properties. Thanks, Félix Sipma. * Added Git.barerepo. Thanks, Félix Sipma. * hasSomePassword and hasPassword now default to using the name of the host as the Context for the password. To specify a different context, use hasSomePassword' and hasPassword' (API change) - * Add --merge, to combine multiple --spin commits into a single, more useful - commit. * cron.runPropellor now runs propellor, rather than using its Makefile. This is more robust. * propellor.debug can be set in the git config to enable more persistent debugging output. - * Run apt-cache policy with LANG=C. + * Run apt-cache policy with LANG=C so it works on other locales. -- Joey Hess Sat, 22 Nov 2014 00:12:35 -0400 -- cgit v1.3-2-g0d8e From 97e9433f1b719cc13fc524ee0399d0b51af5a5c1 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Fri, 5 Dec 2014 12:50:01 -0400 Subject: rollback if renameing fails This avoids leaving the system in a broken state where some directories have been renamed away any others not. Future work: If the rename list contains (foo, bar) and (newfoo,foo), reorder the list to gather those two actions together to minimize the amount of time that foo is missing. In case of power loss or something. --- src/Propellor/Property/OS.hs | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index b81b7c4e..3ed23fb4 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -18,6 +18,7 @@ import Propellor.Property.Mount import Propellor.Property.Chroot.Util (stdPATH) import System.Posix.Files (rename, fileExist) +import Control.Exception (throw) -- | Replaces whatever OS was installed before with a clean installation -- of the OS that the Host is configured to have. @@ -85,16 +86,20 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ return $ if null mnts then NoChange else MadeChange flipped = property (newOSDir ++ " moved into place") $ liftIO $ do - createDirectoryIfMissing True oldOSDir rootcontents <- dirContents "/" - forM_ rootcontents $ \d -> - when (d `notElem` (oldOSDir:newOSDir:trickydirs)) $ - rename d (oldOSDir ++ d) newrootcontents <- dirContents newOSDir - forM_ newrootcontents $ \d -> do + createDirectoryIfMissing True oldOSDir + renamesout <- forM rootcontents $ \d -> + if d `notElem` (oldOSDir:newOSDir:trickydirs) + then return $ Just (d, oldOSDir ++ d) + else return Nothing + renamesin <- forM newrootcontents $ \d -> do let dest = "/" ++ takeFileName d - whenM (not <$> fileExist dest) $ - rename d dest + ifM (not <$> fileExist dest) + ( return $ Just (d, dest) + , return Nothing + ) + massRename $ catMaybes (renamesout ++ renamesin) removeDirectoryRecursive newOSDir -- Prepare environment for running additional properties. @@ -125,6 +130,22 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ , "/proc" ] +-- Performs all the renames. If any rename fails, rolls back all +-- previous renames. Thus, this either successfully performs all +-- the renames, or does not change the system state at all. +massRename :: [(FilePath, FilePath)] -> IO () +massRename = go [] + where + go _ [] = return () + go undo ((from, to):rest) = + tryNonAsync (rename from to) + >>= either + (rollback undo) + (const $ go ((to, from):undo) rest) + rollback undo e = do + mapM_ (uncurry rename) undo + throw e + data Confirmation = Confirmed HostName confirmed :: Desc -> Confirmation -> Property -- cgit v1.3-2-g0d8e -- cgit v1.3-2-g0d8e -- cgit v1.3-2-g0d8e From c24bdec62054983d0ae2621cb50e03a3432c7592 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Fri, 5 Dec 2014 14:06:48 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 3ed23fb4..bc575512 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -137,7 +137,8 @@ massRename :: [(FilePath, FilePath)] -> IO () massRename = go [] where go _ [] = return () - go undo ((from, to):rest) = + go undo ((from, to):rest) = do + warningMessage $ show ("rename", from, to) tryNonAsync (rename from to) >>= either (rollback undo) -- cgit v1.3-2-g0d8e From 46bf569cf9d161c0e4a6d8ac244db32f0de06cfb Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Fri, 5 Dec 2014 14:14:02 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index bc575512..d0d470ed 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -86,20 +86,13 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ return $ if null mnts then NoChange else MadeChange flipped = property (newOSDir ++ " moved into place") $ liftIO $ do - rootcontents <- dirContents "/" - newrootcontents <- dirContents newOSDir + renamesout <- map (\d -> (d, oldOSDir ++ d, pure $ d `notElem` (oldOSDir:newOSDir:trickydirs))) + <$> dirContents "/" + renamesin <- map (\d -> let dest = "/" ++ takeFileName d in (d, dest, not <$> fileExist dest)) + <$> dirContents newOSDir + createDirectoryIfMissing True oldOSDir - renamesout <- forM rootcontents $ \d -> - if d `notElem` (oldOSDir:newOSDir:trickydirs) - then return $ Just (d, oldOSDir ++ d) - else return Nothing - renamesin <- forM newrootcontents $ \d -> do - let dest = "/" ++ takeFileName d - ifM (not <$> fileExist dest) - ( return $ Just (d, dest) - , return Nothing - ) - massRename $ catMaybes (renamesout ++ renamesin) + massRename (renamesout ++ renamesin) removeDirectoryRecursive newOSDir -- Prepare environment for running additional properties. @@ -133,16 +126,17 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ -- Performs all the renames. If any rename fails, rolls back all -- previous renames. Thus, this either successfully performs all -- the renames, or does not change the system state at all. -massRename :: [(FilePath, FilePath)] -> IO () +massRename :: [(FilePath, FilePath, IO Bool)] -> IO () massRename = go [] where go _ [] = return () - go undo ((from, to):rest) = do - warningMessage $ show ("rename", from, to) - tryNonAsync (rename from to) + go undo ((from, to, test):rest) = ifM test + ( tryNonAsync (rename from to) >>= either (rollback undo) (const $ go ((to, from):undo) rest) + , go undo rest + ) rollback undo e = do mapM_ (uncurry rename) undo throw e -- cgit v1.3-2-g0d8e From a2ee8e20daf4fee456a3365c9b322abde53defa9 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Fri, 5 Dec 2014 14:32:45 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index d0d470ed..2ebf04cb 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -16,6 +16,7 @@ import qualified Propellor.Property.Ssh as Ssh import qualified Propellor.Property.User as User import Propellor.Property.Mount import Propellor.Property.Chroot.Util (stdPATH) +import Utility.SafeCommand import System.Posix.Files (rename, fileExist) import Control.Exception (throw) @@ -68,8 +69,6 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ `requires` flipped `requires` - umountall - `requires` osbootstrapped osbootstrapped = withOS (newOSDir ++ " bootstrapped") $ \o -> case o of @@ -79,26 +78,32 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ debootstrap targetos = ensureProperty $ toProp $ Debootstrap.built newOSDir targetos Debootstrap.DefaultConfig - umountall = property "mount points unmounted" $ liftIO $ do + flipped = property (newOSDir ++ " moved into place") $ liftIO $ do + -- First, unmount most mount points, lazily, so + -- they don't interfere with moving things around. + devfstype <- fromMaybe "devtmpfs" <$> getFsType "/dev" mnts <- filter (`notElem` ("/": trickydirs)) <$> mountPoints -- reverse so that deeper mount points come first forM_ (reverse mnts) umountLazy - return $ if null mnts then NoChange else MadeChange - flipped = property (newOSDir ++ " moved into place") $ liftIO $ do renamesout <- map (\d -> (d, oldOSDir ++ d, pure $ d `notElem` (oldOSDir:newOSDir:trickydirs))) <$> dirContents "/" renamesin <- map (\d -> let dest = "/" ++ takeFileName d in (d, dest, not <$> fileExist dest)) <$> dirContents newOSDir - createDirectoryIfMissing True oldOSDir massRename (renamesout ++ renamesin) removeDirectoryRecursive newOSDir - + -- Prepare environment for running additional properties. - liftIO $ writeFile flagfile "" void $ setEnv "PATH" stdPATH True + -- Remount /dev, so that block devices etc are + -- available for other properties to use. + unlessM (mount devfstype devfstype "/dev") $ do + warningMessage $ "failed mounting /dev using " ++ devfstype ++ "; falling back to MAKEDEV generic" + void $ boolSystem "sh" [Param "-c", Param "cd /dev && /sbin/MAKEDEV generic"] + + liftIO $ writeFile flagfile "" return MadeChange propellorbootstrapped = property "propellor re-debootstrapped in new os" $ -- cgit v1.3-2-g0d8e From ad822196aa268c09f3022fe856cc30ec69f3c4d1 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Fri, 5 Dec 2014 14:40:08 -0400 Subject: propellor spin --- src/Propellor/Property/OS.hs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 2ebf04cb..144b17f6 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -37,19 +37,21 @@ import Control.Exception (throw) -- -- The files from the old os will be left in /old-os -- +-- TODO: A forced reboot should be schedued to run after propellor finishes +-- ensuring all properties of the host. +-- -- You will typically want to run some more properties after the clean -- install succeeds, to bootstrap from the cleanly installed system to -- a fully working system. For example: -- -- > & os (System (Debian Unstable) "amd64") --- > & cleanInstall (Confirmed "foo.example.com") +-- > & cleanInstallOnce (Confirmed "foo.example.com") -- > `onChange` propertyList "fixing up after clean install" -- > [ preserveNetworkInterfaces -- > , preserverRootSshAuthorized -- > -- , kernelInstalled -- > -- , grubBoots "hd0" -- > -- , oldOsRemoved (Confirmed "foo.example.com") --- > -- , rebootForced -- > ] -- > & Apt.installed ["ssh"] -- > & User.hasSomePassword "root" @@ -166,7 +168,7 @@ preserveNetworkInterfaces = undefined -- were authorized in the old OS. Any other contents of the file are -- retained. preserveRootSshAuthorized :: Property -preserveRootSshAuthorized = check (doesDirectoryExist oldloc) $ +preserveRootSshAuthorized = check (fileExist oldloc) $ property (newloc ++ " copied from old OS") $ do ks <- liftIO $ lines <$> readFile oldloc ensureProperties (map (Ssh.authorizedKey "root") ks) -- cgit v1.3-2-g0d8e From ef8c1d22cd6f4957f9d169338899d5282698bc25 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Fri, 5 Dec 2014 15:30:34 -0400 Subject: propellor spin --- config-joey.hs | 10 +++++++- debian/changelog | 1 + src/Propellor/Property/Grub.hs | 42 ++++++++++++++++++++++++++++++-- src/Propellor/Property/OS.hs | 54 ++++++++++++++++++++++++------------------ 4 files changed, 81 insertions(+), 26 deletions(-) diff --git a/config-joey.hs b/config-joey.hs index a1ea21d1..0e46cb56 100644 --- a/config-joey.hs +++ b/config-joey.hs @@ -57,8 +57,16 @@ testvm = host "testvm.kitenet.net" & os (System (Debian Unstable) "amd64") & OS.cleanInstallOnce (OS.Confirmed "testvm.kitenet.net") `onChange` propertyList "fixing up after clean install" - [ OS.preserveRootSshAuthorized + [ User.shadowConfig True + , OS.preserveRootSshAuthorized + , OS.preserveResolvConf + , Grub.boots "/dev/sda" + `requires` Grub.installed Grub.PC ] + & Hostname.sane + & Hostname.searchDomain + & Apt.installed ["linux-image-amd64"] + & Apt.installed ["ssh"] darkstar :: Host darkstar = host "darkstar.kitenet.net" diff --git a/debian/changelog b/debian/changelog index 079a737c..7ee1198b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -17,6 +17,7 @@ propellor (1.1.0) UNRELEASED; urgency=medium to do an in-place conversion from Fedora to Debian. Use with caution! * Added group-related properties. Thanks, Félix Sipma. * Added Git.barerepo. Thanks, Félix Sipma. + * Added Grub.installed and Grub.boots properties. * hasSomePassword and hasPassword now default to using the name of the host as the Context for the password. To specify a different context, use hasSomePassword' and hasPassword' (API change) diff --git a/src/Propellor/Property/Grub.hs b/src/Propellor/Property/Grub.hs index 841861f4..00592d0b 100644 --- a/src/Propellor/Property/Grub.hs +++ b/src/Propellor/Property/Grub.hs @@ -7,8 +7,46 @@ import qualified Propellor.Property.Apt as Apt -- | Eg, hd0,0 or xen/xvda1 type GrubDevice = String +-- | Eg, /dev/sda +type OSDevice = String + type TimeoutSecs = Int +-- | Types of machines that grub can boot. +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, so that the grub boot menu is +-- created. It will be automatically updated when kernel packages are +-- installed. +installed :: BIOS -> Property +installed bios = + Apt.installed [pkg] `describe` "grub package installed" + `before` + cmdProperty "update-grub" [] + where + pkg = case bios of + PC -> "grub-pc" + EFI64 -> "grub-efi-amd64" + EFI32 -> "grub-efi-ia32" + Coreboot -> "grub-coreboot" + Xen -> "grub-xen" + +-- | Installs grub onto a device, so the system can boot from that device. +-- +-- You may want to install grub to multiple devices; eg for a system +-- that uses software RAID. +-- +-- Note that this property does not check if grub is already installed +-- on the device; it always does the work to reinstall it. It's a good idea +-- to arrange for this property to only run once, by eg making it be run +-- onChange after OS.cleanInstallOnce. +boots :: OSDevice -> Property +boots dev = cmdProperty "grub-install" [dev] + `describe` ("grub boots " ++ dev) + -- | Use PV-grub chaining to boot -- -- Useful when the VPS's pv-grub is too old to boot a modern kernel image. @@ -31,8 +69,8 @@ chainPVGrub rootdev bootdev timeout = combineProperties desc ] , "/boot/load.cf" `File.hasContent` [ "configfile (" ++ bootdev ++ ")/boot/grub/grub.cfg" ] - , Apt.installed ["grub-xen"] - , flagFile (scriptProperty ["update-grub; grub-mkimage --prefix '(" ++ bootdev ++ ")/boot/grub' -c /boot/load.cf -O x86_64-xen /usr/lib/grub/x86_64-xen/*.mod > /boot/xen-shim"]) "/boot/xen-shim" + , installed Xen + , flagFile (scriptProperty ["grub-mkimage --prefix '(" ++ bootdev ++ ")/boot/grub' -c /boot/load.cf -O x86_64-xen /usr/lib/grub/x86_64-xen/*.mod > /boot/xen-shim"]) "/boot/xen-shim" `describe` "/boot-xen-shim" ] where diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 144b17f6..30f8c4bb 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -2,18 +2,16 @@ module Propellor.Property.OS ( cleanInstallOnce, Confirmation(..), preserveNetworkInterfaces, + preserveResolvConf, preserveRootSshAuthorized, - grubBoots, - GrubDev, rebootForced, - kernelInstalled, oldOSRemoved, ) where import Propellor import qualified Propellor.Property.Debootstrap as Debootstrap import qualified Propellor.Property.Ssh as Ssh -import qualified Propellor.Property.User as User +import qualified Propellor.Property.File as File import Propellor.Property.Mount import Propellor.Property.Chroot.Util (stdPATH) import Utility.SafeCommand @@ -47,12 +45,17 @@ import Control.Exception (throw) -- > & os (System (Debian Unstable) "amd64") -- > & cleanInstallOnce (Confirmed "foo.example.com") -- > `onChange` propertyList "fixing up after clean install" --- > [ preserveNetworkInterfaces +-- > [ User.shadowConfig True +-- > , preserveNetworkInterfaces +-- > , preserveResolvConf -- > , preserverRootSshAuthorized --- > -- , kernelInstalled --- > -- , grubBoots "hd0" +-- > , Apt.update +-- > -- , Grub.boots "/dev/sda" +-- > -- `requires` Grub.installed Grub.PC -- > -- , oldOsRemoved (Confirmed "foo.example.com") -- > ] +-- > & Hostname.sane +-- > & Apt.installed ["linux-image-amd64"] -- > & Apt.installed ["ssh"] -- > & User.hasSomePassword "root" -- > & User.accountFor "joey" @@ -67,8 +70,6 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ `requires` propellorbootstrapped `requires` - User.shadowConfig True - `requires` flipped `requires` osbootstrapped @@ -96,8 +97,10 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ massRename (renamesout ++ renamesin) removeDirectoryRecursive newOSDir - -- Prepare environment for running additional properties. + -- Prepare environment for running additional properties, + -- overriding old OS's environment. void $ setEnv "PATH" stdPATH True + void $ unsetEnv "LANG" -- Remount /dev, so that block devices etc are -- available for other properties to use. @@ -105,6 +108,14 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ warningMessage $ "failed mounting /dev using " ++ devfstype ++ "; falling back to MAKEDEV generic" void $ boolSystem "sh" [Param "-c", Param "cd /dev && /sbin/MAKEDEV generic"] + -- Mount /sys too, needed by eg, grub-mkconfig. + unlessM (mount "sysfs" "sysfs" "/sys") $ + warningMessage "failed mounting /sys" + + -- And /dev/pts, used by apt. + unlessM (mount "devpts" "devpts" "/dev/pts") $ + warningMessage "failed mounting /dev/pts" + liftIO $ writeFile flagfile "" return MadeChange @@ -164,6 +175,16 @@ confirmed desc (Confirmed c) = property desc $ do preserveNetworkInterfaces :: Property preserveNetworkInterfaces = undefined +-- | /etc/resolv.conf is copied the from the old OS +preserveResolvConf :: Property +preserveResolvConf = check (fileExist oldloc) $ + property (newloc ++ " copied from old OS") $ do + ls <- liftIO $ lines <$> readFile oldloc + ensureProperty $ newloc `File.hasContent` ls + where + newloc = "/etc/resolv.conf" + oldloc = oldOSDir ++ newloc + -- | Root's .ssh/authorized_keys has added to it any ssh keys that -- were authorized in the old OS. Any other contents of the file are -- retained. @@ -176,19 +197,6 @@ preserveRootSshAuthorized = check (fileExist oldloc) $ newloc = "/root/.ssh/authorized_keys" oldloc = oldOSDir ++ newloc --- | Installs an appropriate kernel from the OS distribution. -kernelInstalled :: Property -kernelInstalled = undefined - --- | Installs grub onto a device to boot the system. --- --- You may want to install grub to multiple devices; eg for a system --- that uses software RAID. -grubBoots :: GrubDev -> Property -grubBoots = undefined - -type GrubDev = String - -- | Forces an immediate reboot, without contacting the init system. -- -- Can be used after cleanInstallOnce. -- cgit v1.3-2-g0d8e From 5c686afab2279921f3da3baad792dc38d5c81687 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Fri, 5 Dec 2014 15:32:23 -0400 Subject: propellor spin --- config-joey.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/config-joey.hs b/config-joey.hs index 0e46cb56..b41af4a2 100644 --- a/config-joey.hs +++ b/config-joey.hs @@ -60,6 +60,7 @@ testvm = host "testvm.kitenet.net" [ User.shadowConfig True , OS.preserveRootSshAuthorized , OS.preserveResolvConf + , Apt.update , Grub.boots "/dev/sda" `requires` Grub.installed Grub.PC ] -- cgit v1.3-2-g0d8e From bf9284d05cf29e4058ca6007486fbe261b0e0769 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Fri, 5 Dec 2014 16:22:11 -0400 Subject: I have now successfully used propellor to convert a Fedora system into a bootable and fully working Debian system --- config-joey.hs | 11 ++++- debian/changelog | 1 + src/Propellor/Property/Grub.hs | 42 +++++++++++++++- src/Propellor/Property/OS.hs | 108 ++++++++++++++++++++++------------------- 4 files changed, 110 insertions(+), 52 deletions(-) diff --git a/config-joey.hs b/config-joey.hs index a1ea21d1..b41af4a2 100644 --- a/config-joey.hs +++ b/config-joey.hs @@ -57,8 +57,17 @@ testvm = host "testvm.kitenet.net" & os (System (Debian Unstable) "amd64") & OS.cleanInstallOnce (OS.Confirmed "testvm.kitenet.net") `onChange` propertyList "fixing up after clean install" - [ OS.preserveRootSshAuthorized + [ User.shadowConfig True + , OS.preserveRootSshAuthorized + , OS.preserveResolvConf + , Apt.update + , Grub.boots "/dev/sda" + `requires` Grub.installed Grub.PC ] + & Hostname.sane + & Hostname.searchDomain + & Apt.installed ["linux-image-amd64"] + & Apt.installed ["ssh"] darkstar :: Host darkstar = host "darkstar.kitenet.net" diff --git a/debian/changelog b/debian/changelog index 079a737c..7ee1198b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -17,6 +17,7 @@ propellor (1.1.0) UNRELEASED; urgency=medium to do an in-place conversion from Fedora to Debian. Use with caution! * Added group-related properties. Thanks, Félix Sipma. * Added Git.barerepo. Thanks, Félix Sipma. + * Added Grub.installed and Grub.boots properties. * hasSomePassword and hasPassword now default to using the name of the host as the Context for the password. To specify a different context, use hasSomePassword' and hasPassword' (API change) diff --git a/src/Propellor/Property/Grub.hs b/src/Propellor/Property/Grub.hs index 841861f4..00592d0b 100644 --- a/src/Propellor/Property/Grub.hs +++ b/src/Propellor/Property/Grub.hs @@ -7,8 +7,46 @@ import qualified Propellor.Property.Apt as Apt -- | Eg, hd0,0 or xen/xvda1 type GrubDevice = String +-- | Eg, /dev/sda +type OSDevice = String + type TimeoutSecs = Int +-- | Types of machines that grub can boot. +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, so that the grub boot menu is +-- created. It will be automatically updated when kernel packages are +-- installed. +installed :: BIOS -> Property +installed bios = + Apt.installed [pkg] `describe` "grub package installed" + `before` + cmdProperty "update-grub" [] + where + pkg = case bios of + PC -> "grub-pc" + EFI64 -> "grub-efi-amd64" + EFI32 -> "grub-efi-ia32" + Coreboot -> "grub-coreboot" + Xen -> "grub-xen" + +-- | Installs grub onto a device, so the system can boot from that device. +-- +-- You may want to install grub to multiple devices; eg for a system +-- that uses software RAID. +-- +-- Note that this property does not check if grub is already installed +-- on the device; it always does the work to reinstall it. It's a good idea +-- to arrange for this property to only run once, by eg making it be run +-- onChange after OS.cleanInstallOnce. +boots :: OSDevice -> Property +boots dev = cmdProperty "grub-install" [dev] + `describe` ("grub boots " ++ dev) + -- | Use PV-grub chaining to boot -- -- Useful when the VPS's pv-grub is too old to boot a modern kernel image. @@ -31,8 +69,8 @@ chainPVGrub rootdev bootdev timeout = combineProperties desc ] , "/boot/load.cf" `File.hasContent` [ "configfile (" ++ bootdev ++ ")/boot/grub/grub.cfg" ] - , Apt.installed ["grub-xen"] - , flagFile (scriptProperty ["update-grub; grub-mkimage --prefix '(" ++ bootdev ++ ")/boot/grub' -c /boot/load.cf -O x86_64-xen /usr/lib/grub/x86_64-xen/*.mod > /boot/xen-shim"]) "/boot/xen-shim" + , installed Xen + , flagFile (scriptProperty ["grub-mkimage --prefix '(" ++ bootdev ++ ")/boot/grub' -c /boot/load.cf -O x86_64-xen /usr/lib/grub/x86_64-xen/*.mod > /boot/xen-shim"]) "/boot/xen-shim" `describe` "/boot-xen-shim" ] where diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs index 3ed23fb4..30f8c4bb 100644 --- a/src/Propellor/Property/OS.hs +++ b/src/Propellor/Property/OS.hs @@ -2,20 +2,19 @@ module Propellor.Property.OS ( cleanInstallOnce, Confirmation(..), preserveNetworkInterfaces, + preserveResolvConf, preserveRootSshAuthorized, - grubBoots, - GrubDev, rebootForced, - kernelInstalled, oldOSRemoved, ) where import Propellor import qualified Propellor.Property.Debootstrap as Debootstrap import qualified Propellor.Property.Ssh as Ssh -import qualified Propellor.Property.User as User +import qualified Propellor.Property.File as File import Propellor.Property.Mount import Propellor.Property.Chroot.Util (stdPATH) +import Utility.SafeCommand import System.Posix.Files (rename, fileExist) import Control.Exception (throw) @@ -36,20 +35,27 @@ import Control.Exception (throw) -- -- The files from the old os will be left in /old-os -- +-- TODO: A forced reboot should be schedued to run after propellor finishes +-- ensuring all properties of the host. +-- -- You will typically want to run some more properties after the clean -- install succeeds, to bootstrap from the cleanly installed system to -- a fully working system. For example: -- -- > & os (System (Debian Unstable) "amd64") --- > & cleanInstall (Confirmed "foo.example.com") +-- > & cleanInstallOnce (Confirmed "foo.example.com") -- > `onChange` propertyList "fixing up after clean install" --- > [ preserveNetworkInterfaces +-- > [ User.shadowConfig True +-- > , preserveNetworkInterfaces +-- > , preserveResolvConf -- > , preserverRootSshAuthorized --- > -- , kernelInstalled --- > -- , grubBoots "hd0" +-- > , Apt.update +-- > -- , Grub.boots "/dev/sda" +-- > -- `requires` Grub.installed Grub.PC -- > -- , oldOsRemoved (Confirmed "foo.example.com") --- > -- , rebootForced -- > ] +-- > & Hostname.sane +-- > & Apt.installed ["linux-image-amd64"] -- > & Apt.installed ["ssh"] -- > & User.hasSomePassword "root" -- > & User.accountFor "joey" @@ -64,12 +70,8 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ `requires` propellorbootstrapped `requires` - User.shadowConfig True - `requires` flipped `requires` - umountall - `requires` osbootstrapped osbootstrapped = withOS (newOSDir ++ " bootstrapped") $ \o -> case o of @@ -79,33 +81,42 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ debootstrap targetos = ensureProperty $ toProp $ Debootstrap.built newOSDir targetos Debootstrap.DefaultConfig - umountall = property "mount points unmounted" $ liftIO $ do + flipped = property (newOSDir ++ " moved into place") $ liftIO $ do + -- First, unmount most mount points, lazily, so + -- they don't interfere with moving things around. + devfstype <- fromMaybe "devtmpfs" <$> getFsType "/dev" mnts <- filter (`notElem` ("/": trickydirs)) <$> mountPoints -- reverse so that deeper mount points come first forM_ (reverse mnts) umountLazy - return $ if null mnts then NoChange else MadeChange - flipped = property (newOSDir ++ " moved into place") $ liftIO $ do - rootcontents <- dirContents "/" - newrootcontents <- dirContents newOSDir + renamesout <- map (\d -> (d, oldOSDir ++ d, pure $ d `notElem` (oldOSDir:newOSDir:trickydirs))) + <$> dirContents "/" + renamesin <- map (\d -> let dest = "/" ++ takeFileName d in (d, dest, not <$> fileExist dest)) + <$> dirContents newOSDir createDirectoryIfMissing True oldOSDir - renamesout <- forM rootcontents $ \d -> - if d `notElem` (oldOSDir:newOSDir:trickydirs) - then return $ Just (d, oldOSDir ++ d) - else return Nothing - renamesin <- forM newrootcontents $ \d -> do - let dest = "/" ++ takeFileName d - ifM (not <$> fileExist dest) - ( return $ Just (d, dest) - , return Nothing - ) - massRename $ catMaybes (renamesout ++ renamesin) + massRename (renamesout ++ renamesin) removeDirectoryRecursive newOSDir - - -- Prepare environment for running additional properties. - liftIO $ writeFile flagfile "" + + -- Prepare environment for running additional properties, + -- overriding old OS's environment. void $ setEnv "PATH" stdPATH True + void $ unsetEnv "LANG" + + -- Remount /dev, so that block devices etc are + -- available for other properties to use. + unlessM (mount devfstype devfstype "/dev") $ do + warningMessage $ "failed mounting /dev using " ++ devfstype ++ "; falling back to MAKEDEV generic" + void $ boolSystem "sh" [Param "-c", Param "cd /dev && /sbin/MAKEDEV generic"] + + -- Mount /sys too, needed by eg, grub-mkconfig. + unlessM (mount "sysfs" "sysfs" "/sys") $ + warningMessage "failed mounting /sys" + + -- And /dev/pts, used by apt. + unlessM (mount "devpts" "devpts" "/dev/pts") $ + warningMessage "failed mounting /dev/pts" + liftIO $ writeFile flagfile "" return MadeChange propellorbootstrapped = property "propellor re-debootstrapped in new os" $ @@ -133,15 +144,17 @@ cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $ -- Performs all the renames. If any rename fails, rolls back all -- previous renames. Thus, this either successfully performs all -- the renames, or does not change the system state at all. -massRename :: [(FilePath, FilePath)] -> IO () +massRename :: [(FilePath, FilePath, IO Bool)] -> IO () massRename = go [] where go _ [] = return () - go undo ((from, to):rest) = - tryNonAsync (rename from to) + go undo ((from, to, test):rest) = ifM test + ( tryNonAsync (rename from to) >>= either (rollback undo) (const $ go ((to, from):undo) rest) + , go undo rest + ) rollback undo e = do mapM_ (uncurry rename) undo throw e @@ -162,11 +175,21 @@ confirmed desc (Confirmed c) = property desc $ do preserveNetworkInterfaces :: Property preserveNetworkInterfaces = undefined +-- | /etc/resolv.conf is copied the from the old OS +preserveResolvConf :: Property +preserveResolvConf = check (fileExist oldloc) $ + property (newloc ++ " copied from old OS") $ do + ls <- liftIO $ lines <$> readFile oldloc + ensureProperty $ newloc `File.hasContent` ls + where + newloc = "/etc/resolv.conf" + oldloc = oldOSDir ++ newloc + -- | Root's .ssh/authorized_keys has added to it any ssh keys that -- were authorized in the old OS. Any other contents of the file are -- retained. preserveRootSshAuthorized :: Property -preserveRootSshAuthorized = check (doesDirectoryExist oldloc) $ +preserveRootSshAuthorized = check (fileExist oldloc) $ property (newloc ++ " copied from old OS") $ do ks <- liftIO $ lines <$> readFile oldloc ensureProperties (map (Ssh.authorizedKey "root") ks) @@ -174,19 +197,6 @@ preserveRootSshAuthorized = check (doesDirectoryExist oldloc) $ newloc = "/root/.ssh/authorized_keys" oldloc = oldOSDir ++ newloc --- | Installs an appropriate kernel from the OS distribution. -kernelInstalled :: Property -kernelInstalled = undefined - --- | Installs grub onto a device to boot the system. --- --- You may want to install grub to multiple devices; eg for a system --- that uses software RAID. -grubBoots :: GrubDev -> Property -grubBoots = undefined - -type GrubDev = String - -- | Forces an immediate reboot, without contacting the init system. -- -- Can be used after cleanInstallOnce. -- cgit v1.3-2-g0d8e