module Propellor.Property.Reboot ( now, atEnd, toDistroKernel, toKernelNewerThan, KernelVersion, ) where import Propellor.Base import Data.List import Data.Version import Text.ParserCombinators.ReadP -- | Kernel version number, in a string. type KernelVersion = String -- | Using this property causes an immediate reboot. -- -- So, this is not a useful property on its own, but it can be useful to -- compose with other properties. For example: -- -- > Apt.installed ["new-kernel"] -- > `onChange` Reboot.now now :: Property Linux now = tightenTargets $ cmdProperty "reboot" [] `assume` MadeChange `describe` "reboot now" type Force = Bool -- | Schedules a reboot at the end of the current propellor run. -- -- The `Result` code of the entire propellor run can be checked; -- the reboot proceeds only if the function returns True. -- -- The reboot can be forced to run, which bypasses the init system. Useful -- if the init system might not be running for some reason. atEnd :: Force -> (Result -> Bool) -> Property Linux atEnd force resultok = property "scheduled reboot at end of propellor run" $ do endAction "rebooting" atend return NoChange where atend r | resultok r = liftIO $ toResult <$> boolSystem "reboot" rebootparams | otherwise = do warningMessage "Not rebooting, due to status of propellor run." return FailedChange rebootparams | force = [Param "--force"] | otherwise = [] -- | Reboots immediately if a kernel other than the distro-installed kernel is -- running. -- -- This will only work if you have taken measures to ensure that the other -- kernel won't just get booted again. -- See 'Propellor.Property.HostingProvider.DigitalOcean' -- for an example of how to do this. toDistroKernel :: Property DebianLike toDistroKernel = tightenTargets $ check (not <$> runningInstalledKernel) now `describe` "running installed kernel" -- | Given a kernel version string @v@, reboots immediately if the running -- kernel version is strictly less than @v@ and there is an installed kernel -- version is greater than or equal to @v@. Fails if the requested kernel -- version is not installed. -- -- For this to be useful, you need to have ensured that the installed kernel -- with the highest version number is the one that will be started after a -- reboot. -- -- This is useful when upgrading to a new version of Debian where you need to -- ensure that a new enough kernel is running before ensuring other properties. toKernelNewerThan :: KernelVersion -> Property DebianLike toKernelNewerThan ver = property' ("reboot to kernel newer than " ++ ver) $ \w -> do wantV <- tryReadVersion ver runningV <- tryReadVersion =<< liftIO runningKernelVersion if runningV >= wantV then noChange else maximum <$> installedVs >>= \installedV -> if installedV >= wantV then ensureProperty w now else errorMessage $ "kernel newer than " ++ ver ++ " not installed" where installedVs = mapM tryReadVersion =<< liftIO installedKernelVersions runningInstalledKernel :: IO Bool runningInstalledKernel = do kernelver <- runningKernelVersion when (null kernelver) $ error "failed to read uname -r" kernelimages <- installedKernelImages when (null kernelimages) $ error "failed to find any installed kernel images" findVersion kernelver <$> readProcess "file" ("-L" : kernelimages) runningKernelVersion :: IO KernelVersion runningKernelVersion = takeWhile (/= '\n') <$> readProcess "uname" ["-r"] installedKernelImages :: IO [String] installedKernelImages = concat <$> mapM kernelsIn ["/", "/boot/"] -- | File output looks something like this, we want to unambiguously -- match the running kernel version: -- Linux kernel x86 boot executable bzImage, version 3.16-3-amd64 (debian-kernel@lists.debian.org) #1 SMP Debian 3.1, RO-rootFS, swap_dev 0x2, Normal VGA findVersion :: KernelVersion -> String -> Bool findVersion ver s = (" version " ++ ver ++ " ") `isInfixOf` s installedKernelVersions :: IO [KernelVersion] installedKernelVersions = do kernelimages <- installedKernelImages when (null kernelimages) $ error "failed to find any installed kernel images" imageLines <- lines <$> readProcess "file" ("-L" : kernelimages) return $ extractKernelVersion <$> imageLines kernelsIn :: FilePath -> IO [FilePath] kernelsIn d = filter ("vmlinu" `isInfixOf`) <$> dirContents d extractKernelVersion :: String -> KernelVersion extractKernelVersion = unwords . take 1 . drop 1 . dropWhile (/= "version") . words readVersionMaybe :: KernelVersion -> Maybe Version readVersionMaybe ver = case map fst $ readP_to_S parseVersion ver of [] -> Nothing l -> Just $ maximum l tryReadVersion :: KernelVersion -> Propellor Version tryReadVersion ver = case readVersionMaybe ver of Just x -> return x Nothing -> errorMessage ("couldn't parse version " ++ ver)