{-# LANGUAGE TypeOperators, RankNTypes, TypeFamilies, FlexibleContexts #-}
module Propellor.Property.Installer.Target (
TargetPartTable(..),
targetInstalled,
fstabLists,
mountTarget,
targetBootable,
partitionTargetDisk,
targetDir,
probeDisk,
findDiskDevices,
TargetFilled,
TargetFilledHandle,
prepTargetFilled,
checkTargetFilled,
TargetFilledPercent(..),
targetFilledPercent,
) where
import Propellor
import Propellor.Property.Installer.Types
import Propellor.Message
import Propellor.Types.Bootloader
import Propellor.Types.PartSpec
import Propellor.Property.Chroot
import Propellor.Property.Versioned
import Propellor.Property.Parted
import Propellor.Property.Mount
import qualified Propellor.Property.Fstab as Fstab
import qualified Propellor.Property.Grub as Grub
import qualified Propellor.Property.Rsync as Rsync
import Text.Read
import Control.Monad
import Control.Monad.IO.Class (liftIO)
import System.Directory
import System.FilePath
import Data.Maybe
import Data.List
import Data.Char
import Data.Ord
import Data.Ratio
import qualified Data.Semigroup as Sem
import System.Process (readProcess)
data TargetPartTable = TargetPartTable TableType [PartSpec DiskPart]
targetInstalled
:: UserInput i
=> Versioned v Host
-> v
-> i
-> TargetPartTable
-> RevertableProperty (HasInfo + DebianLike) (HasInfo + DebianLike)
targetInstalled vtargethost v userinput (TargetPartTable tabletype partspec) =
case (targetDiskDevice userinput, diskEraseConfirmed userinput) of
(Just (TargetDiskDevice targetdev), Just _diskeraseconfirmed) ->
go `describe` ("target system installed to " ++ targetdev)
_ -> tightenTargets installdeps <!> doNothing
where
targethost = vtargethost `version` v
go = RevertableProperty
(setupRevertableProperty p)
(undoRevertableProperty p `setInfoProperty` mempty)
where
p = partitionTargetDisk userinput tabletype partspec
`before` mountTarget userinput partspec
`before` provisioned chroot
chroot = hostChroot targethost RsyncBootstrapper targetDir
installdeps = Rsync.installed
data RsyncBootstrapper = RsyncBootstrapper
instance ChrootBootstrapper RsyncBootstrapper where
buildchroot RsyncBootstrapper _ target = Right $
mountaside
`before` rsynced
`before` umountaside
where
mountaside = bindMount "/" "/mnt"
rsynced = Rsync.rsync
[ "--one-file-system"
, "-aHAXS"
, "--delete"
, "/mnt/"
, target
]
umountaside = cmdProperty "umount" ["-l", "/mnt"]
`assume` MadeChange
mountTarget
:: UserInput i
=> i
-> [PartSpec DiskPart]
-> RevertableProperty Linux Linux
mountTarget userinput partspec = setup <!> cleanup
where
setup = property "target mounted" $
case targetDiskDevice userinput of
Just (TargetDiskDevice targetdev) -> do
liftIO unmountTarget
r <- liftIO $ forM tomount $
mountone targetdev
if and r
then return MadeChange
else return FailedChange
Nothing -> return NoChange
cleanup = property "target unmounted" $ do
liftIO unmountTarget
liftIO $ removeDirectoryRecursive targetDir
return NoChange
tomount = sortOn (fst . fst) $
map (\((mp, mo, _, _), n) -> ((mp, mo), n)) $
zip partspec partNums
mountone targetdev ((mmountpoint, mountopts), num) =
case mmountpoint of
Nothing -> return True
Just mountpoint -> do
let targetmount = targetDir ++ mountpoint
createDirectoryIfMissing True targetmount
let dev = diskPartition targetdev num
mount "auto" dev targetmount mountopts
fstabLists
:: UserInput i
=> i
-> TargetPartTable
-> RevertableProperty Linux Linux
fstabLists userinput (TargetPartTable _ partspecs) = setup <!> doNothing
where
setup = case targetDiskDevice userinput of
Just (TargetDiskDevice targetdev) ->
Fstab.fstabbed mnts (swaps targetdev)
`requires` devmounted
`before` devumounted
Nothing -> doNothing
devmounted :: Property Linux
devmounted = tightenTargets $ mounted "devtmpfs" "udev" "/dev" mempty
devumounted :: Property Linux
devumounted = tightenTargets $ cmdProperty "umount" ["-l", "/dev"]
`assume` MadeChange
partitions = map (\(mp, _, mkpart, _) -> (mp, mkpart mempty)) partspecs
mnts = mapMaybe fst $
filter (\(_, p) -> partFs p /= Just LinuxSwap && partFs p /= Nothing) partitions
swaps targetdev =
map (Fstab.SwapPartition . diskPartition targetdev . snd) $
filter (\((_, p), _) -> partFs p == Just LinuxSwap)
(zip partitions partNums)
targetBootable
:: UserInput i
=> i
-> RevertableProperty Linux Linux
targetBootable userinput =
case (targetDiskDevice userinput, diskEraseConfirmed userinput) of
(Just (TargetDiskDevice targetdev), Just _diskeraseconfirmed) ->
go targetdev <!> doNothing
_ -> doNothing <!> doNothing
where
desc = "bootloader installed on target disk"
go :: FilePath -> Property Linux
go targetdev = property' desc $ \w -> do
bootloaders <- askInfo
case bootloaders of
[GrubInstalled gt] -> ensureProperty w $
Grub.bootsMounted targetDir targetdev gt
[] -> do
warningMessage "no bootloader was installed"
return NoChange
l -> do
warningMessage $ "don't know how to enable bootloader(s) " ++ show l
return FailedChange
partitionTargetDisk
:: UserInput i
=> i
-> TableType
-> [PartSpec DiskPart]
-> RevertableProperty DebianLike DebianLike
partitionTargetDisk userinput tabletype partspec = go <!> doNothing
where
go = check targetNotMounted $ property' "target disk partitioned" $ \w -> do
case (targetDiskDevice userinput, diskEraseConfirmed userinput) of
(Just (TargetDiskDevice targetdev), Just _diskeraseconfirmed) -> do
liftIO $ unmountTarget
disksize <- liftIO $ getDiskSize targetdev
let parttable = calcPartTable disksize tabletype safeAlignment partspec
ensureProperty w $
partitioned YesReallyDeleteDiskContents targetdev parttable
_ -> error "user input does not allow partitioning disk"
unmountTarget :: IO ()
unmountTarget = mapM_ umountLazy . reverse . sort =<< targetMountPoints
targetMountPoints :: IO [MountPoint]
targetMountPoints = filter isTargetMountPoint <$> mountPoints
isTargetMountPoint :: MountPoint -> Bool
isTargetMountPoint mp =
mp == targetDir
|| addTrailingPathSeparator targetDir `isPrefixOf` mp
targetNotMounted :: IO Bool
targetNotMounted = not . any (== targetDir) <$> mountPoints
targetDir :: FilePath
targetDir = "/target"
partNums :: [Integer]
partNums = [1..]
diskPartition :: FilePath -> Integer -> FilePath
diskPartition dev num = dev ++ show num
probeDisk :: IO TargetDiskDevice
probeDisk = do
unmountTarget
mounteddevs <- getMountedDeviceIDs
let notmounted d = flip notElem (map Just mounteddevs)
<$> getMinorNumber d
candidates <- mapM probeCandidate
=<< filterM notmounted
=<< findDiskDevices
case reverse (sort candidates) of
(Candidate { candidateDevice = Down dev } : _) ->
return $ TargetDiskDevice dev
[] -> error "Unable to find any disk to install to!"
findDiskDevices :: IO [FilePath]
findDiskDevices = map ("/dev" </>) . filter isdisk
<$> getDirectoryContents "/dev"
where
isdisk ('s':'d':_:[]) = True
isdisk _ = False
data Candidate = Candidate
{ candidateBigEnoughForOS :: Bool
, candidateIsFixedDisk :: Bool
, candidateDevice :: Down FilePath
} deriving (Eq, Ord)
probeCandidate :: FilePath -> IO Candidate
probeCandidate dev = do
DiskSize sz <- getDiskSize dev
isfixeddisk <- not <$> isRemovableDisk dev
return $ Candidate
{ candidateBigEnoughForOS = sz >= 8 * onegb
, candidateIsFixedDisk = isfixeddisk
, candidateDevice = Down dev
}
where
onegb = 1024*1024*1000
newtype MinorNumber = MinorNumber Integer
deriving (Eq, Show)
getMountedDeviceIDs :: IO [MinorNumber]
getMountedDeviceIDs = mapMaybe parse . lines <$> readProcess "findmnt"
[ "-rn"
, "--output"
, "MAJ:MIN"
]
""
where
parse = fmap MinorNumber . readMaybe
. dropWhile (not . isDigit) . dropWhile (/= ':')
getMinorNumber :: FilePath -> IO (Maybe MinorNumber)
getMinorNumber dev = fmap MinorNumber . readMaybe
<$> readProcess "stat" [ "--printf", "%T", dev ] ""
isRemovableDisk :: FilePath -> IO Bool
isRemovableDisk dev = do
isremovable <- checkblk "RM"
ishotplug <- checkblk "HOTPLUG"
return (isremovable || ishotplug)
where
checkblk field = (== "1\n") <$> readProcess "lsblk"
[ "-rn"
, "--nodeps"
, "--output", field
, dev
]
""
getDiskSize :: FilePath -> IO DiskSize
getDiskSize dev = do
sectors <- fromMaybe 0 . readMaybe
<$> readProcess "blockdev" ["--getsz", dev] ""
return (DiskSize (sectors * 512))
getMountsSizes :: IO [(MountPoint, Integer)]
getMountsSizes = mapMaybe (parse . words) . lines <$> readProcess "findmnt" ps ""
where
ps = ["-rnb", "-o", "TARGET,USED"]
parse (mp:szs:[]) = do
sz <- readMaybe szs
return (mp, sz)
parse _ = Nothing
data TargetFilled = TargetFilled (Ratio Integer)
deriving (Show, Eq)
instance Sem.Semigroup TargetFilled where
TargetFilled n <> TargetFilled m = TargetFilled (n+m)
instance Monoid TargetFilled where
mempty = TargetFilled (0 % 1)
mappend = (Sem.<>)
newtype TargetFilledHandle = TargetFilledHandle Integer
prepTargetFilled :: IO TargetFilledHandle
prepTargetFilled = go =<< getMountSource "/"
where
go (Just dev) = do
DiskSize sz <- getDiskSize dev
return (TargetFilledHandle sz)
go Nothing = return (TargetFilledHandle 0)
checkTargetFilled :: TargetFilledHandle -> IO TargetFilled
checkTargetFilled (TargetFilledHandle installsz) = do
targetsz <- sum . map snd . filter (isTargetMountPoint . fst)
<$> getMountsSizes
return (TargetFilled (targetsz % max 1 installsz))
newtype TargetFilledPercent = TargetFilledPercent Int
deriving (Show, Eq)
targetFilledPercent :: TargetFilled -> TargetFilledPercent
targetFilledPercent (TargetFilled r) = TargetFilledPercent $ floor percent
where
percent :: Double
percent = min 100 (fromRational r * 100)